Merge pull request #9969 from home-assistant/release-0-56

0.56
This commit is contained in:
Fabian Affolter 2017-10-22 00:37:23 +02:00 committed by GitHub
commit ead4e44cd6
223 changed files with 7351 additions and 1809 deletions

View File

@ -170,6 +170,9 @@ omit =
homeassistant/components/scsgate.py homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py homeassistant/components/*/scsgate.py
homeassistant/components/skybell.py
homeassistant/components/*/skybell.py
homeassistant/components/tado.py homeassistant/components/tado.py
homeassistant/components/*/tado.py homeassistant/components/*/tado.py
@ -187,6 +190,9 @@ omit =
homeassistant/components/*/thinkingcleaner.py homeassistant/components/*/thinkingcleaner.py
homeassistant/components/toon.py
homeassistant/components/*/toon.py
homeassistant/components/tradfri.py homeassistant/components/tradfri.py
homeassistant/components/*/tradfri.py homeassistant/components/*/tradfri.py
@ -217,7 +223,7 @@ omit =
homeassistant/components/wemo.py homeassistant/components/wemo.py
homeassistant/components/*/wemo.py homeassistant/components/*/wemo.py
homeassistant/components/wink.py homeassistant/components/wink/*
homeassistant/components/*/wink.py homeassistant/components/*/wink.py
homeassistant/components/xiaomi_aqara.py homeassistant/components/xiaomi_aqara.py
@ -267,6 +273,7 @@ omit =
homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/rpi_camera.py
homeassistant/components/camera/onvif.py homeassistant/components/camera/onvif.py
homeassistant/components/camera/synology.py homeassistant/components/camera/synology.py
homeassistant/components/camera/yi.py
homeassistant/components/climate/eq3btsmart.py homeassistant/components/climate/eq3btsmart.py
homeassistant/components/climate/flexit.py homeassistant/components/climate/flexit.py
homeassistant/components/climate/heatmiser.py homeassistant/components/climate/heatmiser.py
@ -405,7 +412,7 @@ omit =
homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/aws_sqs.py
homeassistant/components/notify/ciscospark.py homeassistant/components/notify/ciscospark.py
homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend.py
homeassistant/components/notify/clicksendaudio.py homeassistant/components/notify/clicksend_tts.py
homeassistant/components/notify/discord.py homeassistant/components/notify/discord.py
homeassistant/components/notify/facebook.py homeassistant/components/notify/facebook.py
homeassistant/components/notify/free_mobile.py homeassistant/components/notify/free_mobile.py
@ -427,6 +434,7 @@ omit =
homeassistant/components/notify/pushover.py homeassistant/components/notify/pushover.py
homeassistant/components/notify/pushsafer.py homeassistant/components/notify/pushsafer.py
homeassistant/components/notify/rest.py homeassistant/components/notify/rest.py
homeassistant/components/notify/rocketchat.py
homeassistant/components/notify/sendgrid.py homeassistant/components/notify/sendgrid.py
homeassistant/components/notify/simplepush.py homeassistant/components/notify/simplepush.py
homeassistant/components/notify/slack.py homeassistant/components/notify/slack.py
@ -529,6 +537,7 @@ omit =
homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py
homeassistant/components/sensor/serial.py
homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial_pm.py
homeassistant/components/sensor/shodan.py homeassistant/components/sensor/shodan.py
homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/skybeacon.py
@ -549,6 +558,7 @@ omit =
homeassistant/components/sensor/time_date.py homeassistant/components/sensor/time_date.py
homeassistant/components/sensor/torque.py homeassistant/components/sensor/torque.py
homeassistant/components/sensor/transmission.py homeassistant/components/sensor/transmission.py
homeassistant/components/sensor/travisci.py
homeassistant/components/sensor/twitch.py homeassistant/components/sensor/twitch.py
homeassistant/components/sensor/uber.py homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py homeassistant/components/sensor/upnp.py
@ -585,6 +595,7 @@ omit =
homeassistant/components/switch/telnet.py homeassistant/components/switch/telnet.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/switch/xiaomi_miio.py
homeassistant/components/telegram_bot/* homeassistant/components/telegram_bot/*
homeassistant/components/thingspeak.py homeassistant/components/thingspeak.py
homeassistant/components/tts/amazon_polly.py homeassistant/components/tts/amazon_polly.py

View File

@ -29,6 +29,9 @@ homeassistant/components/weblink.py @home-assistant/core
homeassistant/components/websocket_api.py @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core
homeassistant/components/zone.py @home-assistant/core homeassistant/components/zone.py @home-assistant/core
# To monitor non-pypi additions
requirements_all.txt @andrey-git
Dockerfile @home-assistant/docker Dockerfile @home-assistant/docker
virtualization/Docker/* @home-assistant/docker virtualization/Docker/* @home-assistant/docker
@ -36,10 +39,28 @@ homeassistant/components/zwave/* @home-assistant/z-wave
homeassistant/components/*/zwave.py @home-assistant/z-wave homeassistant/components/*/zwave.py @home-assistant/z-wave
# Indiviudal components # Indiviudal components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/eq3btsmart.py @rytilahti
homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/media_player/kodi.py @armills homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/miflora.py @danielhiversen
homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/climate/eq3btsmart.py @rytilahti
homeassistant/components/*/axis.py @Kane610
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/tesla.py @zabuldon
homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/*/xiaomi_aqara.py @danielhiversen
homeassistant/components/*/xiaomi_miio.py @rytilahti homeassistant/components/*/xiaomi_miio.py @rytilahti

View File

@ -11,10 +11,8 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
#ENV INSTALL_FFMPEG no #ENV INSTALL_FFMPEG no
#ENV INSTALL_LIBCEC no #ENV INSTALL_LIBCEC no
#ENV INSTALL_PHANTOMJS no #ENV INSTALL_PHANTOMJS no
#ENV INSTALL_COAP no
#ENV INSTALL_SSOCR no #ENV INSTALL_SSOCR no
VOLUME /config VOLUME /config
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
@ -26,10 +24,10 @@ RUN virtualization/Docker/setup_docker_prereqs
# Install hass component dependencies # Install hass component dependencies
COPY requirements_all.txt requirements_all.txt COPY requirements_all.txt requirements_all.txt
# Uninstall enum34 because some depenndecies install it but breaks Python 3.4+. # Uninstall enum34 because some dependencies install it but breaks Python 3.4+.
# See PR #8103 for more info. # See PR #8103 for more info.
RUN pip3 install --no-cache-dir -r requirements_all.txt && \ RUN pip3 install --no-cache-dir -r requirements_all.txt && \
pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet pip3 install --no-cache-dir mysqlclient psycopg2 uvloop cchardet cython
# Copy source # Copy source
COPY . . COPY . .

View File

@ -11,13 +11,11 @@ from typing import Any, Optional, Dict
import voluptuous as vol import voluptuous as vol
import homeassistant.components as core_components from homeassistant import (
core, config as conf_util, loader, components as core_components)
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
import homeassistant.config as conf_util
import homeassistant.core as core
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
import homeassistant.loader as loader
from homeassistant.util.logging import AsyncHandler from homeassistant.util.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, get_user_site from homeassistant.util.package import async_get_user_site, get_user_site
from homeassistant.util.yaml import clear_secret_cache from homeassistant.util.yaml import clear_secret_cache

View File

@ -10,6 +10,7 @@ Component design guidelines:
import asyncio import asyncio
import itertools as it import itertools as it
import logging import logging
import os
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.config as conf_util import homeassistant.config as conf_util
@ -110,6 +111,11 @@ def async_reload_core_config(hass):
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Set up general services related to Home Assistant.""" """Set up general services related to Home Assistant."""
descriptions = yield from hass.async_add_job(
conf_util.load_yaml_config_file, os.path.join(
os.path.dirname(__file__), 'services.yaml')
)
@asyncio.coroutine @asyncio.coroutine
def async_handle_turn_service(service): def async_handle_turn_service(service):
"""Handle calls to homeassistant.turn_on/off.""" """Handle calls to homeassistant.turn_on/off."""
@ -149,11 +155,14 @@ def async_setup(hass, config):
yield from asyncio.wait(tasks, loop=hass.loop) yield from asyncio.wait(tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service) ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service,
descriptions[ha.DOMAIN][SERVICE_TURN_OFF])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service) ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service,
descriptions[ha.DOMAIN][SERVICE_TURN_ON])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service) ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service,
descriptions[ha.DOMAIN][SERVICE_TOGGLE])
@asyncio.coroutine @asyncio.coroutine
def async_handle_core_service(call): def async_handle_core_service(call):
@ -178,11 +187,14 @@ def async_setup(hass, config):
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE)) hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service,
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_STOP])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service,
descriptions[ha.DOMAIN][SERVICE_HOMEASSISTANT_RESTART])
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service,
descriptions[ha.DOMAIN][SERVICE_CHECK_CONFIG])
@asyncio.coroutine @asyncio.coroutine
def async_handle_reload_config(call): def async_handle_reload_config(call):
@ -197,6 +209,7 @@ def async_setup(hass, config):
hass, conf.get(ha.DOMAIN) or {}) hass, conf.get(ha.DOMAIN) or {})
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config) ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config,
descriptions[ha.DOMAIN][SERVICE_RELOAD_CORE_CONFIG])
return True return True

View File

@ -10,24 +10,23 @@ from functools import partial
from os import path from os import path
import voluptuous as vol import voluptuous as vol
from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.config import load_yaml_config_file
from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME,
ATTR_ENTITY_ID, CONF_USERNAME, CONF_PASSWORD,
CONF_EXCLUDE, CONF_NAME,
EVENT_HOMEASSISTANT_STOP,
EVENT_HOMEASSISTANT_START)
REQUIREMENTS = ['abodepy==0.11.9'] from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from requests.exceptions import HTTPError, ConnectTimeout
REQUIREMENTS = ['abodepy==0.12.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_ATTRIBUTION = "Data provided by goabode.com" CONF_ATTRIBUTION = "Data provided by goabode.com"
CONF_LIGHTS = "lights" CONF_POLLING = 'polling'
CONF_POLLING = "polling"
DOMAIN = 'abode' DOMAIN = 'abode'
@ -93,9 +92,8 @@ class AbodeSystem(object):
def __init__(self, username, password, name, polling, exclude, lights): def __init__(self, username, password, name, polling, exclude, lights):
"""Initialize the system.""" """Initialize the system."""
import abodepy import abodepy
self.abode = abodepy.Abode(username, password, self.abode = abodepy.Abode(
auto_login=True, username, password, auto_login=True, get_devices=True,
get_devices=True,
get_automations=True) get_automations=True)
self.name = name self.name = name
self.polling = polling self.polling = polling
@ -210,7 +208,7 @@ def setup_hass_services(hass):
def setup_hass_events(hass): def setup_hass_events(hass):
"""Home assistant start and stop callbacks.""" """Home Assistant start and stop callbacks."""
def startup(event): def startup(event):
"""Listen for push events.""" """Listen for push events."""
hass.data[DOMAIN].abode.events.start() hass.data[DOMAIN].abode.events.start()

View File

@ -124,20 +124,13 @@ def async_setup(hass, config):
method = "async_{}".format(SERVICE_TO_METHOD[service.service]) method = "async_{}".format(SERVICE_TO_METHOD[service.service])
update_tasks = []
for alarm in target_alarms: for alarm in target_alarms:
yield from getattr(alarm, method)(code) yield from getattr(alarm, method)(code)
update_tasks = []
for alarm in target_alarms:
if not alarm.should_poll: if not alarm.should_poll:
continue continue
update_tasks.append(alarm.async_update_ha_state(True))
update_coro = hass.async_add_job(
alarm.async_update_ha_state(True))
if hasattr(alarm, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -0,0 +1,121 @@
"""
Support for Arlo Alarm Control Panels.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.arlo/
"""
import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.alarm_control_panel import (
AlarmControlPanel, PLATFORM_SCHEMA)
from homeassistant.components.arlo import (DATA_ARLO, CONF_ATTRIBUTION)
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
_LOGGER = logging.getLogger(__name__)
ARMED = 'armed'
CONF_HOME_MODE_NAME = 'home_mode_name'
DEPENDENCIES = ['arlo']
DISARMED = 'disarmed'
ICON = 'mdi:security'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Arlo Alarm Control Panels."""
data = hass.data[DATA_ARLO]
if not data.base_stations:
return
home_mode_name = config.get(CONF_HOME_MODE_NAME)
base_stations = []
for base_station in data.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name))
async_add_devices(base_stations, True)
class ArloBaseStation(AlarmControlPanel):
"""Representation of an Arlo Alarm Control Panel."""
def __init__(self, data, home_mode_name):
"""Initialize the alarm control panel."""
self._base_station = data
self._home_mode_name = home_mode_name
self._state = None
@property
def icon(self):
"""Return icon."""
return ICON
@property
def state(self):
"""Return the state of the device."""
return self._state
def update(self):
"""Update the state of the device."""
# PyArlo sometimes returns None for mode. So retry 3 times before
# returning None.
num_retries = 3
i = 0
while i < num_retries:
mode = self._base_station.mode
if mode:
self._state = self._get_state_from_mode(mode)
return
i += 1
self._state = None
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
"""Send disarm command."""
self._base_station.mode = DISARMED
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
self._base_station.mode = ARMED
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
"""Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name
@property
def name(self):
"""Return the name of the base station."""
return self._base_station.name
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'device_id': self._base_station.device_id
}
def _get_state_from_mode(self, mode):
"""Convert Arlo mode to Home Assistant state."""
if mode == ARMED:
return STATE_ALARM_ARMED_AWAY
elif mode == DISARMED:
return STATE_ALARM_DISARMED
elif mode == self._home_mode_name:
return STATE_ALARM_ARMED_HOME
return None

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
REQUIREMENTS = ['pythonegardia==1.0.21'] REQUIREMENTS = ['pythonegardia==1.0.22']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -11,19 +11,18 @@ from homeassistant.util.decorator import Registry
HANDLERS = Registry() HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_HEADER = 'header' API_DIRECTIVE = 'directive'
ATTR_NAME = 'name' API_EVENT = 'event'
ATTR_NAMESPACE = 'namespace' API_HEADER = 'header'
ATTR_MESSAGE_ID = 'messageId' API_PAYLOAD = 'payload'
ATTR_PAYLOAD = 'payload' API_ENDPOINT = 'endpoint'
ATTR_PAYLOAD_VERSION = 'payloadVersion'
MAPPING_COMPONENT = { MAPPING_COMPONENT = {
switch.DOMAIN: ['SWITCH', ('turnOff', 'turnOn'), None], switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
light.DOMAIN: [ light.DOMAIN: [
'LIGHT', ('turnOff', 'turnOn'), { 'LIGHT', ('Alexa.PowerController',), {
light.SUPPORT_BRIGHTNESS: 'setPercentage' light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController'
} }
], ],
} }
@ -32,51 +31,75 @@ MAPPING_COMPONENT = {
@asyncio.coroutine @asyncio.coroutine
def async_handle_message(hass, message): def async_handle_message(hass, message):
"""Handle incoming API messages.""" """Handle incoming API messages."""
assert int(message[ATTR_HEADER][ATTR_PAYLOAD_VERSION]) == 2 assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
# Read head data
message = message[API_DIRECTIVE]
namespace = message[API_HEADER]['namespace']
name = message[API_HEADER]['name']
# Do we support this API request? # Do we support this API request?
funct_ref = HANDLERS.get(message[ATTR_HEADER][ATTR_NAME]) funct_ref = HANDLERS.get((namespace, name))
if not funct_ref: if not funct_ref:
_LOGGER.warning( _LOGGER.warning(
"Unsupported API request %s", message[ATTR_HEADER][ATTR_NAME]) "Unsupported API request %s/%s", namespace, name)
return api_error(message) return api_error(message)
return (yield from funct_ref(hass, message)) return (yield from funct_ref(hass, message))
def api_message(name, namespace, payload=None): def api_message(request, name='Response', namespace='Alexa', payload=None):
"""Create a API formatted response message. """Create a API formatted response message.
Async friendly. Async friendly.
""" """
payload = payload or {} payload = payload or {}
return {
ATTR_HEADER: { response = {
ATTR_MESSAGE_ID: str(uuid4()), API_EVENT: {
ATTR_NAME: name, API_HEADER: {
ATTR_NAMESPACE: namespace, 'namespace': namespace,
ATTR_PAYLOAD_VERSION: '2', 'name': name,
'messageId': str(uuid4()),
'payloadVersion': '3',
}, },
ATTR_PAYLOAD: payload, API_PAYLOAD: payload,
}
} }
# If a correlation token exsits, add it to header / Need by Async requests
token = request[API_HEADER].get('correlationToken')
if token:
response[API_EVENT][API_HEADER]['correlationToken'] = token
def api_error(request, exc='DriverInternalError'): # Extend event with endpoint object / Need by Async requests
if API_ENDPOINT in request:
response[API_EVENT][API_ENDPOINT] = request[API_ENDPOINT].copy()
return response
def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
"""Create a API formatted error response. """Create a API formatted error response.
Async friendly. Async friendly.
""" """
return api_message(exc, request[ATTR_HEADER][ATTR_NAMESPACE]) payload = {
'type': error_type,
'message': error_message,
}
return api_message(request, name='ErrorResponse', payload=payload)
@HANDLERS.register('DiscoverAppliancesRequest') @HANDLERS.register(('Alexa.Discovery', 'Discover'))
@asyncio.coroutine @asyncio.coroutine
def async_api_discovery(hass, request): def async_api_discovery(hass, request):
"""Create a API formatted discovery response. """Create a API formatted discovery response.
Async friendly. Async friendly.
""" """
discovered_appliances = [] discovery_endpoints = []
for entity in hass.states.async_all(): for entity in hass.states.async_all():
class_data = MAPPING_COMPONENT.get(entity.domain) class_data = MAPPING_COMPONENT.get(entity.domain)
@ -84,35 +107,42 @@ def async_api_discovery(hass, request):
if not class_data: if not class_data:
continue continue
appliance = { endpoint = {
'actions': [], 'displayCategories': [class_data[0]],
'applianceTypes': [class_data[0]],
'additionalApplianceDetails': {}, 'additionalApplianceDetails': {},
'applianceId': entity.entity_id.replace('.', '#'), 'endpointId': entity.entity_id.replace('.', '#'),
'friendlyDescription': '',
'friendlyName': entity.name, 'friendlyName': entity.name,
'isReachable': True, 'description': '',
'manufacturerName': 'Unknown', 'manufacturerName': 'Unknown',
'modelName': 'Unknown',
'version': 'Unknown',
} }
actions = set()
# static actions # static actions
if class_data[1]: if class_data[1]:
appliance['actions'].extend(list(class_data[1])) actions |= set(class_data[1])
# dynamic actions # dynamic actions
if class_data[2]: if class_data[2]:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
for feature, action_name in class_data[2].items(): for feature, action_name in class_data[2].items():
if feature & supported > 0: if feature & supported > 0:
appliance['actions'].append(action_name) actions.add(action_name)
discovered_appliances.append(appliance) # Write action into capabilities
capabilities = []
for action in actions:
capabilities.append({
'type': 'AlexaInterface',
'interface': action,
'version': 3,
})
endpoint['capabilities'] = capabilities
discovery_endpoints.append(endpoint)
return api_message( return api_message(
'DiscoverAppliancesResponse', 'Alexa.ConnectedHome.Discovery', request, name='Discover.Response', namespace='Alexa.Discovery',
payload={'discoveredAppliances': discovered_appliances}) payload={'endpoints': discovery_endpoints})
def extract_entity(funct): def extract_entity(funct):
@ -120,22 +150,21 @@ def extract_entity(funct):
@asyncio.coroutine @asyncio.coroutine
def async_api_entity_wrapper(hass, request): def async_api_entity_wrapper(hass, request):
"""Process a turn on request.""" """Process a turn on request."""
entity_id = \ entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
request[ATTR_PAYLOAD]['appliance']['applianceId'].replace('#', '.')
# extract state object # extract state object
entity = hass.states.get(entity_id) entity = hass.states.get(entity_id)
if not entity: if not entity:
_LOGGER.error("Can't process %s for %s", _LOGGER.error("Can't process %s for %s",
request[ATTR_HEADER][ATTR_NAME], entity_id) request[API_HEADER]['name'], entity_id)
return api_error(request) return api_error(request, error_type='NO_SUCH_ENDPOINT')
return (yield from funct(hass, request, entity)) return (yield from funct(hass, request, entity))
return async_api_entity_wrapper return async_api_entity_wrapper
@HANDLERS.register('TurnOnRequest') @HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_turn_on(hass, request, entity): def async_api_turn_on(hass, request, entity):
@ -144,10 +173,10 @@ def async_api_turn_on(hass, request, entity):
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=True) }, blocking=True)
return api_message('TurnOnConfirmation', 'Alexa.ConnectedHome.Control') return api_message(request)
@HANDLERS.register('TurnOffRequest') @HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_turn_off(hass, request, entity): def async_api_turn_off(hass, request, entity):
@ -156,22 +185,19 @@ def async_api_turn_off(hass, request, entity):
ATTR_ENTITY_ID: entity.entity_id ATTR_ENTITY_ID: entity.entity_id
}, blocking=True) }, blocking=True)
return api_message('TurnOffConfirmation', 'Alexa.ConnectedHome.Control') return api_message(request)
@HANDLERS.register('SetPercentageRequest') @HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@extract_entity @extract_entity
@asyncio.coroutine @asyncio.coroutine
def async_api_set_percentage(hass, request, entity): def async_api_set_brightness(hass, request, entity):
"""Process a set percentage request.""" """Process a set brightness request."""
if entity.domain == light.DOMAIN: brightness = request[API_PAYLOAD]['brightness']
brightness = request[ATTR_PAYLOAD]['percentageState']['value']
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id, ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS: brightness, light.ATTR_BRIGHTNESS: brightness,
}, blocking=True) }, blocking=True)
else:
return api_error(request)
return api_message( return api_message(request)
'SetPercentageConfirmation', 'Alexa.ConnectedHome.Control')

View File

@ -1,5 +1,5 @@
""" """
This component provides basic support for Netgear Arlo IP cameras. This component provides support for Netgear Arlo IP 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/arlo/ https://home-assistant.io/components/arlo/
@ -12,7 +12,7 @@ from requests.exceptions import HTTPError, ConnectTimeout
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
REQUIREMENTS = ['pyarlo==0.0.6'] REQUIREMENTS = ['pyarlo==0.0.7']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -23,7 +23,7 @@ DEFAULT_BRAND = 'Netgear Arlo'
DOMAIN = 'arlo' DOMAIN = 'arlo'
NOTIFICATION_ID = 'arlo_notification' NOTIFICATION_ID = 'arlo_notification'
NOTIFICATION_TITLE = 'Arlo Camera Setup' NOTIFICATION_TITLE = 'Arlo Component Setup'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({

View File

@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.Schema({ TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'event', vol.Required(CONF_PLATFORM): 'event',
vol.Required(CONF_EVENT_TYPE): cv.string, vol.Required(CONF_EVENT_TYPE): cv.string,
vol.Optional(CONF_EVENT_DATA): dict, vol.Optional(CONF_EVENT_DATA, default={}): dict,
}) })
@ -29,13 +29,19 @@ TRIGGER_SCHEMA = vol.Schema({
def async_trigger(hass, config, action): def async_trigger(hass, config, action):
"""Listen for events based on configuration.""" """Listen for events based on configuration."""
event_type = config.get(CONF_EVENT_TYPE) event_type = config.get(CONF_EVENT_TYPE)
event_data = config.get(CONF_EVENT_DATA) event_data_schema = vol.Schema(
config.get(CONF_EVENT_DATA),
extra=vol.ALLOW_EXTRA)
@callback @callback
def handle_event(event): def handle_event(event):
"""Listen for events and calls the action when data matches.""" """Listen for events and calls the action when data matches."""
if not event_data or all(val == event.data.get(key) for key, val try:
in event_data.items()): event_data_schema(event.data)
except vol.Invalid:
# If event data doesn't match requested schema, skip event
return
hass.async_run_job(action, { hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'event', 'platform': 'event',

View File

@ -99,8 +99,8 @@ def async_trigger(hass, config, action):
return return
async_remove_track_same = async_track_same_state( async_remove_track_same = async_track_same_state(
hass, True, time_delta, call_action, entity_ids=entity_id, hass, time_delta, call_action, entity_ids=entity_id,
async_check_func=check_numeric_state) async_check_same_func=check_numeric_state)
unsub = async_track_state_change( unsub = async_track_state_change(
hass, entity_id, state_automation_listener) hass, entity_id, state_automation_listener)

View File

@ -65,7 +65,9 @@ def async_trigger(hass, config, action):
return return
async_remove_track_same = async_track_same_state( async_remove_track_same = async_track_same_state(
hass, to_s.state, time_delta, call_action, entity_ids=entity_id) hass, time_delta, call_action,
lambda _, _2, to_state: to_state.state == to_s.state,
entity_ids=entity_id)
unsub = async_track_state_change( unsub = async_track_state_change(
hass, entity_id, state_automation_listener, from_state, to_state) hass, entity_id, state_automation_listener, from_state, to_state)

View File

@ -13,7 +13,8 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE) from homeassistant.const import (
CONF_NAME, ATTR_LONGITUDE, ATTR_LATITUDE, CONF_SHOW_ON_MAP)
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['pyiss==1.0.1'] REQUIREMENTS = ['pyiss==1.0.1']
@ -23,8 +24,6 @@ _LOGGER = logging.getLogger(__name__)
ATTR_ISS_NEXT_RISE = 'next_rise' ATTR_ISS_NEXT_RISE = 'next_rise'
ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space' ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
CONF_SHOW_ON_MAP = 'show_on_map'
DEFAULT_NAME = 'ISS' DEFAULT_NAME = 'ISS'
DEFAULT_DEVICE_CLASS = 'visible' DEFAULT_DEVICE_CLASS = 'visible'

View File

@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.netatmo import CameraData from homeassistant.components.netatmo import CameraData
from homeassistant.loader import get_component from homeassistant.loader import get_component
from homeassistant.const import CONF_TIMEOUT, CONF_OFFSET from homeassistant.const import CONF_TIMEOUT
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -44,14 +44,12 @@ CONF_WELCOME_SENSORS = 'welcome_sensors'
CONF_PRESENCE_SENSORS = 'presence_sensors' CONF_PRESENCE_SENSORS = 'presence_sensors'
CONF_TAG_SENSORS = 'tag_sensors' CONF_TAG_SENSORS = 'tag_sensors'
DEFAULT_TIMEOUT = 15 DEFAULT_TIMEOUT = 90
DEFAULT_OFFSET = 90
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_CAMERAS, default=[]): vol.Optional(CONF_CAMERAS, default=[]):
vol.All(cv.ensure_list, [cv.string]), vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_HOME): cv.string, vol.Optional(CONF_HOME): cv.string,
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): cv.positive_int,
vol.Optional(CONF_PRESENCE_SENSORS, default=PRESENCE_SENSOR_TYPES): vol.Optional(CONF_PRESENCE_SENSORS, default=PRESENCE_SENSOR_TYPES):
vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]), vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]),
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
@ -66,7 +64,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
netatmo = get_component('netatmo') netatmo = get_component('netatmo')
home = config.get(CONF_HOME) home = config.get(CONF_HOME)
timeout = config.get(CONF_TIMEOUT) timeout = config.get(CONF_TIMEOUT)
offset = config.get(CONF_OFFSET) if timeout is None:
timeout = DEFAULT_TIMEOUT
module_name = None module_name = None
@ -94,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for variable in welcome_sensors: for variable in welcome_sensors:
add_devices([NetatmoBinarySensor( add_devices([NetatmoBinarySensor(
data, camera_name, module_name, home, timeout, data, camera_name, module_name, home, timeout,
offset, camera_type, variable)], True) camera_type, variable)], True)
if camera_type == 'NOC': if camera_type == 'NOC':
if CONF_CAMERAS in config: if CONF_CAMERAS in config:
if config[CONF_CAMERAS] != [] and \ if config[CONF_CAMERAS] != [] and \
@ -102,14 +101,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
continue continue
for variable in presence_sensors: for variable in presence_sensors:
add_devices([NetatmoBinarySensor( add_devices([NetatmoBinarySensor(
data, camera_name, module_name, home, timeout, offset, data, camera_name, module_name, home, timeout,
camera_type, variable)], True) camera_type, variable)], True)
for module_name in data.get_module_names(camera_name): for module_name in data.get_module_names(camera_name):
for variable in tag_sensors: for variable in tag_sensors:
camera_type = None camera_type = None
add_devices([NetatmoBinarySensor( add_devices([NetatmoBinarySensor(
data, camera_name, module_name, home, timeout, offset, data, camera_name, module_name, home, timeout,
camera_type, variable)], True) camera_type, variable)], True)
@ -117,14 +116,13 @@ class NetatmoBinarySensor(BinarySensorDevice):
"""Represent a single binary sensor in a Netatmo Camera device.""" """Represent a single binary sensor in a Netatmo Camera device."""
def __init__(self, data, camera_name, module_name, home, def __init__(self, data, camera_name, module_name, home,
timeout, offset, camera_type, sensor): timeout, camera_type, sensor):
"""Set up for access to the Netatmo camera events.""" """Set up for access to the Netatmo camera events."""
self._data = data self._data = data
self._camera_name = camera_name self._camera_name = camera_name
self._module_name = module_name self._module_name = module_name
self._home = home self._home = home
self._timeout = timeout self._timeout = timeout
self._offset = offset
if home: if home:
self._name = '{} / {}'.format(home, camera_name) self._name = '{} / {}'.format(home, camera_name)
else: else:
@ -173,40 +171,39 @@ class NetatmoBinarySensor(BinarySensorDevice):
if self._sensor_name == "Someone known": if self._sensor_name == "Someone known":
self._state =\ self._state =\
self._data.camera_data.someoneKnownSeen( self._data.camera_data.someoneKnownSeen(
self._home, self._camera_name, self._timeout*60) self._home, self._camera_name, self._timeout)
elif self._sensor_name == "Someone unknown": elif self._sensor_name == "Someone unknown":
self._state =\ self._state =\
self._data.camera_data.someoneUnknownSeen( self._data.camera_data.someoneUnknownSeen(
self._home, self._camera_name, self._timeout*60) self._home, self._camera_name, self._timeout)
elif self._sensor_name == "Motion": elif self._sensor_name == "Motion":
self._state =\ self._state =\
self._data.camera_data.motionDetected( self._data.camera_data.motionDetected(
self._home, self._camera_name, self._timeout*60) self._home, self._camera_name, self._timeout)
elif self._cameratype == 'NOC': elif self._cameratype == 'NOC':
if self._sensor_name == "Outdoor motion": if self._sensor_name == "Outdoor motion":
self._state =\ self._state =\
self._data.camera_data.outdoormotionDetected( self._data.camera_data.outdoormotionDetected(
self._home, self._camera_name, self._offset) self._home, self._camera_name, self._timeout)
elif self._sensor_name == "Outdoor human": elif self._sensor_name == "Outdoor human":
self._state =\ self._state =\
self._data.camera_data.humanDetected( self._data.camera_data.humanDetected(
self._home, self._camera_name, self._offset) self._home, self._camera_name, self._timeout)
elif self._sensor_name == "Outdoor animal": elif self._sensor_name == "Outdoor animal":
self._state =\ self._state =\
self._data.camera_data.animalDetected( self._data.camera_data.animalDetected(
self._home, self._camera_name, self._offset) self._home, self._camera_name, self._timeout)
elif self._sensor_name == "Outdoor vehicle": elif self._sensor_name == "Outdoor vehicle":
self._state =\ self._state =\
self._data.camera_data.carDetected( self._data.camera_data.carDetected(
self._home, self._camera_name, self._offset) self._home, self._camera_name, self._timeout)
if self._sensor_name == "Tag Vibration": if self._sensor_name == "Tag Vibration":
self._state =\ self._state =\
self._data.camera_data.moduleMotionDetected( self._data.camera_data.moduleMotionDetected(
self._home, self._module_name, self._camera_name, self._home, self._module_name, self._camera_name,
self._timeout*60) self._timeout)
elif self._sensor_name == "Tag Open": elif self._sensor_name == "Tag Open":
self._state =\ self._state =\
self._data.camera_data.moduleOpened( self._data.camera_data.moduleOpened(
self._home, self._module_name, self._camera_name) self._home, self._module_name, self._camera_name,
else: self._timeout)
return None

View File

@ -59,6 +59,8 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice):
"""Get the latest data and updates the state.""" """Get the latest data and updates the state."""
_LOGGER.debug("Updating RainCloud sensor: %s", self._name) _LOGGER.debug("Updating RainCloud sensor: %s", self._name)
self._state = getattr(self.data, self._sensor_type) self._state = getattr(self.data, self._sensor_type)
if self._sensor_type == 'status':
self._state = self._state == 'Online'
@property @property
def icon(self): def icon(self):

View File

@ -0,0 +1,97 @@
"""
Binary sensor support for the Skybell HD Doorbell.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.skybell/
"""
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.skybell import (
DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice)
from homeassistant.const import (
CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS)
import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['skybell']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=5)
# Sensor types: Name, device_class, event
SENSOR_TYPES = {
'button': ['Button', 'occupancy', 'device:sensor:button'],
'motion': ['Motion', 'motion', 'device:sensor:motion'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE):
cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the platform for a Skybell device."""
skybell = hass.data.get(SKYBELL_DOMAIN)
sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
for device in skybell.get_devices():
sensors.append(SkybellBinarySensor(device, sensor_type))
add_devices(sensors, True)
class SkybellBinarySensor(SkybellDevice, BinarySensorDevice):
"""A binary sensor implementation for Skybell devices."""
def __init__(self, device, sensor_type):
"""Initialize a binary sensor for a Skybell device."""
super().__init__(device)
self._sensor_type = sensor_type
self._name = "{0} {1}".format(self._device.name,
SENSOR_TYPES[self._sensor_type][0])
self._device_class = SENSOR_TYPES[self._sensor_type][1]
self._event = {}
self._state = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def is_on(self):
"""Return True if the binary sensor is on."""
return self._state
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = super().device_state_attributes
attrs['event_date'] = self._event.get('createdAt')
return attrs
def update(self):
"""Get the latest data and updates the state."""
super().update()
event = self._device.latest(SENSOR_TYPES[self._sensor_type][2])
self._state = bool(event and event.get('id') != self._event.get('id'))
self._event = event

View File

@ -15,13 +15,12 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA) DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START, STATE_ON) CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
async_track_state_change, async_track_same_state) async_track_state_change, async_track_same_state)
from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -94,10 +93,6 @@ class BinarySensorTemplate(BinarySensorDevice):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._state = state.state == STATE_ON
@callback @callback
def template_bsensor_state_listener(entity, old_state, new_state): def template_bsensor_state_listener(entity, old_state, new_state):
"""Handle the target device state changes.""" """Handle the target device state changes."""
@ -135,7 +130,7 @@ class BinarySensorTemplate(BinarySensorDevice):
return False return False
@callback @callback
def _async_render(self, *args): def _async_render(self):
"""Get the state of template.""" """Get the state of template."""
try: try:
return self._template.async_render().lower() == 'true' return self._template.async_render().lower() == 'true'
@ -171,5 +166,5 @@ class BinarySensorTemplate(BinarySensorDevice):
period = self._delay_on if state else self._delay_off period = self._delay_on if state else self._delay_off
async_track_same_state( async_track_same_state(
self.hass, state, period, set_state, entity_ids=self._entities, self.hass, period, set_state, entity_ids=self._entities,
async_check_func=self._async_render) async_check_same_func=lambda *args: self._async_render() == state)

View File

@ -30,7 +30,6 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
def __init__(self, tesla_device, controller, sensor_type): def __init__(self, tesla_device, controller, sensor_type):
"""Initialisation of binary sensor.""" """Initialisation of binary sensor."""
super().__init__(tesla_device, controller) super().__init__(tesla_device, controller)
self._name = self.tesla_device.name
self._state = False self._state = False
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
self._sensor_type = sensor_type self._sensor_type = sensor_type

View File

@ -9,7 +9,6 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.wink import WinkDevice, DOMAIN from homeassistant.components.wink import WinkDevice, DOMAIN
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -87,7 +86,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.info("Device isn't a sensor, skipping") _LOGGER.info("Device isn't a sensor, skipping")
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice):
"""Representation of a Wink binary sensor.""" """Representation of a Wink binary sensor."""
def __init__(self, wink, hass): def __init__(self, wink, hass):
@ -117,6 +116,11 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Return the class of this sensor, from DEVICE_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
return SENSOR_TYPES.get(self.capability) return SENSOR_TYPES.get(self.capability)
@property
def device_state_attributes(self):
"""Return the state attributes."""
return super().device_state_attributes
class WinkSmokeDetector(WinkBinarySensorDevice): class WinkSmokeDetector(WinkBinarySensorDevice):
"""Representation of a Wink Smoke detector.""" """Representation of a Wink Smoke detector."""
@ -124,9 +128,9 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { _attributes = super().device_state_attributes
'test_activated': self.wink.test_activated() _attributes['test_activated'] = self.wink.test_activated()
} return _attributes
class WinkHub(WinkBinarySensorDevice): class WinkHub(WinkBinarySensorDevice):
@ -135,11 +139,11 @@ class WinkHub(WinkBinarySensorDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { _attributes = super().device_state_attributes
'update_needed': self.wink.update_needed(), _attributes['update_needed'] = self.wink.update_needed()
'firmware_version': self.wink.firmware_version(), _attributes['firmware_version'] = self.wink.firmware_version()
'pairing_mode': self.wink.pairing_mode() _attributes['pairing_mode'] = self.wink.pairing_mode()
} return _attributes
class WinkRemote(WinkBinarySensorDevice): class WinkRemote(WinkBinarySensorDevice):
@ -148,12 +152,12 @@ class WinkRemote(WinkBinarySensorDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { _attributes = super().device_state_attributes
'button_on_pressed': self.wink.button_on_pressed(), _attributes['button_on_pressed'] = self.wink.button_on_pressed()
'button_off_pressed': self.wink.button_off_pressed(), _attributes['button_off_pressed'] = self.wink.button_off_pressed()
'button_up_pressed': self.wink.button_up_pressed(), _attributes['button_up_pressed'] = self.wink.button_up_pressed()
'button_down_pressed': self.wink.button_down_pressed() _attributes['button_down_pressed'] = self.wink.button_down_pressed()
} return _attributes
@property @property
def device_class(self): def device_class(self):
@ -167,10 +171,10 @@ class WinkButton(WinkBinarySensorDevice):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { _attributes = super().device_state_attributes
'pressed': self.wink.pressed(), _attributes['pressed'] = self.wink.pressed()
'long_pressed': self.wink.long_pressed() _attributes['long_pressed'] = self.wink.long_pressed()
} return _attributes
class WinkGang(WinkBinarySensorDevice): class WinkGang(WinkBinarySensorDevice):

View File

@ -12,6 +12,7 @@ ATTR_OPEN_SINCE = 'Open since'
MOTION = 'motion' MOTION = 'motion'
NO_MOTION = 'no_motion' NO_MOTION = 'no_motion'
ATTR_LAST_ACTION = 'last_action'
ATTR_NO_MOTION_SINCE = 'No motion since' ATTR_NO_MOTION_SINCE = 'No motion since'
DENSITY = 'density' DENSITY = 'density'
@ -327,10 +328,18 @@ class XiaomiCube(XiaomiBinarySensor):
def __init__(self, device, hass, xiaomi_hub): def __init__(self, device, hass, xiaomi_hub):
"""Initialize the Xiaomi Cube.""" """Initialize the Xiaomi Cube."""
self._hass = hass self._hass = hass
self._last_action = None
self._state = False self._state = False
XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub, XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
None, None) None, None)
@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {ATTR_LAST_ACTION: self._last_action}
attrs.update(super().device_state_attributes)
return attrs
def parse_data(self, data): def parse_data(self, data):
"""Parse data sent by gateway.""" """Parse data sent by gateway."""
if 'status' in data: if 'status' in data:
@ -338,6 +347,7 @@ class XiaomiCube(XiaomiBinarySensor):
'entity_id': self.entity_id, 'entity_id': self.entity_id,
'action_type': data['status'] 'action_type': data['status']
}) })
self._last_action = data['status']
if 'rotate' in data: if 'rotate' in data:
self._hass.bus.fire('cube_action', { self._hass.bus.fire('cube_action', {
@ -345,4 +355,6 @@ class XiaomiCube(XiaomiBinarySensor):
'action_type': 'rotate', 'action_type': 'rotate',
'action_value': float(data['rotate'].replace(",", ".")) 'action_value': float(data['rotate'].replace(",", "."))
}) })
return False self._last_action = 'rotate'
return True

View File

@ -126,23 +126,16 @@ def async_setup(hass, config):
"""Handle calls to the camera services.""" """Handle calls to the camera services."""
target_cameras = component.async_extract_from_service(service) target_cameras = component.async_extract_from_service(service)
update_tasks = []
for camera in target_cameras: for camera in target_cameras:
if service.service == SERVICE_EN_MOTION: if service.service == SERVICE_EN_MOTION:
yield from camera.async_enable_motion_detection() yield from camera.async_enable_motion_detection()
elif service.service == SERVICE_DISEN_MOTION: elif service.service == SERVICE_DISEN_MOTION:
yield from camera.async_disable_motion_detection() yield from camera.async_disable_motion_detection()
update_tasks = []
for camera in target_cameras:
if not camera.should_poll: if not camera.should_poll:
continue continue
update_tasks.append(camera.async_update_ha_state(True))
update_coro = hass.async_add_job(
camera.async_update_ha_state(True))
if hasattr(camera, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -1,37 +1,40 @@
""" """
This component provides basic support for Netgear Arlo IP cameras. Support for Netgear Arlo 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.arlo/ https://home-assistant.io/components/camera.arlo/
""" """
import asyncio import asyncio
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.helpers import config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
DEPENDENCIES = ['arlo', 'ffmpeg']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=10)
ARLO_MODE_ARMED = 'armed'
ARLO_MODE_DISARMED = 'disarmed'
ATTR_BRIGHTNESS = 'brightness' ATTR_BRIGHTNESS = 'brightness'
ATTR_FLIPPED = 'flipped' ATTR_FLIPPED = 'flipped'
ATTR_MIRRORED = 'mirrored' ATTR_MIRRORED = 'mirrored'
ATTR_MOTION_SENSITIVITY = 'motion_detection_sensitivity' ATTR_MOTION = 'motion_detection_sensitivity'
ATTR_POWER_SAVE_MODE = 'power_save_mode' ATTR_POWERSAVE = 'power_save_mode'
ATTR_SIGNAL_STRENGTH = 'signal_strength' ATTR_SIGNAL_STRENGTH = 'signal_strength'
ATTR_UNSEEN_VIDEOS = 'unseen_videos' ATTR_UNSEEN_VIDEOS = 'unseen_videos'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
ARLO_MODE_ARMED = 'armed' DEPENDENCIES = ['arlo', 'ffmpeg']
ARLO_MODE_DISARMED = 'disarmed'
POWERSAVE_MODE_MAPPING = { POWERSAVE_MODE_MAPPING = {
1: 'best_battery_life', 1: 'best_battery_life',
@ -40,7 +43,8 @@ POWERSAVE_MODE_MAPPING = {
} }
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string, vol.Optional(CONF_FFMPEG_ARGUMENTS):
cv.string,
}) })
@ -69,6 +73,7 @@ class ArloCam(Camera):
self._motion_status = False self._motion_status = False
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
self.attrs = {}
def camera_image(self): def camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
@ -100,32 +105,24 @@ class ArloCam(Camera):
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { return {
ATTR_BATTERY_LEVEL: ATTR_BATTERY_LEVEL: self.attrs.get(ATTR_BATTERY_LEVEL),
self._camera.get_battery_level, ATTR_BRIGHTNESS: self.attrs.get(ATTR_BRIGHTNESS),
ATTR_BRIGHTNESS: ATTR_FLIPPED: self.attrs.get(ATTR_FLIPPED),
self._camera.get_brightness, ATTR_MIRRORED: self.attrs.get(ATTR_MIRRORED),
ATTR_FLIPPED: ATTR_MOTION: self.attrs.get(ATTR_MOTION),
self._camera.get_flip_state, ATTR_POWERSAVE: self.attrs.get(ATTR_POWERSAVE),
ATTR_MIRRORED: ATTR_SIGNAL_STRENGTH: self.attrs.get(ATTR_SIGNAL_STRENGTH),
self._camera.get_mirror_state, ATTR_UNSEEN_VIDEOS: self.attrs.get(ATTR_UNSEEN_VIDEOS),
ATTR_MOTION_SENSITIVITY:
self._camera.get_motion_detection_sensitivity,
ATTR_POWER_SAVE_MODE:
POWERSAVE_MODE_MAPPING[self._camera.get_powersave_mode],
ATTR_SIGNAL_STRENGTH:
self._camera.get_signal_strength,
ATTR_UNSEEN_VIDEOS:
self._camera.unseen_videos
} }
@property @property
def model(self): def model(self):
"""Camera model.""" """Return the camera model."""
return self._camera.model_id return self._camera.model_id
@property @property
def brand(self): def brand(self):
"""Camera brand.""" """Return the camera brand."""
return DEFAULT_BRAND return DEFAULT_BRAND
@property @property
@ -135,7 +132,7 @@ class ArloCam(Camera):
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
"""Camera Motion Detection Status.""" """Return the camera motion detection status."""
return self._motion_status return self._motion_status
def set_base_station_mode(self, mode): def set_base_station_mode(self, mode):
@ -160,3 +157,16 @@ class ArloCam(Camera):
"""Disable the motion detection in base station (Disarm).""" """Disable the motion detection in base station (Disarm)."""
self._motion_status = False self._motion_status = False
self.set_base_station_mode(ARLO_MODE_DISARMED) self.set_base_station_mode(ARLO_MODE_DISARMED)
def update(self):
"""Add an attribute-update task to the executor pool."""
self.attrs[ATTR_BATTERY_LEVEL] = self._camera.get_battery_level
self.attrs[ATTR_BRIGHTNESS] = self._camera.get_battery_level
self.attrs[ATTR_FLIPPED] = self._camera.get_flip_state,
self.attrs[ATTR_MIRRORED] = self._camera.get_mirror_state,
self.attrs[
ATTR_MOTION] = self._camera.get_motion_detection_sensitivity,
self.attrs[ATTR_POWERSAVE] = POWERSAVE_MODE_MAPPING[
self._camera.get_powersave_mode],
self.attrs[ATTR_SIGNAL_STRENGTH] = self._camera.get_signal_strength,
self.attrs[ATTR_UNSEEN_VIDEOS] = self._camera.unseen_videos

View File

@ -55,9 +55,9 @@ class FFmpegCamera(Camera):
from haffmpeg import ImageFrame, IMAGE_JPEG from haffmpeg import ImageFrame, IMAGE_JPEG
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
image = yield from ffmpeg.get_image( image = yield from asyncio.shield(ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG, self._input, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments) extra_cmd=self._extra_arguments), loop=self.hass.loop)
return image return image
@asyncio.coroutine @asyncio.coroutine

View File

@ -78,9 +78,9 @@ class ONVIFCamera(Camera):
ffmpeg = ImageFrame( ffmpeg = ImageFrame(
self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop)
image = yield from ffmpeg.get_image( image = yield from asyncio.shield(ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG, self._input, output_format=IMAGE_JPEG,
extra_cmd=self._ffmpeg_arguments) extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
return image return image
@asyncio.coroutine @asyncio.coroutine

View File

@ -0,0 +1,67 @@
"""
Camera support for the Skybell HD Doorbell.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.skybell/
"""
from datetime import timedelta
import logging
import requests
from homeassistant.components.camera import Camera
from homeassistant.components.skybell import (
DOMAIN as SKYBELL_DOMAIN, SkybellDevice)
DEPENDENCIES = ['skybell']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=90)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the platform for a Skybell device."""
skybell = hass.data.get(SKYBELL_DOMAIN)
sensors = []
for device in skybell.get_devices():
sensors.append(SkybellCamera(device))
add_devices(sensors, True)
class SkybellCamera(SkybellDevice, Camera):
"""A camera implementation for Skybell devices."""
def __init__(self, device):
"""Initialize a camera for a Skybell device."""
SkybellDevice.__init__(self, device)
Camera.__init__(self)
self._name = self._device.name
self._url = None
self._response = None
@property
def name(self):
"""Return the name of the sensor."""
return self._name
def camera_image(self):
"""Get the latest camera image."""
super().update()
if self._url != self._device.image:
self._url = self._device.image
try:
self._response = requests.get(
self._url, stream=True, timeout=10)
except requests.HTTPError as err:
_LOGGER.warning("Failed to get camera image: %s", err)
self._response = None
if not self._response:
return None
return self._response.content

View File

@ -16,11 +16,11 @@ from homeassistant.const import (
from homeassistant.components.camera import ( from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA) Camera, PLATFORM_SCHEMA)
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_create_clientsession, async_aiohttp_proxy_web,
async_aiohttp_proxy_web) async_get_clientsession)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['py-synology==0.1.3'] REQUIREMENTS = ['py-synology==0.1.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -58,13 +58,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return False return False
cameras = surveillance.get_all_cameras() cameras = surveillance.get_all_cameras()
websession = async_create_clientsession(hass, verify_ssl)
# add cameras # add cameras
devices = [] devices = []
for camera in cameras: for camera in cameras:
if not config.get(CONF_WHITELIST): if not config.get(CONF_WHITELIST):
device = SynologyCamera(websession, surveillance, camera.camera_id) device = SynologyCamera(surveillance, camera.camera_id, verify_ssl)
devices.append(device) devices.append(device)
async_add_devices(devices) async_add_devices(devices)
@ -73,12 +72,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class SynologyCamera(Camera): class SynologyCamera(Camera):
"""An implementation of a Synology NAS based IP camera.""" """An implementation of a Synology NAS based IP camera."""
def __init__(self, websession, surveillance, camera_id): def __init__(self, surveillance, camera_id, verify_ssl):
"""Initialize a Synology Surveillance Station camera.""" """Initialize a Synology Surveillance Station camera."""
super().__init__() super().__init__()
self._websession = websession
self._surveillance = surveillance self._surveillance = surveillance
self._camera_id = camera_id self._camera_id = camera_id
self._verify_ssl = verify_ssl
self._camera = self._surveillance.get_camera(camera_id) self._camera = self._surveillance.get_camera(camera_id)
self._motion_setting = self._surveillance.get_motion_setting(camera_id) self._motion_setting = self._surveillance.get_motion_setting(camera_id)
self.is_streaming = self._camera.is_enabled self.is_streaming = self._camera.is_enabled
@ -91,7 +90,9 @@ class SynologyCamera(Camera):
def handle_async_mjpeg_stream(self, request): 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 = self._camera.video_stream_url streaming_url = self._camera.video_stream_url
stream_coro = self._websession.get(streaming_url)
websession = async_get_clientsession(self.hass, self._verify_ssl)
stream_coro = websession.get(streaming_url)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)

View File

@ -0,0 +1,137 @@
"""
This component provides support for Xiaomi Cameras (HiSilicon Hi3518e V200).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.yi/
"""
import asyncio
import logging
import voluptuous as vol
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (CONF_HOST, CONF_NAME, CONF_PATH,
CONF_PASSWORD, CONF_PORT, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
DEPENDENCIES = ['ffmpeg']
_LOGGER = logging.getLogger(__name__)
DEFAULT_BRAND = 'YI Home Camera'
DEFAULT_PASSWORD = ''
DEFAULT_PATH = '/tmp/sd/record'
DEFAULT_PORT = 21
DEFAULT_USERNAME = 'root'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a Yi Camera."""
_LOGGER.debug('Received configuration: %s', config)
async_add_devices([YiCamera(hass, config)], True)
class YiCamera(Camera):
"""Define an implementation of a Yi Camera."""
def __init__(self, hass, config):
"""Initialize."""
super().__init__()
self._extra_arguments = config.get(CONF_FFMPEG_ARGUMENTS)
self._last_image = None
self._last_url = None
self._manager = hass.data[DATA_FFMPEG]
self._name = config.get(CONF_NAME)
self.host = config.get(CONF_HOST)
self.port = config.get(CONF_PORT)
self.path = config.get(CONF_PATH)
self.user = config.get(CONF_USERNAME)
self.passwd = config.get(CONF_PASSWORD)
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def brand(self):
"""Camera brand."""
return DEFAULT_BRAND
def get_latest_video_url(self):
"""Retrieve the latest video file from the customized Yi FTP server."""
from ftplib import FTP, error_perm
ftp = FTP(self.host)
try:
ftp.login(self.user, self.passwd)
except error_perm as exc:
_LOGGER.error('There was an error while logging into the camera')
_LOGGER.debug(exc)
return False
try:
ftp.cwd(self.path)
except error_perm as exc:
_LOGGER.error('Unable to find path: %s', self.path)
_LOGGER.debug(exc)
return False
dirs = [d for d in ftp.nlst() if '.' not in d]
if not dirs:
_LOGGER.warning("There don't appear to be any uploaded videos")
return False
latest_dir = dirs[-1]
ftp.cwd(latest_dir)
videos = ftp.nlst()
if not videos:
_LOGGER.info('Video folder "%s" is empty; delaying', latest_dir)
return False
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
self.user, self.passwd, self.host, self.port, self.path,
latest_dir, videos[-1])
@asyncio.coroutine
def async_camera_image(self):
"""Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG
url = yield from self.hass.async_add_job(self.get_latest_video_url)
if url != self._last_url:
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
self._last_image = yield from asyncio.shield(ffmpeg.get_image(
url, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments), loop=self.hass.loop)
self._last_url = url
return self._last_image
@asyncio.coroutine
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
yield from stream.open_camera(
self._last_url, extra_cmd=self._extra_arguments)
yield from async_aiohttp_proxy_stream(
self.hass, request, stream,
'multipart/x-mixed-replace;boundary=ffserver')
yield from stream.close()

View File

@ -236,24 +236,6 @@ def async_setup(hass, config):
load_yaml_config_file, load_yaml_config_file,
os.path.join(os.path.dirname(__file__), 'services.yaml')) os.path.join(os.path.dirname(__file__), 'services.yaml'))
@asyncio.coroutine
def _async_update_climate(target_climate):
"""Update climate entity after service stuff."""
update_tasks = []
for climate in target_climate:
if not climate.should_poll:
continue
update_coro = hass.async_add_job(
climate.async_update_ha_state(True))
if hasattr(climate, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
@asyncio.coroutine @asyncio.coroutine
def async_away_mode_set_service(service): def async_away_mode_set_service(service):
"""Set away mode on target climate devices.""" """Set away mode on target climate devices."""
@ -261,13 +243,19 @@ def async_setup(hass, config):
away_mode = service.data.get(ATTR_AWAY_MODE) away_mode = service.data.get(ATTR_AWAY_MODE)
update_tasks = []
for climate in target_climate: for climate in target_climate:
if away_mode: if away_mode:
yield from climate.async_turn_away_mode_on() yield from climate.async_turn_away_mode_on()
else: else:
yield from climate.async_turn_away_mode_off() yield from climate.async_turn_away_mode_off()
yield from _async_update_climate(target_climate) if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service, DOMAIN, SERVICE_SET_AWAY_MODE, async_away_mode_set_service,
@ -281,10 +269,16 @@ def async_setup(hass, config):
hold_mode = service.data.get(ATTR_HOLD_MODE) hold_mode = service.data.get(ATTR_HOLD_MODE)
update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_hold_mode(hold_mode) yield from climate.async_set_hold_mode(hold_mode)
yield from _async_update_climate(target_climate) if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service, DOMAIN, SERVICE_SET_HOLD_MODE, async_hold_mode_set_service,
@ -298,13 +292,19 @@ def async_setup(hass, config):
aux_heat = service.data.get(ATTR_AUX_HEAT) aux_heat = service.data.get(ATTR_AUX_HEAT)
update_tasks = []
for climate in target_climate: for climate in target_climate:
if aux_heat: if aux_heat:
yield from climate.async_turn_aux_heat_on() yield from climate.async_turn_aux_heat_on()
else: else:
yield from climate.async_turn_aux_heat_off() yield from climate.async_turn_aux_heat_off()
yield from _async_update_climate(target_climate) if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service, DOMAIN, SERVICE_SET_AUX_HEAT, async_aux_heat_set_service,
@ -316,6 +316,7 @@ def async_setup(hass, config):
"""Set temperature on the target climate devices.""" """Set temperature on the target climate devices."""
target_climate = component.async_extract_from_service(service) target_climate = component.async_extract_from_service(service)
update_tasks = []
for climate in target_climate: for climate in target_climate:
kwargs = {} kwargs = {}
for value, temp in service.data.items(): for value, temp in service.data.items():
@ -330,7 +331,12 @@ def async_setup(hass, config):
yield from climate.async_set_temperature(**kwargs) yield from climate.async_set_temperature(**kwargs)
yield from _async_update_climate(target_climate) if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service, DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
@ -344,10 +350,15 @@ def async_setup(hass, config):
humidity = service.data.get(ATTR_HUMIDITY) humidity = service.data.get(ATTR_HUMIDITY)
update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_humidity(humidity) yield from climate.async_set_humidity(humidity)
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
yield from _async_update_climate(target_climate) if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service, DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
@ -361,10 +372,15 @@ def async_setup(hass, config):
fan = service.data.get(ATTR_FAN_MODE) fan = service.data.get(ATTR_FAN_MODE)
update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_fan_mode(fan) yield from climate.async_set_fan_mode(fan)
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
yield from _async_update_climate(target_climate) if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service, DOMAIN, SERVICE_SET_FAN_MODE, async_fan_mode_set_service,
@ -378,10 +394,15 @@ def async_setup(hass, config):
operation_mode = service.data.get(ATTR_OPERATION_MODE) operation_mode = service.data.get(ATTR_OPERATION_MODE)
update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_operation_mode(operation_mode) yield from climate.async_set_operation_mode(operation_mode)
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
yield from _async_update_climate(target_climate) if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service, DOMAIN, SERVICE_SET_OPERATION_MODE, async_operation_set_service,
@ -395,10 +416,15 @@ def async_setup(hass, config):
swing_mode = service.data.get(ATTR_SWING_MODE) swing_mode = service.data.get(ATTR_SWING_MODE)
update_tasks = []
for climate in target_climate: for climate in target_climate:
yield from climate.async_set_swing_mode(swing_mode) yield from climate.async_set_swing_mode(swing_mode)
if not climate.should_poll:
continue
update_tasks.append(climate.async_update_ha_state(True))
yield from _async_update_climate(target_climate) if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service, DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,

View File

@ -17,7 +17,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['python-eq3bt==0.1.5'] REQUIREMENTS = ['python-eq3bt==0.1.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -164,4 +164,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
def update(self): def update(self):
"""Update the data from the thermostat.""" """Update the data from the thermostat."""
from bluepy.btle import BTLEException
try:
self._thermostat.update() self._thermostat.update()
except BTLEException as ex:
_LOGGER.warning("Updating the state failed: %s", ex)

View File

@ -14,6 +14,8 @@ from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
CONF_SETPOINT_ADDRESS = 'setpoint_address' CONF_SETPOINT_ADDRESS = 'setpoint_address'
CONF_SETPOINT_SHIFT_ADDRESS = 'setpoint_shift_address'
CONF_SETPOINT_SHIFT_STATE_ADDRESS = 'setpoint_shift_state_address'
CONF_TEMPERATURE_ADDRESS = 'temperature_address' CONF_TEMPERATURE_ADDRESS = 'temperature_address'
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address' CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address' CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
@ -33,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SETPOINT_ADDRESS): cv.string, vol.Required(CONF_SETPOINT_ADDRESS): cv.string,
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string, vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string, vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
vol.Optional(CONF_SETPOINT_SHIFT_ADDRESS): cv.string,
vol.Optional(CONF_SETPOINT_SHIFT_STATE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string, vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string, vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string, vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
@ -82,6 +86,10 @@ def async_add_devices_config(hass, config, async_add_devices):
CONF_TARGET_TEMPERATURE_ADDRESS), CONF_TARGET_TEMPERATURE_ADDRESS),
group_address_setpoint=config.get( group_address_setpoint=config.get(
CONF_SETPOINT_ADDRESS), CONF_SETPOINT_ADDRESS),
group_address_setpoint_shift=config.get(
CONF_SETPOINT_SHIFT_ADDRESS),
group_address_setpoint_shift_state=config.get(
CONF_SETPOINT_SHIFT_STATE_ADDRESS),
group_address_operation_mode=config.get( group_address_operation_mode=config.get(
CONF_OPERATION_MODE_ADDRESS), CONF_OPERATION_MODE_ADDRESS),
group_address_operation_mode_state=config.get( group_address_operation_mode_state=config.get(
@ -140,13 +148,29 @@ class KNXClimate(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.device.temperature.value
@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.supports_target_temperature: return self.device.target_temperature_comfort
return self.device.target_temperature
@property
def target_temperature_high(self):
"""Return the highbound target temperature we try to reach."""
if self.device.target_temperature_comfort:
return max(
self.device.target_temperature_comfort,
self.device.target_temperature.value)
return None
@property
def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach."""
if self.device.target_temperature_comfort:
return min(
self.device.target_temperature_comfort,
self.device.target_temperature.value)
return None return None
@asyncio.coroutine @asyncio.coroutine
@ -155,8 +179,8 @@ class KNXClimate(ClimateDevice):
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None:
return return
if self.device.supports_target_temperature: yield from self.device.set_target_temperature_comfort(temperature)
yield from self.device.set_target_temperature(temperature) yield from self.async_update_ha_state()
@property @property
def current_operation(self): def current_operation(self):

View File

@ -18,7 +18,8 @@ from homeassistant.components.climate import (
ATTR_OPERATION_MODE) ATTR_OPERATION_MODE)
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME) STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME)
from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN) from homeassistant.components.mqtt import (CONF_QOS, CONF_RETAIN,
MQTT_BASE_PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH) SPEED_HIGH)
@ -57,7 +58,8 @@ CONF_SWING_MODE_LIST = 'swing_modes'
CONF_INITIAL = 'initial' CONF_INITIAL = 'initial'
CONF_SEND_IF_OFF = 'send_if_off' CONF_SEND_IF_OFF = 'send_if_off'
PLATFORM_SCHEMA = CLIMATE_PLATFORM_SCHEMA.extend({ SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema)
PLATFORM_SCHEMA = SCHEMA_BASE.extend({
vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic,

View File

@ -101,7 +101,7 @@ set_swing_mode:
fields: fields:
entity_id: entity_id:
description: Name(s) of entities to change description: Name(s) of entities to change
example: '.nest' example: 'climate.nest'
swing_mode: swing_mode:
description: New value of swing mode description: New value of swing mode

View File

@ -35,7 +35,6 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
self._target_temperature = None self._target_temperature = None
self._temperature = None self._temperature = None
self._name = self.tesla_device.name
@property @property
def current_operation(self): def current_operation(self):

View File

@ -0,0 +1,95 @@
"""
Toon van Eneco Thermostat Support.
This provides a component for the rebranded Quby thermostat as provided by
Eneco.
"""
from homeassistant.components.climate import (ClimateDevice,
ATTR_TEMPERATURE,
STATE_PERFORMANCE,
STATE_HEAT,
STATE_ECO,
STATE_COOL)
from homeassistant.const import TEMP_CELSIUS
import homeassistant.components.toon as toon_main
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup thermostat."""
# Add toon
add_devices((ThermostatDevice(hass), ), True)
class ThermostatDevice(ClimateDevice):
"""Interface class for the toon module and HA."""
def __init__(self, hass):
"""Initialize the device."""
self._name = 'Toon van Eneco'
self.hass = hass
self.thermos = hass.data[toon_main.TOON_HANDLE]
# set up internal state vars
self._state = None
self._temperature = None
self._setpoint = None
self._operation_list = [STATE_PERFORMANCE,
STATE_HEAT,
STATE_ECO,
STATE_COOL]
@property
def name(self):
"""Name of this Thermostat."""
return self._name
@property
def should_poll(self):
"""Polling is required."""
return True
@property
def temperature_unit(self):
"""The unit of measurement used by the platform."""
return TEMP_CELSIUS
@property
def current_operation(self):
"""Return current operation i.e. comfort, home, away."""
state = self.thermos.get_data('state')
return state
@property
def operation_list(self):
"""List of available operation modes."""
return self._operation_list
@property
def current_temperature(self):
"""Return the current temperature."""
return self.thermos.get_data('temp')
@property
def target_temperature(self):
"""Return the temperature we try to reach."""
return self.thermos.get_data('setpoint')
def set_temperature(self, **kwargs):
"""Change the setpoint of the thermostat."""
temp = kwargs.get(ATTR_TEMPERATURE)
self.thermos.set_temp(temp)
def set_operation_mode(self, operation_mode):
"""Set new operation mode as toonlib requires it."""
toonlib_values = {STATE_PERFORMANCE: 'Comfort',
STATE_HEAT: 'Home',
STATE_ECO: 'Away',
STATE_COOL: 'Sleep'}
self.thermos.set_state(toonlib_values[operation_mode])
def update(self):
"""Update local state."""
self.thermos.update()

View File

@ -1,47 +1,147 @@
"""Component to integrate the Home Assistant cloud.""" """Component to integrate the Home Assistant cloud."""
import asyncio import asyncio
import json
import logging import logging
import os
import voluptuous as vol import voluptuous as vol
from . import http_api, auth_api from homeassistant.const import EVENT_HOMEASSISTANT_START
from .const import DOMAIN
from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.2.0'] REQUIREMENTS = ['warrant==0.5.0']
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
CONF_MODE = 'mode' CONF_MODE = 'mode'
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_REGION = 'region'
CONF_RELAYER = 'relayer'
MODE_DEV = 'development' MODE_DEV = 'development'
MODE_STAGING = 'staging'
MODE_PRODUCTION = 'production'
DEFAULT_MODE = MODE_DEV DEFAULT_MODE = MODE_DEV
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV, MODE_STAGING, MODE_PRODUCTION]), vol.In([MODE_DEV] + list(SERVERS)),
# Change to optional when we include real servers
vol.Required(CONF_COGNITO_CLIENT_ID): str,
vol.Required(CONF_USER_POOL_ID): str,
vol.Required(CONF_REGION): str,
vol.Required(CONF_RELAYER): str,
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
@asyncio.coroutine @asyncio.coroutine
def async_setup(hass, config): def async_setup(hass, config):
"""Initialize the Home Assistant cloud.""" """Initialize the Home Assistant cloud."""
mode = MODE_PRODUCTION
if DOMAIN in config: if DOMAIN in config:
mode = config[DOMAIN].get(CONF_MODE) kwargs = config[DOMAIN]
else:
kwargs = {CONF_MODE: DEFAULT_MODE}
if mode != 'development': cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
_LOGGER.error('Only development mode is currently allowed.')
return False
data = hass.data[DOMAIN] = { @asyncio.coroutine
'mode': mode def init_cloud(event):
} """Initialize connection."""
yield from cloud.initialize()
data['auth'] = yield from hass.async_add_job(auth_api.load_auth, hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, init_cloud)
yield from http_api.async_setup(hass) yield from http_api.async_setup(hass)
return True return True
class Cloud:
"""Store the configuration of the cloud connection."""
def __init__(self, hass, mode, cognito_client_id=None, user_pool_id=None,
region=None, relayer=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.email = None
self.id_token = None
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
self.user_pool_id = user_pool_id
self.region = region
self.relayer = relayer
else:
info = SERVERS[mode]
self.cognito_client_id = info['cognito_client_id']
self.user_pool_id = info['user_pool_id']
self.region = info['region']
self.relayer = info['relayer']
@property
def is_logged_in(self):
"""Get if cloud is logged in."""
return self.email is not None
@property
def user_info_path(self):
"""Get path to the stored auth."""
return self.path('{}_auth.json'.format(self.mode))
@asyncio.coroutine
def initialize(self):
"""Initialize and load cloud info."""
def load_config():
"""Load the configuration."""
# Ensure config dir exists
path = self.hass.config.path(CONFIG_DIR)
if not os.path.isdir(path):
os.mkdir(path)
user_info = self.user_info_path
if os.path.isfile(user_info):
with open(user_info, 'rt') as file:
info = json.loads(file.read())
self.email = info['email']
self.id_token = info['id_token']
self.access_token = info['access_token']
self.refresh_token = info['refresh_token']
yield from self.hass.async_add_job(load_config)
if self.email is not None:
yield from self.iot.connect()
def path(self, *parts):
"""Get config path inside cloud dir."""
return self.hass.config.path(CONFIG_DIR, *parts)
@asyncio.coroutine
def logout(self):
"""Close connection and remove all credentials."""
yield from self.iot.disconnect()
self.email = None
self.id_token = None
self.access_token = None
self.refresh_token = None
yield from self.hass.async_add_job(
lambda: os.remove(self.user_info_path))
def write_user_info(self):
"""Write user info to a file."""
with open(self.user_info_path, 'wt') as file:
file.write(json.dumps({
'email': self.email,
'id_token': self.id_token,
'access_token': self.access_token,
'refresh_token': self.refresh_token,
}, indent=4))

View File

@ -1,10 +1,7 @@
"""Package to offer tools to authenticate with the cloud.""" """Package to communicate with the authentication API."""
import json import hashlib
import logging import logging
import os
from .const import AUTH_FILE, SERVERS
from .util import get_mode
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -61,116 +58,95 @@ def _map_aws_exception(err):
return ex(err.response['Error']['Message']) return ex(err.response['Error']['Message'])
def load_auth(hass): def _generate_username(email):
"""Load authentication from disk and verify it.""" """Generate a username from an email address."""
info = _read_info(hass) return hashlib.sha512(email.encode('utf-8')).hexdigest()
if info is None:
return Auth(hass)
auth = Auth(hass, _cognito(
hass,
id_token=info['id_token'],
access_token=info['access_token'],
refresh_token=info['refresh_token'],
))
if auth.validate_auth():
return auth
return Auth(hass)
def register(hass, email, password): def register(cloud, email, password):
"""Register a new account.""" """Register a new account."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email) cognito = _cognito(cloud)
try: try:
cognito.register(email, password) cognito.register(_generate_username(email), password, email=email)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
def confirm_register(hass, confirmation_code, email): def confirm_register(cloud, confirmation_code, email):
"""Confirm confirmation code after registration.""" """Confirm confirmation code after registration."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email) cognito = _cognito(cloud)
try: try:
cognito.confirm_sign_up(confirmation_code, email) cognito.confirm_sign_up(confirmation_code, _generate_username(email))
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
def forgot_password(hass, email): def forgot_password(cloud, email):
"""Initiate forgotten password flow.""" """Initiate forgotten password flow."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email) cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.initiate_forgot_password() cognito.initiate_forgot_password()
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
def confirm_forgot_password(hass, confirmation_code, email, new_password): def confirm_forgot_password(cloud, confirmation_code, email, new_password):
"""Confirm forgotten password code and change password.""" """Confirm forgotten password code and change password."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(hass, username=email) cognito = _cognito(cloud, username=_generate_username(email))
try: try:
cognito.confirm_forgot_password(confirmation_code, new_password) cognito.confirm_forgot_password(confirmation_code, new_password)
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
class Auth(object): def login(cloud, email, password):
"""Class that holds Cloud authentication.""" """Log user in and fetch certificate."""
cognito = _authenticate(cloud, email, password)
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.refresh_token = cognito.refresh_token
cloud.email = email
cloud.write_user_info()
def __init__(self, hass, cognito=None):
"""Initialize Hass cloud info object."""
self.hass = hass
self.cognito = cognito
self.account = None
@property def check_token(cloud):
def is_logged_in(self): """Check that the token is valid and verify if needed."""
"""Return if user is logged in."""
return self.account is not None
def validate_auth(self):
"""Validate that the contained auth is valid."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
cognito = _cognito(
cloud,
access_token=cloud.access_token,
refresh_token=cloud.refresh_token)
try: try:
self._refresh_account_info() if cognito.check_token():
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err: except ClientError as err:
if err.response['Error']['Code'] != 'NotAuthorizedException': raise _map_aws_exception(err)
_LOGGER.error('Unexpected error verifying auth: %s', err)
return False
try:
self.renew_access_token()
self._refresh_account_info()
except ClientError:
_LOGGER.error('Unable to refresh auth token: %s', err)
return False
return True def _authenticate(cloud, email, password):
"""Log in and return an authenticated Cognito instance."""
def login(self, username, password):
"""Login using a username and password."""
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from warrant.exceptions import ForceChangePasswordException from warrant.exceptions import ForceChangePasswordException
cognito = _cognito(self.hass, username=username) assert not cloud.is_logged_in, 'Cannot login if already logged in.'
cognito = _cognito(cloud, username=email)
try: try:
cognito.authenticate(password=password) cognito.authenticate(password=password)
self.cognito = cognito return cognito
self._refresh_account_info()
_write_info(self.hass, self)
except ForceChangePasswordException as err: except ForceChangePasswordException as err:
raise PasswordChangeRequired raise PasswordChangeRequired
@ -178,93 +154,24 @@ class Auth(object):
except ClientError as err: except ClientError as err:
raise _map_aws_exception(err) raise _map_aws_exception(err)
def _refresh_account_info(self):
"""Refresh the account info.
Raises boto3 exceptions. def _cognito(cloud, **kwargs):
"""
self.account = self.cognito.get_user()
def renew_access_token(self):
"""Refresh token."""
from botocore.exceptions import ClientError
try:
self.cognito.renew_access_token()
_write_info(self.hass, self)
return True
except ClientError as err:
_LOGGER.error('Error refreshing token: %s', err)
return False
def logout(self):
"""Invalidate token."""
from botocore.exceptions import ClientError
try:
self.cognito.logout()
self.account = None
_write_info(self.hass, self)
except ClientError as err:
raise _map_aws_exception(err)
def _read_info(hass):
"""Read auth file."""
path = hass.config.path(AUTH_FILE)
if not os.path.isfile(path):
return None
with open(path) as file:
return json.load(file).get(get_mode(hass))
def _write_info(hass, auth):
"""Write auth info for specified mode.
Pass in None for data to remove authentication for that mode.
"""
path = hass.config.path(AUTH_FILE)
mode = get_mode(hass)
if os.path.isfile(path):
with open(path) as file:
content = json.load(file)
else:
content = {}
if auth.is_logged_in:
content[mode] = {
'id_token': auth.cognito.id_token,
'access_token': auth.cognito.access_token,
'refresh_token': auth.cognito.refresh_token,
}
else:
content.pop(mode, None)
with open(path, 'wt') as file:
file.write(json.dumps(content, indent=4, sort_keys=True))
def _cognito(hass, **kwargs):
"""Get the client credentials.""" """Get the client credentials."""
import botocore
import boto3
from warrant import Cognito from warrant import Cognito
mode = get_mode(hass)
info = SERVERS.get(mode)
if info is None:
raise ValueError('Mode {} is not supported.'.format(mode))
cognito = Cognito( cognito = Cognito(
user_pool_id=info['identity_pool_id'], user_pool_id=cloud.user_pool_id,
client_id=info['client_id'], client_id=cloud.cognito_client_id,
user_pool_region=info['region'], user_pool_region=cloud.region,
access_key=info['access_key_id'],
secret_key=info['secret_access_key'],
**kwargs **kwargs
) )
cognito.client = boto3.client(
'cognito-idp',
region_name=cloud.region,
config=botocore.config.Config(
signature_version=botocore.UNSIGNED
)
)
return cognito return cognito

View File

@ -1,14 +1,14 @@
"""Constants for the cloud component.""" """Constants for the cloud component."""
DOMAIN = 'cloud' DOMAIN = 'cloud'
CONFIG_DIR = '.cloud'
REQUEST_TIMEOUT = 10 REQUEST_TIMEOUT = 10
AUTH_FILE = '.cloud'
SERVERS = { SERVERS = {
'development': { # Example entry:
'client_id': '3k755iqfcgv8t12o4pl662mnos', # 'production': {
'identity_pool_id': 'us-west-2_vDOfweDJo', # 'cognito_client_id': '',
'region': 'us-west-2', # 'user_pool_id': '',
'access_key_id': 'AKIAJGRK7MILPRJTT2ZQ', # 'region': '',
'secret_access_key': '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' # 'relayer': ''
} # }
} }

View File

@ -10,7 +10,7 @@ from homeassistant.components.http import (
HomeAssistantView, RequestDataValidator) HomeAssistantView, RequestDataValidator)
from . import auth_api from . import auth_api
from .const import REQUEST_TIMEOUT from .const import DOMAIN, REQUEST_TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -74,13 +74,14 @@ class CloudLoginView(HomeAssistantView):
def post(self, request, data): def post(self, request, data):
"""Handle login request.""" """Handle login request."""
hass = request.app['hass'] hass = request.app['hass']
auth = hass.data['cloud']['auth'] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth.login, data['email'], yield from hass.async_add_job(auth_api.login, cloud, data['email'],
data['password']) data['password'])
hass.async_add_job(cloud.iot.connect)
return self.json(_auth_data(auth)) return self.json(_account_data(cloud))
class CloudLogoutView(HomeAssistantView): class CloudLogoutView(HomeAssistantView):
@ -94,10 +95,10 @@ class CloudLogoutView(HomeAssistantView):
def post(self, request): def post(self, request):
"""Handle logout request.""" """Handle logout request."""
hass = request.app['hass'] hass = request.app['hass']
auth = hass.data['cloud']['auth'] cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job(auth.logout) yield from cloud.logout()
return self.json_message('ok') return self.json_message('ok')
@ -112,12 +113,12 @@ class CloudAccountView(HomeAssistantView):
def get(self, request): def get(self, request):
"""Get account info.""" """Get account info."""
hass = request.app['hass'] hass = request.app['hass']
auth = hass.data['cloud']['auth'] cloud = hass.data[DOMAIN]
if not auth.is_logged_in: if not cloud.is_logged_in:
return self.json_message('Not logged in', 400) return self.json_message('Not logged in', 400)
return self.json(_auth_data(auth)) return self.json(_account_data(cloud))
class CloudRegisterView(HomeAssistantView): class CloudRegisterView(HomeAssistantView):
@ -135,10 +136,11 @@ class CloudRegisterView(HomeAssistantView):
def post(self, request, data): def post(self, request, data):
"""Handle registration request.""" """Handle registration request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job( yield from hass.async_add_job(
auth_api.register, hass, data['email'], data['password']) auth_api.register, cloud, data['email'], data['password'])
return self.json_message('ok') return self.json_message('ok')
@ -158,10 +160,11 @@ class CloudConfirmRegisterView(HomeAssistantView):
def post(self, request, data): def post(self, request, data):
"""Handle registration confirmation request.""" """Handle registration confirmation request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job( yield from hass.async_add_job(
auth_api.confirm_register, hass, data['confirmation_code'], auth_api.confirm_register, cloud, data['confirmation_code'],
data['email']) data['email'])
return self.json_message('ok') return self.json_message('ok')
@ -181,10 +184,11 @@ class CloudForgotPasswordView(HomeAssistantView):
def post(self, request, data): def post(self, request, data):
"""Handle forgot password request.""" """Handle forgot password request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job( yield from hass.async_add_job(
auth_api.forgot_password, hass, data['email']) auth_api.forgot_password, cloud, data['email'])
return self.json_message('ok') return self.json_message('ok')
@ -205,18 +209,19 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
def post(self, request, data): def post(self, request, data):
"""Handle forgot password confirm request.""" """Handle forgot password confirm request."""
hass = request.app['hass'] hass = request.app['hass']
cloud = hass.data[DOMAIN]
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
yield from hass.async_add_job( yield from hass.async_add_job(
auth_api.confirm_forgot_password, hass, auth_api.confirm_forgot_password, cloud,
data['confirmation_code'], data['email'], data['confirmation_code'], data['email'],
data['new_password']) data['new_password'])
return self.json_message('ok') return self.json_message('ok')
def _auth_data(auth): def _account_data(cloud):
"""Generate the auth data JSON response.""" """Generate the auth data JSON response."""
return { return {
'email': auth.account.email 'email': cloud.email
} }

View File

@ -0,0 +1,194 @@
"""Module to handle messages from Home Assistant cloud."""
import asyncio
import logging
from aiohttp import hdrs, client_exceptions, WSMsgType
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.components.alexa import smart_home
from homeassistant.util.decorator import Registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import auth_api
HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
class UnknownHandler(Exception):
"""Exception raised when trying to handle unknown handler."""
class CloudIoT:
"""Class to manage the IoT connection."""
def __init__(self, cloud):
"""Initialize the CloudIoT class."""
self.cloud = cloud
self.client = None
self.close_requested = False
self.tries = 0
@property
def is_connected(self):
"""Return if connected to the cloud."""
return self.client is not None
@asyncio.coroutine
def connect(self):
"""Connect to the IoT broker."""
if self.client is not None:
raise RuntimeError('Cannot connect while already connected')
self.close_requested = False
hass = self.cloud.hass
remove_hass_stop_listener = None
session = async_get_clientsession(self.cloud.hass)
@asyncio.coroutine
def _handle_hass_stop(event):
"""Handle Home Assistant shutting down."""
nonlocal remove_hass_stop_listener
remove_hass_stop_listener = None
yield from self.disconnect()
client = None
disconnect_warn = None
try:
yield from hass.async_add_job(auth_api.check_token, self.cloud)
self.client = client = yield from session.ws_connect(
self.cloud.relayer, headers={
hdrs.AUTHORIZATION:
'Bearer {}'.format(self.cloud.access_token)
})
self.tries = 0
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
_LOGGER.info('Connected')
while not client.closed:
msg = yield from client.receive()
if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
WSMsgType.CLOSING):
disconnect_warn = 'Closed by server'
break
elif msg.type != WSMsgType.TEXT:
disconnect_warn = 'Received non-Text message: {}'.format(
msg.type)
break
try:
msg = msg.json()
except ValueError:
disconnect_warn = 'Received invalid JSON.'
break
_LOGGER.debug('Received message: %s', msg)
response = {
'msgid': msg['msgid'],
}
try:
result = yield from async_handle_message(
hass, self.cloud, msg['handler'], msg['payload'])
# No response from handler
if result is None:
continue
response['payload'] = result
except UnknownHandler:
response['error'] = 'unknown-handler'
except Exception: # pylint: disable=broad-except
_LOGGER.exception('Error handling message')
response['error'] = 'exception'
_LOGGER.debug('Publishing message: %s', response)
yield from client.send_json(response)
except auth_api.CloudError:
_LOGGER.warning('Unable to connect: Unable to refresh token.')
except client_exceptions.WSServerHandshakeError as err:
if err.code == 401:
disconnect_warn = 'Invalid auth.'
self.close_requested = True
# Should we notify user?
else:
_LOGGER.warning('Unable to connect: %s', err)
except client_exceptions.ClientError as err:
_LOGGER.warning('Unable to connect: %s', err)
except Exception: # pylint: disable=broad-except
if not self.close_requested:
_LOGGER.exception('Unexpected error')
finally:
if disconnect_warn is not None:
_LOGGER.warning('Connection closed: %s', disconnect_warn)
if remove_hass_stop_listener is not None:
remove_hass_stop_listener()
if client is not None:
self.client = None
yield from client.close()
if not self.close_requested:
self.tries += 1
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
yield from asyncio.sleep(
min(30, (self.tries - 1) * 5), loop=hass.loop)
hass.async_add_job(self.connect())
@asyncio.coroutine
def disconnect(self):
"""Disconnect the client."""
self.close_requested = True
yield from self.client.close()
@asyncio.coroutine
def async_handle_message(hass, cloud, handler_name, payload):
"""Handle incoming IoT message."""
handler = HANDLERS.get(handler_name)
if handler is None:
raise UnknownHandler()
return (yield from handler(hass, cloud, payload))
@HANDLERS.register('alexa')
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
return (yield from smart_home.async_handle_message(hass, payload))
@HANDLERS.register('cloud')
@asyncio.coroutine
def async_handle_cloud(hass, cloud, payload):
"""Handle an incoming IoT message for cloud component."""
action = payload['action']
if action == 'logout':
yield from cloud.logout()
_LOGGER.error('You have been logged out from Home Assistant cloud: %s',
payload['reason'])
else:
_LOGGER.warning('Received unknown cloud action: %s', action)
return None

View File

@ -1,10 +0,0 @@
"""Utilities for the cloud integration."""
from .const import DOMAIN
def get_mode(hass):
"""Return the current mode of the cloud component.
Async friendly.
"""
return hass.data[DOMAIN]['mode']

View File

@ -169,21 +169,12 @@ def async_setup(hass, config):
params.pop(ATTR_ENTITY_ID, None) params.pop(ATTR_ENTITY_ID, None)
# call method # call method
update_tasks = []
for cover in covers: for cover in covers:
yield from getattr(cover, method['method'])(**params) yield from getattr(cover, method['method'])(**params)
update_tasks = []
for cover in covers:
if not cover.should_poll: if not cover.should_poll:
continue continue
update_tasks.append(cover.async_update_ha_state(True))
update_coro = hass.async_add_job(
cover.async_update_ha_state(True))
if hasattr(cover, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -24,7 +24,6 @@ from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.script import Script from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -134,7 +133,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
_LOGGER.error("No covers added") _LOGGER.error("No covers added")
return False return False
async_add_devices(covers, True) async_add_devices(covers)
return True return True
@ -190,10 +189,6 @@ class CoverTemplate(CoverDevice):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._position = 100 if state.state == STATE_OPEN else 0
@callback @callback
def template_cover_state_listener(entity, old_state, new_state): def template_cover_state_listener(entity, old_state, new_state):
"""Handle target device state changes.""" """Handle target device state changes."""

View File

@ -18,11 +18,10 @@ from homeassistant.setup import async_prepare_setup_platform
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.components import group, zone from homeassistant.components import group, zone
from homeassistant.components.discovery import SERVICE_NETGEAR
from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.config import load_yaml_config_file, async_log_exception
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.restore_state import async_get_last_state
@ -89,10 +88,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
cv.time_period, cv.positive_timedelta) cv.time_period, cv.positive_timedelta)
}) })
DISCOVERY_PLATFORMS = {
SERVICE_NETGEAR: 'netgear',
}
@bind_hass @bind_hass
def is_on(hass: HomeAssistantType, entity_id: str=None): def is_on(hass: HomeAssistantType, entity_id: str=None):
@ -180,22 +175,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
tracker.async_setup_group() tracker.async_setup_group()
@callback
def async_device_tracker_discovered(service, info):
"""Handle the discovery of device tracker platforms."""
hass.async_add_job(
async_setup_platform(DISCOVERY_PLATFORMS[service], {}, info))
discovery.async_listen(
hass, DISCOVERY_PLATFORMS.keys(), async_device_tracker_discovered)
@asyncio.coroutine
def async_platform_discovered(platform, info):
"""Load a platform."""
yield from async_setup_platform(platform, {}, disc_info=info)
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
# Clean up stale devices # Clean up stale devices
async_track_utc_time_change( async_track_utc_time_change(
hass, tracker.async_update_stale, second=range(0, 60, 5)) hass, tracker.async_update_stale, second=range(0, 60, 5))

View File

@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
REQUIREMENTS = ['fritzconnection==0.6.3'] REQUIREMENTS = ['fritzconnection==0.6.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
REQUIREMENTS = ['librouteros==1.0.2'] REQUIREMENTS = ['librouteros==1.0.4']
MTK_DEFAULT_API_PORT = '8728' MTK_DEFAULT_API_PORT = '8728'
@ -83,6 +83,15 @@ class MikrotikScanner(DeviceScanner):
routerboard_info[0].get('model', 'Router'), routerboard_info[0].get('model', 'Router'),
self.host) self.host)
self.connected = True self.connected = True
self.capsman_exist = self.client(
cmd='/capsman/interface/getall'
)
if not self.capsman_exist:
_LOGGER.info(
'Mikrotik %s: Not a CAPSman controller. Trying '
'local interfaces ',
self.host
)
self.wireless_exist = self.client( self.wireless_exist = self.client(
cmd='/interface/wireless/getall' cmd='/interface/wireless/getall'
) )
@ -111,7 +120,9 @@ class MikrotikScanner(DeviceScanner):
def _update_info(self): def _update_info(self):
"""Retrieve latest information from the Mikrotik box.""" """Retrieve latest information from the Mikrotik box."""
if self.wireless_exist: if self.capsman_exist:
devices_tracker = 'capsman'
elif self.wireless_exist:
devices_tracker = 'wireless' devices_tracker = 'wireless'
else: else:
devices_tracker = 'ip' devices_tracker = 'ip'
@ -123,7 +134,11 @@ class MikrotikScanner(DeviceScanner):
) )
device_names = self.client(cmd='/ip/dhcp-server/lease/getall') device_names = self.client(cmd='/ip/dhcp-server/lease/getall')
if self.wireless_exist: if devices_tracker == 'capsman':
devices = self.client(
cmd='/caps-man/registration-table/getall'
)
elif devices_tracker == 'wireless':
devices = self.client( devices = self.client(
cmd='/interface/wireless/registration-table/getall' cmd='/interface/wireless/registration-table/getall'
) )

View File

@ -5,23 +5,22 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.owntracks/ https://home-assistant.io/components/device_tracker.owntracks/
""" """
import asyncio import asyncio
import base64
import json import json
import logging import logging
import base64
from collections import defaultdict from collections import defaultdict
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import STATE_HOME import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify, decorator
from homeassistant.components import zone as zone_comp from homeassistant.components import zone as zone_comp
from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker import PLATFORM_SCHEMA
from homeassistant.const import STATE_HOME
from homeassistant.core import callback
from homeassistant.util import slugify, decorator
DEPENDENCIES = ['mqtt'] REQUIREMENTS = ['libnacl==1.6.0']
REQUIREMENTS = ['libnacl==1.5.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -34,6 +33,8 @@ CONF_SECRET = 'secret'
CONF_WAYPOINT_IMPORT = 'waypoints' CONF_WAYPOINT_IMPORT = 'waypoints'
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist' CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
DEPENDENCIES = ['mqtt']
OWNTRACKS_TOPIC = 'owntracks/#' OWNTRACKS_TOPIC = 'owntracks/#'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -74,6 +75,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
except ValueError: except ValueError:
# If invalid JSON # If invalid JSON
_LOGGER.error("Unable to parse payload as JSON: %s", payload) _LOGGER.error("Unable to parse payload as JSON: %s", payload)
return
message['topic'] = topic message['topic'] = topic
@ -90,7 +92,11 @@ def _parse_topic(topic):
Async friendly. Async friendly.
""" """
try:
_, user, device, *_ = topic.split('/', 3) _, user, device, *_ = topic.split('/', 3)
except ValueError:
_LOGGER.error("Can't parse topic: '%s'", topic)
raise
return user, device return user, device
@ -399,6 +405,13 @@ def async_handle_encrypted_message(hass, context, message):
yield from async_handle_message(hass, context, decrypted) yield from async_handle_message(hass, context, decrypted)
@HANDLERS.register('lwt')
@asyncio.coroutine
def async_handle_lwt_message(hass, context, message):
"""Handle an lwt message."""
_LOGGER.debug('Not handling lwt message: %s', message)
@asyncio.coroutine @asyncio.coroutine
def async_handle_message(hass, context, message): def async_handle_message(hass, context, message):
"""Handle an OwnTracks message.""" """Handle an OwnTracks message."""

View File

@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pysnmp==4.3.9'] REQUIREMENTS = ['pysnmp==4.3.10']
CONF_COMMUNITY = 'community' CONF_COMMUNITY = 'community'
CONF_AUTHKEY = 'authkey' CONF_AUTHKEY = 'authkey'
@ -36,7 +36,7 @@ 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 an snmp scanner.""" """Validate the configuration and return an SNMP scanner."""
scanner = SnmpScanner(config[DOMAIN]) scanner = SnmpScanner(config[DOMAIN])
return scanner if scanner.success_init else None return scanner if scanner.success_init else None

View File

@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.unifi/ https://home-assistant.io/components/device_tracker.unifi/
""" """
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -12,16 +13,19 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.const import CONF_VERIFY_SSL from homeassistant.const import CONF_VERIFY_SSL
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyunifi==2.13'] REQUIREMENTS = ['pyunifi==2.13']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port' CONF_PORT = 'port'
CONF_SITE_ID = 'site_id' CONF_SITE_ID = 'site_id'
CONF_DETECTION_TIME = 'detection_time'
DEFAULT_HOST = 'localhost' DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8443 DEFAULT_PORT = 8443
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
DEFAULT_DETECTION_TIME = timedelta(seconds=300)
NOTIFICATION_ID = 'unifi_notification' NOTIFICATION_ID = 'unifi_notification'
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup' NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
@ -32,7 +36,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any(
cv.boolean, cv.isfile),
vol.Optional(CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME): vol.All(
cv.time_period, cv.positive_timedelta)
}) })
@ -46,6 +53,7 @@ def get_scanner(hass, config):
site_id = config[DOMAIN].get(CONF_SITE_ID) site_id = config[DOMAIN].get(CONF_SITE_ID)
port = config[DOMAIN].get(CONF_PORT) port = config[DOMAIN].get(CONF_PORT)
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL) verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
detection_time = config[DOMAIN].get(CONF_DETECTION_TIME)
try: try:
ctrl = Controller(host, username, password, port, version='v4', ctrl = Controller(host, username, password, port, version='v4',
@ -61,14 +69,15 @@ def get_scanner(hass, config):
notification_id=NOTIFICATION_ID) notification_id=NOTIFICATION_ID)
return False return False
return UnifiScanner(ctrl) return UnifiScanner(ctrl, detection_time)
class UnifiScanner(DeviceScanner): class UnifiScanner(DeviceScanner):
"""Provide device_tracker support from Unifi WAP client data.""" """Provide device_tracker support from Unifi WAP client data."""
def __init__(self, controller): def __init__(self, controller, detection_time: timedelta):
"""Initialize the scanner.""" """Initialize the scanner."""
self._detection_time = detection_time
self._controller = controller self._controller = controller
self._update() self._update()
@ -81,7 +90,11 @@ class UnifiScanner(DeviceScanner):
_LOGGER.error("Failed to scan clients: %s", ex) _LOGGER.error("Failed to scan clients: %s", ex)
clients = [] clients = []
self._clients = {client['mac']: client for client in clients} self._clients = {
client['mac']: client
for client in clients
if (dt_util.utcnow() - dt_util.utc_from_timestamp(float(
client['last_seen']))) < self._detection_time}
def scan_devices(self): def scan_devices(self):
"""Scan for devices.""" """Scan for devices."""
@ -96,5 +109,5 @@ class UnifiScanner(DeviceScanner):
""" """
client = self._clients.get(mac, {}) client = self._clients.get(mac, {})
name = client.get('name') or client.get('hostname') name = client.get('name') or client.get('hostname')
_LOGGER.debug("Device %s name %s", mac, name) _LOGGER.debug("Device mac %s name %s", mac, name)
return name return name

View File

@ -6,7 +6,6 @@ https://home-assistant.io/components/device_tracker.upc_connect/
""" """
import asyncio import asyncio
import logging import logging
import xml.etree.ElementTree as ET
import aiohttp import aiohttp
import async_timeout import async_timeout
@ -19,6 +18,8 @@ from homeassistant.const import CONF_HOST
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['defusedxml==0.5.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_IP = '192.168.0.1' DEFAULT_IP = '192.168.0.1'
@ -63,6 +64,8 @@ class UPCDeviceScanner(DeviceScanner):
@asyncio.coroutine @asyncio.coroutine
def async_scan_devices(self): def async_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."""
import defusedxml.ElementTree as ET
if self.token is None: if self.token is None:
token_initialized = yield from self.async_initialize_token() token_initialized = yield from self.async_initialize_token()
if not token_initialized: if not token_initialized:

View File

@ -11,7 +11,7 @@ import voluptuous as vol
from homeassistant.const import CONF_DEVICE from homeassistant.const import CONF_DEVICE
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['enocean==0.31'] REQUIREMENTS = ['enocean==0.40']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -215,20 +215,12 @@ def async_setup(hass, config: dict):
target_fans = component.async_extract_from_service(service) target_fans = component.async_extract_from_service(service)
params.pop(ATTR_ENTITY_ID, None) params.pop(ATTR_ENTITY_ID, None)
update_tasks = []
for fan in target_fans: for fan in target_fans:
yield from getattr(fan, method['method'])(**params) yield from getattr(fan, method['method'])(**params)
update_tasks = []
for fan in target_fans:
if not fan.should_poll: if not fan.should_poll:
continue continue
update_tasks.append(fan.async_update_ha_state(True))
update_coro = hass.async_add_job(fan.async_update_ha_state(True))
if hasattr(fan, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -11,7 +11,7 @@ from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
SPEED_LOW, SPEED_MEDIUM, SPEED_LOW, SPEED_MEDIUM,
SPEED_HIGH) SPEED_HIGH)
import homeassistant.components.isy994 as isy import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_UNKNOWN, STATE_ON, STATE_OFF from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -73,19 +73,16 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
@property @property
def speed(self) -> str: def speed(self) -> str:
"""Return the current speed.""" """Return the current speed."""
return self.state return VALUE_TO_STATE.get(self.value)
@property @property
def state(self) -> str: def is_on(self) -> str:
"""Get the state of the ISY994 fan device.""" """Get if the fan is on."""
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN) return self.value != 0
def set_speed(self, speed: str) -> None: def set_speed(self, speed: str) -> None:
"""Send the set speed command to the ISY994 fan device.""" """Send the set speed command to the ISY994 fan device."""
if not self._node.on(val=STATE_TO_VALUE.get(speed, 0)): self._node.on(val=STATE_TO_VALUE.get(speed, 255))
_LOGGER.debug("Unable to set fan speed")
else:
self.speed = self.state
def turn_on(self, speed: str=None, **kwargs) -> None: def turn_on(self, speed: str=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 fan device.""" """Send the turn on command to the ISY994 fan device."""
@ -93,10 +90,7 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
def turn_off(self, **kwargs) -> None: def turn_off(self, **kwargs) -> None:
"""Send the turn off command to the ISY994 fan device.""" """Send the turn off command to the ISY994 fan device."""
if not self._node.off(): self._node.off()
_LOGGER.debug("Unable to set fan speed")
else:
self.speed = self.state
@property @property
def speed_list(self) -> list: def speed_list(self) -> list:

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['ha-ffmpeg==1.7'] REQUIREMENTS = ['ha-ffmpeg==1.9']
DOMAIN = 'ffmpeg' DOMAIN = 'ffmpeg'

View File

@ -225,8 +225,6 @@ def setup(hass, config):
if DATA_EXTRA_HTML_URL not in hass.data: if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set() hass.data[DATA_EXTRA_HTML_URL] = set()
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state', for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
'dev-template', 'dev-mqtt', 'kiosk'): 'dev-template', 'dev-mqtt', 'kiosk'):
register_built_in_panel(hass, panel) register_built_in_panel(hass, panel)

View File

@ -0,0 +1,52 @@
"""
Support for Actions on Google Assistant Smart Home Control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/google_assistant/
"""
import asyncio
import logging
import voluptuous as vol
# Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False:
from homeassistant.core import HomeAssistant # NOQA
from typing import Dict, Any # NOQA
from homeassistant.helpers import config_validation as cv
from .const import (
DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN,
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS
)
from .auth import GoogleAssistantAuthView
from .http import GoogleAssistantView
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: {
vol.Required(CONF_PROJECT_ID): cv.string,
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_ACCESS_TOKEN): cv.string,
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
}
},
extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
"""Activate Google Actions component."""
config = yaml_config.get(DOMAIN, {})
hass.http.register_view(GoogleAssistantAuthView(hass, config))
hass.http.register_view(GoogleAssistantView(hass, config))
return True

View File

@ -0,0 +1,86 @@
"""Google Assistant OAuth View."""
import asyncio
import logging
# Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False:
from homeassistant.core import HomeAssistant # NOQA
from aiohttp.web import Request, Response # NOQA
from typing import Dict, Any # NOQA
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (
HTTP_BAD_REQUEST,
HTTP_UNAUTHORIZED,
HTTP_MOVED_PERMANENTLY,
)
from .const import (
GOOGLE_ASSISTANT_API_ENDPOINT,
CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN
)
BASE_OAUTH_URL = 'https://oauth-redirect.googleusercontent.com'
REDIRECT_TEMPLATE_URL = \
'{}/r/{}#access_token={}&token_type=bearer&state={}'
_LOGGER = logging.getLogger(__name__)
class GoogleAssistantAuthView(HomeAssistantView):
"""Handle Google Actions auth requests."""
url = GOOGLE_ASSISTANT_API_ENDPOINT + '/auth'
name = 'api:google_assistant:auth'
requires_auth = False
def __init__(self, hass: HomeAssistant, cfg: Dict[str, Any]) -> None:
"""Initialize instance of the view."""
super().__init__()
self.project_id = cfg.get(CONF_PROJECT_ID)
self.client_id = cfg.get(CONF_CLIENT_ID)
self.access_token = cfg.get(CONF_ACCESS_TOKEN)
@asyncio.coroutine
def get(self, request: Request) -> Response:
"""Handle oauth token request."""
query = request.query
redirect_uri = query.get('redirect_uri')
if not redirect_uri:
msg = 'missing redirect_uri field'
_LOGGER.warning(msg)
return self.json_message(msg, status_code=HTTP_BAD_REQUEST)
if self.project_id not in redirect_uri:
msg = 'missing project_id in redirect_uri'
_LOGGER.warning(msg)
return self.json_message(msg, status_code=HTTP_BAD_REQUEST)
state = query.get('state')
if not state:
msg = 'oauth request missing state'
_LOGGER.warning(msg)
return self.json_message(msg, status_code=HTTP_BAD_REQUEST)
client_id = query.get('client_id')
if self.client_id != client_id:
msg = 'invalid client id'
_LOGGER.warning(msg)
return self.json_message(msg, status_code=HTTP_UNAUTHORIZED)
generated_url = redirect_url(self.project_id, self.access_token, state)
_LOGGER.info('user login in from Google Assistant')
return self.json_message(
'redirect success',
status_code=HTTP_MOVED_PERMANENTLY,
headers={'Location': generated_url})
def redirect_url(project_id: str, access_token: str, state: str) -> str:
"""Generate the redirect format for the oauth request."""
return REDIRECT_TEMPLATE_URL.format(BASE_OAUTH_URL, project_id,
access_token, state)

View File

@ -0,0 +1,37 @@
"""Constants for Google Assistant."""
DOMAIN = 'google_assistant'
GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant'
ATTR_GOOGLE_ASSISTANT = 'google_assistant'
ATTR_GOOGLE_ASSISTANT_NAME = 'google_assistant_name'
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
CONF_EXPOSED_DOMAINS = 'exposed_domains'
CONF_PROJECT_ID = 'project_id'
CONF_ACCESS_TOKEN = 'access_token'
CONF_CLIENT_ID = 'client_id'
CONF_ALIASES = 'aliases'
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
'switch', 'light', 'group', 'media_player', 'fan', 'cover'
]
PREFIX_TRAITS = 'action.devices.traits.'
TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff'
TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness'
TRAIT_RGB_COLOR = PREFIX_TRAITS + 'ColorSpectrum'
TRAIT_COLOR_TEMP = PREFIX_TRAITS + 'ColorTemperature'
TRAIT_SCENE = PREFIX_TRAITS + 'Scene'
PREFIX_COMMANDS = 'action.devices.commands.'
COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff'
COMMAND_BRIGHTNESS = PREFIX_COMMANDS + 'BrightnessAbsolute'
COMMAND_COLOR = PREFIX_COMMANDS + 'ColorAbsolute'
COMMAND_ACTIVATESCENE = PREFIX_COMMANDS + 'ActivateScene'
PREFIX_TYPES = 'action.devices.types.'
TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
TYPE_SWITCH = PREFIX_TYPES + 'SWITCH'
TYPE_SCENE = PREFIX_TYPES + 'SCENE'

View File

@ -0,0 +1,180 @@
"""
Support for Google Actions Smart Home Control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/google_assistant/
"""
import asyncio
import logging
# Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False:
from homeassistant.core import HomeAssistant # NOQA
from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED)
from .const import (
GOOGLE_ASSISTANT_API_ENDPOINT,
CONF_ACCESS_TOKEN,
DEFAULT_EXPOSE_BY_DEFAULT,
DEFAULT_EXPOSED_DOMAINS,
CONF_EXPOSE_BY_DEFAULT,
CONF_EXPOSED_DOMAINS,
ATTR_GOOGLE_ASSISTANT)
from .smart_home import entity_to_device, query_device, determine_service
_LOGGER = logging.getLogger(__name__)
class GoogleAssistantView(HomeAssistantView):
"""Handle Google Assistant requests."""
url = GOOGLE_ASSISTANT_API_ENDPOINT
name = 'api:google_assistant'
requires_auth = False # Uses access token from oauth flow
def __init__(self, hass: HomeAssistant, cfg: Dict[str, Any]) -> None:
"""Initialize Google Assistant view."""
super().__init__()
self.access_token = cfg.get(CONF_ACCESS_TOKEN)
self.expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT,
DEFAULT_EXPOSE_BY_DEFAULT)
self.exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS,
DEFAULT_EXPOSED_DOMAINS)
def is_entity_exposed(self, entity) -> bool:
"""Determine if an entity should be exposed to Google Assistant."""
if entity.attributes.get('view') is not None:
# Ignore entities that are views
return False
domain = entity.domain.lower()
explicit_expose = entity.attributes.get(ATTR_GOOGLE_ASSISTANT, None)
domain_exposed_by_default = \
self.expose_by_default and domain in self.exposed_domains
# Expose an entity if the entity's domain is exposed by default and
# the configuration doesn't explicitly exclude it from being
# exposed, or if the entity is explicitly exposed
is_default_exposed = \
domain_exposed_by_default and explicit_expose is not False
return is_default_exposed or explicit_expose
@asyncio.coroutine
def handle_sync(self, hass: HomeAssistant, request_id: str):
"""Handle SYNC action."""
devices = []
for entity in hass.states.async_all():
if not self.is_entity_exposed(entity):
continue
device = entity_to_device(entity)
if device is None:
_LOGGER.warning("No mapping for %s domain", entity.domain)
continue
devices.append(device)
return self.json(
make_actions_response(request_id, {'devices': devices}))
@asyncio.coroutine
def handle_query(self,
hass: HomeAssistant,
request_id: str,
requested_devices: list):
"""Handle the QUERY action."""
devices = {}
for device in requested_devices:
devid = device.get('id')
# In theory this should never happpen
if not devid:
_LOGGER.error('Device missing ID: %s', device)
continue
state = hass.states.get(devid)
if not state:
# If we can't find a state, the device is offline
devices[devid] = {'online': False}
devices[devid] = query_device(state)
return self.json(
make_actions_response(request_id, {'devices': devices}))
@asyncio.coroutine
def handle_execute(self,
hass: HomeAssistant,
request_id: str,
requested_commands: list):
"""Handle the EXECUTE action."""
commands = []
for command in requested_commands:
ent_ids = [ent.get('id') for ent in command.get('devices', [])]
execution = command.get('execution')[0]
for eid in ent_ids:
domain = eid.split('.')[0]
(service, service_data) = determine_service(
eid, execution.get('command'), execution.get('params'))
success = yield from hass.services.async_call(
domain, service, service_data, blocking=True)
result = {"ids": [eid], "states": {}}
if success:
result['status'] = 'SUCCESS'
else:
result['status'] = 'ERROR'
commands.append(result)
return self.json(
make_actions_response(request_id, {'commands': commands}))
@asyncio.coroutine
def post(self, request: Request) -> Response:
"""Handle Google Assistant requests."""
auth = request.headers.get('Authorization', None)
if 'Bearer {}'.format(self.access_token) != auth:
return self.json_message(
"missing authorization", status_code=HTTP_UNAUTHORIZED)
data = yield from request.json() # type: dict
inputs = data.get('inputs') # type: list
if len(inputs) != 1:
_LOGGER.error('Too many inputs in request %d', len(inputs))
return self.json_message(
"too many inputs", status_code=HTTP_BAD_REQUEST)
request_id = data.get('requestId') # type: str
intent = inputs[0].get('intent')
payload = inputs[0].get('payload')
hass = request.app['hass'] # type: HomeAssistant
res = None
if intent == 'action.devices.SYNC':
res = yield from self.handle_sync(hass, request_id)
elif intent == 'action.devices.QUERY':
res = yield from self.handle_query(hass, request_id,
payload.get('devices', []))
elif intent == 'action.devices.EXECUTE':
res = yield from self.handle_execute(hass, request_id,
payload.get('commands', []))
if res:
return res
return self.json_message(
"invalid intent", status_code=HTTP_BAD_REQUEST)
def make_actions_response(request_id: str, payload: dict) -> dict:
"""Helper to simplify format for response."""
return {'requestId': request_id, 'payload': payload}

View File

@ -0,0 +1,161 @@
"""Support for Google Assistant Smart Home API."""
import logging
# Typing imports
# pylint: disable=using-constant-test,unused-import,ungrouped-imports
# if False:
from aiohttp.web import Request, Response # NOQA
from typing import Dict, Tuple, Any # NOQA
from homeassistant.helpers.entity import Entity # NOQA
from homeassistant.core import HomeAssistant # NOQA
from homeassistant.const import (
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID,
CONF_FRIENDLY_NAME, STATE_OFF,
SERVICE_TURN_OFF, SERVICE_TURN_ON
)
from homeassistant.components import (
switch, light, cover, media_player, group, fan, scene
)
from .const import (
ATTR_GOOGLE_ASSISTANT_NAME,
COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE,
TRAIT_ONOFF, TRAIT_BRIGHTNESS, TRAIT_COLOR_TEMP,
TRAIT_RGB_COLOR, TRAIT_SCENE,
TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH,
CONF_ALIASES,
)
_LOGGER = logging.getLogger(__name__)
# Mapping is [actions schema, primary trait, optional features]
# optional is SUPPORT_* = (trait, command)
MAPPING_COMPONENT = {
group.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None],
scene.DOMAIN: [TYPE_SCENE, TRAIT_SCENE, None],
switch.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None],
fan.DOMAIN: [TYPE_SWITCH, TRAIT_ONOFF, None],
light.DOMAIN: [
TYPE_LIGHT, TRAIT_ONOFF, {
light.SUPPORT_BRIGHTNESS: TRAIT_BRIGHTNESS,
light.SUPPORT_RGB_COLOR: TRAIT_RGB_COLOR,
light.SUPPORT_COLOR_TEMP: TRAIT_COLOR_TEMP,
}
],
cover.DOMAIN: [
TYPE_LIGHT, TRAIT_ONOFF, {
cover.SUPPORT_SET_POSITION: TRAIT_BRIGHTNESS
}
],
media_player.DOMAIN: [
TYPE_LIGHT, TRAIT_ONOFF, {
media_player.SUPPORT_VOLUME_SET: TRAIT_BRIGHTNESS
}
],
} # type: Dict[str, list]
def make_actions_response(request_id: str, payload: dict) -> dict:
"""Helper to simplify format for response."""
return {'requestId': request_id, 'payload': payload}
def entity_to_device(entity: Entity):
"""Convert a hass entity into an google actions device."""
class_data = MAPPING_COMPONENT.get(entity.domain)
if class_data is None:
return None
device = {
'id': entity.entity_id,
'name': {},
'traits': [],
'willReportState': False,
}
device['type'] = class_data[0]
device['traits'].append(class_data[1])
# handle custom names
device['name']['name'] = \
entity.attributes.get(ATTR_GOOGLE_ASSISTANT_NAME) or \
entity.attributes.get(CONF_FRIENDLY_NAME)
# use aliases
aliases = entity.attributes.get(CONF_ALIASES)
if isinstance(aliases, list):
device['name']['nicknames'] = aliases
else:
_LOGGER.warning("%s must be a list", CONF_ALIASES)
# add trait if entity supports feature
if class_data[2]:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
for feature, trait in class_data[2].items():
if feature & supported > 0:
device['traits'].append(trait)
return device
def query_device(entity: Entity) -> dict:
"""Take an entity and return a properly formatted device object."""
final_state = entity.state != STATE_OFF
final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
if final_state else 0)
if entity.domain == media_player.DOMAIN:
level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL, 1.0
if final_state else 0.0)
# Convert 0.0-1.0 to 0-255
final_brightness = round(min(1.0, level) * 255)
if final_brightness is None:
final_brightness = 255 if final_state else 0
final_brightness = 100 * (final_brightness / 255)
return {
"on": final_state,
"online": True,
"brightness": int(final_brightness)
}
# erroneous bug on old pythons and pylint
# https://github.com/PyCQA/pylint/issues/1212
# pylint: disable=invalid-sequence-index
def determine_service(entity_id: str, command: str,
params: dict) -> Tuple[str, dict]:
"""
Determine service and service_data.
Attempt to return a tuple of service and service_data based on the entity
and action requested.
"""
domain = entity_id.split('.')[0]
service_data = {ATTR_ENTITY_ID: entity_id} # type: Dict[str, Any]
# special media_player handling
if domain == media_player.DOMAIN and command == COMMAND_BRIGHTNESS:
brightness = params.get('brightness', 0)
service_data[media_player.ATTR_MEDIA_VOLUME_LEVEL] = brightness / 100
return (media_player.SERVICE_VOLUME_SET, service_data)
# special cover handling
if domain == cover.DOMAIN:
if command == COMMAND_BRIGHTNESS:
service_data['position'] = params.get('brightness', 0)
return (cover.SERVICE_SET_COVER_POSITION, service_data)
if command == COMMAND_ONOFF and params.get('on') is True:
return (cover.SERVICE_OPEN_COVER, service_data)
return (cover.SERVICE_CLOSE_COVER, service_data)
if command == COMMAND_BRIGHTNESS:
brightness = params.get('brightness')
service_data['brightness'] = int(brightness / 100 * 255)
return (SERVICE_TURN_ON, service_data)
if command == COMMAND_ACTIVATESCENE or (COMMAND_ONOFF == command and
params.get('on') is True):
return (SERVICE_TURN_ON, service_data)
return (SERVICE_TURN_OFF, service_data)

View File

@ -17,7 +17,8 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT from homeassistant.const import (
CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT, CONF_TIME_ZONE)
from homeassistant.components.http import ( from homeassistant.components.http import (
HomeAssistantView, KEY_AUTHENTICATED, CONF_API_PASSWORD, CONF_SERVER_PORT, HomeAssistantView, KEY_AUTHENTICATED, CONF_API_PASSWORD, CONF_SERVER_PORT,
CONF_SSL_CERTIFICATE) CONF_SSL_CERTIFICATE)
@ -33,6 +34,8 @@ SERVICE_ADDON_START = 'addon_start'
SERVICE_ADDON_STOP = 'addon_stop' SERVICE_ADDON_STOP = 'addon_stop'
SERVICE_ADDON_RESTART = 'addon_restart' SERVICE_ADDON_RESTART = 'addon_restart'
SERVICE_ADDON_STDIN = 'addon_stdin' SERVICE_ADDON_STDIN = 'addon_stdin'
SERVICE_HOST_SHUTDOWN = 'host_shutdown'
SERVICE_HOST_REBOOT = 'host_reboot'
ATTR_ADDON = 'addon' ATTR_ADDON = 'addon'
ATTR_INPUT = 'input' ATTR_INPUT = 'input'
@ -63,6 +66,8 @@ MAP_SERVICE_API = {
SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON), SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON),
SERVICE_ADDON_RESTART: ('/addons/{addon}/restart', SCHEMA_ADDON), SERVICE_ADDON_RESTART: ('/addons/{addon}/restart', SCHEMA_ADDON),
SERVICE_ADDON_STDIN: ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN), SERVICE_ADDON_STDIN: ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN),
SERVICE_HOST_SHUTDOWN: ('/host/shutdown', None),
SERVICE_HOST_REBOOT: ('/host/reboot', None),
} }
@ -89,13 +94,16 @@ def async_setup(hass, config):
'mdi:access-point-network') 'mdi:access-point-network')
if 'http' in config: if 'http' in config:
yield from hassio.update_hass_api(config.get('http')) yield from hassio.update_hass_api(config['http'])
if 'homeassistant' in config:
yield from hassio.update_hass_timezone(config['homeassistant'])
@asyncio.coroutine @asyncio.coroutine
def async_service_handler(service): def async_service_handler(service):
"""Handle service calls for HassIO.""" """Handle service calls for HassIO."""
api_command = MAP_SERVICE_API[service.service][0] api_command = MAP_SERVICE_API[service.service][0]
addon = service.data[ATTR_ADDON] addon = service.data.get(ATTR_ADDON)
data = service.data[ATTR_INPUT] if ATTR_INPUT in service.data else None data = service.data[ATTR_INPUT] if ATTR_INPUT in service.data else None
yield from hassio.send_command( yield from hassio.send_command(
@ -138,6 +146,15 @@ class HassIO(object):
return self.send_command("/homeassistant/options", payload=options) return self.send_command("/homeassistant/options", payload=options)
def update_hass_timezone(self, core_config):
"""Update Home-Assistant timezone data on HassIO.
This method return a coroutine.
"""
return self.send_command("/supervisor/options", payload={
'timezone': core_config.get(CONF_TIME_ZONE)
})
@asyncio.coroutine @asyncio.coroutine
def send_command(self, command, method="post", payload=None, timeout=10): def send_command(self, command, method="post", payload=None, timeout=10):
"""Send API command to HassIO. """Send API command to HassIO.

View File

@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
REQUIREMENTS = ['pyhomematic==0.1.32'] REQUIREMENTS = ['pyhomematic==0.1.34']
DOMAIN = 'homematic' DOMAIN = 'homematic'
@ -69,7 +69,8 @@ HM_DEVICE_TYPES = {
'IPSmoke'], 'IPSmoke'],
DISCOVER_CLIMATE: [ DISCOVER_CLIMATE: [
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall'], 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
'ThermostatGroup'],
DISCOVER_BINARY_SENSORS: [ DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact',
@ -129,6 +130,7 @@ CONF_LOCAL_IP = 'local_ip'
CONF_LOCAL_PORT = 'local_port' CONF_LOCAL_PORT = 'local_port'
CONF_IP = 'ip' CONF_IP = 'ip'
CONF_PORT = 'port' CONF_PORT = 'port'
CONF_PATH = 'path'
CONF_CALLBACK_IP = 'callback_ip' CONF_CALLBACK_IP = 'callback_ip'
CONF_CALLBACK_PORT = 'callback_port' CONF_CALLBACK_PORT = 'callback_port'
CONF_RESOLVENAMES = 'resolvenames' CONF_RESOLVENAMES = 'resolvenames'
@ -140,6 +142,7 @@ DEFAULT_LOCAL_IP = '0.0.0.0'
DEFAULT_LOCAL_PORT = 0 DEFAULT_LOCAL_PORT = 0
DEFAULT_RESOLVENAMES = False DEFAULT_RESOLVENAMES = False
DEFAULT_PORT = 2001 DEFAULT_PORT = 2001
DEFAULT_PATH = ''
DEFAULT_USERNAME = 'Admin' DEFAULT_USERNAME = 'Admin'
DEFAULT_PASSWORD = '' DEFAULT_PASSWORD = ''
DEFAULT_VARIABLES = False DEFAULT_VARIABLES = False
@ -160,8 +163,8 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
vol.Required(CONF_HOSTS): {cv.match_all: { vol.Required(CONF_HOSTS): {cv.match_all: {
vol.Required(CONF_IP): cv.string, vol.Required(CONF_IP): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
cv.port, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES): vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
@ -258,6 +261,7 @@ def setup(hass, config):
remotes[rname] = {} remotes[rname] = {}
remotes[rname][CONF_IP] = server remotes[rname][CONF_IP] = server
remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT) remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT)
remotes[rname][CONF_PATH] = rconfig.get(CONF_PATH)
remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES) remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES)
remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME) remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME)
remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD) remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD)

View File

@ -358,19 +358,21 @@ class HomeAssistantView(object):
requires_auth = True # Views inheriting from this class can override this requires_auth = True # Views inheriting from this class can override this
# pylint: disable=no-self-use # pylint: disable=no-self-use
def json(self, result, status_code=200): def json(self, result, status_code=200, headers=None):
"""Return a JSON response.""" """Return a JSON response."""
msg = json.dumps( msg = json.dumps(
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8') result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
return web.Response( return web.Response(
body=msg, content_type=CONTENT_TYPE_JSON, status=status_code) body=msg, content_type=CONTENT_TYPE_JSON, status=status_code,
headers=headers)
def json_message(self, message, status_code=200, message_code=None): def json_message(self, message, status_code=200, message_code=None,
headers=None):
"""Return a JSON message response.""" """Return a JSON message response."""
data = {'message': message} data = {'message': message}
if message_code is not None: if message_code is not None:
data['code'] = message_code data['code'] = message_code
return self.json(data, status_code) return self.json(data, status_code, headers=headers)
@asyncio.coroutine @asyncio.coroutine
# pylint: disable=no-self-use # pylint: disable=no-self-use

View File

@ -26,6 +26,7 @@ CONF_KNX_TUNNELING = "tunneling"
CONF_KNX_LOCAL_IP = "local_ip" CONF_KNX_LOCAL_IP = "local_ip"
CONF_KNX_FIRE_EVENT = "fire_event" CONF_KNX_FIRE_EVENT = "fire_event"
CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter" CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter"
CONF_KNX_STATE_UPDATER = "state_updater"
SERVICE_KNX_SEND = "send" SERVICE_KNX_SEND = "send"
SERVICE_KNX_ATTR_ADDRESS = "address" SERVICE_KNX_ATTR_ADDRESS = "address"
@ -35,7 +36,7 @@ ATTR_DISCOVER_DEVICES = 'devices'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['xknx==0.7.14'] REQUIREMENTS = ['xknx==0.7.16']
TUNNELING_SCHEMA = vol.Schema({ TUNNELING_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@ -58,7 +59,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'): vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'):
vol.All( vol.All(
cv.ensure_list, cv.ensure_list,
[cv.string]) [cv.string]),
vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean,
}) })
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -134,7 +136,7 @@ class KNXModule(object):
"""Start KNX object. Connect to tunneling or Routing device.""" """Start KNX object. Connect to tunneling or Routing device."""
connection_config = self.connection_config() connection_config = self.connection_config()
yield from self.xknx.start( yield from self.xknx.start(
state_updater=True, state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
connection_config=connection_config) connection_config=connection_config)
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
self.initialized = True self.initialized = True

View File

@ -274,6 +274,7 @@ def async_setup(hass, config):
preprocess_turn_on_alternatives(params) preprocess_turn_on_alternatives(params)
update_tasks = []
for light in target_lights: for light in target_lights:
if service.service == SERVICE_TURN_ON: if service.service == SERVICE_TURN_ON:
yield from light.async_turn_on(**params) yield from light.async_turn_on(**params)
@ -282,18 +283,9 @@ def async_setup(hass, config):
else: else:
yield from light.async_toggle(**params) yield from light.async_toggle(**params)
update_tasks = []
for light in target_lights:
if not light.should_poll: if not light.should_poll:
continue continue
update_tasks.append(light.async_update_ha_state(True))
update_coro = hass.async_add_job(
light.async_update_ha_state(True))
if hasattr(light, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -263,7 +263,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
# create a service for calling run_scene directly on the bridge, # create a service for calling run_scene directly on the bridge,
# used to simplify automation rules. # used to simplify automation rules.
def hue_activate_scene(call): def hue_activate_scene(call):
"""Service to call directly directly into bridge to set scenes.""" """Service to call directly into bridge to set scenes."""
group_name = call.data[ATTR_GROUP_NAME] group_name = call.data[ATTR_GROUP_NAME]
scene_name = call.data[ATTR_SCENE_NAME] scene_name = call.data[ATTR_SCENE_NAME]
bridge.run_scene(group_name, scene_name) bridge.run_scene(group_name, scene_name)

View File

@ -213,7 +213,7 @@ class MqttJson(Light):
except KeyError: except KeyError:
pass pass
except ValueError: except ValueError:
_LOGGER.warning("Invalid white value value received") _LOGGER.warning("Invalid white value received")
if self._xy is not None: if self._xy is not None:
try: try:

View File

@ -269,7 +269,7 @@ class OsramLightifyGroup(Luminary):
def _get_state(self): def _get_state(self):
"""Get state of group. """Get state of group.
The group is on, if any of the lights in on. The group is on, if any of the lights is on.
""" """
lights = self._bridge.lights() lights = self._bridge.lights()
return any(lights[light_id].on() for light_id in self._light_ids) return any(lights[light_id].on() for light_id in self._light_ids)

View File

@ -0,0 +1,87 @@
"""
Light/LED support for the Skybell HD Doorbell.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.skybell/
"""
import logging
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_RGB_COLOR,
SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light)
from homeassistant.components.skybell import (
DOMAIN as SKYBELL_DOMAIN, SkybellDevice)
DEPENDENCIES = ['skybell']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the platform for a Skybell device."""
skybell = hass.data.get(SKYBELL_DOMAIN)
sensors = []
for device in skybell.get_devices():
sensors.append(SkybellLight(device))
add_devices(sensors, True)
def _to_skybell_level(level):
"""Convert the given HASS light level (0-255) to Skybell (0-100)."""
return int((level * 100) / 255)
def _to_hass_level(level):
"""Convert the given Skybell (0-100) light level to HASS (0-255)."""
return int((level * 255) / 100)
class SkybellLight(SkybellDevice, Light):
"""A binary sensor implementation for Skybell devices."""
def __init__(self, device):
"""Initialize a light for a Skybell device."""
super().__init__(device)
self._name = self._device.name
@property
def name(self):
"""Return the name of the sensor."""
return self._name
def turn_on(self, **kwargs):
"""Turn on the light."""
if ATTR_RGB_COLOR in kwargs:
self._device.led_rgb = kwargs[ATTR_RGB_COLOR]
elif ATTR_BRIGHTNESS in kwargs:
self._device.led_intensity = _to_skybell_level(
kwargs[ATTR_BRIGHTNESS])
else:
self._device.led_intensity = _to_skybell_level(255)
def turn_off(self, **kwargs):
"""Turn off the light."""
self._device.led_intensity = 0
@property
def is_on(self):
"""Return true if device is on."""
return self._device.led_intensity > 0
@property
def brightness(self):
"""Return the brightness of the light."""
return _to_hass_level(self._device.led_intensity)
@property
def rgb_color(self):
"""Return the color of the light."""
return self._device.led_rgb
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR

View File

@ -14,26 +14,22 @@ from homeassistant.components.light import (
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS) ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
from homeassistant.const import ( from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, STATE_ON, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, STATE_ON,
STATE_OFF, EVENT_HOMEASSISTANT_START, MATCH_ALL STATE_OFF, EVENT_HOMEASSISTANT_START, MATCH_ALL, CONF_LIGHTS)
)
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.helpers.restore_state import async_get_last_state
from homeassistant.helpers.script import Script from homeassistant.helpers.script import Script
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false'] _VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false']
CONF_LIGHTS = 'lights'
CONF_ON_ACTION = 'turn_on' CONF_ON_ACTION = 'turn_on'
CONF_OFF_ACTION = 'turn_off' CONF_OFF_ACTION = 'turn_off'
CONF_LEVEL_ACTION = 'set_level' CONF_LEVEL_ACTION = 'set_level'
CONF_LEVEL_TEMPLATE = 'level_template' CONF_LEVEL_TEMPLATE = 'level_template'
LIGHT_SCHEMA = vol.Schema({ LIGHT_SCHEMA = vol.Schema({
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
@ -51,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up Template Lights.""" """Set up the Template Lights."""
lights = [] lights = []
for device, device_config in config[CONF_LIGHTS].items(): for device, device_config in config[CONF_LIGHTS].items():
@ -90,7 +86,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
_LOGGER.error("No lights added") _LOGGER.error("No lights added")
return False return False
async_add_devices(lights, True) async_add_devices(lights)
return True return True
@ -153,10 +149,6 @@ class LightTemplate(Light):
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
state = yield from async_get_last_state(self.hass, self.entity_id)
if state:
self._state = state.state == STATE_ON
@callback @callback
def template_light_state_listener(entity, old_state, new_state): def template_light_state_listener(entity, old_state, new_state):
"""Handle target device state changes.""" """Handle target device state changes."""
@ -210,6 +202,7 @@ class LightTemplate(Light):
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Update the state from the template.""" """Update the state from the template."""
print("ASYNC UPDATE")
if self._template is not None: if self._template is not None:
try: try:
state = self._template.async_render().lower() state = self._template.async_render().lower()

View File

@ -6,6 +6,8 @@ https://home-assistant.io/components/light.tplink/
""" """
import logging import logging
import colorsys import colorsys
import time
from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.const import (CONF_HOST, CONF_NAME)
from homeassistant.components.light import ( from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR, Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR,
@ -17,11 +19,13 @@ from homeassistant.util.color import (
from typing import Tuple from typing import Tuple
REQUIREMENTS = ['pyHS100==0.2.4.2'] REQUIREMENTS = ['pyHS100==0.3.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_TPLINK = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) ATTR_CURRENT_CONSUMPTION = 'current_consumption'
ATTR_DAILY_CONSUMPTION = 'daily_consumption'
ATTR_MONTHLY_CONSUMPTION = 'monthly_consumption'
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -64,24 +68,26 @@ class TPLinkSmartBulb(Light):
def __init__(self, smartbulb: 'SmartBulb', name): def __init__(self, smartbulb: 'SmartBulb', name):
"""Initialize the bulb.""" """Initialize the bulb."""
self.smartbulb = smartbulb self.smartbulb = smartbulb
self._name = None
# Use the name set on the device if not set if name is not None:
if name is None:
self._name = self.smartbulb.alias
else:
self._name = name self._name = name
self._state = None self._state = None
self._color_temp = None self._color_temp = None
self._brightness = None self._brightness = None
self._rgb = None self._rgb = None
_LOGGER.debug("Setting up TP-Link Smart Bulb") self._supported_features = 0
self._emeter_params = {}
@property @property
def name(self): def name(self):
"""Return the name of the Smart Bulb, if any.""" """Return the name of the Smart Bulb, if any."""
return self._name return self._name
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
return self._emeter_params
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
"""Turn the light on.""" """Turn the light on."""
self.smartbulb.state = self.smartbulb.BULB_STATE_ON self.smartbulb.state = self.smartbulb.BULB_STATE_ON
@ -119,30 +125,57 @@ class TPLinkSmartBulb(Light):
@property @property
def is_on(self): def is_on(self):
"""True if device is on.""" """Return True if device is on."""
return self._state return self._state
def update(self): def update(self):
"""Update the TP-Link Bulb's state.""" """Update the TP-Link Bulb's state."""
from pyHS100 import SmartPlugException from pyHS100 import SmartDeviceException
try: try:
if self._supported_features == 0:
self.get_features()
self._state = ( self._state = (
self.smartbulb.state == self.smartbulb.BULB_STATE_ON) self.smartbulb.state == self.smartbulb.BULB_STATE_ON)
if self._name is None:
self._name = self.smartbulb.alias
if self._supported_features & SUPPORT_BRIGHTNESS:
self._brightness = brightness_from_percentage( self._brightness = brightness_from_percentage(
self.smartbulb.brightness) self.smartbulb.brightness)
if self.smartbulb.is_color: if self._supported_features & SUPPORT_COLOR_TEMP:
if (self.smartbulb.color_temp is not None and if (self.smartbulb.color_temp is not None and
self.smartbulb.color_temp != 0): self.smartbulb.color_temp != 0):
self._color_temp = kelvin_to_mired( self._color_temp = kelvin_to_mired(
self.smartbulb.color_temp) self.smartbulb.color_temp)
if self._supported_features & SUPPORT_RGB_COLOR:
self._rgb = hsv_to_rgb(self.smartbulb.hsv) self._rgb = hsv_to_rgb(self.smartbulb.hsv)
except (SmartPlugException, OSError) as ex: if self.smartbulb.has_emeter:
_LOGGER.warning('Could not read state for %s: %s', self.name, ex) self._emeter_params[ATTR_CURRENT_CONSUMPTION] \
= "%.1f W" % self.smartbulb.current_consumption()
daily_statistics = self.smartbulb.get_emeter_daily()
monthly_statistics = self.smartbulb.get_emeter_monthly()
try:
self._emeter_params[ATTR_DAILY_CONSUMPTION] \
= "%.2f kW" % daily_statistics[int(
time.strftime("%d"))]
self._emeter_params[ATTR_MONTHLY_CONSUMPTION] \
= "%.2f kW" % monthly_statistics[int(
time.strftime("%m"))]
except KeyError:
# device returned no daily/monthly history
pass
except (SmartDeviceException, OSError) as ex:
_LOGGER.warning('Could not read state for %s: %s', self._name, ex)
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
supported_features = SUPPORT_TPLINK return self._supported_features
def get_features(self):
"""Determine all supported features in one go."""
if self.smartbulb.is_dimmable:
self._supported_features += SUPPORT_BRIGHTNESS
if self.smartbulb.is_variable_color_temp:
self._supported_features += SUPPORT_COLOR_TEMP
if self.smartbulb.is_color: if self.smartbulb.is_color:
supported_features += SUPPORT_RGB_COLOR self._supported_features += SUPPORT_RGB_COLOR
return supported_features

View File

@ -40,7 +40,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
devices_command = gateway.get_devices() devices_command = gateway.get_devices()
devices_commands = yield from api(devices_command) devices_commands = yield from api(devices_command)
devices = yield from api(*devices_commands) devices = yield from api(devices_commands)
lights = [dev for dev in devices if dev.has_light_control] lights = [dev for dev in devices if dev.has_light_control]
if lights: if lights:
async_add_devices(TradfriLight(light, api) for light in lights) async_add_devices(TradfriLight(light, api) for light in lights)
@ -49,7 +49,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if allow_tradfri_groups: if allow_tradfri_groups:
groups_command = gateway.get_groups() groups_command = gateway.get_groups()
groups_commands = yield from api(groups_command) groups_commands = yield from api(groups_command)
groups = yield from api(*groups_commands) groups = yield from api(groups_commands)
if groups: if groups:
async_add_devices(TradfriGroup(group, api) for group in groups) async_add_devices(TradfriGroup(group, api) for group in groups)

View File

@ -54,6 +54,10 @@ SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
SUPPORT_EFFECT | SUPPORT_EFFECT |
SUPPORT_COLOR_TEMP) SUPPORT_COLOR_TEMP)
YEELIGHT_MIN_KELVIN = YEELIGHT_MAX_KELVIN = 2700
YEELIGHT_RGB_MIN_KELVIN = 1700
YEELIGHT_RGB_MAX_KELVIN = 6500
EFFECT_DISCO = "Disco" EFFECT_DISCO = "Disco"
EFFECT_TEMP = "Slow Temp" EFFECT_TEMP = "Slow Temp"
EFFECT_STROBE = "Strobe epilepsy!" EFFECT_STROBE = "Strobe epilepsy!"
@ -191,6 +195,20 @@ class YeelightLight(Light):
"""Return the brightness of this light between 1..255.""" """Return the brightness of this light between 1..255."""
return self._brightness return self._brightness
@property
def min_mireds(self):
"""Return minimum supported color temperature."""
if self.supported_features & SUPPORT_COLOR_TEMP:
return kelvin_to_mired(YEELIGHT_RGB_MAX_KELVIN)
return kelvin_to_mired(YEELIGHT_MAX_KELVIN)
@property
def max_mireds(self):
"""Return maximum supported color temperature."""
if self.supported_features & SUPPORT_COLOR_TEMP:
return kelvin_to_mired(YEELIGHT_RGB_MIN_KELVIN)
return kelvin_to_mired(YEELIGHT_MIN_KELVIN)
def _get_rgb_from_properties(self): def _get_rgb_from_properties(self):
rgb = self._properties.get('rgb', None) rgb = self._properties.get('rgb', None)
color_mode = self._properties.get('color_mode', None) color_mode = self._properties.get('color_mode', None)

View File

@ -90,24 +90,16 @@ def async_setup(hass, config):
code = service.data.get(ATTR_CODE) code = service.data.get(ATTR_CODE)
update_tasks = []
for entity in target_locks: for entity in target_locks:
if service.service == SERVICE_LOCK: if service.service == SERVICE_LOCK:
yield from entity.async_lock(code=code) yield from entity.async_lock(code=code)
else: else:
yield from entity.async_unlock(code=code) yield from entity.async_unlock(code=code)
update_tasks = []
for entity in target_locks:
if not entity.should_poll: if not entity.should_poll:
continue continue
update_tasks.append(entity.async_update_ha_state(True))
update_coro = hass.async_add_job(
entity.async_update_ha_state(True))
if hasattr(entity, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -29,20 +29,17 @@ class TeslaLock(TeslaDevice, LockDevice):
"""Initialisation of the lock.""" """Initialisation of the lock."""
self._state = None self._state = None
super().__init__(tesla_device, controller) super().__init__(tesla_device, controller)
self._name = self.tesla_device.name
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id) self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
def lock(self, **kwargs): def lock(self, **kwargs):
"""Send the lock command.""" """Send the lock command."""
_LOGGER.debug("Locking doors for: %s", self._name) _LOGGER.debug("Locking doors for: %s", self._name)
self.tesla_device.lock() self.tesla_device.lock()
self._state = STATE_LOCKED
def unlock(self, **kwargs): def unlock(self, **kwargs):
"""Send the unlock command.""" """Send the unlock command."""
_LOGGER.debug("Unlocking doors for: %s", self._name) _LOGGER.debug("Unlocking doors for: %s", self._name)
self.tesla_device.unlock() self.tesla_device.unlock()
self._state = STATE_UNLOCKED
@property @property
def is_locked(self): def is_locked(self):

View File

@ -0,0 +1,18 @@
"""
Provides a map panel for showing device locations.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/map/
"""
import asyncio
from homeassistant.components.frontend import register_built_in_panel
DOMAIN = 'map'
@asyncio.coroutine
def async_setup(hass, config):
"""Register the built-in map panel."""
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
return True

View File

@ -15,7 +15,7 @@ from homeassistant.components.media_player import (
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
REQUIREMENTS = ['youtube_dl==2017.10.01'] REQUIREMENTS = ['youtube_dl==2017.10.12']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -406,16 +406,9 @@ def async_setup(hass, config):
update_tasks = [] update_tasks = []
for player in target_players: for player in target_players:
yield from getattr(player, method['method'])(**params) yield from getattr(player, method['method'])(**params)
for player in target_players:
if not player.should_poll: if not player.should_poll:
continue continue
update_tasks.append(player.async_update_ha_state(True))
update_coro = player.async_update_ha_state(True)
if hasattr(player, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -225,7 +225,7 @@ class DenonDevice(MediaPlayerDevice):
self.telnet_command('MU' + ('ON' if mute else 'OFF')) self.telnet_command('MU' + ('ON' if mute else 'OFF'))
def media_play(self): def media_play(self):
"""Play media media player.""" """Play media player."""
self.telnet_command('NS9A') self.telnet_command('NS9A')
def media_pause(self): def media_pause(self):

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['denonavr==0.5.3'] REQUIREMENTS = ['denonavr==0.5.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT) CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['directpy==0.1'] REQUIREMENTS = ['directpy==0.2']
DEFAULT_DEVICE = '0' DEFAULT_DEVICE = '0'
DEFAULT_NAME = 'DirecTV Receiver' DEFAULT_NAME = 'DirecTV Receiver'

View File

@ -124,7 +124,7 @@ class DuneHDPlayerEntity(MediaPlayerDevice):
self.schedule_update_ha_state() self.schedule_update_ha_state()
def media_play(self): def media_play(self):
"""Play media media player.""" """Play media player."""
self._state = self._player.play() self._state = self._player.play()
self.schedule_update_ha_state() self.schedule_update_ha_state()

View File

@ -0,0 +1,185 @@
"""
Support for interfacing with Monoprice 6 zone home audio controller.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.monoprice/
"""
import logging
import voluptuous as vol
from homeassistant.const import (CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_VOLUME_MUTE,
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP)
REQUIREMENTS = ['pymonoprice==0.2']
_LOGGER = logging.getLogger(__name__)
SUPPORT_MONOPRICE = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | \
SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
SOURCE_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
})
CONF_ZONES = 'zones'
CONF_SOURCES = 'sources'
# Valid zone ids: 11-16 or 21-26 or 31-36
ZONE_IDS = vol.All(vol.Coerce(int), vol.Any(vol.Range(min=11, max=16),
vol.Range(min=21, max=26),
vol.Range(min=31, max=36)))
# Valid source ids: 1-6
SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=6))
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PORT): cv.string,
vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}),
vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}),
})
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Monoprice 6-zone amplifier platform."""
port = config.get(CONF_PORT)
from serial import SerialException
from pymonoprice import Monoprice
try:
monoprice = Monoprice(port)
except SerialException:
_LOGGER.error('Error connecting to Monoprice controller.')
return
sources = {source_id: extra[CONF_NAME] for source_id, extra
in config[CONF_SOURCES].items()}
for zone_id, extra in config[CONF_ZONES].items():
_LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME])
add_devices([MonopriceZone(monoprice, sources,
zone_id, extra[CONF_NAME])], True)
class MonopriceZone(MediaPlayerDevice):
"""Representation of a a Monoprice amplifier zone."""
# pylint: disable=too-many-public-methods
def __init__(self, monoprice, sources, zone_id, zone_name):
"""Initialize new zone."""
self._monoprice = monoprice
# dict source_id -> source name
self._source_id_name = sources
# dict source name -> source_id
self._source_name_id = {v: k for k, v in sources.items()}
# ordered list of all source names
self._source_names = sorted(self._source_name_id.keys(),
key=lambda v: self._source_name_id[v])
self._zone_id = zone_id
self._name = zone_name
self._state = None
self._volume = None
self._source = None
self._mute = None
def update(self):
"""Retrieve latest state."""
state = self._monoprice.zone_status(self._zone_id)
if not state:
return False
self._state = STATE_ON if state.power else STATE_OFF
self._volume = state.volume
self._mute = state.mute
idx = state.source
if idx in self._source_id_name:
self._source = self._source_id_name[idx]
else:
self._source = None
return True
@property
def name(self):
"""Return the name of the zone."""
return self._name
@property
def state(self):
"""Return the state of the zone."""
return self._state
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
if self._volume is None:
return None
return self._volume / 38.0
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
return self._mute
@property
def supported_features(self):
"""Return flag of media commands that are supported."""
return SUPPORT_MONOPRICE
@property
def source(self):
""""Return the current input source of the device."""
return self._source
@property
def source_list(self):
"""List of available input sources."""
return self._source_names
def select_source(self, source):
"""Set input source."""
if source not in self._source_name_id:
return
idx = self._source_name_id[source]
self._monoprice.set_source(self._zone_id, idx)
def turn_on(self):
"""Turn the media player on."""
self._monoprice.set_power(self._zone_id, True)
def turn_off(self):
"""Turn the media player off."""
self._monoprice.set_power(self._zone_id, False)
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
self._monoprice.set_mute(self._zone_id, mute)
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
self._monoprice.set_volume(self._zone_id, int(volume * 38))
def volume_up(self):
"""Volume up the media player."""
if self._volume is None:
return
self._monoprice.set_volume(self._zone_id,
min(self._volume + 1, 38))
def volume_down(self):
"""Volume down media player."""
if self._volume is None:
return
self._monoprice.set_volume(self._zone_id,
max(self._volume - 1, 0))

View File

@ -287,12 +287,6 @@ class PlexClient(MediaPlayerDevice):
self._is_player_available = False self._is_player_available = False
self._machine_identifier = None self._machine_identifier = None
self._make = '' self._make = ''
self._media_content_id = None
self._media_content_rating = None
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
self._media_title = None
self._name = None self._name = None
self._player_state = 'idle' self._player_state = 'idle'
self._previous_volume_level = 1 # Used in fake muting self._previous_volume_level = 1 # Used in fake muting
@ -308,16 +302,7 @@ class PlexClient(MediaPlayerDevice):
self.update_devices = update_devices self.update_devices = update_devices
self.update_sessions = update_sessions self.update_sessions = update_sessions
# Music self._clear_media()
self._media_album_artist = None
self._media_album_name = None
self._media_artist = None
self._media_track = None
# TV Show
self._media_episode = None
self._media_season = None
self._media_series_title = None
self.refresh(device, session) self.refresh(device, session)
@ -339,10 +324,32 @@ class PlexClient(MediaPlayerDevice):
'media_player', prefix, 'media_player', prefix,
self.name.lower().replace('-', '_')) self.name.lower().replace('-', '_'))
def _clear_media(self):
"""Set all Media Items to None."""
# General
self._media_content_id = None
self._media_content_rating = None
self._media_content_type = None
self._media_duration = None
self._media_image_url = None
self._media_title = None
self._media_position = None
# Music
self._media_album_artist = None
self._media_album_name = None
self._media_artist = None
self._media_track = None
# TV Show
self._media_episode = None
self._media_season = None
self._media_series_title = None
def refresh(self, device, session): def refresh(self, device, session):
"""Refresh key device data.""" """Refresh key device data."""
# new data refresh # new data refresh
if session: self._clear_media()
if session: # Not being triggered by Chrome or FireTablet Plex App
self._session = session self._session = session
if device: if device:
self._device = device self._device = device
@ -369,9 +376,6 @@ class PlexClient(MediaPlayerDevice):
self._session.ratingKey) self._session.ratingKey)
self._media_content_rating = self._convert_na_to_none( self._media_content_rating = self._convert_na_to_none(
self._session.contentRating) self._session.contentRating)
else:
self._media_position = None
self._media_content_id = None
# player dependent data # player dependent data
if self._session and self._session.player: if self._session and self._session.player:
@ -405,7 +409,6 @@ class PlexClient(MediaPlayerDevice):
self._session.duration) self._session.duration)
else: else:
self._session_type = None self._session_type = None
self._media_duration = None
# media type # media type
if self._session_type == 'clip': if self._session_type == 'clip':
@ -418,11 +421,9 @@ class PlexClient(MediaPlayerDevice):
self._media_content_type = MEDIA_TYPE_VIDEO self._media_content_type = MEDIA_TYPE_VIDEO
elif self._session_type == 'track': elif self._session_type == 'track':
self._media_content_type = MEDIA_TYPE_MUSIC self._media_content_type = MEDIA_TYPE_MUSIC
else:
self._media_content_type = None
# title (movie name, tv episode name, music song name) # title (movie name, tv episode name, music song name)
if self._session: if self._session and self._is_player_active:
self._media_title = self._convert_na_to_none(self._session.title) self._media_title = self._convert_na_to_none(self._session.title)
# Movies # Movies
@ -431,9 +432,7 @@ class PlexClient(MediaPlayerDevice):
self._media_title += ' (' + str(self._session.year) + ')' self._media_title += ' (' + str(self._session.year) + ')'
# TV Show # TV Show
if (self._is_player_active and if self._media_content_type is MEDIA_TYPE_TVSHOW:
self._media_content_type is MEDIA_TYPE_TVSHOW):
# season number (00) # season number (00)
if callable(self._convert_na_to_none(self._session.seasons)): if callable(self._convert_na_to_none(self._session.seasons)):
self._media_season = self._convert_na_to_none( self._media_season = self._convert_na_to_none(
@ -443,23 +442,15 @@ class PlexClient(MediaPlayerDevice):
self._media_season = self._session.parentIndex.zfill(2) self._media_season = self._session.parentIndex.zfill(2)
else: else:
self._media_season = None self._media_season = None
# show name # show name
self._media_series_title = self._convert_na_to_none( self._media_series_title = self._convert_na_to_none(
self._session.grandparentTitle) self._session.grandparentTitle)
# episode number (00) # episode number (00)
if self._convert_na_to_none( if self._convert_na_to_none(self._session.index) is not None:
self._session.index) is not None:
self._media_episode = str(self._session.index).zfill(2) self._media_episode = str(self._session.index).zfill(2)
else:
self._media_season = None
self._media_series_title = None
self._media_episode = None
# Music # Music
if (self._is_player_active and if self._media_content_type == MEDIA_TYPE_MUSIC:
self._media_content_type == MEDIA_TYPE_MUSIC):
self._media_album_name = self._convert_na_to_none( self._media_album_name = self._convert_na_to_none(
self._session.parentTitle) self._session.parentTitle)
self._media_album_artist = self._convert_na_to_none( self._media_album_artist = self._convert_na_to_none(
@ -469,14 +460,9 @@ class PlexClient(MediaPlayerDevice):
self._session.originalTitle) self._session.originalTitle)
# use album artist if track artist is missing # use album artist if track artist is missing
if self._media_artist is None: if self._media_artist is None:
_LOGGER.debug("Using album artist because track artist was " _LOGGER.debug("Using album artist because track artist "
"not found: %s", self.entity_id) "was not found: %s", self.entity_id)
self._media_artist = self._media_album_artist self._media_artist = self._media_album_artist
else:
self._media_album_name = None
self._media_album_artist = None
self._media_track = None
self._media_artist = None
# set app name to library name # set app name to library name
if (self._session is not None if (self._session is not None
@ -501,8 +487,6 @@ class PlexClient(MediaPlayerDevice):
thumb_url = self._get_thumbnail_url(self._session.art) thumb_url = self._get_thumbnail_url(self._session.art)
self._media_image_url = thumb_url self._media_image_url = thumb_url
else:
self._media_image_url = None
def _get_thumbnail_url(self, property_value): def _get_thumbnail_url(self, property_value):
"""Return full URL (if exists) for a thumbnail property.""" """Return full URL (if exists) for a thumbnail property."""
@ -521,6 +505,7 @@ class PlexClient(MediaPlayerDevice):
"""Force client to idle.""" """Force client to idle."""
self._state = STATE_IDLE self._state = STATE_IDLE
self._session = None self._session = None
self._clear_media()
@property @property
def unique_id(self): def unique_id(self):

View File

@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON,
STATE_PLAYING, STATE_IDLE) STATE_PLAYING, STATE_IDLE)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['rxv==0.4.0'] REQUIREMENTS = ['rxv==0.5.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.const import ( from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
CONF_HOST, CONF_METHOD, CONF_PORT, ATTR_STATE) CONF_HOST, CONF_METHOD, CONF_PORT, CONF_TYPE, CONF_TIMEOUT, ATTR_STATE)
DOMAIN = 'modbus' DOMAIN = 'modbus'
@ -24,7 +24,6 @@ REQUIREMENTS = ['pymodbus==1.3.1']
CONF_BAUDRATE = 'baudrate' CONF_BAUDRATE = 'baudrate'
CONF_BYTESIZE = 'bytesize' CONF_BYTESIZE = 'bytesize'
CONF_STOPBITS = 'stopbits' CONF_STOPBITS = 'stopbits'
CONF_TYPE = 'type'
CONF_PARITY = 'parity' CONF_PARITY = 'parity'
SERIAL_SCHEMA = { SERIAL_SCHEMA = {
@ -35,12 +34,14 @@ SERIAL_SCHEMA = {
vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'), vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'),
vol.Required(CONF_STOPBITS): vol.Any(1, 2), vol.Required(CONF_STOPBITS): vol.Any(1, 2),
vol.Required(CONF_TYPE): 'serial', vol.Required(CONF_TYPE): 'serial',
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
} }
ETHERNET_SCHEMA = { ETHERNET_SCHEMA = {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PORT): cv.positive_int, vol.Required(CONF_PORT): cv.positive_int,
vol.Required(CONF_TYPE): vol.Any('tcp', 'udp'), vol.Required(CONF_TYPE): vol.Any('tcp', 'udp'),
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
} }
@ -89,15 +90,18 @@ def setup(hass, config):
baudrate=config[DOMAIN][CONF_BAUDRATE], baudrate=config[DOMAIN][CONF_BAUDRATE],
stopbits=config[DOMAIN][CONF_STOPBITS], stopbits=config[DOMAIN][CONF_STOPBITS],
bytesize=config[DOMAIN][CONF_BYTESIZE], bytesize=config[DOMAIN][CONF_BYTESIZE],
parity=config[DOMAIN][CONF_PARITY]) parity=config[DOMAIN][CONF_PARITY],
timeout=config[DOMAIN][CONF_TIMEOUT])
elif client_type == 'tcp': elif client_type == 'tcp':
from pymodbus.client.sync import ModbusTcpClient as ModbusClient from pymodbus.client.sync import ModbusTcpClient as ModbusClient
client = ModbusClient(host=config[DOMAIN][CONF_HOST], client = ModbusClient(host=config[DOMAIN][CONF_HOST],
port=config[DOMAIN][CONF_PORT]) port=config[DOMAIN][CONF_PORT],
timeout=config[DOMAIN][CONF_TIMEOUT])
elif client_type == 'udp': elif client_type == 'udp':
from pymodbus.client.sync import ModbusUdpClient as ModbusClient from pymodbus.client.sync import ModbusUdpClient as ModbusClient
client = ModbusClient(host=config[DOMAIN][CONF_HOST], client = ModbusClient(host=config[DOMAIN][CONF_HOST],
port=config[DOMAIN][CONF_PORT]) port=config[DOMAIN][CONF_PORT],
timeout=config[DOMAIN][CONF_TIMEOUT])
else: else:
return False return False

View File

@ -30,7 +30,7 @@ from homeassistant.const import (
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD) CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA from homeassistant.components.mqtt.server import HBMQTT_CONFIG_SCHEMA
REQUIREMENTS = ['paho-mqtt==1.3.0'] REQUIREMENTS = ['paho-mqtt==1.3.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,70 @@
"""Integrate with NamecheapDNS."""
import asyncio
from datetime import timedelta
import logging
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_ACCESS_TOKEN, CONF_DOMAIN
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.aiohttp_client import async_get_clientsession
DOMAIN = 'namecheapdns'
UPDATE_URL = 'https://dynamicdns.park-your-domain.com/update'
INTERVAL = timedelta(minutes=5)
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_DOMAIN): cv.string,
vol.Required(CONF_ACCESS_TOKEN): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
@asyncio.coroutine
def async_setup(hass, config):
"""Initialize the NamecheapDNS component."""
host = config[DOMAIN][CONF_HOST]
domain = config[DOMAIN][CONF_DOMAIN]
token = config[DOMAIN][CONF_ACCESS_TOKEN]
session = async_get_clientsession(hass)
result = yield from _update_namecheapdns(session, host, domain, token)
if not result:
return False
@asyncio.coroutine
def update_domain_interval(now):
"""Update the NamecheapDNS entry."""
yield from _update_namecheapdns(session, host, domain, token)
async_track_time_interval(hass, update_domain_interval, INTERVAL)
return result
@asyncio.coroutine
def _update_namecheapdns(session, host, domain, token):
"""Update NamecheapDNS."""
import xml.etree.ElementTree as ET
params = {
'host': host,
'domain': domain,
'password': token,
}
resp = yield from session.get(UPDATE_URL, params=params)
xml_string = yield from resp.text()
root = ET.fromstring(xml_string)
err_count = root.find('ErrCount').text
if int(err_count) != 0:
_LOGGER.warning('Updating Namecheap domain %s failed', domain)
return False
return True

View File

@ -1,10 +1,10 @@
""" """
Clicksend audio platform for notify component. clicksend_tts platform for notify component.
This platform sends text to speech audio messages through clicksend This platform sends text to speech audio messages through clicksend
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/notify.clicksendaudio/ https://home-assistant.io/components/notify.clicksend_tts/
""" """
import json import json
import logging import logging

View File

@ -94,8 +94,8 @@ NOTIFY_CALLBACK_EVENT = 'html5_notification'
# Badge and timestamp are Chrome specific (not in official spec) # Badge and timestamp are Chrome specific (not in official spec)
HTML5_SHOWNOTIFICATION_PARAMETERS = ( HTML5_SHOWNOTIFICATION_PARAMETERS = (
'actions', 'badge', 'body', 'dir', 'icon', 'lang', 'renotify', 'actions', 'badge', 'body', 'dir', 'icon', 'image', 'lang',
'requireInteraction', 'tag', 'timestamp', 'vibrate') 'renotify', 'requireInteraction', 'tag', 'timestamp', 'vibrate')
def get_service(hass, config, discovery_info=None): def get_service(hass, config, discovery_info=None):

View File

@ -0,0 +1,76 @@
"""
Rocket.Chat notification service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.rocketchat/
"""
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_URL, CONF_USERNAME, CONF_PASSWORD)
from homeassistant.components.notify import (
ATTR_DATA, PLATFORM_SCHEMA,
BaseNotificationService)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['rocketchat-API==0.6.1']
CONF_ROOM = 'room'
_LOGGER = logging.getLogger(__name__)
# pylint: disable=no-value-for-parameter
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): vol.Url(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_ROOM): cv.string,
})
def get_service(hass, config, discovery_info=None):
"""Return the notify service."""
from rocketchat_API.APIExceptions.RocketExceptions import (
RocketConnectionException, RocketAuthenticationException)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
url = config.get(CONF_URL)
room = config.get(CONF_ROOM)
try:
return RocketChatNotificationService(url, username, password, room)
except RocketConnectionException:
_LOGGER.warning(
"Unable to connect to Rocket.Chat server at %s.", url)
except RocketAuthenticationException:
_LOGGER.warning(
"Rocket.Chat authentication failed for user %s.", username)
_LOGGER.info("Please check your username/password.")
return None
class RocketChatNotificationService(BaseNotificationService):
"""Implement the notification service for Rocket.Chat."""
def __init__(self, url, username, password, room):
"""Initialize the service."""
from rocketchat_API.rocketchat import RocketChat
self._room = room
self._server = RocketChat(username, password, server_url=url)
def send_message(self, message="", **kwargs):
"""Send a message to Rocket.Chat."""
data = kwargs.get(ATTR_DATA) or {}
resp = self._server.chat_post_message(message, channel=self._room,
**data)
if resp.status_code == 200:
success = resp.json()["success"]
if not success:
_LOGGER.error("Unable to post Rocket.Chat message")
else:
_LOGGER.error("Incorrect status code when posting message: %d",
resp.status_code)

View File

@ -15,13 +15,14 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
REQUIREMENTS = ['sleekxmpp==1.3.2', REQUIREMENTS = ['sleekxmpp==1.3.2',
'dnspython3==1.15.0', 'dnspython3==1.15.0',
'pyasn1==0.3.6', 'pyasn1==0.3.7',
'pyasn1-modules==0.1.4'] 'pyasn1-modules==0.1.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_TLS = 'tls' CONF_TLS = 'tls'
CONF_VERIFY = 'verify' CONF_VERIFY = 'verify'
CONF_ROOM = 'room'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENDER): cv.string, vol.Required(CONF_SENDER): cv.string,
@ -29,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RECIPIENT): cv.string, vol.Required(CONF_RECIPIENT): cv.string,
vol.Optional(CONF_TLS, default=True): cv.boolean, vol.Optional(CONF_TLS, default=True): cv.boolean,
vol.Optional(CONF_VERIFY, default=True): cv.boolean, vol.Optional(CONF_VERIFY, default=True): cv.boolean,
vol.Optional(CONF_ROOM, default=''): cv.string,
}) })
@ -37,31 +39,33 @@ def get_service(hass, config, discovery_info=None):
return XmppNotificationService( return XmppNotificationService(
config.get(CONF_SENDER), config.get(CONF_PASSWORD), config.get(CONF_SENDER), config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT), config.get(CONF_TLS), config.get(CONF_RECIPIENT), config.get(CONF_TLS),
config.get(CONF_VERIFY)) config.get(CONF_VERIFY), config.get(CONF_ROOM))
class XmppNotificationService(BaseNotificationService): class XmppNotificationService(BaseNotificationService):
"""Implement the notification service for Jabber (XMPP).""" """Implement the notification service for Jabber (XMPP)."""
def __init__(self, sender, password, recipient, tls, verify): def __init__(self, sender, password, recipient, tls, verify, room):
"""Initialize the service.""" """Initialize the service."""
self._sender = sender self._sender = sender
self._password = password self._password = password
self._recipient = recipient self._recipient = recipient
self._tls = tls self._tls = tls
self._verify = verify self._verify = verify
self._room = room
def send_message(self, message="", **kwargs): def send_message(self, message="", **kwargs):
"""Send a message to a user.""" """Send a message to a user."""
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = '{}: {}'.format(title, message) if title else message data = '{}: {}'.format(title, message) if title else message
send_message('{}/home-assistant'.format(self._sender), self._password, send_message('{}/home-assistant'.format(self._sender),
self._recipient, self._tls, self._verify, data) self._password, self._recipient, self._tls,
self._verify, self._room, data)
def send_message(sender, password, recipient, use_tls, def send_message(sender, password, recipient, use_tls,
verify_certificate, message): verify_certificate, room, message):
"""Send a message over XMPP.""" """Send a message over XMPP."""
import sleekxmpp import sleekxmpp
@ -78,6 +82,8 @@ def send_message(sender, password, recipient, use_tls,
self.use_ipv6 = False self.use_ipv6 = False
self.add_event_handler('failed_auth', self.check_credentials) self.add_event_handler('failed_auth', self.check_credentials)
self.add_event_handler('session_start', self.start) self.add_event_handler('session_start', self.start)
if room:
self.register_plugin('xep_0045') # MUC
if not verify_certificate: if not verify_certificate:
self.add_event_handler('ssl_invalid_cert', self.add_event_handler('ssl_invalid_cert',
self.discard_ssl_invalid_cert) self.discard_ssl_invalid_cert)
@ -89,6 +95,12 @@ def send_message(sender, password, recipient, use_tls,
"""Start the communication and sends the message.""" """Start the communication and sends the message."""
self.send_presence() self.send_presence()
self.get_roster() self.get_roster()
if room:
_LOGGER.debug("Joining room %s.", room)
self.plugin['xep_0045'].joinMUC(room, sender, wait=True)
self.send_message(mto=room, mbody=message, mtype='groupchat')
else:
self.send_message(mto=recipient, mbody=message, mtype='chat') self.send_message(mto=recipient, mbody=message, mtype='chat')
self.disconnect(wait=True) self.disconnect(wait=True)

View File

@ -1,8 +1,9 @@
"""Component to allow running Python scripts.""" """Component to allow running Python scripts."""
import glob
import os
import logging
import datetime import datetime
import glob
import logging
import os
import time
import voluptuous as vol import voluptuous as vol
@ -10,6 +11,7 @@ from homeassistant.const import SERVICE_RELOAD
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util import sanitize_filename from homeassistant.util import sanitize_filename
import homeassistant.util.dt as dt_util
DOMAIN = 'python_script' DOMAIN = 'python_script'
REQUIREMENTS = ['restrictedpython==4.0a3'] REQUIREMENTS = ['restrictedpython==4.0a3']
@ -25,6 +27,13 @@ ALLOWED_EVENTBUS = set(['fire'])
ALLOWED_STATEMACHINE = set(['entity_ids', 'all', 'get', 'is_state', ALLOWED_STATEMACHINE = set(['entity_ids', 'all', 'get', 'is_state',
'is_state_attr', 'remove', 'set']) 'is_state_attr', 'remove', 'set'])
ALLOWED_SERVICEREGISTRY = set(['services', 'has_service', 'call']) ALLOWED_SERVICEREGISTRY = set(['services', 'has_service', 'call'])
ALLOWED_TIME = set(['sleep', 'strftime', 'strptime', 'gmtime', 'localtime',
'ctime', 'time', 'mktime'])
ALLOWED_DATETIME = set(['date', 'time', 'datetime', 'timedelta', 'tzinfo'])
ALLOWED_DT_UTIL = set([
'utcnow', 'now', 'as_utc', 'as_timestamp', 'as_local',
'utc_from_timestamp', 'start_of_local_day', 'parse_datetime', 'parse_date',
'get_age'])
class ScriptError(HomeAssistantError): class ScriptError(HomeAssistantError):
@ -111,7 +120,10 @@ def execute(hass, filename, source, data=None):
elif (obj is hass and name not in ALLOWED_HASS or elif (obj is hass and name not in ALLOWED_HASS or
obj is hass.bus and name not in ALLOWED_EVENTBUS or obj is hass.bus and name not in ALLOWED_EVENTBUS or
obj is hass.states and name not in ALLOWED_STATEMACHINE or obj is hass.states and name not in ALLOWED_STATEMACHINE or
obj is hass.services and name not in ALLOWED_SERVICEREGISTRY): obj is hass.services and name not in ALLOWED_SERVICEREGISTRY or
obj is dt_util and name not in ALLOWED_DT_UTIL or
obj is datetime and name not in ALLOWED_DATETIME or
isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME):
raise ScriptError('Not allowed to access {}.{}'.format( raise ScriptError('Not allowed to access {}.{}'.format(
obj.__class__.__name__, name)) obj.__class__.__name__, name))
@ -120,6 +132,8 @@ def execute(hass, filename, source, data=None):
builtins = safe_builtins.copy() builtins = safe_builtins.copy()
builtins.update(utility_builtins) builtins.update(utility_builtins)
builtins['datetime'] = datetime builtins['datetime'] = datetime
builtins['time'] = TimeWrapper()
builtins['dt_util'] = dt_util
restricted_globals = { restricted_globals = {
'__builtins__': builtins, '__builtins__': builtins,
'_print_': StubPrinter, '_print_': StubPrinter,
@ -159,3 +173,24 @@ class StubPrinter:
# pylint: disable=no-self-use # pylint: disable=no-self-use
_LOGGER.warning( _LOGGER.warning(
"Don't use print() inside scripts. Use logger.info() instead.") "Don't use print() inside scripts. Use logger.info() instead.")
class TimeWrapper:
"""Wrapper of the time module."""
# Class variable, only going to warn once per Home Assistant run
warned = False
# pylint: disable=no-self-use
def sleep(self, *args, **kwargs):
"""Sleep method that warns once."""
if not TimeWrapper.warned:
TimeWrapper.warned = True
_LOGGER.warning('Using time.sleep can reduce the performance of '
'Home Assistant')
time.sleep(*args, **kwargs)
def __getattr__(self, attr):
"""Fetch an attribute from Time module."""
return getattr(time, attr)

View File

@ -20,7 +20,7 @@ from homeassistant.helpers.dispatcher import (
from requests.exceptions import HTTPError, ConnectTimeout from requests.exceptions import HTTPError, ConnectTimeout
REQUIREMENTS = ['raincloudy==0.0.1'] REQUIREMENTS = ['raincloudy==0.0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -351,6 +351,7 @@ class Recorder(threading.Thread):
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlite3 import Connection
from . import models from . import models
@ -360,7 +361,7 @@ class Recorder(threading.Thread):
@event.listens_for(Engine, "connect") @event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record): def set_sqlite_pragma(dbapi_connection, connection_record):
"""Set sqlite's WAL mode.""" """Set sqlite's WAL mode."""
if self.db_url.startswith("sqlite://"): if isinstance(dbapi_connection, Connection):
old_isolation = dbapi_connection.isolation_level old_isolation = dbapi_connection.isolation_level
dbapi_connection.isolation_level = None dbapi_connection.isolation_level = None
cursor = dbapi_connection.cursor() cursor = dbapi_connection.cursor()

View File

@ -148,6 +148,7 @@ def async_setup(hass, config):
num_repeats = service.data.get(ATTR_NUM_REPEATS) num_repeats = service.data.get(ATTR_NUM_REPEATS)
delay_secs = service.data.get(ATTR_DELAY_SECS) delay_secs = service.data.get(ATTR_DELAY_SECS)
update_tasks = []
for remote in target_remotes: for remote in target_remotes:
if service.service == SERVICE_TURN_ON: if service.service == SERVICE_TURN_ON:
yield from remote.async_turn_on(activity=activity_id) yield from remote.async_turn_on(activity=activity_id)
@ -160,17 +161,9 @@ def async_setup(hass, config):
else: else:
yield from remote.async_turn_off(activity=activity_id) yield from remote.async_turn_off(activity=activity_id)
update_tasks = []
for remote in target_remotes:
if not remote.should_poll: if not remote.should_poll:
continue continue
update_tasks.append(remote.async_update_ha_state(True))
update_coro = hass.async_add_job(
remote.async_update_ha_state(True))
if hasattr(remote, 'async_update'):
update_tasks.append(update_coro)
else:
yield from update_coro
if update_tasks: if update_tasks:
yield from asyncio.wait(update_tasks, loop=hass.loop) yield from asyncio.wait(update_tasks, loop=hass.loop)

View File

@ -0,0 +1,81 @@
"""
Support for Abode Security System sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.abode/
"""
import logging
from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['abode']
# Sensor types: Name, icon
SENSOR_TYPES = {
'temp': ['Temperature', 'thermometer'],
'humidity': ['Humidity', 'water-percent'],
'lux': ['Lux', 'lightbulb'],
}
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for an Abode device."""
import abodepy.helpers.constants as CONST
data = hass.data[ABODE_DOMAIN]
devices = []
for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR):
if data.is_excluded(device):
continue
for sensor_type in SENSOR_TYPES:
devices.append(AbodeSensor(data, device, sensor_type))
data.devices.extend(devices)
add_devices(devices)
class AbodeSensor(AbodeDevice):
"""A sensor implementation for Abode devices."""
def __init__(self, data, device, sensor_type):
"""Initialize a sensor for an Abode device."""
super().__init__(data, device)
self._sensor_type = sensor_type
self._icon = 'mdi:{}'.format(SENSOR_TYPES[self._sensor_type][1])
self._name = '{0} {1}'.format(self._device.name,
SENSOR_TYPES[self._sensor_type][0])
@property
def icon(self):
"""Icon to use in the frontend, if any."""
return self._icon
@property
def name(self):
"""Return the name of the sensor."""
return self._name
@property
def state(self):
"""Return the state of the sensor."""
if self._sensor_type == 'temp':
return self._device.temp
elif self._sensor_type == 'humidity':
return self._device.humidity
elif self._sensor_type == 'lux':
return self._device.lux
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
if self._sensor_type == 'temp':
return self._device.temp_unit
elif self._sensor_type == 'humidity':
return self._device.humidity_unit
elif self._sensor_type == 'lux':
return self._device.lux_unit

View File

@ -4,8 +4,6 @@ Support for AirVisual air quality sensors.
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/sensor.airvisual/ https://home-assistant.io/components/sensor.airvisual/
""" """
import asyncio
from logging import getLogger from logging import getLogger
from datetime import timedelta from datetime import timedelta
@ -15,13 +13,15 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY,
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_STATE) CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_STATE,
CONF_SHOW_ON_MAP)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
_LOGGER = getLogger(__name__)
REQUIREMENTS = ['pyairvisual==1.0.0'] REQUIREMENTS = ['pyairvisual==1.0.0']
_LOGGER = getLogger(__name__)
ATTR_CITY = 'city' ATTR_CITY = 'city'
ATTR_COUNTRY = 'country' ATTR_COUNTRY = 'country'
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol' ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
@ -32,6 +32,7 @@ ATTR_TIMESTAMP = 'timestamp'
CONF_CITY = 'city' CONF_CITY = 'city'
CONF_COUNTRY = 'country' CONF_COUNTRY = 'country'
CONF_RADIUS = 'radius' CONF_RADIUS = 'radius'
CONF_ATTRIBUTION = "Data provided by AirVisual"
MASS_PARTS_PER_MILLION = 'ppm' MASS_PARTS_PER_MILLION = 'ppm'
MASS_PARTS_PER_BILLION = 'ppb' MASS_PARTS_PER_BILLION = 'ppb'
@ -39,56 +40,22 @@ VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
POLLUTANT_LEVEL_MAPPING = [{ POLLUTANT_LEVEL_MAPPING = [
'label': 'Good', {'label': 'Good', 'minimum': 0, 'maximum': 50},
'minimum': 0, {'label': 'Moderate', 'minimum': 51, 'maximum': 100},
'maximum': 50 {'label': 'Unhealthy for sensitive group', 'minimum': 101, 'maximum': 150},
}, { {'label': 'Unhealthy', 'minimum': 151, 'maximum': 200},
'label': 'Moderate', {'label': 'Very Unhealthy', 'minimum': 201, 'maximum': 300},
'minimum': 51, {'label': 'Hazardous', 'minimum': 301, 'maximum': 10000}
'maximum': 100 ]
}, {
'label': 'Unhealthy for Sensitive Groups',
'minimum': 101,
'maximum': 150
}, {
'label': 'Unhealthy',
'minimum': 151,
'maximum': 200
}, {
'label': 'Very Unhealthy',
'minimum': 201,
'maximum': 300
}, {
'label': 'Hazardous',
'minimum': 301,
'maximum': 10000
}]
POLLUTANT_MAPPING = { POLLUTANT_MAPPING = {
'co': { 'co': {'label': 'Carbon Monoxide', 'unit': MASS_PARTS_PER_MILLION},
'label': 'Carbon Monoxide', 'n2': {'label': 'Nitrogen Dioxide', 'unit': MASS_PARTS_PER_BILLION},
'unit': MASS_PARTS_PER_MILLION 'o3': {'label': 'Ozone', 'unit': MASS_PARTS_PER_BILLION},
}, 'p1': {'label': 'PM10', 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER},
'n2': { 'p2': {'label': 'PM2.5', 'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER},
'label': 'Nitrogen Dioxide', 's2': {'label': 'Sulfur Dioxide', 'unit': MASS_PARTS_PER_BILLION},
'unit': MASS_PARTS_PER_BILLION
},
'o3': {
'label': 'Ozone',
'unit': MASS_PARTS_PER_BILLION
},
'p1': {
'label': 'PM10',
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
},
'p2': {
'label': 'PM2.5',
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
},
's2': {
'label': 'Sulfur Dioxide',
'unit': MASS_PARTS_PER_BILLION
}
} }
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'} SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
@ -99,32 +66,23 @@ SENSOR_TYPES = [
] ]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): vol.Required(CONF_API_KEY): cv.string,
cv.string,
vol.Required(CONF_MONITORED_CONDITIONS): vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]), vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
vol.Optional(CONF_LATITUDE): vol.Optional(CONF_CITY): cv.string,
cv.latitude, vol.Optional(CONF_COUNTRY): cv.string,
vol.Optional(CONF_LONGITUDE): vol.Optional(CONF_LATITUDE): cv.latitude,
cv.longitude, vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_RADIUS, default=1000): vol.Optional(CONF_RADIUS, default=1000): cv.positive_int,
cv.positive_int, vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
vol.Optional(CONF_CITY): vol.Optional(CONF_STATE): cv.string,
cv.string,
vol.Optional(CONF_STATE):
cv.string,
vol.Optional(CONF_COUNTRY):
cv.string
}) })
@asyncio.coroutine def setup_platform(hass, config, add_devices, discovery_info=None):
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Configure the platform and add the sensors.""" """Configure the platform and add the sensors."""
import pyairvisual as pav import pyairvisual as pav
_LOGGER.debug('Received configuration: %s', config)
api_key = config.get(CONF_API_KEY) api_key = config.get(CONF_API_KEY)
monitored_locales = config.get(CONF_MONITORED_CONDITIONS) monitored_locales = config.get(CONF_MONITORED_CONDITIONS)
latitude = config.get(CONF_LATITUDE, hass.config.latitude) latitude = config.get(CONF_LATITUDE, hass.config.latitude)
@ -133,27 +91,28 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
city = config.get(CONF_CITY) city = config.get(CONF_CITY)
state = config.get(CONF_STATE) state = config.get(CONF_STATE)
country = config.get(CONF_COUNTRY) country = config.get(CONF_COUNTRY)
show_on_map = config.get(CONF_SHOW_ON_MAP)
if city and state and country: if city and state and country:
_LOGGER.debug('Using city, state, and country: %s, %s, %s', city, _LOGGER.debug(
state, country) "Using city, state, and country: %s, %s, %s", city, state, country)
data = AirVisualData( data = AirVisualData(
pav.Client(api_key), city=city, state=state, country=country) pav.Client(api_key), city=city, state=state, country=country,
show_on_map=show_on_map)
else: else:
_LOGGER.debug('Using latitude and longitude: %s, %s', latitude, _LOGGER.debug(
longitude) "Using latitude and longitude: %s, %s", latitude, longitude)
data = AirVisualData( data = AirVisualData(
pav.Client(api_key), pav.Client(api_key), latitude=latitude, longitude=longitude,
latitude=latitude, radius=radius, show_on_map=show_on_map)
longitude=longitude,
radius=radius)
data.update()
sensors = [] sensors = []
for locale in monitored_locales: for locale in monitored_locales:
for sensor_class, name, icon in SENSOR_TYPES: for sensor_class, name, icon in SENSOR_TYPES:
sensors.append(globals()[sensor_class](data, name, icon, locale)) sensors.append(globals()[sensor_class](data, name, icon, locale))
async_add_devices(sensors, True) add_devices(sensors, True)
def merge_two_dicts(dict1, dict2): def merge_two_dicts(dict1, dict2):
@ -167,7 +126,7 @@ class AirVisualBaseSensor(Entity):
"""Define a base class for all of our sensors.""" """Define a base class for all of our sensors."""
def __init__(self, data, name, icon, locale): def __init__(self, data, name, icon, locale):
"""Initialize.""" """Initialize the sensor."""
self._data = data self._data = data
self._icon = icon self._icon = icon
self._locale = locale self._locale = locale
@ -177,17 +136,24 @@ class AirVisualBaseSensor(Entity):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the device state attributes."""
return { attrs = {
ATTR_ATTRIBUTION: 'AirVisual©', ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
ATTR_CITY: self._data.city, ATTR_CITY: self._data.city,
ATTR_COUNTRY: self._data.country, ATTR_COUNTRY: self._data.country,
ATTR_REGION: self._data.state, ATTR_REGION: self._data.state,
ATTR_LATITUDE: self._data.latitude,
ATTR_LONGITUDE: self._data.longitude,
ATTR_TIMESTAMP: self._data.pollution_info.get('ts') ATTR_TIMESTAMP: self._data.pollution_info.get('ts')
} }
if self._data.show_on_map:
attrs[ATTR_LATITUDE] = self._data.latitude
attrs[ATTR_LONGITUDE] = self._data.longitude
else:
attrs['lati'] = self._data.latitude
attrs['long'] = self._data.longitude
return attrs
@property @property
def icon(self): def icon(self):
"""Return the icon.""" """Return the icon."""
@ -203,20 +169,14 @@ class AirVisualBaseSensor(Entity):
"""Return the state.""" """Return the state."""
return self._state return self._state
@asyncio.coroutine
def async_update(self):
"""Update the status of the sensor."""
_LOGGER.debug('Updating sensor: %s', self._name)
self._data.update()
class AirPollutionLevelSensor(AirVisualBaseSensor): class AirPollutionLevelSensor(AirVisualBaseSensor):
"""Define a sensor to measure air pollution level.""" """Define a sensor to measure air pollution level."""
@asyncio.coroutine def update(self):
def async_update(self):
"""Update the status of the sensor.""" """Update the status of the sensor."""
yield from super().async_update() self._data.update()
aqi = self._data.pollution_info.get('aqi{0}'.format(self._locale)) aqi = self._data.pollution_info.get('aqi{0}'.format(self._locale))
try: try:
[level] = [ [level] = [
@ -238,10 +198,9 @@ class AirQualityIndexSensor(AirVisualBaseSensor):
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return 'PSI' return 'PSI'
@asyncio.coroutine def update(self):
def async_update(self):
"""Update the status of the sensor.""" """Update the status of the sensor."""
yield from super().async_update() self._data.update()
self._state = self._data.pollution_info.get( self._state = self._data.pollution_info.get(
'aqi{0}'.format(self._locale)) 'aqi{0}'.format(self._locale))
@ -251,23 +210,23 @@ class MainPollutantSensor(AirVisualBaseSensor):
"""Define a sensor to the main pollutant of an area.""" """Define a sensor to the main pollutant of an area."""
def __init__(self, data, name, icon, locale): def __init__(self, data, name, icon, locale):
"""Initialize.""" """Initialize the sensor."""
super().__init__(data, name, icon, locale) super().__init__(data, name, icon, locale)
self._symbol = None self._symbol = None
self._unit = None self._unit = None
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the device state attributes."""
return merge_two_dicts(super().device_state_attributes, { return merge_two_dicts(super().device_state_attributes, {
ATTR_POLLUTANT_SYMBOL: self._symbol, ATTR_POLLUTANT_SYMBOL: self._symbol,
ATTR_POLLUTANT_UNIT: self._unit ATTR_POLLUTANT_UNIT: self._unit
}) })
@asyncio.coroutine def update(self):
def async_update(self):
"""Update the status of the sensor.""" """Update the status of the sensor."""
yield from super().async_update() self._data.update()
symbol = self._data.pollution_info.get('main{0}'.format(self._locale)) symbol = self._data.pollution_info.get('main{0}'.format(self._locale))
pollution_info = POLLUTANT_MAPPING.get(symbol, {}) pollution_info = POLLUTANT_MAPPING.get(symbol, {})
self._state = pollution_info.get('label') self._state = pollution_info.get('label')
@ -279,7 +238,7 @@ class AirVisualData(object):
"""Define an object to hold sensor data.""" """Define an object to hold sensor data."""
def __init__(self, client, **kwargs): def __init__(self, client, **kwargs):
"""Initialize.""" """Initialize the AirVisual data element."""
self._client = client self._client = client
self.pollution_info = None self.pollution_info = None
@ -291,6 +250,8 @@ class AirVisualData(object):
self.longitude = kwargs.get(CONF_LONGITUDE) self.longitude = kwargs.get(CONF_LONGITUDE)
self._radius = kwargs.get(CONF_RADIUS) self._radius = kwargs.get(CONF_RADIUS)
self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP)
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Update with new AirVisual data.""" """Update with new AirVisual data."""
@ -298,21 +259,21 @@ class AirVisualData(object):
try: try:
if self.city and self.state and self.country: if self.city and self.state and self.country:
resp = self._client.city(self.city, self.state, resp = self._client.city(
self.country).get('data') self.city, self.state, self.country).get('data')
self.longitude, self.latitude = resp.get('location').get(
'coordinates')
else: else:
resp = self._client.nearest_city(self.latitude, self.longitude, resp = self._client.nearest_city(
self._radius).get('data') self.latitude, self.longitude, self._radius).get('data')
_LOGGER.debug('New data retrieved: %s', resp) _LOGGER.debug("New data retrieved: %s", resp)
self.city = resp.get('city') self.city = resp.get('city')
self.state = resp.get('state') self.state = resp.get('state')
self.country = resp.get('country') self.country = resp.get('country')
self.longitude, self.latitude = resp.get('location').get(
'coordinates')
self.pollution_info = resp.get('current', {}).get('pollution', {}) self.pollution_info = resp.get('current', {}).get('pollution', {})
except exceptions.HTTPError as exc_info: except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to retrieve data on this location: %s', _LOGGER.error("Unable to retrieve data on this location: %s",
self.__dict__) self.__dict__)
_LOGGER.debug(exc_info) _LOGGER.debug(exc_info)
self.pollution_info = {} self.pollution_info = {}

View File

@ -9,6 +9,7 @@ import asyncio
from homeassistant.components.android_ip_webcam import ( from homeassistant.components.android_ip_webcam import (
KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST,
CONF_NAME, CONF_SENSORS) CONF_NAME, CONF_SENSORS)
from homeassistant.helpers.icon import icon_for_battery_level
DEPENDENCIES = ['android_ip_webcam'] DEPENDENCIES = ['android_ip_webcam']
@ -75,14 +76,5 @@ class IPWebcamSensor(AndroidIPCamEntity):
def icon(self): def icon(self):
"""Return the icon for the sensor.""" """Return the icon for the sensor."""
if self._sensor == 'battery_level' and self._state is not None: if self._sensor == 'battery_level' and self._state is not None:
rounded_level = round(int(self._state), -1) return icon_for_battery_level(int(self._state))
returning_icon = 'mdi:battery'
if rounded_level < 10:
returning_icon = 'mdi:battery-outline'
elif self._state == 100:
returning_icon = 'mdi:battery'
else:
returning_icon = 'mdi:battery-{}'.format(str(rounded_level))
return returning_icon
return ICON_MAP.get(self._sensor, 'mdi:eye') return ICON_MAP.get(self._sensor, 'mdi:eye')

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