diff --git a/.coveragerc b/.coveragerc index 43de8df4088aeb..fb7e3edd62c284 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,9 +14,15 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/android_ip_webcam.py + homeassistant/components/*/android_ip_webcam.py + homeassistant/components/bbb_gpio.py homeassistant/components/*/bbb_gpio.py + homeassistant/components/blink.py + homeassistant/components/*/blink.py + homeassistant/components/bloomsky.py homeassistant/components/*/bloomsky.py @@ -85,6 +91,10 @@ omit = homeassistant/components/*/thinkingcleaner.py + homeassistant/components/twilio.py + homeassistant/components/notify/twilio_sms.py + homeassistant/components/notify/twilio_call.py + homeassistant/components/vera.py homeassistant/components/*/vera.py @@ -132,6 +142,9 @@ omit = homeassistant/components/zabbix.py homeassistant/components/*/zabbix.py + homeassistant/components/maxcube.py + homeassistant/components/*/maxcube.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py @@ -231,6 +244,7 @@ omit = homeassistant/components/media_player/dunehd.py homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py + homeassistant/components/media_player/frontier_silicon.py homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gstreamer.py homeassistant/components/media_player/hdmi_cec.py @@ -259,6 +273,7 @@ omit = homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py + homeassistant/components/notify/ciscospark.py homeassistant/components/notify/discord.py homeassistant/components/notify/facebook.py homeassistant/components/notify/free_mobile.py @@ -286,8 +301,6 @@ omit = homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py homeassistant/components/notify/telstra.py - homeassistant/components/notify/twilio_sms.py - homeassistant/components/notify/twilio_call.py homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py homeassistant/components/nuimo_controller.py @@ -303,12 +316,14 @@ omit = homeassistant/components/sensor/broadlink.py homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/coinmarketcap.py + homeassistant/components/sensor/comed_hourly_pricing.py homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cups.py homeassistant/components/sensor/currencylayer.py homeassistant/components/sensor/darksky.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py + homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dovado.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/ebox.py @@ -333,11 +348,12 @@ omit = homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/influxdb.py + homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py - homeassistant/components/sensor/mhz19.py homeassistant/components/sensor/miflora.py + homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/mqtt_room.py homeassistant/components/sensor/netdata.py homeassistant/components/sensor/neurio_energy.py @@ -411,6 +427,7 @@ omit = homeassistant/components/upnp.py homeassistant/components/weather/bom.py homeassistant/components/weather/openweathermap.py + homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6131662dc5fee3..dd030c73d1aeb6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,16 +1,16 @@ -**Description:** +## Description: **Related issue (if applicable):** fixes # **Pull request in [home-assistant.github.io](https://github.com/home-assistant/home-assistant.github.io) with documentation (if applicable):** home-assistant/home-assistant.github.io# -**Example entry for `configuration.yaml` (if applicable):** +## Example entry for `configuration.yaml` (if applicable): ```yaml ``` -**Checklist:** +## Checklist: 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) diff --git a/.travis.yml b/.travis.yml index 2de101af24bbb9..864699a2fbdc9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,8 @@ matrix: env: TOXENV=py35 - python: "3.6" env: TOXENV=py36 + - python: "3.6-dev" + env: TOXENV=py36 # allow_failures: # - python: "3.5" # env: TOXENV=typing diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c6709aea7cccf6..2cca8e1495b41c 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -4,320 +4,31 @@ import logging.handlers import os import sys +from time import time from collections import OrderedDict -from types import ModuleType from typing import Any, Optional, Dict import voluptuous as vol -from voluptuous.humanize import humanize_error import homeassistant.components as core_components from homeassistant.components import persistent_notification import homeassistant.config as conf_util import homeassistant.core as core from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.setup import async_setup_component import homeassistant.loader as loader -import homeassistant.util.package as pkg_util -from homeassistant.util.async import ( - run_coroutine_threadsafe, run_callback_threadsafe) from homeassistant.util.logging import AsyncHandler from homeassistant.util.yaml import clear_secret_cache -from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import ( - event_decorators, service, config_per_platform, extract_domain_configs) +from homeassistant.helpers import event_decorators, service from homeassistant.helpers.signal import async_register_signal_handling _LOGGER = logging.getLogger(__name__) -ATTR_COMPONENT = 'component' - ERROR_LOG_FILENAME = 'home-assistant.log' -_PERSISTENT_ERRORS = {} -HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)' - - -def setup_component(hass: core.HomeAssistant, domain: str, - config: Optional[Dict]=None) -> bool: - """Setup a component and all its dependencies.""" - return run_coroutine_threadsafe( - async_setup_component(hass, domain, config), loop=hass.loop).result() - - -@asyncio.coroutine -def async_setup_component(hass: core.HomeAssistant, domain: str, - config: Optional[Dict]=None) -> bool: - """Setup a component and all its dependencies. - - This method is a coroutine. - """ - if domain in hass.config.components: - _LOGGER.debug('Component %s already set up.', domain) - return True - - if not loader.PREPARED: - yield from hass.loop.run_in_executor(None, loader.prepare, hass) - - if config is None: - config = {} - - components = loader.load_order_component(domain) - - # OrderedSet is empty if component or dependencies could not be resolved - if not components: - _async_persistent_notification(hass, domain, True) - return False - - for component in components: - res = yield from _async_setup_component(hass, component, config) - if not res: - _LOGGER.error('Component %s failed to setup', component) - _async_persistent_notification(hass, component, True) - return False - - return True - - -def _handle_requirements(hass: core.HomeAssistant, component, - name: str) -> bool: - """Install the requirements for a component. - - This method needs to run in an executor. - """ - if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'): - return True - - for req in component.REQUIREMENTS: - if not pkg_util.install_package(req, target=hass.config.path('deps')): - _LOGGER.error('Not initializing %s because could not install ' - 'dependency %s', name, req) - _async_persistent_notification(hass, name) - return False - - return True - - -@asyncio.coroutine -def _async_setup_component(hass: core.HomeAssistant, - domain: str, config) -> bool: - """Setup a component for Home Assistant. - - This method is a coroutine. - """ - # pylint: disable=too-many-return-statements - if domain in hass.config.components: - return True - - setup_lock = hass.data.get('setup_lock') - if setup_lock is None: - setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop) - - setup_progress = hass.data.get('setup_progress') - if setup_progress is None: - setup_progress = hass.data['setup_progress'] = [] - - if domain in setup_progress: - _LOGGER.error('Attempt made to setup %s during setup of %s', - domain, domain) - _async_persistent_notification(hass, domain, True) - return False - - try: - # Used to indicate to discovery that a setup is ongoing and allow it - # to wait till it is done. - did_lock = False - if not setup_lock.locked(): - yield from setup_lock.acquire() - did_lock = True - - setup_progress.append(domain) - config = yield from async_prepare_setup_component(hass, config, domain) - - if config is None: - return False - - component = loader.get_component(domain) - if component is None: - _async_persistent_notification(hass, domain) - return False - - async_comp = hasattr(component, 'async_setup') - - try: - _LOGGER.info("Setting up %s", domain) - if async_comp: - result = yield from component.async_setup(hass, config) - else: - result = yield from hass.loop.run_in_executor( - None, component.setup, hass, config) - except Exception: # pylint: disable=broad-except - _LOGGER.exception('Error during setup of component %s', domain) - _async_persistent_notification(hass, domain, True) - return False - - if result is False: - _LOGGER.error('component %s failed to initialize', domain) - _async_persistent_notification(hass, domain, True) - return False - elif result is not True: - _LOGGER.error('component %s did not return boolean if setup ' - 'was successful. Disabling component.', domain) - _async_persistent_notification(hass, domain, True) - loader.set_component(domain, None) - return False - - hass.config.components.add(component.DOMAIN) - - hass.bus.async_fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} - ) - - return True - finally: - setup_progress.remove(domain) - if did_lock: - setup_lock.release() - - -def prepare_setup_component(hass: core.HomeAssistant, config: dict, - domain: str): - """Prepare setup of a component and return processed config.""" - return run_coroutine_threadsafe( - async_prepare_setup_component(hass, config, domain), loop=hass.loop - ).result() - - -@asyncio.coroutine -def async_prepare_setup_component(hass: core.HomeAssistant, config: dict, - domain: str): - """Prepare setup of a component and return processed config. - - This method is a coroutine. - """ - # pylint: disable=too-many-return-statements - component = loader.get_component(domain) - missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) - if dep not in hass.config.components] - - if missing_deps: - _LOGGER.error( - 'Not initializing %s because not all dependencies loaded: %s', - domain, ", ".join(missing_deps)) - return None - - if hasattr(component, 'CONFIG_SCHEMA'): - try: - config = component.CONFIG_SCHEMA(config) - except vol.Invalid as ex: - async_log_exception(ex, domain, config, hass) - return None - - elif hasattr(component, 'PLATFORM_SCHEMA'): - 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: - async_log_exception(ex, domain, config, hass) - 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 = yield from async_prepare_setup_platform( - hass, config, domain, p_name) - - if platform is None: - continue - - # Validate platform specific schema - if hasattr(platform, 'PLATFORM_SCHEMA'): - try: - # pylint: disable=no-member - p_validated = platform.PLATFORM_SCHEMA(p_validated) - except vol.Invalid as ex: - async_log_exception(ex, '{}.{}'.format(domain, p_name), - p_validated, hass) - continue - - platforms.append(p_validated) - - # Create a copy of the configuration with all config for current - # component removed and add validated config back in. - filter_keys = extract_domain_configs(config, domain) - config = {key: value for key, value in config.items() - if key not in filter_keys} - config[domain] = platforms - - res = yield from hass.loop.run_in_executor( - None, _handle_requirements, hass, component, domain) - if not res: - return None - - return config - - -def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, - platform_name: str) -> Optional[ModuleType]: - """Load a platform and makes sure dependencies are setup.""" - return run_coroutine_threadsafe( - async_prepare_setup_platform(hass, config, domain, platform_name), - loop=hass.loop - ).result() - - -@asyncio.coroutine -def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, - platform_name: str) \ - -> Optional[ModuleType]: - """Load a platform and makes sure dependencies are setup. - - This method is a coroutine. - """ - if not loader.PREPARED: - yield from hass.loop.run_in_executor(None, loader.prepare, hass) - - platform_path = PLATFORM_FORMAT.format(domain, platform_name) - - platform = loader.get_platform(domain, platform_name) - - # Not found - if platform is None: - _LOGGER.error('Unable to find platform %s', platform_path) - _async_persistent_notification(hass, platform_path) - return None - - # Already loaded - elif platform_path in hass.config.components: - return platform - - # Load dependencies - for component in getattr(platform, 'DEPENDENCIES', []): - if component in loader.DEPENDENCY_BLACKLIST: - raise HomeAssistantError( - '{} is not allowed to be a dependency.'.format(component)) - - res = yield from async_setup_component(hass, component, config) - if not res: - _LOGGER.error( - 'Unable to prepare setup for platform %s because ' - 'dependency %s could not be initialized', platform_path, - component) - _async_persistent_notification(hass, platform_path, True) - return None - - res = yield from hass.loop.run_in_executor( - None, _handle_requirements, hass, platform, platform_path) - if not res: - return None - - return platform +FIRST_INIT_COMPONENT = set(( + 'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction')) def from_config_dict(config: Dict[str, Any], @@ -339,23 +50,14 @@ def from_config_dict(config: Dict[str, Any], hass.config.config_dir = config_dir mount_local_lib_path(config_dir) - @asyncio.coroutine - def _async_init_from_config_dict(future): - try: - re_hass = yield from async_from_config_dict( - config, hass, config_dir, enable_log, verbose, skip_pip, - log_rotate_days) - future.set_result(re_hass) - # pylint: disable=broad-except - except Exception as exc: - future.set_exception(exc) - # run task - future = asyncio.Future(loop=hass.loop) - hass.async_add_job(_async_init_from_config_dict(future)) - hass.loop.run_until_complete(future) + hass = hass.loop.run_until_complete( + async_from_config_dict( + config, hass, config_dir, enable_log, verbose, skip_pip, + log_rotate_days) + ) - return future.result() + return hass @asyncio.coroutine @@ -372,19 +74,15 @@ def async_from_config_dict(config: Dict[str, Any], Dynamically loads required components and its dependencies. This method is a coroutine. """ + start = time() hass.async_track_tasks() - setup_lock = hass.data.get('setup_lock') - if setup_lock is None: - setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop) - - yield from setup_lock.acquire() core_config = config.get(core.DOMAIN, {}) try: yield from conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as ex: - async_log_exception(ex, 'homeassistant', core_config, hass) + conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) return None yield from hass.loop.run_in_executor( @@ -433,20 +131,25 @@ def async_from_config_dict(config: Dict[str, Any], event_decorators.HASS = hass service.HASS = hass - # Setup the components - dependency_blacklist = loader.DEPENDENCY_BLACKLIST - set(components) - - for domain in loader.load_order_components(components): - if domain in dependency_blacklist: - raise HomeAssistantError( - '{} is not allowed to be a dependency'.format(domain)) + # stage 1 + for component in components: + if component not in FIRST_INIT_COMPONENT: + continue + hass.async_add_job(async_setup_component(hass, component, config)) - yield from _async_setup_component(hass, domain, config) + yield from hass.async_block_till_done() - setup_lock.release() + # stage 2 + for component in components: + if component in FIRST_INIT_COMPONENT: + continue + hass.async_add_job(async_setup_component(hass, component, config)) yield from hass.async_stop_track_tasks() + stop = time() + _LOGGER.info('Home Assistant initialized in %ss', round(stop-start, 2)) + async_register_signal_handling(hass) return hass @@ -464,22 +167,13 @@ def from_config_file(config_path: str, if hass is None: hass = core.HomeAssistant() - @asyncio.coroutine - def _async_init_from_config_file(future): - try: - re_hass = yield from async_from_config_file( - config_path, hass, verbose, skip_pip, log_rotate_days) - future.set_result(re_hass) - # pylint: disable=broad-except - except Exception as exc: - future.set_exception(exc) - # run task - future = asyncio.Future(loop=hass.loop) - hass.loop.create_task(_async_init_from_config_file(future)) - hass.loop.run_until_complete(future) + hass = hass.loop.run_until_complete( + async_from_config_file( + config_path, hass, verbose, skip_pip, log_rotate_days) + ) - return future.result() + return hass @asyncio.coroutine @@ -504,7 +198,8 @@ def async_from_config_file(config_path: str, try: config_dict = yield from hass.loop.run_in_executor( None, conf_util.load_yaml_config_file, config_path) - except HomeAssistantError: + except HomeAssistantError as err: + _LOGGER.error('Error loading %s: %s', config_path, err) return None finally: clear_secret_cache() @@ -588,57 +283,6 @@ def async_stop_async_handler(event): 'Unable to setup error log %s (access denied)', err_log_path) -def log_exception(ex, domain, config, hass): - """Generate log exception for config validation.""" - run_callback_threadsafe( - hass.loop, async_log_exception, ex, domain, config, hass).result() - - -@core.callback -def _async_persistent_notification(hass: core.HomeAssistant, component: str, - link: Optional[bool]=False): - """Print a persistent notification. - - This method must be run in the event loop. - """ - _PERSISTENT_ERRORS[component] = _PERSISTENT_ERRORS.get(component) or link - _lst = [HA_COMPONENT_URL.format(name.replace('_', '-'), name) - if link else name for name, link in _PERSISTENT_ERRORS.items()] - message = ('The following components and platforms could not be set up:\n' - '* ' + '\n* '.join(list(_lst)) + '\nPlease check your config') - persistent_notification.async_create( - hass, message, 'Invalid config', 'invalid_config') - - -@core.callback -def async_log_exception(ex, domain, config, hass): - """Generate log exception for config validation. - - This method must be run in the event loop. - """ - message = 'Invalid config for [{}]: '.format(domain) - if hass is not None: - _async_persistent_notification(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, - '->'.join(str(m) for m in ex.path)) - else: - message += '{}.'.format(humanize_error(config, ex)) - - domain_config = config.get(domain, config) - message += " (See {}, line {}). ".format( - getattr(domain_config, '__config_file__', '?'), - getattr(domain_config, '__line__', '?')) - - if domain != 'homeassistant': - message += ('Please check the docs at ' - 'https://home-assistant.io/components/{}/'.format(domain)) - - _LOGGER.error(message) - - def mount_local_lib_path(config_dir: str) -> str: """Add local library to Python Path. diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index cd5bddbad49a45..25f9257f3937ee 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -55,7 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): ) devices.append(device) - yield from async_add_devices(devices) + async_add_devices(devices) @callback def alarm_keypress_handler(service): @@ -94,10 +94,13 @@ def __init__(self, hass, partition_number, alarm_name, code, panic_type, _LOGGER.debug("Setting up alarm: %s", alarm_name) super().__init__(alarm_name, info, controller) + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" async_dispatcher_connect( - hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) + self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback) async_dispatcher_connect( - hass, SIGNAL_PARTITION_UPDATE, self._update_callback) + self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback) @callback def _update_callback(self, partition): diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 455f60319c63d3..b22f50b6575514 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -46,7 +46,7 @@ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the MQTT platform.""" - yield from async_add_devices([MqttAlarm( + async_add_devices([MqttAlarm( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py index 40c91784a42e10..24c14e7c9a8fc3 100644 --- a/homeassistant/components/alert.py +++ b/homeassistant/components/alert.py @@ -18,7 +18,6 @@ SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers import service, event -from homeassistant.util.async import run_callback_threadsafe import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -62,8 +61,7 @@ def is_on(hass, entity_id): def turn_on(hass, entity_id): """Reset the alert.""" - run_callback_threadsafe( - hass.loop, async_turn_on, hass, entity_id).result() + hass.add_job(async_turn_on, hass, entity_id) @callback @@ -76,8 +74,7 @@ def async_turn_on(hass, entity_id): def turn_off(hass, entity_id): """Acknowledge alert.""" - run_callback_threadsafe( - hass.loop, async_turn_off, hass, entity_id).result() + hass.add_job(async_turn_off, hass, entity_id) @callback @@ -90,7 +87,7 @@ def async_turn_off(hass, entity_id): def toggle(hass, entity_id): """Toggle acknowledgement of alert.""" - run_callback_threadsafe(hass.loop, async_toggle, hass, entity_id) + hass.add_job(async_toggle, hass, entity_id) @callback diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py new file mode 100644 index 00000000000000..e6810a67029868 --- /dev/null +++ b/homeassistant/components/android_ip_webcam.py @@ -0,0 +1,303 @@ +""" +Support for IP Webcam, an Android app that acts as a full-featured webcam. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/android_ip_webcam/ +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, + CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL, + CONF_PLATFORM) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, async_dispatcher_connect) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow +from homeassistant.components.camera.mjpeg import ( + CONF_MJPEG_URL, CONF_STILL_IMAGE_URL) + +DOMAIN = 'android_ip_webcam' +REQUIREMENTS = ["pydroid-ipcam==0.4"] + +_LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=10) + +DATA_IP_WEBCAM = 'android_ip_webcam' + +ATTR_HOST = 'host' +ATTR_VID_CONNS = 'Video Connections' +ATTR_AUD_CONNS = 'Audio Connections' + +KEY_MAP = { + 'audio_connections': 'Audio Connections', + 'adet_limit': 'Audio Trigger Limit', + 'antibanding': 'Anti-banding', + 'audio_only': 'Audio Only', + 'battery_level': 'Battery Level', + 'battery_temp': 'Battery Temperature', + 'battery_voltage': 'Battery Voltage', + 'coloreffect': 'Color Effect', + 'exposure': 'Exposure Level', + 'exposure_lock': 'Exposure Lock', + 'ffc': 'Front-facing Camera', + 'flashmode': 'Flash Mode', + 'focus': 'Focus', + 'focus_homing': 'Focus Homing', + 'focus_region': 'Focus Region', + 'focusmode': 'Focus Mode', + 'gps_active': 'GPS Active', + 'idle': 'Idle', + 'ip_address': 'IPv4 Address', + 'ipv6_address': 'IPv6 Address', + 'ivideon_streaming': 'Ivideon Streaming', + 'light': 'Light Level', + 'mirror_flip': 'Mirror Flip', + 'motion': 'Motion', + 'motion_active': 'Motion Active', + 'motion_detect': 'Motion Detection', + 'motion_event': 'Motion Event', + 'motion_limit': 'Motion Limit', + 'night_vision': 'Night Vision', + 'night_vision_average': 'Night Vision Average', + 'night_vision_gain': 'Night Vision Gain', + 'orientation': 'Orientation', + 'overlay': 'Overlay', + 'photo_size': 'Photo Size', + 'pressure': 'Pressure', + 'proximity': 'Proximity', + 'quality': 'Quality', + 'scenemode': 'Scene Mode', + 'sound': 'Sound', + 'sound_event': 'Sound Event', + 'sound_timeout': 'Sound Timeout', + 'torch': 'Torch', + 'video_connections': 'Video Connections', + 'video_chunk_len': 'Video Chunk Length', + 'video_recording': 'Video Recording', + 'video_size': 'Video Size', + 'whitebalance': 'White Balance', + 'whitebalance_lock': 'White Balance Lock', + 'zoom': 'Zoom' +} + +ICON_MAP = { + 'audio_connections': 'mdi:speaker', + 'battery_level': 'mdi:battery', + 'battery_temp': 'mdi:thermometer', + 'battery_voltage': 'mdi:battery-charging-100', + 'exposure_lock': 'mdi:camera', + 'ffc': 'mdi:camera-front-variant', + 'focus': 'mdi:image-filter-center-focus', + 'gps_active': 'mdi:crosshairs-gps', + 'light': 'mdi:flashlight', + 'motion': 'mdi:run', + 'night_vision': 'mdi:weather-night', + 'overlay': 'mdi:monitor', + 'pressure': 'mdi:gauge', + 'proximity': 'mdi:map-marker-radius', + 'quality': 'mdi:quality-high', + 'sound': 'mdi:speaker', + 'sound_event': 'mdi:speaker', + 'sound_timeout': 'mdi:speaker', + 'torch': 'mdi:white-balance-sunny', + 'video_chunk_len': 'mdi:video', + 'video_connections': 'mdi:eye', + 'video_recording': 'mdi:record-rec', + 'whitebalance_lock': 'mdi:white-balance-auto' +} + +SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision', + 'overlay', 'torch', 'whitebalance_lock', 'video_recording'] + +SENSORS = ['audio_connections', 'battery_level', 'battery_temp', + 'battery_voltage', 'light', 'motion', 'pressure', 'proximity', + 'sound', 'video_connections'] + +SIGNAL_UPDATE_DATA = 'android_ip_webcam_update' + +CONF_AUTO_DISCOVERY = 'auto_discovery' +CONF_MOTION_SENSOR = 'motion_sensor' + +DEFAULT_AUTO_DISCOVERY = True +DEFAULT_MOTION_SENSOR = False +DEFAULT_NAME = 'IP Webcam' +DEFAULT_PORT = 8080 +DEFAULT_TIMEOUT = 10 + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + vol.Optional(CONF_AUTO_DISCOVERY, default=DEFAULT_AUTO_DISCOVERY): + cv.boolean, + vol.Optional(CONF_SWITCHES, default=[]): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_SENSORS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_MOTION_SENSOR, default=DEFAULT_MOTION_SENSOR): + cv.boolean, + })]) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Setup the IP Webcam component.""" + from pydroid_ipcam import PyDroidIPCam + + webcams = hass.data[DATA_IP_WEBCAM] = {} + websession = async_get_clientsession(hass) + + @asyncio.coroutine + def async_setup_ipcamera(cam_config): + """Setup a ip camera.""" + host = cam_config[CONF_HOST] + username = cam_config.get(CONF_USERNAME) + password = cam_config.get(CONF_PASSWORD) + name = cam_config[CONF_NAME] + interval = cam_config[CONF_SCAN_INTERVAL] + switches = cam_config[CONF_SWITCHES] + sensors = cam_config[CONF_SENSORS] + motion = cam_config[CONF_MOTION_SENSOR] + + # init ip webcam + cam = PyDroidIPCam( + hass.loop, websession, host, cam_config[CONF_PORT], + username=username, password=password, + timeout=cam_config[CONF_TIMEOUT] + ) + + @asyncio.coroutine + def async_update_data(now): + """Update data from ipcam in SCAN_INTERVAL.""" + yield from cam.update() + async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) + + async_track_point_in_utc_time( + hass, async_update_data, utcnow() + interval) + + yield from async_update_data(None) + + # use autodiscovery to detect sensors/configs + if cam_config[CONF_AUTO_DISCOVERY]: + if not cam.available: + _LOGGER.error( + "Android webcam %s not found for discovery!", cam.base_url) + return + + sensors = [sensor for sensor in cam.enabled_sensors + if sensor in SENSORS] + switches = [setting for setting in cam.enabled_settings + if setting in SWITCHES] + motion = True if 'motion_active' in cam.enabled_sensors else False + sensors.extend(['audio_connections', 'video_connections']) + + # load platforms + webcams[host] = cam + + mjpeg_camera = { + CONF_PLATFORM: 'mjpeg', + CONF_MJPEG_URL: cam.mjpeg_url, + CONF_STILL_IMAGE_URL: cam.image_url, + CONF_NAME: name, + } + if username and password: + mjpeg_camera.update({ + CONF_USERNAME: username, + CONF_PASSWORD: password + }) + + hass.async_add_job(discovery.async_load_platform( + hass, 'camera', 'mjpeg', mjpeg_camera, config)) + + if sensors: + hass.async_add_job(discovery.async_load_platform( + hass, 'sensor', DOMAIN, { + CONF_NAME: name, + CONF_HOST: host, + CONF_SENSORS: sensors, + }, config)) + + if switches: + hass.async_add_job(discovery.async_load_platform( + hass, 'switch', DOMAIN, { + CONF_NAME: name, + CONF_HOST: host, + CONF_SWITCHES: switches, + }, config)) + + if motion: + hass.async_add_job(discovery.async_load_platform( + hass, 'binary_sensor', DOMAIN, { + CONF_HOST: host, + CONF_NAME: name, + }, config)) + + tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] + if tasks: + yield from asyncio.wait(tasks, loop=hass.loop) + + return True + + +class AndroidIPCamEntity(Entity): + """The Android device running IP Webcam.""" + + def __init__(self, host, ipcam): + """Initialize the data oject.""" + self._host = host + self._ipcam = ipcam + + @asyncio.coroutine + def async_added_to_hass(self): + """Register update dispatcher.""" + @callback + def async_ipcam_update(host): + """Update callback.""" + if self._host != host: + return + self.hass.async_add_job(self.async_update_ha_state(True)) + + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) + + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._ipcam.available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + state_attr = {ATTR_HOST: self._host} + if self._ipcam.status_data is None: + return state_attr + + state_attr[ATTR_VID_CONNS] = \ + self._ipcam.status_data.get('video_connections') + state_attr[ATTR_AUD_CONNS] = \ + self._ipcam.status_data.get('audio_connections') + + return state_attr diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index bebace6d8272ae..96d5b0499d2d50 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,16 +11,17 @@ import voluptuous as vol -from homeassistant.bootstrap import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform from homeassistant import config as conf_util from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, - SERVICE_TOGGLE) + SERVICE_TOGGLE, SERVICE_RELOAD) from homeassistant.components import logbook from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv @@ -28,8 +29,6 @@ DOMAIN = 'automation' ENTITY_ID_FORMAT = DOMAIN + '.{}' -DEPENDENCIES = ['group'] - GROUP_NAME_ALL_AUTOMATIONS = 'all automations' CONF_ALIAS = 'alias' @@ -52,7 +51,6 @@ ATTR_LAST_TRIGGERED = 'last_triggered' ATTR_VARIABLES = 'variables' SERVICE_TRIGGER = 'trigger' -SERVICE_RELOAD = 'reload' _LOGGER = logging.getLogger(__name__) @@ -226,7 +224,7 @@ class AutomationEntity(ToggleEntity): """Entity to show status of entity.""" def __init__(self, name, async_attach_triggers, cond_func, async_action, - hidden): + hidden, initial_state): """Initialize an automation entity.""" self._name = name self._async_attach_triggers = async_attach_triggers @@ -236,6 +234,7 @@ def __init__(self, name, async_attach_triggers, cond_func, async_action, self._enabled = False self._last_triggered = None self._hidden = hidden + self._initial_state = initial_state @property def name(self): @@ -264,6 +263,18 @@ def is_on(self) -> bool: """Return True if entity is on.""" return self._enabled + @asyncio.coroutine + def async_added_to_hass(self) -> None: + """Startup with initial state or previous state.""" + state = yield from async_get_last_state(self.hass, self.entity_id) + if state is None: + if self._initial_state: + yield from self.async_enable() + else: + self._last_triggered = state.attributes.get('last_triggered') + if state.state == STATE_ON: + yield from self.async_enable() + @asyncio.coroutine def async_turn_on(self, **kwargs) -> None: """Turn the entity on and update the state.""" @@ -322,7 +333,6 @@ def _async_process_config(hass, config, component): This method is a coroutine. """ entities = [] - tasks = [] for config_key in extract_domain_configs(config, DOMAIN): conf = config[config_key] @@ -332,6 +342,7 @@ def _async_process_config(hass, config, component): list_no) hidden = config_block[CONF_HIDE_ENTITY] + initial_state = config_block[CONF_INITIAL_STATE] action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name) @@ -348,15 +359,14 @@ def cond_func(variables): async_attach_triggers = partial( _async_process_trigger, hass, config, - config_block.get(CONF_TRIGGER, []), name) - entity = AutomationEntity(name, async_attach_triggers, cond_func, - action, hidden) - if config_block[CONF_INITIAL_STATE]: - tasks.append(entity.async_enable()) + config_block.get(CONF_TRIGGER, []), name + ) + entity = AutomationEntity( + name, async_attach_triggers, cond_func, action, hidden, + initial_state) + entities.append(entity) - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) if entities: yield from component.async_add_entities(entities) diff --git a/homeassistant/components/binary_sensor/android_ip_webcam.py b/homeassistant/components/binary_sensor/android_ip_webcam.py new file mode 100644 index 00000000000000..4b9b4af24afed1 --- /dev/null +++ b/homeassistant/components/binary_sensor/android_ip_webcam.py @@ -0,0 +1,62 @@ +""" +Support for IP Webcam binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.android_ip_webcam/ +""" +import asyncio + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.android_ip_webcam import ( + KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME) + +DEPENDENCIES = ['android_ip_webcam'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup IP Webcam binary sensors.""" + if discovery_info is None: + return + + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + ipcam = hass.data[DATA_IP_WEBCAM][host] + + async_add_devices( + [IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True) + + +class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): + """Represents an IP Webcam binary sensor.""" + + def __init__(self, name, host, ipcam, sensor): + """Initialize the binary sensor.""" + super().__init__(host, ipcam) + + self._sensor = sensor + self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) + self._name = '{} {}'.format(name, self._mapped_name) + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the binary sensor, if any.""" + return self._name + + @property + def is_on(self): + """True if the binary sensor is on.""" + return self._state + + @asyncio.coroutine + def async_update(self): + """Retrieve latest state.""" + state, _ = self._ipcam.export_sensor(self._sensor) + self._state = state == 1.0 + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'motion' diff --git a/homeassistant/components/binary_sensor/blink.py b/homeassistant/components/binary_sensor/blink.py new file mode 100644 index 00000000000000..8d84ffb9c90ee9 --- /dev/null +++ b/homeassistant/components/binary_sensor/blink.py @@ -0,0 +1,74 @@ +""" +Support for Blink system camera control. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.blink/ +""" +from homeassistant.components.blink import DOMAIN +from homeassistant.components.binary_sensor import BinarySensorDevice + +DEPENDENCIES = ['blink'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the blink binary sensors.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN].blink + devs = list() + for name in data.cameras: + devs.append(BlinkCameraMotionSensor(name, data)) + devs.append(BlinkSystemSensor(data)) + add_devices(devs, True) + + +class BlinkCameraMotionSensor(BinarySensorDevice): + """A representation of a Blink binary sensor.""" + + def __init__(self, name, data): + """Initialize the sensor.""" + self._name = 'blink_' + name + '_motion_enabled' + self._camera_name = name + self.data = data + self._state = self.data.cameras[self._camera_name].armed + + @property + def name(self): + """Return the name of the blink sensor.""" + return self._name + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + def update(self): + """Update sensor state.""" + self.data.refresh() + self._state = self.data.cameras[self._camera_name].armed + + +class BlinkSystemSensor(BinarySensorDevice): + """A representation of a Blink system sensor.""" + + def __init__(self, data): + """Initialize the sensor.""" + self._name = 'blink armed status' + self.data = data + self._state = self.data.arm + + @property + def name(self): + """Return the name of the blink sensor.""" + return self._name.replace(" ", "_") + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + def update(self): + """Update sensor state.""" + self.data.refresh() + self._state = self.data.arm diff --git a/homeassistant/components/binary_sensor/enocean.py b/homeassistant/components/binary_sensor/enocean.py index c89148ebc15313..be01f63e657d99 100644 --- a/homeassistant/components/binary_sensor/enocean.py +++ b/homeassistant/components/binary_sensor/enocean.py @@ -67,7 +67,7 @@ def value_changed(self, value, value2): This method is called when there is an incoming packet associated with this platform. """ - self.update_ha_state() + self.schedule_update_ha_state() if value2 == 0x70: self.which = 0 self.onoff = 0 diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index 279dadf120fce0..22a3256f9fef5d 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -37,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): ) devices.append(device) - yield from async_add_devices(devices) + async_add_devices(devices) class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): @@ -52,8 +52,11 @@ def __init__(self, hass, zone_number, zone_name, zone_type, info, _LOGGER.debug('Setting up zone: ' + zone_name) super().__init__(zone_name, info, controller) + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" async_dispatcher_connect( - hass, SIGNAL_ZONE_UPDATE, self._update_callback) + self.hass, SIGNAL_ZONE_UPDATE, self._update_callback) @property def device_state_attributes(self): diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py index 8c822c5636159d..418a6342172efd 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_motion.py +++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py @@ -57,16 +57,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # generate sensor object entity = FFmpegMotion(hass, manager, config) - - # add to system - manager.async_register_device(entity) - yield from async_add_devices([entity]) + async_add_devices([entity]) class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice): """A binary sensor which use ffmpeg for noise detection.""" - def __init__(self, hass, config): + def __init__(self, config): """Constructor for binary sensor noise detection.""" super().__init__(config.get(CONF_INITIAL_STATE)) @@ -98,15 +95,19 @@ def __init__(self, hass, manager, config): """Initialize ffmpeg motion binary sensor.""" from haffmpeg import SensorMotion - super().__init__(hass, config) + super().__init__(config) self.ffmpeg = SensorMotion( manager.binary, hass.loop, self._async_callback) - def async_start_ffmpeg(self): + @asyncio.coroutine + def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. - This method must be run in the event loop and returns a coroutine. + This method is a coroutine. """ + if entity_ids is not None and self.entity_id not in entity_ids: + return + # init config self.ffmpeg.set_options( time_reset=self._config.get(CONF_RESET), @@ -116,7 +117,7 @@ def async_start_ffmpeg(self): ) # run - return self.ffmpeg.open_sensor( + yield from self.ffmpeg.open_sensor( input_source=self._config.get(CONF_INPUT), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), ) diff --git a/homeassistant/components/binary_sensor/ffmpeg_noise.py b/homeassistant/components/binary_sensor/ffmpeg_noise.py index 8db4691d743b6b..c3400150f74160 100644 --- a/homeassistant/components/binary_sensor/ffmpeg_noise.py +++ b/homeassistant/components/binary_sensor/ffmpeg_noise.py @@ -54,10 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # generate sensor object entity = FFmpegNoise(hass, manager, config) - - # add to system - manager.async_register_device(entity) - yield from async_add_devices([entity]) + async_add_devices([entity]) class FFmpegNoise(FFmpegBinarySensor): @@ -67,15 +64,19 @@ def __init__(self, hass, manager, config): """Initialize ffmpeg noise binary sensor.""" from haffmpeg import SensorNoise - super().__init__(hass, config) + super().__init__(config) self.ffmpeg = SensorNoise( manager.binary, hass.loop, self._async_callback) - def async_start_ffmpeg(self): + @asyncio.coroutine + def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. - This method must be run in the event loop and returns a coroutine. + This method is a coroutine. """ + if entity_ids is not None and self.entity_id not in entity_ids: + return + # init config self.ffmpeg.set_options( time_duration=self._config.get(CONF_DURATION), @@ -84,7 +85,7 @@ def async_start_ffmpeg(self): ) # run - return self.ffmpeg.open_sensor( + yield from self.ffmpeg.open_sensor( input_source=self._config.get(CONF_INPUT), output_dest=self._config.get(CONF_OUTPUT), extra_cmd=self._config.get(CONF_EXTRA_ARGUMENTS), diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index e14d4149ffe626..135d9a1e02857e 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -15,9 +15,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_SSL, EVENT_HOMEASSISTANT_STOP, ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) + CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, + ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE) -REQUIREMENTS = ['pyhik==0.0.7', 'pydispatcher==2.0.5'] +REQUIREMENTS = ['pyhik==0.1.0'] _LOGGER = logging.getLogger(__name__) CONF_IGNORED = 'ignored' @@ -119,30 +120,32 @@ def __init__(self, hass, url, port, name, username, password): self._password = password # Establish camera - self._cam = HikCamera(self._url, self._port, - self._username, self._password) + self.camdata = HikCamera(self._url, self._port, + self._username, self._password) if self._name is None: - self._name = self._cam.get_name - - # Start event stream - self._cam.start_stream() + self._name = self.camdata.get_name hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.stop_hik) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.start_hik) def stop_hik(self, event): """Shutdown Hikvision subscriptions and subscription thread on exit.""" - self._cam.disconnect() + self.camdata.disconnect() + + def start_hik(self, event): + """Start Hikvision event stream thread.""" + self.camdata.start_stream() @property def sensors(self): """Return list of available sensors and their states.""" - return self._cam.current_event_states + return self.camdata.current_event_states @property def cam_id(self): """Return camera id.""" - return self._cam.get_id + return self.camdata.get_id @property def name(self): @@ -155,8 +158,6 @@ class HikvisionBinarySensor(BinarySensorDevice): def __init__(self, hass, sensor, cam, delay): """Initialize the binary_sensor.""" - from pydispatch import dispatcher - self._hass = hass self._cam = cam self._name = self._cam.name + ' ' + sensor @@ -170,12 +171,8 @@ def __init__(self, hass, sensor, cam, delay): self._timer = None - # Form signal for dispatcher - signal = 'ValueChanged.{}'.format(self._cam.cam_id) - - dispatcher.connect(self._update_callback, - signal=signal, - sender=self._sensor) + # Register callback function with pyHik + self._cam.camdata.add_update_callback(self._update_callback, self._id) def _sensor_state(self): """Extract sensor state.""" @@ -225,13 +222,9 @@ def device_state_attributes(self): return attr - def _update_callback(self, signal, sender): + def _update_callback(self, msg): """Update the sensor's state, if needed.""" - _LOGGER.debug('Dispatcher callback, signal: %s, sender: %s', - signal, sender) - - if sender is not self._sensor: - return + _LOGGER.debug('Callback signal from: %s', msg) if self._delay > 0 and not self.is_on: # Set timer to wait until updating the state diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/binary_sensor/maxcube.py new file mode 100644 index 00000000000000..77448fd6adc391 --- /dev/null +++ b/homeassistant/components/binary_sensor/maxcube.py @@ -0,0 +1,76 @@ +""" +Support for MAX! Window Shutter via MAX! Cube. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/maxcube/ +""" + +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.maxcube import MAXCUBE_HANDLE +from homeassistant.const import STATE_UNKNOWN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Iterate through all MAX! Devices and add window shutters to HASS.""" + cube = hass.data[MAXCUBE_HANDLE].cube + + # List of devices + devices = [] + + for device in cube.devices: + # Create device name by concatenating room name + device name + name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name) + + # Only add Window Shutters + if cube.is_windowshutter(device): + # add device to HASS + devices.append(MaxCubeShutter(hass, name, device.rf_address)) + + if len(devices) > 0: + add_devices(devices) + + +class MaxCubeShutter(BinarySensorDevice): + """MAX! Cube BinarySensor device.""" + + def __init__(self, hass, name, rf_address): + """Initialize MAX! Cube BinarySensorDevice.""" + self._name = name + self._sensor_type = 'opening' + self._rf_address = rf_address + self._cubehandle = hass.data[MAXCUBE_HANDLE] + self._state = STATE_UNKNOWN + + @property + def should_poll(self): + """Polling is required.""" + return True + + @property + def name(self): + """Return the name of the BinarySensorDevice.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._sensor_type + + @property + def is_on(self): + """Return true if the binary sensor is on/open.""" + return self._state + + def update(self): + """Get latest data from MAX! Cube.""" + self._cubehandle.update() + + # Get the device we want to update + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Update our internal state + self._state = device.is_open diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 06814d85f8823c..d8467a6cbfe771 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -46,7 +46,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if value_template is not None: value_template.hass = hass - yield from async_add_devices([MqttBinarySensor( + async_add_devices([MqttBinarySensor( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS), diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 8f11424f54c9ed..396f591923b80c 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -15,12 +15,14 @@ DEVICE_CLASSES_SCHEMA) from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, - CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS) + CONF_SENSOR_CLASS, CONF_SENSORS, CONF_DEVICE_CLASS, + EVENT_HOMEASSISTANT_START, STATE_ON) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import get_deprecated +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -66,7 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error('No sensors added') return False - yield from async_add_devices(sensors, True) + async_add_devices(sensors, True) return True @@ -83,14 +85,30 @@ def __init__(self, hass, device, friendly_name, device_class, self._device_class = device_class self._template = value_template self._state = None + self._entities = entity_ids + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + state = yield from async_get_last_state(self.hass, self.entity_id) + if state: + self._state = state.state == STATE_ON @callback def template_bsensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - hass.async_add_job(self.async_update_ha_state, True) + self.hass.async_add_job(self.async_update_ha_state(True)) + + @callback + def template_bsensor_startup(event): + """Update template on startup.""" + async_track_state_change( + self.hass, self._entities, template_bsensor_state_listener) + + self.hass.async_add_job(self.async_update_ha_state(True)) - async_track_state_change( - hass, entity_ids, template_bsensor_state_listener) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_bsensor_startup) @property def name(self): diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index be41fd96556bc3..c97ba17b8748b5 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -52,7 +52,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): limit_type = config.get(CONF_TYPE) device_class = get_deprecated(config, CONF_DEVICE_CLASS, CONF_SENSOR_CLASS) - yield from async_add_devices( + async_add_devices( [ThresholdSensor(hass, entity_id, name, threshold, limit_type, device_class)], True) return True diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py index ce2b8b715bd953..e16f4e17fa0589 100644 --- a/homeassistant/components/binary_sensor/vera.py +++ b/homeassistant/components/binary_sensor/vera.py @@ -7,9 +7,9 @@ import logging from homeassistant.components.binary_sensor import ( - BinarySensorDevice) + BinarySensorDevice, ENTITY_ID_FORMAT) from homeassistant.components.vera import ( - VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + VERA_CONTROLLER, VERA_DEVICES, VeraDevice) DEPENDENCIES = ['vera'] @@ -30,6 +30,7 @@ def __init__(self, vera_device, controller): """Initialize the binary_sensor.""" self._state = False VeraDevice.__init__(self, vera_device, controller) + self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/zwave.py b/homeassistant/components/binary_sensor/zwave.py index 3a8144d9188b2a..48ef1479eeccda 100644 --- a/homeassistant/components/binary_sensor/zwave.py +++ b/homeassistant/components/binary_sensor/zwave.py @@ -10,6 +10,7 @@ from homeassistant.helpers.event import track_point_in_time from homeassistant.components import zwave from homeassistant.components.zwave import workaround +from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.components.binary_sensor import ( DOMAIN, BinarySensorDevice) @@ -18,31 +19,20 @@ DEPENDENCIES = [] -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Z-Wave platform for binary sensors.""" - if discovery_info is None or zwave.NETWORK is None: - return - - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - value.set_change_verified(False) - +def get_device(value, **kwargs): + """Create zwave entity device.""" device_mapping = workaround.get_device_mapping(value) if device_mapping == workaround.WORKAROUND_NO_OFF_EVENT: # Default the multiplier to 4 re_arm_multiplier = (zwave.get_config_value(value.node, 9) or 4) - add_devices([ - ZWaveTriggerSensor(value, "motion", - hass, re_arm_multiplier * 8) - ]) - return + return ZWaveTriggerSensor(value, "motion", re_arm_multiplier * 8) if workaround.get_device_component_mapping(value) == DOMAIN: - add_devices([ZWaveBinarySensor(value, None)]) - return + return ZWaveBinarySensor(value, None) if value.command_class == zwave.const.COMMAND_CLASS_SENSOR_BINARY: - add_devices([ZWaveBinarySensor(value, None)]) + return ZWaveBinarySensor(value, None) + return None class ZWaveBinarySensor(BinarySensorDevice, zwave.ZWaveDeviceEntity): @@ -77,26 +67,23 @@ def should_poll(self): class ZWaveTriggerSensor(ZWaveBinarySensor): """Representation of a stateless sensor within Z-Wave.""" - def __init__(self, value, device_class, hass, re_arm_sec=60): + def __init__(self, value, device_class, re_arm_sec=60): """Initialize the sensor.""" super(ZWaveTriggerSensor, self).__init__(value, device_class) - self._hass = hass self.re_arm_sec = re_arm_sec - self.invalidate_after = dt_util.utcnow() + datetime.timedelta( - seconds=self.re_arm_sec) - # If it's active make sure that we set the timeout tracker - track_point_in_time( - self._hass, self.async_update_ha_state, - self.invalidate_after) + self.invalidate_after = None def update_properties(self): """Called when a value for this entity's node has changed.""" self._state = self._value.data # only allow this value to be true for re_arm secs + if not self.hass: + return + self.invalidate_after = dt_util.utcnow() + datetime.timedelta( seconds=self.re_arm_sec) track_point_in_time( - self._hass, self.async_update_ha_state, + self.hass, self.async_update_ha_state, self.invalidate_after) @property diff --git a/homeassistant/components/blink.py b/homeassistant/components/blink.py new file mode 100644 index 00000000000000..94635e2ae594de --- /dev/null +++ b/homeassistant/components/blink.py @@ -0,0 +1,87 @@ +""" +Support for Blink Home Camera System. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/blink/ +""" +import logging +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.const import (CONF_USERNAME, + CONF_PASSWORD, + ATTR_FRIENDLY_NAME, + ATTR_ARMED) +from homeassistant.helpers import discovery +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'blink' +REQUIREMENTS = ['blinkpy==0.4.4'] + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + +ARM_SYSTEM_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ARMED): cv.boolean +}) + +ARM_CAMERA_SCHEMA = vol.Schema({ + vol.Required(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_ARMED): cv.boolean +}) + +SNAP_PICTURE_SCHEMA = vol.Schema({ + vol.Required(ATTR_FRIENDLY_NAME): cv.string +}) + + +class BlinkSystem(object): + """Blink System class.""" + + def __init__(self, config_info): + """Initialize the system.""" + import blinkpy + self.blink = blinkpy.Blink(username=config_info[DOMAIN][CONF_USERNAME], + password=config_info[DOMAIN][CONF_PASSWORD]) + self.blink.setup_system() + + +def setup(hass, config): + """Setup Blink System.""" + hass.data[DOMAIN] = BlinkSystem(config) + discovery.load_platform(hass, 'camera', DOMAIN, {}, config) + discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) + discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) + + def snap_picture(call): + """Take a picture.""" + cameras = hass.data[DOMAIN].blink.cameras + name = call.data.get(ATTR_FRIENDLY_NAME, '') + if name in cameras: + cameras[name].snap_picture() + + def arm_camera(call): + """Arm a camera.""" + cameras = hass.data[DOMAIN].blink.cameras + name = call.data.get(ATTR_FRIENDLY_NAME, '') + value = call.data.get(ATTR_ARMED, True) + if name in cameras: + cameras[name].set_motion_detect(value) + + def arm_system(call): + """Arm the system.""" + value = call.data.get(ATTR_ARMED, True) + hass.data[DOMAIN].blink.arm = value + hass.data[DOMAIN].blink.refresh() + + hass.services.register(DOMAIN, 'snap_picture', snap_picture, + schema=SNAP_PICTURE_SCHEMA) + hass.services.register(DOMAIN, 'arm_camera', arm_camera, + schema=ARM_CAMERA_SCHEMA) + hass.services.register(DOMAIN, 'arm_system', arm_system, + schema=ARM_SYSTEM_SCHEMA) + + return True diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index e4de69c3ce8598..70477198ea01d3 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -3,8 +3,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/calendar/ - """ +import asyncio import logging from datetime import timedelta @@ -27,13 +27,13 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Track states and offer events for calendars.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL, DOMAIN) - component.setup(config) - + yield from component.async_setup(config) return True @@ -155,7 +155,7 @@ def _get_date(date): start = _get_date(self.data.event['start']) end = _get_date(self.data.event['end']) - summary = self.data.event['summary'] + summary = self.data.event.get('summary', '') # check if we have an offset tag in the message # time is HH:MM or MM diff --git a/homeassistant/components/camera/blink.py b/homeassistant/components/camera/blink.py new file mode 100644 index 00000000000000..685ee5bd0fade2 --- /dev/null +++ b/homeassistant/components/camera/blink.py @@ -0,0 +1,81 @@ +""" +Support for Blink system camera. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.blink/ +""" +import logging + +from datetime import timedelta +import requests + +from homeassistant.components.blink import DOMAIN +from homeassistant.components.camera import Camera +from homeassistant.util import Throttle + +DEPENDENCIES = ['blink'] + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup a Blink Camera.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN].blink + devs = list() + for name in data.cameras: + devs.append(BlinkCamera(hass, config, data, name)) + + add_devices(devs) + + +class BlinkCamera(Camera): + """An implementation of a Blink Camera.""" + + def __init__(self, hass, config, data, name): + """Initialize a camera.""" + super().__init__() + self.data = data + self.hass = hass + self._name = name + self.notifications = self.data.cameras[self._name].notifications + self.response = None + + _LOGGER.info("Initialized blink camera %s", self._name) + + @property + def name(self): + """A camera name.""" + return self._name + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def request_image(self): + """An image request from Blink servers.""" + _LOGGER.info("Requesting new image from blink servers") + image_url = self.check_for_motion() + header = self.data.cameras[self._name].header + self.response = requests.get(image_url, headers=header, stream=True) + + def check_for_motion(self): + """A method to check if motion has been detected since last update.""" + self.data.refresh() + notifs = self.data.cameras[self._name].notifications + if notifs > self.notifications: + # We detected motion at some point + self.data.last_motion() + self.notifications = notifs + # returning motion image currently not working + # return self.data.cameras[self._name].motion['image'] + elif notifs < self.notifications: + self.notifications = notifs + + return self.data.camera_thumbs[self._name] + + def camera_image(self): + """Return a still image reponse from the camera.""" + self.request_image() + return self.response.content diff --git a/homeassistant/components/camera/dispatcher.py b/homeassistant/components/camera/dispatcher.py new file mode 100644 index 00000000000000..b5a846665adde8 --- /dev/null +++ b/homeassistant/components/camera/dispatcher.py @@ -0,0 +1,67 @@ +""" +Support for internal dispatcher image push to Camera. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.dispatcher/ +""" +import asyncio +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import CONF_NAME +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +CONF_SIGNAL = 'signal' +DEFAULT_NAME = 'Dispatcher Camera' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_SIGNAL): cv.slugify, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Setup a dispatcher camera.""" + if discovery_info: + config = PLATFORM_SCHEMA(discovery_info) + + async_add_devices( + [DispatcherCamera(config[CONF_NAME], config[CONF_SIGNAL])]) + + +class DispatcherCamera(Camera): + """A dispatcher implementation of an camera.""" + + def __init__(self, name, signal): + """Initialize a dispatcher camera.""" + super().__init__() + self._name = name + self._signal = signal + self._image = None + + @asyncio.coroutine + def async_added_to_hass(self): + """Register dispatcher and callbacks.""" + @callback + def async_update_image(image): + """Update image from dispatcher call.""" + self._image = image + + async_dispatcher_connect(self.hass, self._signal, async_update_image) + + @asyncio.coroutine + def async_camera_image(self): + """Return a still image response from the camera.""" + return self._image + + @property + def name(self): + """Return the name of this device.""" + return self._name diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 6b00ae240ed34f..ed8c84f90dfbd3 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -34,7 +34,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): return - yield from async_add_devices([FFmpegCamera(hass, config)]) + async_add_devices([FFmpegCamera(hass, config)]) class FFmpegCamera(Camera): diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index e84794356b27a3..a374d19f4d1293 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -47,15 +47,21 @@ def __init__(self, device_info): port = device_info.get(CONF_PORT) self._base_url = 'http://{}:{}/'.format(ip_address, port) + + uri_template = self._base_url \ + + 'cgi-bin/CGIProxy.fcgi?' \ + + 'cmd=snapPicture2&usr={}&pwd={}' + self._username = device_info.get(CONF_USERNAME) self._password = device_info.get(CONF_PASSWORD) - self._snap_picture_url = self._base_url \ - + 'cgi-bin/CGIProxy.fcgi?cmd=snapPicture2&usr=' \ - + self._username + '&pwd=' + self._password + self._snap_picture_url = uri_template.format( + self._username, + self._password + ) self._name = device_info.get(CONF_NAME) _LOGGER.info('Using the following URL for %s: %s', - self._name, self._snap_picture_url) + self._name, uri_template.format('***', '***')) def camera_image(self): """Return a still image reponse from the camera.""" diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index f9a4e8c2f062be..3f50bc799c4932 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -44,7 +44,7 @@ # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a generic IP Camera.""" - yield from async_add_devices([GenericCamera(hass, config)]) + async_add_devices([GenericCamera(hass, config)]) class GenericCamera(Camera): diff --git a/homeassistant/components/camera/local_file.py b/homeassistant/components/camera/local_file.py index 65defb4557b184..854388203931bc 100644 --- a/homeassistant/components/camera/local_file.py +++ b/homeassistant/components/camera/local_file.py @@ -20,7 +20,7 @@ DEFAULT_NAME = 'Local File' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FILE_PATH): cv.isfile, + vol.Required(CONF_FILE_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string }) @@ -31,8 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # check filepath given is readable if not os.access(file_path, os.R_OK): - _LOGGER.error("file path is not readable") - return False + _LOGGER.warning("Could not read camera %s image from file: %s", + config[CONF_NAME], file_path) add_devices([LocalFile(config[CONF_NAME], file_path)]) @@ -49,8 +49,12 @@ def __init__(self, name, file_path): def camera_image(self): """Return image response.""" - with open(self._file_path, 'rb') as file: - return file.read() + try: + with open(self._file_path, 'rb') as file: + return file.read() + except FileNotFoundError: + _LOGGER.warning("Could not read camera %s image from file: %s", + self._name, self._file_path) @property def name(self): diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 8d52785557b38e..532b91e7442b0f 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -45,7 +45,9 @@ # pylint: disable=unused-argument def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MJPEG IP Camera.""" - yield from async_add_devices([MjpegCamera(hass, config)]) + if discovery_info: + config = PLATFORM_SCHEMA(discovery_info) + async_add_devices([MjpegCamera(hass, config)]) def extract_image_from_mjpeg(stream): diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index 39939c73d0d5de..c5d87c39086f08 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -153,7 +153,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): ) devices.append(device) - yield from async_add_devices(devices) + async_add_devices(devices) @asyncio.coroutine diff --git a/homeassistant/components/camera/zoneminder.py b/homeassistant/components/camera/zoneminder.py index 12615262b261fd..5148ce8b245dd7 100644 --- a/homeassistant/components/camera/zoneminder.py +++ b/homeassistant/components/camera/zoneminder.py @@ -75,4 +75,4 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.warning('No active cameras found') return - yield from async_add_devices(cameras) + async_add_devices(cameras) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 18ccff459b036b..c9403fbf2ed476 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -25,6 +25,8 @@ ATTR_RESUME_ALL = 'resume_all' DEFAULT_RESUME_ALL = False +TEMPERATURE_HOLD = 'temp' +VACATION_HOLD = 'vacation' DEPENDENCIES = ['ecobee'] @@ -112,6 +114,8 @@ def __init__(self, data, thermostat_index, hold_temp): self.thermostat_index) self._name = self.thermostat['name'] self.hold_temp = hold_temp + self.vacation = None + self._climate_list = self.climate_list self._operation_list = ['auto', 'auxHeatOnly', 'cool', 'heat', 'off'] self.update_without_throttle = False @@ -187,29 +191,30 @@ def fan(self): def current_hold_mode(self): """Return current hold mode.""" events = self.thermostat['events'] - if any((event['holdClimateRef'] == 'away' and - int(event['endDate'][0:4])-int(event['startDate'][0:4]) <= 1) - or event['type'] == 'autoAway' - for event in events): - # away hold is auto away or a temporary hold from away climate - hold = 'away' - elif any(event['holdClimateRef'] == 'away' and - int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1 - for event in events): - # a permanent away is not considered a hold, but away_mode - hold = None - elif any(event['holdClimateRef'] == 'home' or - event['type'] == 'autoHome' - for event in events): - # home mode is auto home or any home hold - hold = 'home' - elif any(event['type'] == 'hold' and event['running'] - for event in events): - hold = 'temp' - # temperature hold is any other hold not based on climate - else: - hold = None - return hold + for event in events: + if event['running']: + if event['type'] == 'hold': + if event['holdClimateRef'] == 'away': + if int(event['endDate'][0:4]) - \ + int(event['startDate'][0:4]) <= 1: + # a temporary hold from away climate is a hold + return 'away' + else: + # a premanent hold from away climate is away_mode + return None + elif event['holdClimateRef'] != "": + # any other hold based on climate + return event['holdClimateRef'] + else: + # any hold not based on a climate is a temp hold + return TEMPERATURE_HOLD + elif event['type'].startswith('auto'): + # all auto modes are treated as holds + return event['type'][4:].lower() + elif event['type'] == 'vacation': + self.vacation = event['name'] + return VACATION_HOLD + return None @property def current_operation(self): @@ -232,8 +237,11 @@ def operation_mode(self): @property def mode(self): - """Return current mode ie. home, away, sleep.""" - return self.thermostat['program']['currentClimateRef'] + """Return current mode, as the user-visible name.""" + cur = self.thermostat['program']['currentClimateRef'] + climates = self.thermostat['program']['climates'] + current = list(filter(lambda x: x['climateRef'] == cur, climates)) + return current[0]['name'] @property def fan_min_on_time(self): @@ -261,52 +269,44 @@ def device_state_attributes(self): "fan": self.fan, "mode": self.mode, "operation": operation, + "climate_list": self.climate_list, "fan_min_on_time": self.fan_min_on_time } - def is_vacation_on(self): - """Return true if vacation mode is on.""" - events = self.thermostat['events'] - return any(event['type'] == 'vacation' and event['running'] - for event in events) - @property def is_away_mode_on(self): """Return true if away mode is on.""" - events = self.thermostat['events'] - return any(event['holdClimateRef'] == 'away' and - int(event['endDate'][0:4])-int(event['startDate'][0:4]) > 1 - for event in events) + return self.current_hold_mode == 'away' def turn_away_mode_on(self): """Turn away on.""" - self.data.ecobee.set_climate_hold(self.thermostat_index, - "away", 'indefinite') - self.update_without_throttle = True + self.set_hold_mode('away') def turn_away_mode_off(self): """Turn away off.""" - self.data.ecobee.resume_program(self.thermostat_index) - self.update_without_throttle = True + self.set_hold_mode(None) def set_hold_mode(self, hold_mode): - """Set hold mode (away, home, temp).""" + """Set hold mode (away, home, temp, sleep, etc.).""" hold = self.current_hold_mode if hold == hold_mode: # no change, so no action required return - elif hold_mode == 'away': - self.data.ecobee.set_climate_hold(self.thermostat_index, - "away", self.hold_preference()) - elif hold_mode == 'home': - self.data.ecobee.set_climate_hold(self.thermostat_index, - "home", self.hold_preference()) - elif hold_mode == 'temp': - self.set_temp_hold(int(self.current_temperature)) + elif hold_mode == 'None' or hold_mode is None: + if hold == VACATION_HOLD: + self.data.ecobee.delete_vacation(self.thermostat_index, + self.vacation) + else: + self.data.ecobee.resume_program(self.thermostat_index) else: - self.data.ecobee.resume_program(self.thermostat_index) - self.update_without_throttle = True + if hold_mode == TEMPERATURE_HOLD: + self.set_temp_hold(int(self.current_temperature)) + else: + self.data.ecobee.set_climate_hold(self.thermostat_index, + hold_mode, + self.hold_preference()) + self.update_without_throttle = True def set_auto_temp_hold(self, heat_temp, cool_temp): """Set temperature hold in auto mode.""" @@ -382,3 +382,9 @@ def hold_preference(self): # as an indefinite away hold is interpreted as away_mode else: return 'nextTransition' + + @property + def climate_list(self): + """Return the list of climates currently available.""" + climates = self.thermostat['program']['climates'] + return list(map((lambda x: x['name']), climates)) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index da746270197f0f..4fc667a53261b9 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -16,7 +16,8 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE) from homeassistant.helpers import condition -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import ( + async_track_state_change, async_track_time_interval) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,7 @@ CONF_AC_MODE = 'ac_mode' CONF_MIN_DUR = 'min_cycle_duration' CONF_TOLERANCE = 'tolerance' +CONF_KEEP_ALIVE = 'keep_alive' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -47,6 +49,8 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TOLERANCE, default=DEFAULT_TOLERANCE): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), + vol.Optional(CONF_KEEP_ALIVE): vol.All( + cv.time_period, cv.positive_timedelta), }) @@ -62,10 +66,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): ac_mode = config.get(CONF_AC_MODE) min_cycle_duration = config.get(CONF_MIN_DUR) tolerance = config.get(CONF_TOLERANCE) + keep_alive = config.get(CONF_KEEP_ALIVE) - yield from async_add_devices([GenericThermostat( + async_add_devices([GenericThermostat( hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, - target_temp, ac_mode, min_cycle_duration, tolerance)]) + target_temp, ac_mode, min_cycle_duration, tolerance, keep_alive)]) class GenericThermostat(ClimateDevice): @@ -73,7 +78,7 @@ class GenericThermostat(ClimateDevice): def __init__(self, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration, - tolerance): + tolerance, keep_alive): """Initialize the thermostat.""" self.hass = hass self._name = name @@ -81,6 +86,7 @@ def __init__(self, hass, name, heater_entity_id, sensor_entity_id, self.ac_mode = ac_mode self.min_cycle_duration = min_cycle_duration self._tolerance = tolerance + self._keep_alive = keep_alive self._active = False self._cur_temp = None @@ -94,6 +100,10 @@ def __init__(self, hass, name, heater_entity_id, sensor_entity_id, async_track_state_change( hass, heater_entity_id, self._async_switch_changed) + if self._keep_alive: + async_track_time_interval( + hass, self._async_keep_alive, self._keep_alive) + sensor_state = hass.states.get(sensor_entity_id) if sensor_state: self._async_update_temp(sensor_state) @@ -180,6 +190,14 @@ def _async_switch_changed(self, entity_id, old_state, new_state): return self.hass.async_add_job(self.async_update_ha_state()) + @callback + def _async_keep_alive(self, time): + """Called at constant intervals for keep-alive purposes.""" + if self.current_operation in [STATE_COOL, STATE_HEAT]: + switch.async_turn_on(self.hass, self.heater_entity_id) + else: + switch.async_turn_off(self.hass, self.heater_entity_id) + @callback def _async_update_temp(self, state): """Update thermostat with latest state from sensor.""" diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 3387baf76d8981..5152519459b2d3 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -6,10 +6,15 @@ """ import logging import socket +import datetime import voluptuous as vol +import requests -from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA) +from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA, + ATTR_FAN_MODE, ATTR_FAN_LIST, + ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST) from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) @@ -21,27 +26,35 @@ _LOGGER = logging.getLogger(__name__) ATTR_FAN = 'fan' -ATTR_FANMODE = 'fanmode' ATTR_SYSTEM_MODE = 'system_mode' +ATTR_CURRENT_OPERATION = 'equipment_output_status' CONF_AWAY_TEMPERATURE = 'away_temperature' +CONF_COOL_AWAY_TEMPERATURE = 'away_cool_temperature' +CONF_HEAT_AWAY_TEMPERATURE = 'away_heat_temperature' CONF_REGION = 'region' DEFAULT_AWAY_TEMPERATURE = 16 +DEFAULT_COOL_AWAY_TEMPERATURE = 30 +DEFAULT_HEAT_AWAY_TEMPERATURE = 16 DEFAULT_REGION = 'eu' REGIONS = ['eu', 'us'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_AWAY_TEMPERATURE, default=DEFAULT_AWAY_TEMPERATURE): - vol.Coerce(float), + vol.Optional(CONF_AWAY_TEMPERATURE, + default=DEFAULT_AWAY_TEMPERATURE): vol.Coerce(float), + vol.Optional(CONF_COOL_AWAY_TEMPERATURE, + default=DEFAULT_COOL_AWAY_TEMPERATURE): vol.Coerce(float), + vol.Optional(CONF_HEAT_AWAY_TEMPERATURE, + default=DEFAULT_HEAT_AWAY_TEMPERATURE): vol.Coerce(float), vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(REGIONS), }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the HoneywelL thermostat.""" + """Setup the Honeywell thermostat.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) region = config.get(CONF_REGION) @@ -88,8 +101,11 @@ def _setup_us(username, password, config, add_devices): dev_id = config.get('thermostat') loc_id = config.get('location') + cool_away_temp = config.get(CONF_COOL_AWAY_TEMPERATURE) + heat_away_temp = config.get(CONF_HEAT_AWAY_TEMPERATURE) - add_devices([HoneywellUSThermostat(client, device) + add_devices([HoneywellUSThermostat(client, device, cool_away_temp, + heat_away_temp, username, password) for location in client.locations_by_id.values() for device in location.devices_by_id.values() if ((not loc_id or location.locationid == loc_id) and @@ -160,7 +176,7 @@ def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: def turn_away_mode_on(self): """Turn away on. - Evohome does have a proprietary away mode, but it doesn't really work + Honeywell does have a proprietary away mode, but it doesn't really work the way it should. For example: If you set a temperature manually it doesn't get overwritten when away mode is switched on. """ @@ -199,10 +215,16 @@ def update(self): class HoneywellUSThermostat(ClimateDevice): """Representation of a Honeywell US Thermostat.""" - def __init__(self, client, device): + def __init__(self, client, device, cool_away_temp, + heat_away_temp, username, password): """Initialize the thermostat.""" self._client = client self._device = device + self._cool_away_temp = cool_away_temp + self._heat_away_temp = heat_away_temp + self._away = False + self._username = username + self._password = password @property def is_fan_on(self): @@ -236,7 +258,10 @@ def target_temperature(self): @property def current_operation(self: ClimateDevice) -> str: """Return current operation ie. heat, cool, idle.""" - return getattr(self._device, ATTR_SYSTEM_MODE, None) + oper = getattr(self._device, ATTR_CURRENT_OPERATION, None) + if oper == "off": + oper = "idle" + return oper def set_temperature(self, **kwargs): """Set target temperature.""" @@ -245,29 +270,84 @@ def set_temperature(self, **kwargs): return import somecomfort try: - if self._device.system_mode == 'cool': - self._device.setpoint_cool = temperature - else: - self._device.setpoint_heat = temperature + # Get current mode + mode = self._device.system_mode + # Set hold if this is not the case + if getattr(self._device, "hold_{}".format(mode)) is False: + # Get next period key + next_period_key = '{}NextPeriod'.format(mode.capitalize()) + # Get next period raw value + next_period = self._device.raw_ui_data.get(next_period_key) + # Get next period time + hour, minute = divmod(next_period * 15, 60) + # Set hold time + setattr(self._device, + "hold_{}".format(mode), + datetime.time(hour, minute)) + # Set temperature + setattr(self._device, + "setpoint_{}".format(mode), + temperature) except somecomfort.SomeComfortError: _LOGGER.error('Temperature %.1f out of range', temperature) @property def device_state_attributes(self): """Return the device specific state attributes.""" - return { + import somecomfort + data = { ATTR_FAN: (self.is_fan_on and 'running' or 'idle'), - ATTR_FANMODE: self._device.fan_mode, - ATTR_SYSTEM_MODE: self._device.system_mode, + ATTR_FAN_MODE: self._device.fan_mode, + ATTR_OPERATION_MODE: self._device.system_mode, } + data[ATTR_FAN_LIST] = somecomfort.FAN_MODES + data[ATTR_OPERATION_LIST] = somecomfort.SYSTEM_MODES + return data + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._away def turn_away_mode_on(self): - """Turn away on.""" - pass + """Turn away on. + + Somecomfort does have a proprietary away mode, but it doesn't really + work the way it should. For example: If you set a temperature manually + it doesn't get overwritten when away mode is switched on. + """ + self._away = True + import somecomfort + try: + # Get current mode + mode = self._device.system_mode + except somecomfort.SomeComfortError: + _LOGGER.error('Can not get system mode') + return + try: + + # Set permanent hold + setattr(self._device, + "hold_{}".format(mode), + True) + # Set temperature + setattr(self._device, + "setpoint_{}".format(mode), + getattr(self, "_{}_away_temp".format(mode))) + except somecomfort.SomeComfortError: + _LOGGER.error('Temperature %.1f out of range', + getattr(self, "_{}_away_temp".format(mode))) def turn_away_mode_off(self): """Turn away off.""" - pass + self._away = False + import somecomfort + try: + # Disabling all hold modes + self._device.hold_cool = False + self._device.hold_heat = False + except somecomfort.SomeComfortError: + _LOGGER.error('Can not stop hold mode') def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: """Set the system mode (Cool, Heat, etc).""" @@ -276,4 +356,49 @@ def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: def update(self): """Update the state.""" - self._device.refresh() + import somecomfort + retries = 3 + while retries > 0: + try: + self._device.refresh() + break + except (somecomfort.client.APIRateLimited, OSError, + requests.exceptions.ReadTimeout) as exp: + retries -= 1 + if retries == 0: + raise exp + if not self._retry(): + raise exp + _LOGGER.error("SomeComfort update failed, Retrying " + "- Error: %s", exp) + + def _retry(self): + """Recreate a new somecomfort client. + + When we got an error, the best way to be sure that the next query + will succeed, is to recreate a new somecomfort client. + """ + import somecomfort + try: + self._client = somecomfort.SomeComfort(self._username, + self._password) + except somecomfort.AuthError: + _LOGGER.error('Failed to login to honeywell account %s', + self._username) + return False + except somecomfort.SomeComfortError as ex: + _LOGGER.error('Failed to initialize honeywell client: %s', + str(ex)) + return False + + devices = [device + for location in self._client.locations_by_id.values() + for device in location.devices_by_id.values() + if device.name == self._device.name] + + if len(devices) != 1: + _LOGGER.error('Failed to find device %s', self._device.name) + return False + + self._device = devices[0] + return True diff --git a/homeassistant/components/climate/maxcube.py b/homeassistant/components/climate/maxcube.py new file mode 100644 index 00000000000000..a04a547f534e0b --- /dev/null +++ b/homeassistant/components/climate/maxcube.py @@ -0,0 +1,216 @@ +""" +Support for MAX! Thermostats via MAX! Cube. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/maxcube/ +""" + +import socket +import logging + +from homeassistant.components.climate import ClimateDevice, STATE_AUTO +from homeassistant.components.maxcube import MAXCUBE_HANDLE +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import STATE_UNKNOWN + +_LOGGER = logging.getLogger(__name__) + +STATE_MANUAL = "manual" +STATE_BOOST = "boost" +STATE_VACATION = "vacation" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Iterate through all MAX! Devices and add thermostats to HASS.""" + cube = hass.data[MAXCUBE_HANDLE].cube + + # List of devices + devices = [] + + for device in cube.devices: + # Create device name by concatenating room name + device name + name = "%s %s" % (cube.room_by_id(device.room_id).name, device.name) + + # Only add thermostats and wallthermostats + if cube.is_thermostat(device) or cube.is_wallthermostat(device): + # Add device to HASS + devices.append(MaxCubeClimate(hass, name, device.rf_address)) + + # Add all devices at once + if len(devices) > 0: + add_devices(devices) + + +class MaxCubeClimate(ClimateDevice): + """MAX! Cube ClimateDevice.""" + + def __init__(self, hass, name, rf_address): + """Initialize MAX! Cube ClimateDevice.""" + self._name = name + self._unit_of_measurement = TEMP_CELSIUS + self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST, + STATE_VACATION] + self._rf_address = rf_address + self._cubehandle = hass.data[MAXCUBE_HANDLE] + + @property + def should_poll(self): + """Polling is required.""" + return True + + @property + def name(self): + """Return the name of the ClimateDevice.""" + return self._name + + @property + def min_temp(self): + """Return the minimum temperature.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return minimum temperature + return self.map_temperature_max_hass(device.min_temperature) + + @property + def max_temp(self): + """Return the maximum temperature.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return maximum temperature + return self.map_temperature_max_hass(device.max_temperature) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def current_temperature(self): + """Return the current temperature.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return current temperature + return self.map_temperature_max_hass(device.actual_temperature) + + @property + def current_operation(self): + """Return current operation (auto, manual, boost, vacation).""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Mode Mapping + return self.map_mode_max_hass(device.mode) + + @property + def operation_list(self): + """List of available operation modes.""" + return self._operation_list + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + # Get the device we want (does not do any IO, just reads from memory) + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Map and return target temperature + return self.map_temperature_max_hass(device.target_temperature) + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + # Fail is target temperature has not been supplied as argument + if kwargs.get(ATTR_TEMPERATURE) is None: + return False + + # Determine the new target temperature + target_temperature = kwargs.get(ATTR_TEMPERATURE) + + # Write the target temperature to the MAX! Cube. + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + cube = self._cubehandle.cube + + with self._cubehandle.mutex: + try: + cube.set_target_temperature(device, target_temperature) + except (socket.timeout, socket.error): + _LOGGER.error("Setting target temperature failed") + return False + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + # Get the device we want to update + device = self._cubehandle.cube.device_by_rf(self._rf_address) + + # Mode Mapping + mode = self.map_mode_hass_max(operation_mode) + + # Write new mode to thermostat + if mode is None: + return False + + with self._cubehandle.mutex: + try: + self._cubehandle.cube.set_mode(device, mode) + except (socket.timeout, socket.error): + _LOGGER.error("Setting operation mode failed") + return False + + def update(self): + """Get latest data from MAX! Cube.""" + # Update the CubeHandle + self._cubehandle.update() + + @staticmethod + def map_temperature_max_hass(temperature): + """Map Temperature from MAX! to HASS.""" + if temperature is None: + return STATE_UNKNOWN + + return temperature + + @staticmethod + def map_mode_hass_max(operation_mode): + """Map HASS Operation Modes to MAX! Operation Modes.""" + from maxcube.device import \ + MAX_DEVICE_MODE_AUTOMATIC, \ + MAX_DEVICE_MODE_MANUAL, \ + MAX_DEVICE_MODE_VACATION, \ + MAX_DEVICE_MODE_BOOST + + if operation_mode == STATE_AUTO: + mode = MAX_DEVICE_MODE_AUTOMATIC + elif operation_mode == STATE_MANUAL: + mode = MAX_DEVICE_MODE_MANUAL + elif operation_mode == STATE_VACATION: + mode = MAX_DEVICE_MODE_VACATION + elif operation_mode == STATE_BOOST: + mode = MAX_DEVICE_MODE_BOOST + else: + mode = None + + return mode + + @staticmethod + def map_mode_max_hass(mode): + """Map MAX! Operation Modes to HASS Operation Modes.""" + from maxcube.device import \ + MAX_DEVICE_MODE_AUTOMATIC, \ + MAX_DEVICE_MODE_MANUAL, \ + MAX_DEVICE_MODE_VACATION, \ + MAX_DEVICE_MODE_BOOST + + if mode == MAX_DEVICE_MODE_AUTOMATIC: + operation_mode = STATE_AUTO + elif mode == MAX_DEVICE_MODE_MANUAL: + operation_mode = STATE_MANUAL + elif mode == MAX_DEVICE_MODE_VACATION: + operation_mode = STATE_VACATION + elif mode == MAX_DEVICE_MODE_BOOST: + operation_mode = STATE_BOOST + else: + operation_mode = None + + return operation_mode diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py index fa4244497e679d..ffedcb8260288e 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/climate/vera.py @@ -7,14 +7,14 @@ import logging from homeassistant.util import convert -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT from homeassistant.const import ( TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE) from homeassistant.components.vera import ( - VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + VERA_CONTROLLER, VERA_DEVICES, VeraDevice) DEPENDENCIES = ['vera'] @@ -37,6 +37,7 @@ class VeraThermostat(VeraDevice, ClimateDevice): def __init__(self, vera_device, controller): """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller) + self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property def current_operation(self): diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index e069c5a1e1758e..660eb76098d345 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -11,6 +11,7 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components import zwave +from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE) @@ -32,19 +33,10 @@ } -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Z-Wave Climate devices.""" - if discovery_info is None or zwave.NETWORK is None: - _LOGGER.debug("No discovery_info=%s or no NETWORK=%s", - discovery_info, zwave.NETWORK) - return +def get_device(hass, value, **kwargs): + """Create zwave entity device.""" temp_unit = hass.config.units.temperature_unit - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - value.set_change_verified(False) - add_devices([ZWaveClimate(value, temp_unit)]) - _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", - discovery_info, zwave.NETWORK) + return ZWaveClimate(value, temp_unit) class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): @@ -224,7 +216,7 @@ def set_temperature(self, **kwargs): self.set_value( class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_SETPOINT, index=self._index, data=temperature) - self.update_ha_state() + self.schedule_update_ha_state() def set_fan_mode(self, fan): """Set new target fan mode.""" @@ -254,3 +246,8 @@ def device_state_attributes(self): if self._fan_state: data[ATTR_FAN_STATE] = self._fan_state return data + + @property + def dependent_value_ids(self): + """List of value IDs a device depends on.""" + return None diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 631650077ce401..ab175d1d56f46b 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.bootstrap import ( +from homeassistant.setup import ( async_prepare_setup_platform, ATTR_COMPONENT) from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.http import HomeAssistantView diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 97ddad74d79b55..6403e0bbc85786 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if value_template is not None: value_template.hass = hass - yield from async_add_devices([MqttCover( + async_add_devices([MqttCover( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index 429c330877c37e..826be580f93eb1 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -14,8 +14,8 @@ import homeassistant.helpers.config_validation as cv REQUIREMENTS = [ - 'https://github.com/arraylabs/pymyq/archive/v0.0.6.zip' - '#pymyq==0.0.6'] + 'https://github.com/arraylabs/pymyq/archive/v0.0.7.zip' + '#pymyq==0.0.7'] COVER_SCHEMA = vol.Schema({ vol.Required(CONF_TYPE): cv.string, diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py index 2c26fbf1723c92..48abe373eacc2d 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/cover/vera.py @@ -6,9 +6,9 @@ """ import logging -from homeassistant.components.cover import CoverDevice +from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT from homeassistant.components.vera import ( - VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + VERA_CONTROLLER, VERA_DEVICES, VeraDevice) DEPENDENCIES = ['vera'] @@ -28,6 +28,7 @@ class VeraCover(VeraDevice, CoverDevice): def __init__(self, vera_device, controller): """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller) + self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property def current_cover_position(self): diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index 2d995ca7acab29..46f23a68515d09 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -11,6 +11,7 @@ DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE) from homeassistant.components.zwave import ZWaveDeviceEntity from homeassistant.components import zwave +from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.components.zwave import workaround from homeassistant.components.cover import CoverDevice @@ -19,27 +20,15 @@ SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE -def setup_platform(hass, config, add_devices, discovery_info=None): - """Find and return Z-Wave covers.""" - if discovery_info is None or zwave.NETWORK is None: - return - - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - +def get_device(value, **kwargs): + """Create zwave entity device.""" if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.index == 0): - value.set_change_verified(False) - add_devices([ZwaveRollershutter(value)]) + return ZwaveRollershutter(value) elif (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or value.command_class == zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): - if (value.type != zwave.const.TYPE_BOOL and - value.genre != zwave.const.GENRE_USER): - return - value.set_change_verified(False) - add_devices([ZwaveGarageDoor(value)]) - else: - return + return ZwaveGarageDoor(value) + return None class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): @@ -52,6 +41,7 @@ def __init__(self, value): self._node = value.node self._open_id = None self._close_id = None + self._current_position_id = None self._current_position = None self._workaround = workaround.get_device_mapping(value) @@ -59,20 +49,35 @@ def __init__(self, value): _LOGGER.debug("Using workaround %s", self._workaround) self.update_properties() + @property + def dependent_value_ids(self): + """List of value IDs a device depends on.""" + if not self._node.is_ready: + return None + return [self._current_position_id] + def update_properties(self): """Callback on data changes for node values.""" # Position value - self._current_position = self.get_value( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, - label=['Level'], member='data') - self._open_id = self.get_value( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, - label=['Open', 'Up'], member='value_id') - self._close_id = self.get_value( - class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, - label=['Close', 'Down'], member='value_id') - if self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE: + if not self._node.is_ready: + if self._current_position_id is None: + self._current_position_id = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, + label=['Level'], member='value_id') + if self._open_id is None: + self._open_id = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, + label=['Open', 'Up'], member='value_id') + if self._close_id is None: + self._close_id = self.get_value( + class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL, + label=['Close', 'Down'], member='value_id') + if self._open_id and self._close_id and \ + self._workaround == workaround.WORKAROUND_REVERSE_OPEN_CLOSE: self._open_id, self._close_id = self._close_id, self._open_id + self._workaround = None + self._current_position = self._node.get_dimmer_level( + self._current_position_id) @property def is_closed(self): diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 170159e1d25c9d..e03cb72ea4466d 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -4,6 +4,7 @@ For more details about this component, please refer to the documentation https://home-assistant.io/components/demo/ """ +import asyncio import time import homeassistant.bootstrap as bootstrap @@ -34,7 +35,8 @@ ] -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup a demo environment.""" group = loader.get_component('group') configurator = loader.get_component('configurator') @@ -44,7 +46,7 @@ def setup(hass, config): config.setdefault(DOMAIN, {}) if config[DOMAIN].get('hide_demo_state') != 1: - hass.states.set('a.Demo_Mode', 'Enabled') + hass.states.async_set('a.Demo_Mode', 'Enabled') # Setup sun if not hass.config.latitude: @@ -53,50 +55,71 @@ def setup(hass, config): if not hass.config.longitude: hass.config.longitude = 117.22743 - bootstrap.setup_component(hass, 'sun') + tasks = [ + bootstrap.async_setup_component(hass, 'sun') + ] # Setup demo platforms demo_config = config.copy() for component in COMPONENTS_WITH_DEMO_PLATFORM: demo_config[component] = {CONF_PLATFORM: 'demo'} - bootstrap.setup_component(hass, component, demo_config) + tasks.append( + bootstrap.async_setup_component(hass, component, demo_config)) + + # Set up input select + tasks.append(bootstrap.async_setup_component( + hass, 'input_select', + {'input_select': + {'living_room_preset': {'options': ['Visitors', + 'Visitors with kids', + 'Home Alone']}, + 'who_cooks': {'icon': 'mdi:panda', + 'initial': 'Anne Therese', + 'name': 'Cook today', + 'options': ['Paulus', 'Anne Therese']}}})) + # Set up input boolean + tasks.append(bootstrap.async_setup_component( + hass, 'input_boolean', + {'input_boolean': {'notify': { + 'icon': 'mdi:car', + 'initial': False, + 'name': 'Notify Anne Therese is home'}}})) + + # Set up input boolean + tasks.append(bootstrap.async_setup_component( + hass, 'input_slider', + {'input_slider': { + 'noise_allowance': {'icon': 'mdi:bell-ring', + 'min': 0, + 'max': 10, + 'name': 'Allowed Noise', + 'unit_of_measurement': 'dB'}}})) + + # Set up weblink + tasks.append(bootstrap.async_setup_component( + hass, 'weblink', + {'weblink': {'entities': [{'name': 'Router', + 'url': 'http://192.168.1.1'}]}})) + + results = yield from asyncio.gather(*tasks, loop=hass.loop) + + if any(not result for result in results): + return False # Setup example persistent notification - persistent_notification.create( + persistent_notification.async_create( hass, 'This is an example of a persistent notification.', title='Example Notification') # Setup room groups - lights = sorted(hass.states.entity_ids('light')) - switches = sorted(hass.states.entity_ids('switch')) - media_players = sorted(hass.states.entity_ids('media_player')) + lights = sorted(hass.states.async_entity_ids('light')) + switches = sorted(hass.states.async_entity_ids('switch')) + media_players = sorted(hass.states.async_entity_ids('media_player')) - group.Group.create_group(hass, 'living room', [ - lights[1], switches[0], 'input_select.living_room_preset', - 'rollershutter.living_room_window', media_players[1], - 'scene.romantic_lights']) - group.Group.create_group(hass, 'bedroom', [ - lights[0], switches[1], media_players[0], - 'input_slider.noise_allowance']) - group.Group.create_group(hass, 'kitchen', [ - lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door']) - group.Group.create_group(hass, 'doors', [ - 'lock.front_door', 'lock.kitchen_door', - 'garage_door.right_garage_door', 'garage_door.left_garage_door']) - group.Group.create_group(hass, 'automations', [ - 'input_select.who_cooks', 'input_boolean.notify', ]) - group.Group.create_group(hass, 'people', [ - 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', - 'device_tracker.demo_paulus']) - group.Group.create_group(hass, 'downstairs', [ - 'group.living_room', 'group.kitchen', - 'scene.romantic_lights', 'rollershutter.kitchen_window', - 'rollershutter.living_room_window', 'group.doors', - 'thermostat.ecobee', - ], view=True) + tasks2 = [] # Setup scripts - bootstrap.setup_component( + tasks2.append(bootstrap.async_setup_component( hass, 'script', {'script': { 'demo': { @@ -115,10 +138,10 @@ def setup(hass, config): 'service': 'light.turn_off', 'data': {ATTR_ENTITY_ID: lights[0]} }] - }}}) + }}})) # Setup scenes - bootstrap.setup_component( + tasks2.append(bootstrap.async_setup_component( hass, 'scene', {'scene': [ {'name': 'Romantic lights', @@ -132,41 +155,37 @@ def setup(hass, config): switches[0]: True, switches[1]: False, }}, - ]}) + ]})) - # Set up input select - bootstrap.setup_component( - hass, 'input_select', - {'input_select': - {'living_room_preset': {'options': ['Visitors', - 'Visitors with kids', - 'Home Alone']}, - 'who_cooks': {'icon': 'mdi:panda', - 'initial': 'Anne Therese', - 'name': 'Cook today', - 'options': ['Paulus', 'Anne Therese']}}}) - # Set up input boolean - bootstrap.setup_component( - hass, 'input_boolean', - {'input_boolean': {'notify': {'icon': 'mdi:car', - 'initial': False, - 'name': 'Notify Anne Therese is home'}}}) + tasks2.append(group.Group.async_create_group(hass, 'living room', [ + lights[1], switches[0], 'input_select.living_room_preset', + 'rollershutter.living_room_window', media_players[1], + 'scene.romantic_lights'])) + tasks2.append(group.Group.async_create_group(hass, 'bedroom', [ + lights[0], switches[1], media_players[0], + 'input_slider.noise_allowance'])) + tasks2.append(group.Group.async_create_group(hass, 'kitchen', [ + lights[2], 'rollershutter.kitchen_window', 'lock.kitchen_door'])) + tasks2.append(group.Group.async_create_group(hass, 'doors', [ + 'lock.front_door', 'lock.kitchen_door', + 'garage_door.right_garage_door', 'garage_door.left_garage_door'])) + tasks2.append(group.Group.async_create_group(hass, 'automations', [ + 'input_select.who_cooks', 'input_boolean.notify', ])) + tasks2.append(group.Group.async_create_group(hass, 'people', [ + 'device_tracker.demo_anne_therese', 'device_tracker.demo_home_boy', + 'device_tracker.demo_paulus'])) + tasks2.append(group.Group.async_create_group(hass, 'downstairs', [ + 'group.living_room', 'group.kitchen', + 'scene.romantic_lights', 'rollershutter.kitchen_window', + 'rollershutter.living_room_window', 'group.doors', + 'thermostat.ecobee', + ], view=True)) - # Set up input boolean - bootstrap.setup_component( - hass, 'input_slider', - {'input_slider': { - 'noise_allowance': {'icon': 'mdi:bell-ring', - 'min': 0, - 'max': 10, - 'name': 'Allowed Noise', - 'unit_of_measurement': 'dB'}}}) + results = yield from asyncio.gather(*tasks2, loop=hass.loop) + + if any(not result for result in results): + return False - # Set up weblink - bootstrap.setup_component( - hass, 'weblink', - {'weblink': {'entities': [{'name': 'Router', - 'url': 'http://192.168.1.1'}]}}) # Setup configurator configurator_ids = [] @@ -184,14 +203,17 @@ def hue_configuration_callback(data): else: configurator.request_done(configurator_ids[0]) - request_id = configurator.request_config( - hass, "Philips Hue", hue_configuration_callback, - description=("Press the button on the bridge to register Philips Hue " - "with Home Assistant."), - description_image="/static/images/config_philips_hue.jpg", - submit_caption="I have pressed the button" - ) - - configurator_ids.append(request_id) + def setup_configurator(): + """Setup configurator.""" + request_id = configurator.request_config( + hass, "Philips Hue", hue_configuration_callback, + description=("Press the button on the bridge to register Philips " + "Hue with Home Assistant."), + description_image="/static/images/config_philips_hue.jpg", + submit_caption="I have pressed the button" + ) + configurator_ids.append(request_id) + + hass.async_add_job(setup_configurator) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 5aa9765d983ef9..3e04f46cb50eaa 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -14,12 +14,11 @@ import async_timeout import voluptuous as vol -from homeassistant.bootstrap import ( - async_prepare_setup_platform, async_log_exception) +from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.components import group, zone from homeassistant.components.discovery import SERVICE_NETGEAR -from homeassistant.config import load_yaml_config_file +from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_per_platform, discovery @@ -133,18 +132,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType): devices = yield from async_load_config(yaml_path, hass, consider_home) tracker = DeviceTracker(hass, consider_home, track_new, devices) - # added_to_hass - add_tasks = [device.async_added_to_hass() for device in devices - if device.track] - if add_tasks: - yield from asyncio.wait(add_tasks, loop=hass.loop) - - # update tracked devices - update_tasks = [device.async_update_ha_state() for device in devices - if device.track] - if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) - @asyncio.coroutine def async_setup_platform(p_type, p_config, disc_info=None): """Setup a device tracker platform.""" @@ -227,6 +214,8 @@ def async_see_service(call): hass.services.async_register( DOMAIN, SERVICE_SEE, async_see_service, descriptions.get(SERVICE_SEE)) + # restore + yield from tracker.async_setup_tracked_device() return True @@ -357,6 +346,27 @@ def async_update_stale(self, now: dt_util.dt.datetime): device.stale(now): self.hass.async_add_job(device.async_update_ha_state(True)) + @asyncio.coroutine + def async_setup_tracked_device(self): + """Setup all not exists tracked devices. + + This method is a coroutine. + """ + @asyncio.coroutine + def async_init_single_device(dev): + """Init a single device_tracker entity.""" + yield from dev.async_added_to_hass() + yield from dev.async_update_ha_state() + + tasks = [] + for device in self.devices.values(): + if device.track and not device.last_seen: + tasks.append(self.hass.async_add_job( + async_init_single_device(device))) + + if tasks: + yield from asyncio.wait(tasks, loop=self.hass.loop) + class Device(Entity): """Represent a tracked device.""" diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py index a4a933fe778d6c..7b7454d0a2832f 100644 --- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py @@ -110,7 +110,7 @@ def update_ble(now): _LOGGER.info("Discovered Bluetooth LE device %s", address) see_device(address, devs[address], new_device=True) - track_point_in_utc_time(hass, update_ble, now + interval) + track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) update_ble(dt_util.utcnow()) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 1de0629c7c5aa1..f71f8c4271a58e 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -82,7 +82,8 @@ def update_bluetooth(now): see_device((mac, result)) except bluetooth.BluetoothError: _LOGGER.exception('Error looking up bluetooth device!') - track_point_in_utc_time(hass, update_bluetooth, now + interval) + track_point_in_utc_time( + hass, update_bluetooth, dt_util.utcnow() + interval) update_bluetooth(dt_util.utcnow()) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index f6396ba7c34a2a..b6fd91212954aa 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -197,13 +197,22 @@ def reset_account_icloud(self): def icloud_trusted_device_callback(self, callback_data): """The trusted device is chosen.""" - self._trusted_device = int(callback_data.get('0', '0')) + self._trusted_device = int(callback_data.get('trusted_device')) self._trusted_device = self.api.trusted_devices[self._trusted_device] + + if not self.api.send_verification_code(self._trusted_device): + _LOGGER.error('Failed to send verification code') + self._trusted_device = None + return + if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = get_component('configurator') configurator.request_done(request_id) + # Trigger the next step immediately + self.icloud_need_verification_code() + def icloud_need_trusted_device(self): """We need a trusted device.""" configurator = get_component('configurator') @@ -213,7 +222,10 @@ def icloud_need_trusted_device(self): devicesstring = '' devices = self.api.trusted_devices for i, device in enumerate(devices): - devicesstring += "{}: {};".format(i, device.get('deviceName')) + devicename = device.get( + 'deviceName', + 'SMS to %s' % device.get('phoneNumber')) + devicesstring += "{}: {};".format(i, devicename) _CONFIGURING[self.accountname] = configurator.request_config( self.hass, 'iCloud {}'.format(self.accountname), @@ -223,12 +235,27 @@ def icloud_need_trusted_device(self): ' the index from this list: ' + devicesstring), entity_picture="/static/images/config_icloud.png", submit_caption='Confirm', - fields=[{'id': '0'}] + fields=[{'id': 'trusted_device', 'name': 'Trusted Device'}] ) def icloud_verification_callback(self, callback_data): """The trusted device is chosen.""" - self._verification_code = callback_data.get('0') + from pyicloud.exceptions import PyiCloudException + self._verification_code = callback_data.get('code') + + try: + if not self.api.validate_verification_code( + self._trusted_device, self._verification_code): + raise PyiCloudException('Unknown failure') + except PyiCloudException as error: + # Reset to the inital 2FA state to allow the user to retry + _LOGGER.error('Failed to verify verification code: %s', error) + self._trusted_device = None + self._verification_code = None + + # Trigger the next step immediately + self.icloud_need_trusted_device() + if self.accountname in _CONFIGURING: request_id = _CONFIGURING.pop(self.accountname) configurator = get_component('configurator') @@ -240,22 +267,17 @@ def icloud_need_verification_code(self): if self.accountname in _CONFIGURING: return - if self.api.send_verification_code(self._trusted_device): - self._verification_code = 'waiting' - _CONFIGURING[self.accountname] = configurator.request_config( self.hass, 'iCloud {}'.format(self.accountname), self.icloud_verification_callback, description=('Please enter the validation code:'), entity_picture="/static/images/config_icloud.png", submit_caption='Confirm', - fields=[{'code': '0'}] + fields=[{'id': 'code', 'name': 'code'}] ) def keep_alive(self, now): """Keep the api alive.""" - from pyicloud.exceptions import PyiCloud2FARequiredError - if self.api is None: self.reset_account_icloud() @@ -263,9 +285,8 @@ def keep_alive(self, now): return if self.api.requires_2fa: + from pyicloud.exceptions import PyiCloudException try: - self.api.authenticate() - except PyiCloud2FARequiredError: if self._trusted_device is None: self.icloud_need_trusted_device() return @@ -274,12 +295,14 @@ def keep_alive(self, now): self.icloud_need_verification_code() return - if self._verification_code == 'waiting': - return + self.api.authenticate() + if self.api.requires_2fa: + raise Exception('Unknown failure') - if self.api.validate_verification_code( - self._trusted_device, self._verification_code): - self._verification_code = None + self._trusted_device = None + self._verification_code = None + except PyiCloudException as error: + _LOGGER.error("Error setting up 2fa: %s", error) else: self.api.authenticate() @@ -397,13 +420,13 @@ def update_icloud(self, devicename=None): try: if devicename is not None: if devicename in self.devices: - self.devices[devicename].update_icloud() + self.devices[devicename].location() else: _LOGGER.error("devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME]) else: for device in self.devices: - self.devices[device].update_icloud() + self.devices[device].location() except PyiCloudNoDevicesException: _LOGGER.error('No iCloud Devices found!') diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py index a93263fada9ea5..1f7fa9c1b847f1 100644 --- a/homeassistant/components/device_tracker/mqtt.py +++ b/homeassistant/components/device_tracker/mqtt.py @@ -4,11 +4,13 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.mqtt/ """ +import asyncio import logging import voluptuous as vol import homeassistant.components.mqtt as mqtt +from homeassistant.core import callback from homeassistant.const import CONF_DEVICES from homeassistant.components.mqtt import CONF_QOS from homeassistant.components.device_tracker import PLATFORM_SCHEMA @@ -23,19 +25,23 @@ }) -def setup_scanner(hass, config, see, discovery_info=None): +@asyncio.coroutine +def async_setup_scanner(hass, config, async_see, discovery_info=None): """Setup the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] dev_id_lookup = {} - def device_tracker_message_received(topic, payload, qos): + @callback + def async_tracker_message_received(topic, payload, qos): """MQTT message received.""" - see(dev_id=dev_id_lookup[topic], location_name=payload) + hass.async_add_job( + async_see(dev_id=dev_id_lookup[topic], location_name=payload)) for dev_id, topic in devices.items(): dev_id_lookup[topic] = dev_id - mqtt.subscribe(hass, topic, device_tracker_message_received, qos) + yield from mqtt.async_subscribe( + hass, topic, async_tracker_message_received, qos) return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index c03041b63170aa..156e9d6a08aef6 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -4,14 +4,15 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.owntracks/ """ +import asyncio import json import logging -import threading import base64 from collections import defaultdict import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.components.mqtt as mqtt from homeassistant.const import STATE_HOME @@ -19,6 +20,7 @@ from homeassistant.components import zone as zone_comp from homeassistant.components.device_tracker import PLATFORM_SCHEMA +DEPENDENCIES = ['mqtt'] REQUIREMENTS = ['libnacl==1.5.0'] _LOGGER = logging.getLogger(__name__) @@ -30,16 +32,9 @@ CONF_WAYPOINT_IMPORT = 'waypoints' CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' -DEPENDENCIES = ['mqtt'] - EVENT_TOPIC = 'owntracks/+/+/event' LOCATION_TOPIC = 'owntracks/+/+' -LOCK = threading.Lock() - -MOBILE_BEACONS_ACTIVE = defaultdict(list) - -REGIONS_ENTERED = defaultdict(list) VALIDATE_LOCATION = 'location' VALIDATE_TRANSITION = 'transition' @@ -61,7 +56,10 @@ def get_cipher(): - """Return decryption function and length of key.""" + """Return decryption function and length of key. + + Async friendly. + """ from libnacl import crypto_secretbox_KEYBYTES as KEYLEN from libnacl.secret import SecretBox @@ -71,13 +69,17 @@ def decrypt(ciphertext, key): return (KEYLEN, decrypt) -def setup_scanner(hass, config, see, discovery_info=None): +@asyncio.coroutine +def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up an OwnTracks tracker.""" max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) waypoint_import = config.get(CONF_WAYPOINT_IMPORT) waypoint_whitelist = config.get(CONF_WAYPOINT_WHITELIST) secret = config.get(CONF_SECRET) + mobile_beacons_active = defaultdict(list) + regions_entered = defaultdict(list) + def decrypt_payload(topic, ciphertext): """Decrypt encrypted payload.""" try: @@ -154,7 +156,8 @@ def validate_payload(topic, payload, data_type): return data - def owntracks_location_update(topic, payload, qos): + @callback + def async_owntracks_location_update(topic, payload, qos): """MQTT message received.""" # Docs on available data: # http://owntracks.org/booklet/tech/json/#_typelocation @@ -164,18 +167,17 @@ def owntracks_location_update(topic, payload, qos): dev_id, kwargs = _parse_see_args(topic, data) - # Block updates if we're in a region - with LOCK: - if REGIONS_ENTERED[dev_id]: - _LOGGER.debug( - "location update ignored - inside region %s", - REGIONS_ENTERED[-1]) - return + if regions_entered[dev_id]: + _LOGGER.debug( + "location update ignored - inside region %s", + regions_entered[-1]) + return - see(**kwargs) - see_beacons(dev_id, kwargs) + hass.async_add_job(async_see(**kwargs)) + async_see_beacons(dev_id, kwargs) - def owntracks_event_update(topic, payload, qos): + @callback + def async_owntracks_event_update(topic, payload, qos): """MQTT event (geofences) received.""" # Docs on available data: # http://owntracks.org/booklet/tech/json/#_typetransition @@ -199,67 +201,65 @@ def owntracks_event_update(topic, payload, qos): def enter_event(): """Execute enter event.""" zone = hass.states.get("zone.{}".format(slugify(location))) - with LOCK: - if zone is None and data.get('t') == 'b': - # Not a HA zone, and a beacon so assume mobile - beacons = MOBILE_BEACONS_ACTIVE[dev_id] - if location not in beacons: - beacons.append(location) - _LOGGER.info("Added beacon %s", location) - else: - # Normal region - regions = REGIONS_ENTERED[dev_id] - if location not in regions: - regions.append(location) - _LOGGER.info("Enter region %s", location) - _set_gps_from_zone(kwargs, location, zone) - - see(**kwargs) - see_beacons(dev_id, kwargs) + if zone is None and data.get('t') == 'b': + # Not a HA zone, and a beacon so assume mobile + beacons = mobile_beacons_active[dev_id] + if location not in beacons: + beacons.append(location) + _LOGGER.info("Added beacon %s", location) + else: + # Normal region + regions = regions_entered[dev_id] + if location not in regions: + regions.append(location) + _LOGGER.info("Enter region %s", location) + _set_gps_from_zone(kwargs, location, zone) + + hass.async_add_job(async_see(**kwargs)) + async_see_beacons(dev_id, kwargs) def leave_event(): """Execute leave event.""" - with LOCK: - regions = REGIONS_ENTERED[dev_id] - if location in regions: - regions.remove(location) - new_region = regions[-1] if regions else None - - if new_region: - # Exit to previous region - zone = hass.states.get( - "zone.{}".format(slugify(new_region))) - _set_gps_from_zone(kwargs, new_region, zone) - _LOGGER.info("Exit to %s", new_region) - see(**kwargs) - see_beacons(dev_id, kwargs) - - else: - _LOGGER.info("Exit to GPS") - # Check for GPS accuracy - valid_gps = True - if 'acc' in data: - if data['acc'] == 0.0: - valid_gps = False - _LOGGER.warning( - 'Ignoring GPS in region exit because accuracy' - 'is zero: %s', - payload) - if (max_gps_accuracy is not None and - data['acc'] > max_gps_accuracy): - valid_gps = False - _LOGGER.info( - 'Ignoring GPS in region exit because expected ' - 'GPS accuracy %s is not met: %s', - max_gps_accuracy, payload) - if valid_gps: - see(**kwargs) - see_beacons(dev_id, kwargs) - - beacons = MOBILE_BEACONS_ACTIVE[dev_id] - if location in beacons: - beacons.remove(location) - _LOGGER.info("Remove beacon %s", location) + regions = regions_entered[dev_id] + if location in regions: + regions.remove(location) + new_region = regions[-1] if regions else None + + if new_region: + # Exit to previous region + zone = hass.states.get( + "zone.{}".format(slugify(new_region))) + _set_gps_from_zone(kwargs, new_region, zone) + _LOGGER.info("Exit to %s", new_region) + hass.async_add_job(async_see(**kwargs)) + async_see_beacons(dev_id, kwargs) + + else: + _LOGGER.info("Exit to GPS") + # Check for GPS accuracy + valid_gps = True + if 'acc' in data: + if data['acc'] == 0.0: + valid_gps = False + _LOGGER.warning( + 'Ignoring GPS in region exit because accuracy' + 'is zero: %s', + payload) + if (max_gps_accuracy is not None and + data['acc'] > max_gps_accuracy): + valid_gps = False + _LOGGER.info( + 'Ignoring GPS in region exit because expected ' + 'GPS accuracy %s is not met: %s', + max_gps_accuracy, payload) + if valid_gps: + hass.async_add_job(async_see(**kwargs)) + async_see_beacons(dev_id, kwargs) + + beacons = mobile_beacons_active[dev_id] + if location in beacons: + beacons.remove(location) + _LOGGER.info("Remove beacon %s", location) if data['event'] == 'enter': enter_event() @@ -271,7 +271,8 @@ def leave_event(): data['event']) return - def owntracks_waypoint_update(topic, payload, qos): + @callback + def async_owntracks_waypoint_update(topic, payload, qos): """List of waypoints published by a user.""" # Docs on available data: # http://owntracks.org/booklet/tech/json/#_typewaypoints @@ -298,36 +299,43 @@ def owntracks_waypoint_update(topic, payload, qos): zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad, zone_comp.ICON_IMPORT, False) zone.entity_id = entity_id - zone.update_ha_state() + hass.async_add_job(zone.async_update_ha_state()) - def see_beacons(dev_id, kwargs_param): + @callback + def async_see_beacons(dev_id, kwargs_param): """Set active beacons to the current location.""" kwargs = kwargs_param.copy() # the battery state applies to the tracking device, not the beacon kwargs.pop('battery', None) - for beacon in MOBILE_BEACONS_ACTIVE[dev_id]: + for beacon in mobile_beacons_active[dev_id]: kwargs['dev_id'] = "{}_{}".format(BEACON_DEV_ID, beacon) kwargs['host_name'] = beacon - see(**kwargs) + hass.async_add_job(async_see(**kwargs)) - mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) - mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + yield from mqtt.async_subscribe( + hass, LOCATION_TOPIC, async_owntracks_location_update, 1) + yield from mqtt.async_subscribe( + hass, EVENT_TOPIC, async_owntracks_event_update, 1) if waypoint_import: if waypoint_whitelist is None: - mqtt.subscribe(hass, WAYPOINT_TOPIC.format('+', '+'), - owntracks_waypoint_update, 1) + yield from mqtt.async_subscribe( + hass, WAYPOINT_TOPIC.format('+', '+'), + async_owntracks_waypoint_update, 1) else: for whitelist_user in waypoint_whitelist: - mqtt.subscribe(hass, WAYPOINT_TOPIC.format(whitelist_user, - '+'), - owntracks_waypoint_update, 1) + yield from mqtt.async_subscribe( + hass, WAYPOINT_TOPIC.format(whitelist_user, '+'), + async_owntracks_waypoint_update, 1) return True def parse_topic(topic, pretty=False): - """Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple.""" + """Parse an MQTT topic owntracks/user/dev, return (user, dev) tuple. + + Async friendly. + """ parts = topic.split('/') dev_id_format = '' if pretty: @@ -340,7 +348,10 @@ def parse_topic(topic, pretty=False): def _parse_see_args(topic, data): - """Parse the OwnTracks location parameters, into the format see expects.""" + """Parse the OwnTracks location parameters, into the format see expects. + + Async friendly. + """ (host_name, dev_id) = parse_topic(topic, False) kwargs = { 'dev_id': dev_id, @@ -355,7 +366,10 @@ def _parse_see_args(topic, data): def _set_gps_from_zone(kwargs, location, zone): - """Set the see parameters from the zone parameters.""" + """Set the see parameters from the zone parameters. + + Async friendly. + """ if zone is not None: kwargs['gps'] = ( zone.attributes['latitude'], diff --git a/homeassistant/components/device_tracker/ping.py b/homeassistant/components/device_tracker/ping.py index 2af400ba89cba8..04537dd6e4d0a2 100644 --- a/homeassistant/components/device_tracker/ping.py +++ b/homeassistant/components/device_tracker/ping.py @@ -86,7 +86,7 @@ def update(now): """Update all the hosts on every interval time.""" for host in hosts: host.update(see) - track_point_in_utc_time(hass, update, now + interval) + track_point_in_utc_time(hass, update, util.dt.utcnow() + interval) return True return update(util.dt.utcnow()) diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 4cbaa557517dc9..6e8b07e6babf94 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pysnmp==4.3.3'] +REQUIREMENTS = ['pysnmp==4.3.4'] CONF_COMMUNITY = 'community' CONF_AUTHKEY = 'authkey' diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index ca6e5d5ef7cb86..ca0bec297066a8 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -142,7 +142,7 @@ def _update_info(self): # Find devices that have geofencing enabled, and are currently at home. for mobile_device in tado_json: - if 'location' in mobile_device: + if mobile_device.get('location'): if mobile_device['location']['atHome']: device_id = mobile_device['id'] device_name = mobile_device['name'] diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index fa6668638f56a0..e46abda08d0ee3 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -13,13 +13,15 @@ from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_VERIFY_SSL # Unifi package doesn't list urllib3 as a requirement -REQUIREMENTS = ['urllib3', 'pyunifi==1.3'] +REQUIREMENTS = ['pyunifi==2.0'] _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' CONF_SITE_ID = 'site_id' +DEFAULT_VERIFY_SSL = True NOTIFICATION_ID = 'unifi_notification' NOTIFICATION_TITLE = 'Unifi Device Tracker Setup' @@ -29,7 +31,8 @@ vol.Optional(CONF_SITE_ID, default='default'): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PORT, default=8443): cv.port + vol.Required(CONF_PORT, default=8443): cv.port, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, }) @@ -42,10 +45,12 @@ def get_scanner(hass, config): password = config[DOMAIN].get(CONF_PASSWORD) site_id = config[DOMAIN].get(CONF_SITE_ID) port = config[DOMAIN].get(CONF_PORT) + verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL) persistent_notification = loader.get_component('persistent_notification') try: - ctrl = Controller(host, username, password, port, 'v4', site_id) + ctrl = Controller(host, username, password, port, version='v4', + site_id=site_id, ssl_verify=verify_ssl) except urllib.error.HTTPError as ex: _LOGGER.error('Failed to connect to Unifi: %s', ex) persistent_notification.create( diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 2d66f4e4b7c56c..26036342452275 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -19,7 +19,7 @@ from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==0.9.1'] +REQUIREMENTS = ['netdisco==0.9.2'] DOMAIN = 'discovery' @@ -46,6 +46,7 @@ 'yeelight': ('light', 'yeelight'), 'flux_led': ('light', 'flux_led'), 'apple_tv': ('media_player', 'apple_tv'), + 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), } diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 3463cc01bbc1c2..968f666fa72a14 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -78,7 +78,7 @@ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup MQTT fan platform.""" - yield from async_add_devices([MqttFan( + async_add_devices([MqttFan( config.get(CONF_NAME), { key: config.get(key) for key in ( diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index c98354662e2ad0..5b012ffad4ace8 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -14,6 +14,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.config import load_yaml_config_file +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, async_dispatcher_connect) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -26,6 +28,10 @@ SERVICE_STOP = 'stop' SERVICE_RESTART = 'restart' +SIGNAL_FFMPEG_START = 'ffmpeg.start' +SIGNAL_FFMPEG_STOP = 'ffmpeg.stop' +SIGNAL_FFMPEG_RESTART = 'ffmpeg.restart' + DATA_FFMPEG = 'ffmpeg' CONF_INITIAL_STATE = 'initial_state' @@ -50,22 +56,25 @@ }) -def start(hass, entity_id=None): +@callback +def async_start(hass, entity_id=None): """Start a ffmpeg process on entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_START, data) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_START, data)) -def stop(hass, entity_id=None): +@callback +def async_stop(hass, entity_id=None): """Stop a ffmpeg process on entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_STOP, data) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_STOP, data)) -def restart(hass, entity_id=None): +@callback +def async_restart(hass, entity_id=None): """Restart a ffmpeg process on entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_RESTART, data) + hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_RESTART, data)) @asyncio.coroutine @@ -89,30 +98,12 @@ def async_service_handle(service): """Handle service ffmpeg process.""" entity_ids = service.data.get(ATTR_ENTITY_ID) - if entity_ids: - devices = [device for device in manager.entities - if device.entity_id in entity_ids] + if service.service == SERVICE_START: + async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids) + elif service.service == SERVICE_STOP: + async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids) else: - devices = manager.entities - - tasks = [] - for device in devices: - if service.service == SERVICE_START: - tasks.append(device.async_start_ffmpeg()) - elif service.service == SERVICE_STOP: - tasks.append(device.async_stop_ffmpeg()) - else: - tasks.append(device.async_restart_ffmpeg()) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) - - tasks.clear() - for device in devices: - tasks.append(device.async_update_ha_state()) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids) hass.services.async_register( DOMAIN, SERVICE_START, async_service_handle, @@ -140,42 +131,12 @@ def __init__(self, hass, ffmpeg_bin, run_test): self._cache = {} self._bin = ffmpeg_bin self._run_test = run_test - self._entities = [] @property def binary(self): """Return ffmpeg binary from config.""" return self._bin - @property - def entities(self): - """Return ffmpeg entities for services.""" - return self._entities - - @callback - def async_register_device(self, device): - """Register a ffmpeg process/device.""" - self._entities.append(device) - - @asyncio.coroutine - def async_shutdown(event): - """Stop ffmpeg process.""" - yield from device.async_stop_ffmpeg() - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, async_shutdown) - - # start on startup - if device.initial_state: - @asyncio.coroutine - def async_start(event): - """Start ffmpeg process.""" - yield from device.async_start_ffmpeg() - yield from device.async_update_ha_state() - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_start) - @asyncio.coroutine def async_run_test(self, input_source): """Run test on this input. TRUE is deactivate or run correct. @@ -208,6 +169,22 @@ def __init__(self, initial_state=True): self.ffmpeg = None self.initial_state = initial_state + @asyncio.coroutine + def async_added_to_hass(self): + """Register dispatcher & events. + + This method is a coroutine. + """ + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_START, self._async_start_ffmpeg) + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_STOP, self._async_stop_ffmpeg) + async_dispatcher_connect( + self.hass, SIGNAL_FFMPEG_RESTART, self._async_restart_ffmpeg) + + # register start/stop + self._async_register_events() + @property def available(self): """Return True if entity is available.""" @@ -218,22 +195,53 @@ def should_poll(self): """Return True if entity has to be polled for state.""" return False - def async_start_ffmpeg(self): + @asyncio.coroutine + def _async_start_ffmpeg(self, entity_ids): """Start a ffmpeg process. - This method must be run in the event loop and returns a coroutine. + This method is a coroutine. """ raise NotImplementedError() - def async_stop_ffmpeg(self): + @asyncio.coroutine + def _async_stop_ffmpeg(self, entity_ids): """Stop a ffmpeg process. - This method must be run in the event loop and returns a coroutine. + This method is a coroutine. """ - return self.ffmpeg.close() + if entity_ids is None or self.entity_id in entity_ids: + yield from self.ffmpeg.close() @asyncio.coroutine - def async_restart_ffmpeg(self): - """Stop a ffmpeg process.""" - yield from self.async_stop_ffmpeg() - yield from self.async_start_ffmpeg() + def _async_restart_ffmpeg(self, entity_ids): + """Stop a ffmpeg process. + + This method is a coroutine. + """ + if entity_ids is None or self.entity_id in entity_ids: + yield from self._async_stop_ffmpeg(None) + yield from self._async_start_ffmpeg(None) + + @callback + def _async_register_events(self): + """Register a ffmpeg process/device.""" + @asyncio.coroutine + def async_shutdown_handle(event): + """Stop ffmpeg process.""" + yield from self._async_stop_ffmpeg(None) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_shutdown_handle) + + # start on startup + if not self.initial_state: + return + + @asyncio.coroutine + def async_start_handle(event): + """Start ffmpeg process.""" + yield from self._async_start_ffmpeg(None) + self.hass.async_add_job(self.async_update_ha_state()) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, async_start_handle) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 3a91e972a70d6d..b90dcfce4c1689 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,13 +3,13 @@ FINGERPRINTS = { "compatibility.js": "83d9c77748dafa9db49ae77d7f3d8fb0", "core.js": "1f7f88d8f5dada08bce1d935cfa5f33e", - "frontend.html": "ca9efa7e4506aa6b1a668703c8d0f800", - "mdi.html": "c1dde43ccf5667f687c418fc8daf9668", + "frontend.html": "418f6ef8354ce71f1b9594ee2068ebef", + "mdi.html": "65413cdf82f822bd6480e577852f0292", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-config.html": "412b3e24515ffa1ee8074ce974cf4057", "panels/ha-panel-dev-event.html": "91347dedf3b4fa9b49ccf4c0a28a03c4", "panels/ha-panel-dev-info.html": "61610e015a411cfc84edd2c4d489e71d", - "panels/ha-panel-dev-service.html": "a9247f255174b084fad2c04bdb9ec7a9", + "panels/ha-panel-dev-service.html": "153aad076f98bbd626466bac50986874", "panels/ha-panel-dev-state.html": "90f3bede9602241552ef7bb7958198c6", "panels/ha-panel-dev-template.html": "c249a4fc18a3a6994de3d6330cfe6cbb", "panels/ha-panel-history.html": "fdaa4d2402d49d4c8bd64a1708ab7a50", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 0de54ad7c77e73..83b90e4db607a3 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -529,7 +529,7 @@ this.hass.callService('cover', service, serviceData); }, }); -}()); \ No newline at end of file +}()); \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 396ea6fa8a1282..40657a51e08566 100644 Binary files a/homeassistant/components/frontend/www_static/frontend.html.gz and b/homeassistant/components/frontend/www_static/frontend.html.gz differ diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index e509ed07a08d35..de1b20b70a16ae 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit e509ed07a08d35152b9eea6e263411dfc027867b +Subproject commit de1b20b70a16aeb7c48a1b4867c97864c88adb1c diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index 438a682efafa7f..07fd6d9e02ec39 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/mdi.html.gz b/homeassistant/components/frontend/www_static/mdi.html.gz index 4c71babd599acb..e8350d32a91599 100644 Binary files a/homeassistant/components/frontend/www_static/mdi.html.gz and b/homeassistant/components/frontend/www_static/mdi.html.gz differ diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html index c56b1197952471..cb9b5c206c4ddd 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz index bd749bcb2a5675..bc0d2b540aa4c4 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-service.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 0bc47086c6bb33..44213ec679f173 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1 +1 @@ -"use strict";function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}function notificationEventCallback(e,t){firePushCallback({action:t.action,data:t.notification.data,tag:t.notification.tag,type:e},t.notification.data.jwt)}function firePushCallback(e,t){delete e.data.jwt,0===Object.keys(e.data).length&&e.data.constructor===Object&&delete e.data,fetch("/api/notify.html5/callback",{method:"POST",headers:new Headers({"Content-Type":"application/json",Authorization:"Bearer "+t}),body:JSON.stringify(e)})}var precacheConfig=[["/","d9d4e57acebbf537facdf2a5341015ea"],["/frontend/panels/dev-event-91347dedf3b4fa9b49ccf4c0a28a03c4.html","f74c44ab9bfbdc81badb56518ef8113d"],["/frontend/panels/dev-info-61610e015a411cfc84edd2c4d489e71d.html","6568377ee31cbd78fedc003b317f7faf"],["/frontend/panels/dev-service-a9247f255174b084fad2c04bdb9ec7a9.html","4d5f34f8ebc6c5fc4bdcff1ef7b4eb35"],["/frontend/panels/dev-state-90f3bede9602241552ef7bb7958198c6.html","277716ed9b76fa4313a1653dc757741b"],["/frontend/panels/dev-template-c249a4fc18a3a6994de3d6330cfe6cbb.html","8d7eaec6389ea1417cec667798740399"],["/frontend/panels/map-e10704a3469e44d1714eac9ed8e4b6a0.html","b9528c06194ad4b8b22e369fe4211500"],["/static/compatibility-83d9c77748dafa9db49ae77d7f3d8fb0.js","5f05c83be2b028d577962f9625904806"],["/static/core-1f7f88d8f5dada08bce1d935cfa5f33e.js","8a58624e6ea5958e817bf6cd5658e3a2"],["/static/frontend-ca9efa7e4506aa6b1a668703c8d0f800.html","a81ff9e9d93ba29530ef5ed4a401b35a"],["/static/mdi-c1dde43ccf5667f687c418fc8daf9668.html","6a3c9317736ca26e3390316335be9ba5"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"],["static/webcomponents-lite.min.js","32b5a9b7ada86304bec6b43d3f2194f0"]],cacheName="sw-precache-v2--"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},createCacheKey=function(e,t,a,n){var c=new URL(e);return n&&c.toString().match(n)||(c.search+=(c.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),c.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,t){var a=new URL(e);return a.search=a.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(e){return t.every(function(t){return!t.test(e[0])})}).map(function(e){return e.join("=")}).join("&"),a.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),c=createCacheKey(n,hashParamName,a,!1);return[n.toString(),c]}));self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(e){return setOfCachedUrls(e).then(function(t){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(a){if(!t.has(a))return e.add(new Request(a,{credentials:"same-origin",redirect:"follow"}))}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var t=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(e){return e.keys().then(function(a){return Promise.all(a.map(function(a){if(!t.has(a.url))return e.delete(a)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(e){if("GET"===e.request.method){var t,a=stripIgnoredUrlParameters(e.request.url,ignoreUrlParametersMatching);t=urlsToCacheKeys.has(a);var n="index.html";!t&&n&&(a=addDirectoryIndex(a,n),t=urlsToCacheKeys.has(a));var c="/";!t&&c&&"navigate"===e.request.mode&&isPathWhitelisted(["^((?!(static|api|local|service_worker.js|manifest.json)).)*$"],e.request.url)&&(a=new URL(c,self.location).toString(),t=urlsToCacheKeys.has(a)),t&&e.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(t){return console.warn('Couldn\'t serve response for "%s" from cache: %O',e.request.url,t),fetch(e.request)}))}}),self.addEventListener("push",function(e){var t;e.data&&(t=e.data.json(),e.waitUntil(self.registration.showNotification(t.title,t).then(function(e){firePushCallback({type:"received",tag:t.tag,data:t.data},t.data.jwt)})))}),self.addEventListener("notificationclick",function(e){var t;notificationEventCallback("clicked",e),e.notification.close(),e.notification.data&&e.notification.data.url&&(t=e.notification.data.url,t&&e.waitUntil(clients.matchAll({type:"window"}).then(function(e){var a,n;for(a=0;a{} and enter' + hass, 'In order to authorize Home-Assistant to view your calendars ' + 'you must visit: {} and enter ' 'code: {}'.format(dev_flow.verification_url, dev_flow.verification_url, dev_flow.user_code), @@ -223,7 +223,7 @@ def do_setup(hass, config): setup_services(hass, track_new_found_calendars, calendar_service) # Ensure component is loaded - bootstrap.setup_component(hass, 'calendar', config) + setup_component(hass, 'calendar', config) for calendar in hass.data[DATA_INDEX].values(): discovery.load_platform(hass, 'calendar', DOMAIN, calendar) diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 230e0e4567fa5e..f582ff33a07511 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -14,14 +14,13 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, - STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE) + STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE, SERVICE_RELOAD) from homeassistant.core import callback 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_callback_threadsafe, run_coroutine_threadsafe) +from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'group' @@ -43,7 +42,6 @@ vol.Required(ATTR_VISIBLE): cv.boolean }) -SERVICE_RELOAD = 'reload' RELOAD_SERVICE_SCHEMA = vol.Schema({}) _LOGGER = logging.getLogger(__name__) @@ -98,7 +96,7 @@ def is_on(hass, entity_id): def reload(hass): """Reload the automation from config.""" - hass.services.call(DOMAIN, SERVICE_RELOAD) + hass.add_job(async_reload, hass) @asyncio.coroutine @@ -365,7 +363,7 @@ def async_update_tracked_entity_ids(self, entity_ids): def start(self): """Start tracking members.""" - run_callback_threadsafe(self.hass.loop, self.async_start).result() + self.hass.add_job(self.async_start) @callback def async_start(self): @@ -396,17 +394,16 @@ def async_update(self): self._state = STATE_UNKNOWN self._async_update_group_state() - @asyncio.coroutine def async_remove(self): """Remove group from HASS. - This method must be run in the event loop. + This method must be run in the event loop and returns a coroutine. """ if self._async_unsub_state_changed: self._async_unsub_state_changed() self._async_unsub_state_changed = None - yield from super().async_remove() + return super().async_remove() @asyncio.coroutine def _async_state_changed_listener(self, entity_id, old_state, new_state): diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 254115c55b1858..5c68f767cd25a9 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -20,6 +20,7 @@ from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_HIDDEN +from homeassistant.components.recorder.util import session_scope, execute _LOGGER = logging.getLogger(__name__) @@ -34,19 +35,20 @@ IGNORE_DOMAINS = ('zone', 'scene',) -def last_recorder_run(): +def last_recorder_run(hass): """Retireve the last closed recorder run from the DB.""" - recorder.get_instance() - rec_runs = recorder.get_model('RecorderRuns') - with recorder.session_scope() as session: - res = recorder.query(rec_runs).order_by(rec_runs.end.desc()).first() + from homeassistant.components.recorder.models import RecorderRuns + + with session_scope(hass=hass) as session: + res = (session.query(RecorderRuns) + .order_by(RecorderRuns.end.desc()).first()) if res is None: return None session.expunge(res) return res -def get_significant_states(start_time, end_time=None, entity_id=None, +def get_significant_states(hass, start_time, end_time=None, entity_id=None, filters=None): """ Return states changes during UTC period start_time - end_time. @@ -55,50 +57,60 @@ def get_significant_states(start_time, end_time=None, entity_id=None, as well as all states from certain domains (for instance thermostat so that we get current temperature in our graphs). """ + from homeassistant.components.recorder.models import States + entity_ids = (entity_id.lower(), ) if entity_id is not None else None - states = recorder.get_model('States') - query = recorder.query(states).filter( - (states.domain.in_(SIGNIFICANT_DOMAINS) | - (states.last_changed == states.last_updated)) & - (states.last_updated > start_time)) - if filters: - query = filters.apply(query, entity_ids) - if end_time is not None: - query = query.filter(states.last_updated < end_time) + with session_scope(hass=hass) as session: + query = session.query(States).filter( + (States.domain.in_(SIGNIFICANT_DOMAINS) | + (States.last_changed == States.last_updated)) & + (States.last_updated > start_time)) + + if filters: + query = filters.apply(query, entity_ids) + + if end_time is not None: + query = query.filter(States.last_updated < end_time) - states = ( - state for state in recorder.execute( - query.order_by(states.entity_id, states.last_updated)) - if (_is_significant(state) and - not state.attributes.get(ATTR_HIDDEN, False))) + states = ( + state for state in execute( + query.order_by(States.entity_id, States.last_updated)) + if (_is_significant(state) and + not state.attributes.get(ATTR_HIDDEN, False))) - return states_to_json(states, start_time, entity_id, filters) + return states_to_json(hass, states, start_time, entity_id, filters) -def state_changes_during_period(start_time, end_time=None, entity_id=None): +def state_changes_during_period(hass, start_time, end_time=None, + entity_id=None): """Return states changes during UTC period start_time - end_time.""" - states = recorder.get_model('States') - query = recorder.query(states).filter( - (states.last_changed == states.last_updated) & - (states.last_changed > start_time)) + from homeassistant.components.recorder.models import States - if end_time is not None: - query = query.filter(states.last_updated < end_time) + with session_scope(hass=hass) as session: + query = session.query(States).filter( + (States.last_changed == States.last_updated) & + (States.last_changed > start_time)) - if entity_id is not None: - query = query.filter_by(entity_id=entity_id.lower()) + if end_time is not None: + query = query.filter(States.last_updated < end_time) - states = recorder.execute( - query.order_by(states.entity_id, states.last_updated)) + if entity_id is not None: + query = query.filter_by(entity_id=entity_id.lower()) - return states_to_json(states, start_time, entity_id) + states = execute( + query.order_by(States.entity_id, States.last_updated)) + return states_to_json(hass, states, start_time, entity_id) -def get_states(utc_point_in_time, entity_ids=None, run=None, filters=None): + +def get_states(hass, utc_point_in_time, entity_ids=None, run=None, + filters=None): """Return the states at a specific point in time.""" + from homeassistant.components.recorder.models import States + if run is None: - run = recorder.run_information(utc_point_in_time) + run = recorder.run_information(hass, utc_point_in_time) # History did not run before utc_point_in_time if run is None: @@ -106,29 +118,29 @@ def get_states(utc_point_in_time, entity_ids=None, run=None, filters=None): from sqlalchemy import and_, func - states = recorder.get_model('States') - most_recent_state_ids = recorder.query( - func.max(states.state_id).label('max_state_id') - ).filter( - (states.created >= run.start) & - (states.created < utc_point_in_time) & - (~states.domain.in_(IGNORE_DOMAINS))) - if filters: - most_recent_state_ids = filters.apply(most_recent_state_ids, - entity_ids) + with session_scope(hass=hass) as session: + most_recent_state_ids = session.query( + func.max(States.state_id).label('max_state_id') + ).filter( + (States.created >= run.start) & + (States.created < utc_point_in_time) & + (~States.domain.in_(IGNORE_DOMAINS))) - most_recent_state_ids = most_recent_state_ids.group_by( - states.entity_id).subquery() + if filters: + most_recent_state_ids = filters.apply(most_recent_state_ids, + entity_ids) - query = recorder.query(states).join(most_recent_state_ids, and_( - states.state_id == most_recent_state_ids.c.max_state_id)) + most_recent_state_ids = most_recent_state_ids.group_by( + States.entity_id).subquery() - for state in recorder.execute(query): - if not state.attributes.get(ATTR_HIDDEN, False): - yield state + query = session.query(States).join(most_recent_state_ids, and_( + States.state_id == most_recent_state_ids.c.max_state_id)) + return [state for state in execute(query) + if not state.attributes.get(ATTR_HIDDEN, False)] -def states_to_json(states, start_time, entity_id, filters=None): + +def states_to_json(hass, states, start_time, entity_id, filters=None): """Convert SQL results into JSON friendly data structure. This takes our state list and turns it into a JSON friendly data @@ -143,7 +155,7 @@ def states_to_json(states, start_time, entity_id, filters=None): entity_ids = [entity_id] if entity_id is not None else None # Get the states at the start time - for state in get_states(start_time, entity_ids, filters=filters): + for state in get_states(hass, start_time, entity_ids, filters=filters): state.last_changed = start_time state.last_updated = start_time result[state.entity_id].append(state) @@ -154,9 +166,9 @@ def states_to_json(states, start_time, entity_id, filters=None): return result -def get_state(utc_point_in_time, entity_id, run=None): +def get_state(hass, utc_point_in_time, entity_id, run=None): """Return a state at a specific point in time.""" - states = list(get_states(utc_point_in_time, (entity_id,), run)) + states = list(get_states(hass, utc_point_in_time, (entity_id,), run)) return states[0] if states else None @@ -173,7 +185,6 @@ def setup(hass, config): filters.included_entities = include[CONF_ENTITIES] filters.included_domains = include[CONF_DOMAINS] - recorder.get_instance() hass.http.register_view(HistoryPeriodView(filters)) register_built_in_panel(hass, 'history', 'History', 'mdi:poll-box') @@ -223,8 +234,8 @@ def get(self, request, datetime=None): entity_id = request.GET.get('filter_entity_id') result = yield from request.app['hass'].loop.run_in_executor( - None, get_significant_states, start_time, end_time, entity_id, - self.filters) + None, get_significant_states, request.app['hass'], start_time, + end_time, entity_id, self.filters) result = result.values() if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start @@ -254,41 +265,42 @@ def apply(self, query, entity_ids=None): * if include and exclude is defined - select the entities specified in the include and filter out the ones from the exclude list. """ - states = recorder.get_model('States') + from homeassistant.components.recorder.models import States + # specific entities requested - do not in/exclude anything if entity_ids is not None: - return query.filter(states.entity_id.in_(entity_ids)) - query = query.filter(~states.domain.in_(IGNORE_DOMAINS)) + return query.filter(States.entity_id.in_(entity_ids)) + query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) filter_query = None # filter if only excluded domain is configured if self.excluded_domains and not self.included_domains: - filter_query = ~states.domain.in_(self.excluded_domains) + filter_query = ~States.domain.in_(self.excluded_domains) if self.included_entities: - filter_query &= states.entity_id.in_(self.included_entities) + filter_query &= States.entity_id.in_(self.included_entities) # filter if only included domain is configured elif not self.excluded_domains and self.included_domains: - filter_query = states.domain.in_(self.included_domains) + filter_query = States.domain.in_(self.included_domains) if self.included_entities: - filter_query |= states.entity_id.in_(self.included_entities) + filter_query |= States.entity_id.in_(self.included_entities) # filter if included and excluded domain is configured elif self.excluded_domains and self.included_domains: - filter_query = ~states.domain.in_(self.excluded_domains) + filter_query = ~States.domain.in_(self.excluded_domains) if self.included_entities: - filter_query &= (states.domain.in_(self.included_domains) | - states.entity_id.in_(self.included_entities)) + filter_query &= (States.domain.in_(self.included_domains) | + States.entity_id.in_(self.included_entities)) else: - filter_query &= (states.domain.in_(self.included_domains) & ~ - states.domain.in_(self.excluded_domains)) + filter_query &= (States.domain.in_(self.included_domains) & ~ + States.domain.in_(self.excluded_domains)) # no domain filter just included entities elif not self.excluded_domains and not self.included_domains and \ self.included_entities: - filter_query = states.entity_id.in_(self.included_entities) + filter_query = States.entity_id.in_(self.included_entities) if filter_query is not None: query = query.filter(filter_query) # finally apply excluded entities filter if configured if self.excluded_entities: - query = query.filter(~states.entity_id.in_(self.excluded_entities)) + query = query.filter(~States.entity_id.in_(self.excluded_entities)) return query diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 96a32d1ae6eb9d..8ae18ef6e804cd 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -4,6 +4,7 @@ from datetime import datetime from ipaddress import ip_address import logging +import os from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized import voluptuous as vol @@ -115,13 +116,14 @@ def load_ip_bans_config(path: str): """Loading list of banned IPs from config file.""" ip_list = [] + if not os.path.isfile(path): + return ip_list + try: list_ = load_yaml_config_file(path) - except FileNotFoundError: - return [] except HomeAssistantError as err: _LOGGER.error('Unable to load %s: %s', path, str(err)) - return [] + return ip_list for ip_ban, ip_info in list_.items(): try: diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 43c5c9dd7f08db..bb1a7accd15f56 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -60,7 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera[CONF_ENTITY_ID], api, attributes, camera.get(CONF_NAME) )) - yield from async_add_devices(entities) + async_add_devices(entities) class MicrosoftFaceDetectEntity(ImageProcessingFaceEntity): diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 8d716bea0d57fb..ec4549dfe0ce74 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -54,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera.get(CONF_NAME) )) - yield from async_add_devices(entities) + async_add_devices(entities) class ImageProcessingFaceEntity(ImageProcessingEntity): @@ -108,8 +108,7 @@ def state_attributes(self): def process_faces(self, faces, total): """Send event with detected faces and store data.""" run_callback_threadsafe( - self.hass.loop, self.async_process_faces, faces, total - ).result() + self.hass.loop, self.async_process_faces, faces, total).result() @callback def async_process_faces(self, faces, total): diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py index 7c7d26ce724368..7f8bd83116cbe8 100644 --- a/homeassistant/components/image_processing/openalpr_cloud.py +++ b/homeassistant/components/image_processing/openalpr_cloud.py @@ -66,7 +66,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera[CONF_ENTITY_ID], params, confidence, camera.get(CONF_NAME) )) - yield from async_add_devices(entities) + async_add_devices(entities) class OpenAlprCloudEntity(ImageProcessingAlprEntity): diff --git a/homeassistant/components/image_processing/openalpr_local.py b/homeassistant/components/image_processing/openalpr_local.py index a9378dd653d49f..4040efe3bf4368 100644 --- a/homeassistant/components/image_processing/openalpr_local.py +++ b/homeassistant/components/image_processing/openalpr_local.py @@ -70,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): camera[CONF_ENTITY_ID], command, confidence, camera.get(CONF_NAME) )) - yield from async_add_devices(entities) + async_add_devices(entities) class ImageProcessingAlprEntity(ImageProcessingEntity): diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 5221679b6b5c36..7f233d09acc22b 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -85,7 +85,7 @@ def setup(hass, config): try: influx = InfluxDBClient(**kwargs) - influx.query("SELECT * FROM /.*/ LIMIT 1;") + influx.query("SHOW DIAGNOSTICS;") except exceptions.InfluxDBClientError as exc: _LOGGER.error("Database host is not accessible due to '%s', please " "check your entries in the configuration file and that " diff --git a/homeassistant/components/input_slider.py b/homeassistant/components/input_slider.py index d2453a97d14973..9e4faaf3d78ea6 100644 --- a/homeassistant/components/input_slider.py +++ b/homeassistant/components/input_slider.py @@ -14,6 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import async_get_last_state DOMAIN = 'input_slider' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -165,6 +166,18 @@ def state_attributes(self): ATTR_STEP: self._step } + @asyncio.coroutine + def async_added_to_hass(self): + """Called when entity about to be added to hass.""" + state = yield from async_get_last_state(self.hass, self.entity_id) + if not state: + return + + num_value = float(state.state) + if num_value < self._minimum or num_value > self._maximum: + return + self._current_value = num_value + @asyncio.coroutine def async_select_value(self, value): """Select new value.""" diff --git a/homeassistant/components/insteon_local.py b/homeassistant/components/insteon_local.py index c2007dd51f334c..cf93bc97e57ed7 100644 --- a/homeassistant/components/insteon_local.py +++ b/homeassistant/components/insteon_local.py @@ -13,7 +13,7 @@ CONF_PASSWORD, CONF_USERNAME, CONF_HOST, CONF_PORT, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['insteonlocal==0.39'] +REQUIREMENTS = ['insteonlocal==0.48'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index cbe7c7166e7e04..171c78a2fc86cd 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -231,7 +231,7 @@ def __init__(self, node) -> None: # pylint: disable=unused-argument def on_update(self, event: object) -> None: """Handle the update event from the ISY994 Node.""" - self.update_ha_state() + self.schedule_update_ha_state() @property def domain(self) -> str: diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 05002788207c4d..a5e4b87185e73c 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -24,8 +24,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import async_restore_state import homeassistant.util.color as color_util -from homeassistant.util.async import run_callback_threadsafe - DOMAIN = "light" SCAN_INTERVAL = timedelta(seconds=30) @@ -88,7 +86,7 @@ } # Service call validation schemas -VALID_TRANSITION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=900)) +VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) LIGHT_TURN_ON_SCHEMA = vol.Schema({ @@ -145,10 +143,10 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, rgb_color=None, xy_color=None, color_temp=None, white_value=None, profile=None, flash=None, effect=None, color_name=None): """Turn all or specified light on.""" - run_callback_threadsafe( - hass.loop, async_turn_on, hass, entity_id, transition, brightness, + hass.add_job( + async_turn_on, hass, entity_id, transition, brightness, rgb_color, xy_color, color_temp, white_value, - profile, flash, effect, color_name).result() + profile, flash, effect, color_name) @callback @@ -178,8 +176,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None, def turn_off(hass, entity_id=None, transition=None): """Turn all or specified light off.""" - run_callback_threadsafe( - hass.loop, async_turn_off, hass, entity_id, transition).result() + hass.add_job(async_turn_off, hass, entity_id, transition) @callback diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/light/enocean.py index e24aca4902ddf1..844cba1e6318fa 100644 --- a/homeassistant/components/light/enocean.py +++ b/homeassistant/components/light/enocean.py @@ -106,4 +106,4 @@ def value_changed(self, val): """Update the internal state of this device.""" self._brightness = math.floor(val / 100.0 * 256.0) self._on_state = bool(val != 0) - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index f64719a6529b12..f0f719fd15fd28 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -18,7 +18,7 @@ PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['flux_led==0.13'] +REQUIREMENTS = ['flux_led==0.15'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 778652872c32c2..1c0970f154c66a 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -47,9 +47,19 @@ PHUE_CONFIG_FILE = 'phue.conf' -SUPPORT_HUE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | - SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION | - SUPPORT_XY_COLOR) +SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_FLASH) +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_EXTENDED = (SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR) + +SUPPORT_HUE = { + 'Extended color light': SUPPORT_HUE_EXTENDED, + 'Color light': SUPPORT_HUE_COLOR, + 'Dimmable light': SUPPORT_HUE_DIMMABLE, + 'Color temperature light': SUPPORT_HUE_COLOR_TEMP + } CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue" DEFAULT_ALLOW_IN_EMULATED_HUE = True @@ -354,7 +364,7 @@ def is_on(self): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_HUE + return SUPPORT_HUE.get(self.info.get('type'), SUPPORT_HUE_EXTENDED) @property def effect_list(self): @@ -366,15 +376,30 @@ def turn_on(self, **kwargs): command = {'on': True} if ATTR_TRANSITION in kwargs: - command['transitiontime'] = kwargs[ATTR_TRANSITION] * 10 + command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) if ATTR_XY_COLOR in kwargs: - command['xy'] = kwargs[ATTR_XY_COLOR] + if self.info.get('manufacturername') == "OSRAM": + hsv = color_util.color_xy_brightness_to_hsv( + *kwargs[ATTR_XY_COLOR], + ibrightness=self.info['bri']) + command['hue'] = hsv[0] + command['sat'] = hsv[1] + command['bri'] = hsv[2] + else: + command['xy'] = kwargs[ATTR_XY_COLOR] elif ATTR_RGB_COLOR in kwargs: - xyb = color_util.color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - command['xy'] = xyb[0], xyb[1] - command['bri'] = xyb[2] + if self.info.get('manufacturername') == "OSRAM": + hsv = color_util.color_RGB_to_hsv( + *(int(val) for val in kwargs[ATTR_RGB_COLOR])) + command['hue'] = hsv[0] + command['sat'] = hsv[1] + command['bri'] = hsv[2] + else: + xyb = color_util.color_RGB_to_xy( + *(int(val) for val in kwargs[ATTR_RGB_COLOR])) + command['xy'] = xyb[0], xyb[1] + command['bri'] = xyb[2] if ATTR_BRIGHTNESS in kwargs: command['bri'] = kwargs[ATTR_BRIGHTNESS] @@ -401,7 +426,8 @@ def turn_on(self, **kwargs): command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) elif self.bridge_type == 'hue': - command['effect'] = 'none' + if self.info.get('manufacturername') != "OSRAM": + command['effect'] = 'none' self._command_func(self.light_id, command) @@ -410,9 +436,7 @@ def turn_off(self, **kwargs): command = {'on': False} if ATTR_TRANSITION in kwargs: - # Transition time is in 1/10th seconds and cannot exceed - # 900 seconds. - command['transitiontime'] = min(9000, kwargs[ATTR_TRANSITION] * 10) + command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) flash = kwargs.get(ATTR_FLASH) diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index c51c7d9d839040..865458eae24a25 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -152,6 +152,10 @@ def brightness(self): def update(self): """Update state of the light.""" resp = self.node.status(0) + + while 'error' in resp and resp['error'] is True: + resp = self.node.status(0) + if 'cmd2' in resp: self._value = int(resp['cmd2'], 16) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 0777396316ac4d..6b0c8a63f990e7 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -98,7 +98,7 @@ def on_device(self, ipaddr, name, power, hue, sat, bri, kel): ipaddr, name, power, hue, sat, bri, kel) bulb.set_power(power) bulb.set_color(hue, sat, bri, kel) - bulb.update_ha_state() + bulb.schedule_update_ha_state() def on_color(self, ipaddr, hue, sat, bri, kel): """Initialize the light.""" @@ -106,7 +106,7 @@ def on_color(self, ipaddr, hue, sat, bri, kel): if bulb is not None: bulb.set_color(hue, sat, bri, kel) - bulb.update_ha_state() + bulb.schedule_update_ha_state() def on_power(self, ipaddr, power): """Initialize the light.""" @@ -114,7 +114,7 @@ def on_power(self, ipaddr, power): if bulb is not None: bulb.set_power(power) - bulb.update_ha_state() + bulb.schedule_update_ha_state() # pylint: disable=unused-argument def poll(self, now): @@ -202,7 +202,7 @@ def supported_features(self): def turn_on(self, **kwargs): """Turn the device on.""" if ATTR_TRANSITION in kwargs: - fade = kwargs[ATTR_TRANSITION] * 1000 + fade = int(kwargs[ATTR_TRANSITION] * 1000) else: fade = 0 @@ -230,15 +230,17 @@ def turn_on(self, **kwargs): hue, saturation, brightness, kelvin, fade) if self._power == 0: + self._liffylights.set_color(self._ip, hue, saturation, + brightness, kelvin, 0) self._liffylights.set_power(self._ip, 65535, fade) - - self._liffylights.set_color(self._ip, hue, saturation, - brightness, kelvin, fade) + else: + self._liffylights.set_color(self._ip, hue, saturation, + brightness, kelvin, fade) def turn_off(self, **kwargs): """Turn the device off.""" if ATTR_TRANSITION in kwargs: - fade = kwargs[ATTR_TRANSITION] * 1000 + fade = int(kwargs[ATTR_TRANSITION] * 1000) else: fade = 0 diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index a395af30cf0bb0..23d0716e0b4e7f 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -17,7 +17,7 @@ SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['limitlessled==1.0.4'] +REQUIREMENTS = ['limitlessled==1.0.5'] _LOGGER = logging.getLogger(__name__) @@ -143,7 +143,7 @@ def wrapper(self, **kwargs): pipeline.on() # Set transition time. if ATTR_TRANSITION in kwargs: - transition_time = kwargs[ATTR_TRANSITION] + transition_time = int(kwargs[ATTR_TRANSITION]) # Do group type-specific work. function(self, transition_time, pipeline, **kwargs) # Update state. diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index 77b804cb499953..018d7a406391ab 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -12,83 +12,122 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, Light) + 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) from homeassistant.const import ( - CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, CONF_STATE, CONF_BRIGHTNESS, CONF_RGB, - CONF_COLOR_TEMP) + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, + CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( - CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) + CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] -CONF_STATE_VALUE_TEMPLATE = 'state_value_template' -CONF_BRIGHTNESS_STATE_TOPIC = 'brightness_state_topic' CONF_BRIGHTNESS_COMMAND_TOPIC = 'brightness_command_topic' -CONF_BRIGHTNESS_VALUE_TEMPLATE = 'brightness_value_template' -CONF_RGB_STATE_TOPIC = 'rgb_state_topic' -CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic' -CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template' CONF_BRIGHTNESS_SCALE = 'brightness_scale' -CONF_COLOR_TEMP_STATE_TOPIC = 'color_temp_state_topic' +CONF_BRIGHTNESS_STATE_TOPIC = 'brightness_state_topic' +CONF_BRIGHTNESS_VALUE_TEMPLATE = 'brightness_value_template' CONF_COLOR_TEMP_COMMAND_TOPIC = 'color_temp_command_topic' +CONF_COLOR_TEMP_STATE_TOPIC = 'color_temp_state_topic' CONF_COLOR_TEMP_VALUE_TEMPLATE = 'color_temp_value_template' +CONF_EFFECT_COMMAND_TOPIC = 'effect_command_topic' +CONF_EFFECT_LIST = 'effect_list' +CONF_EFFECT_STATE_TOPIC = 'effect_state_topic' +CONF_EFFECT_VALUE_TEMPLATE = 'effect_value_template' +CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic' +CONF_RGB_STATE_TOPIC = 'rgb_state_topic' +CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template' +CONF_STATE_VALUE_TEMPLATE = 'state_value_template' +CONF_XY_COMMAND_TOPIC = 'xy_command_topic' +CONF_XY_STATE_TOPIC = 'xy_state_topic' +CONF_XY_VALUE_TEMPLATE = 'xy_value_template' +CONF_WHITE_VALUE_COMMAND_TOPIC = 'white_value_command_topic' +CONF_WHITE_VALUE_SCALE = 'white_value_scale' +CONF_WHITE_VALUE_STATE_TOPIC = 'white_value_state_topic' +CONF_WHITE_VALUE_TEMPLATE = 'white_value_template' +DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = 'MQTT Light' -DEFAULT_PAYLOAD_ON = 'ON' -DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False -DEFAULT_BRIGHTNESS_SCALE = 255 +DEFAULT_PAYLOAD_OFF = 'OFF' +DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_WHITE_VALUE_SCALE = 255 PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, - 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, - vol.Optional(CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE): + vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE): vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_XY_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_XY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, }) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Add MQTT Light.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + config.setdefault( CONF_STATE_VALUE_TEMPLATE, config.get(CONF_VALUE_TEMPLATE)) - yield from async_add_devices([MqttLight( + async_add_devices([MqttLight( config.get(CONF_NAME), + config.get(CONF_EFFECT_LIST), { key: config.get(key) for key in ( - CONF_STATE_TOPIC, - CONF_COMMAND_TOPIC, - CONF_BRIGHTNESS_STATE_TOPIC, CONF_BRIGHTNESS_COMMAND_TOPIC, - CONF_RGB_STATE_TOPIC, - CONF_RGB_COMMAND_TOPIC, + CONF_BRIGHTNESS_STATE_TOPIC, + CONF_COLOR_TEMP_COMMAND_TOPIC, CONF_COLOR_TEMP_STATE_TOPIC, - CONF_COLOR_TEMP_COMMAND_TOPIC + CONF_COMMAND_TOPIC, + CONF_EFFECT_COMMAND_TOPIC, + CONF_EFFECT_STATE_TOPIC, + CONF_RGB_COMMAND_TOPIC, + CONF_RGB_STATE_TOPIC, + CONF_STATE_TOPIC, + CONF_WHITE_VALUE_COMMAND_TOPIC, + CONF_WHITE_VALUE_STATE_TOPIC, + CONF_XY_COMMAND_TOPIC, + CONF_XY_STATE_TOPIC, ) }, { - CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), CONF_BRIGHTNESS: config.get(CONF_BRIGHTNESS_VALUE_TEMPLATE), + CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE), + CONF_EFFECT: config.get(CONF_EFFECT_VALUE_TEMPLATE), CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE), - CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE) + CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + CONF_WHITE_VALUE: config.get(CONF_WHITE_VALUE_TEMPLATE), + CONF_XY: config.get(CONF_XY_VALUE_TEMPLATE), }, config.get(CONF_QOS), config.get(CONF_RETAIN), @@ -98,16 +137,19 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): }, config.get(CONF_OPTIMISTIC), config.get(CONF_BRIGHTNESS_SCALE), + config.get(CONF_WHITE_VALUE_SCALE), )]) class MqttLight(Light): """MQTT light.""" - def __init__(self, name, topic, templates, qos, retain, payload, - optimistic, brightness_scale): + def __init__(self, name, effect_list, topic, templates, qos, + retain, payload, optimistic, brightness_scale, + white_value_scale): """Initialize MQTT light.""" self._name = name + self._effect_list = effect_list self._topic = topic self._qos = qos self._retain = retain @@ -120,11 +162,21 @@ def __init__(self, name, topic, templates, qos, retain, payload, optimistic or topic[CONF_BRIGHTNESS_STATE_TOPIC] is None) self._optimistic_color_temp = ( optimistic or topic[CONF_COLOR_TEMP_STATE_TOPIC] is None) + self._optimistic_effect = ( + optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None) + self._optimistic_white_value = ( + optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None) + self._optimistic_xy = \ + optimistic or topic[CONF_XY_STATE_TOPIC] is None self._brightness_scale = brightness_scale + self._white_value_scale = white_value_scale self._state = False self._brightness = None self._rgb = 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) @@ -134,6 +186,14 @@ def __init__(self, name, topic, templates, qos, retain, payload, self._supported_features |= ( topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None and SUPPORT_COLOR_TEMP) + self._supported_features |= ( + topic[CONF_EFFECT_STATE_TOPIC] is not None and + SUPPORT_EFFECT) + self._supported_features |= ( + topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and + SUPPORT_WHITE_VALUE) + self._supported_features |= ( + topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_XY_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -215,6 +275,57 @@ def color_temp_received(topic, payload, qos): else: self._color_temp = None + @callback + def effect_received(topic, payload, qos): + """A new MQTT message for effect has been received.""" + self._effect = templates[CONF_EFFECT](payload) + self.hass.async_add_job(self.async_update_ha_state()) + + if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_EFFECT_STATE_TOPIC], + effect_received, self._qos) + self._effect = 'none' + if self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: + self._effect = 'none' + else: + self._effect = None + + @callback + def white_value_received(topic, payload, qos): + """A new MQTT message for the white value has been received.""" + device_value = float(templates[CONF_WHITE_VALUE](payload)) + percent_white = device_value / self._white_value_scale + self._white_value = int(percent_white * 255) + self.hass.async_add_job(self.async_update_ha_state()) + + if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None: + yield from mqtt.async_subscribe( + self.hass, self._topic[CONF_WHITE_VALUE_STATE_TOPIC], + white_value_received, self._qos) + self._white_value = 255 + elif self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: + self._white_value = 255 + else: + self._white_value = None + + @callback + def xy_received(topic, payload, qos): + """A new MQTT message has been received.""" + self._xy = [float(val) for val in + templates[CONF_XY](payload).split(',')] + self.hass.async_add_job(self.async_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] + if self._topic[CONF_XY_COMMAND_TOPIC] is not None: + self._xy = [1, 1] + else: + self._xy = None + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -230,6 +341,16 @@ def color_temp(self): """Return the color temperature in mired.""" return self._color_temp + @property + def white_value(self): + """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.""" @@ -250,6 +371,16 @@ def assumed_state(self): """Return true if we do optimistic updates.""" return self._optimistic + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self): + """Return the current effect.""" + return self._effect + @property def supported_features(self): """Flag supported features.""" @@ -297,6 +428,41 @@ def async_turn_on(self, **kwargs): self._color_temp = kwargs[ATTR_COLOR_TEMP] should_update = True + if ATTR_EFFECT in kwargs and \ + self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: + effect = kwargs[ATTR_EFFECT] + if effect in self._effect_list: + mqtt.async_publish( + self.hass, self._topic[CONF_EFFECT_COMMAND_TOPIC], + effect, self._qos, self._retain) + if self._optimistic_effect: + self._effect = kwargs[ATTR_EFFECT] + should_update = True + + if ATTR_WHITE_VALUE in kwargs and \ + self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: + percent_white = float(kwargs[ATTR_WHITE_VALUE]) / 255 + device_white_value = int(percent_white * self._white_value_scale) + mqtt.async_publish( + self.hass, self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC], + device_white_value, self._qos, self._retain) + + if self._optimistic_white_value: + 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 + 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 49c69ef348b461..969ac1ccddad0c 100755 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -12,11 +12,14 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, PLATFORM_SCHEMA, - ATTR_FLASH, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, + ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_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) from homeassistant.const import ( - CONF_NAME, CONF_OPTIMISTIC, CONF_BRIGHTNESS, CONF_RGB) + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, + CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) import homeassistant.helpers.config_validation as cv @@ -27,42 +30,53 @@ DEPENDENCIES = ['mqtt'] +DEFAULT_BRIGHTNESS = False +DEFAULT_COLOR_TEMP = False +DEFAULT_EFFECT = False +DEFAULT_FLASH_TIME_LONG = 10 +DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_NAME = 'MQTT JSON Light' DEFAULT_OPTIMISTIC = False -DEFAULT_BRIGHTNESS = False DEFAULT_RGB = False -DEFAULT_FLASH_TIME_SHORT = 2 -DEFAULT_FLASH_TIME_LONG = 10 +DEFAULT_WHITE_VALUE = False +DEFAULT_XY = False -CONF_FLASH_TIME_SHORT = 'flash_time_short' -CONF_FLASH_TIME_LONG = 'flash_time_long' +CONF_EFFECT_LIST = 'effect_list' -SUPPORT_MQTT_JSON = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION) +CONF_FLASH_TIME_LONG = 'flash_time_long' +CONF_FLASH_TIME_SHORT = 'flash_time_short' # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): - vol.All(vol.Coerce(int), vol.In([0, 1, 2])), - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, - vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, + vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, + vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, + vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_FLASH_TIME_SHORT, default=DEFAULT_FLASH_TIME_SHORT): cv.positive_int, vol.Optional(CONF_FLASH_TIME_LONG, default=DEFAULT_FLASH_TIME_LONG): - cv.positive_int + cv.positive_int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): + vol.All(vol.Coerce(int), vol.In([0, 1, 2])), + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, + vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, }) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MQTT JSON Light.""" - yield from async_add_devices([MqttJson( + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + async_add_devices([MqttJson( config.get(CONF_NAME), + config.get(CONF_EFFECT_LIST), { key: config.get(key) for key in ( CONF_STATE_TOPIC, @@ -73,7 +87,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_RETAIN), config.get(CONF_OPTIMISTIC), config.get(CONF_BRIGHTNESS), + config.get(CONF_COLOR_TEMP), + config.get(CONF_EFFECT), config.get(CONF_RGB), + config.get(CONF_WHITE_VALUE), + config.get(CONF_XY), { key: config.get(key) for key in ( CONF_FLASH_TIME_SHORT, @@ -86,10 +104,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttJson(Light): """Representation of a MQTT JSON light.""" - def __init__(self, name, topic, qos, retain, optimistic, brightness, rgb, + def __init__(self, name, effect_list, topic, qos, retain, optimistic, + brightness, color_temp, effect, rgb, white_value, xy, flash_times): """Initialize MQTT JSON light.""" self._name = name + self._effect_list = effect_list self._topic = topic self._qos = qos self._retain = retain @@ -100,13 +120,45 @@ def __init__(self, name, topic, qos, retain, optimistic, brightness, rgb, else: self._brightness = None + if color_temp: + self._color_temp = 150 + else: + self._color_temp = None + + if effect: + self._effect = 'none' + else: + self._effect = None + if rgb: self._rgb = [0, 0, 0] else: self._rgb = 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._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH) + self._supported_features |= (rgb is not None and SUPPORT_RGB_COLOR) + self._supported_features |= (brightness is not None and + SUPPORT_BRIGHTNESS) + self._supported_features |= (color_temp is not None and + SUPPORT_COLOR_TEMP) + self._supported_features |= (effect is not None and + SUPPORT_EFFECT) + self._supported_features |= (white_value is not None and + SUPPORT_WHITE_VALUE) + self._supported_features |= (xy is not None and SUPPORT_XY_COLOR) + @asyncio.coroutine def async_added_to_hass(self): """Subscribe mqtt events. @@ -133,7 +185,7 @@ def state_received(topic, payload, qos): except KeyError: pass except ValueError: - _LOGGER.warning("Invalid color value received") + _LOGGER.warning("Invalid RGB color value received") if self._brightness is not None: try: @@ -143,6 +195,41 @@ def state_received(topic, payload, qos): except ValueError: _LOGGER.warning('Invalid brightness value received') + if self._color_temp is not None: + try: + self._color_temp = int(values['color_temp']) + except KeyError: + pass + except ValueError: + _LOGGER.warning('Invalid color temp value received') + + if self._effect is not None: + try: + self._effect = values['effect'] + except KeyError: + pass + except ValueError: + _LOGGER.warning('Invalid effect value received') + + if self._white_value is not None: + try: + self._white_value = int(values['white_value']) + except KeyError: + pass + except ValueError: + _LOGGER.warning('Invalid white value 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.hass.async_add_job(self.async_update_ha_state()) if self._topic[CONF_STATE_TOPIC] is not None: @@ -155,11 +242,36 @@ def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def color_temp(self): + """Return the color temperature in mired.""" + return self._color_temp + + @property + def effect(self): + """Return the current effect.""" + return self._effect + + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + @property def rgb_color(self): """Return the RGB color value.""" return self._rgb + @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.""" @@ -183,7 +295,7 @@ def assumed_state(self): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_MQTT_JSON + return self._supported_features @asyncio.coroutine def async_turn_on(self, **kwargs): @@ -215,7 +327,7 @@ def async_turn_on(self, **kwargs): message['flash'] = self._flash_times[CONF_FLASH_TIME_SHORT] if ATTR_TRANSITION in kwargs: - message['transition'] = kwargs[ATTR_TRANSITION] + message['transition'] = int(kwargs[ATTR_TRANSITION]) if ATTR_BRIGHTNESS in kwargs: message['brightness'] = int(kwargs[ATTR_BRIGHTNESS]) @@ -224,6 +336,37 @@ def async_turn_on(self, **kwargs): self._brightness = kwargs[ATTR_BRIGHTNESS] should_update = True + if ATTR_COLOR_TEMP in kwargs: + message['color_temp'] = int(kwargs[ATTR_COLOR_TEMP]) + + if self._optimistic: + self._color_temp = kwargs[ATTR_COLOR_TEMP] + should_update = True + + if ATTR_EFFECT in kwargs: + message['effect'] = kwargs[ATTR_EFFECT] + + if self._optimistic: + self._effect = kwargs[ATTR_EFFECT] + should_update = True + + if ATTR_WHITE_VALUE in kwargs: + message['white_value'] = int(kwargs[ATTR_WHITE_VALUE]) + + if self._optimistic: + 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) @@ -245,7 +388,7 @@ def async_turn_off(self, **kwargs): message = {'state': 'OFF'} if ATTR_TRANSITION in kwargs: - message['transition'] = kwargs[ATTR_TRANSITION] + message['transition'] = int(kwargs[ATTR_TRANSITION]) mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index d99db968315133..fa0d71f46ef708 100755 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -11,9 +11,10 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, + ATTR_RGB_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) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) @@ -28,43 +29,47 @@ DEFAULT_NAME = 'MQTT Template Light' DEFAULT_OPTIMISTIC = False -CONF_EFFECT_LIST = "effect_list" -CONF_COMMAND_ON_TEMPLATE = 'command_on_template' -CONF_COMMAND_OFF_TEMPLATE = 'command_off_template' -CONF_STATE_TEMPLATE = 'state_template' -CONF_BRIGHTNESS_TEMPLATE = 'brightness_template' -CONF_RED_TEMPLATE = 'red_template' -CONF_GREEN_TEMPLATE = 'green_template' CONF_BLUE_TEMPLATE = 'blue_template' +CONF_BRIGHTNESS_TEMPLATE = 'brightness_template' +CONF_COLOR_TEMP_TEMPLATE = 'color_temp_template' +CONF_COMMAND_OFF_TEMPLATE = 'command_off_template' +CONF_COMMAND_ON_TEMPLATE = 'command_on_template' +CONF_EFFECT_LIST = 'effect_list' CONF_EFFECT_TEMPLATE = 'effect_template' - -SUPPORT_MQTT_TEMPLATE = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | - SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) +CONF_GREEN_TEMPLATE = 'green_template' +CONF_RED_TEMPLATE = 'red_template' +CONF_STATE_TEMPLATE = 'state_template' +CONF_WHITE_VALUE_TEMPLATE = 'white_value_template' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, - vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, - vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, - vol.Optional(CONF_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, - vol.Optional(CONF_RED_TEMPLATE): cv.template, - vol.Optional(CONF_GREEN_TEMPLATE): cv.template, vol.Optional(CONF_BLUE_TEMPLATE): cv.template, + vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, + vol.Optional(CONF_COLOR_TEMP_TEMPLATE): cv.template, + vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, + vol.Optional(CONF_GREEN_TEMPLATE): cv.template, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_RED_TEMPLATE): cv.template, + vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, + vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, + vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, + vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All(vol.Coerce(int), vol.In([0, 1, 2])), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean }) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MQTT Template light.""" - yield from async_add_devices([MqttTemplate( + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + + async_add_devices([MqttTemplate( hass, config.get(CONF_NAME), config.get(CONF_EFFECT_LIST), @@ -76,14 +81,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): }, { key: config.get(key) for key in ( - CONF_COMMAND_ON_TEMPLATE, - CONF_COMMAND_OFF_TEMPLATE, - CONF_STATE_TEMPLATE, + CONF_BLUE_TEMPLATE, CONF_BRIGHTNESS_TEMPLATE, - CONF_RED_TEMPLATE, + CONF_COLOR_TEMP_TEMPLATE, + CONF_COMMAND_OFF_TEMPLATE, + CONF_COMMAND_ON_TEMPLATE, + CONF_EFFECT_TEMPLATE, CONF_GREEN_TEMPLATE, - CONF_BLUE_TEMPLATE, - CONF_EFFECT_TEMPLATE + CONF_RED_TEMPLATE, + CONF_STATE_TEMPLATE, + CONF_WHITE_VALUE_TEMPLATE, ) }, config.get(CONF_OPTIMISTIC), @@ -114,6 +121,16 @@ def __init__(self, hass, name, effect_list, topics, templates, optimistic, else: self._brightness = None + if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: + self._color_temp = 255 + else: + self._color_temp = None + + if self._templates[CONF_WHITE_VALUE_TEMPLATE] is not None: + self._white_value = 255 + else: + self._white_value = None + 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): @@ -156,6 +173,16 @@ def state_received(topic, payload, qos): except ValueError: _LOGGER.warning('Invalid brightness value received') + # read color temperature + if self._color_temp is not None: + try: + self._color_temp = int( + self._templates[CONF_COLOR_TEMP_TEMPLATE]. + async_render_with_possible_json_value(payload) + ) + except ValueError: + _LOGGER.warning('Invalid color temperature value received') + # read color if self._rgb is not None: try: @@ -171,6 +198,16 @@ def state_received(topic, payload, qos): except ValueError: _LOGGER.warning('Invalid color value received') + # read white value + if self._white_value is not None: + try: + self._white_value = int( + self._templates[CONF_WHITE_VALUE_TEMPLATE]. + async_render_with_possible_json_value(payload) + ) + except ValueError: + _LOGGER.warning('Invalid white value received') + # read effect if self._templates[CONF_EFFECT_TEMPLATE] is not None: effect = self._templates[CONF_EFFECT_TEMPLATE].\ @@ -194,11 +231,21 @@ def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def color_temp(self): + """Return the color temperature in mired.""" + return self._color_temp + @property def rgb_color(self): """Return the RGB color value [int, int, int].""" return self._rgb + @property + def white_value(self): + """Return the white property.""" + return self._white_value + @property def should_poll(self): """Return True if entity has to be polled for state. @@ -250,6 +297,13 @@ def async_turn_on(self, **kwargs): if self._optimistic: self._brightness = kwargs[ATTR_BRIGHTNESS] + # color_temp + if ATTR_COLOR_TEMP in kwargs: + values['color_temp'] = int(kwargs[ATTR_COLOR_TEMP]) + + if self._optimistic: + self._color_temp = kwargs[ATTR_COLOR_TEMP] + # color if ATTR_RGB_COLOR in kwargs: values['red'] = kwargs[ATTR_RGB_COLOR][0] @@ -259,6 +313,13 @@ def async_turn_on(self, **kwargs): if self._optimistic: self._rgb = kwargs[ATTR_RGB_COLOR] + # white value + if ATTR_WHITE_VALUE in kwargs: + values['white_value'] = int(kwargs[ATTR_WHITE_VALUE]) + + if self._optimistic: + self._white_value = kwargs[ATTR_WHITE_VALUE] + # effect if ATTR_EFFECT in kwargs: values['effect'] = kwargs.get(ATTR_EFFECT) @@ -269,7 +330,7 @@ def async_turn_on(self, **kwargs): # transition if ATTR_TRANSITION in kwargs: - values['transition'] = kwargs[ATTR_TRANSITION] + values['transition'] = int(kwargs[ATTR_TRANSITION]) mqtt.async_publish( self.hass, self._topics[CONF_COMMAND_TOPIC], @@ -293,7 +354,7 @@ def async_turn_off(self, **kwargs): # transition if ATTR_TRANSITION in kwargs: - values['transition'] = kwargs[ATTR_TRANSITION] + values['transition'] = int(kwargs[ATTR_TRANSITION]) mqtt.async_publish( self.hass, self._topics[CONF_COMMAND_TOPIC], @@ -307,12 +368,16 @@ def async_turn_off(self, **kwargs): @property def supported_features(self): """Flag supported features.""" - features = 0 + 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._effect_list is not None: features = features | SUPPORT_EFFECT + if self._color_temp is not None: + features = features | SUPPORT_COLOR_TEMP + if self._white_value is not None: + features = features | SUPPORT_WHITE_VALUE return features diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index b4c593d83959bc..b460ff52a9b0fb 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -17,6 +17,8 @@ Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, ATTR_TRANSITION, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/tfriedel/python-lightify/archive/' @@ -24,10 +26,6 @@ _LOGGER = logging.getLogger(__name__) -TEMP_MIN = 2000 # lightify minimum temperature -TEMP_MAX = 6500 # lightify maximum temperature -TEMP_MIN_HASS = 154 # home assistant minimum temperature -TEMP_MAX_HASS = 500 # home assistant maximum temperature MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -93,10 +91,10 @@ def __init__(self, light_id, light, update_lights): self._light = light self._light_id = light_id self.update_lights = update_lights - self._brightness = 0 - self._rgb = (0, 0, 0) - self._name = "" - self._temperature = TEMP_MIN + self._brightness = None + self._rgb = None + self._name = None + self._temperature = None self._state = False self.update() @@ -145,7 +143,7 @@ def turn_on(self, **kwargs): self._state = self._light.on() if ATTR_TRANSITION in kwargs: - transition = kwargs[ATTR_TRANSITION] * 10 + transition = int(kwargs[ATTR_TRANSITION] * 10) _LOGGER.debug("turn_on requested transition time for light:" " %s is: %s ", self._name, transition) @@ -164,8 +162,7 @@ def turn_on(self, **kwargs): if ATTR_COLOR_TEMP in kwargs: color_t = kwargs[ATTR_COLOR_TEMP] - kelvin = int(((TEMP_MAX - TEMP_MIN) * (color_t - TEMP_MIN_HASS) / - (TEMP_MAX_HASS - TEMP_MIN_HASS)) + TEMP_MIN) + kelvin = int(color_temperature_mired_to_kelvin(color_t)) _LOGGER.debug("turn_on requested set_temperature for light:" " %s: %s ", self._name, kelvin) self._light.set_temperature(kelvin, transition) @@ -196,7 +193,7 @@ def turn_off(self, **kwargs): _LOGGER.debug("turn_off Attempting to turn off light: %s ", self._name) if ATTR_TRANSITION in kwargs: - transition = kwargs[ATTR_TRANSITION] * 10 + transition = int(kwargs[ATTR_TRANSITION] * 10) _LOGGER.debug("turn_off requested transition time for light:" " %s is: %s ", self._name, transition) @@ -218,6 +215,5 @@ def update(self): self._name = self._light.name() self._rgb = self._light.rgb() o_temp = self._light.temp() - self._temperature = int(TEMP_MIN_HASS + (TEMP_MAX_HASS - TEMP_MIN_HASS) - * (o_temp - TEMP_MIN) / (TEMP_MAX - TEMP_MIN)) + self._temperature = color_temperature_kelvin_to_mired(o_temp) self._state = self._light.on() diff --git a/homeassistant/components/light/rflink.py b/homeassistant/components/light/rflink.py index 82b7b46b1f86e9..4d49186398aeae 100644 --- a/homeassistant/components/light/rflink.py +++ b/homeassistant/components/light/rflink.py @@ -117,7 +117,7 @@ def devices_from_config(domain_config, hass=None): @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Rflink light platform.""" - yield from async_add_devices(devices_from_config(config, hass)) + async_add_devices(devices_from_config(config, hass)) # Add new (unconfigured) devices to user desired group if config[CONF_NEW_DEVICES_GROUP]: @@ -136,7 +136,7 @@ def add_new_device(event): device_config = config[CONF_DEVICE_DEFAULTS] device = entity_class(device_id, hass, **device_config) - yield from async_add_devices([device]) + async_add_devices([device]) # Register entity to listen to incoming Rflink events hass.data[DATA_ENTITY_LOOKUP][ @@ -156,7 +156,10 @@ def add_new_device(event): class RflinkLight(SwitchableRflinkDevice, Light): """Representation of a Rflink light.""" - pass + @property + def entity_id(self): + """Return entity id.""" + return "light.{}".format(self.name) class DimmableRflinkLight(SwitchableRflinkDevice, Light): @@ -164,6 +167,11 @@ class DimmableRflinkLight(SwitchableRflinkDevice, Light): _brightness = 255 + @property + def entity_id(self): + """Return entity id.""" + return "light.{}".format(self.name) + @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on.""" @@ -202,6 +210,11 @@ class HybridRflinkLight(SwitchableRflinkDevice, Light): _brightness = 255 + @property + def entity_id(self): + """Return entity id.""" + return "light.{}".format(self.name) + @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on and set dim level.""" diff --git a/homeassistant/components/light/scsgate.py b/homeassistant/components/light/scsgate.py index 7445977c4f38c9..532dc67562fbb6 100644 --- a/homeassistant/components/light/scsgate.py +++ b/homeassistant/components/light/scsgate.py @@ -106,7 +106,7 @@ def process_event(self, message): return self._toggled = message.toggled - self.update_ha_state() + self.schedule_update_ha_state() command = "off" if self._toggled: diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 0508e654f432cf..ac459ce38ff2e9 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -7,10 +7,10 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) + ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS) from homeassistant.const import (STATE_OFF, STATE_ON) from homeassistant.components.vera import ( - VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + VERA_CONTROLLER, VERA_DEVICES, VeraDevice) _LOGGER = logging.getLogger(__name__) @@ -33,6 +33,7 @@ def __init__(self, vera_device, controller): """Initialize the light.""" self._state = False VeraDevice.__init__(self, vera_device, controller) + self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) @property def brightness(self): diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 5eae4c66bb67bb..7e0bd0e253ee5d 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -259,7 +259,7 @@ def set_flash(self, flash) -> None: _LOGGER.error("Flash supported currently only in RGB mode.") return - transition = self.config[CONF_TRANSITION] + transition = int(self.config[CONF_TRANSITION]) if flash == FLASH_LONG: count = 1 duration = transition * 5 @@ -288,9 +288,9 @@ def turn_on(self, **kwargs) -> None: rgb = kwargs.get(ATTR_RGB_COLOR) flash = kwargs.get(ATTR_FLASH) - duration = self.config[CONF_TRANSITION] # in ms + duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config - duration = kwargs.get(ATTR_TRANSITION) * 1000 # kwarg in s + duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s self._bulb.turn_on(duration=duration) diff --git a/homeassistant/components/light/yeelightsunflower.py b/homeassistant/components/light/yeelightsunflower.py index ead00d97f64769..d1b2bcdab8ec5c 100644 --- a/homeassistant/components/light/yeelightsunflower.py +++ b/homeassistant/components/light/yeelightsunflower.py @@ -2,9 +2,7 @@ Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi). For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/light.yeelight-sunflower -Uses the yeelightsunflower library: -https://github.com/lindsaymarkward/python-yeelight-sunflower +https://home-assistant.io/components/light.yeelightsunflower """ import logging import voluptuous as vol @@ -17,34 +15,29 @@ from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['yeelightsunflower==0.0.6'] -SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +REQUIREMENTS = ['yeelightsunflower==0.0.8'] _LOGGER = logging.getLogger(__name__) -# Validate the user's configuration + +SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Yeelight Sunflower Light platform.""" + """Set up the Yeelight Sunflower Light platform.""" import yeelightsunflower - # Assign configuration variables. - # The configuration check takes care they are present. host = config.get(CONF_HOST) - - # Setup connection with Yeelight Sunflower hub hub = yeelightsunflower.Hub(host) - # Verify that hub is responsive if not hub.available: _LOGGER.error('Could not connect to Yeelight Sunflower hub') return False - # Add devices add_devices(SunflowerBulb(light) for light in hub.get_lights()) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 0c5cf1d081e87b..59df6d8a745270 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -13,6 +13,7 @@ ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \ SUPPORT_RGB_COLOR, 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 HASS_COLOR_MAX, HASS_COLOR_MIN, \ color_temperature_mired_to_kelvin, color_temperature_to_rgb, \ @@ -48,38 +49,25 @@ | SUPPORT_COLOR_TEMP) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Find and add Z-Wave lights.""" - if discovery_info is None or zwave.NETWORK is None: - return - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] +def get_device(node, value, node_config, **kwargs): + """Create zwave entity device.""" name = '{}.{}'.format(DOMAIN, zwave.object_id(value)) - node_config = hass.data[zwave.DATA_DEVICE_CONFIG].get(name) refresh = node_config.get(zwave.CONF_REFRESH_VALUE) delay = node_config.get(zwave.CONF_REFRESH_DELAY) _LOGGER.debug('name=%s node_config=%s CONF_REFRESH_VALUE=%s' ' CONF_REFRESH_DELAY=%s', name, node_config, refresh, delay) - if value.command_class != zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL: - return - if value.type != zwave.const.TYPE_BYTE: - return - if value.genre != zwave.const.GENRE_USER: - return - - value.set_change_verified(False) if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_COLOR): - add_devices([ZwaveColorLight(value, refresh, delay)]) + return ZwaveColorLight(value, refresh, delay) else: - add_devices([ZwaveDimmer(value, refresh, delay)]) + return ZwaveDimmer(value, refresh, delay) def brightness_state(value): """Return the brightness and state.""" if value.data > 0: - return (value.data / 99) * 255, STATE_ON + return round((value.data / 99) * 255, 0), STATE_ON else: return 0, STATE_OFF @@ -119,7 +107,7 @@ def update_properties(self): # Brightness self._brightness, self._state = brightness_state(self._value) - def value_changed(self, value): + def value_changed(self): """Called when a value for this entity's node has changed.""" if self._refresh_value: if self._refreshing: @@ -136,7 +124,7 @@ def _refresh_value(): self._timer = Timer(self._delay, _refresh_value) self._timer.start() return - super().value_changed(value) + super().value_changed() @property def brightness(self): @@ -200,6 +188,12 @@ def __init__(self, value, refresh, delay): self._value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED) self._get_color_values() + @property + def dependent_value_ids(self): + """List of value IDs a device depends on.""" + return [val.value_id for val in [ + self._value_color, self._value_color_channels] if val] + def _get_color_values(self): """Search for color values available on this node.""" from openzwave.network import ZWaveNetwork diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index 00540f661508b9..43d5788af9b409 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -47,7 +47,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if value_template is not None: value_template.hass = hass - yield from async_add_devices([MqttLock( + async_add_devices([MqttLock( config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py index 14606c0853c767..da2a465d5708cd 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/lock/vera.py @@ -6,10 +6,10 @@ """ import logging -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) from homeassistant.components.vera import ( - VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + VERA_CONTROLLER, VERA_DEVICES, VeraDevice) _LOGGER = logging.getLogger(__name__) @@ -30,6 +30,7 @@ def __init__(self, vera_device, controller): """Initialize the Vera device.""" self._state = None VeraDevice.__init__(self, vera_device, controller) + self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) def lock(self, **kwargs): """Lock the device.""" diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 3b01138ccb2d2c..cfafe955e2c6da 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -13,6 +13,7 @@ from homeassistant.components.lock import DOMAIN, LockDevice from homeassistant.components import zwave +from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv @@ -119,15 +120,8 @@ }) -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): - """Find and return Z-Wave locks.""" - if discovery_info is None or zwave.NETWORK is None: - return - - node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] - value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - +def get_device(hass, node, value, **kwargs): + """Create zwave entity device.""" descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) @@ -181,12 +175,6 @@ def clear_usercode(service): _LOGGER.info('Usercode at slot %s is cleared', value.index) break - if value.command_class != zwave.const.COMMAND_CLASS_DOOR_LOCK: - return - if value.type != zwave.const.TYPE_BOOL: - return - if value.genre != zwave.const.GENRE_USER: - return if node.has_command_class(zwave.const.COMMAND_CLASS_USER_CODE): hass.services.register(DOMAIN, SERVICE_SET_USERCODE, @@ -203,8 +191,7 @@ def clear_usercode(service): clear_usercode, descriptions.get(SERVICE_CLEAR_USERCODE), schema=CLEAR_USERCODE_SCHEMA) - value.set_change_verified(False) - add_devices([ZwaveLock(value)]) + return ZwaveLock(value) class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): @@ -305,3 +292,8 @@ def device_state_attributes(self): if self._lock_status: data[ATTR_LOCK_STATUS] = self._lock_status return data + + @property + def dependent_value_ids(self): + """List of value IDs a device depends on.""" + return None diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index b69289db98959f..92f99887867a12 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -14,7 +14,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from homeassistant.components import recorder, sun +from homeassistant.components import sun from homeassistant.components.frontend import register_built_in_panel from homeassistant.components.http import HomeAssistantView from homeassistant.const import (EVENT_HOMEASSISTANT_START, @@ -22,7 +22,6 @@ STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST) from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN -from homeassistant.util.async import run_callback_threadsafe DOMAIN = "logbook" DEPENDENCIES = ['recorder', 'frontend'] @@ -68,9 +67,7 @@ def log_entry(hass, name, message, domain=None, entity_id=None): """Add an entry to the logbook.""" - run_callback_threadsafe( - hass.loop, async_log_entry, hass, name, message, domain, entity_id - ).result() + hass.add_job(async_log_entry, hass, name, message, domain, entity_id) def async_log_entry(hass, name, message, domain=None, entity_id=None): @@ -101,7 +98,7 @@ def log_message(service): message = message.async_render() async_log_entry(hass, name, message, domain, entity_id) - hass.http.register_view(LogbookView(config)) + hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) register_built_in_panel(hass, 'logbook', 'Logbook', 'mdi:format-list-bulleted-type') @@ -135,20 +132,11 @@ def get(self, request, datetime=None): start_day = dt_util.as_utc(datetime) end_day = start_day + timedelta(days=1) + hass = request.app['hass'] - def get_results(): - """Query DB for results.""" - events = recorder.get_model('Events') - query = recorder.query('Events').order_by( - events.time_fired).filter( - (events.time_fired > start_day) & - (events.time_fired < end_day)) - events = recorder.execute(query) - return _exclude_events(events, self.config) - - events = yield from request.app['hass'].loop.run_in_executor( - None, get_results) - + events = yield from hass.loop.run_in_executor( + None, _get_events, hass, start_day, end_day) + events = _exclude_events(events, self.config) return self.json(humanify(events)) @@ -285,17 +273,31 @@ def humanify(events): entity_id) +def _get_events(hass, start_day, end_day): + """Get events for a period of time.""" + from homeassistant.components.recorder.models import Events + from homeassistant.components.recorder.util import ( + execute, session_scope) + + with session_scope(hass=hass) as session: + query = session.query(Events).order_by( + Events.time_fired).filter( + (Events.time_fired > start_day) & + (Events.time_fired < end_day)) + return execute(query) + + def _exclude_events(events, config): """Get lists of excluded entities and platforms.""" excluded_entities = [] excluded_domains = [] included_entities = [] included_domains = [] - exclude = config[DOMAIN].get(CONF_EXCLUDE) + exclude = config.get(CONF_EXCLUDE) if exclude: excluded_entities = exclude[CONF_ENTITIES] excluded_domains = exclude[CONF_DOMAINS] - include = config[DOMAIN].get(CONF_INCLUDE) + include = config.get(CONF_INCLUDE) if include: included_entities = include[CONF_ENTITIES] included_domains = include[CONF_DOMAINS] diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 4bf163ff9ebdcf..8572bbc044a00c 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -4,15 +4,22 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/logger/ """ +import asyncio import logging +import os from collections import OrderedDict import voluptuous as vol +from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv DOMAIN = 'logger' +DATA_LOGGER = 'logger' + +SERVICE_SET_LEVEL = 'set_level' + LOGSEVERITY = { 'CRITICAL': 50, 'FATAL': 50, @@ -29,6 +36,8 @@ _VALID_LOG_LEVEL = vol.All(vol.Upper, vol.In(LOGSEVERITY)) +SERVICE_SET_LEVEL_SCHEMA = vol.Schema({cv.string: _VALID_LOG_LEVEL}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(LOGGER_DEFAULT): _VALID_LOG_LEVEL, @@ -37,6 +46,11 @@ }, extra=vol.ALLOW_EXTRA) +def set_level(hass, logs): + """Set log level for components.""" + hass.services.call(DOMAIN, SERVICE_SET_LEVEL, logs) + + class HomeAssistantLogFilter(logging.Filter): """A log filter.""" @@ -61,7 +75,8 @@ def filter(self, record): return record.levelno >= default -def setup(hass, config=None): +@asyncio.coroutine +def async_setup(hass, config): """Setup the logger component.""" logfilter = {} @@ -72,21 +87,26 @@ def setup(hass, config=None): config.get(DOMAIN)[LOGGER_DEFAULT] ] - # Compute log severity for components - if LOGGER_LOGS in config.get(DOMAIN): - for key, value in config.get(DOMAIN)[LOGGER_LOGS].items(): - config.get(DOMAIN)[LOGGER_LOGS][key] = LOGSEVERITY[value] + def set_log_levels(logpoints): + """Set the specified log levels.""" + logs = {} + + # Preserve existing logs + if LOGGER_LOGS in logfilter: + logs.update(logfilter[LOGGER_LOGS]) - logs = OrderedDict( + # Add new logpoints mapped to correc severity + for key, value in logpoints.items(): + logs[key] = LOGSEVERITY[value] + + logfilter[LOGGER_LOGS] = OrderedDict( sorted( - config.get(DOMAIN)[LOGGER_LOGS].items(), + logs.items(), key=lambda t: len(t[0]), reverse=True ) ) - logfilter[LOGGER_LOGS] = logs - logger = logging.getLogger('') logger.setLevel(logging.NOTSET) @@ -95,4 +115,21 @@ def setup(hass, config=None): handler.setLevel(logging.NOTSET) handler.addFilter(HomeAssistantLogFilter(logfilter)) + if LOGGER_LOGS in config.get(DOMAIN): + set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) + + @asyncio.coroutine + def async_service_handler(service): + """Handle logger services.""" + set_log_levels(service.data) + + descriptions = yield from hass.loop.run_in_executor( + None, load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml')) + + hass.services.async_register( + DOMAIN, SERVICE_SET_LEVEL, async_service_handler, + descriptions[DOMAIN].get(SERVICE_SET_LEVEL), + schema=SERVICE_SET_LEVEL_SCHEMA) + return True diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py new file mode 100644 index 00000000000000..c0c9bd166749b5 --- /dev/null +++ b/homeassistant/components/maxcube.py @@ -0,0 +1,94 @@ +""" +Platform for the MAX! Cube LAN Gateway. + +For more details about this component, please refer to the documentation +https://home-assistant.io/components/maxcube/ +""" + +from socket import timeout +import logging +import time +from threading import Lock + +from homeassistant.helpers.discovery import load_platform +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv +import voluptuous as vol + +REQUIREMENTS = ['maxcube-api==0.1.0'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'maxcube' +MAXCUBE_HANDLE = 'maxcube' + +DEFAULT_PORT = 62910 + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Establish connection to MAX! Cube.""" + from maxcube.connection import MaxCubeConnection + from maxcube.cube import MaxCube + + # Read Config + host = config.get(DOMAIN).get(CONF_HOST) + port = config.get(DOMAIN).get(CONF_PORT) + + # Assign Cube Handle to global variable + try: + cube = MaxCube(MaxCubeConnection(host, port)) + except timeout: + _LOGGER.error("Connection to Max!Cube could not be established") + cube = None + return False + + hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube) + + # Load Climate (for Thermostats) + load_platform(hass, 'climate', DOMAIN) + + # Load BinarySensor (for Window Shutter) + load_platform(hass, 'binary_sensor', DOMAIN) + + # Initialization successfull + return True + + +class MaxCubeHandle(object): + """Keep the cube instance in one place and centralize the update.""" + + def __init__(self, cube): + """Initialize the Cube Handle.""" + # Cube handle + self.cube = cube + + # Instantiate Mutex + self.mutex = Lock() + + # Update Timestamp + self._updatets = time.time() + + def update(self): + """Pull the latest data from the MAX! Cube.""" + # Acquire mutex to prevent simultaneous update from multiple threads + with self.mutex: + # Only update every 60s + if (time.time() - self._updatets) >= 60: + _LOGGER.debug("UPDATE: Updating") + + try: + self.cube.update() + except timeout: + _LOGGER.error("Max!Cube connection failed") + return False + + self._updatets = time.time() + else: + _LOGGER.debug("UPDATE: Skipping") diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f4b828a028915d..a603cb9c3e30a5 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -757,18 +757,15 @@ def support_clear_playlist(self): """Boolean if clear playlist command supported.""" return bool(self.supported_features & SUPPORT_CLEAR_PLAYLIST) - def toggle(self): - """Toggle the power on the media player.""" - if self.state in [STATE_OFF, STATE_IDLE]: - self.turn_on() - else: - self.turn_off() - def async_toggle(self): """Toggle the power on the media player. This method must be run in the event loop and returns a coroutine. """ + if hasattr(self, 'toggle'): + # pylint: disable=no-member + return self.hass.loop.run_in_executor(None, self.toggle) + if self.state in [STATE_OFF, STATE_IDLE]: return self.async_turn_on() else: @@ -804,18 +801,15 @@ def async_volume_down(self): yield from self.async_set_volume_level( max(0, self.volume_level - .1)) - def media_play_pause(self): - """Play or pause the media player.""" - if self.state == STATE_PLAYING: - self.media_pause() - else: - self.media_play() - def async_media_play_pause(self): """Play or pause the media player. This method must be run in the event loop and returns a coroutine. """ + if hasattr(self, 'media_play_pause'): + # pylint: disable=no-member + return self.hass.loop.run_in_executor(None, self.media_play_pause) + if self.state == STATE_PLAYING: return self.async_media_pause() else: diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py index 01b4b32deb2cbc..e6fd4e286abc05 100644 --- a/homeassistant/components/media_player/anthemav.py +++ b/homeassistant/components/media_player/anthemav.py @@ -63,7 +63,7 @@ def async_anthemav_update_callback(message): _LOGGER.debug('dump_rawdata: '+avr.protocol.dump_rawdata) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) - yield from async_add_devices([device]) + async_add_devices([device]) class AnthemAVR(MediaPlayerDevice): diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 566ad7d69335e9..436730b7041378 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -8,7 +8,6 @@ import logging import hashlib -import aiohttp import voluptuous as vol from homeassistant.core import callback @@ -19,13 +18,13 @@ MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW) from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST, - STATE_OFF, CONF_NAME) + STATE_OFF, CONF_NAME, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pyatv==0.1.4'] +REQUIREMENTS = ['pyatv==0.2.1'] _LOGGER = logging.getLogger(__name__) @@ -45,8 +44,7 @@ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the Apple TV platform.""" import pyatv @@ -73,7 +71,14 @@ def async_setup_platform(hass, config, async_add_entities, atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) entity = AppleTvDevice(atv, name, start_off) - yield from async_add_entities([entity], update_before_add=True) + @callback + def on_hass_stop(event): + """Stop push updates when hass stops.""" + atv.push_updater.stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + + async_add_devices([entity]) class AppleTvDevice(MediaPlayerDevice): @@ -86,18 +91,34 @@ def __init__(self, atv, name, is_off): self._is_off = is_off self._playing = None self._artwork_hash = None + self._atv.push_updater.listener = self + + @asyncio.coroutine + def async_added_to_hass(self): + """Called when entity is about to be added to HASS.""" + self._atv.push_updater.start() @callback def _set_power_off(self, is_off): self._playing = None self._artwork_hash = None self._is_off = is_off + if is_off: + self._atv.push_updater.stop() + else: + self._atv.push_updater.start() + self.hass.async_add_job(self.async_update_ha_state()) @property def name(self): """Return the name of the device.""" return self._name + @property + def should_poll(self): + """No polling needed.""" + return False + @property def state(self): """Return the state of the device.""" @@ -120,29 +141,19 @@ def state(self): else: return STATE_STANDBY # Bad or unknown state? - @asyncio.coroutine - def async_update(self): - """Retrieve latest state.""" - if self._is_off: - return - - from pyatv import exceptions - try: - playing = yield from self._atv.metadata.playing() - - if self._has_playing_media_changed(playing): - base = str(playing.title) + str(playing.artist) + \ - str(playing.album) + str(playing.total_time) - self._artwork_hash = hashlib.md5( - base.encode('utf-8')).hexdigest() - - self._playing = playing - except exceptions.AuthenticationError as ex: - _LOGGER.warning('%s (bad login id?)', str(ex)) - except aiohttp.errors.ClientOSError as ex: - _LOGGER.error('failed to connect to Apple TV (%s)', str(ex)) - except asyncio.TimeoutError: - _LOGGER.warning('timed out while connecting to Apple TV') + @callback + def playstatus_update(self, updater, playing): + """Print what is currently playing when it changes.""" + if self.state == STATE_IDLE: + self._artwork_hash = None + elif self._has_playing_media_changed(playing): + base = str(playing.title) + str(playing.artist) + \ + str(playing.album) + str(playing.total_time) + self._artwork_hash = hashlib.md5( + base.encode('utf-8')).hexdigest() + + self._playing = playing + self.hass.async_add_job(self.async_update_ha_state()) def _has_playing_media_changed(self, new_playing): if self._playing is None: @@ -151,6 +162,21 @@ def _has_playing_media_changed(self, new_playing): return new_playing.media_type != old_playing.media_type or \ new_playing.title != old_playing.title + @callback + def playstatus_error(self, updater, exception): + """Inform about an error and restart push updates.""" + _LOGGER.warning('A %s error occurred: %s', + exception.__class__, exception) + + # This will wait 10 seconds before restarting push updates. If the + # connection continues to fail, it will flood the log (every 10 + # seconds) until it succeeds. A better approach should probably be + # implemented here later. + updater.start(initial_delay=10) + self._playing = None + self._artwork_hash = None + self.hass.async_add_job(self.async_update_ha_state()) + @property def media_content_type(self): """Content type of current playing media.""" @@ -191,7 +217,8 @@ def async_play_media(self, media_type, media_id, **kwargs): @property def media_image_hash(self): """Hash value for media image.""" - return self._artwork_hash + if self.state != STATE_IDLE: + return self._artwork_hash @asyncio.coroutine def async_get_media_image(self): @@ -207,6 +234,8 @@ def media_title(self): title = self._playing.title return title if title else "No title" + return 'Not connected to Apple TV' + @property def supported_features(self): """Flag media player features that are supported.""" diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py new file mode 100644 index 00000000000000..386a489b646e03 --- /dev/null +++ b/homeassistant/components/media_player/frontier_silicon.py @@ -0,0 +1,252 @@ +""" +Support for Frontier Silicon Devices (Medion, Hama, Auna,...). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.frontier_silicon/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_PLAY, SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC) +from homeassistant.const import ( + STATE_OFF, STATE_PLAYING, STATE_PAUSED, STATE_UNKNOWN, + CONF_HOST, CONF_PORT, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['fsapi==0.0.7'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FRONTIER_SILICON = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_ON | \ + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +DEFAULT_PORT = 80 +DEFAULT_PASSWORD = '1234' +DEVICE_URL = 'http://{0}:{1}/device' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Frontier Silicon platform.""" + import requests + + if discovery_info is not None: + add_devices( + [FSAPIDevice(discovery_info, DEFAULT_PASSWORD)], + update_before_add=True) + return True + + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + password = config.get(CONF_PASSWORD) + + try: + add_devices( + [FSAPIDevice(DEVICE_URL.format(host, port), password)], + update_before_add=True) + _LOGGER.debug('FSAPI device %s:%s -> %s', host, port, password) + return True + except requests.exceptions.RequestException: + _LOGGER.error('Could not add the FSAPI device at %s:%s -> %s', + host, port, password) + + return False + + +class FSAPIDevice(MediaPlayerDevice): + """Representation of a Frontier Silicon device on the network.""" + + def __init__(self, device_url, password): + """Initialize the Frontier Silicon API device.""" + self._device_url = device_url + self._password = password + self._state = STATE_UNKNOWN + + self._name = None + self._title = None + self._artist = None + self._album_name = None + self._mute = None + self._source = None + self._source_list = None + self._media_image_url = None + + # Properties + @property + def fs_device(self): + """ + Create a fresh fsapi session. + + A new session is created for each request in case someone else + connected to the device in between the updates and invalidated the + existing session (i.e UNDOK). + """ + from fsapi import FSAPI + + return FSAPI(self._device_url, self._password) + + @property + def should_poll(self): + """Device should be polled.""" + return True + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def media_title(self): + """Title of current playing media.""" + return self._title + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self._artist + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + return self._album_name + + @property + def media_content_type(self): + """Content type of current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def supported_features(self): + """Flag of media commands that are supported.""" + return SUPPORT_FRONTIER_SILICON + + @property + def state(self): + """Return the state of the player.""" + return self._state + + # source + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def source(self): + """Name of the current input source.""" + return self._source + + @property + def media_image_url(self): + """Image url of current playing media.""" + return self._media_image_url + + def update(self): + """Get the latest date and update device state.""" + fs_device = self.fs_device + + if not self._name: + self._name = fs_device.friendly_name + + if not self._source_list: + self._source_list = fs_device.mode_list + + status = fs_device.play_status + self._state = { + 'playing': STATE_PLAYING, + 'paused': STATE_PAUSED, + 'stopped': STATE_OFF, + 'unknown': STATE_UNKNOWN, + None: STATE_OFF, + }.get(status, STATE_UNKNOWN) + + info_name = fs_device.play_info_name + info_text = fs_device.play_info_text + + self._title = ' - '.join(filter(None, [info_name, info_text])) + self._artist = fs_device.play_info_artist + self._album_name = fs_device.play_info_album + + self._source = fs_device.mode + self._mute = fs_device.mute + self._media_image_url = fs_device.play_info_graphics + + # Management actions + + # power control + def turn_on(self): + """Turn on the device.""" + self.fs_device.power = True + + def turn_off(self): + """Turn off the device.""" + self.fs_device.power = False + + def media_play(self): + """Send play command.""" + self.fs_device.play() + + def media_pause(self): + """Send pause command.""" + self.fs_device.pause() + + def media_play_pause(self): + """Send play/pause command.""" + if 'playing' in self._state: + self.fs_device.pause() + else: + self.fs_device.play() + + def media_stop(self): + """Send play/pause command.""" + self.fs_device.pause() + + def media_previous_track(self): + """Send previous track command (results in rewind).""" + self.fs_device.prev() + + def media_next_track(self): + """Send next track command (results in fast-forward).""" + self.fs_device.next() + + # mute + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._mute + + def mute_volume(self, mute): + """Send mute command.""" + self.fs_device.mute = mute + + # volume + def volume_up(self): + """Send volume up command.""" + self.fs_device.volume += 1 + + def volume_down(self): + """Send volume down command.""" + self.fs_device.volume -= 1 + + def set_volume_level(self, volume): + """Set volume command.""" + self.fs_device.volume = volume + + def select_source(self, source): + """Select input source.""" + self.fs_device.mode = source diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 5631f7e5da7aab..09bd6ab398719e 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/media_player.kodi/ """ import asyncio +from functools import wraps import logging import urllib @@ -60,8 +61,7 @@ @asyncio.coroutine -def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the Kodi platform.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -84,7 +84,27 @@ def async_setup_platform(hass, config, async_add_entities, password=config.get(CONF_PASSWORD), turn_off_action=config.get(CONF_TURN_OFF_ACTION), websocket=websocket) - yield from async_add_entities([entity], update_before_add=True) + async_add_devices([entity], update_before_add=True) + + +def cmd(func): + """Decorator to catch command exceptions.""" + @wraps(func) + @asyncio.coroutine + def wrapper(obj, *args, **kwargs): + """Wrapper for all command methods.""" + import jsonrpc_base + try: + yield from func(obj, *args, **kwargs) + except jsonrpc_base.jsonrpc.TransportError as exc: + # If Kodi is off, we expect calls to fail. + if obj.state == STATE_OFF: + log_function = _LOGGER.info + else: + log_function = _LOGGER.error + log_function("Error calling %s on entity %s: %r", + func.__name__, obj.entity_id, exc) + return wrapper class KodiDevice(MediaPlayerDevice): @@ -372,6 +392,7 @@ def supported_features(self): return supported_features + @cmd @asyncio.coroutine def async_turn_off(self): """Execute turn_off_action to turn off media player.""" @@ -388,18 +409,21 @@ def async_turn_off(self): else: _LOGGER.warning('turn_off requested but turn_off_action is none') + @cmd @asyncio.coroutine def async_volume_up(self): """Volume up the media player.""" assert ( yield from self.server.Input.ExecuteAction('volumeup')) == 'OK' + @cmd @asyncio.coroutine def async_volume_down(self): """Volume down the media player.""" assert ( yield from self.server.Input.ExecuteAction('volumedown')) == 'OK' + @cmd def async_set_volume_level(self, volume): """Set volume level, range 0..1. @@ -407,6 +431,7 @@ def async_set_volume_level(self, volume): """ return self.server.Application.SetVolume(int(volume * 100)) + @cmd def async_mute_volume(self, mute): """Mute (true) or unmute (false) media player. @@ -419,10 +444,11 @@ def async_set_play_state(self, state): """Helper method for play/pause/toggle.""" players = yield from self._get_players() - if len(players) != 0: + if players is not None and len(players) != 0: yield from self.server.Player.PlayPause( players[0]['playerid'], state) + @cmd def async_media_play_pause(self): """Pause media on media player. @@ -430,6 +456,7 @@ def async_media_play_pause(self): """ return self.async_set_play_state('toggle') + @cmd def async_media_play(self): """Play media. @@ -437,6 +464,7 @@ def async_media_play(self): """ return self.async_set_play_state(True) + @cmd def async_media_pause(self): """Pause the media player. @@ -444,6 +472,7 @@ def async_media_pause(self): """ return self.async_set_play_state(False) + @cmd @asyncio.coroutine def async_media_stop(self): """Stop the media player.""" @@ -466,6 +495,7 @@ def _goto(self, direction): yield from self.server.Player.GoTo( players[0]['playerid'], direction) + @cmd def async_media_next_track(self): """Send next track command. @@ -473,6 +503,7 @@ def async_media_next_track(self): """ return self._goto('next') + @cmd def async_media_previous_track(self): """Send next track command. @@ -480,6 +511,7 @@ def async_media_previous_track(self): """ return self._goto('previous') + @cmd @asyncio.coroutine def async_media_seek(self, position): """Send seek command.""" @@ -501,6 +533,7 @@ def async_media_seek(self, position): if len(players) != 0: yield from self.server.Player.Seek(players[0]['playerid'], time) + @cmd def async_play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player. diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index 093a53786be7b7..52a37eb8faa773 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -21,7 +21,7 @@ STATE_PAUSED, STATE_UNKNOWN, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['liveboxplaytv==1.4.8'] +REQUIREMENTS = ['liveboxplaytv==1.4.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/openhome.py b/homeassistant/components/media_player/openhome.py index 46e8263999b8be..af58b4cb654855 100644 --- a/homeassistant/components/media_player/openhome.py +++ b/homeassistant/components/media_player/openhome.py @@ -14,7 +14,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF) -REQUIREMENTS = ['openhomedevice==0.2'] +REQUIREMENTS = ['openhomedevice==0.2.1'] SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | \ SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 08a3eec17e82ed..a33f331b737af4 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -123,7 +123,10 @@ def should_poll(self): @property def name(self): """Return the name of the device.""" - return self.device_info.userdevicename + if self.device_info.userdevicename: + return self.device_info.userdevicename + else: + return "roku_" + self.roku.device_info.sernum @property def state(self): diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py index 0de775562a5dcd..b71e37fda1917f 100644 --- a/homeassistant/components/media_player/samsungtv.py +++ b/homeassistant/components/media_player/samsungtv.py @@ -62,6 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = "{} ({})".format(tv_name, model) port = DEFAULT_PORT timeout = DEFAULT_TIMEOUT + mac = None else: _LOGGER.warning( 'Internal error on samsungtv component. Cannot determine device') diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index efab17a61a9685..6db376aa073fd8 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -53,8 +53,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) if discovery_info is not None: - host = discovery_info[0] - port = None # Port is not collected in netdisco 0.8.1 + host = discovery_info.get("host") + port = discovery_info.get("port") else: host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -85,7 +85,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return False players = yield from lms.create_players() - yield from async_add_devices(players) + async_add_devices(players) return True diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index aea10e3c44d6f0..b5f88eb28a4cf9 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -63,7 +63,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config[CONF_ATTRS] ) - yield from async_add_devices([player]) + async_add_devices([player]) def validate_config(config): diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index da498dc3d5bd16..fe029af163ed16 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -25,8 +25,8 @@ import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' - '/archive/v0.1.3.zip' - '#pylgtv==0.1.3', + '/archive/v0.1.4.zip' + '#pylgtv==0.1.4', 'websockets==3.2', 'wakeonlan==0.2.2'] diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 38e4fd18b28f33..71271341f33fa6 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -9,19 +9,22 @@ import os import socket import time +import requests.certs import voluptuous as vol from homeassistant.core import callback -from homeassistant.bootstrap import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform from homeassistant.config import load_yaml_config_file from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import template, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) from homeassistant.util.async import ( run_coroutine_threadsafe, run_callback_threadsafe) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) + EVENT_HOMEASSISTANT_STOP, CONF_VALUE_TEMPLATE, CONF_USERNAME, + CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -31,7 +34,7 @@ DATA_MQTT = 'mqtt' SERVICE_PUBLISH = 'publish' -EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' +SIGNAL_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' REQUIREMENTS = ['paho-mqtt==1.2'] @@ -195,16 +198,15 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None): def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS): """Subscribe to an MQTT topic.""" @callback - def async_mqtt_topic_subscriber(event): + def async_mqtt_topic_subscriber(dp_topic, dp_payload, dp_qos): """Match subscribed MQTT topic.""" - if not _match_topic(topic, event.data[ATTR_TOPIC]): + if not _match_topic(topic, dp_topic): return - hass.async_run_job(msg_callback, event.data[ATTR_TOPIC], - event.data[ATTR_PAYLOAD], event.data[ATTR_QOS]) + hass.async_run_job(msg_callback, dp_topic, dp_payload, dp_qos) - async_remove = hass.bus.async_listen( - EVENT_MQTT_MESSAGE_RECEIVED, async_mqtt_topic_subscriber) + async_remove = async_dispatcher_connect( + hass, SIGNAL_MQTT_MESSAGE_RECEIVED, async_mqtt_topic_subscriber) yield from hass.data[DATA_MQTT].async_subscribe(topic, qos) return async_remove @@ -309,6 +311,10 @@ def async_setup(hass, config): certificate = os.path.join(os.path.dirname(__file__), 'addtrustexternalcaroot.crt') + # When the port indicates mqtts, use bundled certificates from requests + if certificate is None and port == 8883: + certificate = requests.certs.where() + will_message = conf.get(CONF_WILL_MESSAGE) birth_message = conf.get(CONF_BIRTH_MESSAGE) @@ -325,18 +331,11 @@ def async_setup(hass, config): @asyncio.coroutine def async_stop_mqtt(event): """Stop MQTT component.""" - yield from hass.data[DATA_MQTT].async_stop() - - @asyncio.coroutine - def async_start_mqtt(event): - """Launch MQTT component when Home Assistant starts up.""" - yield from hass.data[DATA_MQTT].async_start() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) + yield from hass.data[DATA_MQTT].async_disconnect() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_mqtt) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) success = yield from hass.data[DATA_MQTT].async_connect() - if not success: return False @@ -436,13 +435,12 @@ def async_publish(self, topic, payload, qos, retain): with (yield from self._paho_lock): yield from self.hass.loop.run_in_executor( None, self._mqttc.publish, topic, payload, qos, retain) - yield from asyncio.sleep(0, loop=self.hass.loop) @asyncio.coroutine def async_connect(self): - """Connect to the host. Does not process messages yet. + """Connect to the host. Does process messages yet. - This method must be run in the event loop and returns a coroutine. + This method is a coroutine. """ result = yield from self.hass.loop.run_in_executor( None, self._mqttc.connect, self.broker, self.port, self.keepalive) @@ -450,17 +448,12 @@ def async_connect(self): 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 not result - def async_start(self): - """Run the MQTT client. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.loop.run_in_executor(None, self._mqttc.loop_start) - - def async_stop(self): + def async_disconnect(self): """Stop the MQTT client. This method must be run in the event loop and returns a coroutine. @@ -481,17 +474,16 @@ def async_subscribe(self, topic, qos): if not isinstance(topic, str): raise HomeAssistantError("topic need to be a string!") - if topic in self.topics: - return - with (yield from self._paho_lock): + if topic in self.topics: + return + result, mid = yield from self.hass.loop.run_in_executor( None, self._mqttc.subscribe, topic, qos) - yield from asyncio.sleep(0, loop=self.hass.loop) - _raise_on_error(result) - self.progress[mid] = topic - self.topics[topic] = None + _raise_on_error(result) + self.progress[mid] = topic + self.topics[topic] = None @asyncio.coroutine def async_unsubscribe(self, topic): @@ -552,13 +544,11 @@ def _mqtt_on_message(self, _mqttc, _userdata, msg): "MQTT topic: %s, Payload: %s", msg.topic, msg.payload) else: - _LOGGER.debug("Received message on %s: %s", - msg.topic, payload) - self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { - ATTR_TOPIC: msg.topic, - ATTR_QOS: msg.qos, - ATTR_PAYLOAD: payload, - }) + _LOGGER.info("Received message on %s: %s", msg.topic, payload) + dispatcher_send( + self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, payload, + msg.qos + ) def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos): """Unsubscribe successful callback.""" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d01fb848eabb4a..86cce757449c0a 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -10,7 +10,6 @@ import re import homeassistant.components.mqtt as mqtt -from homeassistant.components.mqtt import DOMAIN from homeassistant.helpers.discovery import async_load_platform from homeassistant.const import CONF_PLATFORM from homeassistant.components.mqtt import CONF_STATE_TOPIC @@ -20,7 +19,13 @@ TOPIC_MATCHER = re.compile( r'(?P\w+)/(?P\w+)/(?P\w+)/config') -SUPPORTED_COMPONENTS = ['binary_sensor', 'sensor'] +SUPPORTED_COMPONENTS = ['binary_sensor', 'light', 'sensor'] + +ALLOWED_PLATFORMS = { + 'binary_sensor': ['mqtt'], + 'light': ['mqtt', 'mqtt_json', 'mqtt_template'], + 'sensor': ['mqtt'] +} @asyncio.coroutine @@ -48,13 +53,19 @@ def async_device_message_received(topic, payload, qos): return payload = dict(payload) - payload[CONF_PLATFORM] = 'mqtt' + platform = payload.get(CONF_PLATFORM, 'mqtt') + if platform not in ALLOWED_PLATFORMS.get(component, []): + _LOGGER.warning("Platform %s (component %s) is not allowed", + platform, component) + return + + payload[CONF_PLATFORM] = platform if CONF_STATE_TOPIC not in payload: payload[CONF_STATE_TOPIC] = '{}/{}/{}/state'.format( discovery_topic, component, object_id) yield from async_load_platform( - hass, component, DOMAIN, payload, hass_config) + hass, component, platform, payload, hass_config) yield from mqtt.async_subscribe( hass, discovery_topic + '/#', async_device_message_received, 0) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index c4a4b7bc4abfbe..bd149b6397d1bd 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -19,7 +19,6 @@ from homeassistant.core import EventOrigin, State import homeassistant.helpers.config_validation as cv from homeassistant.remote import JSONEncoder -from .mqtt import EVENT_MQTT_MESSAGE_RECEIVED DOMAIN = "mqtt_eventstream" DEPENDENCIES = ['mqtt'] @@ -54,15 +53,6 @@ def _event_publisher(event): if event.event_type == EVENT_TIME_CHANGED: return - # MQTT fires a bus event for every incoming message, also messages from - # eventstream. Disable publishing these messages to other HA instances - # and possibly creating an infinite loop if these instances publish - # back to this one. - if all([not conf.get(CONF_PUBLISH_EVENTSTREAM_RECEIVED), - event.event_type == EVENT_MQTT_MESSAGE_RECEIVED, - event.data.get('topic') == sub_topic]): - return - # Filter out the events that were triggered by publishing # to the MQTT topic, or you will end up in an infinite loop. if event.event_type == EVENT_CALL_SERVICE: diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index d9c8584a5e9907..7051dc732c8264 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.bootstrap import setup_component +from homeassistant.setup import setup_component from homeassistant.components.mqtt import (valid_publish_topic, valid_subscribe_topic) from homeassistant.const import (ATTR_BATTERY_LEVEL, CONF_NAME, @@ -165,18 +165,22 @@ def sub_callback(topic, callback, qos): out_prefix=out_prefix, retain=retain) else: try: - socket.getaddrinfo(device, None) - # valid ip address - gateway = mysensors.TCPGateway( - device, event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version, port=tcp_port) - except OSError: - # invalid ip address + is_serial_port(device) gateway = mysensors.SerialGateway( device, event_callback=None, persistence=persistence, persistence_file=persistence_file, protocol_version=version, baud=baud_rate) + except vol.Invalid: + try: + socket.getaddrinfo(device, None) + # valid ip address + gateway = mysensors.TCPGateway( + device, event_callback=None, persistence=persistence, + persistence_file=persistence_file, + protocol_version=version, port=tcp_port) + except OSError: + # invalid ip address + return gateway.metric = hass.config.units.is_metric gateway.debug = config[DOMAIN].get(CONF_DEBUG) optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) @@ -186,12 +190,12 @@ def sub_callback(topic, callback, qos): def gw_start(event): """Callback to trigger start of gateway and any persistence.""" - gateway.start() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) if persistence: for node_id in gateway.sensors: gateway.event_callback('persistence', node_id) + gateway.start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: gateway.stop()) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start) @@ -251,6 +255,7 @@ def mysensors_callback(gateway, node_id): _LOGGER.info('No sketch_name: node %s', node_id) return + new_devices = [] for child in gateway.sensors[node_id].children.values(): for value_type in child.values.keys(): key = node_id, child.id, value_type @@ -272,11 +277,12 @@ def mysensors_callback(gateway, node_id): devices[key] = device_class( gateway, node_id, child.id, name, value_type, child.type) if add_devices: - _LOGGER.info('Adding new devices: %s', devices[key]) - add_devices([devices[key]]) - devices[key].schedule_update_ha_state(True) + new_devices.append(devices[key]) else: devices[key].update() + if add_devices and new_devices: + _LOGGER.info('Adding new devices: %s', new_devices) + add_devices(new_devices, True) return mysensors_callback diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index d1d35e07054671..35a01e254752a4 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -11,7 +11,7 @@ import voluptuous as vol -from homeassistant.bootstrap import async_prepare_setup_platform +from homeassistant.setup import async_prepare_setup_platform from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.config import load_yaml_config_file diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index 09716065751144..50842c69a61c79 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -126,6 +126,27 @@ def __ne__(self, other): return not self.__eq__(other) +def _write_device(out, device): + """Write a single device to file.""" + attributes = [] + if device.name is not None: + attributes.append( + 'name: {}'.format(device.name)) + if device.tracking_device_id is not None: + attributes.append( + 'tracking_device_id: {}'.format(device.tracking_device_id)) + if device.disabled: + attributes.append('disabled: True') + + out.write(device.push_id) + out.write(": {") + if len(attributes) > 0: + separator = ", " + out.write(separator.join(attributes)) + + out.write("}\n") + + class ApnsNotificationService(BaseNotificationService): """Implement the notification service for the APNS service.""" @@ -169,34 +190,12 @@ def device_state_changed_listener(self, entity_id, from_s, to_s): has a tracking id specified. """ self.device_states[entity_id] = str(to_s.state) - return - - @staticmethod - def write_device(out, device): - """Write a single device to file.""" - attributes = [] - if device.name is not None: - attributes.append( - 'name: {}'.format(device.name)) - if device.tracking_device_id is not None: - attributes.append( - 'tracking_device_id: {}'.format(device.tracking_device_id)) - if device.disabled: - attributes.append('disabled: True') - - out.write(device.push_id) - out.write(": {") - if len(attributes) > 0: - separator = ", " - out.write(separator.join(attributes)) - - out.write("}\n") def write_devices(self): """Write all known devices to file.""" with open(self.yaml_path, 'w+') as out: for _, device in self.devices.items(): - ApnsNotificationService.write_device(out, device) + _write_device(out, device) def register(self, call): """Register a device to receive push messages.""" @@ -215,7 +214,7 @@ def register(self, call): if current_device is None: self.devices[push_id] = device with open(self.yaml_path, 'a') as out: - self.write_device(out, device) + _write_device(out, device) return True if device != current_device: diff --git a/homeassistant/components/notify/ciscospark.py b/homeassistant/components/notify/ciscospark.py new file mode 100644 index 00000000000000..3a4ef1384d9c90 --- /dev/null +++ b/homeassistant/components/notify/ciscospark.py @@ -0,0 +1,67 @@ +""" +Cisco Spark platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.ciscospark/ +""" +import logging +import voluptuous as vol +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService, ATTR_TITLE) +from homeassistant.const import (CONF_TOKEN) +import homeassistant.helpers.config_validation as cv + +CONF_ROOMID = "roomid" + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['ciscosparkapi==0.4.2'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_ROOMID): cv.string, +}) + + +# pylint: disable=unused-variable +def get_service(hass, config, discovery_info=None): + """Get the CiscoSpark notification service.""" + return CiscoSparkNotificationService( + config.get(CONF_TOKEN), + config.get(CONF_ROOMID)) + + +class CiscoSparkNotificationService(BaseNotificationService): + """CiscoSparkNotificationService.""" + + def __init__(self, token, default_room): + """ + Initialize the service. + + Args: + token: Cisco Spark Developer's Token + default_room: Cisco Spark Room ID + """ + from ciscosparkapi import CiscoSparkAPI + self._default_room = default_room + self._token = token + self._spark = CiscoSparkAPI(access_token=self._token) + + def send_message(self, message="", **kwargs): + """ + Send a message to a user. + + Args: + message: notificaiton text + kwargs: attributes used - 'title' + """ + from ciscosparkapi import SparkApiError + try: + title = "" + if kwargs.get(ATTR_TITLE) is not None: + title = kwargs.get(ATTR_TITLE) + ": " + self._spark.messages.create(roomId=self._default_room, + text=title + message) + except SparkApiError as api_error: + _LOGGER.error("Could not send CiscoSpark notification. Error: %s", + api_error) diff --git a/homeassistant/components/notify/discord.py b/homeassistant/components/notify/discord.py index e6c4b3bad96cd9..189aa0d02bbed4 100644 --- a/homeassistant/components/notify/discord.py +++ b/homeassistant/components/notify/discord.py @@ -42,11 +42,14 @@ def async_send_message(self, message, **kwargs): import discord discord_bot = discord.Client(loop=self.hass.loop) - yield from discord_bot.login(self.token) - - for channelid in kwargs[ATTR_TARGET]: - channel = discord.Object(id=channelid) - yield from discord_bot.send_message(channel, message) - - yield from discord_bot.logout() - yield from discord_bot.close() + @discord_bot.event + @asyncio.coroutine + def on_ready(): # pylint: disable=unused-variable + """Send the messages when the bot is ready.""" + for channelid in kwargs[ATTR_TARGET]: + channel = discord.Object(id=channelid) + yield from discord_bot.send_message(channel, message) + yield from discord_bot.logout() + yield from discord_bot.close() + + yield from discord_bot.start(self.token) diff --git a/homeassistant/components/notify/pushsafer.py b/homeassistant/components/notify/pushsafer.py index e39b94a18d604e..78a600ab8d6b05 100644 --- a/homeassistant/components/notify/pushsafer.py +++ b/homeassistant/components/notify/pushsafer.py @@ -6,65 +6,42 @@ """ import logging +import requests import voluptuous as vol from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, ATTR_DATA, - BaseNotificationService) -from homeassistant.const import CONF_API_KEY + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-pushsafer==0.2'] _LOGGER = logging.getLogger(__name__) +_RESOURCE = 'https://www.pushsafer.com/api' +CONF_DEVICE_KEY = 'private_key' -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, +DEFAULT_TIMEOUT = 10 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE_KEY): cv.string, }) -# pylint: disable=unused-variable def get_service(hass, config, discovery_info=None): - """Get the Pushsafer notification service.""" - from pushsafer import InitError - - try: - return PushsaferNotificationService(config[CONF_API_KEY]) - except InitError: - _LOGGER.error( - 'Wrong private key supplied. Get it at https://www.pushsafer.com') - return None + """Get the Pushsafer.com notification service.""" + return PushsaferNotificationService(config.get(CONF_DEVICE_KEY)) class PushsaferNotificationService(BaseNotificationService): - """Implement the notification service for Pushsafer.""" + """Implementation of the notification service for Pushsafer.com.""" - def __init__(self, privatekey): + def __init__(self, private_key): """Initialize the service.""" - from pushsafer import Client - self._privatekey = privatekey - self.pushsafer = Client( - "", privatekey=self._privatekey) + self._private_key = private_key def send_message(self, message='', **kwargs): """Send a message to a user.""" - # Make a copy and use empty dict if necessary - data = dict(kwargs.get(ATTR_DATA) or {}) - - data['title'] = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - targets = kwargs.get(ATTR_TARGET) - - if not isinstance(targets, list): - targets = [targets] - - for target in targets: - if target is not None: - data['device'] = target - - try: - self.pushsafer.send_message(message, data['title'], "", "", - "", "", "", "", - "0", "", "", "") - except ValueError as val_err: - _LOGGER.error(str(val_err)) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + payload = {'k': self._private_key, 't': title, 'm': message} + response = requests.get(_RESOURCE, params=payload, + timeout=DEFAULT_TIMEOUT) + if response.status_code != 200: + _LOGGER.error("Not possible to send notification") diff --git a/homeassistant/components/notify/twilio_call.py b/homeassistant/components/notify/twilio_call.py index 374e77b95079fc..f917d5cdab3e3c 100644 --- a/homeassistant/components/notify/twilio_call.py +++ b/homeassistant/components/notify/twilio_call.py @@ -9,21 +9,18 @@ import voluptuous as vol +from homeassistant.components.twilio import DATA_TWILIO import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["twilio==5.7.0"] +DEPENDENCIES = ["twilio"] -CONF_ACCOUNT_SID = "account_sid" -CONF_AUTH_TOKEN = "auth_token" CONF_FROM_NUMBER = "from_number" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCOUNT_SID): cv.string, - vol.Required(CONF_AUTH_TOKEN): cv.string, vol.Required(CONF_FROM_NUMBER): vol.All(cv.string, vol.Match(r"^\+?[1-9]\d{1,14}$")), }) @@ -31,13 +28,7 @@ def get_service(hass, config, discovery_info=None): """Get the Twilio Call notification service.""" - # pylint: disable=import-error - from twilio.rest import TwilioRestClient - - twilio_client = TwilioRestClient(config[CONF_ACCOUNT_SID], - config[CONF_AUTH_TOKEN]) - - return TwilioCallNotificationService(twilio_client, + return TwilioCallNotificationService(hass.data[DATA_TWILIO], config[CONF_FROM_NUMBER]) diff --git a/homeassistant/components/notify/twilio_sms.py b/homeassistant/components/notify/twilio_sms.py index ab3ac89e6b21e7..1bdfcb644073b4 100644 --- a/homeassistant/components/notify/twilio_sms.py +++ b/homeassistant/components/notify/twilio_sms.py @@ -8,21 +8,18 @@ import voluptuous as vol +from homeassistant.components.twilio import DATA_TWILIO import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ["twilio==5.7.0"] +DEPENDENCIES = ["twilio"] -CONF_ACCOUNT_SID = "account_sid" -CONF_AUTH_TOKEN = "auth_token" CONF_FROM_NUMBER = "from_number" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ACCOUNT_SID): cv.string, - vol.Required(CONF_AUTH_TOKEN): cv.string, vol.Required(CONF_FROM_NUMBER): vol.All(cv.string, vol.Match(r"^\+?[1-9]\d{1,14}$")), }) @@ -30,13 +27,7 @@ def get_service(hass, config, discovery_info=None): """Get the Twilio SMS notification service.""" - # pylint: disable=import-error - from twilio.rest import TwilioRestClient - - twilio_client = TwilioRestClient(config[CONF_ACCOUNT_SID], - config[CONF_AUTH_TOKEN]) - - return TwilioSMSNotificationService(twilio_client, + return TwilioSMSNotificationService(hass.data[DATA_TWILIO], config[CONF_FROM_NUMBER]) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 60aa2aebd35929..21388c292eb6aa 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -13,7 +13,7 @@ PLATFORM_SCHEMA, BaseNotificationService) from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME -REQUIREMENTS = ['TwitterAPI==2.4.4'] +REQUIREMENTS = ['TwitterAPI==2.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index 476f7b9053e450..e82971e0064e81 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -5,24 +5,29 @@ https://home-assistant.io/components/notify.webostv/ """ import logging +import os import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - BaseNotificationService, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_FILENAME, CONF_HOST) + ATTR_DATA, BaseNotificationService, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_FILENAME, CONF_HOST, CONF_ICON) -REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv/archive/v0.1.3.zip' - '#pylgtv==0.1.3'] +REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv/archive/v0.1.4.zip' + '#pylgtv==0.1.4'] _LOGGER = logging.getLogger(__name__) WEBOSTV_CONFIG_FILE = 'webostv.conf' +HOME_ASSISTANT_ICON_PATH = os.path.join(os.path.dirname(__file__), '..', + 'frontend', 'www_static', 'icons', + 'favicon-1024x1024.png') PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string + vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, + vol.Optional(CONF_ICON, default=HOME_ASSISTANT_ICON_PATH): cv.string }) @@ -44,23 +49,29 @@ def get_service(hass, config, discovery_info=None): _LOGGER.error("TV unreachable") return None - return LgWebOSNotificationService(client) + return LgWebOSNotificationService(client, config.get(CONF_ICON)) class LgWebOSNotificationService(BaseNotificationService): """Implement the notification service for LG WebOS TV.""" - def __init__(self, client): + def __init__(self, client, icon_path): """Initialize the service.""" self._client = client + self._icon_path = icon_path def send_message(self, message="", **kwargs): """Send a message to the tv.""" from pylgtv import PyLGTVPairException try: - self._client.send_message(message) + data = kwargs.get(ATTR_DATA) + icon_path = data.get(CONF_ICON, self._icon_path) if data else \ + self._icon_path + self._client.send_message(message, icon_path=icon_path) except PyLGTVPairException: _LOGGER.error("Pairing with TV failed") + except FileNotFoundError: + _LOGGER.error("Icon %s not found", icon_path) except OSError: _LOGGER.error("TV unreachable") diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index b4dde02baff70a..d7eef848679c78 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -16,7 +16,6 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.util import slugify from homeassistant.config import load_yaml_config_file -from homeassistant.util.async import run_callback_threadsafe DOMAIN = 'persistent_notification' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -39,9 +38,7 @@ def create(hass, message, title=None, notification_id=None): """Generate a notification.""" - run_callback_threadsafe( - hass.loop, async_create, hass, message, title, notification_id - ).result() + hass.add_job(async_create, hass, message, title, notification_id) @callback diff --git a/homeassistant/components/proximity.py b/homeassistant/components/proximity.py index 18548dc203b7bc..084d6ac740791e 100644 --- a/homeassistant/components/proximity.py +++ b/homeassistant/components/proximity.py @@ -71,7 +71,7 @@ def setup_proximity_component(hass, name, config): zone_id, unit_of_measurement) proximity.entity_id = '{}.{}'.format(DOMAIN, proximity_zone) - proximity.update_ha_state() + proximity.schedule_update_ha_state() track_state_change( hass, proximity_devices, proximity.check_proximity_state_change) @@ -161,7 +161,7 @@ def check_proximity_state_change(self, entity, old_state, new_state): self.dist_to = 'not set' self.dir_of_travel = 'not set' self.nearest = 'not set' - self.update_ha_state() + self.schedule_update_ha_state() return # At least one device is in the monitored zone so update the entity. @@ -169,7 +169,7 @@ def check_proximity_state_change(self, entity, old_state, new_state): self.dist_to = 0 self.dir_of_travel = 'arrived' self.nearest = devices_in_zone - self.update_ha_state() + self.schedule_update_ha_state() return # We can't check proximity because latitude and longitude don't exist. @@ -214,7 +214,7 @@ def check_proximity_state_change(self, entity, old_state, new_state): self.dir_of_travel = 'unknown' device_state = self.hass.states.get(closest_device) self.nearest = device_state.name - self.update_ha_state() + self.schedule_update_ha_state() return # Stop if we cannot calculate the direction of travel (i.e. we don't @@ -223,7 +223,7 @@ def check_proximity_state_change(self, entity, old_state, new_state): self.dist_to = round(distances_to_zone[entity]) self.dir_of_travel = 'unknown' self.nearest = entity_name - self.update_ha_state() + self.schedule_update_ha_state() return # Reset the variables @@ -250,7 +250,7 @@ def check_proximity_state_change(self, entity, old_state, new_state): self.dist_to = round(dist_to_zone) self.dir_of_travel = direction_of_travel self.nearest = entity_name - self.update_ha_state() + self.schedule_update_ha_state() _LOGGER.debug('proximity.%s update entity: distance=%s: direction=%s: ' 'device=%s', self.friendly_name, round(dist_to_zone), direction_of_travel, entity_name) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 3c0e66679bc5dc..2d497d382737a6 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -89,7 +89,7 @@ def update_value(self, value): if value != self._value: self._value = value # pylint: disable=no-member - super().update_ha_state() # Part of Entity/ToggleEntity + super().schedule_update_ha_state() # Part of Entity/ToggleEntity return self._value def turn_on(self, **kwargs): diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 0f8d7b48fe2c93..985ec240f71e03 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -8,27 +8,31 @@ https://home-assistant.io/components/recorder/ """ import asyncio +import concurrent.futures import logging import queue import threading import time from datetime import timedelta, datetime -from typing import Any, Union, Optional, List, Dict -from contextlib import contextmanager +from typing import Optional, Dict import voluptuous as vol -from homeassistant.core import HomeAssistant, callback, split_entity_id +from homeassistant.core import ( + HomeAssistant, callback, split_entity_id, CoreState) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITIES, CONF_EXCLUDE, CONF_DOMAINS, - CONF_INCLUDE, EVENT_HOMEASSISTANT_STOP, + CONF_INCLUDE, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType, QueryType +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from . import purge, migration +from .const import DATA_INSTANCE +from .util import session_scope + DOMAIN = 'recorder' REQUIREMENTS = ['sqlalchemy==1.1.5'] @@ -39,10 +43,7 @@ CONF_DB_URL = 'db_url' CONF_PURGE_DAYS = 'purge_days' -RETRIES = 3 -CONNECT_RETRY_WAIT = 10 -QUERY_RETRY_WAIT = 0.1 -ERROR_QUERY = "Error during query: %s" +CONNECT_RETRY_WAIT = 3 FILTER_SCHEMA = vol.Schema({ vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ @@ -65,88 +66,32 @@ }) }, extra=vol.ALLOW_EXTRA) -_INSTANCE = None # type: Any _LOGGER = logging.getLogger(__name__) -@contextmanager -def session_scope(): - """Provide a transactional scope around a series of operations.""" - session = _INSTANCE.get_session() - try: - yield session - session.commit() - except Exception as err: # pylint: disable=broad-except - _LOGGER.error(ERROR_QUERY, err) - session.rollback() - raise - finally: - session.close() - - -@asyncio.coroutine -def async_get_instance(): - """Throw error if recorder not initialized.""" - if _INSTANCE is None: - raise RuntimeError("Recorder not initialized.") - - yield from _INSTANCE.async_db_ready.wait() - - return _INSTANCE - - -def get_instance(): - """Throw error if recorder not initialized.""" - if _INSTANCE is None: - raise RuntimeError("Recorder not initialized.") - - ident = _INSTANCE.hass.loop.__dict__.get("_thread_ident") - if ident is not None and ident == threading.get_ident(): - raise RuntimeError('Cannot be called from within the event loop') - - _wait(_INSTANCE.db_ready, "Database not ready") - - return _INSTANCE +def wait_connection_ready(hass): + """ + Wait till the connection is ready. + Returns a coroutine object. + """ + return hass.data[DATA_INSTANCE].async_db_ready -# pylint: disable=invalid-sequence-index -def execute(qry: QueryType) -> List[Any]: - """Query the database and convert the objects to HA native form. - This method also retries a few times in the case of stale connections. - """ - get_instance() - from sqlalchemy.exc import SQLAlchemyError - with session_scope() as session: - for _ in range(0, RETRIES): - try: - return [ - row for row in - (row.to_native() for row in qry) - if row is not None] - except SQLAlchemyError as err: - _LOGGER.error(ERROR_QUERY, err) - session.rollback() - time.sleep(QUERY_RETRY_WAIT) - return [] - - -def run_information(point_in_time: Optional[datetime]=None): +def run_information(hass, point_in_time: Optional[datetime]=None): """Return information about current run. There is also the run that covers point_in_time. """ - ins = get_instance() + from . import models + ins = hass.data[DATA_INSTANCE] - recorder_runs = get_model('RecorderRuns') + recorder_runs = models.RecorderRuns if point_in_time is None or point_in_time > ins.recording_start: - return recorder_runs( - end=None, - start=ins.recording_start, - closed_incorrect=False) + return ins.run_info - with session_scope() as session: - res = query(recorder_runs).filter( + with session_scope(hass=hass) as session: + res = session.query(recorder_runs).filter( (recorder_runs.start < point_in_time) & (recorder_runs.end > point_in_time)).first() if res: @@ -154,48 +99,26 @@ def run_information(point_in_time: Optional[datetime]=None): return res -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +@asyncio.coroutine +def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Setup the recorder.""" - global _INSTANCE # pylint: disable=global-statement - - if _INSTANCE is not None: - _LOGGER.error("Only a single instance allowed") - return False + conf = config.get(DOMAIN, {}) + purge_days = conf.get(CONF_PURGE_DAYS) - purge_days = config.get(DOMAIN, {}).get(CONF_PURGE_DAYS) - - db_url = config.get(DOMAIN, {}).get(CONF_DB_URL, None) + db_url = conf.get(CONF_DB_URL, None) if not db_url: db_url = DEFAULT_URL.format( hass_config_path=hass.config.path(DEFAULT_DB_FILE)) - include = config.get(DOMAIN, {}).get(CONF_INCLUDE, {}) - exclude = config.get(DOMAIN, {}).get(CONF_EXCLUDE, {}) - _INSTANCE = Recorder(hass, purge_days=purge_days, uri=db_url, - include=include, exclude=exclude) - _INSTANCE.start() - - return True - + include = conf.get(CONF_INCLUDE, {}) + exclude = conf.get(CONF_EXCLUDE, {}) + instance = hass.data[DATA_INSTANCE] = Recorder( + hass, purge_days=purge_days, uri=db_url, include=include, + exclude=exclude) + instance.async_initialize() + instance.start() -def query(model_name: Union[str, Any], session=None, *args) -> QueryType: - """Helper to return a query handle.""" - if session is None: - session = get_instance().get_session() - - if isinstance(model_name, str): - return session.query(get_model(model_name), *args) - return session.query(model_name, *args) - - -def get_model(model_name: str) -> Any: - """Get a model class.""" - from homeassistant.components.recorder import models - try: - return getattr(models, model_name) - except AttributeError: - _LOGGER.error("Invalid model name %s", model_name) - return None + return (yield from instance.async_db_ready) class Recorder(threading.Thread): @@ -204,48 +127,108 @@ class Recorder(threading.Thread): def __init__(self, hass: HomeAssistant, purge_days: int, uri: str, include: Dict, exclude: Dict) -> None: """Initialize the recorder.""" - threading.Thread.__init__(self) + threading.Thread.__init__(self, name='Recorder') self.hass = hass self.purge_days = purge_days self.queue = queue.Queue() # type: Any self.recording_start = dt_util.utcnow() self.db_url = uri - self.db_ready = threading.Event() - self.async_db_ready = asyncio.Event(loop=hass.loop) + self.async_db_ready = asyncio.Future(loop=hass.loop) self.engine = None # type: Any - self._run = None # type: Any + self.run_info = None # type: Any self.include_e = include.get(CONF_ENTITIES, []) self.include_d = include.get(CONF_DOMAINS, []) self.exclude = exclude.get(CONF_ENTITIES, []) + \ exclude.get(CONF_DOMAINS, []) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) - hass.bus.listen(MATCH_ALL, self.event_listener) - self.get_session = None + @callback + def async_initialize(self): + """Initialize the recorder.""" + self.hass.bus.async_listen(MATCH_ALL, self.event_listener) + def run(self): """Start processing events to save.""" - from homeassistant.components.recorder.models import Events, States - from sqlalchemy.exc import SQLAlchemyError + from .models import States, Events + from homeassistant.components import persistent_notification - while True: + tries = 1 + connected = False + + while not connected and tries <= 10: + if tries != 1: + time.sleep(CONNECT_RETRY_WAIT) try: self._setup_connection() + migration.migrate_schema(self) self._setup_run() - self.db_ready.set() - self.hass.loop.call_soon_threadsafe(self.async_db_ready.set) - break - except SQLAlchemyError as err: + connected = True + except Exception as err: # pylint: disable=broad-except _LOGGER.error("Error during connection setup: %s (retrying " "in %s seconds)", err, CONNECT_RETRY_WAIT) - time.sleep(CONNECT_RETRY_WAIT) + tries += 1 + + if not connected: + @callback + def connection_failed(): + """Connection failed tasks.""" + self.async_db_ready.set_result(False) + persistent_notification.async_create( + self.hass, + "The recorder could not start, please check the log", + "Recorder") + + self.hass.add_job(connection_failed) + return - if self.purge_days is not None: - async_track_time_interval( - self.hass, self._purge_old_data, timedelta(days=2)) + purge_task = object() + shutdown_task = object() + hass_started = concurrent.futures.Future() + + @callback + def register(): + """Post connection initialize.""" + self.async_db_ready.set_result(True) + + def shutdown(event): + """Shut down the Recorder.""" + if not hass_started.done(): + hass_started.set_result(shutdown_task) + self.queue.put(None) + self.join() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + shutdown) + + if self.hass.state == CoreState.running: + hass_started.set_result(None) + else: + @callback + def notify_hass_started(event): + """Notify that hass has started.""" + hass_started.set_result(None) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, + notify_hass_started) + + if self.purge_days is not None: + @callback + def do_purge(now): + """Event listener for purging data.""" + self.queue.put(purge_task) + + async_track_time_interval(self.hass, do_purge, + timedelta(days=2)) + + self.hass.add_job(register) + result = hass_started.result() + + # If shutdown happened before HASS finished starting + if result is shutdown_task: + return while True: event = self.queue.get() @@ -255,8 +238,10 @@ def run(self): self._close_connection() self.queue.task_done() return - - if event.event_type == EVENT_TIME_CHANGED: + elif event is purge_task: + purge.purge_old_data(self, self.purge_days) + continue + elif event.event_type == EVENT_TIME_CHANGED: self.queue.task_done() continue @@ -280,17 +265,14 @@ def run(self): self.queue.task_done() continue - with session_scope() as session: + with session_scope(session=self.get_session()) as session: dbevent = Events.from_event(event) - self._commit(session, dbevent) - - if event.event_type != EVENT_STATE_CHANGED: - self.queue.task_done() - continue + session.add(dbevent) - dbstate = States.from_event(event) - dbstate.event_id = dbevent.event_id - self._commit(session, dbstate) + if event.event_type == EVENT_STATE_CHANGED: + dbstate = States.from_event(event) + dbstate.event_id = dbevent.event_id + session.add(dbstate) self.queue.task_done() @@ -299,120 +281,34 @@ def event_listener(self, event): """Listen for new events and put them in the process queue.""" self.queue.put(event) - def shutdown(self, event): - """Tell the recorder to shut down.""" - global _INSTANCE # pylint: disable=global-statement - self.queue.put(None) - self.join() - _INSTANCE = None - def block_till_done(self): """Block till all events processed.""" self.queue.join() - def block_till_db_ready(self): - """Block until the database session is ready.""" - _wait(self.db_ready, "Database not ready") - def _setup_connection(self): """Ensure database is ready to fly.""" - import homeassistant.components.recorder.models as models from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session from sqlalchemy.orm import sessionmaker + from . import models + + kwargs = {} if self.db_url == 'sqlite://' or ':memory:' in self.db_url: from sqlalchemy.pool import StaticPool - self.engine = create_engine( - 'sqlite://', - connect_args={'check_same_thread': False}, - poolclass=StaticPool, - pool_reset_on_return=None) + + kwargs['connect_args'] = {'check_same_thread': False} + kwargs['poolclass'] = StaticPool + kwargs['pool_reset_on_return'] = None else: - self.engine = create_engine(self.db_url, echo=False) + kwargs['echo'] = False + if self.engine is not None: + self.engine.dispose() + + self.engine = create_engine(self.db_url, **kwargs) models.Base.metadata.create_all(self.engine) - session_factory = sessionmaker(bind=self.engine) - self.get_session = scoped_session(session_factory) - self._migrate_schema() - - def _migrate_schema(self): - """Check if the schema needs to be upgraded.""" - from homeassistant.components.recorder.models import SCHEMA_VERSION - schema_changes = get_model('SchemaChanges') - with session_scope() as session: - res = session.query(schema_changes).order_by( - schema_changes.change_id.desc()).first() - current_version = getattr(res, 'schema_version', None) - - if current_version == SCHEMA_VERSION: - return - _LOGGER.debug("Schema version incorrect: %s", current_version) - - if current_version is None: - current_version = self._inspect_schema_version() - _LOGGER.debug("No schema version found. Inspected version: %s", - current_version) - - for version in range(current_version, SCHEMA_VERSION): - new_version = version + 1 - _LOGGER.info("Upgrading recorder db schema to version %s", - new_version) - self._apply_update(new_version) - self._commit(session, - schema_changes(schema_version=new_version)) - _LOGGER.info("Upgraded recorder db schema to version %s", - new_version) - - def _apply_update(self, new_version): - """Perform operations to bring schema up to date.""" - from sqlalchemy import Table - import homeassistant.components.recorder.models as models - - if new_version == 1: - def create_index(table_name, column_name): - """Create an index for the specified table and column.""" - table = Table(table_name, models.Base.metadata) - name = "_".join(("ix", table_name, column_name)) - # Look up the index object that was created from the models - index = next(idx for idx in table.indexes if idx.name == name) - _LOGGER.debug("Creating index for table %s column %s", - table_name, column_name) - index.create(self.engine) - _LOGGER.debug("Index creation done for table %s column %s", - table_name, column_name) - - create_index("events", "time_fired") - else: - raise ValueError("No schema migration defined for version {}" - .format(new_version)) - - def _inspect_schema_version(self): - """Determine the schema version by inspecting the db structure. - - When the schema verison is not present in the db, either db was just - created with the correct schema, or this is a db created before schema - versions were tracked. For now, we'll test if the changes for schema - version 1 are present to make the determination. Eventually this logic - can be removed and we can assume a new db is being created. - """ - from sqlalchemy.engine import reflection - import homeassistant.components.recorder.models as models - inspector = reflection.Inspector.from_engine(self.engine) - indexes = inspector.get_indexes("events") - with session_scope() as session: - for index in indexes: - if index['column_names'] == ["time_fired"]: - # Schema addition from version 1 detected. New DB. - current_version = models.SchemaChanges( - schema_version=models.SCHEMA_VERSION) - self._commit(session, current_version) - return models.SCHEMA_VERSION - - # Version 1 schema changes not found, this db needs to be migrated. - current_version = models.SchemaChanges(schema_version=0) - self._commit(session, current_version) - return current_version.schema_version + self.get_session = scoped_session(sessionmaker(bind=self.engine)) def _close_connection(self): """Close the connection.""" @@ -422,93 +318,27 @@ def _close_connection(self): def _setup_run(self): """Log the start of the current run.""" - recorder_runs = get_model('RecorderRuns') - with session_scope() as session: - for run in query( - recorder_runs, session=session).filter_by(end=None): + from .models import RecorderRuns + + with session_scope(session=self.get_session()) as session: + for run in session.query(RecorderRuns).filter_by(end=None): run.closed_incorrect = True run.end = self.recording_start _LOGGER.warning("Ended unfinished session (id=%s from %s)", run.run_id, run.start) session.add(run) - _LOGGER.warning("Found unfinished sessions") - - self._run = recorder_runs( + self.run_info = RecorderRuns( start=self.recording_start, created=dt_util.utcnow() ) - self._commit(session, self._run) + session.add(self.run_info) + session.flush() + session.expunge(self.run_info) def _close_run(self): """Save end time for current run.""" - with session_scope() as session: - self._run.end = dt_util.utcnow() - self._commit(session, self._run) - self._run = None - - def _purge_old_data(self, _=None): - """Purge events and states older than purge_days ago.""" - from homeassistant.components.recorder.models import Events, States - - if not self.purge_days or self.purge_days < 1: - _LOGGER.debug("purge_days set to %s, will not purge any old data.", - self.purge_days) - return - - purge_before = dt_util.utcnow() - timedelta(days=self.purge_days) - - def _purge_states(session): - deleted_rows = session.query(States) \ - .filter((States.created < purge_before)) \ - .delete(synchronize_session=False) - _LOGGER.debug("Deleted %s states", deleted_rows) - - with session_scope() as session: - if self._commit(session, _purge_states): - _LOGGER.info("Purged states created before %s", purge_before) - - def _purge_events(session): - deleted_rows = session.query(Events) \ - .filter((Events.created < purge_before)) \ - .delete(synchronize_session=False) - _LOGGER.debug("Deleted %s events", deleted_rows) - - with session_scope() as session: - if self._commit(session, _purge_events): - _LOGGER.info("Purged events created before %s", purge_before) - - # Execute sqlite vacuum command to free up space on disk - if self.engine.driver == 'sqlite': - _LOGGER.info("Vacuuming SQLite to free space") - self.engine.execute("VACUUM") - - @staticmethod - def _commit(session, work): - """Commit & retry work: Either a model or in a function.""" - import sqlalchemy.exc - for _ in range(0, RETRIES): - try: - if callable(work): - work(session) - else: - session.add(work) - session.commit() - return True - except sqlalchemy.exc.OperationalError as err: - _LOGGER.error(ERROR_QUERY, err) - session.rollback() - time.sleep(QUERY_RETRY_WAIT) - return False - - -def _wait(event, message): - """Event wait helper.""" - for retry in (10, 20, 30): - event.wait(10) - if event.is_set(): - return - msg = "{} ({} seconds)".format(message, retry) - _LOGGER.warning(msg) - if not event.is_set(): - raise HomeAssistantError(msg) + with session_scope(session=self.get_session()) as session: + self.run_info.end = dt_util.utcnow() + session.add(self.run_info) + self.run_info = None diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py new file mode 100644 index 00000000000000..e2716ea982a24e --- /dev/null +++ b/homeassistant/components/recorder/const.py @@ -0,0 +1,3 @@ +"""Recorder constants.""" + +DATA_INSTANCE = 'recorder_instance' diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py new file mode 100644 index 00000000000000..09c5e9837c3418 --- /dev/null +++ b/homeassistant/components/recorder/migration.py @@ -0,0 +1,88 @@ +"""Schema migration helpers.""" +import logging + +from .util import session_scope + +_LOGGER = logging.getLogger(__name__) + + +def migrate_schema(instance): + """Check if the schema needs to be upgraded.""" + from .models import SchemaChanges, SCHEMA_VERSION + + with session_scope(session=instance.get_session()) as session: + res = session.query(SchemaChanges).order_by( + SchemaChanges.change_id.desc()).first() + current_version = getattr(res, 'schema_version', None) + + if current_version == SCHEMA_VERSION: + return + + _LOGGER.debug("Database requires upgrade. Schema version: %s", + current_version) + + if current_version is None: + current_version = _inspect_schema_version(instance.engine, session) + _LOGGER.debug("No schema version found. Inspected version: %s", + current_version) + + for version in range(current_version, SCHEMA_VERSION): + new_version = version + 1 + _LOGGER.info("Upgrading recorder db schema to version %s", + new_version) + _apply_update(instance.engine, new_version) + session.add(SchemaChanges(schema_version=new_version)) + + _LOGGER.info("Upgrade to version %s done", new_version) + + +def _apply_update(engine, new_version): + """Perform operations to bring schema up to date.""" + from sqlalchemy import Table + from . import models + + if new_version == 1: + def create_index(table_name, column_name): + """Create an index for the specified table and column.""" + table = Table(table_name, models.Base.metadata) + name = "_".join(("ix", table_name, column_name)) + # Look up the index object that was created from the models + index = next(idx for idx in table.indexes if idx.name == name) + _LOGGER.debug("Creating index for table %s column %s", + table_name, column_name) + index.create(engine) + _LOGGER.debug("Index creation done for table %s column %s", + table_name, column_name) + + create_index("events", "time_fired") + else: + raise ValueError("No schema migration defined for version {}" + .format(new_version)) + + +def _inspect_schema_version(engine, session): + """Determine the schema version by inspecting the db structure. + + When the schema verison is not present in the db, either db was just + created with the correct schema, or this is a db created before schema + versions were tracked. For now, we'll test if the changes for schema + version 1 are present to make the determination. Eventually this logic + can be removed and we can assume a new db is being created. + """ + from sqlalchemy.engine import reflection + from .models import SchemaChanges, SCHEMA_VERSION + + inspector = reflection.Inspector.from_engine(engine) + indexes = inspector.get_indexes("events") + + for index in indexes: + if index['column_names'] == ["time_fired"]: + # Schema addition from version 1 detected. New DB. + session.add(SchemaChanges( + schema_version=SCHEMA_VERSION)) + return SCHEMA_VERSION + + # Version 1 schema changes not found, this db needs to be migrated. + current_version = SchemaChanges(schema_version=0) + session.add(current_version) + return current_version.schema_version diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py new file mode 100644 index 00000000000000..2b675e72759fef --- /dev/null +++ b/homeassistant/components/recorder/purge.py @@ -0,0 +1,31 @@ +"""Purge old data helper.""" +from datetime import timedelta +import logging + +import homeassistant.util.dt as dt_util + +from .util import session_scope + +_LOGGER = logging.getLogger(__name__) + + +def purge_old_data(instance, purge_days): + """Purge events and states older than purge_days ago.""" + from .models import States, Events + purge_before = dt_util.utcnow() - timedelta(days=purge_days) + + with session_scope(session=instance.get_session()) as session: + deleted_rows = session.query(States) \ + .filter((States.created < purge_before)) \ + .delete(synchronize_session=False) + _LOGGER.debug("Deleted %s states", deleted_rows) + + deleted_rows = session.query(Events) \ + .filter((Events.created < purge_before)) \ + .delete(synchronize_session=False) + _LOGGER.debug("Deleted %s events", deleted_rows) + + # Execute sqlite vacuum command to free up space on disk + if instance.engine.driver == 'sqlite': + _LOGGER.info("Vacuuming SQLite to free space") + instance.engine.execute("VACUUM") diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py new file mode 100644 index 00000000000000..e4ea1af1060a10 --- /dev/null +++ b/homeassistant/components/recorder/util.py @@ -0,0 +1,71 @@ +"""SQLAlchemy util functions.""" +from contextlib import contextmanager +import logging +import time + +from .const import DATA_INSTANCE + +_LOGGER = logging.getLogger(__name__) + +RETRIES = 3 +QUERY_RETRY_WAIT = 0.1 + + +@contextmanager +def session_scope(*, hass=None, session=None): + """Provide a transactional scope around a series of operations.""" + if session is None and hass is not None: + session = hass.data[DATA_INSTANCE].get_session() + + if session is None: + raise RuntimeError('Session required') + + try: + yield session + session.commit() + except Exception as err: # pylint: disable=broad-except + _LOGGER.error('Error executing query: %s', err) + session.rollback() + raise + finally: + session.close() + + +def commit(session, work): + """Commit & retry work: Either a model or in a function.""" + import sqlalchemy.exc + for _ in range(0, RETRIES): + try: + if callable(work): + work(session) + else: + session.add(work) + session.commit() + return True + except sqlalchemy.exc.OperationalError as err: + _LOGGER.error('Error executing query: %s', err) + session.rollback() + time.sleep(QUERY_RETRY_WAIT) + return False + + +def execute(qry): + """Query the database and convert the objects to HA native form. + + This method also retries a few times in the case of stale connections. + """ + from sqlalchemy.exc import SQLAlchemyError + + for tryno in range(0, RETRIES): + try: + return [ + row for row in + (row.to_native() for row in qry) + if row is not None] + except SQLAlchemyError as err: + _LOGGER.error('Error executing query: %s', err) + + if tryno == RETRIES - 1: + raise + else: + time.sleep(QUERY_RETRY_WAIT) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 60d0b29c51d6f1..351b85cf90225c 100755 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -92,7 +92,7 @@ def _apply_service(service, service_func, *service_func_args): for device in _devices: service_func(device, *service_func_args) - device.update_ha_state(True) + device.schedule_update_ha_state(True) def _sync_service(service): diff --git a/homeassistant/components/remote/itach.py b/homeassistant/components/remote/itach.py index d76c39bf36a8d6..fa424576a116e0 100644 --- a/homeassistant/components/remote/itach.py +++ b/homeassistant/components/remote/itach.py @@ -17,7 +17,7 @@ from homeassistant.components.remote import ( PLATFORM_SCHEMA, ATTR_COMMAND) -REQUIREMENTS = ['pyitachip2ir==0.0.5'] +REQUIREMENTS = ['pyitachip2ir==0.0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 10ccf32068ff95..5999957066f8d2 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -152,9 +152,6 @@ def reconnect(exc=None): def connect(): """Set up connection and hook it into HA for reconnect/shutdown.""" _LOGGER.info('Initiating Rflink connection') - hass.states.async_set( - '{domain}.connection_status'.format( - domain=DOMAIN), 'connecting') # Rflink create_rflink_connection decides based on the value of host # (string or None) if serial or tcp mode should be used @@ -180,9 +177,6 @@ def connect(): _LOGGER.exception( "Error connecting to Rflink, reconnecting in %s", reconnect_interval) - hass.states.async_set( - '{domain}.connection_status'.format( - domain=DOMAIN), 'error') hass.loop.call_later(reconnect_interval, reconnect, exc) return @@ -195,9 +189,6 @@ def connect(): lambda x: transport.close()) _LOGGER.info('Connected to Rflink') - hass.states.async_set( - '{domain}.connection_status'.format( - domain=DOMAIN), 'connected') hass.async_add_job(connect) return True diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index c035836594caf1..6eaf9ad1cf9e62 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -334,7 +334,7 @@ def update_state(self, state, brightness=0): """Update det state of the device.""" self._state = state self._brightness = brightness - self.update_ha_state() + self.schedule_update_ha_state() def _send_command(self, command, brightness=0): if not self._event: @@ -369,4 +369,4 @@ def _send_command(self, command, brightness=0): for _ in range(self.signal_repetitions): self._event.device.send_stop(RFXOBJECT.transport) - self.update_ha_state() + self.schedule_update_ha_state() diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 7e20338f4ab0d3..1abe6432409598 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -18,7 +18,6 @@ from homeassistant.helpers.entity_component import EntityComponent DOMAIN = 'scene' -DEPENDENCIES = ['group'] STATE = 'scening' CONF_ENTITIES = "entities" diff --git a/homeassistant/components/scene/homeassistant.py b/homeassistant/components/scene/homeassistant.py index c7365ea65d914a..2081dfe89ab83d 100644 --- a/homeassistant/components/scene/homeassistant.py +++ b/homeassistant/components/scene/homeassistant.py @@ -13,7 +13,6 @@ from homeassistant.core import State from homeassistant.helpers.state import async_reproduce_state -DEPENDENCIES = ['group'] STATE = 'scening' CONF_ENTITIES = "entities" @@ -29,7 +28,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if not isinstance(scene_config, list): scene_config = [scene_config] - yield from async_add_devices(HomeAssistantScene( + async_add_devices(HomeAssistantScene( hass, _process_config(scene)) for scene in scene_config) return True diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 1cca7c8d79046e..bcab6465dc12b3 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, STATE_ON, CONF_ALIAS) + SERVICE_TOGGLE, SERVICE_RELOAD, STATE_ON, CONF_ALIAS) from homeassistant.core import split_entity_id from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -25,7 +25,6 @@ DOMAIN = "script" ENTITY_ID_FORMAT = DOMAIN + '.{}' GROUP_NAME_ALL_SCRIPTS = 'all scripts' -DEPENDENCIES = ["group"] CONF_SEQUENCE = "sequence" @@ -50,6 +49,7 @@ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_VARIABLES): dict, }) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) def is_on(hass, entity_id): @@ -57,6 +57,11 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) +def reload(hass): + """Reload script component.""" + hass.services.call(DOMAIN, SERVICE_RELOAD) + + def turn_on(hass, entity_id, variables=None): """Turn script on.""" _, object_id = split_entity_id(entity_id) @@ -77,29 +82,19 @@ def toggle(hass, entity_id): @asyncio.coroutine def async_setup(hass, config): """Load the scripts from the configuration.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, - group_name=GROUP_NAME_ALL_SCRIPTS) + component = EntityComponent( + _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS) + + yield from _async_process_config(hass, config, component) @asyncio.coroutine - def service_handler(service): - """Execute a service call to script.