Merge pull request #10630 from home-assistant/release-0-58

0.58
This commit is contained in:
Paulus Schoutsen 2017-11-17 22:27:35 -08:00 committed by GitHub
commit d9a6d9ee73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
229 changed files with 7265 additions and 2497 deletions

View File

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

View File

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

View File

@ -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']

View File

@ -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],

View File

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

View File

@ -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."""

View File

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

View File

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

View File

@ -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."""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."""

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
"""

View File

@ -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,
}

View File

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

View File

@ -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."""

View File

@ -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."""

View File

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

View 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'

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

@ -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"}}"""

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

View File

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

View File

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

View File

@ -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,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
request_sync:
description: Send a request_sync command to Google.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."""

View File

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

View File

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

View File

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

View File

@ -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']

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'],

View File

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

View File

@ -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('.', '/') + '/'

View File

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

View File

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

View File

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

View 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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
]

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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