diff --git a/.coveragerc b/.coveragerc index 15fa27dd1c0..cd86d001e37 100644 --- a/.coveragerc +++ b/.coveragerc @@ -37,6 +37,9 @@ omit = homeassistant/components/isy994.py homeassistant/components/*/isy994.py + homeassistant/components/litejet.py + homeassistant/components/*/litejet.py + homeassistant/components/modbus.py homeassistant/components/*/modbus.py @@ -98,8 +101,6 @@ omit = homeassistant/components/homematic.py homeassistant/components/*/homematic.py - homeassistant/components/switch/pilight.py - homeassistant/components/knx.py homeassistant/components/*/knx.py @@ -109,6 +110,9 @@ omit = homeassistant/components/zoneminder.py homeassistant/components/*/zoneminder.py + homeassistant/components/mochad.py + homeassistant/components/*/mochad.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py @@ -128,6 +132,7 @@ omit = homeassistant/components/climate/knx.py homeassistant/components/climate/proliphix.py homeassistant/components/climate/radiotherm.py + homeassistant/components/cover/garadget.py homeassistant/components/cover/homematic.py homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/scsgate.py @@ -136,10 +141,9 @@ omit = homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/bbox.py - homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bluetooth_le_tracker.py + homeassistant/components/device_tracker/bluetooth_tracker.py homeassistant/components/device_tracker/bt_home_hub_5.py - homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py @@ -157,8 +161,6 @@ omit = homeassistant/components/fan/mqtt.py homeassistant/components/feedreader.py homeassistant/components/foursquare.py - homeassistant/components/garage_door/rpi_gpio.py - homeassistant/components/garage_door/wink.py homeassistant/components/hdmi_cec.py homeassistant/components/ifttt.py homeassistant/components/joaoapps_join.py @@ -172,12 +174,14 @@ omit = homeassistant/components/light/limitlessled.py homeassistant/components/light/osramlightify.py homeassistant/components/light/x10.py + homeassistant/components/light/yeelight.py homeassistant/components/lirc.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py homeassistant/components/media_player/directv.py + homeassistant/components/media_player/emby.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/itunes.py @@ -188,6 +192,7 @@ omit = homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/pandora.py + homeassistant/components/media_player/philips_js.py homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/plex.py homeassistant/components/media_player/roku.py @@ -209,6 +214,7 @@ omit = homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py + homeassistant/components/notify/nfandroidtv.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushetta.py @@ -234,9 +240,12 @@ omit = homeassistant/components/sensor/bom.py homeassistant/components/sensor/coinmarketcap.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/dovado.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/eliqonline.py @@ -250,9 +259,11 @@ omit = homeassistant/components/sensor/gpsd.py homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/haveibeenpwned.py + homeassistant/components/sensor/hddtemp.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py + homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py @@ -267,7 +278,6 @@ omit = homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/plex.py - homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/serial_pm.py @@ -277,6 +287,7 @@ omit = homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_public_transport.py + homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/temper.py @@ -286,7 +297,6 @@ omit = homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py homeassistant/components/sensor/vasttrafik.py - homeassistant/components/sensor/worldclock.py homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/yweather.py homeassistant/components/switch/acer_projector.py @@ -299,18 +309,16 @@ omit = homeassistant/components/switch/neato.py homeassistant/components/switch/netio.py homeassistant/components/switch/orvibo.py + homeassistant/components/switch/pilight.py homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/wake_on_lan.py - homeassistant/components/thermostat/eq3btsmart.py - homeassistant/components/thermostat/heatmiser.py - homeassistant/components/thermostat/homematic.py - homeassistant/components/thermostat/proliphix.py - homeassistant/components/thermostat/radiotherm.py + homeassistant/components/thingspeak.py homeassistant/components/upnp.py + homeassistant/components/weather/openweathermap.py homeassistant/components/zeroconf.py diff --git a/.travis.yml b/.travis.yml index 3d575c1d778..9cf13f2c831 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,11 @@ sudo: false matrix: fast_finish: true include: - - python: "3.4" + - python: "3.4.2" env: TOXENV=py34 - - python: "3.4" + - python: "3.4.2" env: TOXENV=requirements - - python: "3.5" + - python: "3.4.2" env: TOXENV=lint - python: "3.5" env: TOXENV=typing diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 73c03f94144..08b0324371f 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -1,7 +1,7 @@ homeassistant: # Omitted values in this section will be auto detected using freegeoip.io - # Location required to calculate the time the sun rises and sets. + # Location required to calculate the time the sun rises and sets. # Coordinates are also used for location for weather related components. # Google Maps can be used to determine more precise GPS coordinates. latitude: 32.87336 @@ -43,12 +43,10 @@ device_tracker: username: admin password: PASSWORD -chromecast: - switch: platform: wemo -thermostat: +climate: platform: nest # Required: username and password that are used to login to the Nest thermostat. username: myemail@mydomain.com @@ -79,7 +77,6 @@ group: entities: - group.awesome_people - group.climate - kitchen: name: Kitchen entities: @@ -92,52 +89,23 @@ group: - input_boolean.notify_home - camera.demo_camera -example: - -simple_alarm: - # Which light/light group has to flash when a known device comes home - known_light: light.Bowl - # Which light/light group has to flash red when light turns on while no one home - unknown_light: group.living_room - browser: - keyboard: # https://home-assistant.io/getting-started/automation/ automation: -- alias: 'Rule 1 Light on in the evening' - trigger: - - platform: sun + - alias: Turn on light when sun sets + trigger: + platform: sun event: sunset offset: "-01:00:00" - - platform: state + condition: + condition: state entity_id: group.all_devices - state: home - condition: - - platform: state - entity_id: group.all_devices - state: home - - platform: time - after: "16:00:00" - before: "23:00:00" - action: - service: homeassistant.turn_on - entity_id: group.living_room + state: 'home' + action: + service: light.turn_on -- alias: 'Rule 2 - Away Mode' - trigger: - - platform: state - entity_id: group.all_devices - state: 'not_home' - - condition: use_trigger_values - action: - service: light.turn_off - entity_id: group.all_lights - -# Sensors need to be added into the configuration.yaml as sensor:, sensor 2:, sensor 3:, etc. -# Each sensor label should be unique or your sensors might not load correctly. # Another way to do is to collect all entries under one "sensor:" # sensor: # - platform: mqtt @@ -154,34 +122,30 @@ sensor: arg: '/' - type: 'disk_use_percent' arg: '/home' - - type: 'disk_use' - arg: '/home' sensor 2: - platform: forecast - api_key: - monitored_conditions: - - summary - - precip_type - - precip_intensity - - temperature + platform: cpuspeed script: - # Turns on the bedroom lights and then the living room lights 1 minute later wakeup: alias: Wake Up sequence: - # alias is optional + - event: LOGBOOK_ENTRY + event_data: + name: Paulus + message: is waking up + entity_id: device_tracker.paulus + domain: light - alias: Bedroom lights on - execute_service: light.turn_on - service_data: + service: light.turn_on + data: entity_id: group.bedroom + brightness: 100 - delay: - # supports seconds, milliseconds, minutes, hours, etc. minutes: 1 - alias: Living room lights on - execute_service: light.turn_on - service_data: + service: light.turn_on + data: entity_id: group.living_room scene: diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 30dfa6b6db0..510b98b6bdd 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( __version__, EVENT_HOMEASSISTANT_START, REQUIRED_PYTHON_VER, + REQUIRED_PYTHON_VER_WIN, RESTART_EXIT_CODE, ) from homeassistant.util.async import run_callback_threadsafe @@ -44,8 +45,7 @@ def monkey_patch_asyncio(): See https://bugs.python.org/issue26617 for details of the Python bug. """ - # pylint: disable=no-self-use, too-few-public-methods, protected-access - # pylint: disable=bare-except + # pylint: disable=no-self-use, protected-access, bare-except import asyncio.tasks class IgnoreCalls: @@ -64,7 +64,12 @@ def monkey_patch_asyncio(): def validate_python() -> None: """Validate we're running the right Python version.""" - if sys.version_info[:3] < REQUIRED_PYTHON_VER: + if sys.platform == "win32" and \ + sys.version_info[:3] < REQUIRED_PYTHON_VER_WIN: + print("Home Assistant requires at least Python {}.{}.{}".format( + *REQUIRED_PYTHON_VER_WIN)) + sys.exit(1) + elif sys.version_info[:3] < REQUIRED_PYTHON_VER: print("Home Assistant requires at least Python {}.{}.{}".format( *REQUIRED_PYTHON_VER)) sys.exit(1) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 8ad4e16c8cd..31e404ad87a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,11 +1,10 @@ """Provides methods to bootstrap a home assistant instance.""" - +import asyncio import logging import logging.handlers import os import sys from collections import defaultdict -from threading import RLock from types import ModuleType from typing import Any, Optional, Dict @@ -19,6 +18,8 @@ import homeassistant.config as conf_util import homeassistant.core as core 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.yaml import clear_secret_cache from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError @@ -26,24 +27,34 @@ from homeassistant.helpers import ( event_decorators, service, config_per_platform, extract_domain_configs) _LOGGER = logging.getLogger(__name__) -_SETUP_LOCK = RLock() -_CURRENT_SETUP = [] ATTR_COMPONENT = 'component' ERROR_LOG_FILENAME = 'home-assistant.log' -_PERSISTENT_PLATFORMS = set() -_PERSISTENT_VALIDATION = set() +_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 - _ensure_loader_prepared(hass) + if not loader.PREPARED: + yield from hass.loop.run_in_executor(None, loader.prepare, hass) if config is None: config = defaultdict(dict) @@ -52,11 +63,14 @@ def setup_component(hass: core.HomeAssistant, domain: str, # 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: - if not _setup_component(hass, component, config): + 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 @@ -64,7 +78,10 @@ def setup_component(hass: core.HomeAssistant, domain: str, def _handle_requirements(hass: core.HomeAssistant, component, name: str) -> bool: - """Install the requirements for a component.""" + """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 @@ -72,70 +89,117 @@ def _handle_requirements(hass: core.HomeAssistant, component, 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 -def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: - """Setup a component for Home Assistant.""" - # pylint: disable=too-many-return-statements,too-many-branches - # pylint: disable=too-many-statements +@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 - with _SETUP_LOCK: - # It might have been loaded while waiting for lock - 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) - if domain in _CURRENT_SETUP: - _LOGGER.error('Attempt made to setup %s during setup of %s', - domain, domain) - return False + setup_progress = hass.data.get('setup_progress') + if setup_progress is None: + setup_progress = hass.data['setup_progress'] = [] - config = prepare_setup_component(hass, config, domain) + 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) - _CURRENT_SETUP.append(domain) + if component is None: + _async_persistent_notification(hass, domain) + return False + + async_comp = hasattr(component, 'async_setup') try: - result = component.setup(hass, config) - if result is False: - _LOGGER.error('component %s failed to initialize', domain) - return False - elif result is not True: - _LOGGER.error('component %s did not return boolean if setup ' - 'was successful. Disabling component.', domain) - loader.set_component(domain, None) - return False + 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 - finally: - _CURRENT_SETUP.remove(domain) hass.config.components.append(component.DOMAIN) # Assumption: if a component does not depend on groups # it communicates with devices - if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ - hass.pool.worker_count <= 10: - hass.pool.add_worker() + if (not async_comp and + 'group' not in getattr(component, 'DEPENDENCIES', [])): + if hass.pool is None: + hass.async_init_pool() + if hass.pool.worker_count <= 10: + hass.pool.add_worker() - hass.bus.fire( + 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', []) @@ -151,7 +215,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, try: config = component.CONFIG_SCHEMA(config) except vol.Invalid as ex: - log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass) return None elif hasattr(component, 'PLATFORM_SCHEMA'): @@ -161,7 +225,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, try: p_validated = component.PLATFORM_SCHEMA(p_config) except vol.Invalid as ex: - log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass) continue # Not all platform components follow same pattern for platforms @@ -171,8 +235,8 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, platforms.append(p_validated) continue - platform = prepare_setup_platform(hass, config, domain, - p_name) + platform = yield from async_prepare_setup_platform( + hass, config, domain, p_name) if platform is None: continue @@ -180,10 +244,11 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, # 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: - log_exception(ex, '{}.{}'.format(domain, p_name), - p_validated, hass) + async_log_exception(ex, '{}.{}'.format(domain, p_name), + p_validated, hass) continue platforms.append(p_validated) @@ -195,7 +260,9 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, if key not in filter_keys} config[domain] = platforms - if not _handle_requirements(hass, component, domain): + res = yield from hass.loop.run_in_executor( + None, _handle_requirements, hass, component, domain) + if not res: return None return config @@ -204,7 +271,22 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict, def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, platform_name: str) -> Optional[ModuleType]: """Load a platform and makes sure dependencies are setup.""" - _ensure_loader_prepared(hass) + 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) @@ -213,13 +295,7 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, # Not found if platform is None: _LOGGER.error('Unable to find platform %s', platform_path) - - _PERSISTENT_PLATFORMS.add(platform_path) - message = ('Unable to find the following platforms: ' + - ', '.join(list(_PERSISTENT_PLATFORMS)) + - '(please check your configuration)') - persistent_notification.create( - hass, message, 'Invalid platforms', 'platform_errors') + _async_persistent_notification(hass, platform_path) return None # Already loaded @@ -228,20 +304,23 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str, # Load dependencies for component in getattr(platform, 'DEPENDENCIES', []): - if not setup_component(hass, component, config): + 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 - if not _handle_requirements(hass, platform, platform_path): + res = yield from hass.loop.run_in_executor( + None, _handle_requirements, hass, platform, platform_path) + if not res: return None return platform -# pylint: disable=too-many-branches, too-many-statements, too-many-arguments def from_config_dict(config: Dict[str, Any], hass: Optional[core.HomeAssistant]=None, config_dir: Optional[str]=None, @@ -261,15 +340,49 @@ 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.loop.create_task(_async_init_from_config_dict(future)) + hass.loop.run_until_complete(future) + + return future.result() + + +@asyncio.coroutine +def async_from_config_dict(config: Dict[str, Any], + hass: core.HomeAssistant, + config_dir: Optional[str]=None, + enable_log: bool=True, + verbose: bool=False, + skip_pip: bool=False, + log_rotate_days: Any=None) \ + -> Optional[core.HomeAssistant]: + """Try to configure Home Assistant from a config dict. + + Dynamically loads required components and its dependencies. + This method is a coroutine. + """ core_config = config.get(core.DOMAIN, {}) try: - conf_util.process_ha_core_config(hass, core_config) + yield from conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as ex: - log_exception(ex, 'homeassistant', core_config, hass) + async_log_exception(ex, 'homeassistant', core_config, hass) return None - conf_util.process_ha_config_upgrade(hass) + yield from hass.loop.run_in_executor( + None, conf_util.process_ha_config_upgrade, hass) if enable_log: enable_logging(hass, verbose, log_rotate_days) @@ -279,7 +392,8 @@ def from_config_dict(config: Dict[str, Any], _LOGGER.warning('Skipping pip installation of required modules. ' 'This may cause issues.') - _ensure_loader_prepared(hass) + if not loader.PREPARED: + yield from hass.loop.run_in_executor(None, loader.prepare, hass) # Make a copy because we are mutating it. # Convert it to defaultdict so components can always have config dict @@ -291,29 +405,25 @@ def from_config_dict(config: Dict[str, Any], components = set(key.split(' ')[0] for key in config.keys() if key != core.DOMAIN) - # Setup in a thread to avoid blocking - def component_setup(): - """Set up a component.""" - if not core_components.setup(hass, config): - _LOGGER.error('Home Assistant core failed to initialize. ' - 'Further initialization aborted.') - return hass + # setup components + # pylint: disable=not-an-iterable + res = yield from core_components.async_setup(hass, config) + if not res: + _LOGGER.error('Home Assistant core failed to initialize. ' + 'Further initialization aborted.') + return hass - persistent_notification.setup(hass, config) + yield from persistent_notification.async_setup(hass, config) - _LOGGER.info('Home Assistant core initialized') + _LOGGER.info('Home Assistant core initialized') - # Give event decorators access to HASS - event_decorators.HASS = hass - service.HASS = hass + # Give event decorators access to HASS + event_decorators.HASS = hass + service.HASS = hass - # Setup the components - for domain in loader.load_order_components(components): - _setup_component(hass, domain, config) - - hass.loop.run_until_complete( - hass.loop.run_in_executor(None, component_setup) - ) + # Setup the components + for domain in loader.load_order_components(components): + yield from _async_setup_component(hass, domain, config) return hass @@ -331,27 +441,62 @@ 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) + + return future.result() + + +@asyncio.coroutine +def async_from_config_file(config_path: str, + hass: core.HomeAssistant, + verbose: bool=False, + skip_pip: bool=True, + log_rotate_days: Any=None): + """Read the configuration file and try to start all the functionality. + + Will add functionality to 'hass' parameter. + This method is a coroutine. + """ # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - mount_local_lib_path(config_dir) + yield from hass.loop.run_in_executor( + None, mount_local_lib_path, config_dir) enable_logging(hass, verbose, log_rotate_days) try: - config_dict = conf_util.load_yaml_config_file(config_path) + config_dict = yield from hass.loop.run_in_executor( + None, conf_util.load_yaml_config_file, config_path) except HomeAssistantError: return None finally: clear_secret_cache() - return from_config_dict(config_dict, hass, enable_log=False, - skip_pip=skip_pip) + hass = yield from async_from_config_dict( + config_dict, hass, enable_log=False, skip_pip=skip_pip) + return hass def enable_logging(hass: core.HomeAssistant, verbose: bool=False, log_rotate_days=None) -> None: - """Setup the logging.""" + """Setup the logging. + + Async friendly. + """ logging.basicConfig(level=logging.INFO) fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s%(reset)s") @@ -359,6 +504,7 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, # suppress overly verbose logs from libraries that aren't helpful logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("aiohttp.access").setLevel(logging.WARNING) try: from colorlog import ColoredFormatter @@ -406,43 +552,62 @@ def enable_logging(hass: core.HomeAssistant, verbose: bool=False, 'Unable to setup error log %s (access denied)', err_log_path) -def _ensure_loader_prepared(hass: core.HomeAssistant) -> None: - """Ensure Home Assistant loader is prepared.""" - if not loader.PREPARED: - loader.prepare(hass) - - -def log_exception(ex, domain, config, hass=None): +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: - _PERSISTENT_VALIDATION.add(domain) - message = ('The following platforms contain invalid configuration: ' + - ', '.join(list(_PERSISTENT_VALIDATION)) + - ' (please check your configuration)') - persistent_notification.create( - hass, message, 'Invalid config', 'invalid_config') + _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('%s' % m for m in ex.path)) + '->'.join(str(m) for m in ex.path)) else: message += '{}.'.format(humanize_error(config, ex)) - if hasattr(config, '__line__'): - message += " (See {}:{})".format( - config.__config_file__, config.__line__ or '?') + domain_config = config.get(domain, config) + message += " (See {}:{}). ".format( + getattr(domain_config, '__config_file__', '?'), + getattr(domain_config, '__line__', '?')) if domain != 'homeassistant': - message += (' Please check the docs at ' + 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.""" + """Add local library to Python Path. + + Async friendly. + """ deps_dir = os.path.join(config_dir, 'deps') if deps_dir not in sys.path: sys.path.insert(0, os.path.join(config_dir, 'deps')) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 7d025bac765..81450c726f1 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -7,6 +7,7 @@ Component design guidelines: format ".". - Each component should publish services only under its own domain. """ +import asyncio import itertools as it import logging @@ -79,8 +80,10 @@ def reload_core_config(hass): hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup general services related to Home Assistant.""" + @asyncio.coroutine def handle_turn_service(service): """Method to handle calls to homeassistant.turn_on/off.""" entity_ids = extract_entity_ids(hass, service) @@ -96,6 +99,8 @@ def setup(hass, config): by_domain = it.groupby(sorted(entity_ids), lambda item: ha.split_entity_id(item)[0]) + tasks = [] + for domain, ent_ids in by_domain: # We want to block for all calls and only return when all calls # have been processed. If a service does not exist it causes a 10 @@ -111,27 +116,34 @@ def setup(hass, config): # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - hass.services.call(domain, service.service, data, blocking) + tasks.append(hass.services.async_call( + domain, service.service, data, blocking)) - hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) - hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) - hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + yield from asyncio.gather(*tasks, loop=hass.loop) + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + + @asyncio.coroutine def handle_reload_config(call): """Service handler for reloading core config.""" from homeassistant.exceptions import HomeAssistantError from homeassistant import config as conf_util try: - path = conf_util.find_config_file(hass.config.config_dir) - conf = conf_util.load_yaml_config_file(path) + conf = yield from conf_util.async_hass_config_yaml(hass) except HomeAssistantError as err: _LOGGER.error(err) return - conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) + yield from conf_util.async_process_ha_core_config( + hass, conf.get(ha.DOMAIN) or {}) - hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, - handle_reload_config) + hass.services.async_register( + ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config) return True diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index a986d911115..8bf36e176e5 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -42,8 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([AlarmDotCom(hass, name, code, username, password)]) -# pylint: disable=too-many-arguments, too-many-instance-attributes -# pylint: disable=abstract-method class AlarmDotCom(alarm.AlarmControlPanel): """Represent an Alarm.com status.""" diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index 0e0fd026b60..0bdcf274c08 100755 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -4,23 +4,19 @@ Support for Concord232 alarm control panels. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.concord232/ """ - import datetime - import logging +import requests +import voluptuous as vol + import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_PORT, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, STATE_UNKNOWN) + CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv -import requests - -import voluptuous as vol - REQUIREMENTS = ['concord232==0.14'] _LOGGER = logging.getLogger(__name__) @@ -29,17 +25,17 @@ DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'CONCORD232' DEFAULT_PORT = 5007 +SCAN_INTERVAL = 1 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, }) -SCAN_INTERVAL = 1 - def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup concord232 platform.""" + """Set up the Concord232 alarm control panel platform.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -49,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: add_devices([Concord232Alarm(hass, url, name)]) except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) return False @@ -57,7 +53,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): """Represents the Concord232-based alarm panel.""" def __init__(self, hass, url, name): - """Initalize the concord232 alarm panel.""" + """Initialize the Concord232 alarm panel.""" from concord232 import client as concord232_client self._state = STATE_UNKNOWN @@ -68,7 +64,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): try: client = concord232_client.Client(self._url) except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) self._alarm = client self._alarm.partitions = self._alarm.list_partitions() @@ -100,16 +96,16 @@ class Concord232Alarm(alarm.AlarmControlPanel): try: part = self._alarm.list_partitions()[0] except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to %(host)s: %(reason)s', + _LOGGER.error("Unable to connect to %(host)s: %(reason)s", dict(host=self._url, reason=ex)) newstate = STATE_UNKNOWN except IndexError: - _LOGGER.error('concord232 reports no partitions') + _LOGGER.error("Concord232 reports no partitions") newstate = STATE_UNKNOWN - if part['arming_level'] == "Off": + if part['arming_level'] == 'Off': newstate = STATE_ALARM_DISARMED - elif "Home" in part['arming_level']: + elif 'Home' in part['arming_level']: newstate = STATE_ALARM_ARMED_HOME else: newstate = STATE_ALARM_ARMED_AWAY diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index ff1ec2cc7b7..fa013cd3ffe 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -5,25 +5,22 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.envisalink/ """ import logging + import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.envisalink import (EVL_CONTROLLER, - EnvisalinkDevice, - PARTITION_SCHEMA, - CONF_CODE, - CONF_PANIC, - CONF_PARTITIONNAME, - SIGNAL_PARTITION_UPDATE, - SIGNAL_KEYPAD_UPDATE) +from homeassistant.components.envisalink import ( + EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC, + CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, STATE_ALARM_TRIGGERED) -DEPENDENCIES = ['envisalink'] _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['envisalink'] + # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Perform the setup for Envisalink alarm panels.""" _configured_partitions = discovery_info['partitions'] _code = discovery_info[CONF_CODE] @@ -38,30 +35,29 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _panic_type, EVL_CONTROLLER.alarm_state['partition'][part_num], EVL_CONTROLLER) - add_devices_callback([_device]) + add_devices([_device]) return True class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): - """Represents the Envisalink-based alarm panel.""" + """Representation of an Envisalink-based alarm panel.""" - # pylint: disable=too-many-arguments - def __init__(self, partition_number, alarm_name, - code, panic_type, info, controller): + def __init__(self, partition_number, alarm_name, code, panic_type, info, + controller): """Initialize the alarm panel.""" from pydispatch import dispatcher self._partition_number = partition_number self._code = code self._panic_type = panic_type - _LOGGER.debug('Setting up alarm: ' + alarm_name) + _LOGGER.debug("Setting up alarm: %s", alarm_name) EnvisalinkDevice.__init__(self, alarm_name, info, controller) - dispatcher.connect(self._update_callback, - signal=SIGNAL_PARTITION_UPDATE, - sender=dispatcher.Any) - dispatcher.connect(self._update_callback, - signal=SIGNAL_KEYPAD_UPDATE, - sender=dispatcher.Any) + dispatcher.connect( + self._update_callback, signal=SIGNAL_PARTITION_UPDATE, + sender=dispatcher.Any) + dispatcher.connect( + self._update_callback, signal=SIGNAL_KEYPAD_UPDATE, + sender=dispatcher.Any) def _update_callback(self, partition): """Update HA state, if needed.""" @@ -90,20 +86,20 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """Send disarm command.""" if self._code: - EVL_CONTROLLER.disarm_partition(str(code), - self._partition_number) + EVL_CONTROLLER.disarm_partition( + str(code), self._partition_number) def alarm_arm_home(self, code=None): """Send arm home command.""" if self._code: - EVL_CONTROLLER.arm_stay_partition(str(code), - self._partition_number) + EVL_CONTROLLER.arm_stay_partition( + str(code), self._partition_number) def alarm_arm_away(self, code=None): """Send arm away command.""" if self._code: - EVL_CONTROLLER.arm_away_partition(str(code), - self._partition_number) + EVL_CONTROLLER.arm_away_partition( + str(code), self._partition_number) def alarm_trigger(self, code=None): """Alarm trigger command. Will be used to trigger a panic alarm.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 986f1966797..9a7efbeaf5a 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -50,8 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes -# pylint: disable=abstract-method class ManualAlarm(alarm.AlarmControlPanel): """ Represents an alarm status. diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index b5bdf478add..26e2a2f1f77 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -55,8 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_CODE))]) -# pylint: disable=too-many-arguments, too-many-instance-attributes -# pylint: disable=abstract-method class MqttAlarm(alarm.AlarmControlPanel): """Representation of a MQTT alarm status.""" diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 38128489ba0..40ebfb2f39f 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([SimpliSafeAlarm(name, username, password, code)]) -# pylint: disable=abstract-method class SimpliSafeAlarm(alarm.AlarmControlPanel): """Representation a SimpliSafe alarm.""" diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 248d575baf7..4ef07c68f59 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -28,7 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(alarms) -# pylint: disable=abstract-method class VerisureAlarm(alarm.AlarmControlPanel): """Represent a Verisure alarm status.""" diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 64ff50af323..72c0b2a8705 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -4,6 +4,7 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ +import asyncio import copy import enum import logging @@ -12,6 +13,7 @@ from datetime import datetime import voluptuous as vol +from homeassistant.core import callback from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.helpers import template, script, config_validation as cv from homeassistant.components.http import HomeAssistantView @@ -20,7 +22,7 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) INTENTS_API_ENDPOINT = '/api/alexa' -FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/' +FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}' CONF_ACTION = 'action' CONF_CARD = 'card' @@ -102,8 +104,8 @@ def setup(hass, config): intents = config[DOMAIN].get(CONF_INTENTS, {}) flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {}) - hass.wsgi.register_view(AlexaIntentsView(hass, intents)) - hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings)) + hass.http.register_view(AlexaIntentsView(hass, intents)) + hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings)) return True @@ -128,9 +130,10 @@ class AlexaIntentsView(HomeAssistantView): self.intents = intents + @asyncio.coroutine def post(self, request): """Handle Alexa.""" - data = request.json + data = yield from request.json() _LOGGER.debug('Received Alexa request: %s', data) @@ -176,7 +179,7 @@ class AlexaIntentsView(HomeAssistantView): action = config.get(CONF_ACTION) if action is not None: - action.run(response.variables) + yield from action.async_run(response.variables) # pylint: disable=unsubscriptable-object if speech is not None: @@ -218,8 +221,8 @@ class AlexaResponse(object): self.card = card return - card["title"] = title.render(self.variables) - card["content"] = content.render(self.variables) + card["title"] = title.async_render(self.variables) + card["content"] = content.async_render(self.variables) self.card = card def add_speech(self, speech_type, text): @@ -229,7 +232,7 @@ class AlexaResponse(object): key = 'ssml' if speech_type == SpeechType.ssml else 'text' if isinstance(text, template.Template): - text = text.render(self.variables) + text = text.async_render(self.variables) self.speech = { 'type': speech_type.value, @@ -244,7 +247,7 @@ class AlexaResponse(object): self.reprompt = { 'type': speech_type.value, - key: text.render(self.variables) + key: text.async_render(self.variables) } def as_dict(self): @@ -283,7 +286,7 @@ class AlexaFlashBriefingView(HomeAssistantView): self.flash_briefings = copy.deepcopy(flash_briefings) template.attach(hass, self.flash_briefings) - # pylint: disable=too-many-branches + @callback def get(self, request, briefing_id): """Handle Alexa Flash Briefing request.""" _LOGGER.debug('Received Alexa flash briefing request for: %s', @@ -292,7 +295,7 @@ class AlexaFlashBriefingView(HomeAssistantView): if self.flash_briefings.get(briefing_id) is None: err = 'No configured Alexa flash briefing was found for: %s' _LOGGER.error(err, briefing_id) - return self.Response(status=404) + return b'', 404 briefing = [] @@ -300,13 +303,13 @@ class AlexaFlashBriefingView(HomeAssistantView): output = {} if item.get(CONF_TITLE) is not None: if isinstance(item.get(CONF_TITLE), template.Template): - output[ATTR_TITLE_TEXT] = item[CONF_TITLE].render() + output[ATTR_TITLE_TEXT] = item[CONF_TITLE].async_render() else: output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE) if item.get(CONF_TEXT) is not None: if isinstance(item.get(CONF_TEXT), template.Template): - output[ATTR_MAIN_TEXT] = item[CONF_TEXT].render() + output[ATTR_MAIN_TEXT] = item[CONF_TEXT].async_render() else: output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) @@ -315,7 +318,7 @@ class AlexaFlashBriefingView(HomeAssistantView): if item.get(CONF_AUDIO) is not None: if isinstance(item.get(CONF_AUDIO), template.Template): - output[ATTR_STREAM_URL] = item[CONF_AUDIO].render() + output[ATTR_STREAM_URL] = item[CONF_AUDIO].async_render() else: output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) @@ -323,7 +326,7 @@ class AlexaFlashBriefingView(HomeAssistantView): if isinstance(item.get(CONF_DISPLAY_URL), template.Template): output[ATTR_REDIRECTION_URL] = \ - item[CONF_DISPLAY_URL].render() + item[CONF_DISPLAY_URL].async_render() else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 5eb28c53a34..ae5e1de7c1b 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/ import asyncio import json import logging -import queue + +from aiohttp import web +import async_timeout import homeassistant.core as ha import homeassistant.remote as rem @@ -21,7 +23,7 @@ from homeassistant.const import ( URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, __version__) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.state import TrackStates +from homeassistant.helpers.state import AsyncTrackStates from homeassistant.helpers import template from homeassistant.components.http import HomeAssistantView @@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """Register the API with the HTTP interface.""" - hass.wsgi.register_view(APIStatusView) - hass.wsgi.register_view(APIEventStream) - hass.wsgi.register_view(APIConfigView) - hass.wsgi.register_view(APIDiscoveryView) - hass.wsgi.register_view(APIStatesView) - hass.wsgi.register_view(APIEntityStateView) - hass.wsgi.register_view(APIEventListenersView) - hass.wsgi.register_view(APIEventView) - hass.wsgi.register_view(APIServicesView) - hass.wsgi.register_view(APIDomainServicesView) - hass.wsgi.register_view(APIEventForwardingView) - hass.wsgi.register_view(APIComponentsView) - hass.wsgi.register_view(APIErrorLogView) - hass.wsgi.register_view(APITemplateView) + hass.http.register_view(APIStatusView) + hass.http.register_view(APIEventStream) + hass.http.register_view(APIConfigView) + hass.http.register_view(APIDiscoveryView) + hass.http.register_view(APIStatesView) + hass.http.register_view(APIEntityStateView) + hass.http.register_view(APIEventListenersView) + hass.http.register_view(APIEventView) + hass.http.register_view(APIServicesView) + hass.http.register_view(APIDomainServicesView) + hass.http.register_view(APIEventForwardingView) + hass.http.register_view(APIComponentsView) + hass.http.register_view(APIErrorLogView) + hass.http.register_view(APITemplateView) return True @@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView): url = URL_API name = "api:status" + @ha.callback def get(self, request): """Retrieve if API is running.""" return self.json_message('API running.') @@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView): url = URL_API_STREAM name = "api:stream" + @asyncio.coroutine def get(self, request): """Provide a streaming interface for the event bus.""" stop_obj = object() - to_write = queue.Queue() + to_write = asyncio.Queue(loop=self.hass.loop) - restrict = request.args.get('restrict') + restrict = request.GET.get('restrict') if restrict: restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] @@ -96,38 +100,40 @@ class APIEventStream(HomeAssistantView): else: data = json.dumps(event, cls=rem.JSONEncoder) - to_write.put(data) + yield from to_write.put(data) - def stream(): - """Stream events to response.""" - unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events) + response = web.StreamResponse() + response.content_type = 'text/event-stream' + yield from response.prepare(request) - try: - _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) + unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events) - # Fire off one message so browsers fire open event right away - to_write.put(STREAM_PING_PAYLOAD) + try: + _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) - while True: - try: - payload = to_write.get(timeout=STREAM_PING_INTERVAL) + # Fire off one message so browsers fire open event right away + yield from to_write.put(STREAM_PING_PAYLOAD) - if payload is stop_obj: - break + while True: + try: + with async_timeout.timeout(STREAM_PING_INTERVAL, + loop=self.hass.loop): + payload = yield from to_write.get() - msg = "data: {}\n\n".format(payload) - _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), - msg.strip()) - yield msg.encode("UTF-8") - except queue.Empty: - to_write.put(STREAM_PING_PAYLOAD) - except GeneratorExit: + if payload is stop_obj: break - finally: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) - unsub_stream() - return self.Response(stream(), mimetype='text/event-stream') + msg = "data: {}\n\n".format(payload) + _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), + msg.strip()) + response.write(msg.encode("UTF-8")) + yield from response.drain() + except asyncio.TimeoutError: + yield from to_write.put(STREAM_PING_PAYLOAD) + + finally: + _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + unsub_stream() class APIConfigView(HomeAssistantView): @@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView): url = URL_API_CONFIG name = "api:config" + @ha.callback def get(self, request): """Get current configuration.""" return self.json(self.hass.config.as_dict()) @@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView): url = URL_API_DISCOVERY_INFO name = "api:discovery" + @ha.callback def get(self, request): """Get discovery info.""" needs_auth = self.hass.config.api.api_password is not None @@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView): url = URL_API_STATES name = "api:states" + @ha.callback def get(self, request): """Get current states.""" - return self.json(self.hass.states.all()) + return self.json(self.hass.states.async_all()) class APIEntityStateView(HomeAssistantView): """View to handle EntityState requests.""" - url = "/api/states/" + url = "/api/states/{entity_id}" name = "api:entity-state" + @ha.callback def get(self, request, entity_id): """Retrieve state of entity.""" state = self.hass.states.get(entity_id) @@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView): else: return self.json_message('Entity not found', HTTP_NOT_FOUND) + @asyncio.coroutine def post(self, request, entity_id): """Update state of entity.""" try: - new_state = request.json['state'] - except KeyError: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON specified', + HTTP_BAD_REQUEST) + + new_state = data.get('state') + + if not new_state: return self.json_message('No state specified', HTTP_BAD_REQUEST) - attributes = request.json.get('attributes') - force_update = request.json.get('force_update', False) + attributes = data.get('attributes') + force_update = data.get('force_update', False) is_new_state = self.hass.states.get(entity_id) is None # Write state - self.hass.states.set(entity_id, new_state, attributes, force_update) + self.hass.states.async_set(entity_id, new_state, attributes, + force_update) # Read the state back for our response - resp = self.json(self.hass.states.get(entity_id)) - - if is_new_state: - resp.status_code = HTTP_CREATED + status_code = HTTP_CREATED if is_new_state else 200 + resp = self.json(self.hass.states.get(entity_id), status_code) resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) return resp + @ha.callback def delete(self, request, entity_id): """Remove entity.""" - if self.hass.states.remove(entity_id): + if self.hass.states.async_remove(entity_id): return self.json_message('Entity removed') else: return self.json_message('Entity not found', HTTP_NOT_FOUND) @@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView): url = URL_API_EVENTS name = "api:event-listeners" + @ha.callback def get(self, request): """Get event listeners.""" - return self.json(events_json(self.hass)) + return self.json(async_events_json(self.hass)) class APIEventView(HomeAssistantView): """View to handle Event requests.""" - url = '/api/events/' + url = '/api/events/{event_type}' name = "api:event" + @asyncio.coroutine def post(self, request, event_type): """Fire events.""" - event_data = request.json + body = yield from request.text() + event_data = json.loads(body) if body else None if event_data is not None and not isinstance(event_data, dict): return self.json_message('Event data should be a JSON object', @@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView): if state: event_data[key] = state - self.hass.bus.fire(event_type, event_data, ha.EventOrigin.remote) + self.hass.bus.async_fire(event_type, event_data, ha.EventOrigin.remote) return self.json_message("Event {} fired.".format(event_type)) @@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView): url = URL_API_SERVICES name = "api:services" + @ha.callback def get(self, request): """Get registered services.""" - return self.json(services_json(self.hass)) + return self.json(async_services_json(self.hass)) class APIDomainServicesView(HomeAssistantView): """View to handle DomainServices requests.""" - url = "/api/services//" + url = "/api/services/{domain}/{service}" name = "api:domain-services" + @asyncio.coroutine def post(self, request, domain, service): """Call a service. Returns a list of changed states. """ - with TrackStates(self.hass) as changed_states: - self.hass.services.call(domain, service, request.json, True) + body = yield from request.text() + data = json.loads(body) if body else None + + with AsyncTrackStates(self.hass) as changed_states: + yield from self.hass.services.async_call(domain, service, data, + True) return self.json(changed_states) @@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView): name = "api:event-forward" event_forwarder = None + @asyncio.coroutine def post(self, request): """Setup an event forwarder.""" - data = request.json - if data is None: + try: + data = yield from request.json() + except ValueError: return self.json_message("No data received.", HTTP_BAD_REQUEST) + try: host = data['host'] api_password = data['api_password'] @@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView): api = rem.API(host, api_password, port) - if not api.validate_api(): + valid = yield from self.hass.loop.run_in_executor( + None, api.validate_api) + if not valid: return self.json_message("Unable to validate API.", HTTP_UNPROCESSABLE_ENTITY) if self.event_forwarder is None: self.event_forwarder = rem.EventForwarder(self.hass) - self.event_forwarder.connect(api) + self.event_forwarder.async_connect(api) return self.json_message("Event forwarding setup.") + @asyncio.coroutine def delete(self, request): - """Remove event forwarer.""" - data = request.json - if data is None: + """Remove event forwarder.""" + try: + data = yield from request.json() + except ValueError: return self.json_message("No data received.", HTTP_BAD_REQUEST) try: @@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView): if self.event_forwarder is not None: api = rem.API(host, None, port) - self.event_forwarder.disconnect(api) + self.event_forwarder.async_disconnect(api) return self.json_message("Event forwarding cancelled.") @@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView): url = URL_API_COMPONENTS name = "api:components" + @ha.callback def get(self, request): """Get current loaded components.""" return self.json(self.hass.config.components) @@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView): url = URL_API_ERROR_LOG name = "api:error-log" + @asyncio.coroutine def get(self, request): """Serve error log.""" - return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME)) + resp = yield from self.file( + request, self.hass.config.path(ERROR_LOG_FILENAME)) + return resp class APITemplateView(HomeAssistantView): @@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView): url = URL_API_TEMPLATE name = "api:template" + @asyncio.coroutine def post(self, request): """Render a template.""" try: - tpl = template.Template(request.json['template'], self.hass) - return tpl.render(request.json.get('variables')) - except TemplateError as ex: + data = yield from request.json() + tpl = template.Template(data['template'], self.hass) + return tpl.async_render(data.get('variables')) + except (ValueError, TemplateError) as ex: return self.json_message('Error rendering template: {}'.format(ex), HTTP_BAD_REQUEST) -def services_json(hass): +def async_services_json(hass): """Generate services data to JSONify.""" return [{"domain": key, "services": value} - for key, value in hass.services.services.items()] + for key, value in hass.services.async_services().items()] -def events_json(hass): +def async_events_json(hass): """Generate event data to JSONify.""" return [{"event": key, "listener_count": value} - for key, value in hass.bus.listeners.items()] + for key, value in hass.bus.async_listeners().items()] diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 244887ca10a..e88caab6824 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,8 +11,7 @@ import os import voluptuous as vol -from homeassistant.core import callback -from homeassistant.bootstrap import prepare_setup_platform +from homeassistant.bootstrap 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, @@ -25,7 +24,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv -from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'automation' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -144,42 +142,50 @@ def reload(hass): hass.services.call(DOMAIN, SERVICE_RELOAD) -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup the automation.""" component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS) - success = run_coroutine_threadsafe( - _async_process_config(hass, config, component), hass.loop).result() + success = yield from _async_process_config(hass, config, component) if not success: return False - descriptions = conf_util.load_yaml_config_file( - os.path.join(os.path.dirname(__file__), 'services.yaml')) + descriptions = yield from hass.loop.run_in_executor( + None, conf_util.load_yaml_config_file, os.path.join( + os.path.dirname(__file__), 'services.yaml') + ) - @callback + @asyncio.coroutine def trigger_service_handler(service_call): """Handle automation triggers.""" + tasks = [] for entity in component.async_extract_from_service(service_call): - hass.loop.create_task(entity.async_trigger( + tasks.append(entity.async_trigger( service_call.data.get(ATTR_VARIABLES), True)) + yield from asyncio.gather(*tasks, loop=hass.loop) - @callback + @asyncio.coroutine def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" + tasks = [] method = 'async_{}'.format(service_call.service) for entity in component.async_extract_from_service(service_call): - hass.loop.create_task(getattr(entity, method)()) + tasks.append(getattr(entity, method)()) + yield from asyncio.gather(*tasks, loop=hass.loop) - @callback + @asyncio.coroutine def toggle_service_handler(service_call): """Handle automation toggle service calls.""" + tasks = [] for entity in component.async_extract_from_service(service_call): if entity.is_on: - hass.loop.create_task(entity.async_turn_off()) + tasks.append(entity.async_turn_off()) else: - hass.loop.create_task(entity.async_turn_on()) + tasks.append(entity.async_turn_on()) + yield from asyncio.gather(*tasks, loop=hass.loop) @asyncio.coroutine def reload_service_handler(service_call): @@ -187,24 +193,24 @@ def setup(hass, config): conf = yield from component.async_prepare_reload() if conf is None: return - hass.loop.create_task(_async_process_config(hass, conf, component)) + yield from _async_process_config(hass, conf, component) - hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler, - descriptions.get(SERVICE_TRIGGER), - schema=TRIGGER_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TRIGGER, trigger_service_handler, + descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler, - descriptions.get(SERVICE_RELOAD), - schema=RELOAD_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_RELOAD, reload_service_handler, + descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA) - hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler, - descriptions.get(SERVICE_TOGGLE), - schema=SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_TOGGLE, toggle_service_handler, + descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA) for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): - hass.services.register(DOMAIN, service, turn_onoff_service_handler, - descriptions.get(service), - schema=SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, service, turn_onoff_service_handler, + descriptions.get(service), schema=SERVICE_SCHEMA) return True @@ -212,8 +218,6 @@ def setup(hass, config): class AutomationEntity(ToggleEntity): """Entity to show status of entity.""" - # pylint: disable=abstract-method - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, name, async_attach_triggers, cond_func, async_action, hidden): """Initialize an automation entity.""" @@ -260,7 +264,7 @@ class AutomationEntity(ToggleEntity): return yield from self.async_enable() - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() @asyncio.coroutine def async_turn_off(self, **kwargs) -> None: @@ -271,8 +275,6 @@ class AutomationEntity(ToggleEntity): self._async_detach_triggers() self._async_detach_triggers = None self._enabled = False - # It's important that the update is finished before this method - # ends because async_remove depends on it. yield from self.async_update_ha_state() @asyncio.coroutine @@ -284,7 +286,7 @@ class AutomationEntity(ToggleEntity): if skip_condition or self._cond_func(variables): yield from self._async_action(self.entity_id, variables) self._last_triggered = utcnow() - self.hass.loop.create_task(self.async_update_ha_state()) + yield from self.async_update_ha_state() @asyncio.coroutine def async_remove(self): @@ -347,7 +349,7 @@ def _async_process_config(hass, config, component): entities.append(entity) yield from asyncio.gather(*tasks, loop=hass.loop) - hass.loop.create_task(component.async_add_entities(entities)) + yield from component.async_add_entities(entities) return len(entities) > 0 @@ -362,7 +364,7 @@ def _async_get_action(hass, config, name): _LOGGER.info('Executing %s', name) logbook.async_log_entry( hass, name, 'has been triggered', DOMAIN, entity_id) - hass.loop.create_task(script_obj.async_run(variables)) + yield from script_obj.async_run(variables) return action @@ -395,9 +397,8 @@ def _async_process_trigger(hass, config, trigger_configs, name, action): removes = [] for conf in trigger_configs: - platform = yield from hass.loop.run_in_executor( - None, prepare_setup_platform, hass, config, DOMAIN, - conf.get(CONF_PLATFORM)) + platform = yield from async_prepare_setup_platform( + hass, config, DOMAIN, conf.get(CONF_PLATFORM)) if platform is None: return None diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py new file mode 100644 index 00000000000..875a24540ee --- /dev/null +++ b/homeassistant/components/automation/litejet.py @@ -0,0 +1,41 @@ +""" +Trigger an automation when a LiteJet switch is released. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/automation.litejet/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import CONF_PLATFORM +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['litejet'] + +_LOGGER = logging.getLogger(__name__) + +CONF_NUMBER = 'number' + +TRIGGER_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'litejet', + vol.Required(CONF_NUMBER): cv.positive_int +}) + + +def async_trigger(hass, config, action): + """Listen for events based on configuration.""" + number = config.get(CONF_NUMBER) + + @callback + def call_action(): + """Call action with right context.""" + hass.async_run_job(action, { + 'trigger': { + CONF_PLATFORM: 'litejet', + CONF_NUMBER: number + }, + }) + + hass.data['litejet_system'].on_switch_released(number, call_action) diff --git a/homeassistant/components/binary_sensor/arest.py b/homeassistant/components/binary_sensor/arest.py index 02f9d75c404..1c7058cd1b0 100644 --- a/homeassistant/components/binary_sensor/arest.py +++ b/homeassistant/components/binary_sensor/arest.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor_class, pin)]) -# pylint: disable=too-many-instance-attributes, too-many-arguments class ArestBinarySensor(BinarySensorDevice): """Implement an aREST binary sensor for a pin.""" @@ -93,7 +92,6 @@ class ArestBinarySensor(BinarySensorDevice): self.arest.update() -# pylint: disable=too-few-public-methods class ArestData(object): """Class for handling the data retrieval for pins.""" diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 57b3c7c03f7..6950e0b80c4 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -53,7 +53,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): value_template)]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class CommandBinarySensor(BinarySensorDevice): """Represent a command line binary sensor.""" diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py index bc1eab4694a..d9cb11ba6a7 100755 --- a/homeassistant/components/binary_sensor/concord232.py +++ b/homeassistant/components/binary_sensor/concord232.py @@ -5,20 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.concord232/ """ import datetime - import logging +import requests +import voluptuous as vol + from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES) from homeassistant.const import (CONF_HOST, CONF_PORT) - import homeassistant.helpers.config_validation as cv -import requests - -import voluptuous as vol - - REQUIREMENTS = ['concord232==0.14'] _LOGGER = logging.getLogger(__name__) @@ -27,9 +23,12 @@ CONF_EXCLUDE_ZONES = 'exclude_zones' CONF_ZONE_TYPES = 'zone_types' DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'Alarm' DEFAULT_PORT = '5007' DEFAULT_SSL = False +SCAN_INTERVAL = 1 + ZONE_TYPES_SCHEMA = vol.Schema({ cv.positive_int: vol.In(SENSOR_CLASSES), }) @@ -42,14 +41,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA, }) -SCAN_INTERVAL = 1 -DEFAULT_NAME = "Alarm" - - -# pylint: disable=too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Concord232 binary sensor platform.""" + """Set up the Concord232 binary sensor platform.""" from concord232 import client as concord232_client host = config.get(CONF_HOST) @@ -59,24 +53,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors = [] try: - _LOGGER.debug('Initializing Client.') - client = concord232_client.Client('http://{}:{}' - .format(host, port)) + _LOGGER.debug("Initializing Client") + client = concord232_client.Client('http://{}:{}'.format(host, port)) client.zones = client.list_zones() client.last_zone_update = datetime.datetime.now() except requests.exceptions.ConnectionError as ex: - _LOGGER.error('Unable to connect to Concord232: %s', str(ex)) + _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) return False for zone in client.zones: - _LOGGER.info('Loading Zone found: %s', zone['name']) + _LOGGER.info("Loading Zone found: %s", zone['name']) if zone['number'] not in exclude: - sensors.append(Concord232ZoneSensor( - hass, - client, - zone, - zone_types.get(zone['number'], get_opening_type(zone)))) + sensors.append( + Concord232ZoneSensor( + hass, client, zone, zone_types.get(zone['number'], + get_opening_type(zone))) + ) add_devices(sensors) @@ -84,16 +77,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def get_opening_type(zone): - """Helper function to try to guess sensor type frm name.""" - if "MOTION" in zone["name"]: - return "motion" - if "KEY" in zone["name"]: - return "safety" - if "SMOKE" in zone["name"]: - return "smoke" - if "WATER" in zone["name"]: - return "water" - return "opening" + """Helper function to try to guess sensor type from name.""" + if 'MOTION' in zone['name']: + return 'motion' + if 'KEY' in zone['name']: + return 'safety' + if 'SMOKE' in zone['name']: + return 'smoke' + if 'WATER' in zone['name']: + return 'water' + return 'opening' class Concord232ZoneSensor(BinarySensorDevice): diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index b0dad12522b..5bbc15aefcf 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): """Representation of an Envisalink binary sensor.""" - # pylint: disable=too-many-arguments def __init__(self, zone_number, zone_name, zone_type, info, controller): """Initialize the binary_sensor.""" from pydispatch import dispatcher diff --git a/homeassistant/components/binary_sensor/ffmpeg.py b/homeassistant/components/binary_sensor/ffmpeg.py index ce89ae2e4db..72140936e18 100644 --- a/homeassistant/components/binary_sensor/ffmpeg.py +++ b/homeassistant/components/binary_sensor/ffmpeg.py @@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): from haffmpeg import SensorNoise, SensorMotion # check source - if not run_test(config.get(CONF_INPUT)): + if not run_test(hass, config.get(CONF_INPUT)): return # generate sensor object diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index e26ee7d08dc..53c8e9a60b6 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -51,7 +51,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttBinarySensor(BinarySensorDevice): """Representation a binary sensor that is updated by MQTT.""" diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 4dfe4d58b99..65fe6041f34 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.sensor.nest import NestSensor from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) -import homeassistant.components.nest as nest +from homeassistant.components.nest import DATA_NEST import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['nest'] @@ -35,9 +35,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup Nest binary sensors.""" + nest = hass.data[DATA_NEST] + + all_sensors = [] for structure, device in nest.devices(): - add_devices([NestBinarySensor(structure, device, variable) - for variable in config[CONF_MONITORED_CONDITIONS]]) + all_sensors.extend( + [NestBinarySensor(structure, device, variable) + for variable in config[CONF_MONITORED_CONDITIONS]]) + + add_devices(all_sensors, True) class NestBinarySensor(NestSensor, BinarySensorDevice): @@ -46,4 +52,8 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): @property def is_on(self): """True if the binary sensor is on.""" - return bool(getattr(self.device, self.variable)) + return self._state + + def update(self): + """Retrieve latest state.""" + self._state = bool(getattr(self.device, self.variable)) diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py index 6763eaafa55..4284d2844bd 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -58,11 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=too-many-instance-attributes class OctoPrintBinarySensor(BinarySensorDevice): """Representation an OctoPrint binary sensor.""" - # pylint: disable=too-many-arguments def __init__(self, api, condition, sensor_type, sensor_name, unit, endpoint, group, tool=None): """Initialize a new OctoPrint sensor.""" diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 36b455f9dfe..72f506eaefc 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-variable, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the REST binary sensor.""" name = config.get(CONF_NAME) @@ -76,7 +75,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass, rest, name, sensor_class, value_template)]) -# pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): """Representation of a REST binary sensor.""" diff --git a/homeassistant/components/binary_sensor/rpi_gpio.py b/homeassistant/components/binary_sensor/rpi_gpio.py index 7bc05cd6764..13dd7d0b860 100644 --- a/homeassistant/components/binary_sensor/rpi_gpio.py +++ b/homeassistant/components/binary_sensor/rpi_gpio.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(binary_sensors) -# pylint: disable=too-many-arguments, too-many-instance-attributes class RPiGPIOBinarySensor(BinarySensorDevice): """Represent a binary sensor that uses Raspberry Pi GPIO.""" diff --git a/homeassistant/components/binary_sensor/sleepiq.py b/homeassistant/components/binary_sensor/sleepiq.py index 08d575ebcb8..0409d04f9a5 100644 --- a/homeassistant/components/binary_sensor/sleepiq.py +++ b/homeassistant/components/binary_sensor/sleepiq.py @@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(dev) -# pylint: disable=too-many-instance-attributes class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice): """Implementation of a SleepIQ presence sensor.""" diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 365b29eb308..6d470f335e3 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -63,14 +63,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.error('No sensors added') return False - hass.loop.create_task(async_add_devices(sensors)) + hass.loop.create_task(async_add_devices(sensors, True)) return True class BinarySensorTemplate(BinarySensorDevice): """A virtual binary sensor that triggers from another sensor.""" - # pylint: disable=too-many-arguments def __init__(self, hass, device, friendly_name, sensor_class, value_template, entity_ids): """Initialize the Template binary sensor.""" @@ -82,8 +81,6 @@ class BinarySensorTemplate(BinarySensorDevice): self._template = value_template self._state = None - self._async_render() - @callback def template_bsensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" @@ -115,10 +112,6 @@ class BinarySensorTemplate(BinarySensorDevice): @asyncio.coroutine def async_update(self): """Update the state from the template.""" - self._async_render() - - def _async_render(self): - """Render the state from the template.""" try: self._state = self._template.async_render().lower() == 'true' except TemplateError as ex: diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index e258d6cf443..2ef9c487d82 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SensorTrend(BinarySensorDevice): """Representation of a trend Sensor.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, hass, device_id, friendly_name, target_entity, attribute, sensor_class, invert): """Initialize the sensor.""" diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py index 8673c0e4696..ce2b8b715bd 100644 --- a/homeassistant/components/binary_sensor/vera.py +++ b/homeassistant/components/binary_sensor/vera.py @@ -16,9 +16,9 @@ DEPENDENCIES = ['vera'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Perform the setup for Vera controller devices.""" - add_devices_callback( + add_devices( VeraBinarySensor(device, VERA_CONTROLLER) for device in VERA_DEVICES['binary_sensor']) diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py index dd27a0ba954..13225773b3a 100644 --- a/homeassistant/components/bloomsky.py +++ b/homeassistant/components/bloomsky.py @@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument,too-few-public-methods +# pylint: disable=unused-argument def setup(hass, config): """Setup BloomSky component.""" api_key = config[DOMAIN][CONF_API_KEY] diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2f23118a1c3..d02e7954349 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -5,8 +5,10 @@ Component to interface with cameras. For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ +import asyncio import logging -import time + +from aiohttp import web from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -25,17 +27,16 @@ STATE_IDLE = 'idle' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' -# pylint: disable=too-many-branches -def setup(hass, config): +@asyncio.coroutine +def async_setup(hass, config): """Setup the camera component.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) - hass.wsgi.register_view(CameraImageView(hass, component.entities)) - hass.wsgi.register_view(CameraMjpegStream(hass, component.entities)) - - component.setup(config) + hass.http.register_view(CameraImageView(hass, component.entities)) + hass.http.register_view(CameraMjpegStream(hass, component.entities)) + yield from component.async_setup(config) return True @@ -80,33 +81,59 @@ class Camera(Entity): """Return bytes of camera image.""" raise NotImplementedError() - def mjpeg_stream(self, response): - """Generate an HTTP MJPEG stream from camera images.""" - def stream(): - """Stream images as mjpeg stream.""" - try: - last_image = None - while True: - img_bytes = self.camera_image() + @asyncio.coroutine + def async_camera_image(self): + """Return bytes of camera image. - if img_bytes is not None and img_bytes != last_image: - yield bytes( - '--jpegboundary\r\n' - 'Content-Type: image/jpeg\r\n' - 'Content-Length: {}\r\n\r\n'.format( - len(img_bytes)), 'utf-8') + img_bytes + b'\r\n' + This method must be run in the event loop. + """ + image = yield from self.hass.loop.run_in_executor( + None, self.camera_image) + return image - last_image = img_bytes + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): + """Generate an HTTP MJPEG stream from camera images. - time.sleep(0.5) - except GeneratorExit: - pass + This method must be run in the event loop. + """ + response = web.StreamResponse() - return response( - stream(), - content_type=('multipart/x-mixed-replace; ' - 'boundary=--jpegboundary') - ) + response.content_type = ('multipart/x-mixed-replace; ' + 'boundary=--jpegboundary') + response.enable_chunked_encoding() + yield from response.prepare(request) + + def write(img_bytes): + """Write image to stream.""" + response.write(bytes( + '--jpegboundary\r\n' + 'Content-Type: image/jpeg\r\n' + 'Content-Length: {}\r\n\r\n'.format( + len(img_bytes)), 'utf-8') + img_bytes + b'\r\n') + + last_image = None + + try: + while True: + img_bytes = yield from self.async_camera_image() + if not img_bytes: + break + + if img_bytes is not None and img_bytes != last_image: + write(img_bytes) + + # Chrome seems to always ignore first picture, + # print it twice. + if last_image is None: + write(img_bytes) + + last_image = img_bytes + yield from response.drain() + + yield from asyncio.sleep(.5) + finally: + yield from response.write_eof() @property def state(self): @@ -144,22 +171,25 @@ class CameraView(HomeAssistantView): super().__init__(hass) self.entities = entities + @asyncio.coroutine def get(self, request, entity_id): """Start a get request.""" camera = self.entities.get(entity_id) if camera is None: - return self.Response(status=404) + return web.Response(status=404) authenticated = (request.authenticated or - request.args.get('token') == camera.access_token) + request.GET.get('token') == camera.access_token) if not authenticated: - return self.Response(status=401) + return web.Response(status=401) - return self.handle(camera) + response = yield from self.handle(request, camera) + return response - def handle(self, camera): + @asyncio.coroutine + def handle(self, request, camera): """Hanlde the camera request.""" raise NotImplementedError() @@ -167,25 +197,27 @@ class CameraView(HomeAssistantView): class CameraImageView(CameraView): """Camera view to serve an image.""" - url = "/api/camera_proxy/" + url = "/api/camera_proxy/{entity_id}" name = "api:camera:image" - def handle(self, camera): + @asyncio.coroutine + def handle(self, request, camera): """Serve camera image.""" - response = camera.camera_image() + image = yield from camera.async_camera_image() - if response is None: - return self.Response(status=500) + if image is None: + return web.Response(status=500) - return self.Response(response) + return web.Response(body=image) class CameraMjpegStream(CameraView): """Camera View to serve an MJPEG stream.""" - url = "/api/camera_proxy_stream/" + url = "/api/camera_proxy_stream/{entity_id}" name = "api:camera:stream" - def handle(self, camera): + @asyncio.coroutine + def handle(self, request, camera): """Serve camera image.""" - return camera.mjpeg_stream(self.Response) + yield from camera.handle_async_mjpeg_stream(request) diff --git a/homeassistant/components/camera/ffmpeg.py b/homeassistant/components/camera/ffmpeg.py index 1115bc2d2c1..8e238bfdea7 100644 --- a/homeassistant/components/camera/ffmpeg.py +++ b/homeassistant/components/camera/ffmpeg.py @@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.ffmpeg/ """ +import asyncio import logging import voluptuous as vol +from aiohttp import web -from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) +from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.ffmpeg import ( - run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) + async_run_test, get_binary, CONF_INPUT, CONF_EXTRA_ARGUMENTS) import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME +from homeassistant.util.async import run_coroutine_threadsafe DEPENDENCIES = ['ffmpeg'] @@ -27,17 +30,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a FFmpeg Camera.""" - if not run_test(config.get(CONF_INPUT)): + if not async_run_test(hass, config.get(CONF_INPUT)): return - add_devices([FFmpegCamera(config)]) + hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)])) class FFmpegCamera(Camera): """An implementation of an FFmpeg camera.""" - def __init__(self, config): + def __init__(self, hass, config): """Initialize a FFmpeg camera.""" super().__init__() self._name = config.get(CONF_NAME) @@ -45,24 +49,45 @@ class FFmpegCamera(Camera): self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) def camera_image(self): + """Return bytes of camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + @asyncio.coroutine + def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg import ImageSingle, IMAGE_JPEG - ffmpeg = ImageSingle(get_binary()) + from haffmpeg import ImageSingleAsync, IMAGE_JPEG + ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop) - return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG, - extra_cmd=self._extra_arguments) + image = yield from ffmpeg.get_image( + self._input, output_format=IMAGE_JPEG, + extra_cmd=self._extra_arguments) + return image - def mjpeg_stream(self, response): + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg import CameraMjpeg + from haffmpeg import CameraMjpegAsync - stream = CameraMjpeg(get_binary()) - stream.open_camera(self._input, extra_cmd=self._extra_arguments) - return response( - stream, - mimetype='multipart/x-mixed-replace;boundary=ffserver', - direct_passthrough=True - ) + stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop) + yield from stream.open_camera( + self._input, extra_cmd=self._extra_arguments) + + response = web.StreamResponse() + response.content_type = 'multipart/x-mixed-replace;boundary=ffserver' + response.enable_chunked_encoding() + + yield from response.prepare(request) + + try: + while True: + data = yield from stream.read(102400) + if not data: + break + response.write(data) + finally: + self.hass.loop.create_task(stream.close()) + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 987b8c51af5..e84794356b2 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([FoscamCamera(config)]) -# pylint: disable=too-many-instance-attributes class FoscamCamera(Camera): """An implementation of a Foscam IP camera.""" diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 5d7488b8e68..c6664ed70b2 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -4,10 +4,13 @@ Support for IP Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.generic/ """ +import asyncio import logging +import aiohttp +import async_timeout import requests -from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from requests.auth import HTTPDigestAuth import voluptuous as vol from homeassistant.const import ( @@ -16,6 +19,7 @@ from homeassistant.const import ( from homeassistant.exceptions import TemplateError from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.helpers import config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -35,13 +39,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +@asyncio.coroutine # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a generic IP Camera.""" - add_devices([GenericCamera(hass, config)]) + hass.loop.create_task(async_add_devices([GenericCamera(hass, config)])) -# pylint: disable=too-many-instance-attributes class GenericCamera(Camera): """A generic implementation of an IP camera.""" @@ -49,6 +53,7 @@ class GenericCamera(Camera): """Initialize a generic camera.""" super().__init__() self.hass = hass + self._authentication = device_info.get(CONF_AUTHENTICATION) self._name = device_info.get(CONF_NAME) self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url.hass = hass @@ -58,10 +63,10 @@ class GenericCamera(Camera): password = device_info.get(CONF_PASSWORD) if username and password: - if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION: + if self._authentication == HTTP_DIGEST_AUTHENTICATION: self._auth = HTTPDigestAuth(username, password) else: - self._auth = HTTPBasicAuth(username, password) + self._auth = aiohttp.BasicAuth(username, password=password) else: self._auth = None @@ -69,9 +74,15 @@ class GenericCamera(Camera): self._last_image = None def camera_image(self): + """Return bytes of camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + @asyncio.coroutine + def async_camera_image(self): """Return a still image response from the camera.""" try: - url = self._still_image_url.render() + url = self._still_image_url.async_render() except TemplateError as err: _LOGGER.error('Error parsing template %s: %s', self._still_image_url, err) @@ -80,16 +91,35 @@ class GenericCamera(Camera): if url == self._last_url and self._limit_refetch: return self._last_image - kwargs = {'timeout': 10, 'auth': self._auth} + # aiohttp don't support DigestAuth jet + if self._authentication == HTTP_DIGEST_AUTHENTICATION: + def fetch(): + """Read image from a URL.""" + try: + kwargs = {'timeout': 10, 'auth': self._auth} + response = requests.get(url, **kwargs) + return response.content + except requests.exceptions.RequestException as error: + _LOGGER.error('Error getting camera image: %s', error) + return self._last_image - try: - response = requests.get(url, **kwargs) - except requests.exceptions.RequestException as error: - _LOGGER.error('Error getting camera image: %s', error) - return None + self._last_image = yield from self.hass.loop.run_in_executor( + None, fetch) + # async + else: + try: + with async_timeout.timeout(10, loop=self.hass.loop): + respone = yield from self.hass.websession.get( + url, + auth=self._auth + ) + self._last_image = yield from respone.read() + yield from respone.release() + except asyncio.TimeoutError: + _LOGGER.error('Timeout getting camera image') + return self._last_image self._last_url = url - self._last_image = response.content return self._last_image @property diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py index 04f099d8b1e..81759fa86df 100644 --- a/homeassistant/components/camera/mjpeg.py +++ b/homeassistant/components/camera/mjpeg.py @@ -4,9 +4,14 @@ Support for IP Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.mjpeg/ """ +import asyncio import logging from contextlib import closing +import aiohttp +from aiohttp import web +from aiohttp.web_exceptions import HTTPGatewayTimeout +import async_timeout import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol @@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +@asyncio.coroutine # pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a MJPEG IP Camera.""" - add_devices([MjpegCamera(config)]) + hass.loop.create_task(async_add_devices([MjpegCamera(hass, config)])) def extract_image_from_mjpeg(stream): @@ -52,11 +58,10 @@ def extract_image_from_mjpeg(stream): return jpg -# pylint: disable=too-many-instance-attributes class MjpegCamera(Camera): """An implementation of an IP camera that is reachable over a URL.""" - def __init__(self, device_info): + def __init__(self, hass, device_info): """Initialize a MJPEG camera.""" super().__init__() self._name = device_info.get(CONF_NAME) @@ -65,32 +70,61 @@ class MjpegCamera(Camera): self._password = device_info.get(CONF_PASSWORD) self._mjpeg_url = device_info[CONF_MJPEG_URL] - def camera_stream(self): - """Return a MJPEG stream image response directly from the camera.""" + self._auth = None + if self._username and self._password: + if self._authentication == HTTP_BASIC_AUTHENTICATION: + self._auth = aiohttp.BasicAuth( + self._username, password=self._password + ) + + def camera_image(self): + """Return a still image response from the camera.""" if self._username and self._password: if self._authentication == HTTP_DIGEST_AUTHENTICATION: auth = HTTPDigestAuth(self._username, self._password) else: auth = HTTPBasicAuth(self._username, self._password) - return requests.get(self._mjpeg_url, - auth=auth, - stream=True, timeout=10) + req = requests.get( + self._mjpeg_url, auth=auth, stream=True, timeout=10) else: - return requests.get(self._mjpeg_url, stream=True, timeout=10) + req = requests.get(self._mjpeg_url, stream=True, timeout=10) - def camera_image(self): - """Return a still image response from the camera.""" - with closing(self.camera_stream()) as response: - return extract_image_from_mjpeg(response.iter_content(1024)) + with closing(req) as response: + return extract_image_from_mjpeg(response.iter_content(102400)) - def mjpeg_stream(self, response): + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - stream = self.camera_stream() - return response( - stream.iter_content(chunk_size=1024), - mimetype=stream.headers[CONTENT_TYPE_HEADER], - direct_passthrough=True - ) + # aiohttp don't support DigestAuth -> Fallback + if self._authentication == HTTP_DIGEST_AUTHENTICATION: + yield from super().handle_async_mjpeg_stream(request) + return + + # connect to stream + try: + with async_timeout.timeout(10, loop=self.hass.loop): + stream = yield from self.hass.websession.get( + self._mjpeg_url, + auth=self._auth + ) + except asyncio.TimeoutError: + raise HTTPGatewayTimeout() + + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + response.enable_chunked_encoding() + + yield from response.prepare(request) + + try: + while True: + data = yield from stream.content.read(102400) + if not data: + break + response.write(data) + finally: + self.hass.loop.create_task(stream.release()) + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index 22ab72ad8e7..c5603dac142 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -12,7 +12,8 @@ import shutil import voluptuous as vol from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_FILE_PATH) +from homeassistant.const import (CONF_NAME, CONF_FILE_PATH, + EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,7 @@ DEFAULT_TIMELAPSE = 1000 DEFAULT_VERTICAL_FLIP = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_FILE_PATH): cv.isfile, + vol.Optional(CONF_FILE_PATH): cv.string, vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP): vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP): @@ -53,6 +54,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) +def kill_raspistill(*args): + """Kill any previously running raspistill process..""" + subprocess.Popen(['killall', 'raspistill'], + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT) + + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Raspberry Camera.""" if shutil.which("raspistill") is None: @@ -75,11 +83,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } ) - if not os.access(setup_config[CONF_FILE_PATH], os.W_OK): + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill) + + try: + # Try to create an empty file (or open existing) to ensure we have + # proper permissions. + open(setup_config[CONF_FILE_PATH], 'a').close() + + add_devices([RaspberryCamera(setup_config)]) + except PermissionError: _LOGGER.error("File path is not writable") return False - - add_devices([RaspberryCamera(setup_config)]) + except FileNotFoundError: + _LOGGER.error("Could not create output file (missing directory?)") + return False class RaspberryCamera(Camera): @@ -93,9 +110,7 @@ class RaspberryCamera(Camera): self._config = device_info # Kill if there's raspistill instance - subprocess.Popen(['killall', 'raspistill'], - stdout=subprocess.DEVNULL, - stderr=subprocess.STDOUT) + kill_raspistill() cmd_args = [ 'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH], diff --git a/homeassistant/components/camera/synology.py b/homeassistant/components/camera/synology.py index dedf91a0031..4abdf8d22dd 100644 --- a/homeassistant/components/camera/synology.py +++ b/homeassistant/components/camera/synology.py @@ -4,28 +4,30 @@ Support for Synology Surveillance Station Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.synology/ """ +import asyncio import logging import voluptuous as vol -import requests +from aiohttp import web +from aiohttp.web_exceptions import HTTPGatewayTimeout +import async_timeout from homeassistant.const import ( CONF_NAME, CONF_USERNAME, CONF_PASSWORD, - CONF_URL, CONF_WHITELIST) + CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL) from homeassistant.components.camera import ( Camera, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) -# pylint: disable=too-many-locals DEFAULT_NAME = 'Synology Camera' DEFAULT_STREAM_ID = '0' TIMEOUT = 5 CONF_CAMERA_NAME = 'camera_name' CONF_STREAM_ID = 'stream_id' -CONF_VALID_CERT = 'valid_cert' QUERY_CGI = 'query.cgi' QUERY_API = 'SYNO.API.Info' @@ -38,6 +40,7 @@ WEBAPI_PATH = '/webapi/' AUTH_PATH = 'auth.cgi' CAMERA_PATH = 'camera.cgi' STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi' +CONTENT_TYPE_HEADER = 'Content-Type' SYNO_API_URL = '{0}{1}{2}' @@ -47,89 +50,136 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_URL): cv.string, vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list, - vol.Optional(CONF_VALID_CERT, default=True): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, }) -def setup_platform(hass, config, add_devices, discovery_info=None): +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup a Synology IP Camera.""" # Determine API to use for authentication - syno_api_url = SYNO_API_URL.format(config.get(CONF_URL), - WEBAPI_PATH, - QUERY_CGI) - query_payload = {'api': QUERY_API, - 'method': 'Query', - 'version': '1', - 'query': 'SYNO.'} - query_req = requests.get(syno_api_url, - params=query_payload, - verify=config.get(CONF_VALID_CERT), - timeout=TIMEOUT) - query_resp = query_req.json() + syno_api_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI) + + query_payload = { + 'api': QUERY_API, + 'method': 'Query', + 'version': '1', + 'query': 'SYNO.' + } + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + query_req = yield from hass.websession.get( + syno_api_url, + params=query_payload, + verify_ssl=config.get(CONF_VERIFY_SSL) + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", syno_api_url) + return False + + query_resp = yield from query_req.json() auth_path = query_resp['data'][AUTH_API]['path'] camera_api = query_resp['data'][CAMERA_API]['path'] camera_path = query_resp['data'][CAMERA_API]['path'] streaming_path = query_resp['data'][STREAMING_API]['path'] + # cleanup + yield from query_req.release() + # Authticate to NAS to get a session id - syno_auth_url = SYNO_API_URL.format(config.get(CONF_URL), - WEBAPI_PATH, - auth_path) - session_id = get_session_id(config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - syno_auth_url, - config.get(CONF_VALID_CERT)) + syno_auth_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, auth_path) + + session_id = yield from get_session_id( + hass, + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD), + syno_auth_url, + config.get(CONF_VERIFY_SSL) + ) # Use SessionID to get cameras in system - syno_camera_url = SYNO_API_URL.format(config.get(CONF_URL), - WEBAPI_PATH, - camera_api) - camera_payload = {'api': CAMERA_API, - 'method': 'List', - 'version': '1'} - camera_req = requests.get(syno_camera_url, - params=camera_payload, - verify=config.get(CONF_VALID_CERT), - timeout=TIMEOUT, - cookies={'id': session_id}) - camera_resp = camera_req.json() + syno_camera_url = SYNO_API_URL.format( + config.get(CONF_URL), WEBAPI_PATH, camera_api) + + camera_payload = { + 'api': CAMERA_API, + 'method': 'List', + 'version': '1' + } + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + camera_req = yield from hass.websession.get( + syno_camera_url, + params=camera_payload, + verify_ssl=config.get(CONF_VERIFY_SSL), + cookies={'id': session_id} + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", syno_camera_url) + return False + + camera_resp = yield from camera_req.json() cameras = camera_resp['data']['cameras'] + yield from camera_req.release() + + # add cameras + devices = [] + tasks = [] for camera in cameras: if not config.get(CONF_WHITELIST): camera_id = camera['id'] snapshot_path = camera['snapshot_path'] - add_devices([SynologyCamera(config, - camera_id, - camera['name'], - snapshot_path, - streaming_path, - camera_path, - auth_path)]) + device = SynologyCamera( + config, + camera_id, + camera['name'], + snapshot_path, + streaming_path, + camera_path, + auth_path + ) + tasks.append(device.async_read_sid()) + devices.append(device) + + yield from asyncio.gather(*tasks, loop=hass.loop) + hass.loop.create_task(async_add_devices(devices)) -def get_session_id(username, password, login_url, valid_cert): +@asyncio.coroutine +def get_session_id(hass, username, password, login_url, valid_cert): """Get a session id.""" - auth_payload = {'api': AUTH_API, - 'method': 'Login', - 'version': '2', - 'account': username, - 'passwd': password, - 'session': 'SurveillanceStation', - 'format': 'sid'} - auth_req = requests.get(login_url, - params=auth_payload, - verify=valid_cert, - timeout=TIMEOUT) - auth_resp = auth_req.json() + auth_payload = { + 'api': AUTH_API, + 'method': 'Login', + 'version': '2', + 'account': username, + 'passwd': password, + 'session': 'SurveillanceStation', + 'format': 'sid' + } + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + auth_req = yield from hass.websession.get( + login_url, + params=auth_payload, + verify_ssl=valid_cert + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", login_url) + return False + + auth_resp = yield from auth_req.json() + yield from auth_req.release() + return auth_resp['data']['sid'] -# pylint: disable=too-many-instance-attributes class SynologyCamera(Camera): """An implementation of a Synology NAS based IP camera.""" -# pylint: disable=too-many-arguments def __init__(self, config, camera_id, camera_name, snapshot_path, streaming_path, camera_path, auth_path): """Initialize a Synology Surveillance Station camera.""" @@ -142,80 +192,98 @@ class SynologyCamera(Camera): self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi' self._camera_name = config.get(CONF_CAMERA_NAME) self._stream_id = config.get(CONF_STREAM_ID) - self._valid_cert = config.get(CONF_VALID_CERT) + self._valid_cert = config.get(CONF_VERIFY_SSL) self._camera_id = camera_id self._snapshot_path = snapshot_path self._streaming_path = streaming_path self._camera_path = camera_path self._auth_path = auth_path + self._session_id = None - self._session_id = get_session_id(self._username, - self._password, - self._login_url, - self._valid_cert) - - def get_sid(self): + @asyncio.coroutine + def async_read_sid(self): """Get a session id.""" - auth_payload = {'api': AUTH_API, - 'method': 'Login', - 'version': '2', - 'account': self._username, - 'passwd': self._password, - 'session': 'SurveillanceStation', - 'format': 'sid'} - auth_req = requests.get(self._login_url, - params=auth_payload, - verify=self._valid_cert, - timeout=TIMEOUT) - auth_resp = auth_req.json() - self._session_id = auth_resp['data']['sid'] + self._session_id = yield from get_session_id( + self.hass, + self._username, + self._password, + self._login_url, + self._valid_cert + ) def camera_image(self): + """Return bytes of camera image.""" + return run_coroutine_threadsafe( + self.async_camera_image(), self.hass.loop).result() + + @asyncio.coroutine + def async_camera_image(self): """Return a still image response from the camera.""" - image_url = SYNO_API_URL.format(self._synology_url, - WEBAPI_PATH, - self._camera_path) - image_payload = {'api': CAMERA_API, - 'method': 'GetSnapshot', - 'version': '1', - 'cameraId': self._camera_id} + image_url = SYNO_API_URL.format( + self._synology_url, WEBAPI_PATH, self._camera_path) + + image_payload = { + 'api': CAMERA_API, + 'method': 'GetSnapshot', + 'version': '1', + 'cameraId': self._camera_id + } try: - response = requests.get(image_url, - params=image_payload, - timeout=TIMEOUT, - verify=self._valid_cert, - cookies={'id': self._session_id}) - except requests.exceptions.RequestException as error: - _LOGGER.error('Error getting camera image: %s', error) + with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + response = yield from self.hass.websession.get( + image_url, + params=image_payload, + verify_ssl=self._valid_cert, + cookies={'id': self._session_id} + ) + except asyncio.TimeoutError: + _LOGGER.error("Timeout on %s", image_url) return None - return response.content + image = yield from response.read() + yield from response.release() - def camera_stream(self): + return image + + @asyncio.coroutine + def handle_async_mjpeg_stream(self, request): """Return a MJPEG stream image response directly from the camera.""" - streaming_url = SYNO_API_URL.format(self._synology_url, - WEBAPI_PATH, - self._streaming_path) - streaming_payload = {'api': STREAMING_API, - 'method': 'Stream', - 'version': '1', - 'cameraId': self._camera_id, - 'format': 'mjpeg'} - response = requests.get(streaming_url, - payload=streaming_payload, - stream=True, - timeout=TIMEOUT, - cookies={'id': self._session_id}) - return response + streaming_url = SYNO_API_URL.format( + self._synology_url, WEBAPI_PATH, self._streaming_path) - def mjpeg_steam(self, response): - """Generate an HTTP MJPEG Stream from the Synology NAS.""" - stream = self.camera_stream() - return response( - stream.iter_content(chunk_size=1024), - mimetype=stream.headers['CONTENT_TYPE_HEADER'], - direct_passthrough=True - ) + streaming_payload = { + 'api': STREAMING_API, + 'method': 'Stream', + 'version': '1', + 'cameraId': self._camera_id, + 'format': 'mjpeg' + } + try: + with async_timeout.timeout(TIMEOUT, loop=self.hass.loop): + stream = yield from self.hass.websession.get( + streaming_url, + payload=streaming_payload, + verify_ssl=self._valid_cert, + cookies={'id': self._session_id} + ) + except asyncio.TimeoutError: + raise HTTPGatewayTimeout() + + response = web.StreamResponse() + response.content_type = stream.headers.get(CONTENT_TYPE_HEADER) + response.enable_chunked_encoding() + + yield from response.prepare(request) + + try: + while True: + data = yield from stream.content.read(102400) + if not data: + break + response.write(data) + finally: + self.hass.loop.create_task(stream.release()) + yield from response.write_eof() @property def name(self): diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 714581ba331..5ae10fca303 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -123,7 +123,6 @@ def set_aux_heat(hass, aux_heat, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) -# pylint: disable=too-many-arguments def set_temperature(hass, temperature=None, entity_id=None, target_temp_high=None, target_temp_low=None, operation_mode=None): @@ -181,7 +180,6 @@ def set_swing_mode(hass, swing_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) -# pylint: disable=too-many-branches def setup(hass, config): """Setup climate devices.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) @@ -364,7 +362,7 @@ def setup(hass, config): class ClimateDevice(Entity): """Representation of a climate device.""" - # pylint: disable=too-many-public-methods,no-self-use + # pylint: disable=no-self-use @property def state(self): """Return the current state.""" diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py index 0104d9d01af..04053febf90 100644 --- a/homeassistant/components/climate/demo.py +++ b/homeassistant/components/climate/demo.py @@ -21,11 +21,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -# pylint: disable=too-many-arguments, too-many-public-methods class DemoClimate(ClimateDevice): """Representation of a demo climate device.""" - # pylint: disable=too-many-instance-attributes def __init__(self, name, target_temperature, unit_of_measurement, away, current_temperature, current_fan_mode, target_humidity, current_humidity, current_swing_mode, diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 1ed826e55dc..c98ac6d0106 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): schema=SET_FAN_MIN_ON_TIME_SCHEMA) -# pylint: disable=too-many-public-methods, abstract-method class Thermostat(ClimateDevice): """A thermostat class for Ecobee.""" diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 87d9e322405..72bd0b22522 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) -# pylint: disable=too-many-instance-attributes, import-error, abstract-method +# pylint: disable=import-error class EQ3BTSmartThermostat(ClimateDevice): """Representation of a eQ-3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index c5c38d624f5..3030ea9090e 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -62,11 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): target_temp, ac_mode, min_cycle_duration)]) -# pylint: disable=too-many-instance-attributes, abstract-method class GenericThermostat(ClimateDevice): """Representation of a GenericThermostat device.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, target_temp, ac_mode, min_cycle_duration): """Initialize the thermostat.""" diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py index a6dd01af4ab..28d419597d3 100644 --- a/homeassistant/components/climate/heatmiser.py +++ b/homeassistant/components/climate/heatmiser.py @@ -56,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HeatmiserV3Thermostat(ClimateDevice): """Representation of a HeatmiserV3 thermostat.""" - # pylint: disable=too-many-instance-attributes, abstract-method def __init__(self, heatmiser, device, name, serport): """Initialize the thermostat.""" self.heatmiser = heatmiser diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py index 7113779eb57..9be4e7a4886 100644 --- a/homeassistant/components/climate/homematic.py +++ b/homeassistant/components/climate/homematic.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): ) -# pylint: disable=abstract-method class HMThermostat(homematic.HMDevice, ClimateDevice): """Representation of a Homematic thermostat.""" diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 3af4f62246d..540a9a941d1 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -100,7 +100,6 @@ def _setup_us(username, password, config, add_devices): class RoundThermostat(ClimateDevice): """Representation of a Honeywell Round Connected thermostat.""" - # pylint: disable=too-many-instance-attributes, abstract-method def __init__(self, device, zone_id, master, away_temp): """Initialize the thermostat.""" self.device = device @@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice): self._is_dhw = False -# pylint: disable=abstract-method class HoneywellUSThermostat(ClimateDevice): """Representation of a Honeywell US Thermostat.""" diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index c93a69ac0b9..2a815625434 100755 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -37,7 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): map_sv_types, devices, add_devices, MySensorsHVAC)) -# pylint: disable=too-many-arguments, too-many-public-methods class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): """Representation of a MySensorsHVAC hvac.""" diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index fb7e4b7ec11..402cc2b2498 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/climate.nest/ """ import logging + import voluptuous as vol -import homeassistant.components.nest as nest + +from homeassistant.components.nest import DATA_NEST from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -26,11 +28,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Nest thermostat.""" temp_unit = hass.config.units.temperature_unit - add_devices([NestThermostat(structure, device, temp_unit) - for structure, device in nest.devices()]) + add_devices( + [NestThermostat(structure, device, temp_unit) + for structure, device in hass.data[DATA_NEST].devices()], + True + ) -# pylint: disable=abstract-method,too-many-public-methods class NestThermostat(ClimateDevice): """Representation of a Nest thermostat.""" @@ -54,18 +58,33 @@ class NestThermostat(ClimateDevice): if self.device.can_heat and self.device.can_cool: self._operation_list.append(STATE_AUTO) + # feature of device + self._has_humidifier = self.device.has_humidifier + self._has_dehumidifier = self.device.has_dehumidifier + self._has_fan = self.device.has_fan + + # data attributes + self._away = None + self._location = None + self._name = None + self._humidity = None + self._target_humidity = None + self._target_temperature = None + self._temperature = None + self._mode = None + self._fan = None + self._away_temperature = None + @property def name(self): """Return the name of the nest, if any.""" - location = self.device.where - name = self.device.name - if location is None: - return name + if self._location is None: + return self._name else: - if name == '': - return location.capitalize() + if self._name == '': + return self._location.capitalize() else: - return location.capitalize() + '(' + name + ')' + return self._location.capitalize() + '(' + self._name + ')' @property def temperature_unit(self): @@ -75,11 +94,11 @@ class NestThermostat(ClimateDevice): @property def device_state_attributes(self): """Return the device specific state attributes.""" - if self.device.has_humidifier or self.device.has_dehumidifier: + if self._has_humidifier or self._has_dehumidifier: # Move these to Thermostat Device and make them global return { - "humidity": self.device.humidity, - "target_humidity": self.device.target_humidity, + "humidity": self._humidity, + "target_humidity": self._target_humidity, } else: # No way to control humidity not show setting @@ -88,18 +107,18 @@ class NestThermostat(ClimateDevice): @property def current_temperature(self): """Return the current temperature.""" - return self.device.temperature + return self._temperature @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" - if self.device.mode == 'cool': + if self._mode == 'cool': return STATE_COOL - elif self.device.mode == 'heat': + elif self._mode == 'heat': return STATE_HEAT - elif self.device.mode == 'range': + elif self._mode == 'range': return STATE_AUTO - elif self.device.mode == 'off': + elif self._mode == 'off': return STATE_OFF else: return STATE_UNKNOWN @@ -107,37 +126,37 @@ class NestThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - if self.device.mode != 'range' and not self.is_away_mode_on: - return self.device.target + if self._mode != 'range' and not self.is_away_mode_on: + return self._target_temperature else: return None @property def target_temperature_low(self): """Return the lower bound temperature we try to reach.""" - if self.is_away_mode_on and self.device.away_temperature[0]: + if self.is_away_mode_on and self._away_temperature[0]: # away_temperature is always a low, high tuple - return self.device.away_temperature[0] - if self.device.mode == 'range': - return self.device.target[0] + return self._away_temperature[0] + if self._mode == 'range': + return self._target_temperature[0] else: return None @property def target_temperature_high(self): """Return the upper bound temperature we try to reach.""" - if self.is_away_mode_on and self.device.away_temperature[1]: + if self.is_away_mode_on and self._away_temperature[1]: # away_temperature is always a low, high tuple - return self.device.away_temperature[1] - if self.device.mode == 'range': - return self.device.target[1] + return self._away_temperature[1] + if self._mode == 'range': + return self._target_temperature[1] else: return None @property def is_away_mode_on(self): """Return if away mode is on.""" - return self.structure.away + return self._away def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -145,7 +164,7 @@ class NestThermostat(ClimateDevice): target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if target_temp_low is not None and target_temp_high is not None: - if self.device.mode == 'range': + if self._mode == 'range': temp = (target_temp_low, target_temp_high) else: temp = kwargs.get(ATTR_TEMPERATURE) @@ -179,9 +198,9 @@ class NestThermostat(ClimateDevice): @property def current_fan_mode(self): """Return whether the fan is on.""" - if self.device.has_fan: + if self._has_fan: # Return whether the fan is on - return STATE_ON if self.device.fan else STATE_AUTO + return STATE_ON if self._fan else STATE_AUTO else: # No Fan available so disable slider return None @@ -198,7 +217,7 @@ class NestThermostat(ClimateDevice): @property def min_temp(self): """Identify min_temp in Nest API or defaults if not available.""" - temp = self.device.away_temperature.low + temp = self._away_temperature[0] if temp is None: return super().min_temp else: @@ -207,12 +226,21 @@ class NestThermostat(ClimateDevice): @property def max_temp(self): """Identify max_temp in Nest API or defaults if not available.""" - temp = self.device.away_temperature.high + temp = self._away_temperature[1] if temp is None: return super().max_temp else: return temp def update(self): - """Python-nest has its own mechanism for staying up to date.""" - pass + """Cache value from Python-nest.""" + self._location = self.device.where + self._name = self.device.name + self._humidity = self.device.humidity, + self._target_humidity = self.device.target_humidity, + self._temperature = self.device.temperature + self._mode = self.device.mode + self._target_temperature = self.device.target + self._fan = self.device.fan + self._away = self.structure.away + self._away_temperature = self.device.away_temperature diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index b0a5059ef44..163054cd121 100755 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -54,7 +54,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): return None -# pylint: disable=abstract-method class NetatmoThermostat(ClimateDevice): """Representation a Netatmo thermostat.""" diff --git a/homeassistant/components/climate/proliphix.py b/homeassistant/components/climate/proliphix.py index 6aeee6e537c..515c43f7eba 100644 --- a/homeassistant/components/climate/proliphix.py +++ b/homeassistant/components/climate/proliphix.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([ProliphixThermostat(pdp)]) -# pylint: disable=abstract-method class ProliphixThermostat(ClimateDevice): """Representation a Proliphix thermostat.""" diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py index 74778682540..c2d712e19bd 100644 --- a/homeassistant/components/climate/radiotherm.py +++ b/homeassistant/components/climate/radiotherm.py @@ -6,7 +6,6 @@ https://home-assistant.io/components/climate.radiotherm/ """ import datetime import logging -from urllib.error import URLError import voluptuous as vol @@ -52,14 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: tstat = radiotherm.get_thermostat(host) tstats.append(RadioThermostat(tstat, hold_temp)) - except (URLError, OSError): + except OSError: _LOGGER.exception("Unable to connect to Radio Thermostat: %s", host) add_devices(tstats) -# pylint: disable=abstract-method class RadioThermostat(ClimateDevice): """Representation of a Radio Thermostat.""" diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py index 447d2e4f720..fa4244497e6 100644 --- a/homeassistant/components/climate/vera.py +++ b/homeassistant/components/climate/vera.py @@ -8,7 +8,10 @@ import logging from homeassistant.util import convert from homeassistant.components.climate import ClimateDevice -from homeassistant.const import TEMP_FAHRENHEIT, ATTR_TEMPERATURE +from homeassistant.const import ( + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + ATTR_TEMPERATURE) from homeassistant.components.vera import ( VeraDevice, VERA_DEVICES, VERA_CONTROLLER) @@ -28,7 +31,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device in VERA_DEVICES['climate']) -# pylint: disable=abstract-method class VeraThermostat(VeraDevice, ClimateDevice): """Representation of a Vera Thermostat.""" @@ -95,7 +97,13 @@ class VeraThermostat(VeraDevice, ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_FAHRENHEIT + vera_temp_units = ( + self.vera_device.vera_controller.temperature_units) + + if vera_temp_units == 'F': + return TEMP_FAHRENHEIT + + return TEMP_CELSIUS @property def current_temperature(self): diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py index 3e12d4c6006..d94c4f1b94b 100755 --- a/homeassistant/components/climate/zwave.py +++ b/homeassistant/components/climate/zwave.py @@ -69,11 +69,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): discovery_info, zwave.NETWORK) -# pylint: disable=abstract-method class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """Represents a ZWave Climate device.""" - # pylint: disable=too-many-instance-attributes def __init__(self, value, temp_unit): """Initialize the zwave climate device.""" from openzwave.network import ZWaveNetwork @@ -84,6 +82,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._current_temperature = None self._current_operation = None self._operation_list = None + self._operating_state = None self._current_fan_mode = None self._fan_list = None self._current_swing_mode = None @@ -182,6 +181,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("Device can't set setpoint based on operation mode." " Defaulting to index=1") self._target_temperature = int(value.data) + # Operating state + for value in (self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE) + .values()): + self._operating_state = value.data @property def should_poll(self): @@ -323,3 +327,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): value.index == 33: value.data = bytes(swing_mode, 'utf-8') break + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + if self._operating_state: + return { + "operating_state": self._operating_state, + } + else: + return {} diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 9f5cb397587..5e99e02d371 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -8,7 +8,8 @@ the user has submitted configuration information. """ import logging -from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME +from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \ + ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import generate_entity_id _INSTANCES = {} @@ -33,10 +34,10 @@ STATE_CONFIGURE = 'configure' STATE_CONFIGURED = 'configured' -# pylint: disable=too-many-arguments def request_config( hass, name, callback, description=None, description_image=None, - submit_caption=None, fields=None, link_name=None, link_url=None): + submit_caption=None, fields=None, link_name=None, link_url=None, + entity_picture=None): """Create a new request for configuration. Will return an ID to be used for sequent calls. @@ -46,7 +47,7 @@ def request_config( request_id = instance.request_config( name, callback, description, description_image, submit_caption, - fields, link_name, link_url) + fields, link_name, link_url, entity_picture) _REQUESTS[request_id] = instance @@ -100,11 +101,10 @@ class Configurator(object): hass.services.register( DOMAIN, SERVICE_CONFIGURE, self.handle_service_call) - # pylint: disable=too-many-arguments def request_config( self, name, callback, description, description_image, submit_caption, - fields, link_name, link_url): + fields, link_name, link_url, entity_picture): """Setup a request for configuration.""" entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) @@ -119,6 +119,7 @@ class Configurator(object): ATTR_CONFIGURE_ID: request_id, ATTR_FIELDS: fields, ATTR_FRIENDLY_NAME: name, + ATTR_ENTITY_PICTURE: entity_picture, } data.update({ diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index 27e1c810cf4..778496ec6fc 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -60,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(covers) -# pylint: disable=too-many-arguments, too-many-instance-attributes class CommandCover(CoverDevice): """Representation a command line cover.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, command_open, command_close, command_stop, command_state, value_template): """Initialize the cover.""" diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py index acddfcf7c73..5929ab1851a 100644 --- a/homeassistant/components/cover/demo.py +++ b/homeassistant/components/cover/demo.py @@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class DemoCover(CoverDevice): """Representation of a demo cover.""" - # pylint: disable=no-self-use, too-many-instance-attributes + # pylint: disable=no-self-use def __init__(self, hass, name, position=None, tilt_position=None): """Initialize the cover.""" self.hass = hass diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py new file mode 100644 index 00000000000..813ddea7170 --- /dev/null +++ b/homeassistant/components/cover/garadget.py @@ -0,0 +1,275 @@ +""" +Platform for the garadget cover component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/garadget/ +""" +import logging + +import voluptuous as vol + +import requests + +from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import CONF_DEVICE, CONF_USERNAME, CONF_PASSWORD,\ + CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN,\ + CONF_COVERS +import homeassistant.helpers.config_validation as cv + +DEFAULT_NAME = 'Garadget' + +ATTR_SIGNAL_STRENGTH = "wifi signal strength (dB)" +ATTR_TIME_IN_STATE = "time in state" +ATTR_SENSOR_STRENGTH = "sensor reflection rate" +ATTR_AVAILABLE = "available" + +STATE_OPENING = "opening" +STATE_CLOSING = "closing" +STATE_STOPPED = "stopped" +STATE_OFFLINE = "offline" + +STATES_MAP = { + "open": STATE_OPEN, + "opening": STATE_OPENING, + "closed": STATE_CLOSED, + "closing": STATE_CLOSING, + "stopped": STATE_STOPPED +} + + +# Validation of the user's configuration +COVER_SCHEMA = vol.Schema({ + vol.Optional(CONF_DEVICE): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), +}) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo covers.""" + covers = [] + devices = config.get(CONF_COVERS, {}) + + _LOGGER.debug(devices) + + for device_id, device_config in devices.items(): + args = { + "name": device_config.get(CONF_NAME), + "device_id": device_config.get(CONF_DEVICE, device_id), + "username": device_config.get(CONF_USERNAME), + "password": device_config.get(CONF_PASSWORD), + "access_token": device_config.get(CONF_ACCESS_TOKEN) + } + + covers.append(GaradgetCover(hass, args)) + + add_devices(covers) + + +class GaradgetCover(CoverDevice): + """Representation of a demo cover.""" + + # pylint: disable=no-self-use, too-many-instance-attributes + def __init__(self, hass, args): + """Initialize the cover.""" + self.particle_url = 'https://api.particle.io' + self.hass = hass + self._name = args['name'] + self.device_id = args['device_id'] + self.access_token = args['access_token'] + self.obtained_token = False + self._username = args['username'] + self._password = args['password'] + self._state = STATE_UNKNOWN + self.time_in_state = None + self.signal = None + self.sensor = None + self._unsub_listener_cover = None + self._available = True + + if self.access_token is None: + self.access_token = self.get_token() + self._obtained_token = True + + # Lets try to get the configured name if not provided. + try: + if self._name is None: + doorconfig = self._get_variable("doorConfig") + if doorconfig["nme"] is not None: + self._name = doorconfig["nme"] + self.update() + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to server: %(reason)s', + dict(reason=ex)) + self._state = STATE_OFFLINE + self._available = False + self._name = DEFAULT_NAME + except KeyError as ex: + _LOGGER.warning('Garadget device %(device)s seems to be offline', + dict(device=self.device_id)) + self._name = DEFAULT_NAME + self._state = STATE_OFFLINE + self._available = False + + def __del__(self): + """Try to remove token.""" + if self._obtained_token is True: + if self.access_token is not None: + self.remove_token() + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def should_poll(self): + """No polling needed for a demo cover.""" + return True + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + + if self.signal is not None: + data[ATTR_SIGNAL_STRENGTH] = self.signal + + if self.time_in_state is not None: + data[ATTR_TIME_IN_STATE] = self.time_in_state + + if self.sensor is not None: + data[ATTR_SENSOR_STRENGTH] = self.sensor + + if self.access_token is not None: + data[CONF_ACCESS_TOKEN] = self.access_token + + return data + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._state == STATE_UNKNOWN: + return None + else: + return self._state == STATE_CLOSED + + def get_token(self): + """Get new token for usage during this session.""" + args = { + 'grant_type': 'password', + 'username': self._username, + 'password': self._password + } + url = '{}/oauth/token'.format(self.particle_url) + ret = requests.post(url, + auth=('particle', 'particle'), + data=args) + + return ret.json()['access_token'] + + def remove_token(self): + """Remove authorization token from API.""" + ret = requests.delete('{}/v1/access_tokens/{}'.format( + self.particle_url, + self.access_token), + auth=(self._username, self._password)) + return ret.text + + def _start_watcher(self, command): + """Start watcher.""" + _LOGGER.debug("Starting Watcher for command: %s ", command) + if self._unsub_listener_cover is None: + self._unsub_listener_cover = track_utc_time_change( + self.hass, self._check_state) + + def _check_state(self, now): + """Check the state of the service during an operation.""" + self.update() + self.update_ha_state() + + def close_cover(self): + """Close the cover.""" + if self._state not in ["close", "closing"]: + ret = self._put_command("setState", "close") + self._start_watcher('close') + return ret.get('return_value') == 1 + + def open_cover(self): + """Open the cover.""" + if self._state not in ["open", "opening"]: + ret = self._put_command("setState", "open") + self._start_watcher('open') + return ret.get('return_value') == 1 + + def stop_cover(self): + """Stop the door where it is.""" + if self._state not in ["stopped"]: + ret = self._put_command("setState", "stop") + self._start_watcher('stop') + return ret['return_value'] == 1 + + def update(self): + """Get updated status from API.""" + try: + status = self._get_variable("doorStatus") + _LOGGER.debug("Current Status: %s", status['status']) + self._state = STATES_MAP.get(status['status'], STATE_UNKNOWN) + self.time_in_state = status['time'] + self.signal = status['signal'] + self.sensor = status['sensor'] + self._availble = True + except requests.exceptions.ConnectionError as ex: + _LOGGER.error('Unable to connect to server: %(reason)s', + dict(reason=ex)) + self._state = STATE_OFFLINE + except KeyError as ex: + _LOGGER.warning('Garadget device %(device)s seems to be offline', + dict(device=self.device_id)) + self._state = STATE_OFFLINE + + if self._state not in [STATE_CLOSING, STATE_OPENING]: + if self._unsub_listener_cover is not None: + self._unsub_listener_cover() + self._unsub_listener_cover = None + + def _get_variable(self, var): + """Get latest status.""" + url = '{}/v1/devices/{}/{}?access_token={}'.format( + self.particle_url, + self.device_id, + var, + self.access_token, + ) + ret = requests.get(url) + result = {} + for pairs in ret.json()['result'].split('|'): + key = pairs.split('=') + result[key[0]] = key[1] + return result + + def _put_command(self, func, arg=None): + """Send commands to API.""" + params = {'access_token': self.access_token} + if arg: + params['command'] = arg + url = '{}/v1/devices/{}/{}'.format( + self.particle_url, + self.device_id, + func) + ret = requests.post(url, data=params) + return ret.json() diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py index aea05a9160a..189c501aad5 100644 --- a/homeassistant/components/cover/homematic.py +++ b/homeassistant/components/cover/homematic.py @@ -31,7 +31,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): ) -# pylint: disable=abstract-method class HMCover(homematic.HMDevice, CoverDevice): """Represents a Homematic Cover in Home Assistant.""" diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 9b61f52b67c..27b30e5e013 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -67,7 +67,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-arguments, too-many-instance-attributes class MqttCover(CoverDevice): """Representation of a cover that can be controlled using MQTT.""" diff --git a/homeassistant/components/cover/rfxtrx.py b/homeassistant/components/cover/rfxtrx.py index d7ca03f5762..a016103a8fd 100644 --- a/homeassistant/components/cover/rfxtrx.py +++ b/homeassistant/components/cover/rfxtrx.py @@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) -# pylint: disable=abstract-method class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): """Representation of an rfxtrx cover.""" diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 00034bd718b..4cd4e74be06 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -63,11 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(covers) -# pylint: disable=abstract-method class RPiGPIOCover(CoverDevice): """Representation of a Raspberry GPIO cover.""" - # pylint: disable=too-many-arguments def __init__(self, name, relay_pin, state_pin, state_pull_mode, relay_time): """Initialize the cover.""" diff --git a/homeassistant/components/cover/scsgate.py b/homeassistant/components/cover/scsgate.py index c491f3bf548..f2047b03230 100644 --- a/homeassistant/components/cover/scsgate.py +++ b/homeassistant/components/cover/scsgate.py @@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(covers) -# pylint: disable=too-many-arguments, too-many-instance-attributes class SCSGateCover(CoverDevice): """Representation of SCSGate cover.""" diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py index 0a9e8abb243..2c26fbf1723 100644 --- a/homeassistant/components/cover/vera.py +++ b/homeassistant/components/cover/vera.py @@ -15,14 +15,13 @@ DEPENDENCIES = ['vera'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return Vera covers.""" - add_devices_callback( + add_devices( VeraCover(device, VERA_CONTROLLER) for device in VERA_DEVICES['cover']) -# pylint: disable=abstract-method class VeraCover(VeraDevice, CoverDevice): """Represents a Vera Cover in Home Assistant.""" diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py index a3db374ddf1..2794995abb1 100644 --- a/homeassistant/components/cover/zwave.py +++ b/homeassistant/components/cover/zwave.py @@ -32,17 +32,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] - if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \ - and value.index == 0: + if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL + and value.index == 0): value.set_change_verified(False) add_devices([ZwaveRollershutter(value)]) - elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \ - node.has_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)]) + elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL: + if (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 @@ -122,7 +124,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ 'Open' or value.command_class == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Down': + 'Up': self._lozwmgr.pressButton(value.value_id) break @@ -132,7 +134,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): if value.command_class == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ - 'Up' or value.command_class == \ + 'Down' or value.command_class == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ 'Close': self._lozwmgr.pressButton(value.value_id) diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 1bf921c2e06..ed31e624b91 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -41,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-locals def setup(hass, config): """The triggers to turn lights on or off based on device presence.""" logger = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 87b628b050b..6c6ae3adea6 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -4,8 +4,6 @@ Provide functionality to keep track of devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/device_tracker/ """ -# pylint: disable=too-many-instance-attributes, too-many-arguments -# pylint: disable=too-many-locals import asyncio from datetime import timedelta import logging @@ -14,7 +12,6 @@ import threading from typing import Any, Sequence, Callable import voluptuous as vol -import yaml from homeassistant.bootstrap import ( prepare_setup_platform, log_exception) @@ -29,6 +26,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util as util from homeassistant.util.async import run_coroutine_threadsafe import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( @@ -57,6 +55,8 @@ DEFAULT_SCAN_INTERVAL = 12 CONF_AWAY_HIDE = 'hide_if_away' DEFAULT_AWAY_HIDE = False +EVENT_NEW_DEVICE = 'device_tracker_new_device' + SERVICE_SEE = 'see' ATTR_MAC = 'mac' @@ -88,7 +88,6 @@ def is_on(hass: HomeAssistantType, entity_id: str=None): return hass.states.is_state(entity, STATE_HOME) -# pylint: disable=too-many-arguments def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, host_name: str=None, location_name: str=None, gps: GPSType=None, gps_accuracy=None, @@ -103,8 +102,7 @@ def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, (ATTR_GPS_ACCURACY, gps_accuracy), (ATTR_BATTERY, battery)) if value is not None} if attributes: - for key, value in attributes: - data[key] = value + data[ATTR_ATTRIBUTES] = attributes hass.services.call(DOMAIN, SERVICE_SEE, data) @@ -115,7 +113,7 @@ def setup(hass: HomeAssistantType, config: ConfigType): try: conf = config.get(DOMAIN, []) except vol.Invalid as ex: - log_exception(ex, DOMAIN, config) + log_exception(ex, DOMAIN, config, hass) return False else: conf = conf[0] if len(conf) > 0 else {} @@ -240,9 +238,12 @@ class DeviceTracker(object): device.seen(host_name, location_name, gps, gps_accuracy, battery, attributes) + if device.track: device.update_ha_state() + self.hass.bus.async_fire(EVENT_NEW_DEVICE, device) + # During init, we ignore the group if self.group is not None: self.group.update_tracked_entity_ids( @@ -432,7 +433,7 @@ def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): device = dev_schema(device) device['dev_id'] = cv.slugify(dev_id) except vol.Invalid as exp: - log_exception(exp, dev_id, devices) + log_exception(exp, dev_id, devices, hass) else: result.append(Device(hass, **device)) return result @@ -468,8 +469,6 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, def update_config(path: str, dev_id: str, device: Device): """Add device to YAML configuration file.""" with open(path, 'a') as out: - out.write('\n') - device = {device.dev_id: { 'name': device.name, 'mac': device.mac, @@ -477,7 +476,8 @@ def update_config(path: str, dev_id: str, device: Device): 'track': device.track, CONF_AWAY_HIDE: device.away_hide }} - yaml.dump(device, out, default_flow_style=False) + out.write('\n') + out.write(dump(device)) def get_gravatar_for_email(email: str): diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 4fd2771db4f..50411591cb7 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -90,9 +90,7 @@ AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp') class AsusWrtDeviceScanner(object): """This class queries a router running ASUSWRT firmware.""" - # pylint: disable=too-many-instance-attributes, too-many-branches # Eighth attribute needed for mode (AP mode vs router mode) - def __init__(self, config): """Initialize the scanner.""" self.host = config[CONF_HOST] diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py index 27bd9c6b477..a07db8ec404 100644 --- a/homeassistant/components/device_tracker/automatic.py +++ b/homeassistant/components/device_tracker/automatic.py @@ -60,8 +60,6 @@ def setup_scanner(hass, config: dict, see): return True -# pylint: disable=too-many-instance-attributes -# pylint: disable=too-few-public-methods class AutomaticDeviceScanner(object): """A class representing an Automatic device.""" diff --git a/homeassistant/components/device_tracker/bbox.py b/homeassistant/components/device_tracker/bbox.py index c851b622592..50864f47be1 100644 --- a/homeassistant/components/device_tracker/bbox.py +++ b/homeassistant/components/device_tracker/bbox.py @@ -7,15 +7,16 @@ https://home-assistant.io/components/device_tracker.bbox/ from collections import namedtuple import logging from datetime import timedelta + import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import DOMAIN from homeassistant.util import Throttle -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) +REQUIREMENTS = ['pybbox==0.0.5-alpha'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pybbox==0.0.5-alpha'] + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60) def get_scanner(hass, config): @@ -36,7 +37,7 @@ class BboxDeviceScanner(object): self.last_results = [] # type: List[Device] self.success_init = self._update_info() - _LOGGER.info('Bbox scanner initialized') + _LOGGER.info("Bbox scanner initialized") def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -60,7 +61,7 @@ class BboxDeviceScanner(object): Returns boolean if scanning successful. """ - _LOGGER.info('Scanning') + _LOGGER.info("Scanning...") import pybbox @@ -78,5 +79,5 @@ class BboxDeviceScanner(object): self.last_results = last_results - _LOGGER.info('Bbox scan successful') + _LOGGER.info("Bbox scan successful") return True diff --git a/homeassistant/components/device_tracker/ddwrt.py b/homeassistant/components/device_tracker/ddwrt.py index 4dc6229566c..a67ee3d1d39 100644 --- a/homeassistant/components/device_tracker/ddwrt.py +++ b/homeassistant/components/device_tracker/ddwrt.py @@ -35,12 +35,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def get_scanner(hass, config): """Validate the configuration and return a DD-WRT scanner.""" - scanner = DdWrtDeviceScanner(config[DOMAIN]) - - return scanner if scanner.success_init else None + try: + return DdWrtDeviceScanner(config[DOMAIN]) + except ConnectionError: + return None -# pylint: disable=too-many-instance-attributes class DdWrtDeviceScanner(object): """This class queries a wireless router running DD-WRT firmware.""" @@ -53,13 +53,13 @@ class DdWrtDeviceScanner(object): self.lock = threading.Lock() self.last_results = {} - self.mac2name = {} # Test the router is accessible url = 'http://{}/Status_Wireless.live.asp'.format(self.host) data = self.get_ddwrt_data(url) - self.success_init = data is not None + if not data: + raise ConnectionError('Cannot connect to DD-Wrt router') def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -83,14 +83,15 @@ class DdWrtDeviceScanner(object): if not dhcp_leases: return None - # Remove leading and trailing single quotes. - cleaned_str = dhcp_leases.strip().strip('"') - elements = cleaned_str.split('","') - num_clients = int(len(elements)/5) + # Remove leading and trailing quotes and spaces + cleaned_str = dhcp_leases.replace( + "\"", "").replace("\'", "").replace(" ", "") + elements = cleaned_str.split(',') + num_clients = int(len(elements) / 5) self.mac2name = {} for idx in range(0, num_clients): - # This is stupid but the data is a single array - # every 5 elements represents one hosts, the MAC + # The data is a single array + # every 5 elements represents one host, the MAC # is the third element and the name is the first. mac_index = (idx * 5) + 2 if mac_index < len(elements): @@ -105,9 +106,6 @@ class DdWrtDeviceScanner(object): Return boolean if scanning successful. """ - if not self.success_init: - return False - with self.lock: _LOGGER.info('Checking ARP') @@ -123,11 +121,8 @@ class DdWrtDeviceScanner(object): if not active_clients: return False - # This is really lame, instead of using JSON the DD-WRT UI - # uses its own data format for some reason and then - # regex's out values so I guess I have to do the same, - # LAME!!! - + # The DD-WRT UI uses its own data format and then + # regex's out values so this is done here too # Remove leading and trailing single quotes. clean_str = active_clients.strip().strip("'") elements = clean_str.split("','") diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py index 5832fa425be..fd30946c919 100644 --- a/homeassistant/components/device_tracker/fritz.py +++ b/homeassistant/components/device_tracker/fritz.py @@ -38,7 +38,6 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -# pylint: disable=too-many-instance-attributes class FritzBoxScanner(object): """This class queries a FRITZ!Box router.""" diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 7c585244400..b5ae5ded01a 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -1,100 +1,427 @@ """ -Support for iCloud connected devices. +Platform that supports scanning iCloud. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.icloud/ """ import logging +import random +import os + import voluptuous as vol -from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, - EVENT_HOMEASSISTANT_START) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT) +from homeassistant.components.zone import active_zone from homeassistant.helpers.event import track_utc_time_change +import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify -from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT, - PLATFORM_SCHEMA) +import homeassistant.util.dt as dt_util +from homeassistant.util.location import distance +from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pyicloud==0.9.1'] -CONF_INTERVAL = 'interval' -KEEPALIVE_INTERVAL = 4 +CONF_IGNORED_DEVICES = 'ignored_devices' +CONF_ACCOUNTNAME = 'account_name' + +# entity attributes +ATTR_ACCOUNTNAME = 'account_name' +ATTR_INTERVAL = 'interval' +ATTR_DEVICENAME = 'device_name' +ATTR_BATTERY = 'battery' +ATTR_DISTANCE = 'distance' +ATTR_DEVICESTATUS = 'device_status' +ATTR_LOWPOWERMODE = 'low_power_mode' +ATTR_BATTERYSTATUS = 'battery_status' + +ICLOUDTRACKERS = {} + +_CONFIGURING = {} + +DEVICESTATUSSET = ['features', 'maxMsgChar', 'darkWake', 'fmlyShare', + 'deviceStatus', 'remoteLock', 'activationLocked', + 'deviceClass', 'id', 'deviceModel', 'rawDeviceModel', + 'passcodeLength', 'canWipeAfterLock', 'trackingInfo', + 'location', 'msg', 'batteryLevel', 'remoteWipe', + 'thisDevice', 'snd', 'prsId', 'wipeInProgress', + 'lowPowerMode', 'lostModeEnabled', 'isLocating', + 'lostModeCapable', 'mesg', 'name', 'batteryStatus', + 'lockedTimestamp', 'lostTimestamp', 'locationCapable', + 'deviceDisplayName', 'lostDevice', 'deviceColor', + 'wipedTimestamp', 'modelDisplayName', 'locationEnabled', + 'isMac', 'locFoundEnabled'] + +DEVICESTATUSCODES = {'200': 'online', '201': 'offline', '203': 'pending', + '204': 'unregistered'} + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), + vol.Optional(ATTR_DEVICENAME): cv.slugify, + vol.Optional(ATTR_INTERVAL): cv.positive_int, +}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_USERNAME): vol.Coerce(str), - vol.Required(CONF_PASSWORD): vol.Coerce(str), - vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int), - vol.Range(min=1)) - }) + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, +}) -def setup_scanner(hass, config, see): - """Setup the iCloud Scanner.""" - from pyicloud import PyiCloudService - from pyicloud.exceptions import PyiCloudFailedLoginException - from pyicloud.exceptions import PyiCloudNoDevicesException - logging.getLogger("pyicloud.base").setLevel(logging.WARNING) +def setup_scanner(hass, config: dict, see): + """Set up the iCloud Scanner.""" + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0])) - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] + icloudaccount = Icloud(hass, username, password, account, see) - try: - _LOGGER.info('Logging into iCloud Account') - # Attempt the login to iCloud - api = PyiCloudService(username, password, verify=True) - except PyiCloudFailedLoginException as error: - _LOGGER.exception('Error logging into iCloud Service: %s', error) + if icloudaccount.api is not None: + ICLOUDTRACKERS[account] = icloudaccount + + else: + _LOGGER.error("No ICLOUDTRACKERS added") return False - def keep_alive(now): - """Keep authenticating iCloud connection. + def lost_iphone(call): + """Call the lost iphone function if the device is found.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + devicename = call.data.get(ATTR_DEVICENAME) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].lost_iphone(devicename) + hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone, + schema=SERVICE_SCHEMA) - The session timeouts if we are not using it so we - have to re-authenticate & this will send an email. - """ - api.authenticate() - _LOGGER.info("Authenticate against iCloud") + def update_icloud(call): + """Call the update function of an icloud account.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + devicename = call.data.get(ATTR_DEVICENAME) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].update_icloud(devicename) + hass.services.register(DOMAIN, 'icloud_update', update_icloud, + schema=SERVICE_SCHEMA) - seen_devices = {} + def reset_account_icloud(call): + """Reset an icloud account.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].reset_account_icloud() + hass.services.register(DOMAIN, 'icloud_reset_account', + reset_account_icloud, schema=SERVICE_SCHEMA) - def update_icloud(now): - """Authenticate against iCloud and scan for devices.""" - try: - keep_alive(None) - # Loop through every device registered with the iCloud account - for device in api.devices: - status = device.status() - dev_id = slugify(status['name'].replace(' ', '', 99)) + def setinterval(call): + """Call the update function of an icloud account.""" + accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) + interval = call.data.get(ATTR_INTERVAL) + devicename = call.data.get(ATTR_DEVICENAME) + for account in accounts: + if account in ICLOUDTRACKERS: + ICLOUDTRACKERS[account].setinterval(interval, devicename) - # An entity will not be created by see() when track=false in - # 'known_devices.yaml', but we need to see() it at least once - entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id)) - if entity is None and dev_id in seen_devices: - continue - seen_devices[dev_id] = True - - location = device.location() - # If the device has a location add it. If not do nothing - if location: - see( - dev_id=dev_id, - host_name=status['name'], - gps=(location['latitude'], location['longitude']), - battery=status['batteryLevel']*100, - gps_accuracy=location['horizontalAccuracy'] - ) - except PyiCloudNoDevicesException: - _LOGGER.info('No iCloud Devices found!') - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud) - - update_minutes = list(range(0, 60, config[CONF_INTERVAL])) - # Schedule keepalives between the updates - keepalive_minutes = list(x for x in range(0, 60, KEEPALIVE_INTERVAL) - if x not in update_minutes) - - track_utc_time_change(hass, update_icloud, second=0, minute=update_minutes) - track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes) + hass.services.register(DOMAIN, 'icloud_set_interval', setinterval, + schema=SERVICE_SCHEMA) + # Tells the bootstrapper that the component was successfully initialized return True + + +class Icloud(object): + """Represent an icloud account in Home Assistant.""" + + def __init__(self, hass, username, password, name, see): + """Initialize an iCloud account.""" + self.hass = hass + self.username = username + self.password = password + self.api = None + self.accountname = name + self.devices = {} + self.seen_devices = {} + self._overridestates = {} + self._intervals = {} + self.see = see + + self._trusted_device = None + self._verification_code = None + + self._attrs = {} + self._attrs[ATTR_ACCOUNTNAME] = name + + self.reset_account_icloud() + + randomseconds = random.randint(10, 59) + track_utc_time_change( + self.hass, self.keep_alive, + second=randomseconds + ) + + def reset_account_icloud(self): + """Reset an icloud account.""" + from pyicloud import PyiCloudService + from pyicloud.exceptions import ( + PyiCloudFailedLoginException, PyiCloudNoDevicesException) + + icloud_dir = self.hass.config.path('icloud') + if not os.path.exists(icloud_dir): + os.makedirs(icloud_dir) + + try: + self.api = PyiCloudService( + self.username, self.password, + cookie_directory=icloud_dir, + verify=True) + except PyiCloudFailedLoginException as error: + self.api = None + _LOGGER.error('Error logging into iCloud Service: %s', error) + return + + try: + self.devices = {} + self._overridestates = {} + self._intervals = {} + for device in self.api.devices: + status = device.status(DEVICESTATUSSET) + devicename = slugify(status['name'].replace(' ', '', 99)) + if devicename not in self.devices: + self.devices[devicename] = device + self._intervals[devicename] = 1 + self._overridestates[devicename] = None + except PyiCloudNoDevicesException: + _LOGGER.error('No iCloud Devices found!') + + 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 = self.api.trusted_devices[self._trusted_device] + if self.accountname in _CONFIGURING: + request_id = _CONFIGURING.pop(self.accountname) + configurator = get_component('configurator') + configurator.request_done(request_id) + + def icloud_need_trusted_device(self): + """We need a trusted device.""" + configurator = get_component('configurator') + if self.accountname in _CONFIGURING: + return + + devicesstring = '' + devices = self.api.trusted_devices + for i, device in enumerate(devices): + devicesstring += "{}: {};".format(i, device.get('deviceName')) + + _CONFIGURING[self.accountname] = configurator.request_config( + self.hass, 'iCloud {}'.format(self.accountname), + self.icloud_trusted_device_callback, + description=( + 'Please choose your trusted device by entering' + ' the index from this list: ' + devicesstring), + entity_picture="/static/images/config_icloud.png", + submit_caption='Confirm', + fields=[{'id': '0'}] + ) + + def icloud_verification_callback(self, callback_data): + """The trusted device is chosen.""" + self._verification_code = callback_data.get('0') + if self.accountname in _CONFIGURING: + request_id = _CONFIGURING.pop(self.accountname) + configurator = get_component('configurator') + configurator.request_done(request_id) + + def icloud_need_verification_code(self): + """We need a verification code.""" + configurator = get_component('configurator') + 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'}] + ) + + def keep_alive(self, now): + """Keep the api alive.""" + from pyicloud.exceptions import PyiCloud2FARequiredError + + if self.api is None: + self.reset_account_icloud() + + if self.api is None: + return + + if self.api.requires_2fa: + try: + self.api.authenticate() + except PyiCloud2FARequiredError: + if self._trusted_device is None: + self.icloud_need_trusted_device() + return + + if self._verification_code is None: + self.icloud_need_verification_code() + return + + if self._verification_code == 'waiting': + return + + if self.api.validate_verification_code( + self._trusted_device, self._verification_code): + self._verification_code = None + else: + self.api.authenticate() + + currentminutes = dt_util.now().hour * 60 + dt_util.now().minute + for devicename in self.devices: + interval = self._intervals.get(devicename, 1) + if ((currentminutes % interval == 0) or + (interval > 10 and + currentminutes % interval in [2, 4])): + self.update_device(devicename) + + def determine_interval(self, devicename, latitude, longitude, battery): + """Calculate new interval.""" + distancefromhome = None + zone_state = self.hass.states.get('zone.home') + zone_state_lat = zone_state.attributes['latitude'] + zone_state_long = zone_state.attributes['longitude'] + distancefromhome = distance(latitude, longitude, zone_state_lat, + zone_state_long) + distancefromhome = round(distancefromhome / 1000, 1) + + currentzone = active_zone(self.hass, latitude, longitude) + + if ((currentzone is not None and + currentzone == self._overridestates.get(devicename)) or + (currentzone is None and + self._overridestates.get(devicename) == 'away')): + return + + self._overridestates[devicename] = None + + if currentzone is not None: + self._intervals[devicename] = 30 + return + + if distancefromhome is None: + return + if distancefromhome > 25: + self._intervals[devicename] = round(distancefromhome / 2, 0) + elif distancefromhome > 10: + self._intervals[devicename] = 5 + else: + self._intervals[devicename] = 1 + if battery is not None and battery <= 33 and distancefromhome > 3: + self._intervals[devicename] = self._intervals[devicename] * 2 + + def update_device(self, devicename): + """Update the device_tracker entity.""" + from pyicloud.exceptions import PyiCloudNoDevicesException + + # An entity will not be created by see() when track=false in + # 'known_devices.yaml', but we need to see() it at least once + entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) + if entity is None and devicename in self.seen_devices: + return + attrs = {} + kwargs = {} + + if self.api is None: + return + + try: + for device in self.api.devices: + if str(device) != str(self.devices[devicename]): + continue + + status = device.status(DEVICESTATUSSET) + dev_id = status['name'].replace(' ', '', 99) + dev_id = slugify(dev_id) + attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( + status['deviceStatus'], 'error') + attrs[ATTR_LOWPOWERMODE] = status['lowPowerMode'] + attrs[ATTR_BATTERYSTATUS] = status['batteryStatus'] + attrs[ATTR_ACCOUNTNAME] = self.accountname + status = device.status(DEVICESTATUSSET) + battery = status.get('batteryLevel', 0) * 100 + location = status['location'] + if location: + self.determine_interval( + devicename, location['latitude'], + location['longitude'], battery) + interval = self._intervals.get(devicename, 1) + attrs[ATTR_INTERVAL] = interval + accuracy = location['horizontalAccuracy'] + kwargs['dev_id'] = dev_id + kwargs['host_name'] = status['name'] + kwargs['gps'] = (location['latitude'], + location['longitude']) + kwargs['battery'] = battery + kwargs['gps_accuracy'] = accuracy + kwargs[ATTR_ATTRIBUTES] = attrs + self.see(**kwargs) + self.seen_devices[devicename] = True + except PyiCloudNoDevicesException: + _LOGGER.error('No iCloud Devices found!') + + def lost_iphone(self, devicename): + """Call the lost iphone function if the device is found.""" + if self.api is None: + return + + self.api.authenticate() + + for device in self.api.devices: + if devicename is None or device == self.devices[devicename]: + device.play_sound() + + def update_icloud(self, devicename=None): + """Authenticate against iCloud and scan for devices.""" + from pyicloud.exceptions import PyiCloudNoDevicesException + + if self.api is None: + return + + try: + if devicename is not None: + if devicename in self.devices: + self.devices[devicename].update_icloud() + 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() + except PyiCloudNoDevicesException: + _LOGGER.error('No iCloud Devices found!') + + def setinterval(self, interval=None, devicename=None): + """Set the interval of the given devices.""" + devs = [devicename] if devicename else self.devices + for device in devs: + devid = DOMAIN + '.' + device + devicestate = self.hass.states.get(devid) + if interval is not None: + if devicestate is not None: + self._overridestates[device] = active_zone( + self.hass, + float(devicestate.attributes.get('latitude', 0)), + float(devicestate.attributes.get('longitude', 0))) + if self._overridestates[device] is None: + self._overridestates[device] = 'away' + self._intervals[device] = interval + else: + self._overridestates[device] = None + self.update_device(device) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index f3f2c3c94f5..f6419ae2490 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -4,6 +4,8 @@ Support for the Locative platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ +import asyncio +from functools import partial import logging from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME @@ -19,7 +21,7 @@ DEPENDENCIES = ['http'] def setup_scanner(hass, config, see): """Setup an endpoint for the Locative application.""" - hass.wsgi.register_view(LocativeView(hass, see)) + hass.http.register_view(LocativeView(hass, see)) return True @@ -35,15 +37,23 @@ class LocativeView(HomeAssistantView): super().__init__(hass) self.see = see + @asyncio.coroutine def get(self, request): """Locative message received as GET.""" - return self.post(request) + res = yield from self._handle(request.GET) + return res + @asyncio.coroutine def post(self, request): """Locative message received.""" - # pylint: disable=too-many-return-statements - data = request.values + data = yield from request.post() + res = yield from self._handle(data) + return res + @asyncio.coroutine + # pylint: disable=too-many-return-statements + def _handle(self, data): + """Handle locative request.""" if 'latitude' not in data or 'longitude' not in data: return ('Latitude and longitude not specified.', HTTP_UNPROCESSABLE_ENTITY) @@ -68,7 +78,9 @@ class LocativeView(HomeAssistantView): direction = data['trigger'] if direction == 'enter': - self.see(dev_id=device, location_name=location_name) + yield from self.hass.loop.run_in_executor( + None, partial(self.see, dev_id=device, + location_name=location_name)) return 'Setting location to {}'.format(location_name) elif direction == 'exit': @@ -76,7 +88,9 @@ class LocativeView(HomeAssistantView): '{}.{}'.format(DOMAIN, device)) if current_state is None or current_state.state == location_name: - self.see(dev_id=device, location_name=STATE_NOT_HOME) + yield from self.hass.loop.run_in_executor( + None, partial(self.see, dev_id=device, + location_name=STATE_NOT_HOME)) return 'Setting location to not home' else: # Ignore the message if it is telling us to exit a zone that we diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index b97993f9afa..f9e90fee6c7 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -37,7 +37,6 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -# pylint: disable=too-many-instance-attributes class LuciDeviceScanner(object): """This class queries a wireless router running OpenWrt firmware. diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 68155910ffc..e8a6f2b7371 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -18,16 +18,16 @@ from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import CONF_HOSTS from homeassistant.util import Throttle -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) +REQUIREMENTS = ['python-nmap==0.6.1'] _LOGGER = logging.getLogger(__name__) +CONF_EXCLUDE = 'exclude' # Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL = 'home_interval' -CONF_EXCLUDE = 'exclude' -REQUIREMENTS = ['python-nmap==0.6.1'] +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOSTS): cv.ensure_list, @@ -73,7 +73,7 @@ class NmapDeviceScanner(object): self.home_interval = timedelta(minutes=minutes) self.success_init = self._update_info() - _LOGGER.info('nmap scanner initialized') + _LOGGER.info("nmap scanner initialized") def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -97,7 +97,7 @@ class NmapDeviceScanner(object): Returns boolean if scanning successful. """ - _LOGGER.info('Scanning') + _LOGGER.info("Scanning...") from nmap import PortScanner, PortScannerError scanner = PortScanner() @@ -138,5 +138,5 @@ class NmapDeviceScanner(object): self.last_results = last_results - _LOGGER.info('nmap scan successful') + _LOGGER.info("nmap scan successful") return True diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index d903c25a5c8..d73cbfdb2ac 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -114,10 +114,9 @@ def setup_scanner(hass, config, see): 'for topic %s.', topic) return None + # pylint: disable=too-many-return-statements def validate_payload(topic, payload, data_type): """Validate the OwnTracks payload.""" - # pylint: disable=too-many-return-statements - try: data = json.loads(payload) except ValueError: @@ -143,9 +142,9 @@ def setup_scanner(hass, config, see): return data if max_gps_accuracy is not None and \ convert(data.get('acc'), float, 0.0) > max_gps_accuracy: - _LOGGER.warning('Ignoring %s update because expected GPS ' - 'accuracy %s is not met: %s', - data_type, max_gps_accuracy, payload) + _LOGGER.info('Ignoring %s update because expected GPS ' + 'accuracy %s is not met: %s', + data_type, max_gps_accuracy, payload) return None if convert(data.get('acc'), float, 1.0) == 0.0: _LOGGER.warning('Ignoring %s update because GPS accuracy' @@ -248,7 +247,7 @@ def setup_scanner(hass, config, see): if (max_gps_accuracy is not None and data['acc'] > max_gps_accuracy): valid_gps = False - _LOGGER.warning( + _LOGGER.info( 'Ignoring GPS in region exit because expected ' 'GPS accuracy %s is not met: %s', max_gps_accuracy, payload) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index dc573ae0275..2d3315b319a 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -31,3 +31,48 @@ see: battery: description: Battery level of device example: '100' + +icloud: + icloud_lost_iphone: + description: Service to play the lost iphone sound on an iDevice + + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. + example: 'iphonebart' + + icloud_set_interval: + description: Service to set the interval of an iDevice + + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. + example: 'iphonebart' + interval: + description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. + example: 1 + + icloud_update: + description: Service to ask for an update of an iDevice. + + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. + example: 'iphonebart' + + icloud_reset_account: + description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. + + fields: + account_name: + description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. + example: 'bart' diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 33c89110da0..39315ebfd7a 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -49,7 +49,6 @@ def get_scanner(hass, config): class SnmpScanner(object): """Queries any SNMP capable Access Point for connected devices.""" - # pylint: disable=too-many-instance-attributes def __init__(self, config): """Initialize the scanner.""" from pysnmp.entity.rfc3413.oneliner import cmdgen diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 5eaa4bf2fca..9d9b8e718d6 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -37,7 +37,6 @@ def get_scanner(hass, config): return scanner if scanner.success_init else None -# pylint: disable=too-many-instance-attributes class UbusDeviceScanner(object): """ This class queries a wireless router running OpenWrt firmware. diff --git a/homeassistant/components/device_tracker/volvooncall.py b/homeassistant/components/device_tracker/volvooncall.py index 746bbea6ccf..0fea3eadd65 100644 --- a/homeassistant/components/device_tracker/volvooncall.py +++ b/homeassistant/components/device_tracker/volvooncall.py @@ -7,9 +7,7 @@ https://home-assistant.io/components/device_tracker.volvooncall/ """ import logging from datetime import timedelta -from urllib.parse import urljoin import voluptuous as vol -import requests import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time @@ -27,10 +25,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1) _LOGGER = logging.getLogger(__name__) -SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/' -HEADERS = {"X-Device-Id": "Device", - "X-OS-Type": "Android", - "X-Originator-Type": "App"} +REQUIREMENTS = ['volvooncall==0.1.1'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, @@ -40,62 +35,62 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_scanner(hass, config, see): """Validate the configuration and return a scanner.""" - session = requests.Session() - session.headers.update(HEADERS) - session.auth = (config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) + from volvooncall import Connection + connection = Connection( + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) interval = max(MIN_TIME_BETWEEN_SCANS.seconds, config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) - def query(ref, rel=SERVICE_URL): - """Perform a query to the online service.""" - url = urljoin(rel, ref) - _LOGGER.debug("Request for %s", url) - res = session.get(url, timeout=15) - res.raise_for_status() - _LOGGER.debug("Received %s", res.json()) - return res.json() + def _see_vehicle(vehicle): + position = vehicle["position"] + dev_id = "volvo_" + slugify(vehicle["registrationNumber"]) + host_name = "%s (%s/%s)" % ( + vehicle["registrationNumber"], + vehicle["vehicleType"], + vehicle["modelYear"]) + + def any_opened(door): + """True if any door/window is opened.""" + return any([door[key] for key in door if "Open" in key]) + + see(dev_id=dev_id, + host_name=host_name, + gps=(position["latitude"], + position["longitude"]), + attributes=dict( + unlocked=not vehicle["carLocked"], + tank_volume=vehicle["fuelTankVolume"], + average_fuel_consumption=round( + vehicle["averageFuelConsumption"] / 10, 1), # l/100km + washer_fluid_low=vehicle["washerFluidLevel"] != "Normal", + brake_fluid_low=vehicle["brakeFluid"] != "Normal", + service_warning=vehicle["serviceWarningStatus"] != "Normal", + bulb_failures=len(vehicle["bulbFailures"]) > 0, + doors_open=any_opened(vehicle["doors"]), + windows_open=any_opened(vehicle["windows"]), + heater_on=vehicle["heater"]["status"] != "off", + fuel=vehicle["fuelAmount"], + odometer=round(vehicle["odometer"] / 1000), # km + range=vehicle["distanceToEmpty"])) def update(now): """Update status from the online service.""" + _LOGGER.info("Updating") try: - _LOGGER.debug("Updating") - status = query("status", vehicle_url) - position = query("position", vehicle_url) - see(dev_id=dev_id, - host_name=host_name, - gps=(position["position"]["latitude"], - position["position"]["longitude"]), - attributes=dict( - tank_volume=attributes["fuelTankVolume"], - washer_fluid=status["washerFluidLevel"], - brake_fluid=status["brakeFluid"], - service_warning=status["serviceWarningStatus"], - fuel=status["fuelAmount"], - odometer=status["odometer"], - range=status["distanceToEmpty"])) - except requests.exceptions.RequestException as error: - _LOGGER.error("Could not query server: %s", error) + res, vehicles = connection.update() + if not res: + _LOGGER.error("Could not query server") + return False + + for vehicle in vehicles: + _see_vehicle(vehicle) + + return True finally: track_point_in_utc_time(hass, update, now + timedelta(seconds=interval)) - try: - _LOGGER.info('Logging in to service') - user = query("customeraccounts") - rel = query(user["accountVehicleRelations"][0]) - vehicle_url = rel["vehicle"] + '/' - attributes = query("attributes", vehicle_url) - - dev_id = "volvo_" + slugify(attributes["registrationNumber"]) - host_name = "%s %s/%s" % (attributes["registrationNumber"], - attributes["vehicleType"], - attributes["modelYear"]) - update(utcnow()) - return True - except requests.exceptions.RequestException as error: - _LOGGER.error("Could not log in to service. " - "Please check configuration: " - "%s", error) - return False + _LOGGER.info('Logging in to service') + return update(utcnow()) diff --git a/homeassistant/components/digital_ocean.py b/homeassistant/components/digital_ocean.py index b507d9448e5..f976c17ae9d 100644 --- a/homeassistant/components/digital_ocean.py +++ b/homeassistant/components/digital_ocean.py @@ -42,7 +42,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument,too-few-public-methods def setup(hass, config): """Set up the Digital Ocean component.""" conf = config[DOMAIN] diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 32e1bbd5f6a..65a5af79bfb 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.discovery import load_platform, discover -REQUIREMENTS = ['netdisco==0.7.2'] +REQUIREMENTS = ['netdisco==0.7.5'] DOMAIN = 'discovery' @@ -33,6 +33,7 @@ SERVICE_HANDLERS = { 'plex_mediaserver': ('media_player', 'plex'), 'roku': ('media_player', 'roku'), 'sonos': ('media_player', 'sonos'), + 'yamaha': ('media_player', 'yamaha'), 'logitech_mediaserver': ('media_player', 'squeezebox'), 'directv': ('media_player', 'directv'), } diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index 57b6bd4dc6d..4330ad5be2f 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -38,7 +38,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-branches def setup(hass, config): """Listen for download events to download files.""" download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] diff --git a/homeassistant/components/dweet.py b/homeassistant/components/dweet.py index d56d9d2ef93..d812daf50a6 100644 --- a/homeassistant/components/dweet.py +++ b/homeassistant/components/dweet.py @@ -32,7 +32,6 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=too-many-locals def setup(hass, config): """Setup the Dweet.io component.""" conf = config[DOMAIN] diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 24d47365a54..825e7b700a5 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -86,7 +86,6 @@ def setup_ecobee(hass, network, config): discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) -# pylint: disable=too-few-public-methods class EcobeeData(object): """Get the latest data and update the states.""" diff --git a/homeassistant/components/emoncms_history.py b/homeassistant/components/emoncms_history.py index 4e07447b027..b2bc3967bc8 100644 --- a/homeassistant/components/emoncms_history.py +++ b/homeassistant/components/emoncms_history.py @@ -11,9 +11,7 @@ import voluptuous as vol import requests from homeassistant.const import ( - CONF_API_KEY, CONF_WHITELIST, - CONF_URL, STATE_UNKNOWN, - STATE_UNAVAILABLE, + CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE, CONF_SCAN_INTERVAL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper @@ -22,8 +20,8 @@ from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) -DOMAIN = "emoncms_history" -CONF_INPUTNODE = "inputnode" +DOMAIN = 'emoncms_history' +CONF_INPUTNODE = 'inputnode' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -37,20 +35,19 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): - """Setup the emoncms_history component.""" + """Set up the Emoncms history component.""" conf = config[DOMAIN] whitelist = conf.get(CONF_WHITELIST) def send_data(url, apikey, node, payload): - """Send payload data to emoncms.""" + """Send payload data to Emoncms.""" try: - fullurl = "{}/input/post.json".format(url) - req = requests.post(fullurl, - params={"node": node}, - data={"apikey": apikey, - "data": payload}, - allow_redirects=True, - timeout=5) + fullurl = '{}/input/post.json'.format(url) + data = {"apikey": apikey, "data": payload} + parameters = {"node": node} + req = requests.post( + fullurl, params=parameters, data=data, allow_redirects=True, + timeout=5) except requests.exceptions.RequestException: _LOGGER.error("Error saving data '%s' to '%s'", @@ -63,14 +60,14 @@ def setup(hass, config): fullurl, req.status_code) def update_emoncms(time): - """Send whitelisted entities states reguarly to emoncms.""" + """Send whitelisted entities states reguarly to Emoncms.""" payload_dict = {} for entity_id in whitelist: state = hass.states.get(entity_id) if state is None or state.state in ( - STATE_UNKNOWN, "", STATE_UNAVAILABLE): + STATE_UNKNOWN, '', STATE_UNAVAILABLE): continue try: @@ -88,8 +85,7 @@ def setup(hass, config): str(conf.get(CONF_INPUTNODE)), payload) track_point_in_time(hass, update_emoncms, time + - timedelta(seconds=conf.get( - CONF_SCAN_INTERVAL))) + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL))) update_emoncms(dt_util.utcnow()) return True diff --git a/homeassistant/components/emulated_hue.py b/homeassistant/components/emulated_hue.py index a63117fc31b..187ee0de603 100644 --- a/homeassistant/components/emulated_hue.py +++ b/homeassistant/components/emulated_hue.py @@ -4,20 +4,21 @@ Support for local control of entities by emulating the Phillips Hue bridge. For more details about this component, please refer to the documentation at https://home-assistant.io/components/emulated_hue/ """ +import asyncio import threading import socket import logging -import json import os import select +from aiohttp import web import voluptuous as vol from homeassistant import util, core from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_ON, HTTP_BAD_REQUEST + STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS @@ -25,8 +26,6 @@ from homeassistant.components.light import ( from homeassistant.components.http import ( HomeAssistantView, HomeAssistantWSGI ) -# pylint: disable=unused-import -from homeassistant.components.http import REQUIREMENTS # noqa import homeassistant.helpers.config_validation as cv DOMAIN = 'emulated_hue' @@ -87,24 +86,25 @@ def setup(hass, yaml_config): upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port) - def start_emulated_hue_bridge(event): - """Start the emulated hue bridge.""" - server.start() - upnp_listener.start() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) - + @asyncio.coroutine def stop_emulated_hue_bridge(event): """Stop the emulated hue bridge.""" upnp_listener.stop() - server.stop() + yield from server.stop() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) + @asyncio.coroutine + def start_emulated_hue_bridge(event): + """Start the emulated hue bridge.""" + upnp_listener.start() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + stop_emulated_hue_bridge) + yield from server.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) return True -# pylint: disable=too-few-public-methods class Config(object): """Holds configuration variables for the emulated hue bridge.""" @@ -158,6 +158,7 @@ class DescriptionXmlView(HomeAssistantView): super().__init__(hass) self.config = config + @core.callback def get(self, request): """Handle a GET request.""" xml_template = """ @@ -185,7 +186,7 @@ class DescriptionXmlView(HomeAssistantView): resp_text = xml_template.format( self.config.host_ip_addr, self.config.listen_port) - return self.Response(resp_text, mimetype='text/xml') + return web.Response(text=resp_text, content_type='text/xml') class HueUsernameView(HomeAssistantView): @@ -200,9 +201,13 @@ class HueUsernameView(HomeAssistantView): """Initialize the instance of the view.""" super().__init__(hass) + @asyncio.coroutine def post(self, request): """Handle a POST request.""" - data = request.json + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) if 'devicetype' not in data: return self.json_message('devicetype not specified', @@ -214,10 +219,10 @@ class HueUsernameView(HomeAssistantView): class HueLightsView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" - url = '/api//lights' + url = '/api/{username}/lights' name = 'api:username:lights' - extra_urls = ['/api//lights/', - '/api//lights//state'] + extra_urls = ['/api/{username}/lights/{entity_id}', + '/api/{username}/lights/{entity_id}/state'] requires_auth = False def __init__(self, hass, config): @@ -226,58 +231,51 @@ class HueLightsView(HomeAssistantView): self.config = config self.cached_states = {} + @core.callback def get(self, request, username, entity_id=None): """Handle a GET request.""" if entity_id is None: - return self.get_lights_list() + return self.async_get_lights_list() - if not request.base_url.endswith('state'): - return self.get_light_state(entity_id) + if not request.path.endswith('state'): + return self.async_get_light_state(entity_id) - return self.Response("Method not allowed", status=405) + return web.Response(text="Method not allowed", status=405) + @asyncio.coroutine def put(self, request, username, entity_id=None): """Handle a PUT request.""" - if not request.base_url.endswith('state'): - return self.Response("Method not allowed", status=405) + if not request.path.endswith('state'): + return web.Response(text="Method not allowed", status=405) - content_type = request.environ.get('CONTENT_TYPE', '') - if content_type == 'application/x-www-form-urlencoded': - # Alexa sends JSON data with a form data content type, for - # whatever reason, and Werkzeug parses form data automatically, - # so we need to do some gymnastics to get the data we need - json_data = None + if entity_id and self.hass.states.get(entity_id) is None: + return self.json_message('Entity not found', HTTP_NOT_FOUND) - for key in request.form: - try: - json_data = json.loads(key) - break - except ValueError: - # Try the next key? - pass + try: + json_data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) - if json_data is None: - return self.Response("Bad request", status=400) - else: - json_data = request.json + result = yield from self.async_put_light_state(json_data, entity_id) + return result - return self.put_light_state(json_data, entity_id) - - def get_lights_list(self): + @core.callback + def async_get_lights_list(self): """Process a request to get the list of available lights.""" json_response = {} - for entity in self.hass.states.all(): + for entity in self.hass.states.async_all(): if self.is_entity_exposed(entity): json_response[entity.entity_id] = entity_to_json(entity) return self.json(json_response) - def get_light_state(self, entity_id): + @core.callback + def async_get_light_state(self, entity_id): """Process a request to get the state of an individual light.""" entity = self.hass.states.get(entity_id) if entity is None or not self.is_entity_exposed(entity): - return self.Response("Entity not found", status=404) + return web.Response(text="Entity not found", status=404) cached_state = self.cached_states.get(entity_id, None) @@ -292,23 +290,24 @@ class HueLightsView(HomeAssistantView): return self.json(json_response) - def put_light_state(self, request_json, entity_id): + @asyncio.coroutine + def async_put_light_state(self, request_json, entity_id): """Process a request to set the state of an individual light.""" config = self.config # Retrieve the entity from the state machine entity = self.hass.states.get(entity_id) if entity is None: - return self.Response("Entity not found", status=404) + return web.Response(text="Entity not found", status=404) if not self.is_entity_exposed(entity): - return self.Response("Entity not found", status=404) + return web.Response(text="Entity not found", status=404) # Parse the request into requested "on" status and brightness parsed = parse_hue_api_put_light_body(request_json, entity) if parsed is None: - return self.Response("Bad request", status=400) + return web.Response(text="Bad request", status=400) result, brightness = parsed @@ -333,7 +332,8 @@ class HueLightsView(HomeAssistantView): self.cached_states[entity_id] = (result, brightness) # Perform the requested action - self.hass.services.call(core.DOMAIN, service, data, blocking=True) + yield from self.hass.services.async_call(core.DOMAIN, service, data, + blocking=True) json_response = \ [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] @@ -345,7 +345,10 @@ class HueLightsView(HomeAssistantView): return self.json(json_response) def is_entity_exposed(self, entity): - """Determine if an entity should be exposed on the emulated bridge.""" + """Determine if an entity should be exposed on the emulated bridge. + + Async friendly. + """ config = self.config if entity.attributes.get('view') is not None: diff --git a/homeassistant/components/enocean.py b/homeassistant/components/enocean.py index 7c36e173510..33c6359d43f 100644 --- a/homeassistant/components/enocean.py +++ b/homeassistant/components/enocean.py @@ -56,14 +56,14 @@ class EnOceanDongle: """Send a command from the EnOcean dongle.""" self.__communicator.send(command) - def _combine_hex(self, data): # pylint: disable=no-self-use + # pylint: disable=no-self-use + def _combine_hex(self, data): """Combine list of integer values to one big integer.""" output = 0x00 for i, j in enumerate(reversed(data)): output |= (j << i * 8) return output - # pylint: disable=too-many-branches def callback(self, temp): """Callback function for EnOcean Device. @@ -112,7 +112,6 @@ class EnOceanDongle: device.value_changed(value) -# pylint: disable=too-few-public-methods class EnOceanDevice(): """Parent class for all devices associated with the EnOcean component.""" diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 0572de9aba6..21bc081224b 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -77,8 +77,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=unused-argument, too-many-function-args, too-many-locals -# pylint: disable=too-many-return-statements +# pylint: disable=unused-argument def setup(hass, base_config): """Common setup for Envisalink devices.""" from pyenvisalink import EnvisalinkAlarmPanel diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a129ece3609..79793435625 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -86,7 +86,6 @@ def is_on(hass, entity_id: str=None) -> bool: return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] -# pylint: disable=too-many-arguments def turn_on(hass, entity_id: str=None, speed: str=None) -> None: """Turn all or specified fan on.""" data = { @@ -141,7 +140,6 @@ def set_speed(hass, entity_id: str=None, speed: str=None) -> None: hass.services.call(DOMAIN, SERVICE_SET_SPEED, data) -# pylint: disable=too-many-branches, too-many-locals, too-many-statements def setup(hass, config: dict) -> None: """Expose fan control via statemachine and services.""" component = EntityComponent( @@ -198,7 +196,7 @@ def setup(hass, config: dict) -> None: class FanEntity(ToggleEntity): """Representation of a fan.""" - # pylint: disable=no-self-use, abstract-method + # pylint: disable=no-self-use def set_speed(self: ToggleEntity, speed: str) -> None: """Set the speed of the fan.""" diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index b6703e22c19..08db5ead26b 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -110,11 +110,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): )]) -# pylint: disable=too-many-instance-attributes class MqttFan(FanEntity): """A MQTT fan component.""" - # pylint: disable=too-many-arguments def __init__(self, hass, name, topic, templates, qos, retain, payload, speed_list, optimistic): """Initialize the MQTT fan.""" diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index ce3d46b4751..a563b51402e 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -44,7 +44,6 @@ def setup(hass, config): return len(feeds) > 0 -# pylint: disable=too-few-public-methods class FeedManager(object): """Abstraction over feedparser module.""" diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 0ba015a4660..f345153e666 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -4,14 +4,16 @@ Component that will help set the ffmpeg component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ffmpeg/ """ +import asyncio import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.util.async import run_coroutine_threadsafe DOMAIN = 'ffmpeg' -REQUIREMENTS = ["ha-ffmpeg==0.13"] +REQUIREMENTS = ["ha-ffmpeg==0.15"] _LOGGER = logging.getLogger(__name__) @@ -47,13 +49,26 @@ def setup(hass, config): def get_binary(): - """Return ffmpeg binary from config.""" + """Return ffmpeg binary from config. + + Async friendly. + """ return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN) -def run_test(input_source): +def run_test(hass, input_source): """Run test on this input. TRUE is deactivate or run correct.""" - from haffmpeg import Test + return run_coroutine_threadsafe( + async_run_test(hass, input_source), hass.loop).result() + + +@asyncio.coroutine +def async_run_test(hass, input_source): + """Run test on this input. TRUE is deactivate or run correct. + + This method must be run in the event loop. + """ + from haffmpeg import TestAsync if FFMPEG_CONFIG.get(CONF_RUN_TEST): # if in cache @@ -61,8 +76,9 @@ def run_test(input_source): return FFMPEG_TEST_CACHE[input_source] # run test - test = Test(get_binary()) - if not test.run_test(input_source): + ffmpeg_test = TestAsync(get_binary(), loop=hass.loop) + success = yield from ffmpeg_test.run_test(input_source) + if not success: _LOGGER.error("FFmpeg '%s' test fails!", input_source) FFMPEG_TEST_CACHE[input_source] = False return False diff --git a/homeassistant/components/foursquare.py b/homeassistant/components/foursquare.py index b08ba89ca77..bb4c66ad1f9 100644 --- a/homeassistant/components/foursquare.py +++ b/homeassistant/components/foursquare.py @@ -4,14 +4,14 @@ Allows utilizing the Foursquare (Swarm) API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/foursquare/ """ +import asyncio import logging import os -import json import requests import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST from homeassistant.config import load_yaml_config_file import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView @@ -75,7 +75,7 @@ def setup(hass, config): descriptions[DOMAIN][SERVICE_CHECKIN], schema=CHECKIN_SERVICE_SCHEMA) - hass.wsgi.register_view(FoursquarePushReceiver( + hass.http.register_view(FoursquarePushReceiver( hass, config[CONF_PUSH_SECRET])) return True @@ -93,16 +93,21 @@ class FoursquarePushReceiver(HomeAssistantView): super().__init__(hass) self.push_secret = push_secret + @asyncio.coroutine def post(self, request): """Accept the POST from Foursquare.""" - raw_data = request.form - _LOGGER.debug("Received Foursquare push: %s", raw_data) - if self.push_secret != raw_data["secret"]: + try: + data = yield from request.json() + except ValueError: + return self.json_message('Invalid JSON', HTTP_BAD_REQUEST) + + secret = data.pop('secret', None) + + _LOGGER.debug("Received Foursquare push: %s", data) + + if self.push_secret != secret: _LOGGER.error("Received Foursquare push with invalid" - "push secret! Data: %s", raw_data) - return - parsed_payload = { - key: json.loads(val) for key, val in raw_data.items() - if key != "secret" - } - self.hass.bus.fire(EVENT_PUSH, parsed_payload) + "push secret: %s", secret) + return self.json_message('Incorrect secret', HTTP_BAD_REQUEST) + + self.hass.bus.async_fire(EVENT_PUSH, data) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2d9abe8fe33..195d79ec5da 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -1,10 +1,15 @@ """Handle the frontend for Home Assistant.""" +import asyncio import hashlib +import json import logging import os -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.components import api +from aiohttp import web + +from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_NOT_FOUND +from homeassistant.components import api, group from homeassistant.components.http import HomeAssistantView from .version import FINGERPRINTS @@ -22,7 +27,6 @@ MANIFEST_JSON = { "icons": [], "lang": "en-US", "name": "Home Assistant", - "orientation": "any", "short_name": "Assistant", "start_url": "/", "theme_color": "#03A9F4" @@ -36,10 +40,9 @@ _LOGGER = logging.getLogger(__name__) def register_built_in_panel(hass, component_name, sidebar_title=None, sidebar_icon=None, url_path=None, config=None): """Register a built-in panel.""" - # pylint: disable=too-many-arguments path = 'panels/ha-panel-{}.html'.format(component_name) - if hass.wsgi.development: + if hass.http.development: url = ('/static/home-assistant-polymer/panels/' '{0}/ha-panel-{0}.html'.format(component_name)) else: @@ -65,7 +68,6 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None, Warning: this API will probably change. Use at own risk. """ - # pylint: disable=too-many-arguments if url_path is None: url_path = component_name @@ -98,7 +100,7 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None, url = URL_PANEL_COMPONENT.format(component_name) if url not in _REGISTERED_COMPONENTS: - hass.wsgi.register_static_path(url, path) + hass.http.register_static_path(url, path) _REGISTERED_COMPONENTS.add(url) fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) @@ -114,20 +116,23 @@ def add_manifest_json_key(key, val): def setup(hass, config): """Setup serving the frontend.""" - hass.wsgi.register_view(BootstrapView) - hass.wsgi.register_view(ManifestJSONView) + hass.http.register_view(BootstrapView) + hass.http.register_view(ManifestJSONView) - if hass.wsgi.development: + if hass.http.development: sw_path = "home-assistant-polymer/build/service_worker.js" else: sw_path = "service_worker.js" - hass.wsgi.register_static_path("/service_worker.js", + hass.http.register_static_path("/service_worker.js", os.path.join(STATIC_PATH, sw_path), 0) - hass.wsgi.register_static_path("/robots.txt", + hass.http.register_static_path("/robots.txt", os.path.join(STATIC_PATH, "robots.txt")) - hass.wsgi.register_static_path("/static", STATIC_PATH) - hass.wsgi.register_static_path("/local", hass.config.path('www')) + hass.http.register_static_path("/static", STATIC_PATH) + + local = hass.config.path('www') + if os.path.isdir(local): + hass.http.register_static_path("/local", local) register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location') @@ -140,7 +145,7 @@ def setup(hass, config): Done when Home Assistant is started so that all panels are known. """ - hass.wsgi.register_view(IndexView( + hass.http.register_view(IndexView( hass, ['/{}'.format(name) for name in PANELS])) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) @@ -161,13 +166,14 @@ class BootstrapView(HomeAssistantView): url = "/api/bootstrap" name = "api:bootstrap" + @callback def get(self, request): """Return all data needed to bootstrap Home Assistant.""" return self.json({ 'config': self.hass.config.as_dict(), - 'states': self.hass.states.all(), - 'events': api.events_json(self.hass), - 'services': api.services_json(self.hass), + 'states': self.hass.states.async_all(), + 'events': api.async_events_json(self.hass), + 'services': api.async_services_json(self.hass), 'panels': PANELS, }) @@ -178,7 +184,7 @@ class IndexView(HomeAssistantView): url = '/' name = "frontend:index" requires_auth = False - extra_urls = ['/states', '/states/'] + extra_urls = ['/states', '/states/{entity_id}'] def __init__(self, hass, extra_urls): """Initialize the frontend view.""" @@ -193,9 +199,17 @@ class IndexView(HomeAssistantView): ) ) + @asyncio.coroutine def get(self, request, entity_id=None): """Serve the index view.""" - if self.hass.wsgi.development: + if entity_id is not None: + state = self.hass.states.get(entity_id) + + if (not state or state.domain != 'group' or + not state.attributes.get(group.ATTR_VIEW)): + return self.json_message('Entity not found', HTTP_NOT_FOUND) + + if self.hass.http.development: core_url = '/static/home-assistant-polymer/build/core.js' ui_url = '/static/home-assistant-polymer/src/home-assistant.html' else: @@ -215,22 +229,24 @@ class IndexView(HomeAssistantView): if self.hass.config.api.api_password: # require password if set no_auth = 'false' - if self.hass.wsgi.is_trusted_ip( - self.hass.wsgi.get_real_ip(request)): + if self.hass.http.is_trusted_ip( + self.hass.http.get_real_ip(request)): # bypass for trusted networks no_auth = 'true' icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) - template = self.templates.get_template('index.html') + template = yield from self.hass.loop.run_in_executor( + None, self.templates.get_template, 'index.html') # pylint is wrong # pylint: disable=no-member + # This is a jinja2 template, not a HA template so we call 'render'. resp = template.render( core_url=core_url, ui_url=ui_url, no_auth=no_auth, icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], panel_url=panel_url, panels=PANELS) - return self.Response(resp, mimetype='text/html') + return web.Response(text=resp, content_type='text/html') class ManifestJSONView(HomeAssistantView): @@ -240,8 +256,8 @@ class ManifestJSONView(HomeAssistantView): url = "/manifest.json" name = "manifestjson" - def get(self, request): + @asyncio.coroutine + def get(self, request): # pylint: disable=no-self-use """Return the manifest.json.""" - import json msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8') - return self.Response(msg, mimetype="application/manifest+json") + return web.Response(body=msg, content_type="application/manifest+json") diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 1c437dedd5d..1a0dac2f3bc 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -2,14 +2,14 @@ FINGERPRINTS = { "core.js": "5ed5e063d66eb252b5b288738c9c2d16", - "frontend.html": "0a4c2c6e86a0a78c2ff3e03842de609d", + "frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d", "mdi.html": "46a76f877ac9848899b8ed382427c16f", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002", "panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a", - "panels/ha-panel-dev-service.html": "d33657c964041d3ebf114e90a922a15e", + "panels/ha-panel-dev-service.html": "4a051878b92b002b8b018774ba207769", "panels/ha-panel-dev-state.html": "65e5f791cc467561719bf591f1386054", - "panels/ha-panel-dev-template.html": "d23943fa0370f168714da407c90091a2", + "panels/ha-panel-dev-template.html": "7d744ab7f7c08b6d6ad42069989de400", "panels/ha-panel-history.html": "efe1bcdd7733b09e55f4f965d171c295", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 58990a9c766..c6c2fed44be 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,5 +1,5 @@ \ No newline at end of file +},customStyle:null,getComputedStyleValue:function(e){return!i&&this._styleProperties&&this._styleProperties[e]||getComputedStyle(this).getPropertyValue(e)},_setupStyleProperties:function(){this.customStyle={},this._styleCache=null,this._styleProperties=null,this._scopeSelector=null,this._ownStyleProperties=null,this._customStyle=null},_needsStyleProperties:function(){return Boolean(!i&&this._ownStylePropertyNames&&this._ownStylePropertyNames.length)},_validateApplyShim:function(){if(this.__applyShimInvalid){Polymer.ApplyShim.transform(this._styles,this.__proto__);var e=n.elementStyles(this);if(s){var t=this._template.content.querySelector("style");t&&(t.textContent=e)}else{var r=this._scopeStyle&&this._scopeStyle.nextSibling;r&&(r.textContent=e)}}},_beforeAttached:function(){this._scopeSelector&&!this.__stylePropertiesInvalid||!this._needsStyleProperties()||(this.__stylePropertiesInvalid=!1,this._updateStyleProperties())},_findStyleHost:function(){for(var e,t=this;e=Polymer.dom(t).getOwnerRoot();){if(Polymer.isInstance(e.host))return e.host;t=e.host}return r},_updateStyleProperties:function(){var e,n=this._findStyleHost();n._styleProperties||n._computeStyleProperties(),n._styleCache||(n._styleCache=new Polymer.StyleCache);var r=t.propertyDataFromStyles(n._styles,this),i=!this.__notStyleScopeCacheable;i&&(r.key.customStyle=this.customStyle,e=n._styleCache.retrieve(this.is,r.key,this._styles));var a=Boolean(e);a?this._styleProperties=e._styleProperties:this._computeStyleProperties(r.properties),this._computeOwnStyleProperties(),a||(e=o.retrieve(this.is,this._ownStyleProperties,this._styles));var l=Boolean(e)&&!a,c=this._applyStyleProperties(e);a||(c=c&&s?c.cloneNode(!0):c,e={style:c,_scopeSelector:this._scopeSelector,_styleProperties:this._styleProperties},i&&(r.key.customStyle={},this.mixin(r.key.customStyle,this.customStyle),n._styleCache.store(this.is,e,r.key,this._styles)),l||o.store(this.is,Object.create(e),this._ownStyleProperties,this._styles))},_computeStyleProperties:function(e){var n=this._findStyleHost();n._styleProperties||n._computeStyleProperties();var r=Object.create(n._styleProperties),s=t.hostAndRootPropertiesForScope(this);this.mixin(r,s.hostProps),e=e||t.propertyDataFromStyles(n._styles,this).properties,this.mixin(r,e),this.mixin(r,s.rootProps),t.mixinCustomStyle(r,this.customStyle),t.reify(r),this._styleProperties=r},_computeOwnStyleProperties:function(){for(var e,t={},n=0;n0&&l.push(t);return[{removed:a,added:l}]}},Polymer.Collection.get=function(e){return Polymer._collections.get(e)||new Polymer.Collection(e)},Polymer.Collection.applySplices=function(e,t){var n=Polymer._collections.get(e);return n?n._applySplices(t):null},Polymer({is:"dom-repeat",extends:"template",_template:null,properties:{items:{type:Array},as:{type:String,value:"item"},indexAs:{type:String,value:"index"},sort:{type:Function,observer:"_sortChanged"},filter:{type:Function,observer:"_filterChanged"},observe:{type:String,observer:"_observeChanged"},delay:Number,renderedItemCount:{type:Number,notify:!0,readOnly:!0},initialCount:{type:Number,observer:"_initializeChunking"},targetFramerate:{type:Number,value:20},_targetFrameTime:{type:Number,computed:"_computeFrameTime(targetFramerate)"}},behaviors:[Polymer.Templatizer],observers:["_itemsChanged(items.*)"],created:function(){this._instances=[],this._pool=[],this._limit=1/0;var e=this;this._boundRenderChunk=function(){e._renderChunk()}},detached:function(){this.__isDetached=!0;for(var e=0;e=0;t--){var n=this._instances[t];n.isPlaceholder&&t=this._limit&&(n=this._downgradeInstance(t,n.__key__)),e[n.__key__]=t,n.isPlaceholder||n.__setProperty(this.indexAs,t,!0)}this._pool.length=0,this._setRenderedItemCount(this._instances.length),this.fire("dom-change"),this._tryRenderChunk()},_applyFullRefresh:function(){var e,t=this.collection;if(this._sortFn)e=t?t.getKeys():[];else{e=[];var n=this.items;if(n)for(var r=0;r=r;a--)this._detachAndRemoveInstance(a)},_numericSort:function(e,t){return e-t},_applySplicesUserSort:function(e){for(var t,n,r=this.collection,s={},i=0;i=0;i--){var c=a[i];void 0!==c&&this._detachAndRemoveInstance(c)}var h=this;if(l.length){this._filterFn&&(l=l.filter(function(e){return h._filterFn(r.getItem(e))})),l.sort(function(e,t){return h._sortFn(r.getItem(e),r.getItem(t))});var u=0;for(i=0;i>1,a=this._instances[o].__key__,l=this._sortFn(n.getItem(a),r);if(l<0)e=o+1;else{if(!(l>0)){i=o;break}s=o-1}}return i<0&&(i=s+1),this._insertPlaceholder(i,t),i},_applySplicesArrayOrder:function(e){for(var t,n=0;n=0?(e=this.as+"."+e.substring(n+1),i._notifyPath(e,t,!0)):i.__setProperty(this.as,t,!0))}},itemForElement:function(e){var t=this.modelForElement(e);return t&&t[this.as]},keyForElement:function(e){var t=this.modelForElement(e);return t&&t.__key__},indexForElement:function(e){var t=this.modelForElement(e);return t&&t[this.indexAs]}}),Polymer({is:"array-selector",_template:null,properties:{items:{type:Array,observer:"clearSelection"},multi:{type:Boolean,value:!1,observer:"clearSelection"},selected:{type:Object,notify:!0},selectedItem:{type:Object,notify:!0},toggle:{type:Boolean,value:!1}},clearSelection:function(){if(Array.isArray(this.selected))for(var e=0;e \ 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 7e2be4a3b4c..975f1668bd4 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 f3081ed48fd..896e0427675 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit f3081ed48fd11fa89586701dba3792d028473a15 +Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f diff --git a/homeassistant/components/frontend/www_static/images/config_icloud.png b/homeassistant/components/frontend/www_static/images/config_icloud.png new file mode 100644 index 00000000000..2058986018b Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/config_icloud.png differ diff --git a/homeassistant/components/frontend/www_static/images/logo_philips_hue.png b/homeassistant/components/frontend/www_static/images/logo_philips_hue.png new file mode 100644 index 00000000000..ae4df811fa8 Binary files /dev/null and b/homeassistant/components/frontend/www_static/images/logo_philips_hue.png differ diff --git a/homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png b/homeassistant/components/frontend/www_static/images/logo_plex_mediaserver.png similarity index 100% rename from homeassistant/components/frontend/www_static/images/config_plex_mediaserver.png rename to homeassistant/components/frontend/www_static/images/logo_plex_mediaserver.png 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 458e136f7b1..4f93b83631b 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 458b49d4f44..3b51da81366 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/panels/ha-panel-dev-template.html b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html index 0b665c3b983..56d67d09e27 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html @@ -1,2 +1,2 @@ \ No newline at end of file + clear: both;white-space:pre-wrap}.rendered.error{color:red}
Templates

Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.

[[processed]]
\ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz index 5421d69c020..6a2f68ab114 100644 Binary files a/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz and b/homeassistant/components/frontend/www_static/panels/ha-panel-dev-template.html.gz differ diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 6c01252b0d3..e4ba63ad1bc 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=[["/","8ab2c244274dcd7d80a34d37dcb5cb7e"],["/frontend/panels/dev-event-550bf85345c454274a40d15b2795a002.html","6977c253b5b4da588d50b0aaa50b21f4"],["/frontend/panels/dev-info-ec613406ce7e20d93754233d55625c8a.html","8e28a4c617fd6963b45103d5e5c80617"],["/frontend/panels/dev-service-d33657c964041d3ebf114e90a922a15e.html","cd3a429ec2f82fd58fd06b00e8d7df1f"],["/frontend/panels/dev-state-65e5f791cc467561719bf591f1386054.html","78158786a6597ef86c3fd6f4985cde92"],["/frontend/panels/dev-template-d23943fa0370f168714da407c90091a2.html","2cf2426a6aa4ee9c1df74926dc475bc8"],["/frontend/panels/map-49ab2d6f180f8bdea7cffaa66b8a5d3e.html","6e6c9c74e0b2424b62d4cc55b8e89be3"],["/static/core-5ed5e063d66eb252b5b288738c9c2d16.js","59dabb570c57dd421d5197009bf1d07f"],["/static/frontend-0a4c2c6e86a0a78c2ff3e03842de609d.html","5241dccfe1df6b25ef92830d64f90282"],["/static/mdi-46a76f877ac9848899b8ed382427c16f.html","a846c4082dd5cffd88ac72cbe943e691"],["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","b0f32ad3c7749c40d486603f31c9d8b1"]],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"}))}))})}).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