Merge pull request #4166 from home-assistant/dev

0.32
This commit is contained in:
Paulus Schoutsen 2016-11-05 08:52:57 -07:00 committed by GitHub
commit 4770888d22
527 changed files with 10521 additions and 10727 deletions

View File

@ -37,6 +37,9 @@ omit =
homeassistant/components/isy994.py homeassistant/components/isy994.py
homeassistant/components/*/isy994.py homeassistant/components/*/isy994.py
homeassistant/components/litejet.py
homeassistant/components/*/litejet.py
homeassistant/components/modbus.py homeassistant/components/modbus.py
homeassistant/components/*/modbus.py homeassistant/components/*/modbus.py
@ -98,8 +101,6 @@ omit =
homeassistant/components/homematic.py homeassistant/components/homematic.py
homeassistant/components/*/homematic.py homeassistant/components/*/homematic.py
homeassistant/components/switch/pilight.py
homeassistant/components/knx.py homeassistant/components/knx.py
homeassistant/components/*/knx.py homeassistant/components/*/knx.py
@ -109,6 +110,9 @@ omit =
homeassistant/components/zoneminder.py homeassistant/components/zoneminder.py
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/alarmdotcom.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/nx584.py
@ -128,6 +132,7 @@ omit =
homeassistant/components/climate/knx.py homeassistant/components/climate/knx.py
homeassistant/components/climate/proliphix.py homeassistant/components/climate/proliphix.py
homeassistant/components/climate/radiotherm.py homeassistant/components/climate/radiotherm.py
homeassistant/components/cover/garadget.py
homeassistant/components/cover/homematic.py homeassistant/components/cover/homematic.py
homeassistant/components/cover/rpi_gpio.py homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/scsgate.py homeassistant/components/cover/scsgate.py
@ -136,10 +141,9 @@ omit =
homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/aruba.py
homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/asuswrt.py
homeassistant/components/device_tracker/bbox.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_le_tracker.py
homeassistant/components/device_tracker/bluetooth_tracker.py
homeassistant/components/device_tracker/bt_home_hub_5.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/fritz.py
homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/icloud.py
homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/luci.py
@ -157,8 +161,6 @@ omit =
homeassistant/components/fan/mqtt.py homeassistant/components/fan/mqtt.py
homeassistant/components/feedreader.py homeassistant/components/feedreader.py
homeassistant/components/foursquare.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/hdmi_cec.py
homeassistant/components/ifttt.py homeassistant/components/ifttt.py
homeassistant/components/joaoapps_join.py homeassistant/components/joaoapps_join.py
@ -172,12 +174,14 @@ omit =
homeassistant/components/light/limitlessled.py homeassistant/components/light/limitlessled.py
homeassistant/components/light/osramlightify.py homeassistant/components/light/osramlightify.py
homeassistant/components/light/x10.py homeassistant/components/light/x10.py
homeassistant/components/light/yeelight.py
homeassistant/components/lirc.py homeassistant/components/lirc.py
homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py homeassistant/components/media_player/cast.py
homeassistant/components/media_player/cmus.py homeassistant/components/media_player/cmus.py
homeassistant/components/media_player/denon.py homeassistant/components/media_player/denon.py
homeassistant/components/media_player/directv.py homeassistant/components/media_player/directv.py
homeassistant/components/media_player/emby.py
homeassistant/components/media_player/firetv.py homeassistant/components/media_player/firetv.py
homeassistant/components/media_player/gpmdp.py homeassistant/components/media_player/gpmdp.py
homeassistant/components/media_player/itunes.py homeassistant/components/media_player/itunes.py
@ -188,6 +192,7 @@ omit =
homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/onkyo.py
homeassistant/components/media_player/panasonic_viera.py homeassistant/components/media_player/panasonic_viera.py
homeassistant/components/media_player/pandora.py homeassistant/components/media_player/pandora.py
homeassistant/components/media_player/philips_js.py
homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/plex.py homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py homeassistant/components/media_player/roku.py
@ -209,6 +214,7 @@ omit =
homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/llamalab_automate.py
homeassistant/components/notify/matrix.py homeassistant/components/notify/matrix.py
homeassistant/components/notify/message_bird.py homeassistant/components/notify/message_bird.py
homeassistant/components/notify/nfandroidtv.py
homeassistant/components/notify/nma.py homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py homeassistant/components/notify/pushetta.py
@ -234,9 +240,12 @@ omit =
homeassistant/components/sensor/bom.py homeassistant/components/sensor/bom.py
homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/coinmarketcap.py
homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/cpuspeed.py
homeassistant/components/sensor/cups.py
homeassistant/components/sensor/currencylayer.py
homeassistant/components/sensor/darksky.py homeassistant/components/sensor/darksky.py
homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py homeassistant/components/sensor/dht.py
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dte_energy_bridge.py
homeassistant/components/sensor/efergy.py homeassistant/components/sensor/efergy.py
homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/eliqonline.py
@ -250,9 +259,11 @@ omit =
homeassistant/components/sensor/gpsd.py homeassistant/components/sensor/gpsd.py
homeassistant/components/sensor/gtfs.py homeassistant/components/sensor/gtfs.py
homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hddtemp.py
homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py
homeassistant/components/sensor/lastfm.py homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
@ -267,7 +278,6 @@ omit =
homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/pi_hole.py homeassistant/components/sensor/pi_hole.py
homeassistant/components/sensor/plex.py homeassistant/components/sensor/plex.py
homeassistant/components/sensor/rest.py
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
@ -277,6 +287,7 @@ omit =
homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_hydrological_data.py
homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/swiss_public_transport.py
homeassistant/components/sensor/synologydsm.py
homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/systemmonitor.py
homeassistant/components/sensor/ted5000.py homeassistant/components/sensor/ted5000.py
homeassistant/components/sensor/temper.py homeassistant/components/sensor/temper.py
@ -286,7 +297,6 @@ omit =
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/worldclock.py
homeassistant/components/sensor/xbox_live.py homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py homeassistant/components/sensor/yweather.py
homeassistant/components/switch/acer_projector.py homeassistant/components/switch/acer_projector.py
@ -299,18 +309,16 @@ omit =
homeassistant/components/switch/neato.py homeassistant/components/switch/neato.py
homeassistant/components/switch/netio.py homeassistant/components/switch/netio.py
homeassistant/components/switch/orvibo.py homeassistant/components/switch/orvibo.py
homeassistant/components/switch/pilight.py
homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/pulseaudio_loopback.py
homeassistant/components/switch/rest.py homeassistant/components/switch/rest.py
homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/rpi_rf.py
homeassistant/components/switch/tplink.py homeassistant/components/switch/tplink.py
homeassistant/components/switch/transmission.py homeassistant/components/switch/transmission.py
homeassistant/components/switch/wake_on_lan.py homeassistant/components/switch/wake_on_lan.py
homeassistant/components/thermostat/eq3btsmart.py homeassistant/components/thingspeak.py
homeassistant/components/thermostat/heatmiser.py
homeassistant/components/thermostat/homematic.py
homeassistant/components/thermostat/proliphix.py
homeassistant/components/thermostat/radiotherm.py
homeassistant/components/upnp.py homeassistant/components/upnp.py
homeassistant/components/weather/openweathermap.py
homeassistant/components/zeroconf.py homeassistant/components/zeroconf.py

View File

@ -2,11 +2,11 @@ sudo: false
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: "3.4" - python: "3.4.2"
env: TOXENV=py34 env: TOXENV=py34
- python: "3.4" - python: "3.4.2"
env: TOXENV=requirements env: TOXENV=requirements
- python: "3.5" - python: "3.4.2"
env: TOXENV=lint env: TOXENV=lint
- python: "3.5" - python: "3.5"
env: TOXENV=typing env: TOXENV=typing

View File

@ -43,12 +43,10 @@ device_tracker:
username: admin username: admin
password: PASSWORD password: PASSWORD
chromecast:
switch: switch:
platform: wemo platform: wemo
thermostat: climate:
platform: nest platform: nest
# Required: username and password that are used to login to the Nest thermostat. # Required: username and password that are used to login to the Nest thermostat.
username: myemail@mydomain.com username: myemail@mydomain.com
@ -79,7 +77,6 @@ group:
entities: entities:
- group.awesome_people - group.awesome_people
- group.climate - group.climate
kitchen: kitchen:
name: Kitchen name: Kitchen
entities: entities:
@ -92,52 +89,23 @@ group:
- input_boolean.notify_home - input_boolean.notify_home
- camera.demo_camera - 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: browser:
keyboard: keyboard:
# https://home-assistant.io/getting-started/automation/ # https://home-assistant.io/getting-started/automation/
automation: automation:
- alias: 'Rule 1 Light on in the evening' - alias: Turn on light when sun sets
trigger: trigger:
- platform: sun platform: sun
event: sunset event: sunset
offset: "-01:00:00" offset: "-01:00:00"
- platform: state
entity_id: group.all_devices
state: home
condition: condition:
- platform: state condition: state
entity_id: group.all_devices entity_id: group.all_devices
state: home state: 'home'
- platform: time
after: "16:00:00"
before: "23:00:00"
action: action:
service: homeassistant.turn_on service: light.turn_on
entity_id: group.living_room
- 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:" # Another way to do is to collect all entries under one "sensor:"
# sensor: # sensor:
# - platform: mqtt # - platform: mqtt
@ -154,34 +122,30 @@ sensor:
arg: '/' arg: '/'
- type: 'disk_use_percent' - type: 'disk_use_percent'
arg: '/home' arg: '/home'
- type: 'disk_use'
arg: '/home'
sensor 2: sensor 2:
platform: forecast platform: cpuspeed
api_key: <register on Forecast.io for your PRIVATE API>
monitored_conditions:
- summary
- precip_type
- precip_intensity
- temperature
script: script:
# Turns on the bedroom lights and then the living room lights 1 minute later
wakeup: wakeup:
alias: Wake Up alias: Wake Up
sequence: 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 - alias: Bedroom lights on
execute_service: light.turn_on service: light.turn_on
service_data: data:
entity_id: group.bedroom entity_id: group.bedroom
brightness: 100
- delay: - delay:
# supports seconds, milliseconds, minutes, hours, etc.
minutes: 1 minutes: 1
- alias: Living room lights on - alias: Living room lights on
execute_service: light.turn_on service: light.turn_on
service_data: data:
entity_id: group.living_room entity_id: group.living_room
scene: scene:

View File

@ -14,6 +14,7 @@ from homeassistant.const import (
__version__, __version__,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_START,
REQUIRED_PYTHON_VER, REQUIRED_PYTHON_VER,
REQUIRED_PYTHON_VER_WIN,
RESTART_EXIT_CODE, RESTART_EXIT_CODE,
) )
from homeassistant.util.async import run_callback_threadsafe 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 See https://bugs.python.org/issue26617 for details of the Python
bug. bug.
""" """
# pylint: disable=no-self-use, too-few-public-methods, protected-access # pylint: disable=no-self-use, protected-access, bare-except
# pylint: disable=bare-except
import asyncio.tasks import asyncio.tasks
class IgnoreCalls: class IgnoreCalls:
@ -64,7 +64,12 @@ def monkey_patch_asyncio():
def validate_python() -> None: def validate_python() -> None:
"""Validate we're running the right Python version.""" """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( print("Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER)) *REQUIRED_PYTHON_VER))
sys.exit(1) sys.exit(1)

View File

@ -1,11 +1,10 @@
"""Provides methods to bootstrap a home assistant instance.""" """Provides methods to bootstrap a home assistant instance."""
import asyncio
import logging import logging
import logging.handlers import logging.handlers
import os import os
import sys import sys
from collections import defaultdict from collections import defaultdict
from threading import RLock
from types import ModuleType from types import ModuleType
from typing import Any, Optional, Dict from typing import Any, Optional, Dict
@ -19,6 +18,8 @@ import homeassistant.config as conf_util
import homeassistant.core as core import homeassistant.core as core
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.util.package as pkg_util 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.util.yaml import clear_secret_cache
from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -26,24 +27,34 @@ from homeassistant.helpers import (
event_decorators, service, config_per_platform, extract_domain_configs) event_decorators, service, config_per_platform, extract_domain_configs)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_SETUP_LOCK = RLock()
_CURRENT_SETUP = []
ATTR_COMPONENT = 'component' ATTR_COMPONENT = 'component'
ERROR_LOG_FILENAME = 'home-assistant.log' ERROR_LOG_FILENAME = 'home-assistant.log'
_PERSISTENT_PLATFORMS = set() _PERSISTENT_ERRORS = {}
_PERSISTENT_VALIDATION = set() HA_COMPONENT_URL = '[{}](https://home-assistant.io/components/{}/)'
def setup_component(hass: core.HomeAssistant, domain: str, def setup_component(hass: core.HomeAssistant, domain: str,
config: Optional[Dict]=None) -> bool: config: Optional[Dict]=None) -> bool:
"""Setup a component and all its dependencies.""" """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: if domain in hass.config.components:
_LOGGER.debug('Component %s already set up.', domain) _LOGGER.debug('Component %s already set up.', domain)
return True 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: if config is None:
config = defaultdict(dict) 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 # OrderedSet is empty if component or dependencies could not be resolved
if not components: if not components:
_async_persistent_notification(hass, domain, True)
return False return False
for component in components: 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) _LOGGER.error('Component %s failed to setup', component)
_async_persistent_notification(hass, component, True)
return False return False
return True return True
@ -64,7 +78,10 @@ def setup_component(hass: core.HomeAssistant, domain: str,
def _handle_requirements(hass: core.HomeAssistant, component, def _handle_requirements(hass: core.HomeAssistant, component,
name: str) -> bool: 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'): if hass.config.skip_pip or not hasattr(component, 'REQUIREMENTS'):
return True 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')): if not pkg_util.install_package(req, target=hass.config.path('deps')):
_LOGGER.error('Not initializing %s because could not install ' _LOGGER.error('Not initializing %s because could not install '
'dependency %s', name, req) 'dependency %s', name, req)
_async_persistent_notification(hass, name)
return False return False
return True return True
def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool: @asyncio.coroutine
"""Setup a component for Home Assistant.""" def _async_setup_component(hass: core.HomeAssistant,
# pylint: disable=too-many-return-statements,too-many-branches domain: str, config) -> bool:
# pylint: disable=too-many-statements """Setup a component for Home Assistant.
This method is a coroutine.
"""
# pylint: disable=too-many-return-statements
if domain in hass.config.components: if domain in hass.config.components:
return True return True
with _SETUP_LOCK: setup_lock = hass.data.get('setup_lock')
# It might have been loaded while waiting for lock if setup_lock is None:
if domain in hass.config.components: setup_lock = hass.data['setup_lock'] = asyncio.Lock(loop=hass.loop)
return True
if domain in _CURRENT_SETUP: setup_progress = hass.data.get('setup_progress')
if setup_progress is None:
setup_progress = hass.data['setup_progress'] = []
if domain in setup_progress:
_LOGGER.error('Attempt made to setup %s during setup of %s', _LOGGER.error('Attempt made to setup %s during setup of %s',
domain, domain) domain, domain)
_async_persistent_notification(hass, domain, True)
return False return False
config = prepare_setup_component(hass, config, domain) 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: if config is None:
return False return False
component = loader.get_component(domain) 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: try:
result = component.setup(hass, config) 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: if result is False:
_LOGGER.error('component %s failed to initialize', domain) _LOGGER.error('component %s failed to initialize', domain)
_async_persistent_notification(hass, domain, True)
return False return False
elif result is not True: elif result is not True:
_LOGGER.error('component %s did not return boolean if setup ' _LOGGER.error('component %s did not return boolean if setup '
'was successful. Disabling component.', domain) 'was successful. Disabling component.', domain)
_async_persistent_notification(hass, domain, True)
loader.set_component(domain, None) loader.set_component(domain, None)
return False return False
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error during setup of component %s', domain)
return False
finally:
_CURRENT_SETUP.remove(domain)
hass.config.components.append(component.DOMAIN) hass.config.components.append(component.DOMAIN)
# Assumption: if a component does not depend on groups # Assumption: if a component does not depend on groups
# it communicates with devices # it communicates with devices
if 'group' not in getattr(component, 'DEPENDENCIES', []) and \ if (not async_comp and
hass.pool.worker_count <= 10: '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.pool.add_worker()
hass.bus.fire( hass.bus.async_fire(
EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}
) )
return True return True
finally:
setup_progress.remove(domain)
if did_lock:
setup_lock.release()
def prepare_setup_component(hass: core.HomeAssistant, config: dict, def prepare_setup_component(hass: core.HomeAssistant, config: dict,
domain: str): domain: str):
"""Prepare setup of a component and return processed config.""" """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 # pylint: disable=too-many-return-statements
component = loader.get_component(domain) component = loader.get_component(domain)
missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', []) missing_deps = [dep for dep in getattr(component, 'DEPENDENCIES', [])
@ -151,7 +215,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
try: try:
config = component.CONFIG_SCHEMA(config) config = component.CONFIG_SCHEMA(config)
except vol.Invalid as ex: except vol.Invalid as ex:
log_exception(ex, domain, config, hass) async_log_exception(ex, domain, config, hass)
return None return None
elif hasattr(component, 'PLATFORM_SCHEMA'): elif hasattr(component, 'PLATFORM_SCHEMA'):
@ -161,7 +225,7 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
try: try:
p_validated = component.PLATFORM_SCHEMA(p_config) p_validated = component.PLATFORM_SCHEMA(p_config)
except vol.Invalid as ex: except vol.Invalid as ex:
log_exception(ex, domain, config, hass) async_log_exception(ex, domain, config, hass)
continue continue
# Not all platform components follow same pattern for platforms # 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) platforms.append(p_validated)
continue continue
platform = prepare_setup_platform(hass, config, domain, platform = yield from async_prepare_setup_platform(
p_name) hass, config, domain, p_name)
if platform is None: if platform is None:
continue continue
@ -180,9 +244,10 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
# Validate platform specific schema # Validate platform specific schema
if hasattr(platform, 'PLATFORM_SCHEMA'): if hasattr(platform, 'PLATFORM_SCHEMA'):
try: try:
# pylint: disable=no-member
p_validated = platform.PLATFORM_SCHEMA(p_validated) p_validated = platform.PLATFORM_SCHEMA(p_validated)
except vol.Invalid as ex: except vol.Invalid as ex:
log_exception(ex, '{}.{}'.format(domain, p_name), async_log_exception(ex, '{}.{}'.format(domain, p_name),
p_validated, hass) p_validated, hass)
continue continue
@ -195,7 +260,9 @@ def prepare_setup_component(hass: core.HomeAssistant, config: dict,
if key not in filter_keys} if key not in filter_keys}
config[domain] = platforms 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 None
return config 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, def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
platform_name: str) -> Optional[ModuleType]: platform_name: str) -> Optional[ModuleType]:
"""Load a platform and makes sure dependencies are setup.""" """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) platform_path = PLATFORM_FORMAT.format(domain, platform_name)
@ -213,13 +295,7 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
# Not found # Not found
if platform is None: if platform is None:
_LOGGER.error('Unable to find platform %s', platform_path) _LOGGER.error('Unable to find platform %s', platform_path)
_async_persistent_notification(hass, 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')
return None return None
# Already loaded # Already loaded
@ -228,20 +304,23 @@ def prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
# Load dependencies # Load dependencies
for component in getattr(platform, '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( _LOGGER.error(
'Unable to prepare setup for platform %s because ' 'Unable to prepare setup for platform %s because '
'dependency %s could not be initialized', platform_path, 'dependency %s could not be initialized', platform_path,
component) component)
_async_persistent_notification(hass, platform_path, True)
return None 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 None
return platform return platform
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config: Dict[str, Any], def from_config_dict(config: Dict[str, Any],
hass: Optional[core.HomeAssistant]=None, hass: Optional[core.HomeAssistant]=None,
config_dir: Optional[str]=None, config_dir: Optional[str]=None,
@ -261,15 +340,49 @@ def from_config_dict(config: Dict[str, Any],
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
mount_local_lib_path(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, {}) core_config = config.get(core.DOMAIN, {})
try: 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: except vol.Invalid as ex:
log_exception(ex, 'homeassistant', core_config, hass) async_log_exception(ex, 'homeassistant', core_config, hass)
return None 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: if enable_log:
enable_logging(hass, verbose, log_rotate_days) 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. ' _LOGGER.warning('Skipping pip installation of required modules. '
'This may cause issues.') '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. # Make a copy because we are mutating it.
# Convert it to defaultdict so components can always have config dict # Convert it to defaultdict so components can always have config dict
@ -291,15 +405,15 @@ def from_config_dict(config: Dict[str, Any],
components = set(key.split(' ')[0] for key in config.keys() components = set(key.split(' ')[0] for key in config.keys()
if key != core.DOMAIN) if key != core.DOMAIN)
# Setup in a thread to avoid blocking # setup components
def component_setup(): # pylint: disable=not-an-iterable
"""Set up a component.""" res = yield from core_components.async_setup(hass, config)
if not core_components.setup(hass, config): if not res:
_LOGGER.error('Home Assistant core failed to initialize. ' _LOGGER.error('Home Assistant core failed to initialize. '
'Further initialization aborted.') 'Further initialization aborted.')
return hass 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')
@ -309,11 +423,7 @@ def from_config_dict(config: Dict[str, Any],
# Setup the components # Setup the components
for domain in loader.load_order_components(components): for domain in loader.load_order_components(components):
_setup_component(hass, domain, config) yield from _async_setup_component(hass, domain, config)
hass.loop.run_until_complete(
hass.loop.run_in_executor(None, component_setup)
)
return hass return hass
@ -331,27 +441,62 @@ def from_config_file(config_path: str,
if hass is None: if hass is None:
hass = core.HomeAssistant() 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 # Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path)) config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir 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) enable_logging(hass, verbose, log_rotate_days)
try: 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: except HomeAssistantError:
return None return None
finally: finally:
clear_secret_cache() clear_secret_cache()
return from_config_dict(config_dict, hass, enable_log=False, hass = yield from async_from_config_dict(
skip_pip=skip_pip) config_dict, hass, enable_log=False, skip_pip=skip_pip)
return hass
def enable_logging(hass: core.HomeAssistant, verbose: bool=False, def enable_logging(hass: core.HomeAssistant, verbose: bool=False,
log_rotate_days=None) -> None: log_rotate_days=None) -> None:
"""Setup the logging.""" """Setup the logging.
Async friendly.
"""
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) " fmt = ("%(log_color)s%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s%(reset)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 # suppress overly verbose logs from libraries that aren't helpful
logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
try: try:
from colorlog import ColoredFormatter 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) 'Unable to setup error log %s (access denied)', err_log_path)
def _ensure_loader_prepared(hass: core.HomeAssistant) -> None: def log_exception(ex, domain, config, hass):
"""Ensure Home Assistant loader is prepared."""
if not loader.PREPARED:
loader.prepare(hass)
def log_exception(ex, domain, config, hass=None):
"""Generate log exception for config validation.""" """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) message = 'Invalid config for [{}]: '.format(domain)
if hass is not None: if hass is not None:
_PERSISTENT_VALIDATION.add(domain) _async_persistent_notification(hass, domain, True)
message = ('The following platforms contain invalid configuration: ' +
', '.join(list(_PERSISTENT_VALIDATION)) +
' (please check your configuration)')
persistent_notification.create(
hass, message, 'Invalid config', 'invalid_config')
if 'extra keys not allowed' in ex.error_message: if 'extra keys not allowed' in ex.error_message:
message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\ message += '[{}] is an invalid option for [{}]. Check: {}->{}.'\
.format(ex.path[-1], domain, domain, .format(ex.path[-1], domain, domain,
'->'.join('%s' % m for m in ex.path)) '->'.join(str(m) for m in ex.path))
else: else:
message += '{}.'.format(humanize_error(config, ex)) message += '{}.'.format(humanize_error(config, ex))
if hasattr(config, '__line__'): domain_config = config.get(domain, config)
message += " (See {}:{})".format( message += " (See {}:{}). ".format(
config.__config_file__, config.__line__ or '?') getattr(domain_config, '__config_file__', '?'),
getattr(domain_config, '__line__', '?'))
if domain != 'homeassistant': if domain != 'homeassistant':
message += (' Please check the docs at ' message += ('Please check the docs at '
'https://home-assistant.io/components/{}/'.format(domain)) 'https://home-assistant.io/components/{}/'.format(domain))
_LOGGER.error(message) _LOGGER.error(message)
def mount_local_lib_path(config_dir: str) -> str: 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') deps_dir = os.path.join(config_dir, 'deps')
if deps_dir not in sys.path: if deps_dir not in sys.path:
sys.path.insert(0, os.path.join(config_dir, 'deps')) sys.path.insert(0, os.path.join(config_dir, 'deps'))

View File

@ -7,6 +7,7 @@ Component design guidelines:
format "<DOMAIN>.<OBJECT_ID>". format "<DOMAIN>.<OBJECT_ID>".
- Each component should publish services only under its own domain. - Each component should publish services only under its own domain.
""" """
import asyncio
import itertools as it import itertools as it
import logging import logging
@ -79,8 +80,10 @@ def reload_core_config(hass):
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) 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.""" """Setup general services related to Home Assistant."""
@asyncio.coroutine
def handle_turn_service(service): def handle_turn_service(service):
"""Method to handle calls to homeassistant.turn_on/off.""" """Method to handle calls to homeassistant.turn_on/off."""
entity_ids = extract_entity_ids(hass, service) entity_ids = extract_entity_ids(hass, service)
@ -96,6 +99,8 @@ def setup(hass, config):
by_domain = it.groupby(sorted(entity_ids), by_domain = it.groupby(sorted(entity_ids),
lambda item: ha.split_entity_id(item)[0]) lambda item: ha.split_entity_id(item)[0])
tasks = []
for domain, ent_ids in by_domain: for domain, ent_ids in by_domain:
# We want to block for all calls and only return when all calls # 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 # 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. # ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids) 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) yield from asyncio.gather(*tasks, loop=hass.loop)
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service)
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): def handle_reload_config(call):
"""Service handler for reloading core config.""" """Service handler for reloading core config."""
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant import config as conf_util from homeassistant import config as conf_util
try: try:
path = conf_util.find_config_file(hass.config.config_dir) conf = yield from conf_util.async_hass_config_yaml(hass)
conf = conf_util.load_yaml_config_file(path)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error(err) _LOGGER.error(err)
return 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, hass.services.async_register(
handle_reload_config) ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config)
return True return True

View File

@ -42,8 +42,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([AlarmDotCom(hass, name, code, username, password)]) 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): class AlarmDotCom(alarm.AlarmControlPanel):
"""Represent an Alarm.com status.""" """Represent an Alarm.com status."""

View File

@ -4,23 +4,19 @@ Support for Concord232 alarm control panels.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.concord232/ https://home-assistant.io/components/alarm_control_panel.concord232/
""" """
import datetime import datetime
import logging import logging
import requests
import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
STATE_ALARM_DISARMED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import requests
import voluptuous as vol
REQUIREMENTS = ['concord232==0.14'] REQUIREMENTS = ['concord232==0.14']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,17 +25,17 @@ DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'CONCORD232' DEFAULT_NAME = 'CONCORD232'
DEFAULT_PORT = 5007 DEFAULT_PORT = 5007
SCAN_INTERVAL = 1
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
}) })
SCAN_INTERVAL = 1
def setup_platform(hass, config, add_devices, discovery_info=None): 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) name = config.get(CONF_NAME)
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
port = config.get(CONF_PORT) port = config.get(CONF_PORT)
@ -49,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try: try:
add_devices([Concord232Alarm(hass, url, name)]) add_devices([Concord232Alarm(hass, url, name)])
except requests.exceptions.ConnectionError as ex: 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 return False
@ -57,7 +53,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
"""Represents the Concord232-based alarm panel.""" """Represents the Concord232-based alarm panel."""
def __init__(self, hass, url, name): def __init__(self, hass, url, name):
"""Initalize the concord232 alarm panel.""" """Initialize the Concord232 alarm panel."""
from concord232 import client as concord232_client from concord232 import client as concord232_client
self._state = STATE_UNKNOWN self._state = STATE_UNKNOWN
@ -68,7 +64,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
try: try:
client = concord232_client.Client(self._url) client = concord232_client.Client(self._url)
except requests.exceptions.ConnectionError as ex: 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 = client
self._alarm.partitions = self._alarm.list_partitions() self._alarm.partitions = self._alarm.list_partitions()
@ -100,16 +96,16 @@ class Concord232Alarm(alarm.AlarmControlPanel):
try: try:
part = self._alarm.list_partitions()[0] part = self._alarm.list_partitions()[0]
except requests.exceptions.ConnectionError as ex: 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)) dict(host=self._url, reason=ex))
newstate = STATE_UNKNOWN newstate = STATE_UNKNOWN
except IndexError: except IndexError:
_LOGGER.error('concord232 reports no partitions') _LOGGER.error("Concord232 reports no partitions")
newstate = STATE_UNKNOWN newstate = STATE_UNKNOWN
if part['arming_level'] == "Off": if part['arming_level'] == 'Off':
newstate = STATE_ALARM_DISARMED newstate = STATE_ALARM_DISARMED
elif "Home" in part['arming_level']: elif 'Home' in part['arming_level']:
newstate = STATE_ALARM_ARMED_HOME newstate = STATE_ALARM_ARMED_HOME
else: else:
newstate = STATE_ALARM_ARMED_AWAY newstate = STATE_ALARM_ARMED_AWAY

View File

@ -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/ https://home-assistant.io/components/alarm_control_panel.envisalink/
""" """
import logging import logging
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.envisalink import (EVL_CONTROLLER, from homeassistant.components.envisalink import (
EnvisalinkDevice, EVL_CONTROLLER, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
PARTITION_SCHEMA, CONF_PARTITIONNAME, SIGNAL_PARTITION_UPDATE, SIGNAL_KEYPAD_UPDATE)
CONF_CODE,
CONF_PANIC,
CONF_PARTITIONNAME,
SIGNAL_PARTITION_UPDATE,
SIGNAL_KEYPAD_UPDATE)
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN, STATE_ALARM_TRIGGERED) STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
DEPENDENCIES = ['envisalink']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['envisalink']
# pylint: disable=unused-argument # 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.""" """Perform the setup for Envisalink alarm panels."""
_configured_partitions = discovery_info['partitions'] _configured_partitions = discovery_info['partitions']
_code = discovery_info[CONF_CODE] _code = discovery_info[CONF_CODE]
@ -38,29 +35,28 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
_panic_type, _panic_type,
EVL_CONTROLLER.alarm_state['partition'][part_num], EVL_CONTROLLER.alarm_state['partition'][part_num],
EVL_CONTROLLER) EVL_CONTROLLER)
add_devices_callback([_device]) add_devices([_device])
return True return True
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): 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,
def __init__(self, partition_number, alarm_name, controller):
code, panic_type, info, controller):
"""Initialize the alarm panel.""" """Initialize the alarm panel."""
from pydispatch import dispatcher from pydispatch import dispatcher
self._partition_number = partition_number self._partition_number = partition_number
self._code = code self._code = code
self._panic_type = panic_type 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) EnvisalinkDevice.__init__(self, alarm_name, info, controller)
dispatcher.connect(self._update_callback, dispatcher.connect(
signal=SIGNAL_PARTITION_UPDATE, self._update_callback, signal=SIGNAL_PARTITION_UPDATE,
sender=dispatcher.Any) sender=dispatcher.Any)
dispatcher.connect(self._update_callback, dispatcher.connect(
signal=SIGNAL_KEYPAD_UPDATE, self._update_callback, signal=SIGNAL_KEYPAD_UPDATE,
sender=dispatcher.Any) sender=dispatcher.Any)
def _update_callback(self, partition): def _update_callback(self, partition):
@ -90,20 +86,20 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""
if self._code: if self._code:
EVL_CONTROLLER.disarm_partition(str(code), EVL_CONTROLLER.disarm_partition(
self._partition_number) str(code), self._partition_number)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
if self._code: if self._code:
EVL_CONTROLLER.arm_stay_partition(str(code), EVL_CONTROLLER.arm_stay_partition(
self._partition_number) str(code), self._partition_number)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
if self._code: if self._code:
EVL_CONTROLLER.arm_away_partition(str(code), EVL_CONTROLLER.arm_away_partition(
self._partition_number) str(code), self._partition_number)
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Alarm trigger command. Will be used to trigger a panic alarm.""" """Alarm trigger command. Will be used to trigger a panic alarm."""

View File

@ -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): class ManualAlarm(alarm.AlarmControlPanel):
""" """
Represents an alarm status. Represents an alarm status.

View File

@ -55,8 +55,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get(CONF_CODE))]) config.get(CONF_CODE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes
# pylint: disable=abstract-method
class MqttAlarm(alarm.AlarmControlPanel): class MqttAlarm(alarm.AlarmControlPanel):
"""Representation of a MQTT alarm status.""" """Representation of a MQTT alarm status."""

View File

@ -41,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([SimpliSafeAlarm(name, username, password, code)]) add_devices([SimpliSafeAlarm(name, username, password, code)])
# pylint: disable=abstract-method
class SimpliSafeAlarm(alarm.AlarmControlPanel): class SimpliSafeAlarm(alarm.AlarmControlPanel):
"""Representation a SimpliSafe alarm.""" """Representation a SimpliSafe alarm."""

View File

@ -28,7 +28,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(alarms) add_devices(alarms)
# pylint: disable=abstract-method
class VerisureAlarm(alarm.AlarmControlPanel): class VerisureAlarm(alarm.AlarmControlPanel):
"""Represent a Verisure alarm status.""" """Represent a Verisure alarm status."""

View File

@ -4,6 +4,7 @@ Support for Alexa skill service end point.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alexa/ https://home-assistant.io/components/alexa/
""" """
import asyncio
import copy import copy
import enum import enum
import logging import logging
@ -12,6 +13,7 @@ from datetime import datetime
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import HTTP_BAD_REQUEST from homeassistant.const import HTTP_BAD_REQUEST
from homeassistant.helpers import template, script, config_validation as cv from homeassistant.helpers import template, script, config_validation as cv
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -20,7 +22,7 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
INTENTS_API_ENDPOINT = '/api/alexa' INTENTS_API_ENDPOINT = '/api/alexa'
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/<briefing_id>' FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_CARD = 'card' CONF_CARD = 'card'
@ -102,8 +104,8 @@ def setup(hass, config):
intents = config[DOMAIN].get(CONF_INTENTS, {}) intents = config[DOMAIN].get(CONF_INTENTS, {})
flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {}) flash_briefings = config[DOMAIN].get(CONF_FLASH_BRIEFINGS, {})
hass.wsgi.register_view(AlexaIntentsView(hass, intents)) hass.http.register_view(AlexaIntentsView(hass, intents))
hass.wsgi.register_view(AlexaFlashBriefingView(hass, flash_briefings)) hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefings))
return True return True
@ -128,9 +130,10 @@ class AlexaIntentsView(HomeAssistantView):
self.intents = intents self.intents = intents
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle Alexa.""" """Handle Alexa."""
data = request.json data = yield from request.json()
_LOGGER.debug('Received Alexa request: %s', data) _LOGGER.debug('Received Alexa request: %s', data)
@ -176,7 +179,7 @@ class AlexaIntentsView(HomeAssistantView):
action = config.get(CONF_ACTION) action = config.get(CONF_ACTION)
if action is not None: if action is not None:
action.run(response.variables) yield from action.async_run(response.variables)
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
if speech is not None: if speech is not None:
@ -218,8 +221,8 @@ class AlexaResponse(object):
self.card = card self.card = card
return return
card["title"] = title.render(self.variables) card["title"] = title.async_render(self.variables)
card["content"] = content.render(self.variables) card["content"] = content.async_render(self.variables)
self.card = card self.card = card
def add_speech(self, speech_type, text): def add_speech(self, speech_type, text):
@ -229,7 +232,7 @@ class AlexaResponse(object):
key = 'ssml' if speech_type == SpeechType.ssml else 'text' key = 'ssml' if speech_type == SpeechType.ssml else 'text'
if isinstance(text, template.Template): if isinstance(text, template.Template):
text = text.render(self.variables) text = text.async_render(self.variables)
self.speech = { self.speech = {
'type': speech_type.value, 'type': speech_type.value,
@ -244,7 +247,7 @@ class AlexaResponse(object):
self.reprompt = { self.reprompt = {
'type': speech_type.value, 'type': speech_type.value,
key: text.render(self.variables) key: text.async_render(self.variables)
} }
def as_dict(self): def as_dict(self):
@ -283,7 +286,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
self.flash_briefings = copy.deepcopy(flash_briefings) self.flash_briefings = copy.deepcopy(flash_briefings)
template.attach(hass, self.flash_briefings) template.attach(hass, self.flash_briefings)
# pylint: disable=too-many-branches @callback
def get(self, request, briefing_id): def get(self, request, briefing_id):
"""Handle Alexa Flash Briefing request.""" """Handle Alexa Flash Briefing request."""
_LOGGER.debug('Received Alexa flash briefing request for: %s', _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: if self.flash_briefings.get(briefing_id) is None:
err = 'No configured Alexa flash briefing was found for: %s' err = 'No configured Alexa flash briefing was found for: %s'
_LOGGER.error(err, briefing_id) _LOGGER.error(err, briefing_id)
return self.Response(status=404) return b'', 404
briefing = [] briefing = []
@ -300,13 +303,13 @@ class AlexaFlashBriefingView(HomeAssistantView):
output = {} output = {}
if item.get(CONF_TITLE) is not None: if item.get(CONF_TITLE) is not None:
if isinstance(item.get(CONF_TITLE), template.Template): 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: else:
output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE) output[ATTR_TITLE_TEXT] = item.get(CONF_TITLE)
if item.get(CONF_TEXT) is not None: if item.get(CONF_TEXT) is not None:
if isinstance(item.get(CONF_TEXT), template.Template): 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: else:
output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT) output[ATTR_MAIN_TEXT] = item.get(CONF_TEXT)
@ -315,7 +318,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
if item.get(CONF_AUDIO) is not None: if item.get(CONF_AUDIO) is not None:
if isinstance(item.get(CONF_AUDIO), template.Template): 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: else:
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO) output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
@ -323,7 +326,7 @@ class AlexaFlashBriefingView(HomeAssistantView):
if isinstance(item.get(CONF_DISPLAY_URL), if isinstance(item.get(CONF_DISPLAY_URL),
template.Template): template.Template):
output[ATTR_REDIRECTION_URL] = \ output[ATTR_REDIRECTION_URL] = \
item[CONF_DISPLAY_URL].render() item[CONF_DISPLAY_URL].async_render()
else: else:
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)

View File

@ -7,7 +7,9 @@ https://home-assistant.io/developers/api/
import asyncio import asyncio
import json import json
import logging import logging
import queue
from aiohttp import web
import async_timeout
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.remote as rem 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, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE,
__version__) __version__)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.state import TrackStates from homeassistant.helpers.state import AsyncTrackStates
from homeassistant.helpers import template from homeassistant.helpers import template
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -36,20 +38,20 @@ _LOGGER = logging.getLogger(__name__)
def setup(hass, config): def setup(hass, config):
"""Register the API with the HTTP interface.""" """Register the API with the HTTP interface."""
hass.wsgi.register_view(APIStatusView) hass.http.register_view(APIStatusView)
hass.wsgi.register_view(APIEventStream) hass.http.register_view(APIEventStream)
hass.wsgi.register_view(APIConfigView) hass.http.register_view(APIConfigView)
hass.wsgi.register_view(APIDiscoveryView) hass.http.register_view(APIDiscoveryView)
hass.wsgi.register_view(APIStatesView) hass.http.register_view(APIStatesView)
hass.wsgi.register_view(APIEntityStateView) hass.http.register_view(APIEntityStateView)
hass.wsgi.register_view(APIEventListenersView) hass.http.register_view(APIEventListenersView)
hass.wsgi.register_view(APIEventView) hass.http.register_view(APIEventView)
hass.wsgi.register_view(APIServicesView) hass.http.register_view(APIServicesView)
hass.wsgi.register_view(APIDomainServicesView) hass.http.register_view(APIDomainServicesView)
hass.wsgi.register_view(APIEventForwardingView) hass.http.register_view(APIEventForwardingView)
hass.wsgi.register_view(APIComponentsView) hass.http.register_view(APIComponentsView)
hass.wsgi.register_view(APIErrorLogView) hass.http.register_view(APIErrorLogView)
hass.wsgi.register_view(APITemplateView) hass.http.register_view(APITemplateView)
return True return True
@ -60,6 +62,7 @@ class APIStatusView(HomeAssistantView):
url = URL_API url = URL_API
name = "api:status" name = "api:status"
@ha.callback
def get(self, request): def get(self, request):
"""Retrieve if API is running.""" """Retrieve if API is running."""
return self.json_message('API running.') return self.json_message('API running.')
@ -71,12 +74,13 @@ class APIEventStream(HomeAssistantView):
url = URL_API_STREAM url = URL_API_STREAM
name = "api:stream" name = "api:stream"
@asyncio.coroutine
def get(self, request): def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
stop_obj = object() 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: if restrict:
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
@ -96,21 +100,25 @@ class APIEventStream(HomeAssistantView):
else: else:
data = json.dumps(event, cls=rem.JSONEncoder) data = json.dumps(event, cls=rem.JSONEncoder)
to_write.put(data) yield from to_write.put(data)
def stream(): response = web.StreamResponse()
"""Stream events to response.""" response.content_type = 'text/event-stream'
unsub_stream = self.hass.bus.listen(MATCH_ALL, forward_events) yield from response.prepare(request)
unsub_stream = self.hass.bus.async_listen(MATCH_ALL, forward_events)
try: try:
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
# Fire off one message so browsers fire open event right away # Fire off one message so browsers fire open event right away
to_write.put(STREAM_PING_PAYLOAD) yield from to_write.put(STREAM_PING_PAYLOAD)
while True: while True:
try: try:
payload = to_write.get(timeout=STREAM_PING_INTERVAL) with async_timeout.timeout(STREAM_PING_INTERVAL,
loop=self.hass.loop):
payload = yield from to_write.get()
if payload is stop_obj: if payload is stop_obj:
break break
@ -118,17 +126,15 @@ class APIEventStream(HomeAssistantView):
msg = "data: {}\n\n".format(payload) msg = "data: {}\n\n".format(payload)
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip()) msg.strip())
yield msg.encode("UTF-8") response.write(msg.encode("UTF-8"))
except queue.Empty: yield from response.drain()
to_write.put(STREAM_PING_PAYLOAD) except asyncio.TimeoutError:
except GeneratorExit: yield from to_write.put(STREAM_PING_PAYLOAD)
break
finally: finally:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
unsub_stream() unsub_stream()
return self.Response(stream(), mimetype='text/event-stream')
class APIConfigView(HomeAssistantView): class APIConfigView(HomeAssistantView):
"""View to handle Config requests.""" """View to handle Config requests."""
@ -136,6 +142,7 @@ class APIConfigView(HomeAssistantView):
url = URL_API_CONFIG url = URL_API_CONFIG
name = "api:config" name = "api:config"
@ha.callback
def get(self, request): def get(self, request):
"""Get current configuration.""" """Get current configuration."""
return self.json(self.hass.config.as_dict()) return self.json(self.hass.config.as_dict())
@ -148,6 +155,7 @@ class APIDiscoveryView(HomeAssistantView):
url = URL_API_DISCOVERY_INFO url = URL_API_DISCOVERY_INFO
name = "api:discovery" name = "api:discovery"
@ha.callback
def get(self, request): def get(self, request):
"""Get discovery info.""" """Get discovery info."""
needs_auth = self.hass.config.api.api_password is not None needs_auth = self.hass.config.api.api_password is not None
@ -165,17 +173,19 @@ class APIStatesView(HomeAssistantView):
url = URL_API_STATES url = URL_API_STATES
name = "api:states" name = "api:states"
@ha.callback
def get(self, request): def get(self, request):
"""Get current states.""" """Get current states."""
return self.json(self.hass.states.all()) return self.json(self.hass.states.async_all())
class APIEntityStateView(HomeAssistantView): class APIEntityStateView(HomeAssistantView):
"""View to handle EntityState requests.""" """View to handle EntityState requests."""
url = "/api/states/<entity(exist=False):entity_id>" url = "/api/states/{entity_id}"
name = "api:entity-state" name = "api:entity-state"
@ha.callback
def get(self, request, entity_id): def get(self, request, entity_id):
"""Retrieve state of entity.""" """Retrieve state of entity."""
state = self.hass.states.get(entity_id) state = self.hass.states.get(entity_id)
@ -184,34 +194,41 @@ class APIEntityStateView(HomeAssistantView):
else: else:
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
@asyncio.coroutine
def post(self, request, entity_id): def post(self, request, entity_id):
"""Update state of entity.""" """Update state of entity."""
try: try:
new_state = request.json['state'] data = yield from request.json()
except KeyError: 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) return self.json_message('No state specified', HTTP_BAD_REQUEST)
attributes = request.json.get('attributes') attributes = data.get('attributes')
force_update = request.json.get('force_update', False) force_update = data.get('force_update', False)
is_new_state = self.hass.states.get(entity_id) is None is_new_state = self.hass.states.get(entity_id) is None
# Write state # 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 # Read the state back for our response
resp = self.json(self.hass.states.get(entity_id)) status_code = HTTP_CREATED if is_new_state else 200
resp = self.json(self.hass.states.get(entity_id), status_code)
if is_new_state:
resp.status_code = HTTP_CREATED
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id)) resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
return resp return resp
@ha.callback
def delete(self, request, entity_id): def delete(self, request, entity_id):
"""Remove entity.""" """Remove entity."""
if self.hass.states.remove(entity_id): if self.hass.states.async_remove(entity_id):
return self.json_message('Entity removed') return self.json_message('Entity removed')
else: else:
return self.json_message('Entity not found', HTTP_NOT_FOUND) return self.json_message('Entity not found', HTTP_NOT_FOUND)
@ -223,20 +240,23 @@ class APIEventListenersView(HomeAssistantView):
url = URL_API_EVENTS url = URL_API_EVENTS
name = "api:event-listeners" name = "api:event-listeners"
@ha.callback
def get(self, request): def get(self, request):
"""Get event listeners.""" """Get event listeners."""
return self.json(events_json(self.hass)) return self.json(async_events_json(self.hass))
class APIEventView(HomeAssistantView): class APIEventView(HomeAssistantView):
"""View to handle Event requests.""" """View to handle Event requests."""
url = '/api/events/<event_type>' url = '/api/events/{event_type}'
name = "api:event" name = "api:event"
@asyncio.coroutine
def post(self, request, event_type): def post(self, request, event_type):
"""Fire events.""" """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): if event_data is not None and not isinstance(event_data, dict):
return self.json_message('Event data should be a JSON object', return self.json_message('Event data should be a JSON object',
@ -251,7 +271,7 @@ class APIEventView(HomeAssistantView):
if state: if state:
event_data[key] = 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)) return self.json_message("Event {} fired.".format(event_type))
@ -262,24 +282,30 @@ class APIServicesView(HomeAssistantView):
url = URL_API_SERVICES url = URL_API_SERVICES
name = "api:services" name = "api:services"
@ha.callback
def get(self, request): def get(self, request):
"""Get registered services.""" """Get registered services."""
return self.json(services_json(self.hass)) return self.json(async_services_json(self.hass))
class APIDomainServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView):
"""View to handle DomainServices requests.""" """View to handle DomainServices requests."""
url = "/api/services/<domain>/<service>" url = "/api/services/{domain}/{service}"
name = "api:domain-services" name = "api:domain-services"
@asyncio.coroutine
def post(self, request, domain, service): def post(self, request, domain, service):
"""Call a service. """Call a service.
Returns a list of changed states. Returns a list of changed states.
""" """
with TrackStates(self.hass) as changed_states: body = yield from request.text()
self.hass.services.call(domain, service, request.json, True) 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) return self.json(changed_states)
@ -291,11 +317,14 @@ class APIEventForwardingView(HomeAssistantView):
name = "api:event-forward" name = "api:event-forward"
event_forwarder = None event_forwarder = None
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Setup an event forwarder.""" """Setup an event forwarder."""
data = request.json try:
if data is None: data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST) return self.json_message("No data received.", HTTP_BAD_REQUEST)
try: try:
host = data['host'] host = data['host']
api_password = data['api_password'] api_password = data['api_password']
@ -311,21 +340,25 @@ class APIEventForwardingView(HomeAssistantView):
api = rem.API(host, api_password, port) 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.", return self.json_message("Unable to validate API.",
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
if self.event_forwarder is None: if self.event_forwarder is None:
self.event_forwarder = rem.EventForwarder(self.hass) 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.") return self.json_message("Event forwarding setup.")
@asyncio.coroutine
def delete(self, request): def delete(self, request):
"""Remove event forwarer.""" """Remove event forwarder."""
data = request.json try:
if data is None: data = yield from request.json()
except ValueError:
return self.json_message("No data received.", HTTP_BAD_REQUEST) return self.json_message("No data received.", HTTP_BAD_REQUEST)
try: try:
@ -342,7 +375,7 @@ class APIEventForwardingView(HomeAssistantView):
if self.event_forwarder is not None: if self.event_forwarder is not None:
api = rem.API(host, None, port) api = rem.API(host, None, port)
self.event_forwarder.disconnect(api) self.event_forwarder.async_disconnect(api)
return self.json_message("Event forwarding cancelled.") return self.json_message("Event forwarding cancelled.")
@ -353,6 +386,7 @@ class APIComponentsView(HomeAssistantView):
url = URL_API_COMPONENTS url = URL_API_COMPONENTS
name = "api:components" name = "api:components"
@ha.callback
def get(self, request): def get(self, request):
"""Get current loaded components.""" """Get current loaded components."""
return self.json(self.hass.config.components) return self.json(self.hass.config.components)
@ -364,9 +398,12 @@ class APIErrorLogView(HomeAssistantView):
url = URL_API_ERROR_LOG url = URL_API_ERROR_LOG
name = "api:error-log" name = "api:error-log"
@asyncio.coroutine
def get(self, request): def get(self, request):
"""Serve error log.""" """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): class APITemplateView(HomeAssistantView):
@ -375,23 +412,25 @@ class APITemplateView(HomeAssistantView):
url = URL_API_TEMPLATE url = URL_API_TEMPLATE
name = "api:template" name = "api:template"
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Render a template.""" """Render a template."""
try: try:
tpl = template.Template(request.json['template'], self.hass) data = yield from request.json()
return tpl.render(request.json.get('variables')) tpl = template.Template(data['template'], self.hass)
except TemplateError as ex: return tpl.async_render(data.get('variables'))
except (ValueError, TemplateError) as ex:
return self.json_message('Error rendering template: {}'.format(ex), return self.json_message('Error rendering template: {}'.format(ex),
HTTP_BAD_REQUEST) HTTP_BAD_REQUEST)
def services_json(hass): def async_services_json(hass):
"""Generate services data to JSONify.""" """Generate services data to JSONify."""
return [{"domain": key, "services": value} 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.""" """Generate event data to JSONify."""
return [{"event": key, "listener_count": value} return [{"event": key, "listener_count": value}
for key, value in hass.bus.listeners.items()] for key, value in hass.bus.async_listeners().items()]

View File

@ -11,8 +11,7 @@ import os
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.bootstrap import async_prepare_setup_platform
from homeassistant.bootstrap import prepare_setup_platform
from homeassistant import config as conf_util from homeassistant import config as conf_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, 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.loader import get_platform
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'automation' DOMAIN = 'automation'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'
@ -144,42 +142,50 @@ def reload(hass):
hass.services.call(DOMAIN, SERVICE_RELOAD) hass.services.call(DOMAIN, SERVICE_RELOAD)
def setup(hass, config): @asyncio.coroutine
def async_setup(hass, config):
"""Setup the automation.""" """Setup the automation."""
component = EntityComponent(_LOGGER, DOMAIN, hass, component = EntityComponent(_LOGGER, DOMAIN, hass,
group_name=GROUP_NAME_ALL_AUTOMATIONS) group_name=GROUP_NAME_ALL_AUTOMATIONS)
success = run_coroutine_threadsafe( success = yield from _async_process_config(hass, config, component)
_async_process_config(hass, config, component), hass.loop).result()
if not success: if not success:
return False return False
descriptions = conf_util.load_yaml_config_file( descriptions = yield from hass.loop.run_in_executor(
os.path.join(os.path.dirname(__file__), 'services.yaml')) 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): def trigger_service_handler(service_call):
"""Handle automation triggers.""" """Handle automation triggers."""
tasks = []
for entity in component.async_extract_from_service(service_call): 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)) 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): def turn_onoff_service_handler(service_call):
"""Handle automation turn on/off service calls.""" """Handle automation turn on/off service calls."""
tasks = []
method = 'async_{}'.format(service_call.service) method = 'async_{}'.format(service_call.service)
for entity in component.async_extract_from_service(service_call): 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): def toggle_service_handler(service_call):
"""Handle automation toggle service calls.""" """Handle automation toggle service calls."""
tasks = []
for entity in component.async_extract_from_service(service_call): for entity in component.async_extract_from_service(service_call):
if entity.is_on: if entity.is_on:
hass.loop.create_task(entity.async_turn_off()) tasks.append(entity.async_turn_off())
else: 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 @asyncio.coroutine
def reload_service_handler(service_call): def reload_service_handler(service_call):
@ -187,24 +193,24 @@ def setup(hass, config):
conf = yield from component.async_prepare_reload() conf = yield from component.async_prepare_reload()
if conf is None: if conf is None:
return 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, hass.services.async_register(
descriptions.get(SERVICE_TRIGGER), DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
schema=TRIGGER_SERVICE_SCHEMA) descriptions.get(SERVICE_TRIGGER), schema=TRIGGER_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler, hass.services.async_register(
descriptions.get(SERVICE_RELOAD), DOMAIN, SERVICE_RELOAD, reload_service_handler,
schema=RELOAD_SERVICE_SCHEMA) descriptions.get(SERVICE_RELOAD), schema=RELOAD_SERVICE_SCHEMA)
hass.services.register(DOMAIN, SERVICE_TOGGLE, toggle_service_handler, hass.services.async_register(
descriptions.get(SERVICE_TOGGLE), DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
schema=SERVICE_SCHEMA) descriptions.get(SERVICE_TOGGLE), schema=SERVICE_SCHEMA)
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
hass.services.register(DOMAIN, service, turn_onoff_service_handler, hass.services.async_register(
descriptions.get(service), DOMAIN, service, turn_onoff_service_handler,
schema=SERVICE_SCHEMA) descriptions.get(service), schema=SERVICE_SCHEMA)
return True return True
@ -212,8 +218,6 @@ def setup(hass, config):
class AutomationEntity(ToggleEntity): class AutomationEntity(ToggleEntity):
"""Entity to show status of entity.""" """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, def __init__(self, name, async_attach_triggers, cond_func, async_action,
hidden): hidden):
"""Initialize an automation entity.""" """Initialize an automation entity."""
@ -260,7 +264,7 @@ class AutomationEntity(ToggleEntity):
return return
yield from self.async_enable() yield from self.async_enable()
self.hass.loop.create_task(self.async_update_ha_state()) yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
def async_turn_off(self, **kwargs) -> None: def async_turn_off(self, **kwargs) -> None:
@ -271,8 +275,6 @@ class AutomationEntity(ToggleEntity):
self._async_detach_triggers() self._async_detach_triggers()
self._async_detach_triggers = None self._async_detach_triggers = None
self._enabled = False 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() yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
@ -284,7 +286,7 @@ class AutomationEntity(ToggleEntity):
if skip_condition or self._cond_func(variables): if skip_condition or self._cond_func(variables):
yield from self._async_action(self.entity_id, variables) yield from self._async_action(self.entity_id, variables)
self._last_triggered = utcnow() self._last_triggered = utcnow()
self.hass.loop.create_task(self.async_update_ha_state()) yield from self.async_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
def async_remove(self): def async_remove(self):
@ -347,7 +349,7 @@ def _async_process_config(hass, config, component):
entities.append(entity) entities.append(entity)
yield from asyncio.gather(*tasks, loop=hass.loop) 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 return len(entities) > 0
@ -362,7 +364,7 @@ def _async_get_action(hass, config, name):
_LOGGER.info('Executing %s', name) _LOGGER.info('Executing %s', name)
logbook.async_log_entry( logbook.async_log_entry(
hass, name, 'has been triggered', DOMAIN, entity_id) 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 return action
@ -395,9 +397,8 @@ def _async_process_trigger(hass, config, trigger_configs, name, action):
removes = [] removes = []
for conf in trigger_configs: for conf in trigger_configs:
platform = yield from hass.loop.run_in_executor( platform = yield from async_prepare_setup_platform(
None, prepare_setup_platform, hass, config, DOMAIN, hass, config, DOMAIN, conf.get(CONF_PLATFORM))
conf.get(CONF_PLATFORM))
if platform is None: if platform is None:
return None return None

View File

@ -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)

View File

@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensor_class, pin)]) sensor_class, pin)])
# pylint: disable=too-many-instance-attributes, too-many-arguments
class ArestBinarySensor(BinarySensorDevice): class ArestBinarySensor(BinarySensorDevice):
"""Implement an aREST binary sensor for a pin.""" """Implement an aREST binary sensor for a pin."""
@ -93,7 +92,6 @@ class ArestBinarySensor(BinarySensorDevice):
self.arest.update() self.arest.update()
# pylint: disable=too-few-public-methods
class ArestData(object): class ArestData(object):
"""Class for handling the data retrieval for pins.""" """Class for handling the data retrieval for pins."""

View File

@ -53,7 +53,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
value_template)]) value_template)])
# pylint: disable=too-many-arguments, too-many-instance-attributes
class CommandBinarySensor(BinarySensorDevice): class CommandBinarySensor(BinarySensorDevice):
"""Represent a command line binary sensor.""" """Represent a command line binary sensor."""

View File

@ -5,20 +5,16 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.concord232/ https://home-assistant.io/components/binary_sensor.concord232/
""" """
import datetime import datetime
import logging import logging
import requests
import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES) BinarySensorDevice, PLATFORM_SCHEMA, SENSOR_CLASSES)
from homeassistant.const import (CONF_HOST, CONF_PORT) from homeassistant.const import (CONF_HOST, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import requests
import voluptuous as vol
REQUIREMENTS = ['concord232==0.14'] REQUIREMENTS = ['concord232==0.14']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,9 +23,12 @@ CONF_EXCLUDE_ZONES = 'exclude_zones'
CONF_ZONE_TYPES = 'zone_types' CONF_ZONE_TYPES = 'zone_types'
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Alarm'
DEFAULT_PORT = '5007' DEFAULT_PORT = '5007'
DEFAULT_SSL = False DEFAULT_SSL = False
SCAN_INTERVAL = 1
ZONE_TYPES_SCHEMA = vol.Schema({ ZONE_TYPES_SCHEMA = vol.Schema({
cv.positive_int: vol.In(SENSOR_CLASSES), 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, 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): 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 from concord232 import client as concord232_client
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
@ -59,24 +53,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors = [] sensors = []
try: try:
_LOGGER.debug('Initializing Client.') _LOGGER.debug("Initializing Client")
client = concord232_client.Client('http://{}:{}' client = concord232_client.Client('http://{}:{}'.format(host, port))
.format(host, port))
client.zones = client.list_zones() client.zones = client.list_zones()
client.last_zone_update = datetime.datetime.now() client.last_zone_update = datetime.datetime.now()
except requests.exceptions.ConnectionError as ex: 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 return False
for zone in client.zones: 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: if zone['number'] not in exclude:
sensors.append(Concord232ZoneSensor( sensors.append(
hass, Concord232ZoneSensor(
client, hass, client, zone, zone_types.get(zone['number'],
zone, get_opening_type(zone)))
zone_types.get(zone['number'], get_opening_type(zone)))) )
add_devices(sensors) add_devices(sensors)
@ -84,16 +77,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def get_opening_type(zone): def get_opening_type(zone):
"""Helper function to try to guess sensor type frm name.""" """Helper function to try to guess sensor type from name."""
if "MOTION" in zone["name"]: if 'MOTION' in zone['name']:
return "motion" return 'motion'
if "KEY" in zone["name"]: if 'KEY' in zone['name']:
return "safety" return 'safety'
if "SMOKE" in zone["name"]: if 'SMOKE' in zone['name']:
return "smoke" return 'smoke'
if "WATER" in zone["name"]: if 'WATER' in zone['name']:
return "water" return 'water'
return "opening" return 'opening'
class Concord232ZoneSensor(BinarySensorDevice): class Concord232ZoneSensor(BinarySensorDevice):

View File

@ -35,7 +35,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
"""Representation of an Envisalink binary sensor.""" """Representation of an Envisalink binary sensor."""
# pylint: disable=too-many-arguments
def __init__(self, zone_number, zone_name, zone_type, info, controller): def __init__(self, zone_number, zone_name, zone_type, info, controller):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
from pydispatch import dispatcher from pydispatch import dispatcher

View File

@ -81,7 +81,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
from haffmpeg import SensorNoise, SensorMotion from haffmpeg import SensorNoise, SensorMotion
# check source # check source
if not run_test(config.get(CONF_INPUT)): if not run_test(hass, config.get(CONF_INPUT)):
return return
# generate sensor object # generate sensor object

View File

@ -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): class MqttBinarySensor(BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT.""" """Representation a binary sensor that is updated by MQTT."""

View File

@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.sensor.nest import NestSensor from homeassistant.components.sensor.nest import NestSensor
from homeassistant.const import (CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS) 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 import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['nest'] DEPENDENCIES = ['nest']
@ -35,10 +35,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup Nest binary sensors.""" """Setup Nest binary sensors."""
nest = hass.data[DATA_NEST]
all_sensors = []
for structure, device in nest.devices(): for structure, device in nest.devices():
add_devices([NestBinarySensor(structure, device, variable) all_sensors.extend(
[NestBinarySensor(structure, device, variable)
for variable in config[CONF_MONITORED_CONDITIONS]]) for variable in config[CONF_MONITORED_CONDITIONS]])
add_devices(all_sensors, True)
class NestBinarySensor(NestSensor, BinarySensorDevice): class NestBinarySensor(NestSensor, BinarySensorDevice):
"""Represents a Nest binary sensor.""" """Represents a Nest binary sensor."""
@ -46,4 +52,8 @@ class NestBinarySensor(NestSensor, BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""True if the binary sensor is on.""" """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))

View File

@ -58,11 +58,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices) add_devices(devices)
# pylint: disable=too-many-instance-attributes
class OctoPrintBinarySensor(BinarySensorDevice): class OctoPrintBinarySensor(BinarySensorDevice):
"""Representation an OctoPrint binary sensor.""" """Representation an OctoPrint binary sensor."""
# pylint: disable=too-many-arguments
def __init__(self, api, condition, sensor_type, sensor_name, unit, def __init__(self, api, condition, sensor_type, sensor_name, unit,
endpoint, group, tool=None): endpoint, group, tool=None):
"""Initialize a new OctoPrint sensor.""" """Initialize a new OctoPrint sensor."""

View File

@ -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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the REST binary sensor.""" """Setup the REST binary sensor."""
name = config.get(CONF_NAME) 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)]) hass, rest, name, sensor_class, value_template)])
# pylint: disable=too-many-arguments
class RestBinarySensor(BinarySensorDevice): class RestBinarySensor(BinarySensorDevice):
"""Representation of a REST binary sensor.""" """Representation of a REST binary sensor."""

View File

@ -54,7 +54,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(binary_sensors) add_devices(binary_sensors)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class RPiGPIOBinarySensor(BinarySensorDevice): class RPiGPIOBinarySensor(BinarySensorDevice):
"""Represent a binary sensor that uses Raspberry Pi GPIO.""" """Represent a binary sensor that uses Raspberry Pi GPIO."""

View File

@ -25,7 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(dev) add_devices(dev)
# pylint: disable=too-many-instance-attributes
class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice): class IsInBedBinarySensor(sleepiq.SleepIQSensor, BinarySensorDevice):
"""Implementation of a SleepIQ presence sensor.""" """Implementation of a SleepIQ presence sensor."""

View File

@ -63,14 +63,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
_LOGGER.error('No sensors added') _LOGGER.error('No sensors added')
return False return False
hass.loop.create_task(async_add_devices(sensors)) hass.loop.create_task(async_add_devices(sensors, True))
return True return True
class BinarySensorTemplate(BinarySensorDevice): class BinarySensorTemplate(BinarySensorDevice):
"""A virtual binary sensor that triggers from another sensor.""" """A virtual binary sensor that triggers from another sensor."""
# pylint: disable=too-many-arguments
def __init__(self, hass, device, friendly_name, sensor_class, def __init__(self, hass, device, friendly_name, sensor_class,
value_template, entity_ids): value_template, entity_ids):
"""Initialize the Template binary sensor.""" """Initialize the Template binary sensor."""
@ -82,8 +81,6 @@ class BinarySensorTemplate(BinarySensorDevice):
self._template = value_template self._template = value_template
self._state = None self._state = None
self._async_render()
@callback @callback
def template_bsensor_state_listener(entity, old_state, new_state): def template_bsensor_state_listener(entity, old_state, new_state):
"""Called when the target device changes state.""" """Called when the target device changes state."""
@ -115,10 +112,6 @@ class BinarySensorTemplate(BinarySensorDevice):
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Update the state from the template.""" """Update the state from the template."""
self._async_render()
def _async_render(self):
"""Render the state from the template."""
try: try:
self._state = self._template.async_render().lower() == 'true' self._state = self._template.async_render().lower() == 'true'
except TemplateError as ex: except TemplateError as ex:

View File

@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class SensorTrend(BinarySensorDevice): class SensorTrend(BinarySensorDevice):
"""Representation of a trend Sensor.""" """Representation of a trend Sensor."""
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, hass, device_id, friendly_name, def __init__(self, hass, device_id, friendly_name,
target_entity, attribute, sensor_class, invert): target_entity, attribute, sensor_class, invert):
"""Initialize the sensor.""" """Initialize the sensor."""

View File

@ -16,9 +16,9 @@ DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__) _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.""" """Perform the setup for Vera controller devices."""
add_devices_callback( add_devices(
VeraBinarySensor(device, VERA_CONTROLLER) VeraBinarySensor(device, VERA_CONTROLLER)
for device in VERA_DEVICES['binary_sensor']) for device in VERA_DEVICES['binary_sensor'])

View File

@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument,too-few-public-methods # pylint: disable=unused-argument
def setup(hass, config): def setup(hass, config):
"""Setup BloomSky component.""" """Setup BloomSky component."""
api_key = config[DOMAIN][CONF_API_KEY] api_key = config[DOMAIN][CONF_API_KEY]

View File

@ -5,8 +5,10 @@ Component to interface with cameras.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/ https://home-assistant.io/components/camera/
""" """
import asyncio
import logging import logging
import time
from aiohttp import web
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -25,17 +27,16 @@ STATE_IDLE = 'idle'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
# pylint: disable=too-many-branches @asyncio.coroutine
def setup(hass, config): def async_setup(hass, config):
"""Setup the camera component.""" """Setup the camera component."""
component = EntityComponent( component = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
hass.wsgi.register_view(CameraImageView(hass, component.entities)) hass.http.register_view(CameraImageView(hass, component.entities))
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities)) hass.http.register_view(CameraMjpegStream(hass, component.entities))
component.setup(config)
yield from component.async_setup(config)
return True return True
@ -80,33 +81,59 @@ class Camera(Entity):
"""Return bytes of camera image.""" """Return bytes of camera image."""
raise NotImplementedError() raise NotImplementedError()
def mjpeg_stream(self, response): @asyncio.coroutine
"""Generate an HTTP MJPEG stream from camera images.""" def async_camera_image(self):
def stream(): """Return bytes of camera image.
"""Stream images as mjpeg stream."""
try:
last_image = None
while True:
img_bytes = self.camera_image()
if img_bytes is not None and img_bytes != last_image: This method must be run in the event loop.
yield bytes( """
image = yield from self.hass.loop.run_in_executor(
None, self.camera_image)
return image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from camera images.
This method must be run in the event loop.
"""
response = web.StreamResponse()
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' '--jpegboundary\r\n'
'Content-Type: image/jpeg\r\n' 'Content-Type: image/jpeg\r\n'
'Content-Length: {}\r\n\r\n'.format( 'Content-Length: {}\r\n\r\n'.format(
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n' 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 last_image = img_bytes
yield from response.drain()
time.sleep(0.5) yield from asyncio.sleep(.5)
except GeneratorExit: finally:
pass yield from response.write_eof()
return response(
stream(),
content_type=('multipart/x-mixed-replace; '
'boundary=--jpegboundary')
)
@property @property
def state(self): def state(self):
@ -144,22 +171,25 @@ class CameraView(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.entities = entities self.entities = entities
@asyncio.coroutine
def get(self, request, entity_id): def get(self, request, entity_id):
"""Start a get request.""" """Start a get request."""
camera = self.entities.get(entity_id) camera = self.entities.get(entity_id)
if camera is None: if camera is None:
return self.Response(status=404) return web.Response(status=404)
authenticated = (request.authenticated or authenticated = (request.authenticated or
request.args.get('token') == camera.access_token) request.GET.get('token') == camera.access_token)
if not authenticated: 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.""" """Hanlde the camera request."""
raise NotImplementedError() raise NotImplementedError()
@ -167,25 +197,27 @@ class CameraView(HomeAssistantView):
class CameraImageView(CameraView): class CameraImageView(CameraView):
"""Camera view to serve an image.""" """Camera view to serve an image."""
url = "/api/camera_proxy/<entity(domain=camera):entity_id>" url = "/api/camera_proxy/{entity_id}"
name = "api:camera:image" name = "api:camera:image"
def handle(self, camera): @asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image.""" """Serve camera image."""
response = camera.camera_image() image = yield from camera.async_camera_image()
if response is None: if image is None:
return self.Response(status=500) return web.Response(status=500)
return self.Response(response) return web.Response(body=image)
class CameraMjpegStream(CameraView): class CameraMjpegStream(CameraView):
"""Camera View to serve an MJPEG stream.""" """Camera View to serve an MJPEG stream."""
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>" url = "/api/camera_proxy_stream/{entity_id}"
name = "api:camera:stream" name = "api:camera:stream"
def handle(self, camera): @asyncio.coroutine
def handle(self, request, camera):
"""Serve camera image.""" """Serve camera image."""
return camera.mjpeg_stream(self.Response) yield from camera.handle_async_mjpeg_stream(request)

View File

@ -4,15 +4,18 @@ Support for Cameras with FFmpeg as decoder.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.ffmpeg/ https://home-assistant.io/components/camera.ffmpeg/
""" """
import asyncio
import logging import logging
import voluptuous as vol 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 ( 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 import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.util.async import run_coroutine_threadsafe
DEPENDENCIES = ['ffmpeg'] 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.""" """Setup a FFmpeg Camera."""
if not run_test(config.get(CONF_INPUT)): if not async_run_test(hass, config.get(CONF_INPUT)):
return return
add_devices([FFmpegCamera(config)]) hass.loop.create_task(async_add_devices([FFmpegCamera(hass, config)]))
class FFmpegCamera(Camera): class FFmpegCamera(Camera):
"""An implementation of an FFmpeg camera.""" """An implementation of an FFmpeg camera."""
def __init__(self, config): def __init__(self, hass, config):
"""Initialize a FFmpeg camera.""" """Initialize a FFmpeg camera."""
super().__init__() super().__init__()
self._name = config.get(CONF_NAME) self._name = config.get(CONF_NAME)
@ -45,24 +49,45 @@ class FFmpegCamera(Camera):
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
def camera_image(self): 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.""" """Return a still image response from the camera."""
from haffmpeg import ImageSingle, IMAGE_JPEG from haffmpeg import ImageSingleAsync, IMAGE_JPEG
ffmpeg = ImageSingle(get_binary()) ffmpeg = ImageSingleAsync(get_binary(), loop=self.hass.loop)
return ffmpeg.get_image(self._input, output_format=IMAGE_JPEG, image = yield from ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments) 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.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg from haffmpeg import CameraMjpegAsync
stream = CameraMjpeg(get_binary()) stream = CameraMjpegAsync(get_binary(), loop=self.hass.loop)
stream.open_camera(self._input, extra_cmd=self._extra_arguments) yield from stream.open_camera(
return response( self._input, extra_cmd=self._extra_arguments)
stream,
mimetype='multipart/x-mixed-replace;boundary=ffserver', response = web.StreamResponse()
direct_passthrough=True 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 @property
def name(self): def name(self):

View File

@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([FoscamCamera(config)]) add_devices([FoscamCamera(config)])
# pylint: disable=too-many-instance-attributes
class FoscamCamera(Camera): class FoscamCamera(Camera):
"""An implementation of a Foscam IP camera.""" """An implementation of a Foscam IP camera."""

View File

@ -4,10 +4,13 @@ Support for IP Cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.generic/ https://home-assistant.io/components/camera.generic/
""" """
import asyncio
import logging import logging
import aiohttp
import async_timeout
import requests import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth from requests.auth import HTTPDigestAuth
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
@ -16,6 +19,7 @@ from homeassistant.const import (
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,13 +39,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine
# pylint: disable=unused-argument # 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.""" """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): class GenericCamera(Camera):
"""A generic implementation of an IP camera.""" """A generic implementation of an IP camera."""
@ -49,6 +53,7 @@ class GenericCamera(Camera):
"""Initialize a generic camera.""" """Initialize a generic camera."""
super().__init__() super().__init__()
self.hass = hass self.hass = hass
self._authentication = device_info.get(CONF_AUTHENTICATION)
self._name = device_info.get(CONF_NAME) self._name = device_info.get(CONF_NAME)
self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url.hass = hass self._still_image_url.hass = hass
@ -58,10 +63,10 @@ class GenericCamera(Camera):
password = device_info.get(CONF_PASSWORD) password = device_info.get(CONF_PASSWORD)
if username and password: if username and password:
if device_info[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION: if self._authentication == HTTP_DIGEST_AUTHENTICATION:
self._auth = HTTPDigestAuth(username, password) self._auth = HTTPDigestAuth(username, password)
else: else:
self._auth = HTTPBasicAuth(username, password) self._auth = aiohttp.BasicAuth(username, password=password)
else: else:
self._auth = None self._auth = None
@ -69,9 +74,15 @@ class GenericCamera(Camera):
self._last_image = None self._last_image = None
def camera_image(self): 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.""" """Return a still image response from the camera."""
try: try:
url = self._still_image_url.render() url = self._still_image_url.async_render()
except TemplateError as err: except TemplateError as err:
_LOGGER.error('Error parsing template %s: %s', _LOGGER.error('Error parsing template %s: %s',
self._still_image_url, err) self._still_image_url, err)
@ -80,16 +91,35 @@ class GenericCamera(Camera):
if url == self._last_url and self._limit_refetch: if url == self._last_url and self._limit_refetch:
return self._last_image 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: try:
kwargs = {'timeout': 10, 'auth': self._auth}
response = requests.get(url, **kwargs) response = requests.get(url, **kwargs)
return response.content
except requests.exceptions.RequestException as error: except requests.exceptions.RequestException as error:
_LOGGER.error('Error getting camera image: %s', error) _LOGGER.error('Error getting camera image: %s', error)
return None return self._last_image
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_url = url
self._last_image = response.content
return self._last_image return self._last_image
@property @property

View File

@ -4,9 +4,14 @@ Support for IP Cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.mjpeg/ https://home-assistant.io/components/camera.mjpeg/
""" """
import asyncio
import logging import logging
from contextlib import closing from contextlib import closing
import aiohttp
from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
import requests import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth from requests.auth import HTTPBasicAuth, HTTPDigestAuth
import voluptuous as vol import voluptuous as vol
@ -34,10 +39,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine
# pylint: disable=unused-argument # 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.""" """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): def extract_image_from_mjpeg(stream):
@ -52,11 +58,10 @@ def extract_image_from_mjpeg(stream):
return jpg return jpg
# pylint: disable=too-many-instance-attributes
class MjpegCamera(Camera): class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL.""" """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.""" """Initialize a MJPEG camera."""
super().__init__() super().__init__()
self._name = device_info.get(CONF_NAME) self._name = device_info.get(CONF_NAME)
@ -65,32 +70,61 @@ class MjpegCamera(Camera):
self._password = device_info.get(CONF_PASSWORD) self._password = device_info.get(CONF_PASSWORD)
self._mjpeg_url = device_info[CONF_MJPEG_URL] self._mjpeg_url = device_info[CONF_MJPEG_URL]
def camera_stream(self): self._auth = None
"""Return a MJPEG stream image response directly from the camera.""" 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._username and self._password:
if self._authentication == HTTP_DIGEST_AUTHENTICATION: if self._authentication == HTTP_DIGEST_AUTHENTICATION:
auth = HTTPDigestAuth(self._username, self._password) auth = HTTPDigestAuth(self._username, self._password)
else: else:
auth = HTTPBasicAuth(self._username, self._password) auth = HTTPBasicAuth(self._username, self._password)
return requests.get(self._mjpeg_url, req = requests.get(
auth=auth, self._mjpeg_url, auth=auth, stream=True, timeout=10)
stream=True, timeout=10)
else: 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): with closing(req) as response:
"""Return a still image response from the camera.""" return extract_image_from_mjpeg(response.iter_content(102400))
with closing(self.camera_stream()) as response:
return extract_image_from_mjpeg(response.iter_content(1024))
def mjpeg_stream(self, response): @asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
stream = self.camera_stream() # aiohttp don't support DigestAuth -> Fallback
return response( if self._authentication == HTTP_DIGEST_AUTHENTICATION:
stream.iter_content(chunk_size=1024), yield from super().handle_async_mjpeg_stream(request)
mimetype=stream.headers[CONTENT_TYPE_HEADER], return
direct_passthrough=True
# 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 @property
def name(self): def name(self):

View File

@ -12,7 +12,8 @@ import shutil
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) 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 from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -35,7 +36,7 @@ DEFAULT_TIMELAPSE = 1000
DEFAULT_VERTICAL_FLIP = 0 DEFAULT_VERTICAL_FLIP = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 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.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)), vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_HORIZONTAL_FLIP): 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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Raspberry Camera.""" """Setup the Raspberry Camera."""
if shutil.which("raspistill") is None: 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)
_LOGGER.error("File path is not writable")
return False 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)]) add_devices([RaspberryCamera(setup_config)])
except PermissionError:
_LOGGER.error("File path is not writable")
return False
except FileNotFoundError:
_LOGGER.error("Could not create output file (missing directory?)")
return False
class RaspberryCamera(Camera): class RaspberryCamera(Camera):
@ -93,9 +110,7 @@ class RaspberryCamera(Camera):
self._config = device_info self._config = device_info
# Kill if there's raspistill instance # Kill if there's raspistill instance
subprocess.Popen(['killall', 'raspistill'], kill_raspistill()
stdout=subprocess.DEVNULL,
stderr=subprocess.STDOUT)
cmd_args = [ cmd_args = [
'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH], 'raspistill', '--nopreview', '-o', device_info[CONF_FILE_PATH],

View File

@ -4,28 +4,30 @@ Support for Synology Surveillance Station Cameras.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.synology/ https://home-assistant.io/components/camera.synology/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import requests from aiohttp import web
from aiohttp.web_exceptions import HTTPGatewayTimeout
import async_timeout
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_USERNAME, CONF_PASSWORD,
CONF_URL, CONF_WHITELIST) CONF_URL, CONF_WHITELIST, CONF_VERIFY_SSL)
from homeassistant.components.camera import ( from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA) Camera, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# pylint: disable=too-many-locals
DEFAULT_NAME = 'Synology Camera' DEFAULT_NAME = 'Synology Camera'
DEFAULT_STREAM_ID = '0' DEFAULT_STREAM_ID = '0'
TIMEOUT = 5 TIMEOUT = 5
CONF_CAMERA_NAME = 'camera_name' CONF_CAMERA_NAME = 'camera_name'
CONF_STREAM_ID = 'stream_id' CONF_STREAM_ID = 'stream_id'
CONF_VALID_CERT = 'valid_cert'
QUERY_CGI = 'query.cgi' QUERY_CGI = 'query.cgi'
QUERY_API = 'SYNO.API.Info' QUERY_API = 'SYNO.API.Info'
@ -38,6 +40,7 @@ WEBAPI_PATH = '/webapi/'
AUTH_PATH = 'auth.cgi' AUTH_PATH = 'auth.cgi'
CAMERA_PATH = 'camera.cgi' CAMERA_PATH = 'camera.cgi'
STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi' STREAMING_PATH = 'SurveillanceStation/videoStreaming.cgi'
CONTENT_TYPE_HEADER = 'Content-Type'
SYNO_API_URL = '{0}{1}{2}' SYNO_API_URL = '{0}{1}{2}'
@ -47,89 +50,136 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_URL): cv.string, vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_WHITELIST, default=[]): cv.ensure_list, 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.""" """Setup a Synology IP Camera."""
# Determine API to use for authentication # Determine API to use for authentication
syno_api_url = SYNO_API_URL.format(config.get(CONF_URL), syno_api_url = SYNO_API_URL.format(
WEBAPI_PATH, config.get(CONF_URL), WEBAPI_PATH, QUERY_CGI)
QUERY_CGI)
query_payload = {'api': QUERY_API, query_payload = {
'api': QUERY_API,
'method': 'Query', 'method': 'Query',
'version': '1', 'version': '1',
'query': 'SYNO.'} 'query': 'SYNO.'
query_req = requests.get(syno_api_url, }
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
query_req = yield from hass.websession.get(
syno_api_url,
params=query_payload, params=query_payload,
verify=config.get(CONF_VALID_CERT), verify_ssl=config.get(CONF_VERIFY_SSL)
timeout=TIMEOUT) )
query_resp = query_req.json() 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'] auth_path = query_resp['data'][AUTH_API]['path']
camera_api = query_resp['data'][CAMERA_API]['path'] camera_api = query_resp['data'][CAMERA_API]['path']
camera_path = query_resp['data'][CAMERA_API]['path'] camera_path = query_resp['data'][CAMERA_API]['path']
streaming_path = query_resp['data'][STREAMING_API]['path'] streaming_path = query_resp['data'][STREAMING_API]['path']
# cleanup
yield from query_req.release()
# Authticate to NAS to get a session id # Authticate to NAS to get a session id
syno_auth_url = SYNO_API_URL.format(config.get(CONF_URL), syno_auth_url = SYNO_API_URL.format(
WEBAPI_PATH, config.get(CONF_URL), WEBAPI_PATH, auth_path)
auth_path)
session_id = get_session_id(config.get(CONF_USERNAME), session_id = yield from get_session_id(
hass,
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD), config.get(CONF_PASSWORD),
syno_auth_url, syno_auth_url,
config.get(CONF_VALID_CERT)) config.get(CONF_VERIFY_SSL)
)
# Use SessionID to get cameras in system # Use SessionID to get cameras in system
syno_camera_url = SYNO_API_URL.format(config.get(CONF_URL), syno_camera_url = SYNO_API_URL.format(
WEBAPI_PATH, config.get(CONF_URL), WEBAPI_PATH, camera_api)
camera_api)
camera_payload = {'api': CAMERA_API, camera_payload = {
'api': CAMERA_API,
'method': 'List', 'method': 'List',
'version': '1'} 'version': '1'
camera_req = requests.get(syno_camera_url, }
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
camera_req = yield from hass.websession.get(
syno_camera_url,
params=camera_payload, params=camera_payload,
verify=config.get(CONF_VALID_CERT), verify_ssl=config.get(CONF_VERIFY_SSL),
timeout=TIMEOUT, cookies={'id': session_id}
cookies={'id': session_id}) )
camera_resp = camera_req.json() 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'] cameras = camera_resp['data']['cameras']
yield from camera_req.release()
# add cameras
devices = []
tasks = []
for camera in cameras: for camera in cameras:
if not config.get(CONF_WHITELIST): if not config.get(CONF_WHITELIST):
camera_id = camera['id'] camera_id = camera['id']
snapshot_path = camera['snapshot_path'] snapshot_path = camera['snapshot_path']
add_devices([SynologyCamera(config, device = SynologyCamera(
config,
camera_id, camera_id,
camera['name'], camera['name'],
snapshot_path, snapshot_path,
streaming_path, streaming_path,
camera_path, camera_path,
auth_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.""" """Get a session id."""
auth_payload = {'api': AUTH_API, auth_payload = {
'api': AUTH_API,
'method': 'Login', 'method': 'Login',
'version': '2', 'version': '2',
'account': username, 'account': username,
'passwd': password, 'passwd': password,
'session': 'SurveillanceStation', 'session': 'SurveillanceStation',
'format': 'sid'} 'format': 'sid'
auth_req = requests.get(login_url, }
try:
with async_timeout.timeout(TIMEOUT, loop=hass.loop):
auth_req = yield from hass.websession.get(
login_url,
params=auth_payload, params=auth_payload,
verify=valid_cert, verify_ssl=valid_cert
timeout=TIMEOUT) )
auth_resp = auth_req.json() 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'] return auth_resp['data']['sid']
# pylint: disable=too-many-instance-attributes
class SynologyCamera(Camera): class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera.""" """An implementation of a Synology NAS based IP camera."""
# pylint: disable=too-many-arguments
def __init__(self, config, camera_id, camera_name, def __init__(self, config, camera_id, camera_name,
snapshot_path, streaming_path, camera_path, auth_path): snapshot_path, streaming_path, camera_path, auth_path):
"""Initialize a Synology Surveillance Station camera.""" """Initialize a Synology Surveillance Station camera."""
@ -142,80 +192,98 @@ class SynologyCamera(Camera):
self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi' self._login_url = config.get(CONF_URL) + '/webapi/' + 'auth.cgi'
self._camera_name = config.get(CONF_CAMERA_NAME) self._camera_name = config.get(CONF_CAMERA_NAME)
self._stream_id = config.get(CONF_STREAM_ID) 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._camera_id = camera_id
self._snapshot_path = snapshot_path self._snapshot_path = snapshot_path
self._streaming_path = streaming_path self._streaming_path = streaming_path
self._camera_path = camera_path self._camera_path = camera_path
self._auth_path = auth_path self._auth_path = auth_path
self._session_id = None
self._session_id = get_session_id(self._username, @asyncio.coroutine
def async_read_sid(self):
"""Get a session id."""
self._session_id = yield from get_session_id(
self.hass,
self._username,
self._password, self._password,
self._login_url, self._login_url,
self._valid_cert) self._valid_cert
)
def get_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']
def camera_image(self): 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.""" """Return a still image response from the camera."""
image_url = SYNO_API_URL.format(self._synology_url, image_url = SYNO_API_URL.format(
WEBAPI_PATH, self._synology_url, WEBAPI_PATH, self._camera_path)
self._camera_path)
image_payload = {'api': CAMERA_API, image_payload = {
'api': CAMERA_API,
'method': 'GetSnapshot', 'method': 'GetSnapshot',
'version': '1', 'version': '1',
'cameraId': self._camera_id} 'cameraId': self._camera_id
}
try: try:
response = requests.get(image_url, with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
response = yield from self.hass.websession.get(
image_url,
params=image_payload, params=image_payload,
timeout=TIMEOUT, verify_ssl=self._valid_cert,
verify=self._valid_cert, cookies={'id': self._session_id}
cookies={'id': self._session_id}) )
except requests.exceptions.RequestException as error: except asyncio.TimeoutError:
_LOGGER.error('Error getting camera image: %s', error) _LOGGER.error("Timeout on %s", image_url)
return None 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.""" """Return a MJPEG stream image response directly from the camera."""
streaming_url = SYNO_API_URL.format(self._synology_url, streaming_url = SYNO_API_URL.format(
WEBAPI_PATH, self._synology_url, WEBAPI_PATH, self._streaming_path)
self._streaming_path)
streaming_payload = {'api': STREAMING_API, streaming_payload = {
'api': STREAMING_API,
'method': 'Stream', 'method': 'Stream',
'version': '1', 'version': '1',
'cameraId': self._camera_id, 'cameraId': self._camera_id,
'format': 'mjpeg'} 'format': 'mjpeg'
response = requests.get(streaming_url, }
try:
with async_timeout.timeout(TIMEOUT, loop=self.hass.loop):
stream = yield from self.hass.websession.get(
streaming_url,
payload=streaming_payload, payload=streaming_payload,
stream=True, verify_ssl=self._valid_cert,
timeout=TIMEOUT, cookies={'id': self._session_id}
cookies={'id': self._session_id})
return response
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
) )
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 @property
def name(self): def name(self):

View File

@ -123,7 +123,6 @@ def set_aux_heat(hass, aux_heat, entity_id=None):
hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data)
# pylint: disable=too-many-arguments
def set_temperature(hass, temperature=None, entity_id=None, def set_temperature(hass, temperature=None, entity_id=None,
target_temp_high=None, target_temp_low=None, target_temp_high=None, target_temp_low=None,
operation_mode=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) hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data)
# pylint: disable=too-many-branches
def setup(hass, config): def setup(hass, config):
"""Setup climate devices.""" """Setup climate devices."""
component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
@ -364,7 +362,7 @@ def setup(hass, config):
class ClimateDevice(Entity): class ClimateDevice(Entity):
"""Representation of a climate device.""" """Representation of a climate device."""
# pylint: disable=too-many-public-methods,no-self-use # pylint: disable=no-self-use
@property @property
def state(self): def state(self):
"""Return the current state.""" """Return the current state."""

View File

@ -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): class DemoClimate(ClimateDevice):
"""Representation of a demo climate device.""" """Representation of a demo climate device."""
# pylint: disable=too-many-instance-attributes
def __init__(self, name, target_temperature, unit_of_measurement, def __init__(self, name, target_temperature, unit_of_measurement,
away, current_temperature, current_fan_mode, away, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode, target_humidity, current_humidity, current_swing_mode,

View File

@ -72,7 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
schema=SET_FAN_MIN_ON_TIME_SCHEMA) schema=SET_FAN_MIN_ON_TIME_SCHEMA)
# pylint: disable=too-many-public-methods, abstract-method
class Thermostat(ClimateDevice): class Thermostat(ClimateDevice):
"""A thermostat class for Ecobee.""" """A thermostat class for Ecobee."""

View File

@ -42,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices) add_devices(devices)
# pylint: disable=too-many-instance-attributes, import-error, abstract-method # pylint: disable=import-error
class EQ3BTSmartThermostat(ClimateDevice): class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of a eQ-3 Bluetooth Smart thermostat.""" """Representation of a eQ-3 Bluetooth Smart thermostat."""

View File

@ -62,11 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
target_temp, ac_mode, min_cycle_duration)]) target_temp, ac_mode, min_cycle_duration)])
# pylint: disable=too-many-instance-attributes, abstract-method
class GenericThermostat(ClimateDevice): class GenericThermostat(ClimateDevice):
"""Representation of a GenericThermostat device.""" """Representation of a GenericThermostat device."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, heater_entity_id, sensor_entity_id, def __init__(self, hass, name, heater_entity_id, sensor_entity_id,
min_temp, max_temp, target_temp, ac_mode, min_cycle_duration): min_temp, max_temp, target_temp, ac_mode, min_cycle_duration):
"""Initialize the thermostat.""" """Initialize the thermostat."""

View File

@ -56,7 +56,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class HeatmiserV3Thermostat(ClimateDevice): class HeatmiserV3Thermostat(ClimateDevice):
"""Representation of a HeatmiserV3 thermostat.""" """Representation of a HeatmiserV3 thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, heatmiser, device, name, serport): def __init__(self, heatmiser, device, name, serport):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.heatmiser = heatmiser self.heatmiser = heatmiser

View File

@ -36,7 +36,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
) )
# pylint: disable=abstract-method
class HMThermostat(homematic.HMDevice, ClimateDevice): class HMThermostat(homematic.HMDevice, ClimateDevice):
"""Representation of a Homematic thermostat.""" """Representation of a Homematic thermostat."""

View File

@ -100,7 +100,6 @@ def _setup_us(username, password, config, add_devices):
class RoundThermostat(ClimateDevice): class RoundThermostat(ClimateDevice):
"""Representation of a Honeywell Round Connected thermostat.""" """Representation of a Honeywell Round Connected thermostat."""
# pylint: disable=too-many-instance-attributes, abstract-method
def __init__(self, device, zone_id, master, away_temp): def __init__(self, device, zone_id, master, away_temp):
"""Initialize the thermostat.""" """Initialize the thermostat."""
self.device = device self.device = device
@ -197,7 +196,6 @@ class RoundThermostat(ClimateDevice):
self._is_dhw = False self._is_dhw = False
# pylint: disable=abstract-method
class HoneywellUSThermostat(ClimateDevice): class HoneywellUSThermostat(ClimateDevice):
"""Representation of a Honeywell US Thermostat.""" """Representation of a Honeywell US Thermostat."""

View File

@ -37,7 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
map_sv_types, devices, add_devices, MySensorsHVAC)) map_sv_types, devices, add_devices, MySensorsHVAC))
# pylint: disable=too-many-arguments, too-many-public-methods
class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice): class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
"""Representation of a MySensorsHVAC hvac.""" """Representation of a MySensorsHVAC hvac."""

View File

@ -5,8 +5,10 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.nest/ https://home-assistant.io/components/climate.nest/
""" """
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.components.nest as nest
from homeassistant.components.nest import DATA_NEST
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, 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): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Nest thermostat.""" """Setup the Nest thermostat."""
temp_unit = hass.config.units.temperature_unit temp_unit = hass.config.units.temperature_unit
add_devices([NestThermostat(structure, device, temp_unit) add_devices(
for structure, device in nest.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): class NestThermostat(ClimateDevice):
"""Representation of a Nest thermostat.""" """Representation of a Nest thermostat."""
@ -54,18 +58,33 @@ class NestThermostat(ClimateDevice):
if self.device.can_heat and self.device.can_cool: if self.device.can_heat and self.device.can_cool:
self._operation_list.append(STATE_AUTO) 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 @property
def name(self): def name(self):
"""Return the name of the nest, if any.""" """Return the name of the nest, if any."""
location = self.device.where if self._location is None:
name = self.device.name return self._name
if location is None:
return name
else: else:
if name == '': if self._name == '':
return location.capitalize() return self._location.capitalize()
else: else:
return location.capitalize() + '(' + name + ')' return self._location.capitalize() + '(' + self._name + ')'
@property @property
def temperature_unit(self): def temperature_unit(self):
@ -75,11 +94,11 @@ class NestThermostat(ClimateDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device specific state attributes.""" """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 # Move these to Thermostat Device and make them global
return { return {
"humidity": self.device.humidity, "humidity": self._humidity,
"target_humidity": self.device.target_humidity, "target_humidity": self._target_humidity,
} }
else: else:
# No way to control humidity not show setting # No way to control humidity not show setting
@ -88,18 +107,18 @@ class NestThermostat(ClimateDevice):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
return self.device.temperature return self._temperature
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
if self.device.mode == 'cool': if self._mode == 'cool':
return STATE_COOL return STATE_COOL
elif self.device.mode == 'heat': elif self._mode == 'heat':
return STATE_HEAT return STATE_HEAT
elif self.device.mode == 'range': elif self._mode == 'range':
return STATE_AUTO return STATE_AUTO
elif self.device.mode == 'off': elif self._mode == 'off':
return STATE_OFF return STATE_OFF
else: else:
return STATE_UNKNOWN return STATE_UNKNOWN
@ -107,37 +126,37 @@ class NestThermostat(ClimateDevice):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self.device.mode != 'range' and not self.is_away_mode_on: if self._mode != 'range' and not self.is_away_mode_on:
return self.device.target return self._target_temperature
else: else:
return None return None
@property @property
def target_temperature_low(self): def target_temperature_low(self):
"""Return the lower bound temperature we try to reach.""" """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 # away_temperature is always a low, high tuple
return self.device.away_temperature[0] return self._away_temperature[0]
if self.device.mode == 'range': if self._mode == 'range':
return self.device.target[0] return self._target_temperature[0]
else: else:
return None return None
@property @property
def target_temperature_high(self): def target_temperature_high(self):
"""Return the upper bound temperature we try to reach.""" """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 # away_temperature is always a low, high tuple
return self.device.away_temperature[1] return self._away_temperature[1]
if self.device.mode == 'range': if self._mode == 'range':
return self.device.target[1] return self._target_temperature[1]
else: else:
return None return None
@property @property
def is_away_mode_on(self): def is_away_mode_on(self):
"""Return if away mode is on.""" """Return if away mode is on."""
return self.structure.away return self._away
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
@ -145,7 +164,7 @@ class NestThermostat(ClimateDevice):
target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
if target_temp_low is not None and target_temp_high is not None: 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) temp = (target_temp_low, target_temp_high)
else: else:
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
@ -179,9 +198,9 @@ class NestThermostat(ClimateDevice):
@property @property
def current_fan_mode(self): def current_fan_mode(self):
"""Return whether the fan is on.""" """Return whether the fan is on."""
if self.device.has_fan: if self._has_fan:
# Return whether the fan is on # 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: else:
# No Fan available so disable slider # No Fan available so disable slider
return None return None
@ -198,7 +217,7 @@ class NestThermostat(ClimateDevice):
@property @property
def min_temp(self): def min_temp(self):
"""Identify min_temp in Nest API or defaults if not available.""" """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: if temp is None:
return super().min_temp return super().min_temp
else: else:
@ -207,12 +226,21 @@ class NestThermostat(ClimateDevice):
@property @property
def max_temp(self): def max_temp(self):
"""Identify max_temp in Nest API or defaults if not available.""" """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: if temp is None:
return super().max_temp return super().max_temp
else: else:
return temp return temp
def update(self): def update(self):
"""Python-nest has its own mechanism for staying up to date.""" """Cache value from Python-nest."""
pass 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

View File

@ -54,7 +54,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
return None return None
# pylint: disable=abstract-method
class NetatmoThermostat(ClimateDevice): class NetatmoThermostat(ClimateDevice):
"""Representation a Netatmo thermostat.""" """Representation a Netatmo thermostat."""

View File

@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([ProliphixThermostat(pdp)]) add_devices([ProliphixThermostat(pdp)])
# pylint: disable=abstract-method
class ProliphixThermostat(ClimateDevice): class ProliphixThermostat(ClimateDevice):
"""Representation a Proliphix thermostat.""" """Representation a Proliphix thermostat."""

View File

@ -6,7 +6,6 @@ https://home-assistant.io/components/climate.radiotherm/
""" """
import datetime import datetime
import logging import logging
from urllib.error import URLError
import voluptuous as vol import voluptuous as vol
@ -52,14 +51,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
try: try:
tstat = radiotherm.get_thermostat(host) tstat = radiotherm.get_thermostat(host)
tstats.append(RadioThermostat(tstat, hold_temp)) tstats.append(RadioThermostat(tstat, hold_temp))
except (URLError, OSError): except OSError:
_LOGGER.exception("Unable to connect to Radio Thermostat: %s", _LOGGER.exception("Unable to connect to Radio Thermostat: %s",
host) host)
add_devices(tstats) add_devices(tstats)
# pylint: disable=abstract-method
class RadioThermostat(ClimateDevice): class RadioThermostat(ClimateDevice):
"""Representation of a Radio Thermostat.""" """Representation of a Radio Thermostat."""

View File

@ -8,7 +8,10 @@ import logging
from homeassistant.util import convert from homeassistant.util import convert
from homeassistant.components.climate import ClimateDevice 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 ( from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER) 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']) device in VERA_DEVICES['climate'])
# pylint: disable=abstract-method
class VeraThermostat(VeraDevice, ClimateDevice): class VeraThermostat(VeraDevice, ClimateDevice):
"""Representation of a Vera Thermostat.""" """Representation of a Vera Thermostat."""
@ -95,8 +97,14 @@ class VeraThermostat(VeraDevice, ClimateDevice):
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
vera_temp_units = (
self.vera_device.vera_controller.temperature_units)
if vera_temp_units == 'F':
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
return TEMP_CELSIUS
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""

View File

@ -69,11 +69,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
discovery_info, zwave.NETWORK) discovery_info, zwave.NETWORK)
# pylint: disable=abstract-method
class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
"""Represents a ZWave Climate device.""" """Represents a ZWave Climate device."""
# pylint: disable=too-many-instance-attributes
def __init__(self, value, temp_unit): def __init__(self, value, temp_unit):
"""Initialize the zwave climate device.""" """Initialize the zwave climate device."""
from openzwave.network import ZWaveNetwork from openzwave.network import ZWaveNetwork
@ -84,6 +82,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self._current_temperature = None self._current_temperature = None
self._current_operation = None self._current_operation = None
self._operation_list = None self._operation_list = None
self._operating_state = None
self._current_fan_mode = None self._current_fan_mode = None
self._fan_list = None self._fan_list = None
self._current_swing_mode = 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." _LOGGER.debug("Device can't set setpoint based on operation mode."
" Defaulting to index=1") " Defaulting to index=1")
self._target_temperature = int(value.data) 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 @property
def should_poll(self): def should_poll(self):
@ -323,3 +327,13 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
value.index == 33: value.index == 33:
value.data = bytes(swing_mode, 'utf-8') value.data = bytes(swing_mode, 'utf-8')
break 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 {}

View File

@ -8,7 +8,8 @@ the user has submitted configuration information.
""" """
import logging 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 from homeassistant.helpers.entity import generate_entity_id
_INSTANCES = {} _INSTANCES = {}
@ -33,10 +34,10 @@ STATE_CONFIGURE = 'configure'
STATE_CONFIGURED = 'configured' STATE_CONFIGURED = 'configured'
# pylint: disable=too-many-arguments
def request_config( def request_config(
hass, name, callback, description=None, description_image=None, 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. """Create a new request for configuration.
Will return an ID to be used for sequent calls. Will return an ID to be used for sequent calls.
@ -46,7 +47,7 @@ def request_config(
request_id = instance.request_config( request_id = instance.request_config(
name, callback, name, callback,
description, description_image, submit_caption, description, description_image, submit_caption,
fields, link_name, link_url) fields, link_name, link_url, entity_picture)
_REQUESTS[request_id] = instance _REQUESTS[request_id] = instance
@ -100,11 +101,10 @@ class Configurator(object):
hass.services.register( hass.services.register(
DOMAIN, SERVICE_CONFIGURE, self.handle_service_call) DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
# pylint: disable=too-many-arguments
def request_config( def request_config(
self, name, callback, self, name, callback,
description, description_image, submit_caption, description, description_image, submit_caption,
fields, link_name, link_url): fields, link_name, link_url, entity_picture):
"""Setup a request for configuration.""" """Setup a request for configuration."""
entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass) 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_CONFIGURE_ID: request_id,
ATTR_FIELDS: fields, ATTR_FIELDS: fields,
ATTR_FRIENDLY_NAME: name, ATTR_FRIENDLY_NAME: name,
ATTR_ENTITY_PICTURE: entity_picture,
} }
data.update({ data.update({

View File

@ -60,11 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(covers) add_devices(covers)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class CommandCover(CoverDevice): class CommandCover(CoverDevice):
"""Representation a command line cover.""" """Representation a command line cover."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, command_open, command_close, command_stop, def __init__(self, hass, name, command_open, command_close, command_stop,
command_state, value_template): command_state, value_template):
"""Initialize the cover.""" """Initialize the cover."""

View File

@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DemoCover(CoverDevice): class DemoCover(CoverDevice):
"""Representation of a demo cover.""" """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): def __init__(self, hass, name, position=None, tilt_position=None):
"""Initialize the cover.""" """Initialize the cover."""
self.hass = hass self.hass = hass

View File

@ -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()

View File

@ -31,7 +31,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None):
) )
# pylint: disable=abstract-method
class HMCover(homematic.HMDevice, CoverDevice): class HMCover(homematic.HMDevice, CoverDevice):
"""Represents a Homematic Cover in Home Assistant.""" """Represents a Homematic Cover in Home Assistant."""

View File

@ -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): class MqttCover(CoverDevice):
"""Representation of a cover that can be controlled using MQTT.""" """Representation of a cover that can be controlled using MQTT."""

View File

@ -40,7 +40,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update)
# pylint: disable=abstract-method
class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice):
"""Representation of an rfxtrx cover.""" """Representation of an rfxtrx cover."""

View File

@ -63,11 +63,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(covers) add_devices(covers)
# pylint: disable=abstract-method
class RPiGPIOCover(CoverDevice): class RPiGPIOCover(CoverDevice):
"""Representation of a Raspberry GPIO cover.""" """Representation of a Raspberry GPIO cover."""
# pylint: disable=too-many-arguments
def __init__(self, name, relay_pin, state_pin, state_pull_mode, def __init__(self, name, relay_pin, state_pin, state_pull_mode,
relay_time): relay_time):
"""Initialize the cover.""" """Initialize the cover."""

View File

@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(covers) add_devices(covers)
# pylint: disable=too-many-arguments, too-many-instance-attributes
class SCSGateCover(CoverDevice): class SCSGateCover(CoverDevice):
"""Representation of SCSGate cover.""" """Representation of SCSGate cover."""

View File

@ -15,14 +15,13 @@ DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__) _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.""" """Find and return Vera covers."""
add_devices_callback( add_devices(
VeraCover(device, VERA_CONTROLLER) for VeraCover(device, VERA_CONTROLLER) for
device in VERA_DEVICES['cover']) device in VERA_DEVICES['cover'])
# pylint: disable=abstract-method
class VeraCover(VeraDevice, CoverDevice): class VeraCover(VeraDevice, CoverDevice):
"""Represents a Vera Cover in Home Assistant.""" """Represents a Vera Cover in Home Assistant."""

View File

@ -32,14 +32,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]] node = zwave.NETWORK.nodes[discovery_info[zwave.const.ATTR_NODE_ID]]
value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]] value = node.values[discovery_info[zwave.const.ATTR_VALUE_ID]]
if node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL) \ if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL
and value.index == 0: and value.index == 0):
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZwaveRollershutter(value)]) add_devices([ZwaveRollershutter(value)])
elif node.has_command_class(zwave.const.COMMAND_CLASS_SWITCH_BINARY) or \ elif value.node.specific == zwave.const.GENERIC_TYPE_ENTRY_CONTROL:
node.has_command_class(zwave.const.COMMAND_CLASS_BARRIER_OPERATOR): if (value.command_class == zwave.const.COMMAND_CLASS_SWITCH_BINARY or
if value.type != zwave.const.TYPE_BOOL and \ value.command_class ==
value.genre != zwave.const.GENRE_USER: zwave.const.COMMAND_CLASS_BARRIER_OPERATOR):
if (value.type != zwave.const.TYPE_BOOL and
value.genre != zwave.const.GENRE_USER):
return return
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZwaveGarageDoor(value)]) add_devices([ZwaveGarageDoor(value)])
@ -122,7 +124,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Open' or value.command_class == \ 'Open' or value.command_class == \
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Down': 'Up':
self._lozwmgr.pressButton(value.value_id) self._lozwmgr.pressButton(value.value_id)
break break
@ -132,7 +134,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values(): class_id=zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL).values():
if value.command_class == \ if value.command_class == \
zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \ 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 == \ zwave.const.COMMAND_CLASS_SWITCH_MULTILEVEL and value.label == \
'Close': 'Close':
self._lozwmgr.pressButton(value.value_id) self._lozwmgr.pressButton(value.value_id)

View File

@ -41,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals
def setup(hass, config): def setup(hass, config):
"""The triggers to turn lights on or off based on device presence.""" """The triggers to turn lights on or off based on device presence."""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -4,8 +4,6 @@ Provide functionality to keep track of devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/device_tracker/ https://home-assistant.io/components/device_tracker/
""" """
# pylint: disable=too-many-instance-attributes, too-many-arguments
# pylint: disable=too-many-locals
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
import logging import logging
@ -14,7 +12,6 @@ import threading
from typing import Any, Sequence, Callable from typing import Any, Sequence, Callable
import voluptuous as vol import voluptuous as vol
import yaml
from homeassistant.bootstrap import ( from homeassistant.bootstrap import (
prepare_setup_platform, log_exception) prepare_setup_platform, log_exception)
@ -29,6 +26,7 @@ import homeassistant.helpers.config_validation as cv
import homeassistant.util as util import homeassistant.util as util
from homeassistant.util.async import run_coroutine_threadsafe from homeassistant.util.async import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.util.yaml import dump
from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.event import track_utc_time_change
from homeassistant.const import ( from homeassistant.const import (
@ -57,6 +55,8 @@ DEFAULT_SCAN_INTERVAL = 12
CONF_AWAY_HIDE = 'hide_if_away' CONF_AWAY_HIDE = 'hide_if_away'
DEFAULT_AWAY_HIDE = False DEFAULT_AWAY_HIDE = False
EVENT_NEW_DEVICE = 'device_tracker_new_device'
SERVICE_SEE = 'see' SERVICE_SEE = 'see'
ATTR_MAC = 'mac' ATTR_MAC = 'mac'
@ -88,7 +88,6 @@ def is_on(hass: HomeAssistantType, entity_id: str=None):
return hass.states.is_state(entity, STATE_HOME) return hass.states.is_state(entity, STATE_HOME)
# pylint: disable=too-many-arguments
def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None, def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
host_name: str=None, location_name: str=None, host_name: str=None, location_name: str=None,
gps: GPSType=None, gps_accuracy=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_GPS_ACCURACY, gps_accuracy),
(ATTR_BATTERY, battery)) if value is not None} (ATTR_BATTERY, battery)) if value is not None}
if attributes: if attributes:
for key, value in attributes: data[ATTR_ATTRIBUTES] = attributes
data[key] = value
hass.services.call(DOMAIN, SERVICE_SEE, data) hass.services.call(DOMAIN, SERVICE_SEE, data)
@ -115,7 +113,7 @@ def setup(hass: HomeAssistantType, config: ConfigType):
try: try:
conf = config.get(DOMAIN, []) conf = config.get(DOMAIN, [])
except vol.Invalid as ex: except vol.Invalid as ex:
log_exception(ex, DOMAIN, config) log_exception(ex, DOMAIN, config, hass)
return False return False
else: else:
conf = conf[0] if len(conf) > 0 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, device.seen(host_name, location_name, gps, gps_accuracy, battery,
attributes) attributes)
if device.track: if device.track:
device.update_ha_state() device.update_ha_state()
self.hass.bus.async_fire(EVENT_NEW_DEVICE, device)
# During init, we ignore the group # During init, we ignore the group
if self.group is not None: if self.group is not None:
self.group.update_tracked_entity_ids( 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_schema(device)
device['dev_id'] = cv.slugify(dev_id) device['dev_id'] = cv.slugify(dev_id)
except vol.Invalid as exp: except vol.Invalid as exp:
log_exception(exp, dev_id, devices) log_exception(exp, dev_id, devices, hass)
else: else:
result.append(Device(hass, **device)) result.append(Device(hass, **device))
return result return result
@ -468,8 +469,6 @@ def setup_scanner_platform(hass: HomeAssistantType, config: ConfigType,
def update_config(path: str, dev_id: str, device: Device): def update_config(path: str, dev_id: str, device: Device):
"""Add device to YAML configuration file.""" """Add device to YAML configuration file."""
with open(path, 'a') as out: with open(path, 'a') as out:
out.write('\n')
device = {device.dev_id: { device = {device.dev_id: {
'name': device.name, 'name': device.name,
'mac': device.mac, 'mac': device.mac,
@ -477,7 +476,8 @@ def update_config(path: str, dev_id: str, device: Device):
'track': device.track, 'track': device.track,
CONF_AWAY_HIDE: device.away_hide 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): def get_gravatar_for_email(email: str):

View File

@ -90,9 +90,7 @@ AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases arp')
class AsusWrtDeviceScanner(object): class AsusWrtDeviceScanner(object):
"""This class queries a router running ASUSWRT firmware.""" """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) # Eighth attribute needed for mode (AP mode vs router mode)
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the scanner."""
self.host = config[CONF_HOST] self.host = config[CONF_HOST]

View File

@ -60,8 +60,6 @@ def setup_scanner(hass, config: dict, see):
return True return True
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-few-public-methods
class AutomaticDeviceScanner(object): class AutomaticDeviceScanner(object):
"""A class representing an Automatic device.""" """A class representing an Automatic device."""

View File

@ -7,15 +7,16 @@ https://home-assistant.io/components/device_tracker.bbox/
from collections import namedtuple from collections import namedtuple
import logging import logging
from datetime import timedelta from datetime import timedelta
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker import DOMAIN
from homeassistant.util import Throttle from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago REQUIREMENTS = ['pybbox==0.0.5-alpha']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pybbox==0.0.5-alpha']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
def get_scanner(hass, config): def get_scanner(hass, config):
@ -36,7 +37,7 @@ class BboxDeviceScanner(object):
self.last_results = [] # type: List[Device] self.last_results = [] # type: List[Device]
self.success_init = self._update_info() self.success_init = self._update_info()
_LOGGER.info('Bbox scanner initialized') _LOGGER.info("Bbox scanner initialized")
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
@ -60,7 +61,7 @@ class BboxDeviceScanner(object):
Returns boolean if scanning successful. Returns boolean if scanning successful.
""" """
_LOGGER.info('Scanning') _LOGGER.info("Scanning...")
import pybbox import pybbox
@ -78,5 +79,5 @@ class BboxDeviceScanner(object):
self.last_results = last_results self.last_results = last_results
_LOGGER.info('Bbox scan successful') _LOGGER.info("Bbox scan successful")
return True return True

View File

@ -35,12 +35,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument # pylint: disable=unused-argument
def get_scanner(hass, config): def get_scanner(hass, config):
"""Validate the configuration and return a DD-WRT scanner.""" """Validate the configuration and return a DD-WRT scanner."""
scanner = DdWrtDeviceScanner(config[DOMAIN]) try:
return DdWrtDeviceScanner(config[DOMAIN])
return scanner if scanner.success_init else None except ConnectionError:
return None
# pylint: disable=too-many-instance-attributes
class DdWrtDeviceScanner(object): class DdWrtDeviceScanner(object):
"""This class queries a wireless router running DD-WRT firmware.""" """This class queries a wireless router running DD-WRT firmware."""
@ -53,13 +53,13 @@ class DdWrtDeviceScanner(object):
self.lock = threading.Lock() self.lock = threading.Lock()
self.last_results = {} self.last_results = {}
self.mac2name = {} self.mac2name = {}
# Test the router is accessible # Test the router is accessible
url = 'http://{}/Status_Wireless.live.asp'.format(self.host) url = 'http://{}/Status_Wireless.live.asp'.format(self.host)
data = self.get_ddwrt_data(url) 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): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
@ -83,14 +83,15 @@ class DdWrtDeviceScanner(object):
if not dhcp_leases: if not dhcp_leases:
return None return None
# Remove leading and trailing single quotes. # Remove leading and trailing quotes and spaces
cleaned_str = dhcp_leases.strip().strip('"') cleaned_str = dhcp_leases.replace(
elements = cleaned_str.split('","') "\"", "").replace("\'", "").replace(" ", "")
num_clients = int(len(elements)/5) elements = cleaned_str.split(',')
num_clients = int(len(elements) / 5)
self.mac2name = {} self.mac2name = {}
for idx in range(0, num_clients): for idx in range(0, num_clients):
# This is stupid but the data is a single array # The data is a single array
# every 5 elements represents one hosts, the MAC # every 5 elements represents one host, the MAC
# is the third element and the name is the first. # is the third element and the name is the first.
mac_index = (idx * 5) + 2 mac_index = (idx * 5) + 2
if mac_index < len(elements): if mac_index < len(elements):
@ -105,9 +106,6 @@ class DdWrtDeviceScanner(object):
Return boolean if scanning successful. Return boolean if scanning successful.
""" """
if not self.success_init:
return False
with self.lock: with self.lock:
_LOGGER.info('Checking ARP') _LOGGER.info('Checking ARP')
@ -123,11 +121,8 @@ class DdWrtDeviceScanner(object):
if not active_clients: if not active_clients:
return False return False
# This is really lame, instead of using JSON the DD-WRT UI # The DD-WRT UI uses its own data format and then
# uses its own data format for some reason and then # regex's out values so this is done here too
# regex's out values so I guess I have to do the same,
# LAME!!!
# Remove leading and trailing single quotes. # Remove leading and trailing single quotes.
clean_str = active_clients.strip().strip("'") clean_str = active_clients.strip().strip("'")
elements = clean_str.split("','") elements = clean_str.split("','")

View File

@ -38,7 +38,6 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class FritzBoxScanner(object): class FritzBoxScanner(object):
"""This class queries a FRITZ!Box router.""" """This class queries a FRITZ!Box router."""

View File

@ -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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.icloud/ https://home-assistant.io/components/device_tracker.icloud/
""" """
import logging import logging
import random
import os
import voluptuous as vol import voluptuous as vol
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME, from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
EVENT_HOMEASSISTANT_START) 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 from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.components.device_tracker import (ENTITY_ID_FORMAT, import homeassistant.util.dt as dt_util
PLATFORM_SCHEMA) from homeassistant.util.location import distance
from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pyicloud==0.9.1'] REQUIREMENTS = ['pyicloud==0.9.1']
CONF_INTERVAL = 'interval' CONF_IGNORED_DEVICES = 'ignored_devices'
KEEPALIVE_INTERVAL = 4 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): vol.Coerce(str), vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): vol.Coerce(str), vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_INTERVAL, default=8): vol.All(vol.Coerce(int), vol.Optional(ATTR_ACCOUNTNAME): cv.slugify,
vol.Range(min=1)) })
})
def setup_scanner(hass, config, see): def setup_scanner(hass, config: dict, see):
"""Setup the iCloud Scanner.""" """Set up the iCloud Scanner."""
from pyicloud import PyiCloudService username = config.get(CONF_USERNAME)
from pyicloud.exceptions import PyiCloudFailedLoginException password = config.get(CONF_PASSWORD)
from pyicloud.exceptions import PyiCloudNoDevicesException account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0]))
logging.getLogger("pyicloud.base").setLevel(logging.WARNING)
username = config[CONF_USERNAME] icloudaccount = Icloud(hass, username, password, account, see)
password = config[CONF_PASSWORD]
try: if icloudaccount.api is not None:
_LOGGER.info('Logging into iCloud Account') ICLOUDTRACKERS[account] = icloudaccount
# Attempt the login to iCloud
api = PyiCloudService(username, password, verify=True) else:
except PyiCloudFailedLoginException as error: _LOGGER.error("No ICLOUDTRACKERS added")
_LOGGER.exception('Error logging into iCloud Service: %s', error)
return False return False
def keep_alive(now): def lost_iphone(call):
"""Keep authenticating iCloud connection. """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 def update_icloud(call):
have to re-authenticate & this will send an email. """Call the update function of an icloud account."""
""" accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS)
api.authenticate() devicename = call.data.get(ATTR_DEVICENAME)
_LOGGER.info("Authenticate against iCloud") 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 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)
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)
def update_icloud(now):
"""Authenticate against iCloud and scan for devices."""
try: try:
keep_alive(None) self.api = PyiCloudService(
# Loop through every device registered with the iCloud account self.username, self.password,
for device in api.devices: cookie_directory=icloud_dir,
status = device.status() verify=True)
dev_id = slugify(status['name'].replace(' ', '', 99)) 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 # An entity will not be created by see() when track=false in
# 'known_devices.yaml', but we need to see() it at least once # 'known_devices.yaml', but we need to see() it at least once
entity = hass.states.get(ENTITY_ID_FORMAT.format(dev_id)) entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename))
if entity is None and dev_id in seen_devices: 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 continue
seen_devices[dev_id] = True
location = device.location() status = device.status(DEVICESTATUSSET)
# If the device has a location add it. If not do nothing 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: if location:
see( self.determine_interval(
dev_id=dev_id, devicename, location['latitude'],
host_name=status['name'], location['longitude'], battery)
gps=(location['latitude'], location['longitude']), interval = self._intervals.get(devicename, 1)
battery=status['batteryLevel']*100, attrs[ATTR_INTERVAL] = interval
gps_accuracy=location['horizontalAccuracy'] 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: except PyiCloudNoDevicesException:
_LOGGER.info('No iCloud Devices found!') _LOGGER.error('No iCloud Devices found!')
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, update_icloud) def lost_iphone(self, devicename):
"""Call the lost iphone function if the device is found."""
if self.api is None:
return
update_minutes = list(range(0, 60, config[CONF_INTERVAL])) self.api.authenticate()
# 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) for device in self.api.devices:
track_utc_time_change(hass, keep_alive, second=0, minute=keepalive_minutes) if devicename is None or device == self.devices[devicename]:
device.play_sound()
return True 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)

View File

@ -4,6 +4,8 @@ Support for the Locative platform.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.locative/ https://home-assistant.io/components/device_tracker.locative/
""" """
import asyncio
from functools import partial
import logging import logging
from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME
@ -19,7 +21,7 @@ DEPENDENCIES = ['http']
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
"""Setup an endpoint for the Locative application.""" """Setup an endpoint for the Locative application."""
hass.wsgi.register_view(LocativeView(hass, see)) hass.http.register_view(LocativeView(hass, see))
return True return True
@ -35,15 +37,23 @@ class LocativeView(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.see = see self.see = see
@asyncio.coroutine
def get(self, request): def get(self, request):
"""Locative message received as GET.""" """Locative message received as GET."""
return self.post(request) res = yield from self._handle(request.GET)
return res
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Locative message received.""" """Locative message received."""
# pylint: disable=too-many-return-statements data = yield from request.post()
data = request.values 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: if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.', return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY)
@ -68,7 +78,9 @@ class LocativeView(HomeAssistantView):
direction = data['trigger'] direction = data['trigger']
if direction == 'enter': 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) return 'Setting location to {}'.format(location_name)
elif direction == 'exit': elif direction == 'exit':
@ -76,7 +88,9 @@ class LocativeView(HomeAssistantView):
'{}.{}'.format(DOMAIN, device)) '{}.{}'.format(DOMAIN, device))
if current_state is None or current_state.state == location_name: 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' return 'Setting location to not home'
else: else:
# Ignore the message if it is telling us to exit a zone that we # Ignore the message if it is telling us to exit a zone that we

View File

@ -37,7 +37,6 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class LuciDeviceScanner(object): class LuciDeviceScanner(object):
"""This class queries a wireless router running OpenWrt firmware. """This class queries a wireless router running OpenWrt firmware.

View File

@ -18,16 +18,16 @@ from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOSTS from homeassistant.const import CONF_HOSTS
from homeassistant.util import Throttle from homeassistant.util import Throttle
# Return cached results if last scan was less then this time ago REQUIREMENTS = ['python-nmap==0.6.1']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_EXCLUDE = 'exclude'
# Interval in minutes to exclude devices from a scan while they are home # Interval in minutes to exclude devices from a scan while they are home
CONF_HOME_INTERVAL = 'home_interval' 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({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOSTS): cv.ensure_list, vol.Required(CONF_HOSTS): cv.ensure_list,
@ -73,7 +73,7 @@ class NmapDeviceScanner(object):
self.home_interval = timedelta(minutes=minutes) self.home_interval = timedelta(minutes=minutes)
self.success_init = self._update_info() self.success_init = self._update_info()
_LOGGER.info('nmap scanner initialized') _LOGGER.info("nmap scanner initialized")
def scan_devices(self): def scan_devices(self):
"""Scan for new devices and return a list with found device IDs.""" """Scan for new devices and return a list with found device IDs."""
@ -97,7 +97,7 @@ class NmapDeviceScanner(object):
Returns boolean if scanning successful. Returns boolean if scanning successful.
""" """
_LOGGER.info('Scanning') _LOGGER.info("Scanning...")
from nmap import PortScanner, PortScannerError from nmap import PortScanner, PortScannerError
scanner = PortScanner() scanner = PortScanner()
@ -138,5 +138,5 @@ class NmapDeviceScanner(object):
self.last_results = last_results self.last_results = last_results
_LOGGER.info('nmap scan successful') _LOGGER.info("nmap scan successful")
return True return True

View File

@ -114,10 +114,9 @@ def setup_scanner(hass, config, see):
'for topic %s.', topic) 'for topic %s.', topic)
return None return None
# pylint: disable=too-many-return-statements
def validate_payload(topic, payload, data_type): def validate_payload(topic, payload, data_type):
"""Validate the OwnTracks payload.""" """Validate the OwnTracks payload."""
# pylint: disable=too-many-return-statements
try: try:
data = json.loads(payload) data = json.loads(payload)
except ValueError: except ValueError:
@ -143,7 +142,7 @@ def setup_scanner(hass, config, see):
return data return data
if max_gps_accuracy is not None and \ if max_gps_accuracy is not None and \
convert(data.get('acc'), float, 0.0) > max_gps_accuracy: convert(data.get('acc'), float, 0.0) > max_gps_accuracy:
_LOGGER.warning('Ignoring %s update because expected GPS ' _LOGGER.info('Ignoring %s update because expected GPS '
'accuracy %s is not met: %s', 'accuracy %s is not met: %s',
data_type, max_gps_accuracy, payload) data_type, max_gps_accuracy, payload)
return None return None
@ -248,7 +247,7 @@ def setup_scanner(hass, config, see):
if (max_gps_accuracy is not None and if (max_gps_accuracy is not None and
data['acc'] > max_gps_accuracy): data['acc'] > max_gps_accuracy):
valid_gps = False valid_gps = False
_LOGGER.warning( _LOGGER.info(
'Ignoring GPS in region exit because expected ' 'Ignoring GPS in region exit because expected '
'GPS accuracy %s is not met: %s', 'GPS accuracy %s is not met: %s',
max_gps_accuracy, payload) max_gps_accuracy, payload)

View File

@ -31,3 +31,48 @@ see:
battery: battery:
description: Battery level of device description: Battery level of device
example: '100' 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'

View File

@ -49,7 +49,6 @@ def get_scanner(hass, config):
class SnmpScanner(object): class SnmpScanner(object):
"""Queries any SNMP capable Access Point for connected devices.""" """Queries any SNMP capable Access Point for connected devices."""
# pylint: disable=too-many-instance-attributes
def __init__(self, config): def __init__(self, config):
"""Initialize the scanner.""" """Initialize the scanner."""
from pysnmp.entity.rfc3413.oneliner import cmdgen from pysnmp.entity.rfc3413.oneliner import cmdgen

View File

@ -37,7 +37,6 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None return scanner if scanner.success_init else None
# pylint: disable=too-many-instance-attributes
class UbusDeviceScanner(object): class UbusDeviceScanner(object):
""" """
This class queries a wireless router running OpenWrt firmware. This class queries a wireless router running OpenWrt firmware.

View File

@ -7,9 +7,7 @@ https://home-assistant.io/components/device_tracker.volvooncall/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
from urllib.parse import urljoin
import voluptuous as vol import voluptuous as vol
import requests
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_utc_time 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__) _LOGGER = logging.getLogger(__name__)
SERVICE_URL = 'https://vocapi.wirelesscar.net/customerapi/rest/v3.0/' REQUIREMENTS = ['volvooncall==0.1.1']
HEADERS = {"X-Device-Id": "Device",
"X-OS-Type": "Android",
"X-Originator-Type": "App"}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
@ -40,62 +35,62 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
"""Validate the configuration and return a scanner.""" """Validate the configuration and return a scanner."""
session = requests.Session() from volvooncall import Connection
session.headers.update(HEADERS) connection = Connection(
session.auth = (config.get(CONF_USERNAME), config.get(CONF_USERNAME),
config.get(CONF_PASSWORD)) config.get(CONF_PASSWORD))
interval = max(MIN_TIME_BETWEEN_SCANS.seconds, interval = max(MIN_TIME_BETWEEN_SCANS.seconds,
config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL))
def query(ref, rel=SERVICE_URL): def _see_vehicle(vehicle):
"""Perform a query to the online service.""" position = vehicle["position"]
url = urljoin(rel, ref) dev_id = "volvo_" + slugify(vehicle["registrationNumber"])
_LOGGER.debug("Request for %s", url) host_name = "%s (%s/%s)" % (
res = session.get(url, timeout=15) vehicle["registrationNumber"],
res.raise_for_status() vehicle["vehicleType"],
_LOGGER.debug("Received %s", res.json()) vehicle["modelYear"])
return res.json()
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): def update(now):
"""Update status from the online service.""" """Update status from the online service."""
_LOGGER.info("Updating")
try: try:
_LOGGER.debug("Updating") res, vehicles = connection.update()
status = query("status", vehicle_url) if not res:
position = query("position", vehicle_url) _LOGGER.error("Could not query server")
see(dev_id=dev_id, return False
host_name=host_name,
gps=(position["position"]["latitude"], for vehicle in vehicles:
position["position"]["longitude"]), _see_vehicle(vehicle)
attributes=dict(
tank_volume=attributes["fuelTankVolume"], return True
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)
finally: finally:
track_point_in_utc_time(hass, update, track_point_in_utc_time(hass, update,
now + timedelta(seconds=interval)) now + timedelta(seconds=interval))
try:
_LOGGER.info('Logging in to service') _LOGGER.info('Logging in to service')
user = query("customeraccounts") return update(utcnow())
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

View File

@ -42,7 +42,6 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument,too-few-public-methods
def setup(hass, config): def setup(hass, config):
"""Set up the Digital Ocean component.""" """Set up the Digital Ocean component."""
conf = config[DOMAIN] conf = config[DOMAIN]

View File

@ -14,7 +14,7 @@ import voluptuous as vol
from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.const import EVENT_HOMEASSISTANT_START
from homeassistant.helpers.discovery import load_platform, discover from homeassistant.helpers.discovery import load_platform, discover
REQUIREMENTS = ['netdisco==0.7.2'] REQUIREMENTS = ['netdisco==0.7.5']
DOMAIN = 'discovery' DOMAIN = 'discovery'
@ -33,6 +33,7 @@ SERVICE_HANDLERS = {
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'), 'roku': ('media_player', 'roku'),
'sonos': ('media_player', 'sonos'), 'sonos': ('media_player', 'sonos'),
'yamaha': ('media_player', 'yamaha'),
'logitech_mediaserver': ('media_player', 'squeezebox'), 'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'), 'directv': ('media_player', 'directv'),
} }

View File

@ -38,7 +38,6 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-branches
def setup(hass, config): def setup(hass, config):
"""Listen for download events to download files.""" """Listen for download events to download files."""
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]

View File

@ -32,7 +32,6 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=too-many-locals
def setup(hass, config): def setup(hass, config):
"""Setup the Dweet.io component.""" """Setup the Dweet.io component."""
conf = config[DOMAIN] conf = config[DOMAIN]

View File

@ -86,7 +86,6 @@ def setup_ecobee(hass, network, config):
discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
# pylint: disable=too-few-public-methods
class EcobeeData(object): class EcobeeData(object):
"""Get the latest data and update the states.""" """Get the latest data and update the states."""

View File

@ -11,9 +11,7 @@ import voluptuous as vol
import requests import requests
from homeassistant.const import ( from homeassistant.const import (
CONF_API_KEY, CONF_WHITELIST, CONF_API_KEY, CONF_WHITELIST, CONF_URL, STATE_UNKNOWN, STATE_UNAVAILABLE,
CONF_URL, STATE_UNKNOWN,
STATE_UNAVAILABLE,
CONF_SCAN_INTERVAL) CONF_SCAN_INTERVAL)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import state as state_helper from homeassistant.helpers import state as state_helper
@ -22,8 +20,8 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = "emoncms_history" DOMAIN = 'emoncms_history'
CONF_INPUTNODE = "inputnode" CONF_INPUTNODE = 'inputnode'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -37,19 +35,18 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config): def setup(hass, config):
"""Setup the emoncms_history component.""" """Set up the Emoncms history component."""
conf = config[DOMAIN] conf = config[DOMAIN]
whitelist = conf.get(CONF_WHITELIST) whitelist = conf.get(CONF_WHITELIST)
def send_data(url, apikey, node, payload): def send_data(url, apikey, node, payload):
"""Send payload data to emoncms.""" """Send payload data to Emoncms."""
try: try:
fullurl = "{}/input/post.json".format(url) fullurl = '{}/input/post.json'.format(url)
req = requests.post(fullurl, data = {"apikey": apikey, "data": payload}
params={"node": node}, parameters = {"node": node}
data={"apikey": apikey, req = requests.post(
"data": payload}, fullurl, params=parameters, data=data, allow_redirects=True,
allow_redirects=True,
timeout=5) timeout=5)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
@ -63,14 +60,14 @@ def setup(hass, config):
fullurl, req.status_code) fullurl, req.status_code)
def update_emoncms(time): def update_emoncms(time):
"""Send whitelisted entities states reguarly to emoncms.""" """Send whitelisted entities states reguarly to Emoncms."""
payload_dict = {} payload_dict = {}
for entity_id in whitelist: for entity_id in whitelist:
state = hass.states.get(entity_id) state = hass.states.get(entity_id)
if state is None or state.state in ( if state is None or state.state in (
STATE_UNKNOWN, "", STATE_UNAVAILABLE): STATE_UNKNOWN, '', STATE_UNAVAILABLE):
continue continue
try: try:
@ -88,8 +85,7 @@ def setup(hass, config):
str(conf.get(CONF_INPUTNODE)), payload) str(conf.get(CONF_INPUTNODE)), payload)
track_point_in_time(hass, update_emoncms, time + track_point_in_time(hass, update_emoncms, time +
timedelta(seconds=conf.get( timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)))
CONF_SCAN_INTERVAL)))
update_emoncms(dt_util.utcnow()) update_emoncms(dt_util.utcnow())
return True return True

View File

@ -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 For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_hue/ https://home-assistant.io/components/emulated_hue/
""" """
import asyncio
import threading import threading
import socket import socket
import logging import logging
import json
import os import os
import select import select
from aiohttp import web
import voluptuous as vol import voluptuous as vol
from homeassistant import util, core from homeassistant import util, core
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, SERVICE_TURN_OFF, SERVICE_TURN_ON,
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
STATE_ON, HTTP_BAD_REQUEST STATE_ON, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
) )
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS ATTR_BRIGHTNESS, ATTR_SUPPORTED_FEATURES, SUPPORT_BRIGHTNESS
@ -25,8 +26,6 @@ from homeassistant.components.light import (
from homeassistant.components.http import ( from homeassistant.components.http import (
HomeAssistantView, HomeAssistantWSGI HomeAssistantView, HomeAssistantWSGI
) )
# pylint: disable=unused-import
from homeassistant.components.http import REQUIREMENTS # noqa
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = 'emulated_hue' DOMAIN = 'emulated_hue'
@ -87,24 +86,25 @@ def setup(hass, yaml_config):
upnp_listener = UPNPResponderThread( upnp_listener = UPNPResponderThread(
config.host_ip_addr, config.listen_port) config.host_ip_addr, config.listen_port)
def start_emulated_hue_bridge(event): @asyncio.coroutine
"""Start the emulated hue bridge."""
server.start()
upnp_listener.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge)
def stop_emulated_hue_bridge(event): def stop_emulated_hue_bridge(event):
"""Stop the emulated hue bridge.""" """Stop the emulated hue bridge."""
upnp_listener.stop() 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 return True
# pylint: disable=too-few-public-methods
class Config(object): class Config(object):
"""Holds configuration variables for the emulated hue bridge.""" """Holds configuration variables for the emulated hue bridge."""
@ -158,6 +158,7 @@ class DescriptionXmlView(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.config = config self.config = config
@core.callback
def get(self, request): def get(self, request):
"""Handle a GET request.""" """Handle a GET request."""
xml_template = """<?xml version="1.0" encoding="UTF-8" ?> xml_template = """<?xml version="1.0" encoding="UTF-8" ?>
@ -185,7 +186,7 @@ class DescriptionXmlView(HomeAssistantView):
resp_text = xml_template.format( resp_text = xml_template.format(
self.config.host_ip_addr, self.config.listen_port) 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): class HueUsernameView(HomeAssistantView):
@ -200,9 +201,13 @@ class HueUsernameView(HomeAssistantView):
"""Initialize the instance of the view.""" """Initialize the instance of the view."""
super().__init__(hass) super().__init__(hass)
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Handle a POST 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: if 'devicetype' not in data:
return self.json_message('devicetype not specified', return self.json_message('devicetype not specified',
@ -214,10 +219,10 @@ class HueUsernameView(HomeAssistantView):
class HueLightsView(HomeAssistantView): class HueLightsView(HomeAssistantView):
"""Handle requests for getting and setting info about entities.""" """Handle requests for getting and setting info about entities."""
url = '/api/<username>/lights' url = '/api/{username}/lights'
name = 'api:username:lights' name = 'api:username:lights'
extra_urls = ['/api/<username>/lights/<entity_id>', extra_urls = ['/api/{username}/lights/{entity_id}',
'/api/<username>/lights/<entity_id>/state'] '/api/{username}/lights/{entity_id}/state']
requires_auth = False requires_auth = False
def __init__(self, hass, config): def __init__(self, hass, config):
@ -226,58 +231,51 @@ class HueLightsView(HomeAssistantView):
self.config = config self.config = config
self.cached_states = {} self.cached_states = {}
@core.callback
def get(self, request, username, entity_id=None): def get(self, request, username, entity_id=None):
"""Handle a GET request.""" """Handle a GET request."""
if entity_id is None: if entity_id is None:
return self.get_lights_list() return self.async_get_lights_list()
if not request.base_url.endswith('state'): if not request.path.endswith('state'):
return self.get_light_state(entity_id) 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): def put(self, request, username, entity_id=None):
"""Handle a PUT request.""" """Handle a PUT request."""
if not request.base_url.endswith('state'): if not request.path.endswith('state'):
return self.Response("Method not allowed", status=405) return web.Response(text="Method not allowed", status=405)
content_type = request.environ.get('CONTENT_TYPE', '') if entity_id and self.hass.states.get(entity_id) is None:
if content_type == 'application/x-www-form-urlencoded': return self.json_message('Entity not found', HTTP_NOT_FOUND)
# 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
for key in request.form:
try: try:
json_data = json.loads(key) json_data = yield from request.json()
break
except ValueError: except ValueError:
# Try the next key? return self.json_message('Invalid JSON', HTTP_BAD_REQUEST)
pass
if json_data is None: result = yield from self.async_put_light_state(json_data, entity_id)
return self.Response("Bad request", status=400) return result
else:
json_data = request.json
return self.put_light_state(json_data, entity_id) @core.callback
def async_get_lights_list(self):
def get_lights_list(self):
"""Process a request to get the list of available lights.""" """Process a request to get the list of available lights."""
json_response = {} json_response = {}
for entity in self.hass.states.all(): for entity in self.hass.states.async_all():
if self.is_entity_exposed(entity): if self.is_entity_exposed(entity):
json_response[entity.entity_id] = entity_to_json(entity) json_response[entity.entity_id] = entity_to_json(entity)
return self.json(json_response) 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.""" """Process a request to get the state of an individual light."""
entity = self.hass.states.get(entity_id) entity = self.hass.states.get(entity_id)
if entity is None or not self.is_entity_exposed(entity): 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) cached_state = self.cached_states.get(entity_id, None)
@ -292,23 +290,24 @@ class HueLightsView(HomeAssistantView):
return self.json(json_response) 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.""" """Process a request to set the state of an individual light."""
config = self.config config = self.config
# Retrieve the entity from the state machine # Retrieve the entity from the state machine
entity = self.hass.states.get(entity_id) entity = self.hass.states.get(entity_id)
if entity is None: 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): 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 # Parse the request into requested "on" status and brightness
parsed = parse_hue_api_put_light_body(request_json, entity) parsed = parse_hue_api_put_light_body(request_json, entity)
if parsed is None: if parsed is None:
return self.Response("Bad request", status=400) return web.Response(text="Bad request", status=400)
result, brightness = parsed result, brightness = parsed
@ -333,7 +332,8 @@ class HueLightsView(HomeAssistantView):
self.cached_states[entity_id] = (result, brightness) self.cached_states[entity_id] = (result, brightness)
# Perform the requested action # 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 = \ json_response = \
[create_hue_success_response(entity_id, HUE_API_STATE_ON, result)] [create_hue_success_response(entity_id, HUE_API_STATE_ON, result)]
@ -345,7 +345,10 @@ class HueLightsView(HomeAssistantView):
return self.json(json_response) return self.json(json_response)
def is_entity_exposed(self, entity): 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 config = self.config
if entity.attributes.get('view') is not None: if entity.attributes.get('view') is not None:

View File

@ -56,14 +56,14 @@ class EnOceanDongle:
"""Send a command from the EnOcean dongle.""" """Send a command from the EnOcean dongle."""
self.__communicator.send(command) 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.""" """Combine list of integer values to one big integer."""
output = 0x00 output = 0x00
for i, j in enumerate(reversed(data)): for i, j in enumerate(reversed(data)):
output |= (j << i * 8) output |= (j << i * 8)
return output return output
# pylint: disable=too-many-branches
def callback(self, temp): def callback(self, temp):
"""Callback function for EnOcean Device. """Callback function for EnOcean Device.
@ -112,7 +112,6 @@ class EnOceanDongle:
device.value_changed(value) device.value_changed(value)
# pylint: disable=too-few-public-methods
class EnOceanDevice(): class EnOceanDevice():
"""Parent class for all devices associated with the EnOcean component.""" """Parent class for all devices associated with the EnOcean component."""

View File

@ -77,8 +77,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
# pylint: disable=unused-argument, too-many-function-args, too-many-locals # pylint: disable=unused-argument
# pylint: disable=too-many-return-statements
def setup(hass, base_config): def setup(hass, base_config):
"""Common setup for Envisalink devices.""" """Common setup for Envisalink devices."""
from pyenvisalink import EnvisalinkAlarmPanel from pyenvisalink import EnvisalinkAlarmPanel

View File

@ -86,7 +86,6 @@ def is_on(hass, entity_id: str=None) -> bool:
return state.attributes[ATTR_SPEED] not in [SPEED_OFF, STATE_UNKNOWN] 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: def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
"""Turn all or specified fan on.""" """Turn all or specified fan on."""
data = { 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) 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: def setup(hass, config: dict) -> None:
"""Expose fan control via statemachine and services.""" """Expose fan control via statemachine and services."""
component = EntityComponent( component = EntityComponent(
@ -198,7 +196,7 @@ def setup(hass, config: dict) -> None:
class FanEntity(ToggleEntity): class FanEntity(ToggleEntity):
"""Representation of a fan.""" """Representation of a fan."""
# pylint: disable=no-self-use, abstract-method # pylint: disable=no-self-use
def set_speed(self: ToggleEntity, speed: str) -> None: def set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan.""" """Set the speed of the fan."""

View File

@ -110,11 +110,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
)]) )])
# pylint: disable=too-many-instance-attributes
class MqttFan(FanEntity): class MqttFan(FanEntity):
"""A MQTT fan component.""" """A MQTT fan component."""
# pylint: disable=too-many-arguments
def __init__(self, hass, name, topic, templates, qos, retain, payload, def __init__(self, hass, name, topic, templates, qos, retain, payload,
speed_list, optimistic): speed_list, optimistic):
"""Initialize the MQTT fan.""" """Initialize the MQTT fan."""

View File

@ -44,7 +44,6 @@ def setup(hass, config):
return len(feeds) > 0 return len(feeds) > 0
# pylint: disable=too-few-public-methods
class FeedManager(object): class FeedManager(object):
"""Abstraction over feedparser module.""" """Abstraction over feedparser module."""

View File

@ -4,14 +4,16 @@ Component that will help set the ffmpeg component.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ffmpeg/ https://home-assistant.io/components/ffmpeg/
""" """
import asyncio
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util.async import run_coroutine_threadsafe
DOMAIN = 'ffmpeg' DOMAIN = 'ffmpeg'
REQUIREMENTS = ["ha-ffmpeg==0.13"] REQUIREMENTS = ["ha-ffmpeg==0.15"]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -47,13 +49,26 @@ def setup(hass, config):
def get_binary(): def get_binary():
"""Return ffmpeg binary from config.""" """Return ffmpeg binary from config.
Async friendly.
"""
return FFMPEG_CONFIG.get(CONF_FFMPEG_BIN) 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.""" """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 FFMPEG_CONFIG.get(CONF_RUN_TEST):
# if in cache # if in cache
@ -61,8 +76,9 @@ def run_test(input_source):
return FFMPEG_TEST_CACHE[input_source] return FFMPEG_TEST_CACHE[input_source]
# run test # run test
test = Test(get_binary()) ffmpeg_test = TestAsync(get_binary(), loop=hass.loop)
if not test.run_test(input_source): success = yield from ffmpeg_test.run_test(input_source)
if not success:
_LOGGER.error("FFmpeg '%s' test fails!", input_source) _LOGGER.error("FFmpeg '%s' test fails!", input_source)
FFMPEG_TEST_CACHE[input_source] = False FFMPEG_TEST_CACHE[input_source] = False
return False return False

View File

@ -4,14 +4,14 @@ Allows utilizing the Foursquare (Swarm) API.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/foursquare/ https://home-assistant.io/components/foursquare/
""" """
import asyncio
import logging import logging
import os import os
import json
import requests import requests
import voluptuous as vol 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 from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
@ -75,7 +75,7 @@ def setup(hass, config):
descriptions[DOMAIN][SERVICE_CHECKIN], descriptions[DOMAIN][SERVICE_CHECKIN],
schema=CHECKIN_SERVICE_SCHEMA) schema=CHECKIN_SERVICE_SCHEMA)
hass.wsgi.register_view(FoursquarePushReceiver( hass.http.register_view(FoursquarePushReceiver(
hass, config[CONF_PUSH_SECRET])) hass, config[CONF_PUSH_SECRET]))
return True return True
@ -93,16 +93,21 @@ class FoursquarePushReceiver(HomeAssistantView):
super().__init__(hass) super().__init__(hass)
self.push_secret = push_secret self.push_secret = push_secret
@asyncio.coroutine
def post(self, request): def post(self, request):
"""Accept the POST from Foursquare.""" """Accept the POST from Foursquare."""
raw_data = request.form try:
_LOGGER.debug("Received Foursquare push: %s", raw_data) data = yield from request.json()
if self.push_secret != raw_data["secret"]: 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" _LOGGER.error("Received Foursquare push with invalid"
"push secret! Data: %s", raw_data) "push secret: %s", secret)
return return self.json_message('Incorrect secret', HTTP_BAD_REQUEST)
parsed_payload = {
key: json.loads(val) for key, val in raw_data.items() self.hass.bus.async_fire(EVENT_PUSH, data)
if key != "secret"
}
self.hass.bus.fire(EVENT_PUSH, parsed_payload)

View File

@ -1,10 +1,15 @@
"""Handle the frontend for Home Assistant.""" """Handle the frontend for Home Assistant."""
import asyncio
import hashlib import hashlib
import json
import logging import logging
import os import os
from homeassistant.const import EVENT_HOMEASSISTANT_START from aiohttp import web
from homeassistant.components import api
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 homeassistant.components.http import HomeAssistantView
from .version import FINGERPRINTS from .version import FINGERPRINTS
@ -22,7 +27,6 @@ MANIFEST_JSON = {
"icons": [], "icons": [],
"lang": "en-US", "lang": "en-US",
"name": "Home Assistant", "name": "Home Assistant",
"orientation": "any",
"short_name": "Assistant", "short_name": "Assistant",
"start_url": "/", "start_url": "/",
"theme_color": "#03A9F4" "theme_color": "#03A9F4"
@ -36,10 +40,9 @@ _LOGGER = logging.getLogger(__name__)
def register_built_in_panel(hass, component_name, sidebar_title=None, def register_built_in_panel(hass, component_name, sidebar_title=None,
sidebar_icon=None, url_path=None, config=None): sidebar_icon=None, url_path=None, config=None):
"""Register a built-in panel.""" """Register a built-in panel."""
# pylint: disable=too-many-arguments
path = 'panels/ha-panel-{}.html'.format(component_name) path = 'panels/ha-panel-{}.html'.format(component_name)
if hass.wsgi.development: if hass.http.development:
url = ('/static/home-assistant-polymer/panels/' url = ('/static/home-assistant-polymer/panels/'
'{0}/ha-panel-{0}.html'.format(component_name)) '{0}/ha-panel-{0}.html'.format(component_name))
else: 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. Warning: this API will probably change. Use at own risk.
""" """
# pylint: disable=too-many-arguments
if url_path is None: if url_path is None:
url_path = component_name 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) url = URL_PANEL_COMPONENT.format(component_name)
if url not in _REGISTERED_COMPONENTS: if url not in _REGISTERED_COMPONENTS:
hass.wsgi.register_static_path(url, path) hass.http.register_static_path(url, path)
_REGISTERED_COMPONENTS.add(url) _REGISTERED_COMPONENTS.add(url)
fprinted_url = URL_PANEL_COMPONENT_FP.format(component_name, md5) 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): def setup(hass, config):
"""Setup serving the frontend.""" """Setup serving the frontend."""
hass.wsgi.register_view(BootstrapView) hass.http.register_view(BootstrapView)
hass.wsgi.register_view(ManifestJSONView) hass.http.register_view(ManifestJSONView)
if hass.wsgi.development: if hass.http.development:
sw_path = "home-assistant-polymer/build/service_worker.js" sw_path = "home-assistant-polymer/build/service_worker.js"
else: else:
sw_path = "service_worker.js" 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) 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")) os.path.join(STATIC_PATH, "robots.txt"))
hass.wsgi.register_static_path("/static", STATIC_PATH) hass.http.register_static_path("/static", STATIC_PATH)
hass.wsgi.register_static_path("/local", hass.config.path('www'))
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') 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. 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, ['/{}'.format(name) for name in PANELS]))
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, register_frontend_index)
@ -161,13 +166,14 @@ class BootstrapView(HomeAssistantView):
url = "/api/bootstrap" url = "/api/bootstrap"
name = "api:bootstrap" name = "api:bootstrap"
@callback
def get(self, request): def get(self, request):
"""Return all data needed to bootstrap Home Assistant.""" """Return all data needed to bootstrap Home Assistant."""
return self.json({ return self.json({
'config': self.hass.config.as_dict(), 'config': self.hass.config.as_dict(),
'states': self.hass.states.all(), 'states': self.hass.states.async_all(),
'events': api.events_json(self.hass), 'events': api.async_events_json(self.hass),
'services': api.services_json(self.hass), 'services': api.async_services_json(self.hass),
'panels': PANELS, 'panels': PANELS,
}) })
@ -178,7 +184,7 @@ class IndexView(HomeAssistantView):
url = '/' url = '/'
name = "frontend:index" name = "frontend:index"
requires_auth = False requires_auth = False
extra_urls = ['/states', '/states/<entity:entity_id>'] extra_urls = ['/states', '/states/{entity_id}']
def __init__(self, hass, extra_urls): def __init__(self, hass, extra_urls):
"""Initialize the frontend view.""" """Initialize the frontend view."""
@ -193,9 +199,17 @@ class IndexView(HomeAssistantView):
) )
) )
@asyncio.coroutine
def get(self, request, entity_id=None): def get(self, request, entity_id=None):
"""Serve the index view.""" """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' core_url = '/static/home-assistant-polymer/build/core.js'
ui_url = '/static/home-assistant-polymer/src/home-assistant.html' ui_url = '/static/home-assistant-polymer/src/home-assistant.html'
else: else:
@ -215,22 +229,24 @@ class IndexView(HomeAssistantView):
if self.hass.config.api.api_password: if self.hass.config.api.api_password:
# require password if set # require password if set
no_auth = 'false' no_auth = 'false'
if self.hass.wsgi.is_trusted_ip( if self.hass.http.is_trusted_ip(
self.hass.wsgi.get_real_ip(request)): self.hass.http.get_real_ip(request)):
# bypass for trusted networks # bypass for trusted networks
no_auth = 'true' no_auth = 'true'
icons_url = '/static/mdi-{}.html'.format(FINGERPRINTS['mdi.html']) 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 is wrong
# pylint: disable=no-member # pylint: disable=no-member
# This is a jinja2 template, not a HA template so we call 'render'.
resp = template.render( resp = template.render(
core_url=core_url, ui_url=ui_url, no_auth=no_auth, core_url=core_url, ui_url=ui_url, no_auth=no_auth,
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
panel_url=panel_url, panels=PANELS) 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): class ManifestJSONView(HomeAssistantView):
@ -240,8 +256,8 @@ class ManifestJSONView(HomeAssistantView):
url = "/manifest.json" url = "/manifest.json"
name = "manifestjson" name = "manifestjson"
def get(self, request): @asyncio.coroutine
def get(self, request): # pylint: disable=no-self-use
"""Return the manifest.json.""" """Return the manifest.json."""
import json
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8') 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")

View File

@ -2,14 +2,14 @@
FINGERPRINTS = { FINGERPRINTS = {
"core.js": "5ed5e063d66eb252b5b288738c9c2d16", "core.js": "5ed5e063d66eb252b5b288738c9c2d16",
"frontend.html": "0a4c2c6e86a0a78c2ff3e03842de609d", "frontend.html": "78be2dfedc4e95326cbcd9401fb17b4d",
"mdi.html": "46a76f877ac9848899b8ed382427c16f", "mdi.html": "46a76f877ac9848899b8ed382427c16f",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
"panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002", "panels/ha-panel-dev-event.html": "550bf85345c454274a40d15b2795a002",
"panels/ha-panel-dev-info.html": "ec613406ce7e20d93754233d55625c8a", "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-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-history.html": "efe1bcdd7733b09e55f4f965d171c295",
"panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab", "panels/ha-panel-iframe.html": "d920f0aa3c903680f2f8795e2255daab",
"panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40", "panels/ha-panel-logbook.html": "66108d82763359a218c9695f0553de40",

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit f3081ed48fd11fa89586701dba3792d028473a15 Subproject commit 896e0427675bb99348de6f1453bd6f8cf48b5c6f

Some files were not shown because too many files have changed in this diff Show More