mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
commit
d9a6d9ee73
@ -309,6 +309,7 @@ omit =
|
||||
homeassistant/components/device_tracker/cisco_ios.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/gpslogger.py
|
||||
homeassistant/components/device_tracker/hitron_coda.py
|
||||
homeassistant/components/device_tracker/huawei_router.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/keenetic_ndms2.py
|
||||
@ -325,6 +326,7 @@ omit =
|
||||
homeassistant/components/device_tracker/thomson.py
|
||||
homeassistant/components/device_tracker/tomato.py
|
||||
homeassistant/components/device_tracker/tado.py
|
||||
homeassistant/components/device_tracker/tile.py
|
||||
homeassistant/components/device_tracker/tplink.py
|
||||
homeassistant/components/device_tracker/trackr.py
|
||||
homeassistant/components/device_tracker/ubus.py
|
||||
@ -517,6 +519,7 @@ omit =
|
||||
homeassistant/components/sensor/influxdb.py
|
||||
homeassistant/components/sensor/irish_rail_transport.py
|
||||
homeassistant/components/sensor/kwb.py
|
||||
homeassistant/components/sensor/lacrosse.py
|
||||
homeassistant/components/sensor/lastfm.py
|
||||
homeassistant/components/sensor/linux_battery.py
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
@ -545,6 +548,7 @@ omit =
|
||||
homeassistant/components/sensor/pocketcasts.py
|
||||
homeassistant/components/sensor/pushbullet.py
|
||||
homeassistant/components/sensor/pvoutput.py
|
||||
homeassistant/components/sensor/pyload.py
|
||||
homeassistant/components/sensor/qnap.py
|
||||
homeassistant/components/sensor/radarr.py
|
||||
homeassistant/components/sensor/ripple.py
|
||||
@ -579,6 +583,7 @@ omit =
|
||||
homeassistant/components/sensor/upnp.py
|
||||
homeassistant/components/sensor/ups.py
|
||||
homeassistant/components/sensor/vasttrafik.py
|
||||
homeassistant/components/sensor/viaggiatreno.py
|
||||
homeassistant/components/sensor/waqi.py
|
||||
homeassistant/components/sensor/whois.py
|
||||
homeassistant/components/sensor/worldtidesinfo.py
|
||||
|
@ -64,6 +64,10 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
|
||||
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
homeassistant/components/velux.py @Julius2342
|
||||
homeassistant/components/*/velux.py @Julius2342
|
||||
homeassistant/components/knx.py @Julius2342
|
||||
homeassistant/components/*/knx.py @Julius2342
|
||||
homeassistant/components/tesla.py @zabuldon
|
||||
homeassistant/components/*/tesla.py @zabuldon
|
||||
homeassistant/components/*/tradfri.py @ggravlingen
|
||||
|
@ -19,15 +19,13 @@
|
||||
#
|
||||
import sys
|
||||
import os
|
||||
from os.path import relpath
|
||||
import inspect
|
||||
from homeassistant.const import (__version__, __short_version__, PROJECT_NAME,
|
||||
PROJECT_LONG_DESCRIPTION,
|
||||
PROJECT_COPYRIGHT, PROJECT_AUTHOR,
|
||||
PROJECT_GITHUB_USERNAME,
|
||||
PROJECT_GITHUB_REPOSITORY,
|
||||
GITHUB_PATH, GITHUB_URL)
|
||||
|
||||
from homeassistant.const import __version__, __short_version__
|
||||
from setup import (
|
||||
PROJECT_NAME, PROJECT_LONG_DESCRIPTION, PROJECT_COPYRIGHT, PROJECT_AUTHOR,
|
||||
PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY, GITHUB_PATH,
|
||||
GITHUB_URL)
|
||||
|
||||
sys.path.insert(0, os.path.abspath('_ext'))
|
||||
sys.path.insert(0, os.path.abspath('../homeassistant'))
|
||||
@ -87,9 +85,7 @@ edit_on_github_src_path = 'docs/source/'
|
||||
|
||||
|
||||
def linkcode_resolve(domain, info):
|
||||
"""
|
||||
Determine the URL corresponding to Python object
|
||||
"""
|
||||
"""Determine the URL corresponding to Python object."""
|
||||
if domain != 'py':
|
||||
return None
|
||||
modname = info['module']
|
||||
|
@ -30,8 +30,8 @@ ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
DATA_LOGGING = 'logging'
|
||||
|
||||
FIRST_INIT_COMPONENT = set((
|
||||
'recorder', 'mqtt', 'mqtt_eventstream', 'logger', 'introduction',
|
||||
'frontend', 'history'))
|
||||
'system_log', 'recorder', 'mqtt', 'mqtt_eventstream', 'logger',
|
||||
'introduction', 'frontend', 'history'))
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.12.1']
|
||||
REQUIREMENTS = ['abodepy==0.12.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -34,10 +34,8 @@ def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
return
|
||||
|
||||
devices = [SpcAlarm(hass=hass,
|
||||
area_id=area['id'],
|
||||
name=area['name'],
|
||||
state=_get_alarm_state(area['mode']))
|
||||
api = hass.data[DATA_API]
|
||||
devices = [SpcAlarm(api, area)
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_devices(devices)
|
||||
@ -46,21 +44,29 @@ def async_setup_platform(hass, config, async_add_devices,
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Represents the SPC alarm panel."""
|
||||
|
||||
def __init__(self, hass, area_id, name, state):
|
||||
def __init__(self, api, area):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
self._hass = hass
|
||||
self._area_id = area_id
|
||||
self._name = name
|
||||
self._state = state
|
||||
self._api = hass.data[DATA_API]
|
||||
|
||||
hass.data[DATA_REGISTRY].register_alarm_device(area_id, self)
|
||||
self._area_id = area['id']
|
||||
self._name = area['name']
|
||||
self._state = _get_alarm_state(area['mode'])
|
||||
if self._state == STATE_ALARM_DISARMED:
|
||||
self._changed_by = area.get('last_unset_user_name', 'unknown')
|
||||
else:
|
||||
self._changed_by = area.get('last_set_user_name', 'unknown')
|
||||
self._api = api
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
def async_added_to_hass(self):
|
||||
"""Calbback for init handlers."""
|
||||
self.hass.data[DATA_REGISTRY].register_alarm_device(
|
||||
self._area_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state, extra):
|
||||
"""Update the alarm panel with a new state."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
self._changed_by = extra.get('changed_by', 'unknown')
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
@ -72,6 +78,11 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
"""Return the user the last change was triggered by."""
|
||||
return self._changed_by
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.12']
|
||||
REQUIREMENTS = ['total_connect_client==0.13']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -15,4 +15,6 @@ ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
|
||||
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
|
@ -3,6 +3,7 @@ Support for Alexa skill service end point.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/alexa/
|
||||
|
||||
"""
|
||||
import asyncio
|
||||
import enum
|
||||
@ -13,7 +14,7 @@ from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.components import http
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, SYN_RESOLUTION_MATCH
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
|
||||
@ -123,6 +124,43 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||
return self.json(alexa_response)
|
||||
|
||||
|
||||
def resolve_slot_synonyms(key, request):
|
||||
"""Check slot request for synonym resolutions."""
|
||||
# Default to the spoken slot value if more than one or none are found. For
|
||||
# reference to the request object structure, see the Alexa docs:
|
||||
# https://tinyurl.com/ybvm7jhs
|
||||
resolved_value = request['value']
|
||||
|
||||
if ('resolutions' in request and
|
||||
'resolutionsPerAuthority' in request['resolutions'] and
|
||||
len(request['resolutions']['resolutionsPerAuthority']) >= 1):
|
||||
|
||||
# Extract all of the possible values from each authority with a
|
||||
# successful match
|
||||
possible_values = []
|
||||
|
||||
for entry in request['resolutions']['resolutionsPerAuthority']:
|
||||
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
|
||||
continue
|
||||
|
||||
possible_values.extend([item['value']['name']
|
||||
for item
|
||||
in entry['values']])
|
||||
|
||||
# If there is only one match use the resolved value, otherwise the
|
||||
# resolution cannot be determined, so use the spoken slot value
|
||||
if len(possible_values) == 1:
|
||||
resolved_value = possible_values[0]
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
'Found multiple synonym resolutions for slot value: {%s: %s}',
|
||||
key,
|
||||
request['value']
|
||||
)
|
||||
|
||||
return resolved_value
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
@ -135,12 +173,17 @@ class AlexaResponse(object):
|
||||
self.session_attributes = {}
|
||||
self.should_end_session = True
|
||||
self.variables = {}
|
||||
|
||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||
if intent_info is not None:
|
||||
for key, value in intent_info.get('slots', {}).items():
|
||||
if 'value' in value:
|
||||
underscored_key = key.replace('.', '_')
|
||||
self.variables[underscored_key] = value['value']
|
||||
# Only include slots with values
|
||||
if 'value' not in value:
|
||||
continue
|
||||
|
||||
_key = key.replace('.', '_')
|
||||
|
||||
self.variables[_key] = resolve_slot_synonyms(key, value)
|
||||
|
||||
def add_card(self, card_type, title, content):
|
||||
"""Add a card to the response."""
|
||||
|
@ -1,12 +1,20 @@
|
||||
"""Support for alexa Smart Home Skill API."""
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import math
|
||||
from uuid import uuid4
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
from homeassistant.components import switch, light
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET)
|
||||
from homeassistant.components import (
|
||||
alert, automation, cover, fan, group, input_boolean, light, lock,
|
||||
media_player, scene, script, switch)
|
||||
import homeassistant.util.color as color_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
@ -14,14 +22,32 @@ HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_DIRECTIVE = 'directive'
|
||||
API_ENDPOINT = 'endpoint'
|
||||
API_EVENT = 'event'
|
||||
API_HEADER = 'header'
|
||||
API_PAYLOAD = 'payload'
|
||||
API_ENDPOINT = 'endpoint'
|
||||
|
||||
ATTR_ALEXA_DESCRIPTION = 'alexa_description'
|
||||
ATTR_ALEXA_DISPLAY_CATEGORIES = 'alexa_display_categories'
|
||||
ATTR_ALEXA_HIDDEN = 'alexa_hidden'
|
||||
ATTR_ALEXA_NAME = 'alexa_name'
|
||||
|
||||
|
||||
MAPPING_COMPONENT = {
|
||||
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
||||
alert.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
automation.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
cover.DOMAIN: [
|
||||
'DOOR', ('Alexa.PowerController',), {
|
||||
cover.SUPPORT_SET_POSITION: 'Alexa.PercentageController',
|
||||
}
|
||||
],
|
||||
fan.DOMAIN: [
|
||||
'OTHER', ('Alexa.PowerController',), {
|
||||
fan.SUPPORT_SET_SPEED: 'Alexa.PercentageController',
|
||||
}
|
||||
],
|
||||
group.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
input_boolean.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
light.DOMAIN: [
|
||||
'LIGHT', ('Alexa.PowerController',), {
|
||||
light.SUPPORT_BRIGHTNESS: 'Alexa.BrightnessController',
|
||||
@ -30,11 +56,28 @@ MAPPING_COMPONENT = {
|
||||
light.SUPPORT_COLOR_TEMP: 'Alexa.ColorTemperatureController',
|
||||
}
|
||||
],
|
||||
lock.DOMAIN: ['SMARTLOCK', ('Alexa.LockController',), None],
|
||||
media_player.DOMAIN: [
|
||||
'TV', ('Alexa.PowerController',), {
|
||||
media_player.SUPPORT_VOLUME_SET: 'Alexa.Speaker',
|
||||
media_player.SUPPORT_PLAY: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_PAUSE: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_STOP: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_NEXT_TRACK: 'Alexa.PlaybackController',
|
||||
media_player.SUPPORT_PREVIOUS_TRACK: 'Alexa.PlaybackController',
|
||||
}
|
||||
],
|
||||
scene.DOMAIN: ['ACTIVITY_TRIGGER', ('Alexa.SceneController',), None],
|
||||
script.DOMAIN: ['OTHER', ('Alexa.PowerController',), None],
|
||||
switch.DOMAIN: ['SWITCH', ('Alexa.PowerController',), None],
|
||||
}
|
||||
|
||||
|
||||
Config = namedtuple('AlexaConfig', 'filter')
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, message):
|
||||
def async_handle_message(hass, config, message):
|
||||
"""Handle incoming API messages."""
|
||||
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
|
||||
|
||||
@ -50,7 +93,7 @@ def async_handle_message(hass, message):
|
||||
"Unsupported API request %s/%s", namespace, name)
|
||||
return api_error(message)
|
||||
|
||||
return (yield from funct_ref(hass, message))
|
||||
return (yield from funct_ref(hass, config, message))
|
||||
|
||||
|
||||
def api_message(request, name='Response', namespace='Alexa', payload=None):
|
||||
@ -99,7 +142,7 @@ def api_error(request, error_type='INTERNAL_ERROR', error_message=""):
|
||||
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
@asyncio.coroutine
|
||||
def async_api_discovery(hass, request):
|
||||
def async_api_discovery(hass, config, request):
|
||||
"""Create a API formatted discovery response.
|
||||
|
||||
Async friendly.
|
||||
@ -107,18 +150,40 @@ def async_api_discovery(hass, request):
|
||||
discovery_endpoints = []
|
||||
|
||||
for entity in hass.states.async_all():
|
||||
if not config.filter(entity.entity_id):
|
||||
_LOGGER.debug("Not exposing %s because filtered by config",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
if entity.attributes.get(ATTR_ALEXA_HIDDEN, False):
|
||||
_LOGGER.debug("Not exposing %s because alexa_hidden is true",
|
||||
entity.entity_id)
|
||||
continue
|
||||
|
||||
class_data = MAPPING_COMPONENT.get(entity.domain)
|
||||
|
||||
if not class_data:
|
||||
continue
|
||||
|
||||
friendly_name = entity.attributes.get(ATTR_ALEXA_NAME, entity.name)
|
||||
description = entity.attributes.get(ATTR_ALEXA_DESCRIPTION,
|
||||
entity.entity_id)
|
||||
|
||||
# Required description as per Amazon Scene docs
|
||||
if entity.domain == scene.DOMAIN:
|
||||
scene_fmt = '%s (Scene connected via Home Assistant)'
|
||||
description = scene_fmt.format(description)
|
||||
|
||||
cat_key = ATTR_ALEXA_DISPLAY_CATEGORIES
|
||||
display_categories = entity.attributes.get(cat_key, class_data[0])
|
||||
|
||||
endpoint = {
|
||||
'displayCategories': [class_data[0]],
|
||||
'displayCategories': [display_categories],
|
||||
'additionalApplianceDetails': {},
|
||||
'endpointId': entity.entity_id.replace('.', '#'),
|
||||
'friendlyName': entity.name,
|
||||
'description': '',
|
||||
'manufacturerName': 'Unknown',
|
||||
'friendlyName': friendly_name,
|
||||
'description': description,
|
||||
'manufacturerName': 'Home Assistant',
|
||||
}
|
||||
actions = set()
|
||||
|
||||
@ -153,7 +218,7 @@ def async_api_discovery(hass, request):
|
||||
def extract_entity(funct):
|
||||
"""Decorator for extract entity object from request."""
|
||||
@asyncio.coroutine
|
||||
def async_api_entity_wrapper(hass, request):
|
||||
def async_api_entity_wrapper(hass, config, request):
|
||||
"""Process a turn on request."""
|
||||
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
|
||||
|
||||
@ -164,7 +229,7 @@ def extract_entity(funct):
|
||||
request[API_HEADER]['name'], entity_id)
|
||||
return api_error(request, error_type='NO_SUCH_ENDPOINT')
|
||||
|
||||
return (yield from funct(hass, request, entity))
|
||||
return (yield from funct(hass, config, request, entity))
|
||||
|
||||
return async_api_entity_wrapper
|
||||
|
||||
@ -172,9 +237,13 @@ def extract_entity(funct):
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_on(hass, request, entity):
|
||||
def async_api_turn_on(hass, config, request, entity):
|
||||
"""Process a turn on request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
@ -184,9 +253,13 @@ def async_api_turn_on(hass, request, entity):
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_off(hass, request, entity):
|
||||
def async_api_turn_off(hass, config, request, entity):
|
||||
"""Process a turn off request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_OFF, {
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
domain = ha.DOMAIN
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
@ -196,7 +269,7 @@ def async_api_turn_off(hass, request, entity):
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_brightness(hass, request, entity):
|
||||
def async_api_set_brightness(hass, config, request, entity):
|
||||
"""Process a set brightness request."""
|
||||
brightness = int(request[API_PAYLOAD]['brightness'])
|
||||
|
||||
@ -211,7 +284,7 @@ def async_api_set_brightness(hass, request, entity):
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_brightness(hass, request, entity):
|
||||
def async_api_adjust_brightness(hass, config, request, entity):
|
||||
"""Process a adjust brightness request."""
|
||||
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
|
||||
|
||||
@ -235,7 +308,7 @@ def async_api_adjust_brightness(hass, request, entity):
|
||||
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color(hass, request, entity):
|
||||
def async_api_set_color(hass, config, request, entity):
|
||||
"""Process a set color request."""
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES)
|
||||
rgb = color_util.color_hsb_to_RGB(
|
||||
@ -263,7 +336,7 @@ def async_api_set_color(hass, request, entity):
|
||||
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color_temperature(hass, request, entity):
|
||||
def async_api_set_color_temperature(hass, config, request, entity):
|
||||
"""Process a set color temperature request."""
|
||||
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
|
||||
|
||||
@ -279,7 +352,7 @@ def async_api_set_color_temperature(hass, request, entity):
|
||||
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_decrease_color_temp(hass, request, entity):
|
||||
def async_api_decrease_color_temp(hass, config, request, entity):
|
||||
"""Process a decrease color temperature request."""
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
|
||||
@ -297,7 +370,7 @@ def async_api_decrease_color_temp(hass, request, entity):
|
||||
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_increase_color_temp(hass, request, entity):
|
||||
def async_api_increase_color_temp(hass, config, request, entity):
|
||||
"""Process a increase color temperature request."""
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||
@ -309,3 +382,262 @@ def async_api_increase_color_temp(hass, request, entity):
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_activate(hass, config, request, entity):
|
||||
"""Process a activate request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_percentage(hass, config, request, entity):
|
||||
"""Process a set percentage request."""
|
||||
percentage = int(request[API_PAYLOAD]['percentage'])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_percentage(hass, config, request, entity):
|
||||
"""Process a adjust percentage request."""
|
||||
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
|
||||
service = None
|
||||
data = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
if entity.domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_SET_SPEED
|
||||
speed = entity.attributes.get(fan.ATTR_SPEED)
|
||||
|
||||
if speed == "off":
|
||||
current = 0
|
||||
elif speed == "low":
|
||||
current = 33
|
||||
elif speed == "medium":
|
||||
current = 66
|
||||
elif speed == "high":
|
||||
current = 100
|
||||
|
||||
# set percentage
|
||||
percentage = max(0, percentage_delta + current)
|
||||
speed = "off"
|
||||
|
||||
if percentage <= 33:
|
||||
speed = "low"
|
||||
elif percentage <= 66:
|
||||
speed = "medium"
|
||||
elif percentage <= 100:
|
||||
speed = "high"
|
||||
|
||||
data[fan.ATTR_SPEED] = speed
|
||||
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
|
||||
current = entity.attributes.get(cover.ATTR_POSITION)
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
yield from hass.services.async_call(entity.domain, service,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.LockController', 'Lock'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_lock(hass, config, request, entity):
|
||||
"""Process a lock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
# Not supported by Alexa yet
|
||||
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_unlock(hass, config, request, entity):
|
||||
"""Process a unlock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_volume(hass, config, request, entity):
|
||||
"""Process a set volume request."""
|
||||
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_volume(hass, config, request, entity):
|
||||
"""Process a adjust volume request."""
|
||||
volume_delta = int(request[API_PAYLOAD]['volume'])
|
||||
|
||||
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
|
||||
|
||||
# read current state
|
||||
try:
|
||||
current = math.floor(int(current_level * 100))
|
||||
except ZeroDivisionError:
|
||||
current = 0
|
||||
|
||||
volume = float(max(0, volume_delta + current) / 100)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_mute(hass, config, request, entity):
|
||||
"""Process a set mute request."""
|
||||
mute = bool(request[API_PAYLOAD]['mute'])
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_play(hass, config, request, entity):
|
||||
"""Process a play request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_pause(hass, config, request, entity):
|
||||
"""Process a pause request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_stop(hass, config, request, entity):
|
||||
"""Process a stop request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_next(hass, config, request, entity):
|
||||
"""Process a next request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_previous(hass, config, request, entity):
|
||||
"""Process a previous request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(entity.domain,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=True)
|
||||
|
||||
return api_message(request)
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
|
||||
from homeassistant.components.discovery import SERVICE_APPLE_TV
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.5']
|
||||
REQUIREMENTS = ['pyatv==0.3.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -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.7']
|
||||
REQUIREMENTS = ['pyarlo==0.1.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -269,7 +269,8 @@ def setup_device(hass, config, device_config):
|
||||
config)
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
hass.add_job(device.start)
|
||||
if event_types:
|
||||
hass.add_job(device.start)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -30,6 +30,7 @@ DEVICE_CLASSES = [
|
||||
'moving', # On means moving, Off means stopped
|
||||
'occupancy', # On means occupied, Off means not occupied
|
||||
'opening', # Door, window, etc.
|
||||
'plug', # On means plugged in, Off means unplugged
|
||||
'power', # Power, over-current, etc
|
||||
'safety', # Generic on=unsafe, off=safe
|
||||
'smoke', # Smoke detector
|
||||
|
@ -7,25 +7,32 @@ https://home-assistant.io/components/binary_sensor.aurora/
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import USER_AGENT
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor \
|
||||
import (BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_NAME)
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
CONF_THRESHOLD = "forecast_threshold"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \
|
||||
"Administration"
|
||||
CONF_THRESHOLD = 'forecast_threshold'
|
||||
|
||||
DEFAULT_DEVICE_CLASS = 'visible'
|
||||
DEFAULT_NAME = 'Aurora Visibility'
|
||||
DEFAULT_DEVICE_CLASS = "visible"
|
||||
DEFAULT_THRESHOLD = 75
|
||||
|
||||
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
|
||||
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
|
||||
@ -43,10 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
try:
|
||||
aurora_data = AuroraData(
|
||||
hass.config.latitude,
|
||||
hass.config.longitude,
|
||||
threshold
|
||||
)
|
||||
hass.config.latitude, hass.config.longitude, threshold)
|
||||
aurora_data.update()
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error(
|
||||
@ -85,9 +89,9 @@ class AuroraSensor(BinarySensorDevice):
|
||||
attrs = {}
|
||||
|
||||
if self.aurora_data:
|
||||
attrs["visibility_level"] = self.aurora_data.visibility_level
|
||||
attrs["message"] = self.aurora_data.is_visible_text
|
||||
|
||||
attrs['visibility_level'] = self.aurora_data.visibility_level
|
||||
attrs['message'] = self.aurora_data.is_visible_text
|
||||
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
@ -104,10 +108,7 @@ class AuroraData(object):
|
||||
self.longitude = longitude
|
||||
self.number_of_latitude_intervals = 513
|
||||
self.number_of_longitude_intervals = 1024
|
||||
self.api_url = \
|
||||
"http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||
self.headers = {"User-Agent": "Home Assistant Aurora Tracker v.0.1.0"}
|
||||
|
||||
self.headers = {USER_AGENT: HA_USER_AGENT}
|
||||
self.threshold = int(threshold)
|
||||
self.is_visible = None
|
||||
self.is_visible_text = None
|
||||
@ -132,14 +133,14 @@ class AuroraData(object):
|
||||
|
||||
def get_aurora_forecast(self):
|
||||
"""Get forecast data and parse for given long/lat."""
|
||||
raw_data = requests.get(self.api_url, headers=self.headers).text
|
||||
raw_data = requests.get(URL, headers=self.headers, timeout=5).text
|
||||
forecast_table = [
|
||||
row.strip(" ").split(" ")
|
||||
for row in raw_data.split("\n")
|
||||
if not row.startswith("#")
|
||||
]
|
||||
|
||||
# convert lat and long for data points in table
|
||||
# Convert lat and long for data points in table
|
||||
converted_latitude = round((self.latitude / 180)
|
||||
* self.number_of_latitude_intervals)
|
||||
converted_longitude = round((self.longitude / 360)
|
||||
|
@ -67,7 +67,7 @@ class SpcBinarySensor(BinarySensorDevice):
|
||||
spc_registry.register_sensor_device(zone_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state):
|
||||
def async_update_from_spc(self, state, extra):
|
||||
"""Update the state of the device."""
|
||||
self._state = state
|
||||
yield from self.async_update_ha_state()
|
||||
|
103
homeassistant/components/binary_sensor/vultr.py
Normal file
103
homeassistant/components/binary_sensor/vultr.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""
|
||||
Support for monitoring the state of Vultr subscriptions (VPS).
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.vultr/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.vultr import (
|
||||
CONF_SUBSCRIPTION, ATTR_AUTO_BACKUPS, ATTR_ALLOWED_BANDWIDTH,
|
||||
ATTR_CREATED_AT, ATTR_SUBSCRIPTION_ID, ATTR_SUBSCRIPTION_NAME,
|
||||
ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY, ATTR_DISK,
|
||||
ATTR_COST_PER_MONTH, ATTR_OS, ATTR_REGION, ATTR_VCPUS, DATA_VULTR)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DEVICE_CLASS = 'power'
|
||||
DEFAULT_NAME = 'Vultr {}'
|
||||
DEPENDENCIES = ['vultr']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SUBSCRIPTION): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Vultr subscription (server) sensor."""
|
||||
vultr = hass.data[DATA_VULTR]
|
||||
|
||||
subscription = config.get(CONF_SUBSCRIPTION)
|
||||
name = config.get(CONF_NAME)
|
||||
|
||||
if subscription not in vultr.data:
|
||||
_LOGGER.error("Subscription %s not found", subscription)
|
||||
return False
|
||||
|
||||
add_devices([VultrBinarySensor(vultr, subscription, name)], True)
|
||||
|
||||
|
||||
class VultrBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Vultr subscription sensor."""
|
||||
|
||||
def __init__(self, vultr, subscription, name):
|
||||
"""Initialize a new Vultr sensor."""
|
||||
self._vultr = vultr
|
||||
self._name = name
|
||||
|
||||
self.subscription = subscription
|
||||
self.data = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
try:
|
||||
return self._name.format(self.data['label'])
|
||||
except (KeyError, TypeError):
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon of this server."""
|
||||
return 'mdi:server' if self.is_on else 'mdi:server-off'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.data['power_status'] == 'running'
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return DEFAULT_DEVICE_CLASS
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the Vultr subscription."""
|
||||
return {
|
||||
ATTR_ALLOWED_BANDWIDTH: self.data.get('allowed_bandwidth_gb'),
|
||||
ATTR_AUTO_BACKUPS: self.data.get('auto_backups'),
|
||||
ATTR_COST_PER_MONTH: self.data.get('cost_per_month'),
|
||||
ATTR_CREATED_AT: self.data.get('date_created'),
|
||||
ATTR_DISK: self.data.get('disk'),
|
||||
ATTR_IPV4_ADDRESS: self.data.get('main_ip'),
|
||||
ATTR_IPV6_ADDRESS: self.data.get('v6_main_ip'),
|
||||
ATTR_MEMORY: self.data.get('ram'),
|
||||
ATTR_OS: self.data.get('os'),
|
||||
ATTR_REGION: self.data.get('location'),
|
||||
ATTR_SUBSCRIPTION_ID: self.data.get('SUBID'),
|
||||
ATTR_SUBSCRIPTION_NAME: self.data.get('label'),
|
||||
ATTR_VCPUS: self.data.get('vcpu_count')
|
||||
}
|
||||
|
||||
def update(self):
|
||||
"""Update state of sensor."""
|
||||
self._vultr.update()
|
||||
self.data = self._vultr.data[self.subscription]
|
@ -4,16 +4,17 @@ Support for BloomSky weather station.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/bloomsky/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -68,7 +69,7 @@ class BloomSky(object):
|
||||
"""Use the API to retrieve a list of devices."""
|
||||
_LOGGER.debug("Fetching BloomSky update")
|
||||
response = requests.get(
|
||||
self.API_URL, headers={"Authorization": self._api_key}, timeout=10)
|
||||
self.API_URL, headers={AUTHORIZATION: self._api_key}, timeout=10)
|
||||
if response.status_code == 401:
|
||||
raise RuntimeError("Invalid API_KEY")
|
||||
elif response.status_code != 200:
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
SCAN_INTERVAL = timedelta(seconds=90)
|
||||
|
||||
ARLO_MODE_ARMED = 'armed'
|
||||
ARLO_MODE_DISARMED = 'disarmed'
|
||||
@ -31,6 +31,7 @@ ATTR_MOTION = 'motion_detection_sensitivity'
|
||||
ATTR_POWERSAVE = 'power_save_mode'
|
||||
ATTR_SIGNAL_STRENGTH = 'signal_strength'
|
||||
ATTR_UNSEEN_VIDEOS = 'unseen_videos'
|
||||
ATTR_LAST_REFRESH = 'last_refresh'
|
||||
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
|
||||
@ -73,6 +74,8 @@ class ArloCam(Camera):
|
||||
self._motion_status = False
|
||||
self._ffmpeg = hass.data[DATA_FFMPEG]
|
||||
self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS)
|
||||
self._last_refresh = None
|
||||
self._camera.base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
|
||||
self.attrs = {}
|
||||
|
||||
def camera_image(self):
|
||||
@ -105,14 +108,17 @@ class ArloCam(Camera):
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
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),
|
||||
name: value for name, value in (
|
||||
(ATTR_BATTERY_LEVEL, self._camera.battery_level),
|
||||
(ATTR_BRIGHTNESS, self._camera.brightness),
|
||||
(ATTR_FLIPPED, self._camera.flip_state),
|
||||
(ATTR_MIRRORED, self._camera.mirror_state),
|
||||
(ATTR_MOTION, self._camera.motion_detection_sensitivity),
|
||||
(ATTR_POWERSAVE, POWERSAVE_MODE_MAPPING.get(
|
||||
self._camera.powersave_mode)),
|
||||
(ATTR_SIGNAL_STRENGTH, self._camera.signal_strength),
|
||||
(ATTR_UNSEEN_VIDEOS, self._camera.unseen_videos),
|
||||
) if value is not None
|
||||
}
|
||||
|
||||
@property
|
||||
@ -160,13 +166,4 @@ class ArloCam(Camera):
|
||||
|
||||
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
|
||||
self._camera.update()
|
||||
|
@ -9,12 +9,12 @@ from datetime import timedelta
|
||||
import logging
|
||||
import os
|
||||
import functools as ft
|
||||
from numbers import Number
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@ -22,7 +22,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN,
|
||||
TEMP_CELSIUS)
|
||||
TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS)
|
||||
|
||||
DOMAIN = 'climate'
|
||||
|
||||
@ -71,11 +71,6 @@ ATTR_OPERATION_LIST = 'operation_list'
|
||||
ATTR_SWING_MODE = 'swing_mode'
|
||||
ATTR_SWING_LIST = 'swing_list'
|
||||
|
||||
# The degree of precision for each platform
|
||||
PRECISION_WHOLE = 1
|
||||
PRECISION_HALVES = 0.5
|
||||
PRECISION_TENTHS = 0.1
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [
|
||||
ATTR_TEMPERATURE,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
@ -456,12 +451,18 @@ class ClimateDevice(Entity):
|
||||
def state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {
|
||||
ATTR_CURRENT_TEMPERATURE:
|
||||
self._convert_for_display(self.current_temperature),
|
||||
ATTR_MIN_TEMP: self._convert_for_display(self.min_temp),
|
||||
ATTR_MAX_TEMP: self._convert_for_display(self.max_temp),
|
||||
ATTR_TEMPERATURE:
|
||||
self._convert_for_display(self.target_temperature),
|
||||
ATTR_CURRENT_TEMPERATURE: show_temp(
|
||||
self.hass, self.current_temperature, self.temperature_unit,
|
||||
self.precision),
|
||||
ATTR_MIN_TEMP: show_temp(
|
||||
self.hass, self.min_temp, self.temperature_unit,
|
||||
self.precision),
|
||||
ATTR_MAX_TEMP: show_temp(
|
||||
self.hass, self.max_temp, self.temperature_unit,
|
||||
self.precision),
|
||||
ATTR_TEMPERATURE: show_temp(
|
||||
self.hass, self.target_temperature, self.temperature_unit,
|
||||
self.precision),
|
||||
}
|
||||
|
||||
if self.target_temperature_step is not None:
|
||||
@ -469,10 +470,12 @@ class ClimateDevice(Entity):
|
||||
|
||||
target_temp_high = self.target_temperature_high
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
|
||||
self.hass, self.target_temperature_high, self.temperature_unit,
|
||||
self.precision)
|
||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
self.precision)
|
||||
|
||||
humidity = self.target_humidity
|
||||
if humidity is not None:
|
||||
@ -733,24 +736,3 @@ class ClimateDevice(Entity):
|
||||
def max_humidity(self):
|
||||
"""Return the maximum humidity."""
|
||||
return 99
|
||||
|
||||
def _convert_for_display(self, temp):
|
||||
"""Convert temperature into preferred units for display purposes."""
|
||||
if temp is None:
|
||||
return temp
|
||||
|
||||
# if the temperature is not a number this can cause issues
|
||||
# with polymer components, so bail early there.
|
||||
if not isinstance(temp, Number):
|
||||
raise TypeError("Temperature is not a number: %s" % temp)
|
||||
|
||||
if self.temperature_unit != self.unit_of_measurement:
|
||||
temp = convert_temperature(
|
||||
temp, self.temperature_unit, self.unit_of_measurement)
|
||||
# Round in the units appropriate
|
||||
if self.precision == PRECISION_HALVES:
|
||||
return round(temp * 2) / 2.0
|
||||
elif self.precision == PRECISION_TENTHS:
|
||||
return round(temp, 1)
|
||||
# PRECISION_WHOLE as a fall back
|
||||
return round(temp)
|
||||
|
@ -9,12 +9,9 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ClimateDevice, PLATFORM_SCHEMA, PRECISION_HALVES,
|
||||
STATE_AUTO, STATE_ON, STATE_OFF,
|
||||
)
|
||||
STATE_ON, STATE_OFF, STATE_AUTO, PLATFORM_SCHEMA, ClimateDevice)
|
||||
from homeassistant.const import (
|
||||
CONF_MAC, TEMP_CELSIUS, CONF_DEVICES, ATTR_TEMPERATURE)
|
||||
|
||||
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-eq3bt==0.1.6']
|
||||
@ -58,15 +55,17 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
|
||||
def __init__(self, _mac, _name):
|
||||
"""Initialize the thermostat."""
|
||||
# we want to avoid name clash with this module..
|
||||
# We want to avoid name clash with this module.
|
||||
import eq3bt as eq3
|
||||
|
||||
self.modes = {eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST,
|
||||
eq3.Mode.Away: STATE_AWAY}
|
||||
self.modes = {
|
||||
eq3.Mode.Open: STATE_ON,
|
||||
eq3.Mode.Closed: STATE_OFF,
|
||||
eq3.Mode.Auto: STATE_AUTO,
|
||||
eq3.Mode.Manual: STATE_MANUAL,
|
||||
eq3.Mode.Boost: STATE_BOOST,
|
||||
eq3.Mode.Away: STATE_AWAY,
|
||||
}
|
||||
|
||||
self.reverse_modes = {v: k for k, v in self.modes.items()}
|
||||
|
||||
@ -153,11 +152,11 @@ class EQ3BTSmartThermostat(ClimateDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the device specific state attributes."""
|
||||
dev_specific = {
|
||||
ATTR_STATE_AWAY_END: self._thermostat.away_end,
|
||||
ATTR_STATE_LOCKED: self._thermostat.locked,
|
||||
ATTR_STATE_LOW_BAT: self._thermostat.low_battery,
|
||||
ATTR_STATE_VALVE: self._thermostat.valve_state,
|
||||
ATTR_STATE_WINDOW_OPEN: self._thermostat.window_open,
|
||||
ATTR_STATE_AWAY_END: self._thermostat.away_end,
|
||||
}
|
||||
|
||||
return dev_specific
|
||||
|
@ -163,6 +163,7 @@ class GenericThermostat(ClimateDevice):
|
||||
"""Set operation mode."""
|
||||
if operation_mode == STATE_AUTO:
|
||||
self._enabled = True
|
||||
self._async_control_heating()
|
||||
elif operation_mode == STATE_OFF:
|
||||
self._enabled = False
|
||||
if self._is_device_active:
|
||||
|
@ -7,7 +7,6 @@ https://home-assistant.io/components/climate.homematic/
|
||||
import logging
|
||||
from homeassistant.components.climate import ClimateDevice, STATE_AUTO
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.util.temperature import convert
|
||||
from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN, ATTR_TEMPERATURE
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
@ -121,12 +120,12 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||
@property
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature - 4.5 means off."""
|
||||
return convert(4.5, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return 4.5
|
||||
|
||||
@property
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature - 30.5 means on."""
|
||||
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
|
||||
return 30.5
|
||||
|
||||
def _init_data_struct(self):
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
|
@ -13,9 +13,11 @@ from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
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_SETPOINT_SHIFT_STEP = 'setpoint_shift_step'
|
||||
CONF_SETPOINT_SHIFT_MAX = 'setpoint_shift_max'
|
||||
CONF_SETPOINT_SHIFT_MIN = 'setpoint_shift_min'
|
||||
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
|
||||
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
|
||||
@ -28,15 +30,24 @@ CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
|
||||
|
||||
DEFAULT_NAME = 'KNX Climate'
|
||||
DEFAULT_SETPOINT_SHIFT_STEP = 0.5
|
||||
DEFAULT_SETPOINT_SHIFT_MAX = 6
|
||||
DEFAULT_SETPOINT_SHIFT_MIN = -6
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
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_SETPOINT_SHIFT_STEP,
|
||||
default=DEFAULT_SETPOINT_SHIFT_STEP): vol.All(
|
||||
float, vol.Range(min=0, max=2)),
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX):
|
||||
vol.All(int, vol.Range(min=-32, max=0)),
|
||||
vol.Optional(CONF_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN):
|
||||
vol.All(int, vol.Range(min=0, max=32)),
|
||||
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,
|
||||
@ -77,6 +88,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
def async_add_devices_config(hass, config, async_add_devices):
|
||||
"""Set up climate for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
|
||||
climate = xknx.devices.Climate(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
@ -84,12 +96,16 @@ def async_add_devices_config(hass, config, async_add_devices):
|
||||
CONF_TEMPERATURE_ADDRESS),
|
||||
group_address_target_temperature=config.get(
|
||||
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),
|
||||
setpoint_shift_step=config.get(
|
||||
CONF_SETPOINT_SHIFT_STEP),
|
||||
setpoint_shift_max=config.get(
|
||||
CONF_SETPOINT_SHIFT_MAX),
|
||||
setpoint_shift_min=config.get(
|
||||
CONF_SETPOINT_SHIFT_MIN),
|
||||
group_address_operation_mode=config.get(
|
||||
CONF_OPERATION_MODE_ADDRESS),
|
||||
group_address_operation_mode_state=config.get(
|
||||
@ -118,8 +134,6 @@ class KNXClimate(ClimateDevice):
|
||||
self.async_register_callbacks()
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._away = False # not yet supported
|
||||
self._is_fan_on = False # not yet supported
|
||||
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@ -150,28 +164,25 @@ class KNXClimate(ClimateDevice):
|
||||
"""Return the current temperature."""
|
||||
return self.device.temperature.value
|
||||
|
||||
@property
|
||||
def target_temperature_step(self):
|
||||
"""Return the supported step of target temperature."""
|
||||
return self.device.setpoint_shift_step
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self.device.target_temperature_comfort
|
||||
return self.device.target_temperature.value
|
||||
|
||||
@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
|
||||
def min_temp(self):
|
||||
"""Return the minimum temperature."""
|
||||
return self.device.target_temperature_min
|
||||
|
||||
@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
|
||||
def max_temp(self):
|
||||
"""Return the maximum temperature."""
|
||||
return self.device.target_temperature_max
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
@ -179,7 +190,7 @@ class KNXClimate(ClimateDevice):
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
yield from self.device.set_target_temperature_comfort(temperature)
|
||||
yield from self.device.set_target_temperature(temperature)
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
|
@ -4,46 +4,51 @@ Support for Wink thermostats, Air Conditioners, and Water Heaters.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.wink/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.wink import WinkDevice, DOMAIN
|
||||
from homeassistant.components.climate import (
|
||||
STATE_AUTO, STATE_COOL, STATE_HEAT, ClimateDevice,
|
||||
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE, STATE_FAN_ONLY,
|
||||
ATTR_CURRENT_HUMIDITY, STATE_ECO, STATE_ELECTRIC,
|
||||
STATE_PERFORMANCE, STATE_HIGH_DEMAND,
|
||||
STATE_HEAT_PUMP, STATE_GAS)
|
||||
STATE_ECO, STATE_GAS, STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ELECTRIC,
|
||||
STATE_FAN_ONLY, STATE_HEAT_PUMP, ATTR_TEMPERATURE, STATE_HIGH_DEMAND,
|
||||
STATE_PERFORMANCE, ATTR_TARGET_TEMP_LOW, ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_TARGET_TEMP_HIGH, ClimateDevice)
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
from homeassistant.const import (
|
||||
TEMP_CELSIUS, STATE_ON,
|
||||
STATE_OFF, STATE_UNKNOWN)
|
||||
STATE_ON, STATE_OFF, TEMP_CELSIUS, STATE_UNKNOWN, PRECISION_TENTHS)
|
||||
from homeassistant.helpers.temperature import display_temp as show_temp
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ECO_TARGET = 'eco_target'
|
||||
ATTR_EXTERNAL_TEMPERATURE = 'external_temperature'
|
||||
ATTR_OCCUPIED = 'occupied'
|
||||
ATTR_RHEEM_TYPE = 'rheem_type'
|
||||
ATTR_SCHEDULE_ENABLED = 'schedule_enabled'
|
||||
ATTR_SMART_TEMPERATURE = 'smart_temperature'
|
||||
ATTR_TOTAL_CONSUMPTION = 'total_consumption'
|
||||
ATTR_VACATION_MODE = 'vacation_mode'
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
|
||||
SPEED_LOW = 'low'
|
||||
SPEED_MEDIUM = 'medium'
|
||||
SPEED_HIGH = 'high'
|
||||
|
||||
HA_STATE_TO_WINK = {STATE_AUTO: 'auto',
|
||||
STATE_ECO: 'eco',
|
||||
STATE_FAN_ONLY: 'fan_only',
|
||||
STATE_HEAT: 'heat_only',
|
||||
STATE_COOL: 'cool_only',
|
||||
STATE_PERFORMANCE: 'performance',
|
||||
STATE_HIGH_DEMAND: 'high_demand',
|
||||
STATE_HEAT_PUMP: 'heat_pump',
|
||||
STATE_ELECTRIC: 'electric_only',
|
||||
STATE_GAS: 'gas',
|
||||
STATE_OFF: 'off'}
|
||||
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
|
||||
HA_STATE_TO_WINK = {
|
||||
STATE_AUTO: 'auto',
|
||||
STATE_COOL: 'cool_only',
|
||||
STATE_ECO: 'eco',
|
||||
STATE_ELECTRIC: 'electric_only',
|
||||
STATE_FAN_ONLY: 'fan_only',
|
||||
STATE_GAS: 'gas',
|
||||
STATE_HEAT: 'heat_only',
|
||||
STATE_HEAT_PUMP: 'heat_pump',
|
||||
STATE_HIGH_DEMAND: 'high_demand',
|
||||
STATE_OFF: 'off',
|
||||
STATE_PERFORMANCE: 'performance',
|
||||
}
|
||||
|
||||
ATTR_EXTERNAL_TEMPERATURE = "external_temperature"
|
||||
ATTR_SMART_TEMPERATURE = "smart_temperature"
|
||||
ATTR_ECO_TARGET = "eco_target"
|
||||
ATTR_OCCUPIED = "occupied"
|
||||
WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@ -85,15 +90,18 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
|
||||
self.hass, self.target_temperature_high, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
|
||||
if self.external_temperature:
|
||||
data[ATTR_EXTERNAL_TEMPERATURE] = self._convert_for_display(
|
||||
self.external_temperature)
|
||||
data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
|
||||
self.hass, self.external_temperature, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
|
||||
if self.smart_temperature:
|
||||
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
|
||||
@ -139,7 +147,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
|
||||
@property
|
||||
def eco_target(self):
|
||||
"""Return status of eco target (Is the termostat in eco mode)."""
|
||||
"""Return status of eco target (Is the thermostat in eco mode)."""
|
||||
return self.wink.eco_target()
|
||||
|
||||
@property
|
||||
@ -249,7 +257,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
@ -297,7 +305,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
minimum = 7 # Default minimum
|
||||
min_min = self.wink.min_min_set_point()
|
||||
min_max = self.wink.min_max_set_point()
|
||||
return_value = minimum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if min_min:
|
||||
return_value = min_min
|
||||
@ -323,7 +330,6 @@ class WinkThermostat(WinkDevice, ClimateDevice):
|
||||
maximum = 35 # Default maximum
|
||||
max_min = self.wink.max_min_set_point()
|
||||
max_max = self.wink.max_max_set_point()
|
||||
return_value = maximum
|
||||
if self.current_operation == STATE_HEAT:
|
||||
if max_min:
|
||||
return_value = max_min
|
||||
@ -360,13 +366,15 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
target_temp_high = self.target_temperature_high
|
||||
target_temp_low = self.target_temperature_low
|
||||
if target_temp_high is not None:
|
||||
data[ATTR_TARGET_TEMP_HIGH] = self._convert_for_display(
|
||||
self.target_temperature_high)
|
||||
data[ATTR_TARGET_TEMP_HIGH] = show_temp(
|
||||
self.hass, self.target_temperature_high, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
if target_temp_low is not None:
|
||||
data[ATTR_TARGET_TEMP_LOW] = self._convert_for_display(
|
||||
self.target_temperature_low)
|
||||
data["total_consumption"] = self.wink.total_consumption()
|
||||
data["schedule_enabled"] = self.wink.schedule_enabled()
|
||||
data[ATTR_TARGET_TEMP_LOW] = show_temp(
|
||||
self.hass, self.target_temperature_low, self.temperature_unit,
|
||||
PRECISION_TENTHS)
|
||||
data[ATTR_TOTAL_CONSUMPTION] = self.wink.total_consumption()
|
||||
data[ATTR_SCHEDULE_ENABLED] = self.wink.schedule_enabled()
|
||||
|
||||
return data
|
||||
|
||||
@ -377,11 +385,14 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
"""Return current operation ie. auto_eco, cool_only, fan_only."""
|
||||
if not self.wink.is_on():
|
||||
current_op = STATE_OFF
|
||||
else:
|
||||
current_op = WINK_STATE_TO_HA.get(self.wink.current_hvac_mode())
|
||||
wink_mode = self.wink.current_mode()
|
||||
if wink_mode == "auto_eco":
|
||||
wink_mode = "eco"
|
||||
current_op = WINK_STATE_TO_HA.get(wink_mode)
|
||||
if current_op is None:
|
||||
current_op = STATE_UNKNOWN
|
||||
return current_op
|
||||
@ -392,11 +403,13 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
op_list = ['off']
|
||||
modes = self.wink.modes()
|
||||
for mode in modes:
|
||||
if mode == "auto_eco":
|
||||
mode = "eco"
|
||||
ha_mode = WINK_STATE_TO_HA.get(mode)
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
@ -420,15 +433,19 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
|
||||
@property
|
||||
def current_fan_mode(self):
|
||||
"""Return the current fan mode."""
|
||||
"""
|
||||
Return the current fan mode.
|
||||
|
||||
The official Wink app only supports 3 modes [low, medium, high]
|
||||
which are equal to [0.33, 0.66, 1.0] respectively.
|
||||
"""
|
||||
speed = self.wink.current_fan_speed()
|
||||
if speed <= 0.4 and speed > 0.3:
|
||||
if speed <= 0.33:
|
||||
return SPEED_LOW
|
||||
elif speed <= 0.8 and speed > 0.5:
|
||||
elif speed <= 0.66:
|
||||
return SPEED_MEDIUM
|
||||
elif speed <= 1.0 and speed > 0.8:
|
||||
else:
|
||||
return SPEED_HIGH
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def fan_list(self):
|
||||
@ -436,11 +453,16 @@ class WinkAC(WinkDevice, ClimateDevice):
|
||||
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
|
||||
|
||||
def set_fan_mode(self, fan):
|
||||
"""Set fan speed."""
|
||||
"""
|
||||
Set fan speed.
|
||||
|
||||
The official Wink app only supports 3 modes [low, medium, high]
|
||||
which are equal to [0.33, 0.66, 1.0] respectively.
|
||||
"""
|
||||
if fan == SPEED_LOW:
|
||||
speed = 0.4
|
||||
speed = 0.33
|
||||
elif fan == SPEED_MEDIUM:
|
||||
speed = 0.8
|
||||
speed = 0.66
|
||||
elif fan == SPEED_HIGH:
|
||||
speed = 1.0
|
||||
self.wink.set_ac_fan_speed(speed)
|
||||
@ -459,8 +481,8 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
|
||||
def device_state_attributes(self):
|
||||
"""Return the optional state attributes."""
|
||||
data = {}
|
||||
data["vacation_mode"] = self.wink.vacation_mode_enabled()
|
||||
data["rheem_type"] = self.wink.rheem_type()
|
||||
data[ATTR_VACATION_MODE] = self.wink.vacation_mode_enabled()
|
||||
data[ATTR_RHEEM_TYPE] = self.wink.rheem_type()
|
||||
|
||||
return data
|
||||
|
||||
@ -492,7 +514,7 @@ class WinkWaterHeater(WinkDevice, ClimateDevice):
|
||||
if ha_mode is not None:
|
||||
op_list.append(ha_mode)
|
||||
else:
|
||||
error = "Invaid operation mode mapping. " + mode + \
|
||||
error = "Invalid operation mode mapping. " + mode + \
|
||||
" doesn't map. Please report this."
|
||||
_LOGGER.error(error)
|
||||
return op_list
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Component to integrate the Home Assistant cloud."""
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@ -8,6 +9,9 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE)
|
||||
from homeassistant.helpers import entityfilter
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.alexa import smart_home
|
||||
|
||||
from . import http_api, iot
|
||||
from .const import CONFIG_DIR, DOMAIN, SERVERS
|
||||
@ -16,6 +20,8 @@ REQUIREMENTS = ['warrant==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ALEXA = 'alexa'
|
||||
CONF_ALEXA_FILTER = 'filter'
|
||||
CONF_COGNITO_CLIENT_ID = 'cognito_client_id'
|
||||
CONF_RELAYER = 'relayer'
|
||||
CONF_USER_POOL_ID = 'user_pool_id'
|
||||
@ -24,6 +30,13 @@ MODE_DEV = 'development'
|
||||
DEFAULT_MODE = MODE_DEV
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
ALEXA_SCHEMA = vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_ALEXA_FILTER,
|
||||
default=lambda: entityfilter.generate_filter([], [], [], [])
|
||||
): entityfilter.FILTER_SCHEMA,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
||||
@ -33,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USER_POOL_ID): str,
|
||||
vol.Required(CONF_REGION): str,
|
||||
vol.Required(CONF_RELAYER): str,
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -45,6 +59,10 @@ def async_setup(hass, config):
|
||||
else:
|
||||
kwargs = {CONF_MODE: DEFAULT_MODE}
|
||||
|
||||
if CONF_ALEXA not in kwargs:
|
||||
kwargs[CONF_ALEXA] = ALEXA_SCHEMA({})
|
||||
|
||||
kwargs[CONF_ALEXA] = smart_home.Config(**kwargs[CONF_ALEXA])
|
||||
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -62,11 +80,11 @@ 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):
|
||||
region=None, relayer=None, alexa=None):
|
||||
"""Create an instance of Cloud."""
|
||||
self.hass = hass
|
||||
self.mode = mode
|
||||
self.email = None
|
||||
self.alexa_config = alexa
|
||||
self.id_token = None
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
@ -89,7 +107,29 @@ class Cloud:
|
||||
@property
|
||||
def is_logged_in(self):
|
||||
"""Get if cloud is logged in."""
|
||||
return self.email is not None
|
||||
return self.id_token is not None
|
||||
|
||||
@property
|
||||
def subscription_expired(self):
|
||||
"""Return a boolen if the subscription has expired."""
|
||||
# For now, don't enforce subscriptions to exist
|
||||
if 'custom:sub-exp' not in self.claims:
|
||||
return False
|
||||
|
||||
return dt_util.utcnow() > self.expiration_date
|
||||
|
||||
@property
|
||||
def expiration_date(self):
|
||||
"""Return the subscription expiration as a UTC datetime object."""
|
||||
return datetime.combine(
|
||||
dt_util.parse_date(self.claims['custom:sub-exp']),
|
||||
datetime.min.time()).replace(tzinfo=dt_util.UTC)
|
||||
|
||||
@property
|
||||
def claims(self):
|
||||
"""Get the claims from the id token."""
|
||||
from jose import jwt
|
||||
return jwt.get_unverified_claims(self.id_token)
|
||||
|
||||
@property
|
||||
def user_info_path(self):
|
||||
@ -110,18 +150,20 @@ class Cloud:
|
||||
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:
|
||||
if self.id_token is not None:
|
||||
yield from self.iot.connect()
|
||||
|
||||
def path(self, *parts):
|
||||
"""Get config path inside cloud dir."""
|
||||
"""Get config path inside cloud dir.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return self.hass.config.path(CONFIG_DIR, *parts)
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -129,7 +171,6 @@ class Cloud:
|
||||
"""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
|
||||
@ -141,7 +182,6 @@ class Cloud:
|
||||
"""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,
|
||||
|
@ -113,7 +113,6 @@ def login(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()
|
||||
|
||||
|
||||
|
@ -12,3 +12,8 @@ SERVERS = {
|
||||
# 'relayer': ''
|
||||
# }
|
||||
}
|
||||
|
||||
MESSAGE_EXPIRATION = """
|
||||
It looks like your Home Assistant Cloud subscription has expired. Please check
|
||||
your [account page](/config/cloud/account) to continue using the service.
|
||||
"""
|
||||
|
@ -79,8 +79,10 @@ class CloudLoginView(HomeAssistantView):
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from hass.async_add_job(auth_api.login, cloud, data['email'],
|
||||
data['password'])
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
|
||||
hass.async_add_job(cloud.iot.connect)
|
||||
# Allow cloud to start connecting.
|
||||
yield from asyncio.sleep(0, loop=hass.loop)
|
||||
return self.json(_account_data(cloud))
|
||||
|
||||
|
||||
@ -222,6 +224,10 @@ class CloudConfirmForgotPasswordView(HomeAssistantView):
|
||||
|
||||
def _account_data(cloud):
|
||||
"""Generate the auth data JSON response."""
|
||||
claims = cloud.claims
|
||||
|
||||
return {
|
||||
'email': cloud.email
|
||||
'email': claims['email'],
|
||||
'sub_exp': claims.get('custom:sub-exp'),
|
||||
'cloud': cloud.iot.state,
|
||||
}
|
||||
|
@ -9,11 +9,16 @@ 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
|
||||
from .const import MESSAGE_EXPIRATION
|
||||
|
||||
|
||||
HANDLERS = Registry()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_CONNECTING = 'connecting'
|
||||
STATE_CONNECTED = 'connected'
|
||||
STATE_DISCONNECTED = 'disconnected'
|
||||
|
||||
|
||||
class UnknownHandler(Exception):
|
||||
"""Exception raised when trying to handle unknown handler."""
|
||||
@ -25,27 +30,41 @@ class CloudIoT:
|
||||
def __init__(self, cloud):
|
||||
"""Initialize the CloudIoT class."""
|
||||
self.cloud = cloud
|
||||
# The WebSocket client
|
||||
self.client = None
|
||||
# Scheduled sleep task till next connection retry
|
||||
self.retry_task = None
|
||||
# Boolean to indicate if we wanted the connection to close
|
||||
self.close_requested = False
|
||||
# The current number of attempts to connect, impacts wait time
|
||||
self.tries = 0
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
"""Return if connected to the cloud."""
|
||||
return self.client is not None
|
||||
# Current state of the connection
|
||||
self.state = STATE_DISCONNECTED
|
||||
|
||||
@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
|
||||
if self.cloud.subscription_expired:
|
||||
# Try refreshing the token to see if it is still expired.
|
||||
yield from hass.async_add_job(auth_api.check_token, self.cloud)
|
||||
|
||||
if self.cloud.subscription_expired:
|
||||
hass.components.persistent_notification.async_create(
|
||||
MESSAGE_EXPIRATION, 'Subscription expired',
|
||||
'cloud_subscription_expired')
|
||||
self.state = STATE_DISCONNECTED
|
||||
return
|
||||
|
||||
if self.state == STATE_CONNECTED:
|
||||
raise RuntimeError('Already connected')
|
||||
|
||||
self.state = STATE_CONNECTING
|
||||
self.close_requested = False
|
||||
remove_hass_stop_listener = None
|
||||
session = async_get_clientsession(self.cloud.hass)
|
||||
client = None
|
||||
disconnect_warn = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def _handle_hass_stop(event):
|
||||
@ -54,8 +73,6 @@ class CloudIoT:
|
||||
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)
|
||||
|
||||
@ -70,13 +87,14 @@ class CloudIoT:
|
||||
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
|
||||
|
||||
_LOGGER.info('Connected')
|
||||
self.state = STATE_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'
|
||||
disconnect_warn = 'Connection cancelled.'
|
||||
break
|
||||
|
||||
elif msg.type != WSMsgType.TEXT:
|
||||
@ -144,20 +162,33 @@ class CloudIoT:
|
||||
self.client = None
|
||||
yield from client.close()
|
||||
|
||||
if not self.close_requested:
|
||||
if self.close_requested:
|
||||
self.state = STATE_DISCONNECTED
|
||||
|
||||
else:
|
||||
self.state = STATE_CONNECTING
|
||||
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())
|
||||
try:
|
||||
# Sleep 0, 5, 10, 15 … up to 30 seconds between retries
|
||||
self.retry_task = hass.async_add_job(asyncio.sleep(
|
||||
min(30, (self.tries - 1) * 5), loop=hass.loop))
|
||||
yield from self.retry_task
|
||||
self.retry_task = None
|
||||
hass.async_add_job(self.connect())
|
||||
except asyncio.CancelledError:
|
||||
# Happens if disconnect called
|
||||
pass
|
||||
|
||||
@asyncio.coroutine
|
||||
def disconnect(self):
|
||||
"""Disconnect the client."""
|
||||
self.close_requested = True
|
||||
yield from self.client.close()
|
||||
|
||||
if self.client is not None:
|
||||
yield from self.client.close()
|
||||
elif self.retry_task is not None:
|
||||
self.retry_task.cancel()
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -175,7 +206,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
|
||||
@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))
|
||||
return (yield from smart_home.async_handle_message(hass,
|
||||
cloud.alexa_config,
|
||||
payload))
|
||||
|
||||
|
||||
@HANDLERS.register('cloud')
|
||||
|
@ -1,17 +1,19 @@
|
||||
"""Provide configuration end points for Z-Wave."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from collections import deque
|
||||
from aiohttp.web import Response
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import HTTP_NOT_FOUND
|
||||
from homeassistant.const import HTTP_NOT_FOUND, HTTP_OK
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.config import EditKeyBasedConfigView
|
||||
from homeassistant.components.zwave import const, DEVICE_CONFIG_SCHEMA_ENTRY
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONFIG_PATH = 'zwave_device_config.yaml'
|
||||
OZW_LOG_FILENAME = 'OZW_Log.txt'
|
||||
URL_API_OZW_LOG = '/api/zwave/ozwlog'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -25,12 +27,64 @@ def async_setup(hass):
|
||||
hass.http.register_view(ZWaveNodeGroupView)
|
||||
hass.http.register_view(ZWaveNodeConfigView)
|
||||
hass.http.register_view(ZWaveUserCodeView)
|
||||
hass.http.register_static_path(
|
||||
URL_API_OZW_LOG, hass.config.path(OZW_LOG_FILENAME), False)
|
||||
hass.http.register_view(ZWaveLogView)
|
||||
hass.http.register_view(ZWaveConfigWriteView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ZWaveLogView(HomeAssistantView):
|
||||
"""View to read the ZWave log file."""
|
||||
|
||||
url = "/api/zwave/ozwlog"
|
||||
name = "api:zwave:ozwlog"
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Retrieve the lines from ZWave log."""
|
||||
try:
|
||||
lines = int(request.query.get('lines', 0))
|
||||
except ValueError:
|
||||
return Response(text='Invalid datetime', status=400)
|
||||
|
||||
hass = request.app['hass']
|
||||
response = yield from hass.async_add_job(self._get_log, hass, lines)
|
||||
|
||||
return Response(text='\n'.join(response))
|
||||
|
||||
def _get_log(self, hass, lines):
|
||||
"""Retrieve the logfile content."""
|
||||
logfilepath = hass.config.path(OZW_LOG_FILENAME)
|
||||
with open(logfilepath, 'r') as logfile:
|
||||
data = (line.rstrip() for line in logfile)
|
||||
if lines == 0:
|
||||
loglines = list(data)
|
||||
else:
|
||||
loglines = deque(data, lines)
|
||||
return loglines
|
||||
|
||||
|
||||
class ZWaveConfigWriteView(HomeAssistantView):
|
||||
"""View to save the ZWave configuration to zwcfg_xxxxx.xml."""
|
||||
|
||||
url = "/api/zwave/saveconfig"
|
||||
name = "api:zwave:saveconfig"
|
||||
|
||||
@ha.callback
|
||||
def post(self, request):
|
||||
"""Save cache configuration to zwcfg_xxxxx.xml."""
|
||||
hass = request.app['hass']
|
||||
network = hass.data.get(const.DATA_NETWORK)
|
||||
if network is None:
|
||||
return self.json_message('No Z-Wave network data found',
|
||||
HTTP_NOT_FOUND)
|
||||
_LOGGER.info("Z-Wave configuration written to file.")
|
||||
network.write_config()
|
||||
return self.json_message('Z-Wave configuration saved to file.',
|
||||
HTTP_OK)
|
||||
|
||||
|
||||
class ZWaveNodeValueView(HomeAssistantView):
|
||||
"""View to return the node values."""
|
||||
|
||||
|
@ -207,7 +207,7 @@ class Configurator(object):
|
||||
|
||||
self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove)
|
||||
|
||||
@async_callback
|
||||
@asyncio.coroutine
|
||||
def async_handle_service_call(self, call):
|
||||
"""Handle a configure service call."""
|
||||
request_id = call.data.get(ATTR_CONFIGURE_ID)
|
||||
@ -220,7 +220,8 @@ class Configurator(object):
|
||||
|
||||
# field validation goes here?
|
||||
if callback:
|
||||
self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
|
||||
yield from self.hass.async_add_job(callback,
|
||||
call.data.get(ATTR_FIELDS, {}))
|
||||
|
||||
def _generate_unique_id(self):
|
||||
"""Generate a unique configurator ID."""
|
||||
|
@ -140,13 +140,13 @@ def async_setup(hass, config):
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_INCREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_INCREMENT], SERVICE_SCHEMA)
|
||||
descriptions[SERVICE_INCREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DECREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_DECREMENT], SERVICE_SCHEMA)
|
||||
descriptions[SERVICE_DECREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_RESET], SERVICE_SCHEMA)
|
||||
descriptions[SERVICE_RESET], SERVICE_SCHEMA)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
return True
|
20
homeassistant/components/counter/services.yaml
Normal file
20
homeassistant/components/counter/services.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
# Describes the format for available counter services
|
||||
|
||||
decrement:
|
||||
description: Decrement a counter.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id of the counter to decrement.
|
||||
example: 'counter.count0'
|
||||
increment:
|
||||
description: Increment a counter.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id of the counter to increment.
|
||||
example: 'counter.count0'
|
||||
reset:
|
||||
description: Reset a counter.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id of the counter to reset.
|
||||
example: 'counter.count0'
|
@ -4,6 +4,7 @@ Support for Lutron Caseta shades.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.lutron_caseta/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
@ -18,7 +19,8 @@ DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
@ -27,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = LutronCasetaCover(cover_device, bridge)
|
||||
devs.append(dev)
|
||||
|
||||
add_devices(devs, True)
|
||||
async_add_devices(devs, True)
|
||||
|
||||
|
||||
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
@ -48,21 +50,25 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
"""Return the current position of cover."""
|
||||
return self._state['current_state']
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 100)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Move the shade to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Call when forcing a refresh of the device."""
|
||||
self._state = self._smartbridge.get_device_by_id(self._device_id)
|
||||
_LOGGER.debug(self._state)
|
||||
|
@ -104,6 +104,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the MQTT Cover."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
@ -76,6 +76,7 @@ ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_MAC = 'mac'
|
||||
ATTR_NAME = 'name'
|
||||
ATTR_SOURCE_TYPE = 'source_type'
|
||||
ATTR_VENDOR = 'vendor'
|
||||
|
||||
SOURCE_TYPE_GPS = 'gps'
|
||||
SOURCE_TYPE_ROUTER = 'router'
|
||||
@ -285,11 +286,6 @@ class DeviceTracker(object):
|
||||
if device.track:
|
||||
yield from device.async_update_ha_state()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
})
|
||||
|
||||
# During init, we ignore the group
|
||||
if self.group and self.track_new:
|
||||
self.group.async_set_group(
|
||||
@ -299,6 +295,13 @@ class DeviceTracker(object):
|
||||
# lookup mac vendor string to be stored in config
|
||||
yield from device.set_vendor_for_mac()
|
||||
|
||||
self.hass.bus.async_fire(EVENT_NEW_DEVICE, {
|
||||
ATTR_ENTITY_ID: device.entity_id,
|
||||
ATTR_HOST_NAME: device.host_name,
|
||||
ATTR_MAC: device.mac,
|
||||
ATTR_VENDOR: device.vendor,
|
||||
})
|
||||
|
||||
# update known_devices.yaml
|
||||
self.hass.async_add_job(
|
||||
self.async_update_config(
|
||||
|
138
homeassistant/components/device_tracker/hitron_coda.py
Normal file
138
homeassistant/components/device_tracker/hitron_coda.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""
|
||||
Support for the Hitron CODA-4582U, provided by Rogers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.hitron_coda/
|
||||
"""
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string
|
||||
})
|
||||
|
||||
|
||||
def get_scanner(_hass, config):
|
||||
"""Validate the configuration and return a Nmap scanner."""
|
||||
scanner = HitronCODADeviceScanner(config[DOMAIN])
|
||||
|
||||
return scanner if scanner.success_init else None
|
||||
|
||||
|
||||
Device = namedtuple('Device', ['mac', 'name'])
|
||||
|
||||
|
||||
class HitronCODADeviceScanner(DeviceScanner):
|
||||
"""This class scans for devices using the CODA's web interface."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the scanner."""
|
||||
self.last_results = []
|
||||
host = config[CONF_HOST]
|
||||
self._url = 'http://{}/data/getConnectInfo.asp'.format(host)
|
||||
self._loginurl = 'http://{}/goform/login'.format(host)
|
||||
|
||||
self._username = config.get(CONF_USERNAME)
|
||||
self._password = config.get(CONF_PASSWORD)
|
||||
|
||||
self._userid = None
|
||||
|
||||
self.success_init = self._update_info()
|
||||
_LOGGER.info("Scanner initialized")
|
||||
|
||||
def scan_devices(self):
|
||||
"""Scan for new devices and return a list with found device IDs."""
|
||||
self._update_info()
|
||||
|
||||
return [device.mac for device in self.last_results]
|
||||
|
||||
def get_device_name(self, mac):
|
||||
"""Return the name of the device with the given MAC address."""
|
||||
name = next((
|
||||
device.name for device in self.last_results
|
||||
if device.mac == mac), None)
|
||||
return name
|
||||
|
||||
def _login(self):
|
||||
"""Log in to the router. This is required for subsequent api calls."""
|
||||
_LOGGER.info("Logging in to CODA...")
|
||||
|
||||
try:
|
||||
data = [
|
||||
('user', self._username),
|
||||
('pws', self._password),
|
||||
]
|
||||
res = requests.post(self._loginurl, data=data, timeout=10)
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.error(
|
||||
"Connection to the router timed out at URL %s", self._url)
|
||||
return False
|
||||
if res.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Connection failed with http code %s", res.status_code)
|
||||
return False
|
||||
try:
|
||||
self._userid = res.cookies['userid']
|
||||
return True
|
||||
except KeyError:
|
||||
_LOGGER.error("Failed to log in to router")
|
||||
return False
|
||||
|
||||
def _update_info(self):
|
||||
"""Get ARP from router."""
|
||||
_LOGGER.info("Fetching...")
|
||||
|
||||
if self._userid is None:
|
||||
if not self._login():
|
||||
_LOGGER.error("Could not obtain a user ID from the router")
|
||||
return False
|
||||
last_results = []
|
||||
|
||||
# doing a request
|
||||
try:
|
||||
res = requests.get(self._url, timeout=10, cookies={
|
||||
'userid': self._userid
|
||||
})
|
||||
except requests.exceptions.Timeout:
|
||||
_LOGGER.error(
|
||||
"Connection to the router timed out at URL %s", self._url)
|
||||
return False
|
||||
if res.status_code != 200:
|
||||
_LOGGER.error(
|
||||
"Connection failed with http code %s", res.status_code)
|
||||
return False
|
||||
try:
|
||||
result = res.json()
|
||||
except ValueError:
|
||||
# If json decoder could not parse the response
|
||||
_LOGGER.error("Failed to parse response from router")
|
||||
return False
|
||||
|
||||
# parsing response
|
||||
for info in result:
|
||||
mac = info['macAddr']
|
||||
name = info['hostName']
|
||||
# No address = no item :)
|
||||
if mac is None:
|
||||
continue
|
||||
|
||||
last_results.append(Device(mac.upper(), name))
|
||||
|
||||
self.last_results = last_results
|
||||
|
||||
_LOGGER.info("Request successful")
|
||||
return True
|
@ -367,6 +367,29 @@ def async_handle_transition_message(hass, context, message):
|
||||
message['event'])
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_waypoint(hass, name_base, waypoint):
|
||||
"""Handle a waypoint."""
|
||||
name = waypoint['desc']
|
||||
pretty_name = '{} - {}'.format(name_base, name)
|
||||
lat = waypoint['lat']
|
||||
lon = waypoint['lon']
|
||||
rad = waypoint['rad']
|
||||
|
||||
# check zone exists
|
||||
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
|
||||
|
||||
# Check if state already exists
|
||||
if hass.states.get(entity_id) is not None:
|
||||
return
|
||||
|
||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||
zone_comp.ICON_IMPORT, False)
|
||||
zone.entity_id = entity_id
|
||||
yield from zone.async_update_ha_state()
|
||||
|
||||
|
||||
@HANDLERS.register('waypoint')
|
||||
@HANDLERS.register('waypoints')
|
||||
@asyncio.coroutine
|
||||
def async_handle_waypoints_message(hass, context, message):
|
||||
@ -380,30 +403,17 @@ def async_handle_waypoints_message(hass, context, message):
|
||||
if user not in context.waypoint_whitelist:
|
||||
return
|
||||
|
||||
wayps = message['waypoints']
|
||||
if 'waypoints' in message:
|
||||
wayps = message['waypoints']
|
||||
else:
|
||||
wayps = [message]
|
||||
|
||||
_LOGGER.info("Got %d waypoints from %s", len(wayps), message['topic'])
|
||||
|
||||
name_base = ' '.join(_parse_topic(message['topic']))
|
||||
|
||||
for wayp in wayps:
|
||||
name = wayp['desc']
|
||||
pretty_name = '{} - {}'.format(name_base, name)
|
||||
lat = wayp['lat']
|
||||
lon = wayp['lon']
|
||||
rad = wayp['rad']
|
||||
|
||||
# check zone exists
|
||||
entity_id = zone_comp.ENTITY_ID_FORMAT.format(slugify(pretty_name))
|
||||
|
||||
# Check if state already exists
|
||||
if hass.states.get(entity_id) is not None:
|
||||
continue
|
||||
|
||||
zone = zone_comp.Zone(hass, pretty_name, lat, lon, rad,
|
||||
zone_comp.ICON_IMPORT, False)
|
||||
zone.entity_id = entity_id
|
||||
yield from zone.async_update_ha_state()
|
||||
yield from async_handle_waypoint(hass, name_base, wayp)
|
||||
|
||||
|
||||
@HANDLERS.register('encrypted')
|
||||
@ -423,10 +433,22 @@ def async_handle_encrypted_message(hass, context, message):
|
||||
|
||||
|
||||
@HANDLERS.register('lwt')
|
||||
@HANDLERS.register('configuration')
|
||||
@HANDLERS.register('beacon')
|
||||
@HANDLERS.register('cmd')
|
||||
@HANDLERS.register('steps')
|
||||
@HANDLERS.register('card')
|
||||
@asyncio.coroutine
|
||||
def async_handle_lwt_message(hass, context, message):
|
||||
"""Handle an lwt message."""
|
||||
_LOGGER.debug('Not handling lwt message: %s', message)
|
||||
def async_handle_not_impl_msg(hass, context, message):
|
||||
"""Handle valid but not implemented message types."""
|
||||
_LOGGER.debug('Not handling %s message: %s', message.get("_type"), message)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_unsupported_msg(hass, context, message):
|
||||
"""Handle an unsupported or invalid message type."""
|
||||
_LOGGER.warning('Received unsupported message type: %s.',
|
||||
message.get('_type'))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -434,11 +456,6 @@ def async_handle_message(hass, context, message):
|
||||
"""Handle an OwnTracks message."""
|
||||
msgtype = message.get('_type')
|
||||
|
||||
handler = HANDLERS.get(msgtype)
|
||||
|
||||
if handler is None:
|
||||
_LOGGER.warning(
|
||||
'Received unsupported message type: %s.', msgtype)
|
||||
return
|
||||
handler = HANDLERS.get(msgtype, async_handle_unsupported_msg)
|
||||
|
||||
yield from handler(hass, context, message)
|
||||
|
@ -14,14 +14,14 @@ from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.4.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pysnmp==4.4.1']
|
||||
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_AUTHKEY = 'authkey'
|
||||
CONF_PRIVKEY = 'privkey'
|
||||
CONF_BASEOID = 'baseoid'
|
||||
CONF_COMMUNITY = 'community'
|
||||
CONF_PRIVKEY = 'privkey'
|
||||
|
||||
DEFAULT_COMMUNITY = 'public'
|
||||
|
||||
|
@ -6,13 +6,14 @@ https://home-assistant.io/components/device_tracker.swisscom/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -77,7 +78,7 @@ class SwisscomDeviceScanner(DeviceScanner):
|
||||
def get_swisscom_data(self):
|
||||
"""Retrieve data from Swisscom and return parsed result."""
|
||||
url = 'http://{}/ws'.format(self.host)
|
||||
headers = {'Content-Type': 'application/x-sah-ws-4-call+json'}
|
||||
headers = {CONTENT_TYPE: 'application/x-sah-ws-4-call+json'}
|
||||
data = """
|
||||
{"service":"Devices", "method":"get",
|
||||
"parameters":{"expression":"lan and not self"}}"""
|
||||
|
124
homeassistant/components/device_tracker/tile.py
Normal file
124
homeassistant/components/device_tracker/tile.py
Normal file
@ -0,0 +1,124 @@
|
||||
"""
|
||||
Support for Tile® Bluetooth trackers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tile/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD)
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['pytile==1.0.0']
|
||||
|
||||
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
|
||||
DEFAULT_ICON = 'mdi:bluetooth'
|
||||
DEVICE_TYPES = ['PHONE', 'TILE']
|
||||
|
||||
ATTR_ALTITUDE = 'altitude'
|
||||
ATTR_CONNECTION_STATE = 'connection_state'
|
||||
ATTR_IS_DEAD = 'is_dead'
|
||||
ATTR_IS_LOST = 'is_lost'
|
||||
ATTR_LAST_SEEN = 'last_seen'
|
||||
ATTR_LAST_UPDATED = 'last_updated'
|
||||
ATTR_RING_STATE = 'ring_state'
|
||||
ATTR_VOIP_STATE = 'voip_state'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_MONITORED_VARIABLES):
|
||||
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config: dict, see, discovery_info=None):
|
||||
"""Validate the configuration and return a Tile scanner."""
|
||||
TileDeviceScanner(hass, config, see)
|
||||
return True
|
||||
|
||||
|
||||
class TileDeviceScanner(DeviceScanner):
|
||||
"""Define a device scanner for Tiles."""
|
||||
|
||||
def __init__(self, hass, config, see):
|
||||
"""Initialize."""
|
||||
from pytile import Client
|
||||
|
||||
_LOGGER.debug('Received configuration data: %s', config)
|
||||
|
||||
# Load the client UUID (if it exists):
|
||||
config_data = load_json(hass.config.path(CLIENT_UUID_CONFIG_FILE))
|
||||
if config_data:
|
||||
_LOGGER.debug('Using existing client UUID')
|
||||
self._client = Client(
|
||||
config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD],
|
||||
config_data['client_uuid'])
|
||||
else:
|
||||
_LOGGER.debug('Generating new client UUID')
|
||||
self._client = Client(
|
||||
config[CONF_USERNAME],
|
||||
config[CONF_PASSWORD])
|
||||
|
||||
if not save_json(
|
||||
hass.config.path(CLIENT_UUID_CONFIG_FILE),
|
||||
{'client_uuid': self._client.client_uuid}):
|
||||
_LOGGER.error("Failed to save configuration file")
|
||||
|
||||
_LOGGER.debug('Client UUID: %s', self._client.client_uuid)
|
||||
_LOGGER.debug('User UUID: %s', self._client.user_uuid)
|
||||
|
||||
self._types = config.get(CONF_MONITORED_VARIABLES)
|
||||
|
||||
self.devices = {}
|
||||
self.see = see
|
||||
|
||||
track_utc_time_change(
|
||||
hass, self._update_info, second=range(0, 60, 30))
|
||||
|
||||
self._update_info()
|
||||
|
||||
def _update_info(self, now=None) -> None:
|
||||
"""Update the device info."""
|
||||
device_data = self._client.get_tiles(type_whitelist=self._types)
|
||||
|
||||
try:
|
||||
self.devices = device_data['result']
|
||||
except KeyError:
|
||||
_LOGGER.warning('No Tiles found')
|
||||
_LOGGER.debug(device_data)
|
||||
return
|
||||
|
||||
for info in self.devices.values():
|
||||
dev_id = 'tile_{0}'.format(slugify(info['name']))
|
||||
lat = info['tileState']['latitude']
|
||||
lon = info['tileState']['longitude']
|
||||
|
||||
attrs = {
|
||||
ATTR_ALTITUDE: info['tileState']['altitude'],
|
||||
ATTR_CONNECTION_STATE: info['tileState']['connection_state'],
|
||||
ATTR_IS_DEAD: info['is_dead'],
|
||||
ATTR_IS_LOST: info['tileState']['is_lost'],
|
||||
ATTR_LAST_SEEN: info['tileState']['timestamp'],
|
||||
ATTR_LAST_UPDATED: device_data['timestamp_ms'],
|
||||
ATTR_RING_STATE: info['tileState']['ring_state'],
|
||||
ATTR_VOIP_STATE: info['tileState']['voip_state'],
|
||||
}
|
||||
|
||||
self.see(
|
||||
dev_id=dev_id,
|
||||
gps=(lat, lon),
|
||||
attributes=attrs,
|
||||
icon=DEFAULT_ICON
|
||||
)
|
@ -5,21 +5,27 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tplink/
|
||||
"""
|
||||
import base64
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from aiohttp.hdrs import (
|
||||
ACCEPT, COOKIE, PRAGMA, REFERER, CONNECTION, KEEP_ALIVE, USER_AGENT,
|
||||
CONTENT_TYPE, CACHE_CONTROL, ACCEPT_ENCODING, ACCEPT_LANGUAGE)
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_HEADER_X_REQUESTED_WITH)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HTTP_HEADER_NO_CACHE = 'no-cache'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
@ -78,7 +84,7 @@ class TplinkDeviceScanner(DeviceScanner):
|
||||
referer = 'http://{}'.format(self.host)
|
||||
page = requests.get(
|
||||
url, auth=(self.username, self.password),
|
||||
headers={'referer': referer}, timeout=4)
|
||||
headers={REFERER: referer}, timeout=4)
|
||||
|
||||
result = self.parse_macs.findall(page.text)
|
||||
|
||||
@ -123,7 +129,7 @@ class Tplink2DeviceScanner(TplinkDeviceScanner):
|
||||
.format(b64_encoded_username_password)
|
||||
|
||||
response = requests.post(
|
||||
url, headers={'referer': referer, 'cookie': cookie},
|
||||
url, headers={REFERER: referer, COOKIE: cookie},
|
||||
timeout=4)
|
||||
|
||||
try:
|
||||
@ -174,11 +180,11 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
.format(self.host)
|
||||
referer = 'http://{}/webpages/login.html'.format(self.host)
|
||||
|
||||
# If possible implement rsa encryption of password here.
|
||||
# If possible implement RSA encryption of password here.
|
||||
response = requests.post(
|
||||
url, params={'operation': 'login', 'username': self.username,
|
||||
'password': self.password},
|
||||
headers={'referer': referer}, timeout=4)
|
||||
headers={REFERER: referer}, timeout=4)
|
||||
|
||||
try:
|
||||
self.stok = response.json().get('data').get('stok')
|
||||
@ -207,11 +213,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
'form=statistics').format(self.host, self.stok)
|
||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||
|
||||
response = requests.post(url,
|
||||
params={'operation': 'load'},
|
||||
headers={'referer': referer},
|
||||
cookies={'sysauth': self.sysauth},
|
||||
timeout=5)
|
||||
response = requests.post(
|
||||
url, params={'operation': 'load'}, headers={REFERER: referer},
|
||||
cookies={'sysauth': self.sysauth}, timeout=5)
|
||||
|
||||
try:
|
||||
json_response = response.json()
|
||||
@ -248,10 +252,9 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
|
||||
'form=logout').format(self.host, self.stok)
|
||||
referer = 'http://{}/webpages/index.html'.format(self.host)
|
||||
|
||||
requests.post(url,
|
||||
params={'operation': 'write'},
|
||||
headers={'referer': referer},
|
||||
cookies={'sysauth': self.sysauth})
|
||||
requests.post(
|
||||
url, params={'operation': 'write'}, headers={REFERER: referer},
|
||||
cookies={'sysauth': self.sysauth})
|
||||
self.stok = ''
|
||||
self.sysauth = ''
|
||||
|
||||
@ -292,7 +295,7 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||
# Create the authorization cookie.
|
||||
cookie = 'Authorization=Basic {}'.format(self.credentials)
|
||||
|
||||
response = requests.get(url, headers={'cookie': cookie})
|
||||
response = requests.get(url, headers={COOKIE: cookie})
|
||||
|
||||
try:
|
||||
result = re.search(r'window.parent.location.href = '
|
||||
@ -326,8 +329,8 @@ class Tplink4DeviceScanner(TplinkDeviceScanner):
|
||||
cookie = 'Authorization=Basic {}'.format(self.credentials)
|
||||
|
||||
page = requests.get(url, headers={
|
||||
'cookie': cookie,
|
||||
'referer': referer
|
||||
COOKIE: cookie,
|
||||
REFERER: referer,
|
||||
})
|
||||
mac_results.extend(self.parse_macs.findall(page.text))
|
||||
|
||||
@ -361,31 +364,31 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
|
||||
base_url = 'http://{}'.format(self.host)
|
||||
|
||||
header = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
|
||||
" rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Accept-Language": "Accept-Language: en-US,en;q=0.5",
|
||||
"Accept-Encoding": "gzip, deflate",
|
||||
"Content-Type": "application/x-www-form-urlencoded; "
|
||||
"charset=UTF-8",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Referer": "http://" + self.host + "/",
|
||||
"Connection": "keep-alive",
|
||||
"Pragma": "no-cache",
|
||||
"Cache-Control": "no-cache"
|
||||
USER_AGENT:
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;"
|
||||
" rv:53.0) Gecko/20100101 Firefox/53.0",
|
||||
ACCEPT: "application/json, text/javascript, */*; q=0.01",
|
||||
ACCEPT_LANGUAGE: "Accept-Language: en-US,en;q=0.5",
|
||||
ACCEPT_ENCODING: "gzip, deflate",
|
||||
CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest",
|
||||
REFERER: "http://{}/".format(self.host),
|
||||
CONNECTION: KEEP_ALIVE,
|
||||
PRAGMA: HTTP_HEADER_NO_CACHE,
|
||||
CACHE_CONTROL: HTTP_HEADER_NO_CACHE,
|
||||
}
|
||||
|
||||
password_md5 = hashlib.md5(
|
||||
self.password.encode('utf')).hexdigest().upper()
|
||||
|
||||
# create a session to handle cookie easier
|
||||
# Create a session to handle cookie easier
|
||||
session = requests.session()
|
||||
session.get(base_url, headers=header)
|
||||
|
||||
login_data = {"username": self.username, "password": password_md5}
|
||||
session.post(base_url, login_data, headers=header)
|
||||
|
||||
# a timestamp is required to be sent as get parameter
|
||||
# A timestamp is required to be sent as get parameter
|
||||
timestamp = int(datetime.now().timestamp() * 1e3)
|
||||
|
||||
client_list_url = '{}/data/monitor.client.client.json'.format(
|
||||
@ -393,18 +396,17 @@ class Tplink5DeviceScanner(TplinkDeviceScanner):
|
||||
|
||||
get_params = {
|
||||
'operation': 'load',
|
||||
'_': timestamp
|
||||
'_': timestamp,
|
||||
}
|
||||
|
||||
response = session.get(client_list_url,
|
||||
headers=header,
|
||||
params=get_params)
|
||||
response = session.get(
|
||||
client_list_url, headers=header, params=get_params)
|
||||
session.close()
|
||||
try:
|
||||
list_of_devices = response.json()
|
||||
except ValueError:
|
||||
_LOGGER.error("AP didn't respond with JSON. "
|
||||
"Check if credentials are correct.")
|
||||
"Check if credentials are correct")
|
||||
return False
|
||||
|
||||
if list_of_devices:
|
||||
|
@ -8,28 +8,28 @@ import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.hdrs import REFERER, USER_AGENT
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.device_tracker import (
|
||||
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.const import CONF_HOST, HTTP_HEADER_X_REQUESTED_WITH
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['defusedxml==0.5.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CMD_DEVICES = 123
|
||||
|
||||
DEFAULT_IP = '192.168.0.1'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string,
|
||||
})
|
||||
|
||||
CMD_DEVICES = 123
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_scanner(hass, config):
|
||||
@ -52,11 +52,11 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
self.token = None
|
||||
|
||||
self.headers = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Referer': "http://{}/index.html".format(self.host),
|
||||
'User-Agent': ("Mozilla/5.0 (Windows NT 10.0; WOW64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/47.0.2526.106 Safari/537.36")
|
||||
HTTP_HEADER_X_REQUESTED_WITH: 'XMLHttpRequest',
|
||||
REFERER: "http://{}/index.html".format(self.host),
|
||||
USER_AGENT: ("Mozilla/5.0 (Windows NT 10.0; WOW64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/47.0.2526.106 Safari/537.36")
|
||||
}
|
||||
|
||||
self.websession = async_get_clientsession(hass)
|
||||
@ -95,8 +95,7 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||
response = yield from self.websession.get(
|
||||
"http://{}/common_page/login.html".format(self.host),
|
||||
headers=self.headers
|
||||
)
|
||||
headers=self.headers)
|
||||
|
||||
yield from response.text()
|
||||
|
||||
@ -118,17 +117,15 @@ class UPCDeviceScanner(DeviceScanner):
|
||||
response = yield from self.websession.post(
|
||||
"http://{}/xml/getter.xml".format(self.host),
|
||||
data="token={}&fun={}".format(self.token, function),
|
||||
headers=self.headers,
|
||||
allow_redirects=False
|
||||
)
|
||||
headers=self.headers, allow_redirects=False)
|
||||
|
||||
# error?
|
||||
# Error?
|
||||
if response.status != 200:
|
||||
_LOGGER.warning("Receive http code %d", response.status)
|
||||
self.token = None
|
||||
return
|
||||
|
||||
# load data, store token for next request
|
||||
# Load data, store token for next request
|
||||
self.token = response.cookies['sessionToken'].value
|
||||
return (yield from response.text())
|
||||
|
||||
|
@ -9,7 +9,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import PROJECT_NAME, HTTP_BAD_REQUEST
|
||||
from homeassistant.const import HTTP_BAD_REQUEST
|
||||
from homeassistant.helpers import intent, template
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
@ -26,6 +26,8 @@ DOMAIN = 'dialogflow'
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/dialogflow'
|
||||
|
||||
SOURCE = "Home Assistant Dialogflow"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: {}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@ -128,5 +130,5 @@ class DialogflowResponse(object):
|
||||
return {
|
||||
'speech': self.speech,
|
||||
'displayText': self.speech,
|
||||
'source': PROJECT_NAME,
|
||||
'source': SOURCE,
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ATTR_FILENAME = 'filename'
|
||||
ATTR_SUBDIR = 'subdir'
|
||||
ATTR_URL = 'url'
|
||||
ATTR_OVERWRITE = 'overwrite'
|
||||
|
||||
CONF_DOWNLOAD_DIR = 'download_dir'
|
||||
|
||||
@ -31,6 +32,7 @@ SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_URL): cv.url,
|
||||
vol.Optional(ATTR_SUBDIR): cv.string,
|
||||
vol.Optional(ATTR_FILENAME): cv.string,
|
||||
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@ -66,6 +68,8 @@ def setup(hass, config):
|
||||
|
||||
filename = service.data.get(ATTR_FILENAME)
|
||||
|
||||
overwrite = service.data.get(ATTR_OVERWRITE)
|
||||
|
||||
if subdir:
|
||||
subdir = sanitize_filename(subdir)
|
||||
|
||||
@ -73,8 +77,13 @@ def setup(hass, config):
|
||||
|
||||
req = requests.get(url, stream=True, timeout=10)
|
||||
|
||||
if req.status_code == 200:
|
||||
if req.status_code != 200:
|
||||
_LOGGER.warning(
|
||||
"downloading '%s' failed, stauts_code=%d",
|
||||
url,
|
||||
req.status_code)
|
||||
|
||||
else:
|
||||
if filename is None and \
|
||||
'content-disposition' in req.headers:
|
||||
match = re.findall(r"filename=(\S+)",
|
||||
@ -109,20 +118,21 @@ def setup(hass, config):
|
||||
|
||||
# If file exist append a number.
|
||||
# We test filename, filename_2..
|
||||
tries = 1
|
||||
final_path = path + ext
|
||||
while os.path.isfile(final_path):
|
||||
tries += 1
|
||||
if not overwrite:
|
||||
tries = 1
|
||||
final_path = path + ext
|
||||
while os.path.isfile(final_path):
|
||||
tries += 1
|
||||
|
||||
final_path = "{}_{}.{}".format(path, tries, ext)
|
||||
final_path = "{}_{}.{}".format(path, tries, ext)
|
||||
|
||||
_LOGGER.info("%s -> %s", url, final_path)
|
||||
_LOGGER.debug("%s -> %s", url, final_path)
|
||||
|
||||
with open(final_path, 'wb') as fil:
|
||||
for chunk in req.iter_content(1024):
|
||||
fil.write(chunk)
|
||||
|
||||
_LOGGER.info("Downloading of %s done", url)
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.exception("ConnectionError occurred for %s", url)
|
||||
|
@ -72,6 +72,7 @@ class EnOceanDongle:
|
||||
"""
|
||||
from enocean.protocol.packet import RadioPacket
|
||||
if isinstance(temp, RadioPacket):
|
||||
_LOGGER.debug("Received radio packet: %s", temp)
|
||||
rxtype = None
|
||||
value = None
|
||||
if temp.data[6] == 0x30:
|
||||
@ -94,20 +95,20 @@ class EnOceanDongle:
|
||||
value = temp.data[2]
|
||||
for device in self.__devices:
|
||||
if rxtype == "wallswitch" and device.stype == "listener":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
if temp.sender_int == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value, temp.data[1])
|
||||
if rxtype == "power" and device.stype == "powersensor":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
if temp.sender_int == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
if rxtype == "power" and device.stype == "switch":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
if temp.sender_int == self._combine_hex(device.dev_id):
|
||||
if value > 10:
|
||||
device.value_changed(1)
|
||||
if rxtype == "switch_status" and device.stype == "switch":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
if temp.sender_int == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
if rxtype == "dimmerstatus" and device.stype == "dimmer":
|
||||
if temp.sender == self._combine_hex(device.dev_id):
|
||||
if temp.sender_int == self._combine_hex(device.dev_id):
|
||||
device.value_changed(value)
|
||||
|
||||
|
||||
|
@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.0']
|
||||
REQUIREMENTS = ['python-miio==0.3.1']
|
||||
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
ATTR_HUMIDITY = 'humidity'
|
||||
|
@ -9,9 +9,11 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
import jinja2
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
@ -21,21 +23,19 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
REQUIREMENTS = ['home-assistant-frontend==20171105.0']
|
||||
REQUIREMENTS = ['home-assistant-frontend==20171118.0']
|
||||
|
||||
DOMAIN = 'frontend'
|
||||
DEPENDENCIES = ['api', 'websocket_api']
|
||||
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
|
||||
|
||||
URL_PANEL_COMPONENT = '/frontend/panels/{}.html'
|
||||
URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
||||
|
||||
POLYMER_PATH = os.path.join(os.path.dirname(__file__),
|
||||
'home-assistant-polymer/')
|
||||
FINAL_PATH = os.path.join(POLYMER_PATH, 'final')
|
||||
|
||||
CONF_THEMES = 'themes'
|
||||
CONF_EXTRA_HTML_URL = 'extra_html_url'
|
||||
CONF_FRONTEND_REPO = 'development_repo'
|
||||
CONF_JS_VERSION = 'javascript_version'
|
||||
JS_DEFAULT_OPTION = 'es5'
|
||||
JS_OPTIONS = ['es5', 'latest', 'auto']
|
||||
|
||||
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||
|
||||
@ -61,6 +61,7 @@ for size in (192, 384, 512, 1024):
|
||||
|
||||
DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
|
||||
DATA_PANELS = 'frontend_panels'
|
||||
DATA_JS_VERSION = 'frontend_js_version'
|
||||
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
|
||||
DATA_THEMES = 'frontend_themes'
|
||||
DATA_DEFAULT_THEME = 'frontend_default_theme'
|
||||
@ -68,8 +69,6 @@ DEFAULT_THEME = 'default'
|
||||
|
||||
PRIMARY_COLOR = 'primary-color'
|
||||
|
||||
# To keep track we don't register a component twice (gives a warning)
|
||||
# _REGISTERED_COMPONENTS = set()
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
@ -80,6 +79,8 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
}),
|
||||
vol.Optional(CONF_EXTRA_HTML_URL):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
|
||||
vol.In(JS_OPTIONS)
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -102,8 +103,9 @@ class AbstractPanel:
|
||||
# Title to show in the sidebar (optional)
|
||||
sidebar_title = None
|
||||
|
||||
# Url to the webcomponent
|
||||
webcomponent_url = None
|
||||
# Url to the webcomponent (depending on JS version)
|
||||
webcomponent_url_es5 = None
|
||||
webcomponent_url_latest = None
|
||||
|
||||
# Url to show the panel in the frontend
|
||||
frontend_url_path = None
|
||||
@ -135,16 +137,20 @@ class AbstractPanel:
|
||||
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
|
||||
index_view.get)
|
||||
|
||||
def as_dict(self):
|
||||
def to_response(self, hass, request):
|
||||
"""Panel as dictionary."""
|
||||
return {
|
||||
result = {
|
||||
'component_name': self.component_name,
|
||||
'icon': self.sidebar_icon,
|
||||
'title': self.sidebar_title,
|
||||
'url': self.webcomponent_url,
|
||||
'url_path': self.frontend_url_path,
|
||||
'config': self.config,
|
||||
}
|
||||
if _is_latest(hass.data[DATA_JS_VERSION], request):
|
||||
result['url'] = self.webcomponent_url_latest
|
||||
else:
|
||||
result['url'] = self.webcomponent_url_es5
|
||||
return result
|
||||
|
||||
|
||||
class BuiltInPanel(AbstractPanel):
|
||||
@ -166,19 +172,21 @@ class BuiltInPanel(AbstractPanel):
|
||||
If frontend_repository_path is set, will be prepended to path of
|
||||
built-in components.
|
||||
"""
|
||||
panel_path = 'panels/ha-panel-{}.html'.format(self.component_name)
|
||||
|
||||
if frontend_repository_path is None:
|
||||
import hass_frontend
|
||||
import hass_frontend_es5
|
||||
|
||||
self.webcomponent_url = \
|
||||
'/static/panels/ha-panel-{}-{}.html'.format(
|
||||
self.webcomponent_url_latest = \
|
||||
'/frontend_latest/panels/ha-panel-{}-{}.html'.format(
|
||||
self.component_name,
|
||||
hass_frontend.FINGERPRINTS[panel_path])
|
||||
|
||||
hass_frontend.FINGERPRINTS[self.component_name])
|
||||
self.webcomponent_url_es5 = \
|
||||
'/frontend_es5/panels/ha-panel-{}-{}.html'.format(
|
||||
self.component_name,
|
||||
hass_frontend_es5.FINGERPRINTS[self.component_name])
|
||||
else:
|
||||
# Dev mode
|
||||
self.webcomponent_url = \
|
||||
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
|
||||
'/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format(
|
||||
self.component_name, self.component_name)
|
||||
|
||||
@ -208,18 +216,20 @@ class ExternalPanel(AbstractPanel):
|
||||
"""
|
||||
try:
|
||||
if self.md5 is None:
|
||||
yield from hass.async_add_job(_fingerprint, self.path)
|
||||
self.md5 = yield from hass.async_add_job(
|
||||
_fingerprint, self.path)
|
||||
except OSError:
|
||||
_LOGGER.error('Cannot find or access %s at %s',
|
||||
self.component_name, self.path)
|
||||
hass.data[DATA_PANELS].pop(self.frontend_url_path)
|
||||
return
|
||||
|
||||
self.webcomponent_url = \
|
||||
self.webcomponent_url_es5 = self.webcomponent_url_latest = \
|
||||
URL_PANEL_COMPONENT_FP.format(self.component_name, self.md5)
|
||||
|
||||
if self.component_name not in self.REGISTERED_COMPONENTS:
|
||||
hass.http.register_static_path(
|
||||
self.webcomponent_url, self.path,
|
||||
self.webcomponent_url_latest, self.path,
|
||||
# if path is None, we're in prod mode, so cache static assets
|
||||
frontend_repository_path is None)
|
||||
self.REGISTERED_COMPONENTS.add(self.component_name)
|
||||
@ -281,31 +291,50 @@ def async_setup(hass, config):
|
||||
|
||||
repo_path = conf.get(CONF_FRONTEND_REPO)
|
||||
is_dev = repo_path is not None
|
||||
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
|
||||
|
||||
if is_dev:
|
||||
hass.http.register_static_path(
|
||||
"/home-assistant-polymer", repo_path, False)
|
||||
hass.http.register_static_path(
|
||||
"/static/translations",
|
||||
os.path.join(repo_path, "build/translations"), False)
|
||||
sw_path = os.path.join(repo_path, "build/service_worker.js")
|
||||
os.path.join(repo_path, "build-translations"), False)
|
||||
sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js")
|
||||
sw_path_latest = os.path.join(repo_path, "build/service_worker.js")
|
||||
static_path = os.path.join(repo_path, 'hass_frontend')
|
||||
frontend_es5_path = os.path.join(repo_path, 'build-es5')
|
||||
frontend_latest_path = os.path.join(repo_path, 'build')
|
||||
else:
|
||||
import hass_frontend
|
||||
frontend_path = hass_frontend.where()
|
||||
sw_path = os.path.join(frontend_path, "service_worker.js")
|
||||
static_path = frontend_path
|
||||
import hass_frontend_es5
|
||||
sw_path_es5 = os.path.join(hass_frontend_es5.where(),
|
||||
"service_worker.js")
|
||||
sw_path_latest = os.path.join(hass_frontend.where(),
|
||||
"service_worker.js")
|
||||
# /static points to dir with files that are JS-type agnostic.
|
||||
# ES5 files are served from /frontend_es5.
|
||||
# ES6 files are served from /frontend_latest.
|
||||
static_path = hass_frontend.where()
|
||||
frontend_es5_path = hass_frontend_es5.where()
|
||||
frontend_latest_path = static_path
|
||||
|
||||
hass.http.register_static_path("/service_worker.js", sw_path, False)
|
||||
hass.http.register_static_path(
|
||||
"/service_worker_es5.js", sw_path_es5, False)
|
||||
hass.http.register_static_path(
|
||||
"/service_worker.js", sw_path_latest, False)
|
||||
hass.http.register_static_path(
|
||||
"/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev)
|
||||
hass.http.register_static_path("/static", static_path, not is_dev)
|
||||
hass.http.register_static_path(
|
||||
"/frontend_latest", frontend_latest_path, not is_dev)
|
||||
hass.http.register_static_path(
|
||||
"/frontend_es5", frontend_es5_path, not is_dev)
|
||||
|
||||
local = hass.config.path('www')
|
||||
if os.path.isdir(local):
|
||||
hass.http.register_static_path("/local", local, not is_dev)
|
||||
|
||||
index_view = IndexView(is_dev)
|
||||
index_view = IndexView(repo_path, js_version)
|
||||
hass.http.register_view(index_view)
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -405,40 +434,40 @@ class IndexView(HomeAssistantView):
|
||||
requires_auth = False
|
||||
extra_urls = ['/states', '/states/{extra}']
|
||||
|
||||
def __init__(self, use_repo):
|
||||
def __init__(self, repo_path, js_option):
|
||||
"""Initialize the frontend view."""
|
||||
from jinja2 import FileSystemLoader, Environment
|
||||
self.repo_path = repo_path
|
||||
self.js_option = js_option
|
||||
self._template_cache = {}
|
||||
|
||||
self.use_repo = use_repo
|
||||
self.templates = Environment(
|
||||
autoescape=True,
|
||||
loader=FileSystemLoader(
|
||||
os.path.join(os.path.dirname(__file__), 'templates/')
|
||||
)
|
||||
)
|
||||
def get_template(self, latest):
|
||||
"""Get template."""
|
||||
if self.repo_path is not None:
|
||||
root = self.repo_path
|
||||
elif latest:
|
||||
import hass_frontend
|
||||
root = hass_frontend.where()
|
||||
else:
|
||||
import hass_frontend_es5
|
||||
root = hass_frontend_es5.where()
|
||||
|
||||
tpl = self._template_cache.get(root)
|
||||
|
||||
if tpl is None:
|
||||
with open(os.path.join(root, 'index.html')) as file:
|
||||
tpl = jinja2.Template(file.read())
|
||||
|
||||
# Cache template if not running from repository
|
||||
if self.repo_path is None:
|
||||
self._template_cache[root] = tpl
|
||||
|
||||
return tpl
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request, extra=None):
|
||||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if self.use_repo:
|
||||
core_url = '/home-assistant-polymer/build/core.js'
|
||||
compatibility_url = \
|
||||
'/home-assistant-polymer/build/compatibility.js'
|
||||
ui_url = '/home-assistant-polymer/src/home-assistant.html'
|
||||
icons_fp = ''
|
||||
icons_url = '/static/mdi.html'
|
||||
else:
|
||||
import hass_frontend
|
||||
core_url = '/static/core-{}.js'.format(
|
||||
hass_frontend.FINGERPRINTS['core.js'])
|
||||
compatibility_url = '/static/compatibility-{}.js'.format(
|
||||
hass_frontend.FINGERPRINTS['compatibility.js'])
|
||||
ui_url = '/static/frontend-{}.html'.format(
|
||||
hass_frontend.FINGERPRINTS['frontend.html'])
|
||||
icons_fp = '-{}'.format(hass_frontend.FINGERPRINTS['mdi.html'])
|
||||
icons_url = '/static/mdi{}.html'.format(icons_fp)
|
||||
latest = _is_latest(self.js_option, request)
|
||||
|
||||
if request.path == '/':
|
||||
panel = 'states'
|
||||
@ -447,28 +476,27 @@ class IndexView(HomeAssistantView):
|
||||
|
||||
if panel == 'states':
|
||||
panel_url = ''
|
||||
elif latest:
|
||||
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_latest
|
||||
else:
|
||||
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url
|
||||
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
|
||||
|
||||
no_auth = 'true'
|
||||
if hass.config.api.api_password and not is_trusted_ip(request):
|
||||
# do not try to auto connect on load
|
||||
no_auth = 'false'
|
||||
|
||||
template = yield from hass.async_add_job(
|
||||
self.templates.get_template, 'index.html')
|
||||
template = yield from hass.async_add_job(self.get_template, latest)
|
||||
|
||||
# pylint is wrong
|
||||
# pylint: disable=no-member
|
||||
# This is a jinja2 template, not a HA template so we call 'render'.
|
||||
resp = template.render(
|
||||
core_url=core_url, ui_url=ui_url,
|
||||
compatibility_url=compatibility_url, no_auth=no_auth,
|
||||
icons_url=icons_url, icons=icons_fp,
|
||||
panel_url=panel_url, panels=hass.data[DATA_PANELS],
|
||||
dev_mode=self.use_repo,
|
||||
no_auth=no_auth,
|
||||
panel_url=panel_url,
|
||||
panels=hass.data[DATA_PANELS],
|
||||
dev_mode=self.repo_path is not None,
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[DATA_EXTRA_HTML_URL])
|
||||
extra_urls=hass.data[DATA_EXTRA_HTML_URL],
|
||||
latest=latest,
|
||||
)
|
||||
|
||||
return web.Response(text=resp, content_type='text/html')
|
||||
|
||||
@ -483,8 +511,8 @@ class ManifestJSONView(HomeAssistantView):
|
||||
@asyncio.coroutine
|
||||
def get(self, request): # pylint: disable=no-self-use
|
||||
"""Return the manifest.json."""
|
||||
msg = json.dumps(MANIFEST_JSON, sort_keys=True).encode('UTF-8')
|
||||
return web.Response(body=msg, content_type="application/manifest+json")
|
||||
msg = json.dumps(MANIFEST_JSON, sort_keys=True)
|
||||
return web.Response(text=msg, content_type="application/manifest+json")
|
||||
|
||||
|
||||
class ThemesView(HomeAssistantView):
|
||||
@ -509,3 +537,20 @@ def _fingerprint(path):
|
||||
"""Fingerprint a file."""
|
||||
with open(path) as fil:
|
||||
return hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def _is_latest(js_option, request):
|
||||
"""
|
||||
Return whether we should serve latest untranspiled code.
|
||||
|
||||
Set according to user's preference and URL override.
|
||||
"""
|
||||
if request is None:
|
||||
return js_option == 'latest'
|
||||
latest_in_query = 'latest' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'latest' in urlparse(request.headers['Referer']).query)
|
||||
es5_in_query = 'es5' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'es5' in urlparse(request.headers['Referer']).query)
|
||||
return latest_in_query or (not es5_in_query and js_option == 'latest')
|
||||
|
@ -1,118 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Home Assistant</title>
|
||||
|
||||
<link rel='manifest' href='/manifest.json'>
|
||||
<link rel='icon' href='/static/icons/favicon.ico'>
|
||||
<link rel='apple-touch-icon' sizes='180x180'
|
||||
href='/static/icons/favicon-apple-180x180.png'>
|
||||
<link rel="mask-icon" href="/static/icons/mask-icon.svg" color="#3fbbf4">
|
||||
{% if not dev_mode %}
|
||||
<link rel='preload' href='{{ core_url }}' as='script'/>
|
||||
{% for panel in panels.values() -%}
|
||||
<link rel='prefetch' href='{{ panel.webcomponent_url }}'>
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<meta name="msapplication-square70x70logo" content="/static/icons/tile-win-70x70.png"/>
|
||||
<meta name="msapplication-square150x150logo" content="/static/icons/tile-win-150x150.png"/>
|
||||
<meta name="msapplication-wide310x150logo" content="/static/icons/tile-win-310x150.png"/>
|
||||
<meta name="msapplication-square310x310logo" content="/static/icons/tile-win-310x310.png"/>
|
||||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='{{ theme_color }}'>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#ha-init-skeleton::before {
|
||||
display: block;
|
||||
content: "";
|
||||
height: 48px;
|
||||
background-color: {{ theme_color }};
|
||||
}
|
||||
|
||||
#ha-init-skeleton .message {
|
||||
transition: font-size 2s;
|
||||
font-size: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton.error .message {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#ha-init-skeleton a {
|
||||
color: {{ theme_color }};
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function initError() {
|
||||
document.getElementById('ha-init-skeleton').classList.add('error');
|
||||
};
|
||||
window.noAuth = {{ no_auth }};
|
||||
window.Polymer = {
|
||||
lazyRegister: true,
|
||||
useNativeCSSProperties: true,
|
||||
dom: 'shadow',
|
||||
suppressTemplateNotifications: true,
|
||||
suppressBindingNotifications: true,
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='ha-init-skeleton'>
|
||||
<div class='message'>
|
||||
Home Assistant had trouble<br>connecting to the server.<br><br>
|
||||
<a href='/'>TRY AGAIN</a>
|
||||
</div>
|
||||
</div>
|
||||
<home-assistant icons='{{ icons }}'></home-assistant>
|
||||
{# <script src='/static/home-assistant-polymer/build/_demo_data_compiled.js'></script> #}
|
||||
<script>
|
||||
var compatibilityRequired = (
|
||||
typeof Object.assign != 'function');
|
||||
if (compatibilityRequired) {
|
||||
var e = document.createElement('script');
|
||||
e.onerror = initError;
|
||||
e.src = '{{ compatibility_url }}';
|
||||
document.head.appendChild(e);
|
||||
}
|
||||
</script>
|
||||
<script src='{{ core_url }}'></script>
|
||||
{% if not dev_mode %}
|
||||
<script src='/static/custom-elements-es5-adapter.js'></script>
|
||||
{% endif %}
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'customElements' in window &&
|
||||
'import' in document.createElement('link') &&
|
||||
'content' in document.createElement('template'));
|
||||
if (!webComponentsSupported) {
|
||||
var e = document.createElement('script');
|
||||
e.onerror = initError;
|
||||
e.src = '/static/webcomponents-lite.js';
|
||||
document.head.appendChild(e);
|
||||
}
|
||||
</script>
|
||||
<link rel='import' href='{{ ui_url }}' onerror='initError()'>
|
||||
{% if panel_url -%}
|
||||
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
|
||||
{% endif -%}
|
||||
<link rel='import' href='{{ icons_url }}' async>
|
||||
{% for extra_url in extra_urls -%}
|
||||
<link rel='import' href='{{ extra_url }}' async>
|
||||
{% endfor -%}
|
||||
</body>
|
||||
</html>
|
@ -12,7 +12,7 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['python-gc100==1.0.1a']
|
||||
REQUIREMENTS = ['python-gc100==1.0.3a']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -42,7 +42,7 @@ def setup(hass, base_config):
|
||||
|
||||
gc_device = gc100.GC100SocketClient(host, port)
|
||||
|
||||
def cleanup_gc100():
|
||||
def cleanup_gc100(event):
|
||||
"""Stuff to do before stopping."""
|
||||
gc_device.quit()
|
||||
|
||||
|
@ -4,9 +4,13 @@ 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 os
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
# Typing imports
|
||||
@ -15,11 +19,16 @@ import voluptuous as vol
|
||||
from homeassistant.core import HomeAssistant # NOQA
|
||||
from typing import Dict, Any # NOQA
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
DOMAIN, CONF_PROJECT_ID, CONF_CLIENT_ID, CONF_ACCESS_TOKEN,
|
||||
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS
|
||||
CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS,
|
||||
CONF_AGENT_USER_ID, CONF_API_KEY,
|
||||
SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL
|
||||
)
|
||||
from .auth import GoogleAssistantAuthView
|
||||
from .http import GoogleAssistantView
|
||||
@ -28,6 +37,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
DEFAULT_AGENT_USER_ID = 'home-assistant'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: {
|
||||
@ -36,17 +47,57 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_ACCESS_TOKEN): cv.string,
|
||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_AGENT_USER_ID,
|
||||
default=DEFAULT_AGENT_USER_ID): cv.string,
|
||||
vol.Optional(CONF_API_KEY): cv.string
|
||||
}
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def request_sync(hass):
|
||||
"""Request sync."""
|
||||
hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
||||
"""Activate Google Actions component."""
|
||||
config = yaml_config.get(DOMAIN, {})
|
||||
|
||||
agent_user_id = config.get(CONF_AGENT_USER_ID)
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
if api_key is not None:
|
||||
descriptions = yield from hass.async_add_job(
|
||||
conf_util.load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
hass.http.register_view(GoogleAssistantAuthView(hass, config))
|
||||
hass.http.register_view(GoogleAssistantView(hass, config))
|
||||
|
||||
@asyncio.coroutine
|
||||
def request_sync_service_handler(call):
|
||||
"""Handle request sync service calls."""
|
||||
websession = async_get_clientsession(hass)
|
||||
try:
|
||||
with async_timeout.timeout(5, loop=hass.loop):
|
||||
res = yield from websession.post(
|
||||
REQUEST_SYNC_BASE_URL,
|
||||
params={'key': api_key},
|
||||
json={'agent_user_id': agent_user_id})
|
||||
_LOGGER.info("Submitted request_sync request to Google")
|
||||
res.raise_for_status()
|
||||
except aiohttp.ClientResponseError:
|
||||
body = yield from res.read()
|
||||
_LOGGER.error(
|
||||
'request_sync request failed: %d %s', res.status, body)
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
_LOGGER.error("Could not contact Google for request_sync")
|
||||
|
||||
# Register service only if api key is provided
|
||||
if api_key is not None:
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler,
|
||||
descriptions.get(SERVICE_REQUEST_SYNC))
|
||||
|
||||
return True
|
||||
|
@ -13,6 +13,8 @@ CONF_PROJECT_ID = 'project_id'
|
||||
CONF_ACCESS_TOKEN = 'access_token'
|
||||
CONF_CLIENT_ID = 'client_id'
|
||||
CONF_ALIASES = 'aliases'
|
||||
CONF_AGENT_USER_ID = 'agent_user_id'
|
||||
CONF_API_KEY = 'api_key'
|
||||
|
||||
DEFAULT_EXPOSE_BY_DEFAULT = True
|
||||
DEFAULT_EXPOSED_DOMAINS = [
|
||||
@ -44,3 +46,7 @@ TYPE_LIGHT = PREFIX_TYPES + 'LIGHT'
|
||||
TYPE_SWITCH = PREFIX_TYPES + 'SWITCH'
|
||||
TYPE_SCENE = PREFIX_TYPES + 'SCENE'
|
||||
TYPE_THERMOSTAT = PREFIX_TYPES + 'THERMOSTAT'
|
||||
|
||||
SERVICE_REQUEST_SYNC = 'request_sync'
|
||||
HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
|
||||
REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync'
|
||||
|
@ -7,17 +7,18 @@ https://home-assistant.io/components/google_assistant/
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from typing import Any, Dict # NOQA
|
||||
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
from aiohttp.web import Request, Response # NOQA
|
||||
|
||||
# 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 homeassistant.const import HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED
|
||||
from homeassistant.core import HomeAssistant # NOQA
|
||||
from homeassistant.helpers.entity import Entity # NOQA
|
||||
|
||||
from .const import (
|
||||
GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
@ -26,7 +27,9 @@ from .const import (
|
||||
DEFAULT_EXPOSED_DOMAINS,
|
||||
CONF_EXPOSE_BY_DEFAULT,
|
||||
CONF_EXPOSED_DOMAINS,
|
||||
ATTR_GOOGLE_ASSISTANT)
|
||||
ATTR_GOOGLE_ASSISTANT,
|
||||
CONF_AGENT_USER_ID
|
||||
)
|
||||
from .smart_home import entity_to_device, query_device, determine_service
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -48,6 +51,7 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
DEFAULT_EXPOSE_BY_DEFAULT)
|
||||
self.exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS,
|
||||
DEFAULT_EXPOSED_DOMAINS)
|
||||
self.agent_user_id = cfg.get(CONF_AGENT_USER_ID)
|
||||
|
||||
def is_entity_exposed(self, entity) -> bool:
|
||||
"""Determine if an entity should be exposed to Google Assistant."""
|
||||
@ -77,7 +81,7 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
if not self.is_entity_exposed(entity):
|
||||
continue
|
||||
|
||||
device = entity_to_device(entity)
|
||||
device = entity_to_device(entity, hass.config.units)
|
||||
if device is None:
|
||||
_LOGGER.warning("No mapping for %s domain", entity.domain)
|
||||
continue
|
||||
@ -85,7 +89,9 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
devices.append(device)
|
||||
|
||||
return self.json(
|
||||
make_actions_response(request_id, {'devices': devices}))
|
||||
_make_actions_response(request_id,
|
||||
{'agentUserId': self.agent_user_id,
|
||||
'devices': devices}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_query(self,
|
||||
@ -106,10 +112,10 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
# If we can't find a state, the device is offline
|
||||
devices[devid] = {'online': False}
|
||||
|
||||
devices[devid] = query_device(state)
|
||||
devices[devid] = query_device(state, hass.config.units)
|
||||
|
||||
return self.json(
|
||||
make_actions_response(request_id, {'devices': devices}))
|
||||
_make_actions_response(request_id, {'devices': devices}))
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_execute(self,
|
||||
@ -122,9 +128,11 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
ent_ids = [ent.get('id') for ent in command.get('devices', [])]
|
||||
execution = command.get('execution')[0]
|
||||
for eid in ent_ids:
|
||||
success = False
|
||||
domain = eid.split('.')[0]
|
||||
(service, service_data) = determine_service(
|
||||
eid, execution.get('command'), execution.get('params'))
|
||||
eid, execution.get('command'), execution.get('params'),
|
||||
hass.config.units)
|
||||
success = yield from hass.services.async_call(
|
||||
domain, service, service_data, blocking=True)
|
||||
result = {"ids": [eid], "states": {}}
|
||||
@ -135,12 +143,12 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
commands.append(result)
|
||||
|
||||
return self.json(
|
||||
make_actions_response(request_id, {'commands': commands}))
|
||||
_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)
|
||||
auth = request.headers.get(AUTHORIZATION, None)
|
||||
if 'Bearer {}'.format(self.access_token) != auth:
|
||||
return self.json_message(
|
||||
"missing authorization", status_code=HTTP_UNAUTHORIZED)
|
||||
@ -175,6 +183,5 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
"invalid intent", status_code=HTTP_BAD_REQUEST)
|
||||
|
||||
|
||||
def make_actions_response(request_id: str, payload: dict) -> dict:
|
||||
"""Helper to simplify format for response."""
|
||||
def _make_actions_response(request_id: str, payload: dict) -> dict:
|
||||
return {'requestId': request_id, 'payload': payload}
|
||||
|
2
homeassistant/components/google_assistant/services.yaml
Normal file
2
homeassistant/components/google_assistant/services.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
request_sync:
|
||||
description: Send a request_sync command to Google.
|
@ -5,21 +5,26 @@ import logging
|
||||
# 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 typing import Dict, Tuple, Any, Optional # NOQA
|
||||
from homeassistant.helpers.entity import Entity # NOQA
|
||||
from homeassistant.core import HomeAssistant # NOQA
|
||||
from homeassistant.util import color
|
||||
from homeassistant.util.unit_system import UnitSystem # NOQA
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID,
|
||||
CONF_FRIENDLY_NAME, STATE_OFF,
|
||||
SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.components import (
|
||||
switch, light, cover, media_player, group, fan, scene, script, climate
|
||||
)
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import (
|
||||
ATTR_GOOGLE_ASSISTANT_NAME, ATTR_GOOGLE_ASSISTANT_TYPE,
|
||||
ATTR_GOOGLE_ASSISTANT_NAME, COMMAND_COLOR,
|
||||
ATTR_GOOGLE_ASSISTANT_TYPE,
|
||||
COMMAND_BRIGHTNESS, COMMAND_ONOFF, COMMAND_ACTIVATESCENE,
|
||||
COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
|
||||
COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, COMMAND_THERMOSTAT_SET_MODE,
|
||||
@ -65,7 +70,7 @@ def make_actions_response(request_id: str, payload: dict) -> dict:
|
||||
return {'requestId': request_id, 'payload': payload}
|
||||
|
||||
|
||||
def entity_to_device(entity: Entity):
|
||||
def entity_to_device(entity: Entity, units: UnitSystem):
|
||||
"""Convert a hass entity into an google actions device."""
|
||||
class_data = MAPPING_COMPONENT.get(
|
||||
entity.attributes.get(ATTR_GOOGLE_ASSISTANT_TYPE) or entity.domain)
|
||||
@ -75,6 +80,7 @@ def entity_to_device(entity: Entity):
|
||||
device = {
|
||||
'id': entity.entity_id,
|
||||
'name': {},
|
||||
'attributes': {},
|
||||
'traits': [],
|
||||
'willReportState': False,
|
||||
}
|
||||
@ -99,20 +105,62 @@ def entity_to_device(entity: Entity):
|
||||
for feature, trait in class_data[2].items():
|
||||
if feature & supported > 0:
|
||||
device['traits'].append(trait)
|
||||
|
||||
# Actions require this attributes for a device
|
||||
# supporting temperature
|
||||
# For IKEA trådfri, these attributes only seem to
|
||||
# be set only if the device is on?
|
||||
if trait == TRAIT_COLOR_TEMP:
|
||||
if entity.attributes.get(
|
||||
light.ATTR_MAX_MIREDS) is not None:
|
||||
device['attributes']['temperatureMinK'] = \
|
||||
int(round(color.color_temperature_mired_to_kelvin(
|
||||
entity.attributes.get(light.ATTR_MAX_MIREDS))))
|
||||
if entity.attributes.get(
|
||||
light.ATTR_MIN_MIREDS) is not None:
|
||||
device['attributes']['temperatureMaxK'] = \
|
||||
int(round(color.color_temperature_mired_to_kelvin(
|
||||
entity.attributes.get(light.ATTR_MIN_MIREDS))))
|
||||
|
||||
if entity.domain == climate.DOMAIN:
|
||||
modes = ','.join(
|
||||
m for m in entity.attributes.get(climate.ATTR_OPERATION_LIST, [])
|
||||
if m in CLIMATE_SUPPORTED_MODES)
|
||||
device['attributes'] = {
|
||||
'availableThermostatModes': modes,
|
||||
'thermostatTemperatureUnit': 'C',
|
||||
'thermostatTemperatureUnit':
|
||||
'F' if units.temperature_unit == TEMP_FAHRENHEIT else 'C',
|
||||
}
|
||||
|
||||
return device
|
||||
|
||||
|
||||
def query_device(entity: Entity) -> dict:
|
||||
def query_device(entity: Entity, units: UnitSystem) -> dict:
|
||||
"""Take an entity and return a properly formatted device object."""
|
||||
def celsius(deg: Optional[float]) -> Optional[float]:
|
||||
"""Convert a float to Celsius and rounds to one decimal place."""
|
||||
if deg is None:
|
||||
return None
|
||||
return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
|
||||
if entity.domain == climate.DOMAIN:
|
||||
mode = entity.attributes.get(climate.ATTR_OPERATION_MODE)
|
||||
if mode not in CLIMATE_SUPPORTED_MODES:
|
||||
mode = 'on'
|
||||
response = {
|
||||
'thermostatMode': mode,
|
||||
'thermostatTemperatureSetpoint':
|
||||
celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)),
|
||||
'thermostatTemperatureAmbient':
|
||||
celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)),
|
||||
'thermostatTemperatureSetpointHigh':
|
||||
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)),
|
||||
'thermostatTemperatureSetpointLow':
|
||||
celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)),
|
||||
'thermostatHumidityAmbient':
|
||||
entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY),
|
||||
}
|
||||
return {k: v for k, v in response.items() if v is not None}
|
||||
|
||||
final_state = entity.state != STATE_OFF
|
||||
final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
|
||||
if final_state else 0)
|
||||
@ -128,18 +176,42 @@ def query_device(entity: Entity) -> dict:
|
||||
|
||||
final_brightness = 100 * (final_brightness / 255)
|
||||
|
||||
return {
|
||||
query_response = {
|
||||
"on": final_state,
|
||||
"online": True,
|
||||
"brightness": int(final_brightness)
|
||||
}
|
||||
|
||||
supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if supported_features & \
|
||||
(light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR):
|
||||
query_response["color"] = {}
|
||||
|
||||
if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None:
|
||||
query_response["color"]["temperature"] = \
|
||||
int(round(color.color_temperature_mired_to_kelvin(
|
||||
entity.attributes.get(light.ATTR_COLOR_TEMP))))
|
||||
|
||||
if entity.attributes.get(light.ATTR_COLOR_NAME) is not None:
|
||||
query_response["color"]["name"] = \
|
||||
entity.attributes.get(light.ATTR_COLOR_NAME)
|
||||
|
||||
if entity.attributes.get(light.ATTR_RGB_COLOR) is not None:
|
||||
color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR)
|
||||
if color_rgb is not None:
|
||||
query_response["color"]["spectrumRGB"] = \
|
||||
int(color.color_rgb_to_hex(
|
||||
color_rgb[0], color_rgb[1], color_rgb[2]), 16)
|
||||
|
||||
return query_response
|
||||
|
||||
|
||||
# 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]:
|
||||
def determine_service(
|
||||
entity_id: str, command: str, params: dict,
|
||||
units: UnitSystem) -> Tuple[str, dict]:
|
||||
"""
|
||||
Determine service and service_data.
|
||||
|
||||
@ -166,14 +238,17 @@ def determine_service(entity_id: str, command: str,
|
||||
# special climate handling
|
||||
if domain == climate.DOMAIN:
|
||||
if command == COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT:
|
||||
service_data['temperature'] = params.get(
|
||||
'thermostatTemperatureSetpoint', 25)
|
||||
service_data['temperature'] = units.temperature(
|
||||
params.get('thermostatTemperatureSetpoint', 25),
|
||||
TEMP_CELSIUS)
|
||||
return (climate.SERVICE_SET_TEMPERATURE, service_data)
|
||||
if command == COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE:
|
||||
service_data['target_temp_high'] = params.get(
|
||||
'thermostatTemperatureSetpointHigh', 25)
|
||||
service_data['target_temp_low'] = params.get(
|
||||
'thermostatTemperatureSetpointLow', 18)
|
||||
service_data['target_temp_high'] = units.temperature(
|
||||
params.get('thermostatTemperatureSetpointHigh', 25),
|
||||
TEMP_CELSIUS)
|
||||
service_data['target_temp_low'] = units.temperature(
|
||||
params.get('thermostatTemperatureSetpointLow', 18),
|
||||
TEMP_CELSIUS)
|
||||
return (climate.SERVICE_SET_TEMPERATURE, service_data)
|
||||
if command == COMMAND_THERMOSTAT_SET_MODE:
|
||||
service_data['operation_mode'] = params.get(
|
||||
@ -185,7 +260,27 @@ def determine_service(entity_id: str, command: str,
|
||||
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):
|
||||
_LOGGER.debug("Handling command %s with data %s", command, params)
|
||||
if command == COMMAND_COLOR:
|
||||
color_data = params.get('color')
|
||||
if color_data is not None:
|
||||
if color_data.get('temperature', 0) > 0:
|
||||
service_data[light.ATTR_KELVIN] = color_data.get('temperature')
|
||||
return (SERVICE_TURN_ON, service_data)
|
||||
if color_data.get('spectrumRGB', 0) > 0:
|
||||
# blue is 255 so pad up to 6 chars
|
||||
hex_value = \
|
||||
('%0x' % int(color_data.get('spectrumRGB'))).zfill(6)
|
||||
service_data[light.ATTR_RGB_COLOR] = \
|
||||
color.rgb_hex_to_rgb_list(hex_value)
|
||||
return (SERVICE_TURN_ON, service_data)
|
||||
|
||||
if command == COMMAND_ACTIVATESCENE:
|
||||
return (SERVICE_TURN_ON, service_data)
|
||||
return (SERVICE_TURN_OFF, service_data)
|
||||
|
||||
if COMMAND_ONOFF == command:
|
||||
if params.get('on') is True:
|
||||
return (SERVICE_TURN_ON, service_data)
|
||||
return (SERVICE_TURN_OFF, service_data)
|
||||
|
||||
return (None, service_data)
|
||||
|
@ -49,7 +49,7 @@ NO_TIMEOUT = {
|
||||
}
|
||||
|
||||
NO_AUTH = {
|
||||
re.compile(r'^panel$'), re.compile(r'^addons/[^/]*/logo$')
|
||||
re.compile(r'^panel_(es5|latest)$'), re.compile(r'^addons/[^/]*/logo$')
|
||||
}
|
||||
|
||||
SCHEMA_ADDON = vol.Schema({
|
||||
|
@ -5,37 +5,43 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/http/
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
from functools import wraps
|
||||
import logging
|
||||
import ssl
|
||||
from ipaddress import ip_network
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import voluptuous as vol
|
||||
from aiohttp import web
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
|
||||
import ssl
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE
|
||||
from aiohttp.web_exceptions import HTTPUnauthorized, HTTPMovedPermanently
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, HTTP_HEADER_HA_AUTH,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
HTTP_HEADER_X_REQUESTED_WITH)
|
||||
from homeassistant.core import is_callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.remote as rem
|
||||
import homeassistant.util as hass_util
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, ALLOWED_CORS_HEADERS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import is_callback
|
||||
from homeassistant.util.logging import HideSensitiveDataFilter
|
||||
|
||||
from .auth import auth_middleware
|
||||
from .ban import ban_middleware
|
||||
from .const import (
|
||||
KEY_USE_X_FORWARDED_FOR, KEY_TRUSTED_NETWORKS, KEY_BANS_ENABLED,
|
||||
KEY_LOGIN_THRESHOLD, KEY_AUTHENTICATED)
|
||||
KEY_BANS_ENABLED, KEY_AUTHENTICATED, KEY_LOGIN_THRESHOLD,
|
||||
KEY_TRUSTED_NETWORKS, KEY_USE_X_FORWARDED_FOR)
|
||||
from .static import (
|
||||
staticresource_middleware, CachingFileResponse, CachingStaticResource)
|
||||
CachingFileResponse, CachingStaticResource, staticresource_middleware)
|
||||
from .util import get_real_ip
|
||||
|
||||
REQUIREMENTS = ['aiohttp_cors==0.5.3']
|
||||
|
||||
ALLOWED_CORS_HEADERS = [
|
||||
ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE,
|
||||
HTTP_HEADER_HA_AUTH]
|
||||
|
||||
DOMAIN = 'http'
|
||||
|
||||
CONF_API_PASSWORD = 'api_password'
|
||||
@ -176,8 +182,6 @@ class HomeAssistantWSGI(object):
|
||||
use_x_forwarded_for, trusted_networks,
|
||||
login_threshold, is_ban_enabled):
|
||||
"""Initialize the WSGI Home Assistant server."""
|
||||
import aiohttp_cors
|
||||
|
||||
middlewares = [auth_middleware, staticresource_middleware]
|
||||
|
||||
if is_ban_enabled:
|
||||
@ -200,6 +204,8 @@ class HomeAssistantWSGI(object):
|
||||
self.server = None
|
||||
|
||||
if cors_origins:
|
||||
import aiohttp_cors
|
||||
|
||||
self.cors = aiohttp_cors.setup(self.app, defaults={
|
||||
host: aiohttp_cors.ResourceOptions(
|
||||
allow_headers=ALLOWED_CORS_HEADERS,
|
||||
@ -256,7 +262,6 @@ class HomeAssistantWSGI(object):
|
||||
resource = CachingStaticResource
|
||||
else:
|
||||
resource = web.StaticResource
|
||||
|
||||
self.app.router.register_resource(resource(url_path, path))
|
||||
return
|
||||
|
||||
@ -329,7 +334,9 @@ class HomeAssistantWSGI(object):
|
||||
_LOGGER.error("Failed to create HTTP server at port %d: %s",
|
||||
self.server_port, error)
|
||||
|
||||
self.app._frozen = False # pylint: disable=protected-access
|
||||
# pylint: disable=protected-access
|
||||
self.app._middlewares = tuple(self.app._prepare_middleware())
|
||||
self.app._frozen = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def stop(self):
|
||||
@ -339,7 +346,7 @@ class HomeAssistantWSGI(object):
|
||||
yield from self.server.wait_closed()
|
||||
yield from self.app.shutdown()
|
||||
if self._handler:
|
||||
yield from self._handler.finish_connections(60.0)
|
||||
yield from self._handler.shutdown(10)
|
||||
yield from self.app.cleanup()
|
||||
|
||||
|
||||
|
@ -5,6 +5,7 @@ import hmac
|
||||
import logging
|
||||
|
||||
from aiohttp import hdrs
|
||||
from aiohttp.web import middleware
|
||||
|
||||
from homeassistant.const import HTTP_HEADER_HA_AUTH
|
||||
from .util import get_real_ip
|
||||
@ -15,47 +16,37 @@ DATA_API_PASSWORD = 'api_password'
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@middleware
|
||||
@asyncio.coroutine
|
||||
def auth_middleware(app, handler):
|
||||
def auth_middleware(request, handler):
|
||||
"""Authenticate as middleware."""
|
||||
# If no password set, just always set authenticated=True
|
||||
if app['hass'].http.api_password is None:
|
||||
@asyncio.coroutine
|
||||
def no_auth_middleware_handler(request):
|
||||
"""Auth middleware to approve all requests."""
|
||||
request[KEY_AUTHENTICATED] = True
|
||||
return handler(request)
|
||||
|
||||
return no_auth_middleware_handler
|
||||
|
||||
@asyncio.coroutine
|
||||
def auth_middleware_handler(request):
|
||||
"""Auth middleware to check authentication."""
|
||||
# Auth code verbose on purpose
|
||||
authenticated = False
|
||||
|
||||
if (HTTP_HEADER_HA_AUTH in request.headers and
|
||||
validate_password(
|
||||
request, request.headers[HTTP_HEADER_HA_AUTH])):
|
||||
# A valid auth header has been set
|
||||
authenticated = True
|
||||
|
||||
elif (DATA_API_PASSWORD in request.query and
|
||||
validate_password(request, request.query[DATA_API_PASSWORD])):
|
||||
authenticated = True
|
||||
|
||||
elif (hdrs.AUTHORIZATION in request.headers and
|
||||
validate_authorization_header(request)):
|
||||
authenticated = True
|
||||
|
||||
elif is_trusted_ip(request):
|
||||
authenticated = True
|
||||
|
||||
request[KEY_AUTHENTICATED] = authenticated
|
||||
|
||||
if request.app['hass'].http.api_password is None:
|
||||
request[KEY_AUTHENTICATED] = True
|
||||
return handler(request)
|
||||
|
||||
return auth_middleware_handler
|
||||
# Check authentication
|
||||
authenticated = False
|
||||
|
||||
if (HTTP_HEADER_HA_AUTH in request.headers and
|
||||
validate_password(
|
||||
request, request.headers[HTTP_HEADER_HA_AUTH])):
|
||||
# A valid auth header has been set
|
||||
authenticated = True
|
||||
|
||||
elif (DATA_API_PASSWORD in request.query and
|
||||
validate_password(request, request.query[DATA_API_PASSWORD])):
|
||||
authenticated = True
|
||||
|
||||
elif (hdrs.AUTHORIZATION in request.headers and
|
||||
validate_authorization_header(request)):
|
||||
authenticated = True
|
||||
|
||||
elif is_trusted_ip(request):
|
||||
authenticated = True
|
||||
|
||||
request[KEY_AUTHENTICATED] = authenticated
|
||||
return handler(request)
|
||||
|
||||
|
||||
def is_trusted_ip(request):
|
||||
|
@ -6,6 +6,7 @@ from ipaddress import ip_address
|
||||
import logging
|
||||
import os
|
||||
|
||||
from aiohttp.web import middleware
|
||||
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
|
||||
import voluptuous as vol
|
||||
|
||||
@ -32,35 +33,32 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
@middleware
|
||||
@asyncio.coroutine
|
||||
def ban_middleware(app, handler):
|
||||
def ban_middleware(request, handler):
|
||||
"""IP Ban middleware."""
|
||||
if not app[KEY_BANS_ENABLED]:
|
||||
return handler
|
||||
if not request.app[KEY_BANS_ENABLED]:
|
||||
return (yield from handler(request))
|
||||
|
||||
if KEY_BANNED_IPS not in app:
|
||||
hass = app['hass']
|
||||
app[KEY_BANNED_IPS] = yield from hass.async_add_job(
|
||||
if KEY_BANNED_IPS not in request.app:
|
||||
hass = request.app['hass']
|
||||
request.app[KEY_BANNED_IPS] = yield from hass.async_add_job(
|
||||
load_ip_bans_config, hass.config.path(IP_BANS_FILE))
|
||||
|
||||
@asyncio.coroutine
|
||||
def ban_middleware_handler(request):
|
||||
"""Verify if IP is not banned."""
|
||||
ip_address_ = get_real_ip(request)
|
||||
# Verify if IP is not banned
|
||||
ip_address_ = get_real_ip(request)
|
||||
|
||||
is_banned = any(ip_ban.ip_address == ip_address_
|
||||
for ip_ban in request.app[KEY_BANNED_IPS])
|
||||
is_banned = any(ip_ban.ip_address == ip_address_
|
||||
for ip_ban in request.app[KEY_BANNED_IPS])
|
||||
|
||||
if is_banned:
|
||||
raise HTTPForbidden()
|
||||
if is_banned:
|
||||
raise HTTPForbidden()
|
||||
|
||||
try:
|
||||
return (yield from handler(request))
|
||||
except HTTPUnauthorized:
|
||||
yield from process_wrong_login(request)
|
||||
raise
|
||||
|
||||
return ban_middleware_handler
|
||||
try:
|
||||
return (yield from handler(request))
|
||||
except HTTPUnauthorized:
|
||||
yield from process_wrong_login(request)
|
||||
raise
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -3,7 +3,7 @@ import asyncio
|
||||
import re
|
||||
|
||||
from aiohttp import hdrs
|
||||
from aiohttp.web import FileResponse
|
||||
from aiohttp.web import FileResponse, middleware
|
||||
from aiohttp.web_exceptions import HTTPNotFound
|
||||
from aiohttp.web_urldispatcher import StaticResource
|
||||
from yarl import unquote
|
||||
@ -61,21 +61,18 @@ class CachingFileResponse(FileResponse):
|
||||
self._sendfile = sendfile
|
||||
|
||||
|
||||
@middleware
|
||||
@asyncio.coroutine
|
||||
def staticresource_middleware(app, handler):
|
||||
def staticresource_middleware(request, handler):
|
||||
"""Middleware to strip out fingerprint from fingerprinted assets."""
|
||||
@asyncio.coroutine
|
||||
def static_middleware_handler(request):
|
||||
"""Strip out fingerprints from resource names."""
|
||||
if not request.path.startswith('/static/'):
|
||||
return handler(request)
|
||||
|
||||
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
|
||||
|
||||
if fingerprinted:
|
||||
request.match_info['filename'] = \
|
||||
'{}.{}'.format(*fingerprinted.groups())
|
||||
|
||||
path = request.path
|
||||
if not path.startswith('/static/') and not path.startswith('/frontend'):
|
||||
return handler(request)
|
||||
|
||||
return static_middleware_handler
|
||||
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
|
||||
|
||||
if fingerprinted:
|
||||
request.match_info['filename'] = \
|
||||
'{}.{}'.format(*fingerprinted.groups())
|
||||
|
||||
return handler(request)
|
||||
|
@ -5,9 +5,9 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/influxdb/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import re
|
||||
|
||||
import requests.exceptions
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
@ -123,10 +123,12 @@ def setup(hass, config):
|
||||
try:
|
||||
influx = InfluxDBClient(**kwargs)
|
||||
influx.query("SHOW SERIES LIMIT 1;", database=conf[CONF_DB_NAME])
|
||||
except exceptions.InfluxDBClientError as exc:
|
||||
except (exceptions.InfluxDBClientError,
|
||||
requests.exceptions.ConnectionError) as exc:
|
||||
_LOGGER.error("Database host is not accessible due to '%s', please "
|
||||
"check your entries in the configuration file and that "
|
||||
"the database exists and is READ/WRITE.", exc)
|
||||
"check your entries in the configuration file (host, "
|
||||
"port, etc.) and verify that the database exists and is "
|
||||
"READ/WRITE.", exc)
|
||||
return False
|
||||
|
||||
def influx_event_listener(event):
|
||||
|
@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HAS_DATE): cv.boolean,
|
||||
vol.Required(CONF_HAS_TIME): cv.boolean,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL): cv.datetime,
|
||||
vol.Optional(CONF_INITIAL): cv.string,
|
||||
}, cv.has_at_least_one_key_value((CONF_HAS_DATE, True),
|
||||
(CONF_HAS_TIME, True)))})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
@ -137,15 +137,15 @@ class InputDatetime(Entity):
|
||||
old_state = yield from async_get_last_state(self.hass,
|
||||
self.entity_id)
|
||||
if old_state is not None:
|
||||
restore_val = dt_util.parse_datetime(old_state.state)
|
||||
restore_val = old_state.state
|
||||
|
||||
if restore_val is not None:
|
||||
if not self._has_date:
|
||||
self._current_datetime = restore_val.time()
|
||||
self._current_datetime = dt_util.parse_time(restore_val)
|
||||
elif not self._has_time:
|
||||
self._current_datetime = restore_val.date()
|
||||
self._current_datetime = dt_util.parse_date(restore_val)
|
||||
else:
|
||||
self._current_datetime = restore_val
|
||||
self._current_datetime = dt_util.parse_datetime(restore_val)
|
||||
|
||||
def has_date(self):
|
||||
"""Return whether the input datetime carries a date."""
|
||||
|
@ -36,7 +36,7 @@ ATTR_DISCOVER_DEVICES = 'devices'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['xknx==0.7.16']
|
||||
REQUIREMENTS = ['xknx==0.7.18']
|
||||
|
||||
TUNNELING_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
|
@ -38,46 +38,26 @@ def setup(hass, config):
|
||||
conf = config[DOMAIN]
|
||||
hlmn = HassLaMetricManager(client_id=conf[CONF_CLIENT_ID],
|
||||
client_secret=conf[CONF_CLIENT_SECRET])
|
||||
devices = hlmn.manager().get_devices()
|
||||
devices = hlmn.manager.get_devices()
|
||||
if not devices:
|
||||
_LOGGER.error("No LaMetric devices found")
|
||||
return False
|
||||
|
||||
found = False
|
||||
hass.data[DOMAIN] = hlmn
|
||||
for dev in devices:
|
||||
_LOGGER.debug("Discovered LaMetric device: %s", dev)
|
||||
found = True
|
||||
|
||||
return found
|
||||
return True
|
||||
|
||||
|
||||
class HassLaMetricManager():
|
||||
"""
|
||||
A class that encapsulated requests to the LaMetric manager.
|
||||
|
||||
As the original class does not have a re-connect feature that is needed
|
||||
for applications running for a long time as the OAuth tokens expire. This
|
||||
class implements this reconnect() feature.
|
||||
"""
|
||||
"""A class that encapsulated requests to the LaMetric manager."""
|
||||
|
||||
def __init__(self, client_id, client_secret):
|
||||
"""Initialize HassLaMetricManager and connect to LaMetric."""
|
||||
from lmnotify import LaMetricManager
|
||||
|
||||
_LOGGER.debug("Connecting to LaMetric")
|
||||
self.lmn = LaMetricManager(client_id, client_secret)
|
||||
self.manager = LaMetricManager(client_id, client_secret)
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
|
||||
def reconnect(self):
|
||||
"""
|
||||
Reconnect to LaMetric.
|
||||
|
||||
This is usually necessary when the OAuth token is expired.
|
||||
"""
|
||||
from lmnotify import LaMetricManager
|
||||
_LOGGER.debug("Reconnecting to LaMetric")
|
||||
self.lmn = LaMetricManager(self._client_id,
|
||||
self._client_secret)
|
||||
|
||||
def manager(self):
|
||||
"""Return the global LaMetricManager instance."""
|
||||
return self.lmn
|
||||
|
@ -23,7 +23,6 @@ from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.restore_state import async_restore_state
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
DOMAIN = "light"
|
||||
@ -140,14 +139,6 @@ PROFILE_SCHEMA = vol.Schema(
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_info(state):
|
||||
"""Extract light parameters from a state object."""
|
||||
params = {key: state.attributes[key] for key in PROP_TO_ATTR
|
||||
if key in state.attributes}
|
||||
params['is_on'] = state.state == STATE_ON
|
||||
return params
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass, entity_id=None):
|
||||
"""Return if the lights are on based on the statemachine."""
|
||||
@ -431,9 +422,3 @@ class Light(ToggleEntity):
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return 0
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Component added, restore_state using platforms."""
|
||||
if hasattr(self, 'async_restore_state'):
|
||||
yield from async_restore_state(self, extract_info)
|
||||
|
@ -4,7 +4,6 @@ Demo light platform that implements lights.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@ -150,26 +149,3 @@ class DemoLight(Light):
|
||||
# As we have disabled polling, we need to inform
|
||||
# Home Assistant about updates in our state ourselves.
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_restore_state(self, is_on, **kwargs):
|
||||
"""Restore the demo state."""
|
||||
self._state = is_on
|
||||
|
||||
if 'brightness' in kwargs:
|
||||
self._brightness = kwargs['brightness']
|
||||
|
||||
if 'color_temp' in kwargs:
|
||||
self._ct = kwargs['color_temp']
|
||||
|
||||
if 'rgb_color' in kwargs:
|
||||
self._rgb = kwargs['rgb_color']
|
||||
|
||||
if 'xy_color' in kwargs:
|
||||
self._xy_color = kwargs['xy_color']
|
||||
|
||||
if 'white_value' in kwargs:
|
||||
self._white = kwargs['white_value']
|
||||
|
||||
if 'effect' in kwargs:
|
||||
self._effect = kwargs['effect']
|
||||
|
@ -4,6 +4,7 @@ Support for Lutron Caseta lights.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/light.lutron_caseta/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@ -19,7 +20,8 @@ DEPENDENCIES = ['lutron_caseta']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta lights."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
@ -28,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
dev = LutronCasetaLight(light_device, bridge)
|
||||
devs.append(dev)
|
||||
|
||||
add_devices(devs, True)
|
||||
async_add_devices(devs, True)
|
||||
|
||||
|
||||
class LutronCasetaLight(LutronCasetaDevice, Light):
|
||||
@ -44,7 +46,8 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
|
||||
"""Return the brightness of the light."""
|
||||
return to_hass_level(self._state["current_state"])
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
@ -53,7 +56,8 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
|
||||
self._smartbridge.set_value(self._device_id,
|
||||
to_lutron_level(brightness))
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
@ -62,7 +66,8 @@ class LutronCasetaLight(LutronCasetaDevice, Light):
|
||||
"""Return true if device is on."""
|
||||
return self._state["current_state"] > 0
|
||||
|
||||
def update(self):
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Call when forcing a refresh of the device."""
|
||||
self._state = self._smartbridge.get_device_by_id(self._device_id)
|
||||
_LOGGER.debug(self._state)
|
||||
|
@ -8,6 +8,7 @@ import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION,
|
||||
SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP,
|
||||
@ -181,14 +182,12 @@ class TradfriLight(Light):
|
||||
def device_state_attributes(self):
|
||||
"""Return the devices' state attributes."""
|
||||
info = self._light.device_info
|
||||
attrs = {
|
||||
'manufacturer': info.manufacturer,
|
||||
'model_number': info.model_number,
|
||||
'serial': info.serial,
|
||||
'firmware_version': info.firmware_version,
|
||||
'power_source': info.power_source_str,
|
||||
'battery_level': info.battery_level
|
||||
}
|
||||
|
||||
attrs = {}
|
||||
|
||||
if info.battery_level is not None:
|
||||
attrs[ATTR_BATTERY_LEVEL] = info.battery_level
|
||||
|
||||
return attrs
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-miio==0.3.0']
|
||||
REQUIREMENTS = ['python-miio==0.3.1']
|
||||
|
||||
# The light does not accept cct values < 1
|
||||
CCT_MIN = 1
|
||||
@ -64,14 +64,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
light = PhilipsEyecare(host, token)
|
||||
device = XiaomiPhilipsEyecareLamp(name, light, device_info)
|
||||
devices.append(device)
|
||||
elif device_info.model == 'philips.light.ceil':
|
||||
elif device_info.model == 'philips.light.ceiling':
|
||||
from miio import Ceil
|
||||
light = Ceil(host, token)
|
||||
device = XiaomiPhilipsCeilingLamp(name, light, device_info)
|
||||
devices.append(device)
|
||||
elif device_info.model == 'philips.light.bulb':
|
||||
from miio import Ceil
|
||||
light = Ceil(host, token)
|
||||
from miio import PhilipsBulb
|
||||
light = PhilipsBulb(host, token)
|
||||
device = XiaomiPhilipsLightBall(name, light, device_info)
|
||||
devices.append(device)
|
||||
else:
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pylutron-caseta==0.2.8']
|
||||
REQUIREMENTS = ['pylutron-caseta==0.3.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -22,9 +22,16 @@ LUTRON_CASETA_SMARTBRIDGE = 'lutron_smartbridge'
|
||||
|
||||
DOMAIN = 'lutron_caseta'
|
||||
|
||||
CONF_KEYFILE = 'keyfile'
|
||||
CONF_CERTFILE = 'certfile'
|
||||
CONF_CA_CERTS = 'ca_certs'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_KEYFILE): cv.string,
|
||||
vol.Required(CONF_CERTFILE): cv.string,
|
||||
vol.Required(CONF_CA_CERTS): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
@ -33,14 +40,21 @@ LUTRON_CASETA_COMPONENTS = [
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, base_config):
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, base_config):
|
||||
"""Set up the Lutron component."""
|
||||
from pylutron_caseta.smartbridge import Smartbridge
|
||||
|
||||
config = base_config.get(DOMAIN)
|
||||
hass.data[LUTRON_CASETA_SMARTBRIDGE] = Smartbridge(
|
||||
hostname=config[CONF_HOST]
|
||||
)
|
||||
keyfile = hass.config.path(config[CONF_KEYFILE])
|
||||
certfile = hass.config.path(config[CONF_CERTFILE])
|
||||
ca_certs = hass.config.path(config[CONF_CA_CERTS])
|
||||
bridge = Smartbridge.create_tls(hostname=config[CONF_HOST],
|
||||
keyfile=keyfile,
|
||||
certfile=certfile,
|
||||
ca_certs=ca_certs)
|
||||
hass.data[LUTRON_CASETA_SMARTBRIDGE] = bridge
|
||||
yield from bridge.connect()
|
||||
if not hass.data[LUTRON_CASETA_SMARTBRIDGE].is_connected():
|
||||
_LOGGER.error("Unable to connect to Lutron smartbridge at %s",
|
||||
config[CONF_HOST])
|
||||
@ -49,7 +63,8 @@ def setup(hass, base_config):
|
||||
_LOGGER.info("Connected to Lutron smartbridge at %s", config[CONF_HOST])
|
||||
|
||||
for component in LUTRON_CASETA_COMPONENTS:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
hass.async_add_job(discovery.async_load_platform(hass, component,
|
||||
DOMAIN, {}, config))
|
||||
|
||||
return True
|
||||
|
||||
@ -73,13 +88,8 @@ class LutronCasetaDevice(Entity):
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.async_add_job(
|
||||
self._smartbridge.add_subscriber, self._device_id,
|
||||
self._update_callback
|
||||
)
|
||||
|
||||
def _update_callback(self):
|
||||
self.schedule_update_ha_state()
|
||||
self._smartbridge.add_subscriber(self._device_id,
|
||||
self.async_schedule_update_ha_state)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -16,7 +16,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.29']
|
||||
REQUIREMENTS = ['youtube_dl==2017.11.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -7,32 +7,33 @@ https://home-assistant.io/components/media_player/
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import functools as ft
|
||||
import collections
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from random import SystemRandom
|
||||
|
||||
from aiohttp import web, hdrs
|
||||
from aiohttp import web
|
||||
from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED
|
||||
from homeassistant.const import (
|
||||
STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID,
|
||||
SERVICE_TOGGLE, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_SEEK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_VOLUME_SET, SERVICE_MEDIA_PAUSE, SERVICE_SHUFFLE_SET,
|
||||
SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_MEDIA_NEXT_TRACK,
|
||||
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async import run_coroutine_threadsafe
|
||||
from homeassistant.const import (
|
||||
STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE,
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_MUTE, SERVICE_TOGGLE, SERVICE_MEDIA_STOP,
|
||||
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK,
|
||||
SERVICE_SHUFFLE_SET)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_RND = SystemRandom()
|
||||
@ -44,17 +45,16 @@ SCAN_INTERVAL = timedelta(seconds=10)
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
ENTITY_IMAGE_URL = '/api/media_player_proxy/{0}?token={1}&cache={2}'
|
||||
ATTR_CACHE_IMAGES = 'images'
|
||||
ATTR_CACHE_URLS = 'urls'
|
||||
ATTR_CACHE_MAXSIZE = 'maxsize'
|
||||
CACHE_IMAGES = 'images'
|
||||
CACHE_MAXSIZE = 'maxsize'
|
||||
CACHE_LOCK = 'lock'
|
||||
CACHE_URL = 'url'
|
||||
CACHE_CONTENT = 'content'
|
||||
ENTITY_IMAGE_CACHE = {
|
||||
ATTR_CACHE_IMAGES: {},
|
||||
ATTR_CACHE_URLS: [],
|
||||
ATTR_CACHE_MAXSIZE: 16
|
||||
CACHE_IMAGES: collections.OrderedDict(),
|
||||
CACHE_MAXSIZE: 16
|
||||
}
|
||||
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
SERVICE_PLAY_MEDIA = 'play_media'
|
||||
SERVICE_SELECT_SOURCE = 'select_source'
|
||||
SERVICE_CLEAR_PLAYLIST = 'clear_playlist'
|
||||
@ -896,43 +896,36 @@ def _async_fetch_image(hass, url):
|
||||
|
||||
Images are cached in memory (the images are typically 10-100kB in size).
|
||||
"""
|
||||
cache_images = ENTITY_IMAGE_CACHE[ATTR_CACHE_IMAGES]
|
||||
cache_urls = ENTITY_IMAGE_CACHE[ATTR_CACHE_URLS]
|
||||
cache_maxsize = ENTITY_IMAGE_CACHE[ATTR_CACHE_MAXSIZE]
|
||||
cache_images = ENTITY_IMAGE_CACHE[CACHE_IMAGES]
|
||||
cache_maxsize = ENTITY_IMAGE_CACHE[CACHE_MAXSIZE]
|
||||
|
||||
if url in cache_images:
|
||||
return cache_images[url]
|
||||
if url not in cache_images:
|
||||
cache_images[url] = {CACHE_LOCK: asyncio.Lock(loop=hass.loop)}
|
||||
|
||||
content, content_type = (None, None)
|
||||
websession = async_get_clientsession(hass)
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=hass.loop):
|
||||
response = yield from websession.get(url)
|
||||
with (yield from cache_images[url][CACHE_LOCK]):
|
||||
if CACHE_CONTENT in cache_images[url]:
|
||||
return cache_images[url][CACHE_CONTENT]
|
||||
|
||||
if response.status == 200:
|
||||
content = yield from response.read()
|
||||
content_type = response.headers.get(CONTENT_TYPE_HEADER)
|
||||
if content_type:
|
||||
content_type = content_type.split(';')[0]
|
||||
content, content_type = (None, None)
|
||||
websession = async_get_clientsession(hass)
|
||||
try:
|
||||
with async_timeout.timeout(10, loop=hass.loop):
|
||||
response = yield from websession.get(url)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
if response.status == 200:
|
||||
content = yield from response.read()
|
||||
content_type = response.headers.get(CONTENT_TYPE)
|
||||
if content_type:
|
||||
content_type = content_type.split(';')[0]
|
||||
cache_images[url][CACHE_CONTENT] = content, content_type
|
||||
|
||||
if not content:
|
||||
return (None, None)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
cache_images[url] = (content, content_type)
|
||||
cache_urls.append(url)
|
||||
while len(cache_images) > cache_maxsize:
|
||||
cache_images.popitem(last=False)
|
||||
|
||||
while len(cache_urls) > cache_maxsize:
|
||||
# remove oldest item from cache
|
||||
oldest_url = cache_urls[0]
|
||||
if oldest_url in cache_images:
|
||||
del cache_images[oldest_url]
|
||||
|
||||
cache_urls = cache_urls[1:]
|
||||
|
||||
return content, content_type
|
||||
return content, content_type
|
||||
|
||||
|
||||
class MediaPlayerImageView(HomeAssistantView):
|
||||
@ -965,8 +958,6 @@ class MediaPlayerImageView(HomeAssistantView):
|
||||
if data is None:
|
||||
return web.Response(status=500)
|
||||
|
||||
headers = {hdrs.CACHE_CONTROL: 'max-age=3600'}
|
||||
headers = {CACHE_CONTROL: 'max-age=3600'}
|
||||
return web.Response(
|
||||
body=data,
|
||||
content_type=content_type,
|
||||
headers=headers)
|
||||
body=data, content_type=content_type, headers=headers)
|
||||
|
@ -4,33 +4,37 @@ Bluesound.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.bluesound/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from asyncio.futures import CancelledError
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from asyncio.futures import CancelledError
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from aiohttp.hdrs import CONNECTION, KEEP_ALIVE
|
||||
import async_timeout
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.util.dt as dt_util
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
|
||||
SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC,
|
||||
SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_STEP)
|
||||
SUPPORT_PLAY, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PAUSE, PLATFORM_SCHEMA,
|
||||
MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP,
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PREVIOUS_TRACK,
|
||||
MediaPlayerDevice)
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOSTS,
|
||||
CONF_HOST, CONF_PORT, CONF_NAME)
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, CONF_HOSTS, STATE_IDLE, STATE_PAUSED,
|
||||
STATE_PLAYING, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['xmltodict==0.11.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATE_OFFLINE = 'offline'
|
||||
ATTR_MODEL = 'model'
|
||||
ATTR_MODEL_NAME = 'model_name'
|
||||
@ -46,8 +50,6 @@ UPDATE_PRESETS_INTERVAL = timedelta(minutes=30)
|
||||
NODE_OFFLINE_CHECK_TIMEOUT = 180
|
||||
NODE_RETRY_INITIATION = timedelta(minutes=3)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
@ -80,20 +82,15 @@ def _add_player(hass, async_add_devices, host, port=None, name=None):
|
||||
def _add_player_cb():
|
||||
"""Add player after first sync fetch."""
|
||||
async_add_devices([player])
|
||||
_LOGGER.info('Added Bluesound device with name: %s', player.name)
|
||||
_LOGGER.info("Added device with name: %s", player.name)
|
||||
|
||||
if hass.is_running:
|
||||
_start_polling()
|
||||
else:
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
_start_polling
|
||||
)
|
||||
EVENT_HOMEASSISTANT_START, _start_polling)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
_stop_polling
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_polling)
|
||||
|
||||
player = BluesoundPlayer(hass, host, port, name, _add_player_cb)
|
||||
hass.data[DATA_BLUESOUND].append(player)
|
||||
@ -101,10 +98,7 @@ def _add_player(hass, async_add_devices, host, port=None, name=None):
|
||||
if hass.is_running:
|
||||
_init_player()
|
||||
else:
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
_init_player
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@ -121,11 +115,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
hosts = config.get(CONF_HOSTS, None)
|
||||
if hosts:
|
||||
for host in hosts:
|
||||
_add_player(hass,
|
||||
async_add_devices,
|
||||
host.get(CONF_HOST),
|
||||
host.get(CONF_PORT, None),
|
||||
host.get(CONF_NAME, None))
|
||||
_add_player(
|
||||
hass, async_add_devices, host.get(CONF_HOST),
|
||||
host.get(CONF_PORT), host.get(CONF_NAME, None))
|
||||
|
||||
|
||||
class BluesoundPlayer(MediaPlayerDevice):
|
||||
@ -137,7 +129,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
self._hass = hass
|
||||
self._port = port
|
||||
self._polling_session = async_get_clientsession(hass)
|
||||
self._polling_task = None # The actuall polling task.
|
||||
self._polling_task = None # The actual polling task.
|
||||
self._name = name
|
||||
self._brand = None
|
||||
self._model = None
|
||||
@ -156,7 +148,6 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
if self._port is None:
|
||||
self._port = DEFAULT_PORT
|
||||
|
||||
# Internal methods
|
||||
@staticmethod
|
||||
def _try_get_index(string, seach_string):
|
||||
try:
|
||||
@ -165,13 +156,12 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
return -1
|
||||
|
||||
@asyncio.coroutine
|
||||
def _internal_update_sync_status(self, on_updated_cb=None,
|
||||
raise_timeout=False):
|
||||
def _internal_update_sync_status(
|
||||
self, on_updated_cb=None, raise_timeout=False):
|
||||
resp = None
|
||||
try:
|
||||
resp = yield from self.send_bluesound_command(
|
||||
'SyncStatus',
|
||||
raise_timeout, raise_timeout)
|
||||
'SyncStatus', raise_timeout, raise_timeout)
|
||||
except:
|
||||
raise
|
||||
|
||||
@ -193,9 +183,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
if on_updated_cb:
|
||||
on_updated_cb()
|
||||
return True
|
||||
# END Internal methods
|
||||
|
||||
# Poll functionality
|
||||
@asyncio.coroutine
|
||||
def _start_poll_command(self):
|
||||
""""Loop which polls the status of the player."""
|
||||
@ -204,14 +192,13 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
yield from self.async_update_status()
|
||||
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
_LOGGER.info("Bluesound node %s is offline, retrying later",
|
||||
self._name)
|
||||
yield from asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT,
|
||||
loop=self._hass.loop)
|
||||
_LOGGER.info("Node %s is offline, retrying later", self._name)
|
||||
yield from asyncio.sleep(
|
||||
NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop)
|
||||
self.start_polling()
|
||||
|
||||
except CancelledError:
|
||||
_LOGGER.debug("Stopping bluesound polling of node %s", self._name)
|
||||
_LOGGER.debug("Stopping the polling of node %s", self._name)
|
||||
except:
|
||||
_LOGGER.exception("Unexpected error in %s", self._name)
|
||||
raise
|
||||
@ -224,9 +211,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
def stop_polling(self):
|
||||
"""Stop the polling task."""
|
||||
self._polling_task.cancel()
|
||||
# END Poll functionality
|
||||
|
||||
# Initiator
|
||||
@asyncio.coroutine
|
||||
def async_init(self):
|
||||
"""Initiate the player async."""
|
||||
@ -235,22 +220,17 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
self._retry_remove()
|
||||
self._retry_remove = None
|
||||
|
||||
yield from self._internal_update_sync_status(self._init_callback,
|
||||
True)
|
||||
yield from self._internal_update_sync_status(
|
||||
self._init_callback, True)
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
_LOGGER.info("Bluesound node %s is offline, retrying later",
|
||||
self.host)
|
||||
_LOGGER.info("Node %s is offline, retrying later", self.host)
|
||||
self._retry_remove = async_track_time_interval(
|
||||
self._hass,
|
||||
self.async_init,
|
||||
NODE_RETRY_INITIATION)
|
||||
self._hass, self.async_init, NODE_RETRY_INITIATION)
|
||||
except:
|
||||
_LOGGER.exception("Unexpected when initiating error in %s",
|
||||
self.host)
|
||||
raise
|
||||
# END Initiator
|
||||
|
||||
# Status updates fetchers
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update internal status of the entity."""
|
||||
@ -275,7 +255,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
method = method[1:]
|
||||
url = "http://{}:{}/{}".format(self.host, self._port, method)
|
||||
|
||||
_LOGGER.info("calling URL: %s", url)
|
||||
_LOGGER.debug("Calling URL: %s", url)
|
||||
response = None
|
||||
try:
|
||||
websession = async_get_clientsession(self._hass)
|
||||
@ -294,11 +274,10 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
|
||||
except (asyncio.TimeoutError, aiohttp.ClientError):
|
||||
if raise_timeout:
|
||||
_LOGGER.info("Timeout with Bluesound: %s", self.host)
|
||||
_LOGGER.info("Timeout: %s", self.host)
|
||||
raise
|
||||
else:
|
||||
_LOGGER.debug("Failed communicating with Bluesound: %s",
|
||||
self.host)
|
||||
_LOGGER.debug("Failed communicating: %s", self.host)
|
||||
return None
|
||||
|
||||
return data
|
||||
@ -315,17 +294,17 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
etag = self._status.get('@etag', '')
|
||||
|
||||
if etag != '':
|
||||
url = 'Status?etag='+etag+'&timeout=60.0'
|
||||
url = 'Status?etag={}&timeout=60.0'.format(etag)
|
||||
url = "http://{}:{}/{}".format(self.host, self._port, url)
|
||||
|
||||
_LOGGER.debug("calling URL: %s", url)
|
||||
_LOGGER.debug("Calling URL: %s", url)
|
||||
|
||||
try:
|
||||
|
||||
with async_timeout.timeout(65, loop=self._hass.loop):
|
||||
response = yield from self._polling_session.get(
|
||||
url,
|
||||
headers={'connection': 'keep-alive'})
|
||||
headers={CONNECTION: KEEP_ALIVE})
|
||||
|
||||
if response.status != 200:
|
||||
_LOGGER.error("Error %s on %s", response.status, url)
|
||||
@ -350,8 +329,8 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
def async_update_sync_status(self, on_updated_cb=None,
|
||||
raise_timeout=False):
|
||||
"""Update sync status."""
|
||||
yield from self._internal_update_sync_status(on_updated_cb,
|
||||
raise_timeout=False)
|
||||
yield from self._internal_update_sync_status(
|
||||
on_updated_cb, raise_timeout=False)
|
||||
|
||||
@asyncio.coroutine
|
||||
@Throttle(UPDATE_CAPTURE_INTERVAL)
|
||||
@ -436,9 +415,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
_create_service_item(resp['services']['service'])
|
||||
|
||||
return self._services_items
|
||||
# END Status updates fetchers
|
||||
|
||||
# Media player (and core) properties
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No need to poll information."""
|
||||
@ -611,17 +588,17 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
stream_url = self._status.get('streamUrl', '')
|
||||
|
||||
if self._status.get('is_preset', '') == '1' and stream_url != '':
|
||||
# this check doesn't work with all presets, for example playlists.
|
||||
# But it works with radio service_items will catch playlists
|
||||
# This check doesn't work with all presets, for example playlists.
|
||||
# But it works with radio service_items will catch playlists.
|
||||
items = [x for x in self._preset_items if 'url2' in x and
|
||||
parse.unquote(x['url2']) == stream_url]
|
||||
if len(items) > 0:
|
||||
return items[0]['title']
|
||||
|
||||
# this could be a bit difficult to detect. Bluetooth could be named
|
||||
# This could be a bit difficult to detect. Bluetooth could be named
|
||||
# different things and there is not any way to match chooses in
|
||||
# capture list to current playing. It's a bit of guesswork.
|
||||
# This method will be needing some tweaking over time
|
||||
# This method will be needing some tweaking over time.
|
||||
title = self._status.get('title1', '').lower()
|
||||
if title == 'bluetooth' or stream_url == 'Capture:hw:2,0/44100/16/2':
|
||||
items = [x for x in self._capture_items
|
||||
@ -660,7 +637,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
return items[0]['title']
|
||||
|
||||
if self._status.get('streamUrl', '') != '':
|
||||
_LOGGER.debug("Couldn't find source of stream url: %s",
|
||||
_LOGGER.debug("Couldn't find source of stream URL: %s",
|
||||
self._status.get('streamUrl', ''))
|
||||
return None
|
||||
|
||||
@ -695,9 +672,7 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
ATTR_MODEL_NAME: self._model_name,
|
||||
ATTR_BRAND: self._brand,
|
||||
}
|
||||
# END Media player (and core) properties
|
||||
|
||||
# Media player commands
|
||||
@asyncio.coroutine
|
||||
def async_select_source(self, source):
|
||||
"""Select input source."""
|
||||
@ -712,8 +687,8 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
return
|
||||
|
||||
selected_source = items[0]
|
||||
url = 'Play?url={}&preset_id&image={}'.format(selected_source['url'],
|
||||
selected_source['image'])
|
||||
url = 'Play?url={}&preset_id&image={}'.format(
|
||||
selected_source['url'], selected_source['image'])
|
||||
|
||||
if 'is_raw_url' in selected_source and selected_source['is_raw_url']:
|
||||
url = selected_source['url']
|
||||
@ -806,4 +781,3 @@ class BluesoundPlayer(MediaPlayerDevice):
|
||||
else:
|
||||
return self.send_bluesound_command(
|
||||
'Volume?level=' + str(float(self._lastvol) * 100))
|
||||
# END Media player commands
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
REQUIREMENTS = ['snapcast==2.0.7']
|
||||
REQUIREMENTS = ['snapcast==2.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -80,7 +80,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
try:
|
||||
server = yield from snapcast.control.create_server(
|
||||
hass.loop, host, port)
|
||||
hass.loop, host, port, reconnect=True)
|
||||
except socket.gaierror:
|
||||
_LOGGER.error('Could not connect to Snapcast server at %s:%d',
|
||||
host, port)
|
||||
|
@ -9,28 +9,30 @@ import logging
|
||||
# pylint: disable=import-error
|
||||
from copy import copy
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME,
|
||||
ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE,
|
||||
ATTR_MEDIA_PLAYLIST, ATTR_MEDIA_SEASON, ATTR_MEDIA_SEEK_POSITION,
|
||||
ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK,
|
||||
ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_POSITION, ATTR_MEDIA_SHUFFLE,
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT, DOMAIN, SERVICE_PLAY_MEDIA,
|
||||
ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_DURATION, ATTR_MEDIA_EPISODE, ATTR_MEDIA_PLAYLIST,
|
||||
ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_SEASON,
|
||||
ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_SHUFFLE,
|
||||
ATTR_MEDIA_TITLE, ATTR_MEDIA_TRACK, ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED, DOMAIN, MediaPlayerDevice, PLATFORM_SCHEMA,
|
||||
SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE,
|
||||
SUPPORT_CLEAR_PLAYLIST, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET,
|
||||
SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
|
||||
SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST,
|
||||
SUPPORT_SHUFFLE_SET, ATTR_INPUT_SOURCE, SERVICE_SELECT_SOURCE,
|
||||
SERVICE_CLEAR_PLAYLIST, MediaPlayerDevice)
|
||||
SUPPORT_VOLUME_STEP)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, CONF_NAME, SERVICE_MEDIA_NEXT_TRACK,
|
||||
SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, SERVICE_SHUFFLE_SET, STATE_IDLE,
|
||||
STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP, ATTR_SUPPORTED_FEATURES)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, CONF_NAME,
|
||||
CONF_STATE_TEMPLATE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN,
|
||||
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP,
|
||||
SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import async_call_from_config
|
||||
|
||||
ATTR_ACTIVE_CHILD = 'active_child'
|
||||
@ -48,113 +50,75 @@ OFF_STATES = [STATE_IDLE, STATE_OFF]
|
||||
REQUIREMENTS = []
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRS_SCHEMA = vol.Schema({cv.slug: cv.string})
|
||||
CMD_SCHEMA = vol.Schema({cv.slug: cv.SERVICE_SCHEMA})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_CHILDREN, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_COMMANDS, default={}): CMD_SCHEMA,
|
||||
vol.Optional(CONF_ATTRS, default={}):
|
||||
vol.Or(cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA),
|
||||
vol.Optional(CONF_STATE_TEMPLATE): cv.template
|
||||
}, extra=vol.REMOVE_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the universal media players."""
|
||||
if not validate_config(config):
|
||||
return
|
||||
|
||||
player = UniversalMediaPlayer(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config[CONF_CHILDREN],
|
||||
config[CONF_COMMANDS],
|
||||
config[CONF_ATTRS]
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_CHILDREN),
|
||||
config.get(CONF_COMMANDS),
|
||||
config.get(CONF_ATTRS),
|
||||
config.get(CONF_STATE_TEMPLATE)
|
||||
)
|
||||
|
||||
async_add_devices([player])
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
"""Validate universal media player configuration."""
|
||||
del config[CONF_PLATFORM]
|
||||
|
||||
# Validate name
|
||||
if CONF_NAME not in config:
|
||||
_LOGGER.error("Universal Media Player configuration requires name")
|
||||
return False
|
||||
|
||||
validate_children(config)
|
||||
validate_commands(config)
|
||||
validate_attributes(config)
|
||||
|
||||
del_keys = []
|
||||
for key in config:
|
||||
if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]:
|
||||
_LOGGER.warning(
|
||||
"Universal Media Player (%s) unrecognized parameter %s",
|
||||
config[CONF_NAME], key)
|
||||
del_keys.append(key)
|
||||
for key in del_keys:
|
||||
del config[key]
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def validate_children(config):
|
||||
"""Validate children."""
|
||||
if CONF_CHILDREN not in config:
|
||||
_LOGGER.info(
|
||||
"No children under Universal Media Player (%s)", config[CONF_NAME])
|
||||
config[CONF_CHILDREN] = []
|
||||
elif not isinstance(config[CONF_CHILDREN], list):
|
||||
_LOGGER.warning(
|
||||
"Universal Media Player (%s) children not list in config. "
|
||||
"They will be ignored", config[CONF_NAME])
|
||||
config[CONF_CHILDREN] = []
|
||||
|
||||
|
||||
def validate_commands(config):
|
||||
"""Validate commands."""
|
||||
if CONF_COMMANDS not in config:
|
||||
config[CONF_COMMANDS] = {}
|
||||
elif not isinstance(config[CONF_COMMANDS], dict):
|
||||
_LOGGER.warning(
|
||||
"Universal Media Player (%s) specified commands not dict in "
|
||||
"config. They will be ignored", config[CONF_NAME])
|
||||
config[CONF_COMMANDS] = {}
|
||||
|
||||
|
||||
def validate_attributes(config):
|
||||
"""Validate attributes."""
|
||||
if CONF_ATTRS not in config:
|
||||
config[CONF_ATTRS] = {}
|
||||
elif not isinstance(config[CONF_ATTRS], dict):
|
||||
_LOGGER.warning(
|
||||
"Universal Media Player (%s) specified attributes "
|
||||
"not dict in config. They will be ignored", config[CONF_NAME])
|
||||
config[CONF_ATTRS] = {}
|
||||
|
||||
for key, val in config[CONF_ATTRS].items():
|
||||
attr = val.split('|', 1)
|
||||
if len(attr) == 1:
|
||||
attr.append(None)
|
||||
config[CONF_ATTRS][key] = attr
|
||||
|
||||
|
||||
class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
"""Representation of an universal media player."""
|
||||
|
||||
def __init__(self, hass, name, children, commands, attributes):
|
||||
def __init__(self, hass, name, children,
|
||||
commands, attributes, state_template=None):
|
||||
"""Initialize the Universal media device."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
self._children = children
|
||||
self._cmds = commands
|
||||
self._attrs = attributes
|
||||
self._attrs = {}
|
||||
for key, val in attributes.items():
|
||||
attr = val.split('|', 1)
|
||||
if len(attr) == 1:
|
||||
attr.append(None)
|
||||
self._attrs[key] = attr
|
||||
self._child_state = None
|
||||
self._state_template = state_template
|
||||
if state_template is not None:
|
||||
self._state_template.hass = hass
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe to children and template state changes.
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
@callback
|
||||
def async_on_dependency_update(*_):
|
||||
"""Update ha state when dependencies update."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
depend = copy(children)
|
||||
for entity in attributes.values():
|
||||
depend = copy(self._children)
|
||||
for entity in self._attrs.values():
|
||||
depend.append(entity[0])
|
||||
if self._state_template is not None:
|
||||
for entity in self._state_template.extract_entities():
|
||||
depend.append(entity)
|
||||
|
||||
async_track_state_change(hass, depend, async_on_dependency_update)
|
||||
self.hass.helpers.event.async_track_state_change(
|
||||
list(set(depend)), async_on_dependency_update)
|
||||
|
||||
def _entity_lkp(self, entity_id, state_attr=None):
|
||||
"""Look up an entity state."""
|
||||
@ -211,6 +175,8 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
@property
|
||||
def master_state(self):
|
||||
"""Return the master state for entity or None."""
|
||||
if self._state_template is not None:
|
||||
return self._state_template.async_render()
|
||||
if CONF_STATE in self._attrs:
|
||||
master_state = self._entity_lkp(
|
||||
self._attrs[CONF_STATE][0], self._attrs[CONF_STATE][1])
|
||||
@ -232,8 +198,8 @@ class UniversalMediaPlayer(MediaPlayerDevice):
|
||||
else master state or off
|
||||
"""
|
||||
master_state = self.master_state # avoid multiple lookups
|
||||
if master_state == STATE_OFF:
|
||||
return STATE_OFF
|
||||
if (master_state == STATE_OFF) or (self._state_template is not None):
|
||||
return master_state
|
||||
|
||||
active_child = self._child_state
|
||||
if active_child:
|
||||
|
@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA,
|
||||
vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int,
|
||||
vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int,
|
||||
vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
})
|
||||
|
||||
@ -202,29 +202,25 @@ class LgWebOSDevice(MediaPlayerDevice):
|
||||
|
||||
for app in self._client.get_apps():
|
||||
self._app_list[app['id']] = app
|
||||
if conf_sources:
|
||||
if app['id'] == self._current_source_id:
|
||||
self._current_source = app['title']
|
||||
self._source_list[app['title']] = app
|
||||
elif (app['id'] in conf_sources or
|
||||
any(word in app['title']
|
||||
for word in conf_sources) or
|
||||
any(word in app['id']
|
||||
for word in conf_sources)):
|
||||
self._source_list[app['title']] = app
|
||||
else:
|
||||
if app['id'] == self._current_source_id:
|
||||
self._current_source = app['title']
|
||||
self._source_list[app['title']] = app
|
||||
elif (not conf_sources or
|
||||
app['id'] in conf_sources or
|
||||
any(word in app['title']
|
||||
for word in conf_sources) or
|
||||
any(word in app['id']
|
||||
for word in conf_sources)):
|
||||
self._source_list[app['title']] = app
|
||||
|
||||
for source in self._client.get_inputs():
|
||||
if conf_sources:
|
||||
if source['id'] == self._current_source_id:
|
||||
self._source_list[source['label']] = source
|
||||
elif (source['label'] in conf_sources or
|
||||
any(source['label'].find(word) != -1
|
||||
for word in conf_sources)):
|
||||
self._source_list[source['label']] = source
|
||||
else:
|
||||
if source['id'] == self._current_source_id:
|
||||
self._current_source = source['label']
|
||||
self._source_list[source['label']] = source
|
||||
elif (not conf_sources or
|
||||
source['label'] in conf_sources or
|
||||
any(source['label'].find(word) != -1
|
||||
for word in conf_sources)):
|
||||
self._source_list[source['label']] = source
|
||||
except (OSError, ConnectionClosed, TypeError,
|
||||
asyncio.TimeoutError):
|
||||
|
@ -10,10 +10,11 @@ media_player:
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_PORT,
|
||||
STATE_UNKNOWN, STATE_ON
|
||||
STATE_UNKNOWN, STATE_ON, STATE_PLAYING, STATE_PAUSED, STATE_IDLE
|
||||
)
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
|
||||
@ -35,7 +36,7 @@ SUPPORTED_FEATURES = (
|
||||
KNOWN_HOSTS_KEY = 'data_yamaha_musiccast'
|
||||
INTERVAL_SECONDS = 'interval_seconds'
|
||||
|
||||
REQUIREMENTS = ['pymusiccast==0.1.3']
|
||||
REQUIREMENTS = ['pymusiccast==0.1.5']
|
||||
|
||||
DEFAULT_PORT = 5005
|
||||
DEFAULT_INTERVAL = 480
|
||||
@ -111,6 +112,7 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
self._zone = zone
|
||||
self.mute = False
|
||||
self.media_status = None
|
||||
self.media_status_received = None
|
||||
self.power = STATE_UNKNOWN
|
||||
self.status = STATE_UNKNOWN
|
||||
self.volume = 0
|
||||
@ -202,12 +204,34 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
"""Title of current playing media."""
|
||||
return self.media_status.media_title if self.media_status else None
|
||||
|
||||
@property
|
||||
def media_position(self):
|
||||
"""Position of current playing media in seconds."""
|
||||
if self.media_status and self.state in \
|
||||
[STATE_PLAYING, STATE_PAUSED, STATE_IDLE]:
|
||||
return self.media_status.media_position
|
||||
|
||||
@property
|
||||
def media_position_updated_at(self):
|
||||
"""When was the position of the current playing media valid.
|
||||
|
||||
Returns value from homeassistant.util.dt.utcnow().
|
||||
"""
|
||||
return self.media_status_received if self.media_status else None
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the device."""
|
||||
_LOGGER.debug("update: %s", self.entity_id)
|
||||
self._recv.update_status()
|
||||
self._zone.update_status()
|
||||
|
||||
def update_hass(self):
|
||||
"""Push updates to HASS."""
|
||||
if self.entity_id:
|
||||
_LOGGER.debug("update_hass: pushing updates")
|
||||
self.schedule_update_ha_state()
|
||||
return True
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on specified media player or all."""
|
||||
_LOGGER.debug("Turn device: on")
|
||||
@ -259,3 +283,9 @@ class YamahaDevice(MediaPlayerDevice):
|
||||
_LOGGER.debug("select_source: %s", source)
|
||||
self.status = STATE_UNKNOWN
|
||||
self._zone.set_input(source)
|
||||
|
||||
def new_media_status(self, status):
|
||||
"""Handle updates of the media status."""
|
||||
_LOGGER.debug("new media_status arrived")
|
||||
self.media_status = status
|
||||
self.media_status_received = dt_util.utcnow()
|
||||
|
@ -438,7 +438,8 @@ class MQTT(object):
|
||||
self.broker = broker
|
||||
self.port = port
|
||||
self.keepalive = keepalive
|
||||
self.topics = {}
|
||||
self.wanted_topics = {}
|
||||
self.subscribed_topics = {}
|
||||
self.progress = {}
|
||||
self.birth_message = birth_message
|
||||
self._mqttc = None
|
||||
@ -526,15 +527,14 @@ class MQTT(object):
|
||||
raise HomeAssistantError("topic need to be a string!")
|
||||
|
||||
with (yield from self._paho_lock):
|
||||
if topic in self.topics:
|
||||
if topic in self.subscribed_topics:
|
||||
return
|
||||
|
||||
self.wanted_topics[topic] = qos
|
||||
result, mid = yield from self.hass.async_add_job(
|
||||
self._mqttc.subscribe, topic, qos)
|
||||
|
||||
_raise_on_error(result)
|
||||
self.progress[mid] = topic
|
||||
self.topics[topic] = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_unsubscribe(self, topic):
|
||||
@ -542,6 +542,7 @@ class MQTT(object):
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
self.wanted_topics.pop(topic, None)
|
||||
result, mid = yield from self.hass.async_add_job(
|
||||
self._mqttc.unsubscribe, topic)
|
||||
|
||||
@ -562,15 +563,10 @@ class MQTT(object):
|
||||
self._mqttc.disconnect()
|
||||
return
|
||||
|
||||
old_topics = self.topics
|
||||
|
||||
self.topics = {key: value for key, value in self.topics.items()
|
||||
if value is None}
|
||||
|
||||
for topic, qos in old_topics.items():
|
||||
# qos is None if we were in process of subscribing
|
||||
if qos is not None:
|
||||
self.hass.add_job(self.async_subscribe, topic, qos)
|
||||
self.progress = {}
|
||||
self.subscribed_topics = {}
|
||||
for topic, qos in self.wanted_topics.items():
|
||||
self.hass.add_job(self.async_subscribe, topic, qos)
|
||||
|
||||
if self.birth_message:
|
||||
self.hass.add_job(self.async_publish(
|
||||
@ -584,7 +580,7 @@ class MQTT(object):
|
||||
topic = self.progress.pop(mid, None)
|
||||
if topic is None:
|
||||
return
|
||||
self.topics[topic] = granted_qos[0]
|
||||
self.subscribed_topics[topic] = granted_qos[0]
|
||||
|
||||
def _mqtt_on_message(self, _mqttc, _userdata, msg):
|
||||
"""Message received callback."""
|
||||
@ -598,18 +594,12 @@ class MQTT(object):
|
||||
topic = self.progress.pop(mid, None)
|
||||
if topic is None:
|
||||
return
|
||||
self.topics.pop(topic, None)
|
||||
self.subscribed_topics.pop(topic, None)
|
||||
|
||||
def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code):
|
||||
"""Disconnected callback."""
|
||||
self.progress = {}
|
||||
self.topics = {key: value for key, value in self.topics.items()
|
||||
if value is not None}
|
||||
|
||||
# Remove None values from topic list
|
||||
for key in list(self.topics):
|
||||
if self.topics[key] is None:
|
||||
self.topics.pop(key)
|
||||
self.subscribed_topics = {}
|
||||
|
||||
# When disconnected because of calling disconnect()
|
||||
if result_code == 0:
|
||||
|
@ -20,10 +20,12 @@ TOPIC_MATCHER = re.compile(
|
||||
r'(?P<prefix_topic>\w+)/(?P<component>\w+)/'
|
||||
r'(?:(?P<node_id>[a-zA-Z0-9_-]+)/)?(?P<object_id>[a-zA-Z0-9_-]+)/config')
|
||||
|
||||
SUPPORTED_COMPONENTS = ['binary_sensor', 'fan', 'light', 'sensor', 'switch']
|
||||
SUPPORTED_COMPONENTS = [
|
||||
'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch']
|
||||
|
||||
ALLOWED_PLATFORMS = {
|
||||
'binary_sensor': ['mqtt'],
|
||||
'cover': ['mqtt'],
|
||||
'fan': ['mqtt'],
|
||||
'light': ['mqtt', 'mqtt_json', 'mqtt_template'],
|
||||
'sensor': ['mqtt'],
|
||||
|
@ -13,7 +13,7 @@ import voluptuous as vol
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['hbmqtt==0.8']
|
||||
REQUIREMENTS = ['hbmqtt==0.9.1']
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
# None allows custom config to be created through generate_config
|
||||
|
@ -9,9 +9,11 @@ import json
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import MATCH_ALL
|
||||
from homeassistant.const import (CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE,
|
||||
CONF_INCLUDE, MATCH_ALL)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.mqtt import valid_publish_topic
|
||||
from homeassistant.helpers.entityfilter import generate_filter
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.remote import JSONEncoder
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -24,6 +26,16 @@ DOMAIN = 'mqtt_statestream'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string])
|
||||
}),
|
||||
vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
||||
vol.Optional(CONF_DOMAINS, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string])
|
||||
}),
|
||||
vol.Required(CONF_BASE_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_PUBLISH_ATTRIBUTES, default=False): cv.boolean,
|
||||
vol.Optional(CONF_PUBLISH_TIMESTAMPS, default=False): cv.boolean
|
||||
@ -36,8 +48,14 @@ def async_setup(hass, config):
|
||||
"""Set up the MQTT state feed."""
|
||||
conf = config.get(DOMAIN, {})
|
||||
base_topic = conf.get(CONF_BASE_TOPIC)
|
||||
pub_include = conf.get(CONF_INCLUDE, {})
|
||||
pub_exclude = conf.get(CONF_EXCLUDE, {})
|
||||
publish_attributes = conf.get(CONF_PUBLISH_ATTRIBUTES)
|
||||
publish_timestamps = conf.get(CONF_PUBLISH_TIMESTAMPS)
|
||||
publish_filter = generate_filter(pub_include.get(CONF_DOMAINS, []),
|
||||
pub_include.get(CONF_ENTITIES, []),
|
||||
pub_exclude.get(CONF_DOMAINS, []),
|
||||
pub_exclude.get(CONF_ENTITIES, []))
|
||||
if not base_topic.endswith('/'):
|
||||
base_topic = base_topic + '/'
|
||||
|
||||
@ -45,6 +63,10 @@ def async_setup(hass, config):
|
||||
def _state_publisher(entity_id, old_state, new_state):
|
||||
if new_state is None:
|
||||
return
|
||||
|
||||
if not publish_filter(entity_id):
|
||||
return
|
||||
|
||||
payload = new_state.state
|
||||
|
||||
mybase = base_topic + entity_id.replace('.', '/') + '/'
|
||||
|
@ -90,7 +90,7 @@ def setup(hass, config):
|
||||
_LOGGER.debug("Failed to login to Neato API")
|
||||
return False
|
||||
hub.update_robots()
|
||||
for component in ('camera', 'sensor', 'switch'):
|
||||
for component in ('camera', 'vacuum', 'switch'):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
@ -6,23 +6,26 @@ https://home-assistant.io/components/no_ip/
|
||||
"""
|
||||
import asyncio
|
||||
import base64
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.hdrs import USER_AGENT, AUTHORIZATION
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, HTTP_HEADER_AUTH,
|
||||
HTTP_HEADER_USER_AGENT, PROJECT_EMAIL)
|
||||
CONF_DOMAIN, CONF_TIMEOUT, CONF_PASSWORD, CONF_USERNAME)
|
||||
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'no_ip'
|
||||
|
||||
# We should set a dedicated address for the user agent.
|
||||
EMAIL = 'hello@home-assistant.io'
|
||||
|
||||
INTERVAL = timedelta(minutes=5)
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
@ -38,7 +41,7 @@ NO_IP_ERRORS = {
|
||||
}
|
||||
|
||||
UPDATE_URL = 'https://dynupdate.noip.com/nic/update'
|
||||
USER_AGENT = "{} {}".format(SERVER_SOFTWARE, PROJECT_EMAIL)
|
||||
HA_USER_AGENT = "{} {}".format(SERVER_SOFTWARE, EMAIL)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@ -89,8 +92,8 @@ def _update_no_ip(hass, session, domain, auth_str, timeout):
|
||||
}
|
||||
|
||||
headers = {
|
||||
HTTP_HEADER_AUTH: "Basic {}".format(auth_str.decode('utf-8')),
|
||||
HTTP_HEADER_USER_AGENT: USER_AGENT,
|
||||
AUTHORIZATION: "Basic {}".format(auth_str.decode('utf-8')),
|
||||
USER_AGENT: HA_USER_AGENT,
|
||||
}
|
||||
|
||||
try:
|
||||
|
@ -17,7 +17,7 @@ from homeassistant.const import CONF_NAME, CONF_PLATFORM
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import template as template_helper
|
||||
|
||||
REQUIREMENTS = ['apns2==0.1.1']
|
||||
REQUIREMENTS = ['apns2==0.3.0']
|
||||
|
||||
APNS_DEVICES = 'apns.yaml'
|
||||
CONF_CERTFILE = 'cert_file'
|
||||
|
@ -6,22 +6,22 @@ https://home-assistant.io/components/notify.clicksend/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_API_KEY, CONF_RECIPIENT, HTTP_HEADER_CONTENT_TYPE,
|
||||
CONTENT_TYPE_JSON)
|
||||
from homeassistant.components.notify import (
|
||||
PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_USERNAME, CONF_RECIPIENT, CONTENT_TYPE_JSON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BASE_API_URL = 'https://rest.clicksend.com/v3'
|
||||
|
||||
HEADERS = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON}
|
||||
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
|
@ -8,22 +8,22 @@ https://home-assistant.io/components/notify.clicksend_tts/
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_API_KEY, CONF_RECIPIENT, HTTP_HEADER_CONTENT_TYPE,
|
||||
CONTENT_TYPE_JSON)
|
||||
from homeassistant.components.notify import (
|
||||
PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_USERNAME, CONF_RECIPIENT, CONTENT_TYPE_JSON)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BASE_API_URL = 'https://rest.clicksend.com/v3'
|
||||
|
||||
HEADERS = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON}
|
||||
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
|
||||
|
||||
CONF_LANGUAGE = 'language'
|
||||
CONF_VOICE = 'voice'
|
||||
|
@ -6,14 +6,14 @@ https://home-assistant.io/components/notify.facebook/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import requests
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -70,7 +70,7 @@ class FacebookNotificationService(BaseNotificationService):
|
||||
import json
|
||||
resp = requests.post(BASE_URL, data=json.dumps(body),
|
||||
params=payload,
|
||||
headers={'Content-Type': CONTENT_TYPE_JSON},
|
||||
headers={CONTENT_TYPE: CONTENT_TYPE_JSON},
|
||||
timeout=10)
|
||||
if resp.status_code != 200:
|
||||
obj = resp.json()
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['freesms==0.1.1']
|
||||
REQUIREMENTS = ['freesms==0.1.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -5,27 +5,29 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.html5/
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import logging
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from aiohttp.hdrs import AUTHORIZATION
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.const import (HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR,
|
||||
HTTP_UNAUTHORIZED, URL_ROOT)
|
||||
from homeassistant.util import ensure_unique_string
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_DATA,
|
||||
BaseNotificationService, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.components.frontend import add_manifest_json_key
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA, ATTR_TITLE, ATTR_TARGET, PLATFORM_SCHEMA, ATTR_TITLE_DEFAULT,
|
||||
BaseNotificationService)
|
||||
from homeassistant.const import (
|
||||
URL_ROOT, HTTP_BAD_REQUEST, HTTP_UNAUTHORIZED, HTTP_INTERNAL_SERVER_ERROR)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import ensure_unique_string
|
||||
|
||||
REQUIREMENTS = ['pywebpush==1.1.0', 'PyJWT==1.5.3']
|
||||
REQUIREMENTS = ['pywebpush==1.3.0', 'PyJWT==1.5.3']
|
||||
|
||||
DEPENDENCIES = ['frontend']
|
||||
|
||||
@ -62,24 +64,25 @@ ATTR_JWT = 'jwt'
|
||||
# is valid.
|
||||
JWT_VALID_DAYS = 7
|
||||
|
||||
KEYS_SCHEMA = vol.All(dict,
|
||||
vol.Schema({
|
||||
vol.Required(ATTR_AUTH): cv.string,
|
||||
vol.Required(ATTR_P256DH): cv.string
|
||||
}))
|
||||
KEYS_SCHEMA = vol.All(
|
||||
dict, vol.Schema({
|
||||
vol.Required(ATTR_AUTH): cv.string,
|
||||
vol.Required(ATTR_P256DH): cv.string,
|
||||
})
|
||||
)
|
||||
|
||||
SUBSCRIPTION_SCHEMA = vol.All(dict,
|
||||
vol.Schema({
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(ATTR_ENDPOINT): vol.Url(),
|
||||
vol.Required(ATTR_KEYS): KEYS_SCHEMA,
|
||||
vol.Optional(ATTR_EXPIRATIONTIME):
|
||||
vol.Any(None, cv.positive_int)
|
||||
}))
|
||||
SUBSCRIPTION_SCHEMA = vol.All(
|
||||
dict, vol.Schema({
|
||||
# pylint: disable=no-value-for-parameter
|
||||
vol.Required(ATTR_ENDPOINT): vol.Url(),
|
||||
vol.Required(ATTR_KEYS): KEYS_SCHEMA,
|
||||
vol.Optional(ATTR_EXPIRATIONTIME): vol.Any(None, cv.positive_int),
|
||||
})
|
||||
)
|
||||
|
||||
REGISTER_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_SUBSCRIPTION): SUBSCRIPTION_SCHEMA,
|
||||
vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox'])
|
||||
vol.Required(ATTR_BROWSER): vol.In(['chrome', 'firefox']),
|
||||
})
|
||||
|
||||
CALLBACK_EVENT_PAYLOAD_SCHEMA = vol.Schema({
|
||||
@ -123,21 +126,11 @@ def get_service(hass, config, discovery_info=None):
|
||||
|
||||
def _load_config(filename):
|
||||
"""Load configuration."""
|
||||
if not os.path.isfile(filename):
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(filename, 'r') as fdesc:
|
||||
inp = fdesc.read()
|
||||
|
||||
# In case empty file
|
||||
if not inp:
|
||||
return {}
|
||||
|
||||
return json.loads(inp)
|
||||
except (IOError, ValueError) as error:
|
||||
_LOGGER.error("Reading config file %s failed: %s", filename, error)
|
||||
return None
|
||||
return load_json(filename)
|
||||
except HomeAssistantError:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
class JSONBytesDecoder(json.JSONEncoder):
|
||||
@ -145,24 +138,12 @@ class JSONBytesDecoder(json.JSONEncoder):
|
||||
|
||||
# pylint: disable=method-hidden
|
||||
def default(self, obj):
|
||||
"""Decode object if it's a bytes object, else defer to baseclass."""
|
||||
"""Decode object if it's a bytes object, else defer to base class."""
|
||||
if isinstance(obj, bytes):
|
||||
return obj.decode()
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
def _save_config(filename, config):
|
||||
"""Save configuration."""
|
||||
try:
|
||||
with open(filename, 'w') as fdesc:
|
||||
fdesc.write(json.dumps(
|
||||
config, cls=JSONBytesDecoder, indent=4, sort_keys=True))
|
||||
except (IOError, TypeError) as error:
|
||||
_LOGGER.error("Saving config file failed: %s", error)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class HTML5PushRegistrationView(HomeAssistantView):
|
||||
"""Accepts push registrations from a browser."""
|
||||
|
||||
@ -192,7 +173,7 @@ class HTML5PushRegistrationView(HomeAssistantView):
|
||||
|
||||
self.registrations[name] = data
|
||||
|
||||
if not _save_config(self.json_path, self.registrations):
|
||||
if not save_json(self.json_path, self.registrations):
|
||||
return self.json_message(
|
||||
'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR)
|
||||
|
||||
@ -221,7 +202,7 @@ class HTML5PushRegistrationView(HomeAssistantView):
|
||||
|
||||
reg = self.registrations.pop(found)
|
||||
|
||||
if not _save_config(self.json_path, self.registrations):
|
||||
if not save_json(self.json_path, self.registrations):
|
||||
self.registrations[found] = reg
|
||||
return self.json_message(
|
||||
'Error saving registration.', HTTP_INTERNAL_SERVER_ERROR)
|
||||
@ -266,7 +247,7 @@ class HTML5PushCallbackView(HomeAssistantView):
|
||||
def check_authorization_header(self, request):
|
||||
"""Check the authorization header."""
|
||||
import jwt
|
||||
auth = request.headers.get('Authorization', None)
|
||||
auth = request.headers.get(AUTHORIZATION, None)
|
||||
if not auth:
|
||||
return self.json_message('Authorization header is expected',
|
||||
status_code=HTTP_UNAUTHORIZED)
|
||||
@ -323,8 +304,7 @@ class HTML5PushCallbackView(HomeAssistantView):
|
||||
event_name = '{}.{}'.format(NOTIFY_CALLBACK_EVENT,
|
||||
event_payload[ATTR_TYPE])
|
||||
request.app['hass'].bus.fire(event_name, event_payload)
|
||||
return self.json({'status': 'ok',
|
||||
'event': event_payload[ATTR_TYPE]})
|
||||
return self.json({'status': 'ok', 'event': event_payload[ATTR_TYPE]})
|
||||
|
||||
|
||||
class HTML5NotificationService(BaseNotificationService):
|
||||
@ -410,9 +390,9 @@ class HTML5NotificationService(BaseNotificationService):
|
||||
if response.status_code == 410:
|
||||
_LOGGER.info("Notification channel has expired")
|
||||
reg = self.registrations.pop(target)
|
||||
if not _save_config(self.registrations_json_path,
|
||||
self.registrations):
|
||||
if not save_json(self.registrations_json_path,
|
||||
self.registrations):
|
||||
self.registrations[target] = reg
|
||||
_LOGGER.error("Error saving registration.")
|
||||
_LOGGER.error("Error saving registration")
|
||||
else:
|
||||
_LOGGER.info("Configuration saved")
|
||||
|
@ -7,14 +7,14 @@ https://home-assistant.io/components/notify.instapush/
|
||||
import json
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
|
||||
ATTR_TITLE, PLATFORM_SCHEMA, ATTR_TITLE_DEFAULT, BaseNotificationService)
|
||||
from homeassistant.const import CONF_API_KEY, CONTENT_TYPE_JSON
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_RESOURCE = 'https://api.instapush.im/v1/'
|
||||
@ -76,7 +76,7 @@ class InstapushNotificationService(BaseNotificationService):
|
||||
self._headers = {
|
||||
HTTP_HEADER_APPID: self._api_key,
|
||||
HTTP_HEADER_APPSECRET: self._app_secret,
|
||||
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
|
||||
CONTENT_TYPE: CONTENT_TYPE_JSON,
|
||||
}
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
|
@ -13,9 +13,10 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.const import CONF_ICON
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.components.lametric import DOMAIN
|
||||
from homeassistant.components.lametric import DOMAIN as LAMETRIC_DOMAIN
|
||||
|
||||
REQUIREMENTS = ['lmnotify==0.0.4']
|
||||
DEPENDENCIES = ['lametric']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -30,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
# pylint: disable=unused-variable
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the Slack notification service."""
|
||||
hlmn = hass.data.get(DOMAIN)
|
||||
hlmn = hass.data.get(LAMETRIC_DOMAIN)
|
||||
return LaMetricNotificationService(hlmn,
|
||||
config[CONF_ICON],
|
||||
config[CONF_DISPLAY_TIME] * 1000)
|
||||
@ -49,6 +50,7 @@ class LaMetricNotificationService(BaseNotificationService):
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to some LaMetric deviced."""
|
||||
from lmnotify import SimpleFrame, Sound, Model
|
||||
from oauthlib.oauth2 import TokenExpiredError
|
||||
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
@ -76,16 +78,16 @@ class LaMetricNotificationService(BaseNotificationService):
|
||||
|
||||
frames = [text_frame]
|
||||
|
||||
if sound is not None:
|
||||
frames.append(sound)
|
||||
|
||||
_LOGGER.debug(frames)
|
||||
|
||||
model = Model(frames=frames)
|
||||
lmn = self.hasslametricmanager.manager()
|
||||
devices = lmn.get_devices()
|
||||
model = Model(frames=frames, sound=sound)
|
||||
lmn = self.hasslametricmanager.manager
|
||||
try:
|
||||
devices = lmn.get_devices()
|
||||
except TokenExpiredError:
|
||||
_LOGGER.debug("Token expired, fetching new token")
|
||||
lmn.get_token()
|
||||
devices = lmn.get_devices()
|
||||
for dev in devices:
|
||||
if (targets is None) or (dev["name"] in targets):
|
||||
if targets is None or dev["name"] in targets:
|
||||
lmn.set_device(dev)
|
||||
lmn.send_notification(model, lifetime=self._display_time)
|
||||
_LOGGER.debug("Sent notification to LaMetric %s", dev["name"])
|
||||
|
@ -10,7 +10,8 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT, CONTENT_TYPE_TEXT_PLAIN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['sendgrid==5.3.0']
|
||||
@ -67,7 +68,7 @@ class SendgridNotificationService(BaseNotificationService):
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text/plain",
|
||||
"type": CONTENT_TYPE_TEXT_PLAIN,
|
||||
"value": message
|
||||
}
|
||||
]
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
||||
ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
|
||||
REQUIREMENTS = ['simplepush==1.1.3']
|
||||
REQUIREMENTS = ['simplepush==1.1.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -21,6 +21,7 @@ DEPENDENCIES = [DOMAIN]
|
||||
ATTR_KEYBOARD = 'keyboard'
|
||||
ATTR_INLINE_KEYBOARD = 'inline_keyboard'
|
||||
ATTR_PHOTO = 'photo'
|
||||
ATTR_VIDEO = 'video'
|
||||
ATTR_DOCUMENT = 'document'
|
||||
|
||||
CONF_CHAT_ID = 'chat_id'
|
||||
@ -63,7 +64,7 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
keys = keys if isinstance(keys, list) else [keys]
|
||||
service_data.update(inline_keyboard=keys)
|
||||
|
||||
# Send a photo, a document or a location
|
||||
# Send a photo, video, document, or location
|
||||
if data is not None and ATTR_PHOTO in data:
|
||||
photos = data.get(ATTR_PHOTO, None)
|
||||
photos = photos if isinstance(photos, list) else [photos]
|
||||
@ -72,6 +73,14 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
self.hass.services.call(
|
||||
DOMAIN, 'send_photo', service_data=service_data)
|
||||
return
|
||||
elif data is not None and ATTR_VIDEO in data:
|
||||
videos = data.get(ATTR_VIDEO, None)
|
||||
videos = videos if isinstance(videos, list) else [videos]
|
||||
for video_data in videos:
|
||||
service_data.update(video_data)
|
||||
self.hass.services.call(
|
||||
DOMAIN, 'send_video', service_data=service_data)
|
||||
return
|
||||
elif data is not None and ATTR_LOCATION in data:
|
||||
service_data.update(data.get(ATTR_LOCATION))
|
||||
return self.hass.services.call(
|
||||
|
@ -6,12 +6,13 @@ https://home-assistant.io/components/notify.telstra/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
BaseNotificationService, ATTR_TITLE, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import CONTENT_TYPE_JSON, HTTP_HEADER_CONTENT_TYPE
|
||||
ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import CONTENT_TYPE_JSON
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -73,8 +74,8 @@ class TelstraNotificationService(BaseNotificationService):
|
||||
}
|
||||
message_resource = 'https://api.telstra.com/v1/sms/messages'
|
||||
message_headers = {
|
||||
HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
|
||||
'Authorization': 'Bearer ' + token_response['access_token'],
|
||||
CONTENT_TYPE: CONTENT_TYPE_JSON,
|
||||
AUTHORIZATION: 'Bearer {}'.format(token_response['access_token']),
|
||||
}
|
||||
message_response = requests.post(
|
||||
message_resource, headers=message_headers, json=message_data,
|
||||
|
@ -9,6 +9,7 @@ import time
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
from aiohttp.hdrs import CONTENT_TYPE
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -55,8 +56,10 @@ class OctoPrintAPI(object):
|
||||
def __init__(self, api_url, key, bed, number_of_tools):
|
||||
"""Initialize OctoPrint API and set headers needed later."""
|
||||
self.api_url = api_url
|
||||
self.headers = {'content-type': CONTENT_TYPE_JSON,
|
||||
'X-Api-Key': key}
|
||||
self.headers = {
|
||||
CONTENT_TYPE: CONTENT_TYPE_JSON,
|
||||
'X-Api-Key': key,
|
||||
}
|
||||
self.printer_last_reading = [{}, None]
|
||||
self.job_last_reading = [{}, None]
|
||||
self.job_available = False
|
||||
|
@ -140,6 +140,7 @@ def execute(hass, filename, source, data=None):
|
||||
builtins = safe_builtins.copy()
|
||||
builtins.update(utility_builtins)
|
||||
builtins['datetime'] = datetime
|
||||
builtins['sorted'] = sorted
|
||||
builtins['time'] = TimeWrapper()
|
||||
builtins['dt_util'] = dt_util
|
||||
restricted_globals = {
|
||||
|
@ -36,7 +36,7 @@ from . import purge, migration
|
||||
from .const import DATA_INSTANCE
|
||||
from .util import session_scope
|
||||
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.14']
|
||||
REQUIREMENTS = ['sqlalchemy==1.1.15']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -28,5 +28,10 @@ def purge_old_data(instance, purge_days):
|
||||
# Execute sqlite vacuum command to free up space on disk
|
||||
_LOGGER.debug("DB engine driver: %s", instance.engine.driver)
|
||||
if instance.engine.driver == 'pysqlite':
|
||||
from sqlalchemy import exc
|
||||
|
||||
_LOGGER.info("Vacuuming SQLite to free space")
|
||||
instance.engine.execute("VACUUM")
|
||||
try:
|
||||
instance.engine.execute("VACUUM")
|
||||
except exc.OperationalError as err:
|
||||
_LOGGER.error("Error vacuuming SQLite: %s.", err)
|
||||
|
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