mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
commit
ead4e44cd6
15
.coveragerc
15
.coveragerc
@ -170,6 +170,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/skybell.py
|
||||
homeassistant/components/*/skybell.py
|
||||
|
||||
homeassistant/components/tado.py
|
||||
homeassistant/components/*/tado.py
|
||||
|
||||
@ -187,6 +190,9 @@ omit =
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/toon.py
|
||||
homeassistant/components/*/toon.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
homeassistant/components/*/tradfri.py
|
||||
|
||||
@ -217,7 +223,7 @@ omit =
|
||||
homeassistant/components/wemo.py
|
||||
homeassistant/components/*/wemo.py
|
||||
|
||||
homeassistant/components/wink.py
|
||||
homeassistant/components/wink/*
|
||||
homeassistant/components/*/wink.py
|
||||
|
||||
homeassistant/components/xiaomi_aqara.py
|
||||
@ -267,6 +273,7 @@ omit =
|
||||
homeassistant/components/camera/rpi_camera.py
|
||||
homeassistant/components/camera/onvif.py
|
||||
homeassistant/components/camera/synology.py
|
||||
homeassistant/components/camera/yi.py
|
||||
homeassistant/components/climate/eq3btsmart.py
|
||||
homeassistant/components/climate/flexit.py
|
||||
homeassistant/components/climate/heatmiser.py
|
||||
@ -405,7 +412,7 @@ omit =
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
homeassistant/components/notify/ciscospark.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/facebook.py
|
||||
homeassistant/components/notify/free_mobile.py
|
||||
@ -427,6 +434,7 @@ omit =
|
||||
homeassistant/components/notify/pushover.py
|
||||
homeassistant/components/notify/pushsafer.py
|
||||
homeassistant/components/notify/rest.py
|
||||
homeassistant/components/notify/rocketchat.py
|
||||
homeassistant/components/notify/sendgrid.py
|
||||
homeassistant/components/notify/simplepush.py
|
||||
homeassistant/components/notify/slack.py
|
||||
@ -529,6 +537,7 @@ omit =
|
||||
homeassistant/components/sensor/sabnzbd.py
|
||||
homeassistant/components/sensor/scrape.py
|
||||
homeassistant/components/sensor/sensehat.py
|
||||
homeassistant/components/sensor/serial.py
|
||||
homeassistant/components/sensor/serial_pm.py
|
||||
homeassistant/components/sensor/shodan.py
|
||||
homeassistant/components/sensor/skybeacon.py
|
||||
@ -549,6 +558,7 @@ omit =
|
||||
homeassistant/components/sensor/time_date.py
|
||||
homeassistant/components/sensor/torque.py
|
||||
homeassistant/components/sensor/transmission.py
|
||||
homeassistant/components/sensor/travisci.py
|
||||
homeassistant/components/sensor/twitch.py
|
||||
homeassistant/components/sensor/uber.py
|
||||
homeassistant/components/sensor/upnp.py
|
||||
@ -585,6 +595,7 @@ omit =
|
||||
homeassistant/components/switch/telnet.py
|
||||
homeassistant/components/switch/transmission.py
|
||||
homeassistant/components/switch/wake_on_lan.py
|
||||
homeassistant/components/switch/xiaomi_miio.py
|
||||
homeassistant/components/telegram_bot/*
|
||||
homeassistant/components/thingspeak.py
|
||||
homeassistant/components/tts/amazon_polly.py
|
||||
|
25
CODEOWNERS
25
CODEOWNERS
@ -29,6 +29,9 @@ homeassistant/components/weblink.py @home-assistant/core
|
||||
homeassistant/components/websocket_api.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
|
||||
virtualization/Docker/* @home-assistant/docker
|
||||
|
||||
@ -36,10 +39,28 @@ homeassistant/components/zwave/* @home-assistant/z-wave
|
||||
homeassistant/components/*/zwave.py @home-assistant/z-wave
|
||||
|
||||
# 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/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/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/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
|
||||
|
@ -11,10 +11,8 @@ MAINTAINER Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>
|
||||
#ENV INSTALL_FFMPEG no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_COAP no
|
||||
#ENV INSTALL_SSOCR no
|
||||
|
||||
|
||||
VOLUME /config
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
@ -26,10 +24,10 @@ RUN virtualization/Docker/setup_docker_prereqs
|
||||
|
||||
# Install hass component dependencies
|
||||
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.
|
||||
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 . .
|
||||
|
@ -11,13 +11,11 @@ from typing import Any, Optional, Dict
|
||||
|
||||
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
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.util.logging import AsyncHandler
|
||||
from homeassistant.util.package import async_get_user_site, get_user_site
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
|
@ -10,6 +10,7 @@ Component design guidelines:
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
import os
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
@ -110,6 +111,11 @@ def async_reload_core_config(hass):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""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
|
||||
def async_handle_turn_service(service):
|
||||
"""Handle calls to homeassistant.turn_on/off."""
|
||||
@ -149,11 +155,14 @@ def async_setup(hass, config):
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
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(
|
||||
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(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service,
|
||||
descriptions[ha.DOMAIN][SERVICE_TOGGLE])
|
||||
|
||||
@asyncio.coroutine
|
||||
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.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(
|
||||
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(
|
||||
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
|
||||
def async_handle_reload_config(call):
|
||||
@ -197,6 +209,7 @@ def async_setup(hass, config):
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
|
||||
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
|
||||
|
@ -10,24 +10,23 @@ from functools import partial
|
||||
from os import path
|
||||
|
||||
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__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by goabode.com"
|
||||
CONF_LIGHTS = "lights"
|
||||
CONF_POLLING = "polling"
|
||||
CONF_POLLING = 'polling'
|
||||
|
||||
DOMAIN = 'abode'
|
||||
|
||||
@ -93,10 +92,9 @@ class AbodeSystem(object):
|
||||
def __init__(self, username, password, name, polling, exclude, lights):
|
||||
"""Initialize the system."""
|
||||
import abodepy
|
||||
self.abode = abodepy.Abode(username, password,
|
||||
auto_login=True,
|
||||
get_devices=True,
|
||||
get_automations=True)
|
||||
self.abode = abodepy.Abode(
|
||||
username, password, auto_login=True, get_devices=True,
|
||||
get_automations=True)
|
||||
self.name = name
|
||||
self.polling = polling
|
||||
self.exclude = exclude
|
||||
@ -210,7 +208,7 @@ def setup_hass_services(hass):
|
||||
|
||||
|
||||
def setup_hass_events(hass):
|
||||
"""Home assistant start and stop callbacks."""
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
def startup(event):
|
||||
"""Listen for push events."""
|
||||
hass.data[DOMAIN].abode.events.start()
|
||||
|
@ -124,20 +124,13 @@ def async_setup(hass, config):
|
||||
|
||||
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
|
||||
|
||||
update_tasks = []
|
||||
for alarm in target_alarms:
|
||||
yield from getattr(alarm, method)(code)
|
||||
|
||||
update_tasks = []
|
||||
for alarm in target_alarms:
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
|
||||
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
|
||||
update_tasks.append(alarm.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
121
homeassistant/components/alarm_control_panel/arlo.py
Normal file
121
homeassistant/components/alarm_control_panel/arlo.py
Normal 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
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.21']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.22']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -11,19 +11,18 @@ from homeassistant.util.decorator import Registry
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_HEADER = 'header'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_NAMESPACE = 'namespace'
|
||||
ATTR_MESSAGE_ID = 'messageId'
|
||||
ATTR_PAYLOAD = 'payload'
|
||||
ATTR_PAYLOAD_VERSION = 'payloadVersion'
|
||||
API_DIRECTIVE = 'directive'
|
||||
API_EVENT = 'event'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
API_ENDPOINT = 'endpoint'
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
switch.DOMAIN: ['SWITCH', ('turnOff', 'turnOn'), None],
|
||||
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
||||
light.DOMAIN: [
|
||||
'LIGHT', ('turnOff', 'turnOn'), {
|
||||
light.SUPPORT_BRIGHTNESS: 'setPercentage'
|
||||
'LIGHT', ('Alexa.PowerController',), {
|
||||
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController'
|
||||
}
|
||||
],
|
||||
}
|
||||
@ -32,51 +31,75 @@ MAPPING_COMPONENT = {
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
"""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?
|
||||
funct_ref = HANDLERS.get(message[ATTR_HEADER][ATTR_NAME])
|
||||
funct_ref = HANDLERS.get((namespace, name))
|
||||
if not funct_ref:
|
||||
_LOGGER.warning(
|
||||
"Unsupported API request %s", message[ATTR_HEADER][ATTR_NAME])
|
||||
"Unsupported API request %s/%s", namespace, name)
|
||||
return api_error(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.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
payload = payload or {}
|
||||
return {
|
||||
ATTR_HEADER: {
|
||||
ATTR_MESSAGE_ID: str(uuid4()),
|
||||
ATTR_NAME: name,
|
||||
ATTR_NAMESPACE: namespace,
|
||||
ATTR_PAYLOAD_VERSION: '2',
|
||||
},
|
||||
ATTR_PAYLOAD: payload,
|
||||
|
||||
response = {
|
||||
API_EVENT: {
|
||||
API_HEADER: {
|
||||
'namespace': namespace,
|
||||
'name': name,
|
||||
'messageId': str(uuid4()),
|
||||
'payloadVersion': '3',
|
||||
},
|
||||
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.
|
||||
|
||||
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
|
||||
def async_api_discovery(hass, request):
|
||||
"""Create a API formatted discovery response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
discovered_appliances = []
|
||||
discovery_endpoints = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
@ -84,35 +107,42 @@ def async_api_discovery(hass, request):
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
appliance = {
|
||||
'actions': [],
|
||||
'applianceTypes': [class_data[0]],
|
||||
endpoint = {
|
||||
'displayCategories': [class_data[0]],
|
||||
'additionalApplianceDetails': {},
|
||||
'applianceId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyDescription': '',
|
||||
'endpointId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyName': entity.name,
|
||||
'isReachable': True,
|
||||
'description': '',
|
||||
'manufacturerName': 'Unknown',
|
||||
'modelName': 'Unknown',
|
||||
'version': 'Unknown',
|
||||
}
|
||||
actions = set()
|
||||
|
||||
# static actions
|
||||
if class_data[1]:
|
||||
appliance['actions'].extend(list(class_data[1]))
|
||||
actions |= set(class_data[1])
|
||||
|
||||
# dynamic actions
|
||||
if class_data[2]:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
for feature, action_name in class_data[2].items():
|
||||
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(
|
||||
'DiscoverAppliancesResponse', 'Alexa.ConnectedHome.Discovery',
|
||||
payload={'discoveredAppliances': discovered_appliances})
|
||||
request, name='Discover.Response', namespace='Alexa.Discovery',
|
||||
payload={'endpoints': discovery_endpoints})
|
||||
|
||||
|
||||
def extract_entity(funct):
|
||||
@ -120,22 +150,21 @@ def extract_entity(funct):
|
||||
@asyncio.coroutine
|
||||
def async_api_entity_wrapper(hass, request):
|
||||
"""Process a turn on request."""
|
||||
entity_id = \
|
||||
request[ATTR_PAYLOAD]['appliance']['applianceId'].replace('#', '.')
|
||||
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
|
||||
|
||||
# extract state object
|
||||
entity = hass.states.get(entity_id)
|
||||
if not entity:
|
||||
_LOGGER.error("Can't process %s for %s",
|
||||
request[ATTR_HEADER][ATTR_NAME], entity_id)
|
||||
return api_error(request)
|
||||
request[API_HEADER]['name'], entity_id)
|
||||
return api_error(request, error_type='NO_SUCH_ENDPOINT')
|
||||
|
||||
return (yield from funct(hass, request, entity))
|
||||
|
||||
return async_api_entity_wrapper
|
||||
|
||||
|
||||
@HANDLERS.register('TurnOnRequest')
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
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
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOnConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register('TurnOffRequest')
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
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
|
||||
}, blocking=True)
|
||||
|
||||
return api_message('TurnOffConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register('SetPercentageRequest')
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_percentage(hass, request, entity):
|
||||
"""Process a set percentage request."""
|
||||
if entity.domain == light.DOMAIN:
|
||||
brightness = request[ATTR_PAYLOAD]['percentageState']['value']
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS: brightness,
|
||||
}, blocking=True)
|
||||
else:
|
||||
return api_error(request)
|
||||
def async_api_set_brightness(hass, request, entity):
|
||||
"""Process a set brightness request."""
|
||||
brightness = request[API_PAYLOAD]['brightness']
|
||||
|
||||
return api_message(
|
||||
'SetPercentageConfirmation', 'Alexa.ConnectedHome.Control')
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS: brightness,
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
@ -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
|
||||
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.const import CONF_USERNAME, CONF_PASSWORD
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.0.6']
|
||||
REQUIREMENTS = ['pyarlo==0.0.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -23,7 +23,7 @@ DEFAULT_BRAND = 'Netgear Arlo'
|
||||
DOMAIN = 'arlo'
|
||||
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Camera Setup'
|
||||
NOTIFICATION_TITLE = 'Arlo Component Setup'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
|
@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'event',
|
||||
vol.Required(CONF_EVENT_TYPE): cv.string,
|
||||
vol.Optional(CONF_EVENT_DATA): dict,
|
||||
vol.Optional(CONF_EVENT_DATA, default={}): dict,
|
||||
})
|
||||
|
||||
|
||||
@ -29,18 +29,24 @@ TRIGGER_SCHEMA = vol.Schema({
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
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
|
||||
def handle_event(event):
|
||||
"""Listen for events and calls the action when data matches."""
|
||||
if not event_data or all(val == event.data.get(key) for key, val
|
||||
in event_data.items()):
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
try:
|
||||
event_data_schema(event.data)
|
||||
except vol.Invalid:
|
||||
# If event data doesn't match requested schema, skip event
|
||||
return
|
||||
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
})
|
||||
|
||||
return hass.bus.async_listen(event_type, handle_event)
|
||||
|
@ -99,8 +99,8 @@ def async_trigger(hass, config, action):
|
||||
return
|
||||
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, True, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_func=check_numeric_state)
|
||||
hass, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_same_func=check_numeric_state)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
@ -65,7 +65,9 @@ def async_trigger(hass, config, action):
|
||||
return
|
||||
|
||||
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(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
@ -13,7 +13,8 @@ import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['pyiss==1.0.1']
|
||||
@ -23,8 +24,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_ISS_NEXT_RISE = 'next_rise'
|
||||
ATTR_ISS_NUMBER_PEOPLE_SPACE = 'number_of_people_in_space'
|
||||
|
||||
CONF_SHOW_ON_MAP = 'show_on_map'
|
||||
|
||||
DEFAULT_NAME = 'ISS'
|
||||
DEFAULT_DEVICE_CLASS = 'visible'
|
||||
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.netatmo import CameraData
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -44,14 +44,12 @@ CONF_WELCOME_SENSORS = 'welcome_sensors'
|
||||
CONF_PRESENCE_SENSORS = 'presence_sensors'
|
||||
CONF_TAG_SENSORS = 'tag_sensors'
|
||||
|
||||
DEFAULT_TIMEOUT = 15
|
||||
DEFAULT_OFFSET = 90
|
||||
DEFAULT_TIMEOUT = 90
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_CAMERAS, default=[]):
|
||||
vol.All(cv.ensure_list, [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.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]),
|
||||
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')
|
||||
home = config.get(CONF_HOME)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
offset = config.get(CONF_OFFSET)
|
||||
if timeout is None:
|
||||
timeout = DEFAULT_TIMEOUT
|
||||
|
||||
module_name = None
|
||||
|
||||
@ -94,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for variable in welcome_sensors:
|
||||
add_devices([NetatmoBinarySensor(
|
||||
data, camera_name, module_name, home, timeout,
|
||||
offset, camera_type, variable)], True)
|
||||
camera_type, variable)], True)
|
||||
if camera_type == 'NOC':
|
||||
if CONF_CAMERAS in config:
|
||||
if config[CONF_CAMERAS] != [] and \
|
||||
@ -102,14 +101,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
continue
|
||||
for variable in presence_sensors:
|
||||
add_devices([NetatmoBinarySensor(
|
||||
data, camera_name, module_name, home, timeout, offset,
|
||||
data, camera_name, module_name, home, timeout,
|
||||
camera_type, variable)], True)
|
||||
|
||||
for module_name in data.get_module_names(camera_name):
|
||||
for variable in tag_sensors:
|
||||
camera_type = None
|
||||
add_devices([NetatmoBinarySensor(
|
||||
data, camera_name, module_name, home, timeout, offset,
|
||||
data, camera_name, module_name, home, timeout,
|
||||
camera_type, variable)], True)
|
||||
|
||||
|
||||
@ -117,14 +116,13 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
"""Represent a single binary sensor in a Netatmo Camera device."""
|
||||
|
||||
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."""
|
||||
self._data = data
|
||||
self._camera_name = camera_name
|
||||
self._module_name = module_name
|
||||
self._home = home
|
||||
self._timeout = timeout
|
||||
self._offset = offset
|
||||
if home:
|
||||
self._name = '{} / {}'.format(home, camera_name)
|
||||
else:
|
||||
@ -173,40 +171,39 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
if self._sensor_name == "Someone known":
|
||||
self._state =\
|
||||
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":
|
||||
self._state =\
|
||||
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":
|
||||
self._state =\
|
||||
self._data.camera_data.motionDetected(
|
||||
self._home, self._camera_name, self._timeout*60)
|
||||
self._home, self._camera_name, self._timeout)
|
||||
elif self._cameratype == 'NOC':
|
||||
if self._sensor_name == "Outdoor motion":
|
||||
self._state =\
|
||||
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":
|
||||
self._state =\
|
||||
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":
|
||||
self._state =\
|
||||
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":
|
||||
self._state =\
|
||||
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":
|
||||
self._state =\
|
||||
self._data.camera_data.moduleMotionDetected(
|
||||
self._home, self._module_name, self._camera_name,
|
||||
self._timeout*60)
|
||||
self._timeout)
|
||||
elif self._sensor_name == "Tag Open":
|
||||
self._state =\
|
||||
self._data.camera_data.moduleOpened(
|
||||
self._home, self._module_name, self._camera_name)
|
||||
else:
|
||||
return None
|
||||
self._home, self._module_name, self._camera_name,
|
||||
self._timeout)
|
||||
|
@ -59,6 +59,8 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice):
|
||||
"""Get the latest data and updates the state."""
|
||||
_LOGGER.debug("Updating RainCloud sensor: %s", self._name)
|
||||
self._state = getattr(self.data, self._sensor_type)
|
||||
if self._sensor_type == 'status':
|
||||
self._state = self._state == 'Online'
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
|
97
homeassistant/components/binary_sensor/skybell.py
Normal file
97
homeassistant/components/binary_sensor/skybell.py
Normal 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
|
@ -15,13 +15,12 @@ from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
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
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -94,10 +93,6 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle the target device state changes."""
|
||||
@ -135,7 +130,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _async_render(self, *args):
|
||||
def _async_render(self):
|
||||
"""Get the state of template."""
|
||||
try:
|
||||
return self._template.async_render().lower() == 'true'
|
||||
@ -171,5 +166,5 @@ class BinarySensorTemplate(BinarySensorDevice):
|
||||
|
||||
period = self._delay_on if state else self._delay_off
|
||||
async_track_same_state(
|
||||
self.hass, state, period, set_state, entity_ids=self._entities,
|
||||
async_check_func=self._async_render)
|
||||
self.hass, period, set_state, entity_ids=self._entities,
|
||||
async_check_same_func=lambda *args: self._async_render() == state)
|
||||
|
@ -30,7 +30,6 @@ class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
|
||||
def __init__(self, tesla_device, controller, sensor_type):
|
||||
"""Initialisation of binary sensor."""
|
||||
super().__init__(tesla_device, controller)
|
||||
self._name = self.tesla_device.name
|
||||
self._state = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._sensor_type = sensor_type
|
||||
|
@ -9,7 +9,6 @@ import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
_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")
|
||||
|
||||
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice):
|
||||
"""Representation of a Wink binary sensor."""
|
||||
|
||||
def __init__(self, wink, hass):
|
||||
@ -117,6 +116,11 @@ class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return SENSOR_TYPES.get(self.capability)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return super().device_state_attributes
|
||||
|
||||
|
||||
class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
"""Representation of a Wink Smoke detector."""
|
||||
@ -124,9 +128,9 @@ class WinkSmokeDetector(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'test_activated': self.wink.test_activated()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['test_activated'] = self.wink.test_activated()
|
||||
return _attributes
|
||||
|
||||
|
||||
class WinkHub(WinkBinarySensorDevice):
|
||||
@ -135,11 +139,11 @@ class WinkHub(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'update_needed': self.wink.update_needed(),
|
||||
'firmware_version': self.wink.firmware_version(),
|
||||
'pairing_mode': self.wink.pairing_mode()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['update_needed'] = self.wink.update_needed()
|
||||
_attributes['firmware_version'] = self.wink.firmware_version()
|
||||
_attributes['pairing_mode'] = self.wink.pairing_mode()
|
||||
return _attributes
|
||||
|
||||
|
||||
class WinkRemote(WinkBinarySensorDevice):
|
||||
@ -148,12 +152,12 @@ class WinkRemote(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'button_on_pressed': self.wink.button_on_pressed(),
|
||||
'button_off_pressed': self.wink.button_off_pressed(),
|
||||
'button_up_pressed': self.wink.button_up_pressed(),
|
||||
'button_down_pressed': self.wink.button_down_pressed()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['button_on_pressed'] = self.wink.button_on_pressed()
|
||||
_attributes['button_off_pressed'] = self.wink.button_off_pressed()
|
||||
_attributes['button_up_pressed'] = self.wink.button_up_pressed()
|
||||
_attributes['button_down_pressed'] = self.wink.button_down_pressed()
|
||||
return _attributes
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@ -167,10 +171,10 @@ class WinkButton(WinkBinarySensorDevice):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'pressed': self.wink.pressed(),
|
||||
'long_pressed': self.wink.long_pressed()
|
||||
}
|
||||
_attributes = super().device_state_attributes
|
||||
_attributes['pressed'] = self.wink.pressed()
|
||||
_attributes['long_pressed'] = self.wink.long_pressed()
|
||||
return _attributes
|
||||
|
||||
|
||||
class WinkGang(WinkBinarySensorDevice):
|
||||
|
@ -12,6 +12,7 @@ ATTR_OPEN_SINCE = 'Open since'
|
||||
|
||||
MOTION = 'motion'
|
||||
NO_MOTION = 'no_motion'
|
||||
ATTR_LAST_ACTION = 'last_action'
|
||||
ATTR_NO_MOTION_SINCE = 'No motion since'
|
||||
|
||||
DENSITY = 'density'
|
||||
@ -327,10 +328,18 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
def __init__(self, device, hass, xiaomi_hub):
|
||||
"""Initialize the Xiaomi Cube."""
|
||||
self._hass = hass
|
||||
self._last_action = None
|
||||
self._state = False
|
||||
XiaomiBinarySensor.__init__(self, device, 'Cube', xiaomi_hub,
|
||||
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):
|
||||
"""Parse data sent by gateway."""
|
||||
if 'status' in data:
|
||||
@ -338,6 +347,7 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
'entity_id': self.entity_id,
|
||||
'action_type': data['status']
|
||||
})
|
||||
self._last_action = data['status']
|
||||
|
||||
if 'rotate' in data:
|
||||
self._hass.bus.fire('cube_action', {
|
||||
@ -345,4 +355,6 @@ class XiaomiCube(XiaomiBinarySensor):
|
||||
'action_type': 'rotate',
|
||||
'action_value': float(data['rotate'].replace(",", "."))
|
||||
})
|
||||
return False
|
||||
self._last_action = 'rotate'
|
||||
|
||||
return True
|
||||
|
@ -126,23 +126,16 @@ def async_setup(hass, config):
|
||||
"""Handle calls to the camera services."""
|
||||
target_cameras = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for camera in target_cameras:
|
||||
if service.service == SERVICE_EN_MOTION:
|
||||
yield from camera.async_enable_motion_detection()
|
||||
elif service.service == SERVICE_DISEN_MOTION:
|
||||
yield from camera.async_disable_motion_detection()
|
||||
|
||||
update_tasks = []
|
||||
for camera in target_cameras:
|
||||
if not camera.should_poll:
|
||||
continue
|
||||
|
||||
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
|
||||
update_tasks.append(camera.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/camera.arlo/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.arlo import DEFAULT_BRAND, DATA_ARLO
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import DATA_FFMPEG
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
|
||||
ATTR_BRIGHTNESS = 'brightness'
|
||||
ATTR_FLIPPED = 'flipped'
|
||||
ATTR_MIRRORED = 'mirrored'
|
||||
ATTR_MOTION_SENSITIVITY = 'motion_detection_sensitivity'
|
||||
ATTR_POWER_SAVE_MODE = 'power_save_mode'
|
||||
ATTR_MOTION = 'motion_detection_sensitivity'
|
||||
ATTR_POWERSAVE = 'power_save_mode'
|
||||
ATTR_SIGNAL_STRENGTH = 'signal_strength'
|
||||
ATTR_UNSEEN_VIDEOS = 'unseen_videos'
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
DEPENDENCIES = ['arlo', 'ffmpeg']
|
||||
|
||||
POWERSAVE_MODE_MAPPING = {
|
||||
1: 'best_battery_life',
|
||||
@ -40,7 +43,8 @@ POWERSAVE_MODE_MAPPING = {
|
||||
}
|
||||
|
||||
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._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self.attrs = {}
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
@ -100,32 +105,24 @@ class ArloCam(Camera):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_BATTERY_LEVEL:
|
||||
self._camera.get_battery_level,
|
||||
ATTR_BRIGHTNESS:
|
||||
self._camera.get_brightness,
|
||||
ATTR_FLIPPED:
|
||||
self._camera.get_flip_state,
|
||||
ATTR_MIRRORED:
|
||||
self._camera.get_mirror_state,
|
||||
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
|
||||
ATTR_BATTERY_LEVEL: self.attrs.get(ATTR_BATTERY_LEVEL),
|
||||
ATTR_BRIGHTNESS: self.attrs.get(ATTR_BRIGHTNESS),
|
||||
ATTR_FLIPPED: self.attrs.get(ATTR_FLIPPED),
|
||||
ATTR_MIRRORED: self.attrs.get(ATTR_MIRRORED),
|
||||
ATTR_MOTION: self.attrs.get(ATTR_MOTION),
|
||||
ATTR_POWERSAVE: self.attrs.get(ATTR_POWERSAVE),
|
||||
ATTR_SIGNAL_STRENGTH: self.attrs.get(ATTR_SIGNAL_STRENGTH),
|
||||
ATTR_UNSEEN_VIDEOS: self.attrs.get(ATTR_UNSEEN_VIDEOS),
|
||||
}
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""Camera model."""
|
||||
"""Return the camera model."""
|
||||
return self._camera.model_id
|
||||
|
||||
@property
|
||||
def brand(self):
|
||||
"""Camera brand."""
|
||||
"""Return the camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
@ -135,7 +132,7 @@ class ArloCam(Camera):
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
"""Camera Motion Detection Status."""
|
||||
"""Return the camera motion detection status."""
|
||||
return self._motion_status
|
||||
|
||||
def set_base_station_mode(self, mode):
|
||||
@ -143,7 +140,7 @@ class ArloCam(Camera):
|
||||
# Get the list of base stations identified by library
|
||||
base_stations = self.hass.data[DATA_ARLO].base_stations
|
||||
|
||||
# Some Arlo cameras does not have basestation
|
||||
# Some Arlo cameras does not have base station
|
||||
# So check if there is base station detected first
|
||||
# if yes, then choose the primary base station
|
||||
# Set the mode on the chosen base station
|
||||
@ -160,3 +157,16 @@ class ArloCam(Camera):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
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
|
||||
|
@ -55,9 +55,9 @@ class FFmpegCamera(Camera):
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
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,
|
||||
extra_cmd=self._extra_arguments)
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -78,9 +78,9 @@ class ONVIFCamera(Camera):
|
||||
ffmpeg = ImageFrame(
|
||||
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,
|
||||
extra_cmd=self._ffmpeg_arguments)
|
||||
extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
|
67
homeassistant/components/camera/skybell.py
Normal file
67
homeassistant/components/camera/skybell.py
Normal 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
|
@ -16,11 +16,11 @@ from homeassistant.const import (
|
||||
from homeassistant.components.camera import (
|
||||
Camera, PLATFORM_SCHEMA)
|
||||
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
|
||||
|
||||
REQUIREMENTS = ['py-synology==0.1.3']
|
||||
REQUIREMENTS = ['py-synology==0.1.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -58,13 +58,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return False
|
||||
|
||||
cameras = surveillance.get_all_cameras()
|
||||
websession = async_create_clientsession(hass, verify_ssl)
|
||||
|
||||
# add cameras
|
||||
devices = []
|
||||
for camera in cameras:
|
||||
if not config.get(CONF_WHITELIST):
|
||||
device = SynologyCamera(websession, surveillance, camera.camera_id)
|
||||
device = SynologyCamera(surveillance, camera.camera_id, verify_ssl)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
@ -73,12 +72,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
class SynologyCamera(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."""
|
||||
super().__init__()
|
||||
self._websession = websession
|
||||
self._surveillance = surveillance
|
||||
self._camera_id = camera_id
|
||||
self._verify_ssl = verify_ssl
|
||||
self._camera = self._surveillance.get_camera(camera_id)
|
||||
self._motion_setting = self._surveillance.get_motion_setting(camera_id)
|
||||
self.is_streaming = self._camera.is_enabled
|
||||
@ -91,7 +90,9 @@ class SynologyCamera(Camera):
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
"""Return a MJPEG stream image response directly from the camera."""
|
||||
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)
|
||||
|
||||
|
137
homeassistant/components/camera/yi.py
Normal file
137
homeassistant/components/camera/yi.py
Normal 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()
|
@ -236,24 +236,6 @@ def async_setup(hass, config):
|
||||
load_yaml_config_file,
|
||||
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
|
||||
def async_away_mode_set_service(service):
|
||||
"""Set away mode on target climate devices."""
|
||||
@ -261,13 +243,19 @@ def async_setup(hass, config):
|
||||
|
||||
away_mode = service.data.get(ATTR_AWAY_MODE)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if away_mode:
|
||||
yield from climate.async_turn_away_mode_on()
|
||||
else:
|
||||
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(
|
||||
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)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
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(
|
||||
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)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
if aux_heat:
|
||||
yield from climate.async_turn_aux_heat_on()
|
||||
else:
|
||||
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(
|
||||
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."""
|
||||
target_climate = component.async_extract_from_service(service)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
kwargs = {}
|
||||
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 _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(
|
||||
DOMAIN, SERVICE_SET_TEMPERATURE, async_temperature_set_service,
|
||||
@ -344,10 +350,15 @@ def async_setup(hass, config):
|
||||
|
||||
humidity = service.data.get(ATTR_HUMIDITY)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
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(
|
||||
DOMAIN, SERVICE_SET_HUMIDITY, async_humidity_set_service,
|
||||
@ -361,10 +372,15 @@ def async_setup(hass, config):
|
||||
|
||||
fan = service.data.get(ATTR_FAN_MODE)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
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(
|
||||
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)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
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(
|
||||
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)
|
||||
|
||||
update_tasks = []
|
||||
for climate in target_climate:
|
||||
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(
|
||||
DOMAIN, SERVICE_SET_SWING_MODE, async_swing_set_service,
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.const import (
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.5']
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.6']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -164,4 +164,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def update(self):
|
||||
"""Update the data from the thermostat."""
|
||||
self._thermostat.update()
|
||||
from bluepy.btle import BTLEException
|
||||
try:
|
||||
self._thermostat.update()
|
||||
except BTLEException as ex:
|
||||
_LOGGER.warning("Updating the state failed: %s", ex)
|
||||
|
@ -14,6 +14,8 @@ from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
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_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_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_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_STATE_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),
|
||||
group_address_setpoint=config.get(
|
||||
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(
|
||||
CONF_OPERATION_MODE_ADDRESS),
|
||||
group_address_operation_mode_state=config.get(
|
||||
@ -140,13 +148,29 @@ class KNXClimate(ClimateDevice):
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self.device.temperature
|
||||
return self.device.temperature.value
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
if self.device.supports_target_temperature:
|
||||
return self.device.target_temperature
|
||||
return self.device.target_temperature_comfort
|
||||
|
||||
@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
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -155,8 +179,8 @@ class KNXClimate(ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
if self.device.supports_target_temperature:
|
||||
yield from self.device.set_target_temperature(temperature)
|
||||
yield from self.device.set_target_temperature_comfort(temperature)
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
@ -18,7 +18,8 @@ from homeassistant.components.climate import (
|
||||
ATTR_OPERATION_MODE)
|
||||
from homeassistant.const import (
|
||||
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
|
||||
from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
@ -57,7 +58,8 @@ CONF_SWING_MODE_LIST = 'swing_modes'
|
||||
CONF_INITIAL = 'initial'
|
||||
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_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_TEMPERATURE_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
|
@ -101,11 +101,11 @@ set_swing_mode:
|
||||
fields:
|
||||
entity_id:
|
||||
description: Name(s) of entities to change
|
||||
example: '.nest'
|
||||
example: 'climate.nest'
|
||||
|
||||
swing_mode:
|
||||
description: New value of swing mode
|
||||
example: 1
|
||||
description: New value of swing mode
|
||||
example: 1
|
||||
|
||||
ecobee_set_fan_min_on_time:
|
||||
description: Set the minimum fan on time
|
||||
|
@ -35,7 +35,6 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._name = self.tesla_device.name
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
|
95
homeassistant/components/climate/toon.py
Normal file
95
homeassistant/components/climate/toon.py
Normal 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()
|
@ -1,47 +1,147 @@
|
||||
"""Component to integrate the Home Assistant cloud."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from . import http_api, auth_api
|
||||
from .const import DOMAIN
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
|
||||
|
||||
REQUIREMENTS = ['warrant==0.2.0']
|
||||
REQUIREMENTS = ['warrant==0.5.0']
|
||||
DEPENDENCIES = ['http']
|
||||
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_STAGING = 'staging'
|
||||
MODE_PRODUCTION = 'production'
|
||||
DEFAULT_MODE = MODE_DEV
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
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)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Initialize the Home Assistant cloud."""
|
||||
mode = MODE_PRODUCTION
|
||||
|
||||
if DOMAIN in config:
|
||||
mode = config[DOMAIN].get(CONF_MODE)
|
||||
kwargs = config[DOMAIN]
|
||||
else:
|
||||
kwargs = {CONF_MODE: DEFAULT_MODE}
|
||||
|
||||
if mode != 'development':
|
||||
_LOGGER.error('Only development mode is currently allowed.')
|
||||
return False
|
||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||
|
||||
data = hass.data[DOMAIN] = {
|
||||
'mode': mode
|
||||
}
|
||||
@asyncio.coroutine
|
||||
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)
|
||||
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))
|
||||
|
@ -1,10 +1,7 @@
|
||||
"""Package to offer tools to authenticate with the cloud."""
|
||||
import json
|
||||
"""Package to communicate with the authentication API."""
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from .const import AUTH_FILE, SERVERS
|
||||
from .util import get_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -61,210 +58,120 @@ def _map_aws_exception(err):
|
||||
return ex(err.response['Error']['Message'])
|
||||
|
||||
|
||||
def load_auth(hass):
|
||||
"""Load authentication from disk and verify it."""
|
||||
info = _read_info(hass)
|
||||
|
||||
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 _generate_username(email):
|
||||
"""Generate a username from an email address."""
|
||||
return hashlib.sha512(email.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def register(hass, email, password):
|
||||
def register(cloud, email, password):
|
||||
"""Register a new account."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
cognito.register(email, password)
|
||||
cognito.register(_generate_username(email), password, email=email)
|
||||
except ClientError as 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."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud)
|
||||
try:
|
||||
cognito.confirm_sign_up(confirmation_code, email)
|
||||
cognito.confirm_sign_up(confirmation_code, _generate_username(email))
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def forgot_password(hass, email):
|
||||
def forgot_password(cloud, email):
|
||||
"""Initiate forgotten password flow."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
try:
|
||||
cognito.initiate_forgot_password()
|
||||
except ClientError as 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."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
cognito = _cognito(hass, username=email)
|
||||
cognito = _cognito(cloud, username=_generate_username(email))
|
||||
try:
|
||||
cognito.confirm_forgot_password(confirmation_code, new_password)
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
class Auth(object):
|
||||
"""Class that holds Cloud authentication."""
|
||||
|
||||
def __init__(self, hass, cognito=None):
|
||||
"""Initialize Hass cloud info object."""
|
||||
self.hass = hass
|
||||
self.cognito = cognito
|
||||
self.account = None
|
||||
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
"""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
|
||||
|
||||
try:
|
||||
self._refresh_account_info()
|
||||
except ClientError as err:
|
||||
if err.response['Error']['Code'] != 'NotAuthorizedException':
|
||||
_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 login(self, username, password):
|
||||
"""Login using a username and password."""
|
||||
from botocore.exceptions import ClientError
|
||||
from warrant.exceptions import ForceChangePasswordException
|
||||
|
||||
cognito = _cognito(self.hass, username=username)
|
||||
|
||||
try:
|
||||
cognito.authenticate(password=password)
|
||||
self.cognito = cognito
|
||||
self._refresh_account_info()
|
||||
_write_info(self.hass, self)
|
||||
|
||||
except ForceChangePasswordException as err:
|
||||
raise PasswordChangeRequired
|
||||
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
def _refresh_account_info(self):
|
||||
"""Refresh the account info.
|
||||
|
||||
Raises boto3 exceptions.
|
||||
"""
|
||||
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 login(cloud, email, password):
|
||||
"""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 _read_info(hass):
|
||||
"""Read auth file."""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
def check_token(cloud):
|
||||
"""Check that the token is valid and verify if needed."""
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
cognito = _cognito(
|
||||
cloud,
|
||||
access_token=cloud.access_token,
|
||||
refresh_token=cloud.refresh_token)
|
||||
|
||||
with open(path) as file:
|
||||
return json.load(file).get(get_mode(hass))
|
||||
try:
|
||||
if cognito.check_token():
|
||||
cloud.id_token = cognito.id_token
|
||||
cloud.access_token = cognito.access_token
|
||||
cloud.write_user_info()
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def _write_info(hass, auth):
|
||||
"""Write auth info for specified mode.
|
||||
def _authenticate(cloud, email, password):
|
||||
"""Log in and return an authenticated Cognito instance."""
|
||||
from botocore.exceptions import ClientError
|
||||
from warrant.exceptions import ForceChangePasswordException
|
||||
|
||||
Pass in None for data to remove authentication for that mode.
|
||||
"""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
mode = get_mode(hass)
|
||||
assert not cloud.is_logged_in, 'Cannot login if already logged in.'
|
||||
|
||||
if os.path.isfile(path):
|
||||
with open(path) as file:
|
||||
content = json.load(file)
|
||||
else:
|
||||
content = {}
|
||||
cognito = _cognito(cloud, username=email)
|
||||
|
||||
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)
|
||||
try:
|
||||
cognito.authenticate(password=password)
|
||||
return cognito
|
||||
|
||||
with open(path, 'wt') as file:
|
||||
file.write(json.dumps(content, indent=4, sort_keys=True))
|
||||
except ForceChangePasswordException as err:
|
||||
raise PasswordChangeRequired
|
||||
|
||||
except ClientError as err:
|
||||
raise _map_aws_exception(err)
|
||||
|
||||
|
||||
def _cognito(hass, **kwargs):
|
||||
def _cognito(cloud, **kwargs):
|
||||
"""Get the client credentials."""
|
||||
import botocore
|
||||
import boto3
|
||||
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(
|
||||
user_pool_id=info['identity_pool_id'],
|
||||
client_id=info['client_id'],
|
||||
user_pool_region=info['region'],
|
||||
access_key=info['access_key_id'],
|
||||
secret_key=info['secret_access_key'],
|
||||
user_pool_id=cloud.user_pool_id,
|
||||
client_id=cloud.cognito_client_id,
|
||||
user_pool_region=cloud.region,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
cognito.client = boto3.client(
|
||||
'cognito-idp',
|
||||
region_name=cloud.region,
|
||||
config=botocore.config.Config(
|
||||
signature_version=botocore.UNSIGNED
|
||||
)
|
||||
)
|
||||
return cognito
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""Constants for the cloud component."""
|
||||
DOMAIN = 'cloud'
|
||||
CONFIG_DIR = '.cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
AUTH_FILE = '.cloud'
|
||||
|
||||
SERVERS = {
|
||||
'development': {
|
||||
'client_id': '3k755iqfcgv8t12o4pl662mnos',
|
||||
'identity_pool_id': 'us-west-2_vDOfweDJo',
|
||||
'region': 'us-west-2',
|
||||
'access_key_id': 'AKIAJGRK7MILPRJTT2ZQ',
|
||||
'secret_access_key': 'lscdYBApxrLWL0HKuVqVXWv3ou8ZVXgG7rZBu/Sz'
|
||||
}
|
||||
# Example entry:
|
||||
# 'production': {
|
||||
# 'cognito_client_id': '',
|
||||
# 'user_pool_id': '',
|
||||
# 'region': '',
|
||||
# 'relayer': ''
|
||||
# }
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ from homeassistant.components.http import (
|
||||
HomeAssistantView, RequestDataValidator)
|
||||
|
||||
from . import auth_api
|
||||
from .const import REQUEST_TIMEOUT
|
||||
from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -74,13 +74,14 @@ class CloudLoginView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle login request."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
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'])
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
|
||||
return self.json(_auth_data(auth))
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
class CloudLogoutView(HomeAssistantView):
|
||||
@ -94,10 +95,10 @@ class CloudLogoutView(HomeAssistantView):
|
||||
def post(self, request):
|
||||
"""Handle logout request."""
|
||||
hass = request.app['hass']
|
||||
auth = hass.data['cloud']['auth']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
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')
|
||||
|
||||
@ -112,12 +113,12 @@ class CloudAccountView(HomeAssistantView):
|
||||
def get(self, request):
|
||||
"""Get account info."""
|
||||
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(_auth_data(auth))
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
class CloudRegisterView(HomeAssistantView):
|
||||
@ -135,10 +136,11 @@ class CloudRegisterView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle registration request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.register, hass, data['email'], data['password'])
|
||||
auth_api.register, cloud, data['email'], data['password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@ -158,10 +160,11 @@ class CloudConfirmRegisterView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle registration confirmation request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_register, hass, data['confirmation_code'],
|
||||
auth_api.confirm_register, cloud, data['confirmation_code'],
|
||||
data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
@ -181,10 +184,11 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.forgot_password, hass, data['email'])
|
||||
auth_api.forgot_password, cloud, data['email'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
@ -205,18 +209,19 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
|
||||
def post(self, request, data):
|
||||
"""Handle forgot password confirm request."""
|
||||
hass = request.app['hass']
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(
|
||||
auth_api.confirm_forgot_password, hass,
|
||||
auth_api.confirm_forgot_password, cloud,
|
||||
data['confirmation_code'], data['email'],
|
||||
data['new_password'])
|
||||
|
||||
return self.json_message('ok')
|
||||
|
||||
|
||||
def _auth_data(auth):
|
||||
def _account_data(cloud):
|
||||
"""Generate the auth data JSON response."""
|
||||
return {
|
||||
'email': auth.account.email
|
||||
'email': cloud.email
|
||||
}
|
||||
|
194
homeassistant/components/cloud/iot.py
Normal file
194
homeassistant/components/cloud/iot.py
Normal 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
|
@ -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']
|
@ -169,21 +169,12 @@ def async_setup(hass, config):
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
# call method
|
||||
update_tasks = []
|
||||
for cover in covers:
|
||||
yield from getattr(cover, method['method'])(**params)
|
||||
|
||||
update_tasks = []
|
||||
|
||||
for cover in covers:
|
||||
if not cover.should_poll:
|
||||
continue
|
||||
|
||||
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
|
||||
update_tasks.append(cover.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
@ -24,7 +24,6 @@ from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.script import Script
|
||||
|
||||
_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")
|
||||
return False
|
||||
|
||||
async_add_devices(covers, True)
|
||||
async_add_devices(covers)
|
||||
return True
|
||||
|
||||
|
||||
@ -190,10 +189,6 @@ class CoverTemplate(CoverDevice):
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._position = 100 if state.state == STATE_OPEN else 0
|
||||
|
||||
@callback
|
||||
def template_cover_state_listener(entity, old_state, new_state):
|
||||
"""Handle target device state changes."""
|
||||
|
@ -18,11 +18,10 @@ from homeassistant.setup import async_prepare_setup_platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
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.exceptions import HomeAssistantError
|
||||
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.event import async_track_time_interval
|
||||
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)
|
||||
})
|
||||
|
||||
DISCOVERY_PLATFORMS = {
|
||||
SERVICE_NETGEAR: 'netgear',
|
||||
}
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistantType, entity_id: str=None):
|
||||
@ -180,22 +175,6 @@ def async_setup(hass: HomeAssistantType, config: ConfigType):
|
||||
|
||||
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
|
||||
async_track_utc_time_change(
|
||||
hass, tracker.async_update_stale, second=range(0, 60, 5))
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
REQUIREMENTS = ['fritzconnection==0.6.3']
|
||||
REQUIREMENTS = ['fritzconnection==0.6.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
|
||||
|
||||
REQUIREMENTS = ['librouteros==1.0.2']
|
||||
REQUIREMENTS = ['librouteros==1.0.4']
|
||||
|
||||
MTK_DEFAULT_API_PORT = '8728'
|
||||
|
||||
@ -83,6 +83,15 @@ class MikrotikScanner(DeviceScanner):
|
||||
routerboard_info[0].get('model', 'Router'),
|
||||
self.host)
|
||||
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(
|
||||
cmd='/interface/wireless/getall'
|
||||
)
|
||||
@ -111,7 +120,9 @@ class MikrotikScanner(DeviceScanner):
|
||||
|
||||
def _update_info(self):
|
||||
"""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'
|
||||
else:
|
||||
devices_tracker = 'ip'
|
||||
@ -123,7 +134,11 @@ class MikrotikScanner(DeviceScanner):
|
||||
)
|
||||
|
||||
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(
|
||||
cmd='/interface/wireless/registration-table/getall'
|
||||
)
|
||||
|
@ -5,23 +5,22 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.owntracks/
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import base64
|
||||
from collections import defaultdict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.util import slugify, decorator
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import zone as zone_comp
|
||||
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.5.2']
|
||||
REQUIREMENTS = ['libnacl==1.6.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -34,6 +33,8 @@ CONF_SECRET = 'secret'
|
||||
CONF_WAYPOINT_IMPORT = 'waypoints'
|
||||
CONF_WAYPOINT_WHITELIST = 'waypoint_whitelist'
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
OWNTRACKS_TOPIC = 'owntracks/#'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
@ -74,6 +75,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
|
||||
except ValueError:
|
||||
# If invalid JSON
|
||||
_LOGGER.error("Unable to parse payload as JSON: %s", payload)
|
||||
return
|
||||
|
||||
message['topic'] = topic
|
||||
|
||||
@ -90,7 +92,11 @@ def _parse_topic(topic):
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
_, user, device, *_ = topic.split('/', 3)
|
||||
try:
|
||||
_, user, device, *_ = topic.split('/', 3)
|
||||
except ValueError:
|
||||
_LOGGER.error("Can't parse topic: '%s'", topic)
|
||||
raise
|
||||
|
||||
return user, device
|
||||
|
||||
@ -399,6 +405,13 @@ def async_handle_encrypted_message(hass, context, message):
|
||||
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
|
||||
def async_handle_message(hass, context, message):
|
||||
"""Handle an OwnTracks message."""
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import CONF_HOST
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.3.9']
|
||||
REQUIREMENTS = ['pysnmp==4.3.10']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
@ -36,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
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])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.unifi/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -12,16 +13,19 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
|
||||
from homeassistant.const import CONF_VERIFY_SSL
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['pyunifi==2.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_PORT = 'port'
|
||||
CONF_SITE_ID = 'site_id'
|
||||
CONF_DETECTION_TIME = 'detection_time'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8443
|
||||
DEFAULT_VERIFY_SSL = True
|
||||
DEFAULT_DETECTION_TIME = timedelta(seconds=300)
|
||||
|
||||
NOTIFICATION_ID = 'unifi_notification'
|
||||
NOTIFICATION_TITLE = 'Unifi Device Tracker Setup'
|
||||
@ -32,7 +36,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
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)
|
||||
port = config[DOMAIN].get(CONF_PORT)
|
||||
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
|
||||
detection_time = config[DOMAIN].get(CONF_DETECTION_TIME)
|
||||
|
||||
try:
|
||||
ctrl = Controller(host, username, password, port, version='v4',
|
||||
@ -61,14 +69,15 @@ def get_scanner(hass, config):
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
return UnifiScanner(ctrl)
|
||||
return UnifiScanner(ctrl, detection_time)
|
||||
|
||||
|
||||
class UnifiScanner(DeviceScanner):
|
||||
"""Provide device_tracker support from Unifi WAP client data."""
|
||||
|
||||
def __init__(self, controller):
|
||||
def __init__(self, controller, detection_time: timedelta):
|
||||
"""Initialize the scanner."""
|
||||
self._detection_time = detection_time
|
||||
self._controller = controller
|
||||
self._update()
|
||||
|
||||
@ -81,7 +90,11 @@ class UnifiScanner(DeviceScanner):
|
||||
_LOGGER.error("Failed to scan clients: %s", ex)
|
||||
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):
|
||||
"""Scan for devices."""
|
||||
@ -96,5 +109,5 @@ class UnifiScanner(DeviceScanner):
|
||||
"""
|
||||
client = self._clients.get(mac, {})
|
||||
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
|
||||
|
@ -6,7 +6,6 @@ https://home-assistant.io/components/device_tracker.upc_connect/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@ -19,6 +18,8 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
|
||||
REQUIREMENTS = ['defusedxml==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_IP = '192.168.0.1'
|
||||
@ -63,6 +64,8 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
@asyncio.coroutine
|
||||
def async_scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
import defusedxml.ElementTree as ET
|
||||
|
||||
if self.token is None:
|
||||
token_initialized = yield from self.async_initialize_token()
|
||||
if not token_initialized:
|
||||
|
@ -11,7 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_DEVICE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['enocean==0.31']
|
||||
REQUIREMENTS = ['enocean==0.40']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -215,20 +215,12 @@ def async_setup(hass, config: dict):
|
||||
target_fans = component.async_extract_from_service(service)
|
||||
params.pop(ATTR_ENTITY_ID, None)
|
||||
|
||||
update_tasks = []
|
||||
for fan in target_fans:
|
||||
yield from getattr(fan, method['method'])(**params)
|
||||
|
||||
update_tasks = []
|
||||
|
||||
for fan in target_fans:
|
||||
if not fan.should_poll:
|
||||
continue
|
||||
|
||||
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
|
||||
update_tasks.append(fan.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.components.fan import (FanEntity, DOMAIN, SPEED_OFF,
|
||||
SPEED_LOW, SPEED_MEDIUM,
|
||||
SPEED_HIGH)
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -73,19 +73,16 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
|
||||
@property
|
||||
def speed(self) -> str:
|
||||
"""Return the current speed."""
|
||||
return self.state
|
||||
return VALUE_TO_STATE.get(self.value)
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
"""Get the state of the ISY994 fan device."""
|
||||
return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
|
||||
def is_on(self) -> str:
|
||||
"""Get if the fan is on."""
|
||||
return self.value != 0
|
||||
|
||||
def set_speed(self, speed: str) -> None:
|
||||
"""Send the set speed command to the ISY994 fan device."""
|
||||
if not self._node.on(val=STATE_TO_VALUE.get(speed, 0)):
|
||||
_LOGGER.debug("Unable to set fan speed")
|
||||
else:
|
||||
self.speed = self.state
|
||||
self._node.on(val=STATE_TO_VALUE.get(speed, 255))
|
||||
|
||||
def turn_on(self, speed: str=None, **kwargs) -> None:
|
||||
"""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:
|
||||
"""Send the turn off command to the ISY994 fan device."""
|
||||
if not self._node.off():
|
||||
_LOGGER.debug("Unable to set fan speed")
|
||||
else:
|
||||
self.speed = self.state
|
||||
self._node.off()
|
||||
|
||||
@property
|
||||
def speed_list(self) -> list:
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['ha-ffmpeg==1.7']
|
||||
REQUIREMENTS = ['ha-ffmpeg==1.9']
|
||||
|
||||
DOMAIN = 'ffmpeg'
|
||||
|
||||
|
@ -225,8 +225,6 @@ def setup(hass, config):
|
||||
if DATA_EXTRA_HTML_URL not in hass.data:
|
||||
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',
|
||||
'dev-template', 'dev-mqtt', 'kiosk'):
|
||||
register_built_in_panel(hass, panel)
|
||||
|
52
homeassistant/components/google_assistant/__init__.py
Normal file
52
homeassistant/components/google_assistant/__init__.py
Normal 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
|
86
homeassistant/components/google_assistant/auth.py
Normal file
86
homeassistant/components/google_assistant/auth.py
Normal 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)
|
37
homeassistant/components/google_assistant/const.py
Normal file
37
homeassistant/components/google_assistant/const.py
Normal 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'
|
180
homeassistant/components/google_assistant/http.py
Normal file
180
homeassistant/components/google_assistant/http.py
Normal 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}
|
161
homeassistant/components/google_assistant/smart_home.py
Normal file
161
homeassistant/components/google_assistant/smart_home.py
Normal 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)
|
@ -17,7 +17,8 @@ import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
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 (
|
||||
HomeAssistantView, KEY_AUTHENTICATED, CONF_API_PASSWORD, CONF_SERVER_PORT,
|
||||
CONF_SSL_CERTIFICATE)
|
||||
@ -33,6 +34,8 @@ SERVICE_ADDON_START = 'addon_start'
|
||||
SERVICE_ADDON_STOP = 'addon_stop'
|
||||
SERVICE_ADDON_RESTART = 'addon_restart'
|
||||
SERVICE_ADDON_STDIN = 'addon_stdin'
|
||||
SERVICE_HOST_SHUTDOWN = 'host_shutdown'
|
||||
SERVICE_HOST_REBOOT = 'host_reboot'
|
||||
|
||||
ATTR_ADDON = 'addon'
|
||||
ATTR_INPUT = 'input'
|
||||
@ -63,6 +66,8 @@ MAP_SERVICE_API = {
|
||||
SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON),
|
||||
SERVICE_ADDON_RESTART: ('/addons/{addon}/restart', SCHEMA_ADDON),
|
||||
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')
|
||||
|
||||
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
|
||||
def async_service_handler(service):
|
||||
"""Handle service calls for HassIO."""
|
||||
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
|
||||
|
||||
yield from hassio.send_command(
|
||||
@ -138,6 +146,15 @@ class HassIO(object):
|
||||
|
||||
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
|
||||
def send_command(self, command, method="post", payload=None, timeout=10):
|
||||
"""Send API command to HassIO.
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.32']
|
||||
REQUIREMENTS = ['pyhomematic==0.1.34']
|
||||
|
||||
DOMAIN = 'homematic'
|
||||
|
||||
@ -69,7 +69,8 @@ HM_DEVICE_TYPES = {
|
||||
'IPSmoke'],
|
||||
DISCOVER_CLIMATE: [
|
||||
'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2',
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall'],
|
||||
'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall',
|
||||
'ThermostatGroup'],
|
||||
DISCOVER_BINARY_SENSORS: [
|
||||
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
|
||||
'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact',
|
||||
@ -129,6 +130,7 @@ CONF_LOCAL_IP = 'local_ip'
|
||||
CONF_LOCAL_PORT = 'local_port'
|
||||
CONF_IP = 'ip'
|
||||
CONF_PORT = 'port'
|
||||
CONF_PATH = 'path'
|
||||
CONF_CALLBACK_IP = 'callback_ip'
|
||||
CONF_CALLBACK_PORT = 'callback_port'
|
||||
CONF_RESOLVENAMES = 'resolvenames'
|
||||
@ -140,6 +142,7 @@ DEFAULT_LOCAL_IP = '0.0.0.0'
|
||||
DEFAULT_LOCAL_PORT = 0
|
||||
DEFAULT_RESOLVENAMES = False
|
||||
DEFAULT_PORT = 2001
|
||||
DEFAULT_PATH = ''
|
||||
DEFAULT_USERNAME = 'Admin'
|
||||
DEFAULT_PASSWORD = ''
|
||||
DEFAULT_VARIABLES = False
|
||||
@ -160,8 +163,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOSTS): {cv.match_all: {
|
||||
vol.Required(CONF_IP): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT):
|
||||
cv.port,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
|
||||
@ -258,6 +261,7 @@ def setup(hass, config):
|
||||
remotes[rname] = {}
|
||||
remotes[rname][CONF_IP] = server
|
||||
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_USERNAME] = rconfig.get(CONF_USERNAME)
|
||||
remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD)
|
||||
|
@ -358,19 +358,21 @@ class HomeAssistantView(object):
|
||||
requires_auth = True # Views inheriting from this class can override this
|
||||
|
||||
# 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."""
|
||||
msg = json.dumps(
|
||||
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
|
||||
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."""
|
||||
data = {'message': message}
|
||||
if message_code is not None:
|
||||
data['code'] = message_code
|
||||
return self.json(data, status_code)
|
||||
return self.json(data, status_code, headers=headers)
|
||||
|
||||
@asyncio.coroutine
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -26,6 +26,7 @@ CONF_KNX_TUNNELING = "tunneling"
|
||||
CONF_KNX_LOCAL_IP = "local_ip"
|
||||
CONF_KNX_FIRE_EVENT = "fire_event"
|
||||
CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter"
|
||||
CONF_KNX_STATE_UPDATER = "state_updater"
|
||||
|
||||
SERVICE_KNX_SEND = "send"
|
||||
SERVICE_KNX_ATTR_ADDRESS = "address"
|
||||
@ -35,7 +36,7 @@ ATTR_DISCOVER_DEVICES = 'devices'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['xknx==0.7.14']
|
||||
REQUIREMENTS = ['xknx==0.7.16']
|
||||
|
||||
TUNNELING_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@ -58,7 +59,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'):
|
||||
vol.All(
|
||||
cv.ensure_list,
|
||||
[cv.string])
|
||||
[cv.string]),
|
||||
vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -134,7 +136,7 @@ class KNXModule(object):
|
||||
"""Start KNX object. Connect to tunneling or Routing device."""
|
||||
connection_config = self.connection_config()
|
||||
yield from self.xknx.start(
|
||||
state_updater=True,
|
||||
state_updater=self.config[DOMAIN][CONF_KNX_STATE_UPDATER],
|
||||
connection_config=connection_config)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
self.initialized = True
|
||||
|
@ -274,6 +274,7 @@ def async_setup(hass, config):
|
||||
|
||||
preprocess_turn_on_alternatives(params)
|
||||
|
||||
update_tasks = []
|
||||
for light in target_lights:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
yield from light.async_turn_on(**params)
|
||||
@ -282,18 +283,9 @@ def async_setup(hass, config):
|
||||
else:
|
||||
yield from light.async_toggle(**params)
|
||||
|
||||
update_tasks = []
|
||||
|
||||
for light in target_lights:
|
||||
if not light.should_poll:
|
||||
continue
|
||||
|
||||
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
|
||||
update_tasks.append(light.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
@ -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,
|
||||
# used to simplify automation rules.
|
||||
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]
|
||||
scene_name = call.data[ATTR_SCENE_NAME]
|
||||
bridge.run_scene(group_name, scene_name)
|
||||
|
@ -213,7 +213,7 @@ class MqttJson(Light):
|
||||
except KeyError:
|
||||
pass
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid white value value received")
|
||||
_LOGGER.warning("Invalid white value received")
|
||||
|
||||
if self._xy is not None:
|
||||
try:
|
||||
|
@ -269,7 +269,7 @@ class OsramLightifyGroup(Luminary):
|
||||
def _get_state(self):
|
||||
"""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()
|
||||
return any(lights[light_id].on() for light_id in self._light_ids)
|
||||
|
87
homeassistant/components/light/skybell.py
Normal file
87
homeassistant/components/light/skybell.py
Normal 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
|
@ -14,26 +14,22 @@ from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, Light, SUPPORT_BRIGHTNESS)
|
||||
from homeassistant.const import (
|
||||
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.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.helpers.script import Script
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_STATES = [STATE_ON, STATE_OFF, 'true', 'false']
|
||||
|
||||
CONF_LIGHTS = 'lights'
|
||||
CONF_ON_ACTION = 'turn_on'
|
||||
CONF_OFF_ACTION = 'turn_off'
|
||||
CONF_LEVEL_ACTION = 'set_level'
|
||||
CONF_LEVEL_TEMPLATE = 'level_template'
|
||||
|
||||
|
||||
LIGHT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@ -51,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up Template Lights."""
|
||||
"""Set up the Template Lights."""
|
||||
lights = []
|
||||
|
||||
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")
|
||||
return False
|
||||
|
||||
async_add_devices(lights, True)
|
||||
async_add_devices(lights)
|
||||
return True
|
||||
|
||||
|
||||
@ -153,10 +149,6 @@ class LightTemplate(Light):
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
self._state = state.state == STATE_ON
|
||||
|
||||
@callback
|
||||
def template_light_state_listener(entity, old_state, new_state):
|
||||
"""Handle target device state changes."""
|
||||
@ -210,6 +202,7 @@ class LightTemplate(Light):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
print("ASYNC UPDATE")
|
||||
if self._template is not None:
|
||||
try:
|
||||
state = self._template.async_render().lower()
|
||||
|
@ -6,6 +6,8 @@ https://home-assistant.io/components/light.tplink/
|
||||
"""
|
||||
import logging
|
||||
import colorsys
|
||||
import time
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_NAME)
|
||||
from homeassistant.components.light import (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR,
|
||||
@ -17,11 +19,13 @@ from homeassistant.util.color import (
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
REQUIREMENTS = ['pyHS100==0.2.4.2']
|
||||
REQUIREMENTS = ['pyHS100==0.3.0']
|
||||
|
||||
_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):
|
||||
@ -64,24 +68,26 @@ class TPLinkSmartBulb(Light):
|
||||
def __init__(self, smartbulb: 'SmartBulb', name):
|
||||
"""Initialize the bulb."""
|
||||
self.smartbulb = smartbulb
|
||||
|
||||
# Use the name set on the device if not set
|
||||
if name is None:
|
||||
self._name = self.smartbulb.alias
|
||||
else:
|
||||
self._name = None
|
||||
if name is not None:
|
||||
self._name = name
|
||||
|
||||
self._state = None
|
||||
self._color_temp = None
|
||||
self._brightness = None
|
||||
self._rgb = None
|
||||
_LOGGER.debug("Setting up TP-Link Smart Bulb")
|
||||
self._supported_features = 0
|
||||
self._emeter_params = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the Smart Bulb, if any."""
|
||||
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):
|
||||
"""Turn the light on."""
|
||||
self.smartbulb.state = self.smartbulb.BULB_STATE_ON
|
||||
@ -119,30 +125,57 @@ class TPLinkSmartBulb(Light):
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""True if device is on."""
|
||||
"""Return True if device is on."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update the TP-Link Bulb's state."""
|
||||
from pyHS100 import SmartPlugException
|
||||
from pyHS100 import SmartDeviceException
|
||||
try:
|
||||
if self._supported_features == 0:
|
||||
self.get_features()
|
||||
self._state = (
|
||||
self.smartbulb.state == self.smartbulb.BULB_STATE_ON)
|
||||
self._brightness = brightness_from_percentage(
|
||||
self.smartbulb.brightness)
|
||||
if self.smartbulb.is_color:
|
||||
if self._name is None:
|
||||
self._name = self.smartbulb.alias
|
||||
if self._supported_features & SUPPORT_BRIGHTNESS:
|
||||
self._brightness = brightness_from_percentage(
|
||||
self.smartbulb.brightness)
|
||||
if self._supported_features & SUPPORT_COLOR_TEMP:
|
||||
if (self.smartbulb.color_temp is not None and
|
||||
self.smartbulb.color_temp != 0):
|
||||
self._color_temp = kelvin_to_mired(
|
||||
self.smartbulb.color_temp)
|
||||
if self._supported_features & SUPPORT_RGB_COLOR:
|
||||
self._rgb = hsv_to_rgb(self.smartbulb.hsv)
|
||||
except (SmartPlugException, OSError) as ex:
|
||||
_LOGGER.warning('Could not read state for %s: %s', self.name, ex)
|
||||
if self.smartbulb.has_emeter:
|
||||
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
|
||||
def supported_features(self):
|
||||
"""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:
|
||||
supported_features += SUPPORT_RGB_COLOR
|
||||
return supported_features
|
||||
self._supported_features += SUPPORT_RGB_COLOR
|
||||
|
@ -40,7 +40,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
devices_command = gateway.get_devices()
|
||||
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]
|
||||
if 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:
|
||||
groups_command = gateway.get_groups()
|
||||
groups_commands = yield from api(groups_command)
|
||||
groups = yield from api(*groups_commands)
|
||||
groups = yield from api(groups_commands)
|
||||
if groups:
|
||||
async_add_devices(TradfriGroup(group, api) for group in groups)
|
||||
|
||||
|
@ -54,6 +54,10 @@ SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT |
|
||||
SUPPORT_EFFECT |
|
||||
SUPPORT_COLOR_TEMP)
|
||||
|
||||
YEELIGHT_MIN_KELVIN = YEELIGHT_MAX_KELVIN = 2700
|
||||
YEELIGHT_RGB_MIN_KELVIN = 1700
|
||||
YEELIGHT_RGB_MAX_KELVIN = 6500
|
||||
|
||||
EFFECT_DISCO = "Disco"
|
||||
EFFECT_TEMP = "Slow Temp"
|
||||
EFFECT_STROBE = "Strobe epilepsy!"
|
||||
@ -191,6 +195,20 @@ class YeelightLight(Light):
|
||||
"""Return the brightness of this light between 1..255."""
|
||||
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):
|
||||
rgb = self._properties.get('rgb', None)
|
||||
color_mode = self._properties.get('color_mode', None)
|
||||
|
@ -90,24 +90,16 @@ def async_setup(hass, config):
|
||||
|
||||
code = service.data.get(ATTR_CODE)
|
||||
|
||||
update_tasks = []
|
||||
for entity in target_locks:
|
||||
if service.service == SERVICE_LOCK:
|
||||
yield from entity.async_lock(code=code)
|
||||
else:
|
||||
yield from entity.async_unlock(code=code)
|
||||
|
||||
update_tasks = []
|
||||
|
||||
for entity in target_locks:
|
||||
if not entity.should_poll:
|
||||
continue
|
||||
|
||||
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
|
||||
update_tasks.append(entity.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
@ -29,20 +29,17 @@ class TeslaLock(TeslaDevice, LockDevice):
|
||||
"""Initialisation of the lock."""
|
||||
self._state = None
|
||||
super().__init__(tesla_device, controller)
|
||||
self._name = self.tesla_device.name
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Send the lock command."""
|
||||
_LOGGER.debug("Locking doors for: %s", self._name)
|
||||
self.tesla_device.lock()
|
||||
self._state = STATE_LOCKED
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Send the unlock command."""
|
||||
_LOGGER.debug("Unlocking doors for: %s", self._name)
|
||||
self.tesla_device.unlock()
|
||||
self._state = STATE_UNLOCKED
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
|
18
homeassistant/components/map.py
Normal file
18
homeassistant/components/map.py
Normal 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
|
@ -15,7 +15,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['youtube_dl==2017.10.01']
|
||||
REQUIREMENTS = ['youtube_dl==2017.10.12']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -406,16 +406,9 @@ def async_setup(hass, config):
|
||||
update_tasks = []
|
||||
for player in target_players:
|
||||
yield from getattr(player, method['method'])(**params)
|
||||
|
||||
for player in target_players:
|
||||
if not player.should_poll:
|
||||
continue
|
||||
|
||||
update_coro = player.async_update_ha_state(True)
|
||||
if hasattr(player, 'async_update'):
|
||||
update_tasks.append(update_coro)
|
||||
else:
|
||||
yield from update_coro
|
||||
update_tasks.append(player.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
@ -225,7 +225,7 @@ class DenonDevice(MediaPlayerDevice):
|
||||
self.telnet_command('MU' + ('ON' if mute else 'OFF'))
|
||||
|
||||
def media_play(self):
|
||||
"""Play media media player."""
|
||||
"""Play media player."""
|
||||
self.telnet_command('NS9A')
|
||||
|
||||
def media_pause(self):
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['denonavr==0.5.3']
|
||||
REQUIREMENTS = ['denonavr==0.5.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['directpy==0.1']
|
||||
REQUIREMENTS = ['directpy==0.2']
|
||||
|
||||
DEFAULT_DEVICE = '0'
|
||||
DEFAULT_NAME = 'DirecTV Receiver'
|
||||
|
@ -124,7 +124,7 @@ class DuneHDPlayerEntity(MediaPlayerDevice):
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def media_play(self):
|
||||
"""Play media media player."""
|
||||
"""Play media player."""
|
||||
self._state = self._player.play()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
|
185
homeassistant/components/media_player/monoprice.py
Normal file
185
homeassistant/components/media_player/monoprice.py
Normal 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))
|
@ -287,12 +287,6 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._is_player_available = False
|
||||
self._machine_identifier = None
|
||||
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._player_state = 'idle'
|
||||
self._previous_volume_level = 1 # Used in fake muting
|
||||
@ -308,16 +302,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
|
||||
# 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
|
||||
self._clear_media()
|
||||
|
||||
self.refresh(device, session)
|
||||
|
||||
@ -339,10 +324,32 @@ class PlexClient(MediaPlayerDevice):
|
||||
'media_player', prefix,
|
||||
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):
|
||||
"""Refresh key device data."""
|
||||
# new data refresh
|
||||
if session:
|
||||
self._clear_media()
|
||||
|
||||
if session: # Not being triggered by Chrome or FireTablet Plex App
|
||||
self._session = session
|
||||
if device:
|
||||
self._device = device
|
||||
@ -369,9 +376,6 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._session.ratingKey)
|
||||
self._media_content_rating = self._convert_na_to_none(
|
||||
self._session.contentRating)
|
||||
else:
|
||||
self._media_position = None
|
||||
self._media_content_id = None
|
||||
|
||||
# player dependent data
|
||||
if self._session and self._session.player:
|
||||
@ -405,7 +409,6 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._session.duration)
|
||||
else:
|
||||
self._session_type = None
|
||||
self._media_duration = None
|
||||
|
||||
# media type
|
||||
if self._session_type == 'clip':
|
||||
@ -418,11 +421,9 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._media_content_type = MEDIA_TYPE_VIDEO
|
||||
elif self._session_type == 'track':
|
||||
self._media_content_type = MEDIA_TYPE_MUSIC
|
||||
else:
|
||||
self._media_content_type = None
|
||||
|
||||
# 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)
|
||||
|
||||
# Movies
|
||||
@ -431,9 +432,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._media_title += ' (' + str(self._session.year) + ')'
|
||||
|
||||
# TV Show
|
||||
if (self._is_player_active and
|
||||
self._media_content_type is MEDIA_TYPE_TVSHOW):
|
||||
|
||||
if self._media_content_type is MEDIA_TYPE_TVSHOW:
|
||||
# season number (00)
|
||||
if callable(self._convert_na_to_none(self._session.seasons)):
|
||||
self._media_season = self._convert_na_to_none(
|
||||
@ -443,23 +442,15 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._media_season = self._session.parentIndex.zfill(2)
|
||||
else:
|
||||
self._media_season = None
|
||||
|
||||
# show name
|
||||
self._media_series_title = self._convert_na_to_none(
|
||||
self._session.grandparentTitle)
|
||||
|
||||
# episode number (00)
|
||||
if self._convert_na_to_none(
|
||||
self._session.index) is not None:
|
||||
if self._convert_na_to_none(self._session.index) is not None:
|
||||
self._media_episode = str(self._session.index).zfill(2)
|
||||
else:
|
||||
self._media_season = None
|
||||
self._media_series_title = None
|
||||
self._media_episode = None
|
||||
|
||||
# Music
|
||||
if (self._is_player_active and
|
||||
self._media_content_type == MEDIA_TYPE_MUSIC):
|
||||
if self._media_content_type == MEDIA_TYPE_MUSIC:
|
||||
self._media_album_name = self._convert_na_to_none(
|
||||
self._session.parentTitle)
|
||||
self._media_album_artist = self._convert_na_to_none(
|
||||
@ -469,14 +460,9 @@ class PlexClient(MediaPlayerDevice):
|
||||
self._session.originalTitle)
|
||||
# use album artist if track artist is missing
|
||||
if self._media_artist is None:
|
||||
_LOGGER.debug("Using album artist because track artist was "
|
||||
"not found: %s", self.entity_id)
|
||||
_LOGGER.debug("Using album artist because track artist "
|
||||
"was not found: %s", self.entity_id)
|
||||
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
|
||||
if (self._session is not None
|
||||
@ -501,8 +487,6 @@ class PlexClient(MediaPlayerDevice):
|
||||
thumb_url = self._get_thumbnail_url(self._session.art)
|
||||
|
||||
self._media_image_url = thumb_url
|
||||
else:
|
||||
self._media_image_url = None
|
||||
|
||||
def _get_thumbnail_url(self, property_value):
|
||||
"""Return full URL (if exists) for a thumbnail property."""
|
||||
@ -521,6 +505,7 @@ class PlexClient(MediaPlayerDevice):
|
||||
"""Force client to idle."""
|
||||
self._state = STATE_IDLE
|
||||
self._session = None
|
||||
self._clear_media()
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_OFF, STATE_ON,
|
||||
STATE_PLAYING, STATE_IDLE)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['rxv==0.4.0']
|
||||
REQUIREMENTS = ['rxv==0.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (
|
||||
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'
|
||||
|
||||
@ -24,7 +24,6 @@ REQUIREMENTS = ['pymodbus==1.3.1']
|
||||
CONF_BAUDRATE = 'baudrate'
|
||||
CONF_BYTESIZE = 'bytesize'
|
||||
CONF_STOPBITS = 'stopbits'
|
||||
CONF_TYPE = 'type'
|
||||
CONF_PARITY = 'parity'
|
||||
|
||||
SERIAL_SCHEMA = {
|
||||
@ -35,12 +34,14 @@ SERIAL_SCHEMA = {
|
||||
vol.Required(CONF_PARITY): vol.Any('E', 'O', 'N'),
|
||||
vol.Required(CONF_STOPBITS): vol.Any(1, 2),
|
||||
vol.Required(CONF_TYPE): 'serial',
|
||||
vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout,
|
||||
}
|
||||
|
||||
ETHERNET_SCHEMA = {
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): cv.positive_int,
|
||||
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],
|
||||
stopbits=config[DOMAIN][CONF_STOPBITS],
|
||||
bytesize=config[DOMAIN][CONF_BYTESIZE],
|
||||
parity=config[DOMAIN][CONF_PARITY])
|
||||
parity=config[DOMAIN][CONF_PARITY],
|
||||
timeout=config[DOMAIN][CONF_TIMEOUT])
|
||||
elif client_type == 'tcp':
|
||||
from pymodbus.client.sync import ModbusTcpClient as ModbusClient
|
||||
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':
|
||||
from pymodbus.client.sync import ModbusUdpClient as ModbusClient
|
||||
client = ModbusClient(host=config[DOMAIN][CONF_HOST],
|
||||
port=config[DOMAIN][CONF_PORT])
|
||||
port=config[DOMAIN][CONF_PORT],
|
||||
timeout=config[DOMAIN][CONF_TIMEOUT])
|
||||
else:
|
||||
return False
|
||||
|
||||
|
@ -30,7 +30,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_PORT, CONF_PROTOCOL, CONF_PAYLOAD)
|
||||
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__)
|
||||
|
||||
|
70
homeassistant/components/namecheapdns.py
Normal file
70
homeassistant/components/namecheapdns.py
Normal 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
|
@ -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
|
||||
|
||||
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 logging
|
@ -94,8 +94,8 @@ NOTIFY_CALLBACK_EVENT = 'html5_notification'
|
||||
|
||||
# Badge and timestamp are Chrome specific (not in official spec)
|
||||
HTML5_SHOWNOTIFICATION_PARAMETERS = (
|
||||
'actions', 'badge', 'body', 'dir', 'icon', 'lang', 'renotify',
|
||||
'requireInteraction', 'tag', 'timestamp', 'vibrate')
|
||||
'actions', 'badge', 'body', 'dir', 'icon', 'image', 'lang',
|
||||
'renotify', 'requireInteraction', 'tag', 'timestamp', 'vibrate')
|
||||
|
||||
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
|
76
homeassistant/components/notify/rocketchat.py
Normal file
76
homeassistant/components/notify/rocketchat.py
Normal 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)
|
@ -15,13 +15,14 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
|
||||
|
||||
REQUIREMENTS = ['sleekxmpp==1.3.2',
|
||||
'dnspython3==1.15.0',
|
||||
'pyasn1==0.3.6',
|
||||
'pyasn1-modules==0.1.4']
|
||||
'pyasn1==0.3.7',
|
||||
'pyasn1-modules==0.1.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TLS = 'tls'
|
||||
CONF_VERIFY = 'verify'
|
||||
CONF_ROOM = 'room'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SENDER): cv.string,
|
||||
@ -29,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_RECIPIENT): cv.string,
|
||||
vol.Optional(CONF_TLS, 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(
|
||||
config.get(CONF_SENDER), config.get(CONF_PASSWORD),
|
||||
config.get(CONF_RECIPIENT), config.get(CONF_TLS),
|
||||
config.get(CONF_VERIFY))
|
||||
config.get(CONF_VERIFY), config.get(CONF_ROOM))
|
||||
|
||||
|
||||
class XmppNotificationService(BaseNotificationService):
|
||||
"""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."""
|
||||
self._sender = sender
|
||||
self._password = password
|
||||
self._recipient = recipient
|
||||
self._tls = tls
|
||||
self._verify = verify
|
||||
self._room = room
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = '{}: {}'.format(title, message) if title else message
|
||||
|
||||
send_message('{}/home-assistant'.format(self._sender), self._password,
|
||||
self._recipient, self._tls, self._verify, data)
|
||||
send_message('{}/home-assistant'.format(self._sender),
|
||||
self._password, self._recipient, self._tls,
|
||||
self._verify, self._room, data)
|
||||
|
||||
|
||||
def send_message(sender, password, recipient, use_tls,
|
||||
verify_certificate, message):
|
||||
verify_certificate, room, message):
|
||||
"""Send a message over XMPP."""
|
||||
import sleekxmpp
|
||||
|
||||
@ -78,6 +82,8 @@ def send_message(sender, password, recipient, use_tls,
|
||||
self.use_ipv6 = False
|
||||
self.add_event_handler('failed_auth', self.check_credentials)
|
||||
self.add_event_handler('session_start', self.start)
|
||||
if room:
|
||||
self.register_plugin('xep_0045') # MUC
|
||||
if not verify_certificate:
|
||||
self.add_event_handler('ssl_invalid_cert',
|
||||
self.discard_ssl_invalid_cert)
|
||||
@ -89,7 +95,13 @@ def send_message(sender, password, recipient, use_tls,
|
||||
"""Start the communication and sends the message."""
|
||||
self.send_presence()
|
||||
self.get_roster()
|
||||
self.send_message(mto=recipient, mbody=message, mtype='chat')
|
||||
|
||||
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.disconnect(wait=True)
|
||||
|
||||
def check_credentials(self, event):
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""Component to allow running Python scripts."""
|
||||
import glob
|
||||
import os
|
||||
import logging
|
||||
import datetime
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@ -10,6 +11,7 @@ from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util import sanitize_filename
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
DOMAIN = 'python_script'
|
||||
REQUIREMENTS = ['restrictedpython==4.0a3']
|
||||
@ -25,6 +27,13 @@ ALLOWED_EVENTBUS = set(['fire'])
|
||||
ALLOWED_STATEMACHINE = set(['entity_ids', 'all', 'get', 'is_state',
|
||||
'is_state_attr', 'remove', 'set'])
|
||||
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):
|
||||
@ -111,7 +120,10 @@ def execute(hass, filename, source, data=None):
|
||||
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.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(
|
||||
obj.__class__.__name__, name))
|
||||
|
||||
@ -120,6 +132,8 @@ def execute(hass, filename, source, data=None):
|
||||
builtins = safe_builtins.copy()
|
||||
builtins.update(utility_builtins)
|
||||
builtins['datetime'] = datetime
|
||||
builtins['time'] = TimeWrapper()
|
||||
builtins['dt_util'] = dt_util
|
||||
restricted_globals = {
|
||||
'__builtins__': builtins,
|
||||
'_print_': StubPrinter,
|
||||
@ -159,3 +173,24 @@ class StubPrinter:
|
||||
# pylint: disable=no-self-use
|
||||
_LOGGER.warning(
|
||||
"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)
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.helpers.dispatcher import (
|
||||
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
REQUIREMENTS = ['raincloudy==0.0.1']
|
||||
REQUIREMENTS = ['raincloudy==0.0.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -351,6 +351,7 @@ class Recorder(threading.Thread):
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlite3 import Connection
|
||||
|
||||
from . import models
|
||||
|
||||
@ -360,7 +361,7 @@ class Recorder(threading.Thread):
|
||||
@event.listens_for(Engine, "connect")
|
||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
"""Set sqlite's WAL mode."""
|
||||
if self.db_url.startswith("sqlite://"):
|
||||
if isinstance(dbapi_connection, Connection):
|
||||
old_isolation = dbapi_connection.isolation_level
|
||||
dbapi_connection.isolation_level = None
|
||||
cursor = dbapi_connection.cursor()
|
||||
|
@ -148,6 +148,7 @@ def async_setup(hass, config):
|
||||
num_repeats = service.data.get(ATTR_NUM_REPEATS)
|
||||
delay_secs = service.data.get(ATTR_DELAY_SECS)
|
||||
|
||||
update_tasks = []
|
||||
for remote in target_remotes:
|
||||
if service.service == SERVICE_TURN_ON:
|
||||
yield from remote.async_turn_on(activity=activity_id)
|
||||
@ -160,17 +161,9 @@ def async_setup(hass, config):
|
||||
else:
|
||||
yield from remote.async_turn_off(activity=activity_id)
|
||||
|
||||
update_tasks = []
|
||||
for remote in target_remotes:
|
||||
if not remote.should_poll:
|
||||
continue
|
||||
|
||||
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
|
||||
update_tasks.append(remote.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
81
homeassistant/components/sensor/abode.py
Normal file
81
homeassistant/components/sensor/abode.py
Normal 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
|
@ -4,8 +4,6 @@ Support for AirVisual air quality sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.airvisual/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
from datetime import timedelta
|
||||
|
||||
@ -15,13 +13,15 @@ import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
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.util import Throttle
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
REQUIREMENTS = ['pyairvisual==1.0.0']
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
|
||||
ATTR_CITY = 'city'
|
||||
ATTR_COUNTRY = 'country'
|
||||
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
|
||||
@ -32,6 +32,7 @@ ATTR_TIMESTAMP = 'timestamp'
|
||||
CONF_CITY = 'city'
|
||||
CONF_COUNTRY = 'country'
|
||||
CONF_RADIUS = 'radius'
|
||||
CONF_ATTRIBUTION = "Data provided by AirVisual"
|
||||
|
||||
MASS_PARTS_PER_MILLION = 'ppm'
|
||||
MASS_PARTS_PER_BILLION = 'ppb'
|
||||
@ -39,56 +40,22 @@ VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
POLLUTANT_LEVEL_MAPPING = [{
|
||||
'label': 'Good',
|
||||
'minimum': 0,
|
||||
'maximum': 50
|
||||
}, {
|
||||
'label': 'Moderate',
|
||||
'minimum': 51,
|
||||
'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_LEVEL_MAPPING = [
|
||||
{'label': 'Good', 'minimum': 0, 'maximum': 50},
|
||||
{'label': 'Moderate', 'minimum': 51, 'maximum': 100},
|
||||
{'label': 'Unhealthy for sensitive group', '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 = {
|
||||
'co': {
|
||||
'label': 'Carbon Monoxide',
|
||||
'unit': MASS_PARTS_PER_MILLION
|
||||
},
|
||||
'n2': {
|
||||
'label': 'Nitrogen Dioxide',
|
||||
'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
|
||||
}
|
||||
'co': {'label': 'Carbon Monoxide', 'unit': MASS_PARTS_PER_MILLION},
|
||||
'n2': {'label': 'Nitrogen Dioxide', '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.'}
|
||||
@ -99,32 +66,23 @@ SENSOR_TYPES = [
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY):
|
||||
cv.string,
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
|
||||
vol.Optional(CONF_LATITUDE):
|
||||
cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE):
|
||||
cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=1000):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_CITY):
|
||||
cv.string,
|
||||
vol.Optional(CONF_STATE):
|
||||
cv.string,
|
||||
vol.Optional(CONF_COUNTRY):
|
||||
cv.string
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
|
||||
vol.Optional(CONF_CITY): cv.string,
|
||||
vol.Optional(CONF_COUNTRY): cv.string,
|
||||
vol.Optional(CONF_LATITUDE): cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE): cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=1000): cv.positive_int,
|
||||
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
|
||||
vol.Optional(CONF_STATE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Configure the platform and add the sensors."""
|
||||
import pyairvisual as pav
|
||||
|
||||
_LOGGER.debug('Received configuration: %s', config)
|
||||
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
monitored_locales = config.get(CONF_MONITORED_CONDITIONS)
|
||||
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)
|
||||
state = config.get(CONF_STATE)
|
||||
country = config.get(CONF_COUNTRY)
|
||||
show_on_map = config.get(CONF_SHOW_ON_MAP)
|
||||
|
||||
if city and state and country:
|
||||
_LOGGER.debug('Using city, state, and country: %s, %s, %s', city,
|
||||
state, country)
|
||||
_LOGGER.debug(
|
||||
"Using city, state, and country: %s, %s, %s", city, state, country)
|
||||
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:
|
||||
_LOGGER.debug('Using latitude and longitude: %s, %s', latitude,
|
||||
longitude)
|
||||
_LOGGER.debug(
|
||||
"Using latitude and longitude: %s, %s", latitude, longitude)
|
||||
data = AirVisualData(
|
||||
pav.Client(api_key),
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
radius=radius)
|
||||
pav.Client(api_key), latitude=latitude, longitude=longitude,
|
||||
radius=radius, show_on_map=show_on_map)
|
||||
|
||||
data.update()
|
||||
sensors = []
|
||||
for locale in monitored_locales:
|
||||
for sensor_class, name, icon in SENSOR_TYPES:
|
||||
sensors.append(globals()[sensor_class](data, name, icon, locale))
|
||||
|
||||
async_add_devices(sensors, True)
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
def merge_two_dicts(dict1, dict2):
|
||||
@ -167,7 +126,7 @@ class AirVisualBaseSensor(Entity):
|
||||
"""Define a base class for all of our sensors."""
|
||||
|
||||
def __init__(self, data, name, icon, locale):
|
||||
"""Initialize."""
|
||||
"""Initialize the sensor."""
|
||||
self._data = data
|
||||
self._icon = icon
|
||||
self._locale = locale
|
||||
@ -177,17 +136,24 @@ class AirVisualBaseSensor(Entity):
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: 'AirVisual©',
|
||||
"""Return the device state attributes."""
|
||||
attrs = {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
ATTR_CITY: self._data.city,
|
||||
ATTR_COUNTRY: self._data.country,
|
||||
ATTR_REGION: self._data.state,
|
||||
ATTR_LATITUDE: self._data.latitude,
|
||||
ATTR_LONGITUDE: self._data.longitude,
|
||||
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
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
@ -203,20 +169,14 @@ class AirVisualBaseSensor(Entity):
|
||||
"""Return the 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):
|
||||
"""Define a sensor to measure air pollution level."""
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
def update(self):
|
||||
"""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))
|
||||
try:
|
||||
[level] = [
|
||||
@ -238,10 +198,9 @@ class AirQualityIndexSensor(AirVisualBaseSensor):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return 'PSI'
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
def update(self):
|
||||
"""Update the status of the sensor."""
|
||||
yield from super().async_update()
|
||||
self._data.update()
|
||||
|
||||
self._state = self._data.pollution_info.get(
|
||||
'aqi{0}'.format(self._locale))
|
||||
@ -251,23 +210,23 @@ class MainPollutantSensor(AirVisualBaseSensor):
|
||||
"""Define a sensor to the main pollutant of an area."""
|
||||
|
||||
def __init__(self, data, name, icon, locale):
|
||||
"""Initialize."""
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(data, name, icon, locale)
|
||||
self._symbol = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
"""Return the device state attributes."""
|
||||
return merge_two_dicts(super().device_state_attributes, {
|
||||
ATTR_POLLUTANT_SYMBOL: self._symbol,
|
||||
ATTR_POLLUTANT_UNIT: self._unit
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
def update(self):
|
||||
"""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))
|
||||
pollution_info = POLLUTANT_MAPPING.get(symbol, {})
|
||||
self._state = pollution_info.get('label')
|
||||
@ -279,7 +238,7 @@ class AirVisualData(object):
|
||||
"""Define an object to hold sensor data."""
|
||||
|
||||
def __init__(self, client, **kwargs):
|
||||
"""Initialize."""
|
||||
"""Initialize the AirVisual data element."""
|
||||
self._client = client
|
||||
self.pollution_info = None
|
||||
|
||||
@ -291,6 +250,8 @@ class AirVisualData(object):
|
||||
self.longitude = kwargs.get(CONF_LONGITUDE)
|
||||
self._radius = kwargs.get(CONF_RADIUS)
|
||||
|
||||
self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP)
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update with new AirVisual data."""
|
||||
@ -298,21 +259,21 @@ class AirVisualData(object):
|
||||
|
||||
try:
|
||||
if self.city and self.state and self.country:
|
||||
resp = self._client.city(self.city, self.state,
|
||||
self.country).get('data')
|
||||
self.longitude, self.latitude = resp.get('location').get(
|
||||
'coordinates')
|
||||
resp = self._client.city(
|
||||
self.city, self.state, self.country).get('data')
|
||||
else:
|
||||
resp = self._client.nearest_city(self.latitude, self.longitude,
|
||||
self._radius).get('data')
|
||||
_LOGGER.debug('New data retrieved: %s', resp)
|
||||
resp = self._client.nearest_city(
|
||||
self.latitude, self.longitude, self._radius).get('data')
|
||||
_LOGGER.debug("New data retrieved: %s", resp)
|
||||
|
||||
self.city = resp.get('city')
|
||||
self.state = resp.get('state')
|
||||
self.country = resp.get('country')
|
||||
self.longitude, self.latitude = resp.get('location').get(
|
||||
'coordinates')
|
||||
self.pollution_info = resp.get('current', {}).get('pollution', {})
|
||||
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__)
|
||||
_LOGGER.debug(exc_info)
|
||||
self.pollution_info = {}
|
||||
|
@ -9,6 +9,7 @@ import asyncio
|
||||
from homeassistant.components.android_ip_webcam import (
|
||||
KEY_MAP, ICON_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST,
|
||||
CONF_NAME, CONF_SENSORS)
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
|
||||
DEPENDENCIES = ['android_ip_webcam']
|
||||
|
||||
@ -75,14 +76,5 @@ class IPWebcamSensor(AndroidIPCamEntity):
|
||||
def icon(self):
|
||||
"""Return the icon for the sensor."""
|
||||
if self._sensor == 'battery_level' and self._state is not None:
|
||||
rounded_level = round(int(self._state), -1)
|
||||
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_for_battery_level(int(self._state))
|
||||
return ICON_MAP.get(self._sensor, 'mdi:eye')
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user