diff --git a/.coveragerc b/.coveragerc
index 5134f79297c..01187b92d66 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -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
diff --git a/CODEOWNERS b/CODEOWNERS
index 8fd5d0826c1..82ae451e59c 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 8ca22e1a126..595c15717eb 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -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']
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 4de464be88a..64ad88f8c8b 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -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],
diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py
index 581045c3790..b4c6adcc887 100644
--- a/homeassistant/components/abode.py
+++ b/homeassistant/components/abode.py
@@ -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__)
diff --git a/homeassistant/components/alarm_control_panel/spc.py b/homeassistant/components/alarm_control_panel/spc.py
index 1682ef2ae02..4d9c72df2f1 100644
--- a/homeassistant/components/alarm_control_panel/spc.py
+++ b/homeassistant/components/alarm_control_panel/spc.py
@@ -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."""
diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py
index 7abdf5efcab..423628c9365 100644
--- a/homeassistant/components/alarm_control_panel/totalconnect.py
+++ b/homeassistant/components/alarm_control_panel/totalconnect.py
@@ -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__)
diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py
index 9550b6dbade..c243fc12d5e 100644
--- a/homeassistant/components/alexa/const.py
+++ b/homeassistant/components/alexa/const.py
@@ -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'
diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py
index a0d0062414d..3ade199aabb 100644
--- a/homeassistant/components/alexa/intent.py
+++ b/homeassistant/components/alexa/intent.py
@@ -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."""
diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py
index e65345cabca..6e71fc67df1 100644
--- a/homeassistant/components/alexa/smart_home.py
+++ b/homeassistant/components/alexa/smart_home.py
@@ -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)
diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py
index 5e02f80f229..c8eb1841c0d 100644
--- a/homeassistant/components/apple_tv.py
+++ b/homeassistant/components/apple_tv.py
@@ -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__)
diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py
index f3397a884d1..a78b334de0b 100644
--- a/homeassistant/components/arlo.py
+++ b/homeassistant/components/arlo.py
@@ -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__)
diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py
index 18f2c054b0c..401afe8c62c 100644
--- a/homeassistant/components/axis.py
+++ b/homeassistant/components/axis.py
@@ -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
diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py
index 4ba29e9b2ba..baf9c41cfdf 100644
--- a/homeassistant/components/binary_sensor/__init__.py
+++ b/homeassistant/components/binary_sensor/__init__.py
@@ -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
diff --git a/homeassistant/components/binary_sensor/aurora.py b/homeassistant/components/binary_sensor/aurora.py
index 2530fecb7c1..772792f5785 100644
--- a/homeassistant/components/binary_sensor/aurora.py
+++ b/homeassistant/components/binary_sensor/aurora.py
@@ -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)
diff --git a/homeassistant/components/binary_sensor/spc.py b/homeassistant/components/binary_sensor/spc.py
index af3669c2b15..a3a84580edd 100644
--- a/homeassistant/components/binary_sensor/spc.py
+++ b/homeassistant/components/binary_sensor/spc.py
@@ -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()
diff --git a/homeassistant/components/binary_sensor/vultr.py b/homeassistant/components/binary_sensor/vultr.py
new file mode 100644
index 00000000000..66b5a127be1
--- /dev/null
+++ b/homeassistant/components/binary_sensor/vultr.py
@@ -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]
diff --git a/homeassistant/components/bloomsky.py b/homeassistant/components/bloomsky.py
index aff1c14b252..f04e0af7be9 100644
--- a/homeassistant/components/bloomsky.py
+++ b/homeassistant/components/bloomsky.py
@@ -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:
diff --git a/homeassistant/components/camera/arlo.py b/homeassistant/components/camera/arlo.py
index be58b61fb8c..4f597771726 100644
--- a/homeassistant/components/camera/arlo.py
+++ b/homeassistant/components/camera/arlo.py
@@ -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()
diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py
index 61f5773356f..81a7adca1b7 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -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)
diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py
index d70890317fd..dba096bb632 100644
--- a/homeassistant/components/climate/eq3btsmart.py
+++ b/homeassistant/components/climate/eq3btsmart.py
@@ -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
diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py
index 191960d2848..0c0c837b850 100644
--- a/homeassistant/components/climate/generic_thermostat.py
+++ b/homeassistant/components/climate/generic_thermostat.py
@@ -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:
diff --git a/homeassistant/components/climate/homematic.py b/homeassistant/components/climate/homematic.py
index ce6e9580e54..5236c0788fd 100644
--- a/homeassistant/components/climate/homematic.py
+++ b/homeassistant/components/climate/homematic.py
@@ -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."""
diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py
index 784d8a4ed28..69c144985d6 100644
--- a/homeassistant/components/climate/knx.py
+++ b/homeassistant/components/climate/knx.py
@@ -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
diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py
index f72cefc0841..54d8d8617c7 100644
--- a/homeassistant/components/climate/wink.py
+++ b/homeassistant/components/climate/wink.py
@@ -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
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index c5d709d60c3..e6da2de40f2 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -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,
diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py
index 50a88d4be4d..cb9fe15ab4a 100644
--- a/homeassistant/components/cloud/auth_api.py
+++ b/homeassistant/components/cloud/auth_api.py
@@ -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()
diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py
index 334e522f81b..440e4179eea 100644
--- a/homeassistant/components/cloud/const.py
+++ b/homeassistant/components/cloud/const.py
@@ -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.
+"""
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index aa91f5a45e7..d16df130c48 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -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,
}
diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py
index 1bb6668e0cc..91ad1cfc6ff 100644
--- a/homeassistant/components/cloud/iot.py
+++ b/homeassistant/components/cloud/iot.py
@@ -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')
diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py
index 53fa200a1b1..c839ab7bc6e 100644
--- a/homeassistant/components/config/zwave.py
+++ b/homeassistant/components/config/zwave.py
@@ -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."""
diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py
index 2da8967bddf..7d1b1fd7ef1 100644
--- a/homeassistant/components/configurator.py
+++ b/homeassistant/components/configurator.py
@@ -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."""
diff --git a/homeassistant/components/counter.py b/homeassistant/components/counter/__init__.py
similarity index 96%
rename from homeassistant/components/counter.py
rename to homeassistant/components/counter/__init__.py
index 64421306644..aee94c069f6 100644
--- a/homeassistant/components/counter.py
+++ b/homeassistant/components/counter/__init__.py
@@ -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
diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml
new file mode 100644
index 00000000000..ef76f9b9eac
--- /dev/null
+++ b/homeassistant/components/counter/services.yaml
@@ -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'
\ No newline at end of file
diff --git a/homeassistant/components/cover/lutron_caseta.py b/homeassistant/components/cover/lutron_caseta.py
index 31e4f1e3cf2..6ad9b093ed8 100644
--- a/homeassistant/components/cover/lutron_caseta.py
+++ b/homeassistant/components/cover/lutron_caseta.py
@@ -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)
diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py
index d10166a9469..0a49679b9c4 100644
--- a/homeassistant/components/cover/mqtt.py
+++ b/homeassistant/components/cover/mqtt.py
@@ -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
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index 05131a039cd..0b18cc72f6e 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -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(
diff --git a/homeassistant/components/device_tracker/hitron_coda.py b/homeassistant/components/device_tracker/hitron_coda.py
new file mode 100644
index 00000000000..17dc34d1040
--- /dev/null
+++ b/homeassistant/components/device_tracker/hitron_coda.py
@@ -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
diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py
index 77241e1a8ab..0c869dd4b57 100644
--- a/homeassistant/components/device_tracker/owntracks.py
+++ b/homeassistant/components/device_tracker/owntracks.py
@@ -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)
diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py
index 8c1bf6dc67b..add027e1823 100644
--- a/homeassistant/components/device_tracker/snmp.py
+++ b/homeassistant/components/device_tracker/snmp.py
@@ -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'
diff --git a/homeassistant/components/device_tracker/swisscom.py b/homeassistant/components/device_tracker/swisscom.py
index e64d30942ca..d5826ecedff 100644
--- a/homeassistant/components/device_tracker/swisscom.py
+++ b/homeassistant/components/device_tracker/swisscom.py
@@ -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"}}"""
diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py
new file mode 100644
index 00000000000..f27a950a49f
--- /dev/null
+++ b/homeassistant/components/device_tracker/tile.py
@@ -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
+ )
diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py
index a52de48d061..6c5fb697c07 100755
--- a/homeassistant/components/device_tracker/tplink.py
+++ b/homeassistant/components/device_tracker/tplink.py
@@ -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:
diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py
index 338ce34048e..fbcd753713c 100644
--- a/homeassistant/components/device_tracker/upc_connect.py
+++ b/homeassistant/components/device_tracker/upc_connect.py
@@ -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())
diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow.py
index 3f2cae112f5..726b8d99e01 100644
--- a/homeassistant/components/dialogflow.py
+++ b/homeassistant/components/dialogflow.py
@@ -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,
}
diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py
index 07f432a5218..d832bbdfdd1 100644
--- a/homeassistant/components/downloader.py
+++ b/homeassistant/components/downloader.py
@@ -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)
diff --git a/homeassistant/components/enocean.py b/homeassistant/components/enocean.py
index 3c3eefe54cc..879f6a61899 100644
--- a/homeassistant/components/enocean.py
+++ b/homeassistant/components/enocean.py
@@ -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)
diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py
index 3b0e0385f13..8fc77d1bf5e 100644
--- a/homeassistant/components/fan/xiaomi_miio.py
+++ b/homeassistant/components/fan/xiaomi_miio.py
@@ -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'
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index f9c9e2ddcaf..e7cfcf8d88c 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -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')
diff --git a/homeassistant/components/frontend/templates/index.html b/homeassistant/components/frontend/templates/index.html
deleted file mode 100644
index c941fbc15ae..00000000000
--- a/homeassistant/components/frontend/templates/index.html
+++ /dev/null
@@ -1,118 +0,0 @@
-
-
-
-
- Home Assistant
-
-
-
-
-
- {% if not dev_mode %}
-
- {% for panel in panels.values() -%}
-
- {% endfor -%}
- {% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Home Assistant had trouble
connecting to the server.
-
TRY AGAIN
-
-
-
- {# #}
-
-
- {% if not dev_mode %}
-
- {% endif %}
-
-
- {% if panel_url -%}
-
- {% endif -%}
-
- {% for extra_url in extra_urls -%}
-
- {% endfor -%}
-
-
diff --git a/homeassistant/components/gc100.py b/homeassistant/components/gc100.py
index 7c772e345ae..bc627d44417 100644
--- a/homeassistant/components/gc100.py
+++ b/homeassistant/components/gc100.py
@@ -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()
diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py
index 53de8764a12..2db36d8829f 100644
--- a/homeassistant/components/google_assistant/__init__.py
+++ b/homeassistant/components/google_assistant/__init__.py
@@ -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
diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py
index 80afad82938..c15f14bccdb 100644
--- a/homeassistant/components/google_assistant/const.py
+++ b/homeassistant/components/google_assistant/const.py
@@ -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'
diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py
index adc626f73b7..ab9705432fb 100644
--- a/homeassistant/components/google_assistant/http.py
+++ b/homeassistant/components/google_assistant/http.py
@@ -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}
diff --git a/homeassistant/components/google_assistant/services.yaml b/homeassistant/components/google_assistant/services.yaml
new file mode 100644
index 00000000000..6019b75bd98
--- /dev/null
+++ b/homeassistant/components/google_assistant/services.yaml
@@ -0,0 +1,2 @@
+request_sync:
+ description: Send a request_sync command to Google.
\ No newline at end of file
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index 1c8adf3d8f7..cd1583fb377 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -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)
diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py
index 940de2ba12f..048a7d531f4 100644
--- a/homeassistant/components/hassio.py
+++ b/homeassistant/components/hassio.py
@@ -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({
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index f402a9d6892..17ceccfd218 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -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()
diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py
index 4b971c883d3..ce5bfca3ac1 100644
--- a/homeassistant/components/http/auth.py
+++ b/homeassistant/components/http/auth.py
@@ -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):
diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index aa01ccde8d7..f636ad80c36 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -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
diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py
index f991a4ee0fc..c9b094e3f2e 100644
--- a/homeassistant/components/http/static.py
+++ b/homeassistant/components/http/static.py
@@ -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)
diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py
index 1c261d5ec3e..b41deb5e5e3 100644
--- a/homeassistant/components/influxdb.py
+++ b/homeassistant/components/influxdb.py
@@ -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):
diff --git a/homeassistant/components/input_datetime.py b/homeassistant/components/input_datetime.py
index 9dd09f2c245..fecc31f14ae 100644
--- a/homeassistant/components/input_datetime.py
+++ b/homeassistant/components/input_datetime.py
@@ -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."""
diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py
index b86574c1d2e..3966b490f52 100644
--- a/homeassistant/components/knx.py
+++ b/homeassistant/components/knx.py
@@ -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,
diff --git a/homeassistant/components/lametric.py b/homeassistant/components/lametric.py
index b11d874127f..49b4f73ea17 100644
--- a/homeassistant/components/lametric.py
+++ b/homeassistant/components/lametric.py
@@ -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
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index d69d6991ff0..e4fb4542205 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -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)
diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py
index 22ab404a3b2..d01611716eb 100644
--- a/homeassistant/components/light/demo.py
+++ b/homeassistant/components/light/demo.py
@@ -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']
diff --git a/homeassistant/components/light/lutron_caseta.py b/homeassistant/components/light/lutron_caseta.py
index c11b3da6f75..e4e1baf6c58 100644
--- a/homeassistant/components/light/lutron_caseta.py
+++ b/homeassistant/components/light/lutron_caseta.py
@@ -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)
diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py
index 63441e6d8af..c3632351e5f 100644
--- a/homeassistant/components/light/tradfri.py
+++ b/homeassistant/components/light/tradfri.py
@@ -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
diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py
index b25f2745365..df716bcf1e9 100644
--- a/homeassistant/components/light/xiaomi_miio.py
+++ b/homeassistant/components/light/xiaomi_miio.py
@@ -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:
diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py
index 8660546c910..63f0315f35c 100644
--- a/homeassistant/components/lutron_caseta.py
+++ b/homeassistant/components/lutron_caseta.py
@@ -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):
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index 9cee62c39f7..d1f7f89863c 100644
--- a/homeassistant/components/media_extractor.py
+++ b/homeassistant/components/media_extractor.py
@@ -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__)
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index f037dfb708e..89686c312bd 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -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)
diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py
index c1b9bab6937..1f86056efb5 100644
--- a/homeassistant/components/media_player/bluesound.py
+++ b/homeassistant/components/media_player/bluesound.py
@@ -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
diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py
index 3f1607831e5..54015bec277 100644
--- a/homeassistant/components/media_player/snapcast.py
+++ b/homeassistant/components/media_player/snapcast.py
@@ -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)
diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py
index 9647f04f5c3..a7173e35a48 100644
--- a/homeassistant/components/media_player/universal.py
+++ b/homeassistant/components/media_player/universal.py
@@ -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:
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index 8df8ceb0a8e..3215ad82a7c 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -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):
diff --git a/homeassistant/components/media_player/yamaha_musiccast.py b/homeassistant/components/media_player/yamaha_musiccast.py
index 27efc4f3814..bfcffff6bb4 100644
--- a/homeassistant/components/media_player/yamaha_musiccast.py
+++ b/homeassistant/components/media_player/yamaha_musiccast.py
@@ -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()
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index 9decc9a14aa..3a6abec0ddf 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -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:
diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py
index 7140423633e..b6f6a1c5a92 100644
--- a/homeassistant/components/mqtt/discovery.py
+++ b/homeassistant/components/mqtt/discovery.py
@@ -20,10 +20,12 @@ TOPIC_MATCHER = re.compile(
r'(?P\w+)/(?P\w+)/'
r'(?:(?P[a-zA-Z0-9_-]+)/)?(?P[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'],
diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py
index 0e866723b34..db251ab4180 100644
--- a/homeassistant/components/mqtt/server.py
+++ b/homeassistant/components/mqtt/server.py
@@ -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
diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py
index d24361637e9..4427870c294 100644
--- a/homeassistant/components/mqtt_statestream.py
+++ b/homeassistant/components/mqtt_statestream.py
@@ -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('.', '/') + '/'
diff --git a/homeassistant/components/neato.py b/homeassistant/components/neato.py
index 2401bc6604f..e10878833e4 100644
--- a/homeassistant/components/neato.py
+++ b/homeassistant/components/neato.py
@@ -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
diff --git a/homeassistant/components/no_ip.py b/homeassistant/components/no_ip.py
index d92cd752aef..6051fa85f55 100644
--- a/homeassistant/components/no_ip.py
+++ b/homeassistant/components/no_ip.py
@@ -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:
diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py
index 250ef5c50c8..f6f7cc71f14 100644
--- a/homeassistant/components/notify/apns.py
+++ b/homeassistant/components/notify/apns.py
@@ -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'
diff --git a/homeassistant/components/notify/clicksend.py b/homeassistant/components/notify/clicksend.py
index 663f689a975..543ce434a8d 100644
--- a/homeassistant/components/notify/clicksend.py
+++ b/homeassistant/components/notify/clicksend.py
@@ -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,
diff --git a/homeassistant/components/notify/clicksend_tts.py b/homeassistant/components/notify/clicksend_tts.py
index f951dd00307..26a29993290 100644
--- a/homeassistant/components/notify/clicksend_tts.py
+++ b/homeassistant/components/notify/clicksend_tts.py
@@ -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'
diff --git a/homeassistant/components/notify/facebook.py b/homeassistant/components/notify/facebook.py
index db175c6b0a6..791440fdb5b 100644
--- a/homeassistant/components/notify/facebook.py
+++ b/homeassistant/components/notify/facebook.py
@@ -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()
diff --git a/homeassistant/components/notify/free_mobile.py b/homeassistant/components/notify/free_mobile.py
index 92ea75a79dc..a27d0495193 100644
--- a/homeassistant/components/notify/free_mobile.py
+++ b/homeassistant/components/notify/free_mobile.py
@@ -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__)
diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py
index 1b44ec60722..2314722a2ab 100644
--- a/homeassistant/components/notify/html5.py
+++ b/homeassistant/components/notify/html5.py
@@ -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")
diff --git a/homeassistant/components/notify/instapush.py b/homeassistant/components/notify/instapush.py
index 39cdf0fc475..e792045ec80 100644
--- a/homeassistant/components/notify/instapush.py
+++ b/homeassistant/components/notify/instapush.py
@@ -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):
diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py
index a3af1eb1914..56030afb30c 100644
--- a/homeassistant/components/notify/lametric.py
+++ b/homeassistant/components/notify/lametric.py
@@ -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"])
diff --git a/homeassistant/components/notify/sendgrid.py b/homeassistant/components/notify/sendgrid.py
index b0185218846..89117397a53 100644
--- a/homeassistant/components/notify/sendgrid.py
+++ b/homeassistant/components/notify/sendgrid.py
@@ -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
}
]
diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py
index b4c65d116c4..9d5c58fc5b1 100644
--- a/homeassistant/components/notify/simplepush.py
+++ b/homeassistant/components/notify/simplepush.py
@@ -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__)
diff --git a/homeassistant/components/notify/telegram.py b/homeassistant/components/notify/telegram.py
index fb453263dd8..899ccf9b09a 100644
--- a/homeassistant/components/notify/telegram.py
+++ b/homeassistant/components/notify/telegram.py
@@ -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(
diff --git a/homeassistant/components/notify/telstra.py b/homeassistant/components/notify/telstra.py
index 7fabb51eac8..82ac914a647 100644
--- a/homeassistant/components/notify/telstra.py
+++ b/homeassistant/components/notify/telstra.py
@@ -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,
diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py
index fdf237d7180..086242ab070 100644
--- a/homeassistant/components/octoprint.py
+++ b/homeassistant/components/octoprint.py
@@ -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
diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py
index 75b2a1fed71..85f12a18afd 100644
--- a/homeassistant/components/python_script.py
+++ b/homeassistant/components/python_script.py
@@ -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 = {
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index e9b08941b83..f8ae9e9d0be 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -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__)
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index 90a69f8f2a1..719f65abb47 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -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)
diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py
index 41dbec851b5..3f1086c46c7 100755
--- a/homeassistant/components/remote/__init__.py
+++ b/homeassistant/components/remote/__init__.py
@@ -61,8 +61,7 @@ REMOTE_SERVICE_SEND_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
vol.Optional(ATTR_DEVICE): cv.string,
vol.Optional(
ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS): cv.positive_int,
- vol.Optional(
- ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float)
+ vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float),
})
@@ -141,25 +140,18 @@ def async_setup(hass, config):
def async_handle_remote_service(service):
"""Handle calls to the remote services."""
target_remotes = component.async_extract_from_service(service)
-
- activity_id = service.data.get(ATTR_ACTIVITY)
- device = service.data.get(ATTR_DEVICE)
- command = service.data.get(ATTR_COMMAND)
- num_repeats = service.data.get(ATTR_NUM_REPEATS)
- delay_secs = service.data.get(ATTR_DELAY_SECS)
+ kwargs = service.data.copy()
update_tasks = []
for remote in target_remotes:
if service.service == SERVICE_TURN_ON:
- yield from remote.async_turn_on(activity=activity_id)
+ yield from remote.async_turn_on(**kwargs)
elif service.service == SERVICE_TOGGLE:
- yield from remote.async_toggle(activity=activity_id)
+ yield from remote.async_toggle(**kwargs)
elif service.service == SERVICE_SEND_COMMAND:
- yield from remote.async_send_command(
- device=device, command=command,
- num_repeats=num_repeats, delay_secs=delay_secs)
+ yield from remote.async_send_command(**kwargs)
else:
- yield from remote.async_turn_off(activity=activity_id)
+ yield from remote.async_turn_off(**kwargs)
if not remote.should_poll:
continue
diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py
index b25741207de..7a398def5f9 100755
--- a/homeassistant/components/remote/harmony.py
+++ b/homeassistant/components/remote/harmony.py
@@ -5,22 +5,23 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/remote.harmony/
"""
import logging
+import asyncio
from os import path
-import urllib.parse
+import time
import voluptuous as vol
import homeassistant.components.remote as remote
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
- CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID)
+ CONF_NAME, CONF_HOST, CONF_PORT, ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP)
from homeassistant.components.remote import (
PLATFORM_SCHEMA, DOMAIN, ATTR_DEVICE, ATTR_ACTIVITY, ATTR_NUM_REPEATS,
- ATTR_DELAY_SECS)
+ ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
from homeassistant.util import slugify
from homeassistant.config import load_yaml_config_file
-REQUIREMENTS = ['pyharmony==1.0.16']
+REQUIREMENTS = ['pyharmony==1.0.18']
_LOGGER = logging.getLogger(__name__)
@@ -35,6 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Required(ATTR_ACTIVITY, default=None): cv.string,
+ vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS):
+ vol.Coerce(float),
})
HARMONY_SYNC_SCHEMA = vol.Schema({
@@ -44,8 +47,6 @@ HARMONY_SYNC_SCHEMA = vol.Schema({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Harmony platform."""
- import pyharmony
-
host = None
activity = None
@@ -61,6 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = DEFAULT_PORT
if override:
activity = override.get(ATTR_ACTIVITY)
+ delay_secs = override.get(ATTR_DELAY_SECS)
port = override.get(CONF_PORT, DEFAULT_PORT)
host = (
@@ -79,6 +81,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get(CONF_PORT),
)
activity = config.get(ATTR_ACTIVITY)
+ delay_secs = config.get(ATTR_DELAY_SECS)
else:
hass.data[CONF_DEVICE_CACHE].append(config)
return
@@ -86,26 +89,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name, address, port = host
_LOGGER.info("Loading Harmony Platform: %s at %s:%s, startup activity: %s",
name, address, port, activity)
- try:
- _LOGGER.debug("Calling pyharmony.ha_get_token for remote at: %s:%s",
- address, port)
- token = urllib.parse.quote_plus(pyharmony.ha_get_token(address, port))
- _LOGGER.debug("Received token: %s", token)
- except ValueError as err:
- _LOGGER.warning("%s for remote: %s", err.args[0], name)
- return False
harmony_conf_file = hass.config.path(
'{}{}{}'.format('harmony_', slugify(name), '.conf'))
- device = HarmonyRemote(
- name, address, port,
- activity, harmony_conf_file, token)
-
- DEVICES.append(device)
-
- add_devices([device])
- register_services(hass)
- return True
+ try:
+ device = HarmonyRemote(
+ name, address, port, activity, harmony_conf_file, delay_secs)
+ DEVICES.append(device)
+ add_devices([device])
+ register_services(hass)
+ except ValueError:
+ _LOGGER.warning("Failed to initialize remote: %s", name)
def register_services(hass):
@@ -140,7 +134,7 @@ def _sync_service(service):
class HarmonyRemote(remote.RemoteDevice):
"""Remote representation used to control a Harmony device."""
- def __init__(self, name, host, port, activity, out_path, token):
+ def __init__(self, name, host, port, activity, out_path, delay_secs):
"""Initialize HarmonyRemote class."""
import pyharmony
from pathlib import Path
@@ -152,20 +146,35 @@ class HarmonyRemote(remote.RemoteDevice):
self._state = None
self._current_activity = None
self._default_activity = activity
- self._token = token
+ self._client = pyharmony.get_client(host, port, self.new_activity)
self._config_path = out_path
- _LOGGER.debug("Retrieving harmony config using token: %s", token)
- self._config = pyharmony.ha_get_config(self._token, host, port)
+ self._config = self._client.get_config()
if not Path(self._config_path).is_file():
_LOGGER.debug("Writing harmony configuration to file: %s",
out_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
+ self._delay_secs = delay_secs
+
+ @asyncio.coroutine
+ def async_added_to_hass(self):
+ """Complete the initialization."""
+ self.hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_STOP,
+ lambda event: self._client.disconnect(wait=True))
+
+ # Poll for initial state
+ self.new_activity(self._client.get_current_activity())
@property
def name(self):
"""Return the Harmony device's name."""
return self._name
+ @property
+ def should_poll(self):
+ """Return the fact that we should not be polled."""
+ return False
+
@property
def device_state_attributes(self):
"""Add platform specific attributes."""
@@ -176,60 +185,51 @@ class HarmonyRemote(remote.RemoteDevice):
"""Return False if PowerOff is the current activity, otherwise True."""
return self._current_activity not in [None, 'PowerOff']
- def update(self):
- """Return current activity."""
+ def new_activity(self, activity_id):
+ """Callback for updating the current activity."""
import pyharmony
- name = self._name
- _LOGGER.debug("Polling %s for current activity", name)
- state = pyharmony.ha_get_current_activity(
- self._token, self._config, self.host, self._port)
- _LOGGER.debug("%s current activity reported as: %s", name, state)
- self._current_activity = state
- self._state = bool(state != 'PowerOff')
+ activity_name = pyharmony.activity_name(self._config, activity_id)
+ _LOGGER.debug("%s activity reported as: %s", self._name, activity_name)
+ self._current_activity = activity_name
+ self._state = bool(self._current_activity != 'PowerOff')
+ self.schedule_update_ha_state()
def turn_on(self, **kwargs):
"""Start an activity from the Harmony device."""
import pyharmony
- if kwargs[ATTR_ACTIVITY]:
- activity = kwargs[ATTR_ACTIVITY]
- else:
- activity = self._default_activity
+ activity = kwargs.get(ATTR_ACTIVITY, self._default_activity)
if activity:
- pyharmony.ha_start_activity(
- self._token, self.host, self._port, self._config, activity)
+ activity_id = pyharmony.activity_id(self._config, activity)
+ self._client.start_activity(activity_id)
self._state = True
else:
_LOGGER.error("No activity specified with turn_on service")
def turn_off(self, **kwargs):
"""Start the PowerOff activity."""
- import pyharmony
- pyharmony.ha_power_off(self._token, self.host, self._port)
+ self._client.power_off()
- def send_command(self, command, **kwargs):
- """Send a set of commands to one device."""
- import pyharmony
- device = kwargs.pop(ATTR_DEVICE, None)
+ def send_command(self, commands, **kwargs):
+ """Send a list of commands to one device."""
+ device = kwargs.get(ATTR_DEVICE)
if device is None:
_LOGGER.error("Missing required argument: device")
return
- params = {}
- num_repeats = kwargs.pop(ATTR_NUM_REPEATS, None)
- if num_repeats is not None:
- params['repeat_num'] = num_repeats
- delay_secs = kwargs.pop(ATTR_DELAY_SECS, None)
- if delay_secs is not None:
- params['delay_secs'] = delay_secs
- pyharmony.ha_send_commands(
- self._token, self.host, self._port, device, command, **params)
+
+ num_repeats = kwargs.get(ATTR_NUM_REPEATS)
+ delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs)
+
+ for _ in range(num_repeats):
+ for command in commands:
+ self._client.send_command(device, command)
+ time.sleep(delay_secs)
def sync(self):
"""Sync the Harmony device with the web service."""
import pyharmony
_LOGGER.debug("Syncing hub with Harmony servers")
- pyharmony.ha_sync(self._token, self.host, self._port)
- self._config = pyharmony.ha_get_config(
- self._token, self.host, self._port)
+ self._client.sync()
+ self._config = self._client.get_config()
_LOGGER.debug("Writing hub config to file: %s", self._config_path)
pyharmony.ha_write_config_file(self._config, self._config_path)
diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py
index 701889d60b5..c16164d7700 100644
--- a/homeassistant/components/ring.py
+++ b/homeassistant/components/ring.py
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from requests.exceptions import HTTPError, ConnectTimeout
-REQUIREMENTS = ['ring_doorbell==0.1.6']
+REQUIREMENTS = ['ring_doorbell==0.1.7']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/scene/lifx_cloud.py b/homeassistant/components/scene/lifx_cloud.py
index e6f5be71a80..ffbb10cba4e 100644
--- a/homeassistant/components/scene/lifx_cloud.py
+++ b/homeassistant/components/scene/lifx_cloud.py
@@ -7,15 +7,15 @@ https://home-assistant.io/components/scene.lifx_cloud/
import asyncio
import logging
+import aiohttp
+from aiohttp.hdrs import AUTHORIZATION
+import async_timeout
import voluptuous as vol
-import aiohttp
-import async_timeout
-
from homeassistant.components.scene import Scene
-from homeassistant.const import (CONF_PLATFORM, CONF_TOKEN, CONF_TIMEOUT)
+from homeassistant.const import CONF_TOKEN, CONF_TIMEOUT, CONF_PLATFORM
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.aiohttp_client import (async_get_clientsession)
_LOGGER = logging.getLogger(__name__)
@@ -37,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
timeout = config.get(CONF_TIMEOUT)
headers = {
- "Authorization": "Bearer %s" % token,
+ AUTHORIZATION: "Bearer {}".format(token),
}
url = LIFX_API_URL.format('scenes')
diff --git a/homeassistant/components/scene/lutron_caseta.py b/homeassistant/components/scene/lutron_caseta.py
index b98f7f3e6ea..53df0da7617 100644
--- a/homeassistant/components/scene/lutron_caseta.py
+++ b/homeassistant/components/scene/lutron_caseta.py
@@ -4,17 +4,19 @@ Support for Lutron Caseta scenes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.lutron_caseta/
"""
+import asyncio
import logging
-from homeassistant.components.scene import Scene
from homeassistant.components.lutron_caseta import LUTRON_CASETA_SMARTBRIDGE
+from homeassistant.components.scene import Scene
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['lutron_caseta']
-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]
@@ -23,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = LutronCasetaScene(scenes[scene], bridge)
devs.append(dev)
- add_devices(devs, True)
+ async_add_devices(devs, True)
class LutronCasetaScene(Scene):
@@ -50,6 +52,7 @@ class LutronCasetaScene(Scene):
"""There is no way of detecting if a scene is active (yet)."""
return False
- def activate(self, **kwargs):
+ @asyncio.coroutine
+ def async_activate(self, **kwargs):
"""Activate the scene."""
self._bridge.activate_scene(self._scene_id)
diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py
index 56ddf7adcab..5ea24dab823 100644
--- a/homeassistant/components/sensor/airvisual.py
+++ b/homeassistant/components/sensor/airvisual.py
@@ -126,7 +126,7 @@ class AirVisualBaseSensor(Entity):
def __init__(self, data, name, icon, locale):
"""Initialize the sensor."""
- self._data = data
+ self.data = data
self._icon = icon
self._locale = locale
self._name = name
@@ -136,20 +136,17 @@ class AirVisualBaseSensor(Entity):
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- attrs = {
+ attrs = merge_two_dicts({
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
- ATTR_CITY: self._data.city,
- ATTR_COUNTRY: self._data.country,
- ATTR_REGION: self._data.state,
- ATTR_TIMESTAMP: self._data.pollution_info.get('ts')
- }
+ ATTR_TIMESTAMP: self.data.pollution_info.get('ts')
+ }, self.data.attrs)
- if self._data.show_on_map:
- attrs[ATTR_LATITUDE] = self._data.latitude
- attrs[ATTR_LONGITUDE] = self._data.longitude
+ if self.data.show_on_map:
+ attrs[ATTR_LATITUDE] = self.data.latitude
+ attrs[ATTR_LONGITUDE] = self.data.longitude
else:
- attrs['lati'] = self._data.latitude
- attrs['long'] = self._data.longitude
+ attrs['lati'] = self.data.latitude
+ attrs['long'] = self.data.longitude
return attrs
@@ -174,9 +171,9 @@ class AirPollutionLevelSensor(AirVisualBaseSensor):
def update(self):
"""Update the status of the sensor."""
- self._data.update()
+ self.data.update()
- aqi = self._data.pollution_info.get('aqi{0}'.format(self._locale))
+ aqi = self.data.pollution_info.get('aqi{0}'.format(self._locale))
try:
[level] = [
i for i in POLLUTANT_LEVEL_MAPPING
@@ -199,9 +196,9 @@ class AirQualityIndexSensor(AirVisualBaseSensor):
def update(self):
"""Update the status of the sensor."""
- self._data.update()
+ self.data.update()
- self._state = self._data.pollution_info.get(
+ self._state = self.data.pollution_info.get(
'aqi{0}'.format(self._locale))
@@ -224,9 +221,9 @@ class MainPollutantSensor(AirVisualBaseSensor):
def update(self):
"""Update the status of the sensor."""
- self._data.update()
+ self.data.update()
- symbol = self._data.pollution_info.get('main{0}'.format(self._locale))
+ symbol = self.data.pollution_info.get('main{0}'.format(self._locale))
pollution_info = POLLUTANT_MAPPING.get(symbol, {})
self._state = pollution_info.get('label')
self._unit = pollution_info.get('unit')
@@ -239,6 +236,7 @@ class AirVisualData(object):
def __init__(self, client, **kwargs):
"""Initialize the AirVisual data element."""
self._client = client
+ self.attrs = {}
self.pollution_info = None
self.city = kwargs.get(CONF_CITY)
@@ -260,17 +258,20 @@ class AirVisualData(object):
if self.city and self.state and self.country:
resp = self._client.city(
self.city, self.state, self.country).get('data')
+ self.longitude, self.latitude = resp.get('location').get(
+ 'coordinates')
else:
resp = self._client.nearest_city(
self.latitude, self.longitude, self._radius).get('data')
_LOGGER.debug("New data retrieved: %s", resp)
- self.city = resp.get('city')
- self.state = resp.get('state')
- self.country = resp.get('country')
- self.longitude, self.latitude = resp.get('location').get(
- 'coordinates')
self.pollution_info = resp.get('current', {}).get('pollution', {})
+
+ self.attrs = {
+ ATTR_CITY: resp.get('city'),
+ ATTR_REGION: resp.get('state'),
+ ATTR_COUNTRY: resp.get('country')
+ }
except exceptions.HTTPError as exc_info:
_LOGGER.error("Unable to retrieve data on this location: %s",
self.__dict__)
diff --git a/homeassistant/components/sensor/arlo.py b/homeassistant/components/sensor/arlo.py
index f665d8e70ab..97b7ac22909 100644
--- a/homeassistant/components/sensor/arlo.py
+++ b/homeassistant/components/sensor/arlo.py
@@ -29,7 +29,8 @@ SENSOR_TYPES = {
'last_capture': ['Last', None, 'run-fast'],
'total_cameras': ['Arlo Cameras', None, 'video'],
'captured_today': ['Captured Today', None, 'file-video'],
- 'battery_level': ['Battery Level', '%', 'battery-50']
+ 'battery_level': ['Battery Level', '%', 'battery-50'],
+ 'signal_strength': ['Signal Strength', None, 'signal']
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -97,6 +98,16 @@ class ArloSensor(Entity):
def update(self):
"""Get the latest data and updates the state."""
+ try:
+ base_station = self._data.base_station
+ except (AttributeError, IndexError):
+ return
+
+ if not base_station:
+ return
+
+ base_station.refresh_rate = SCAN_INTERVAL.total_seconds()
+
self._data.update()
if self._sensor_type == 'total_cameras':
@@ -114,7 +125,13 @@ class ArloSensor(Entity):
elif self._sensor_type == 'battery_level':
try:
- self._state = self._data.get_battery_level
+ self._state = self._data.battery_level
+ except TypeError:
+ self._state = None
+
+ elif self._sensor_type == 'signal_strength':
+ try:
+ self._state = self._data.signal_strength
except TypeError:
self._state = None
@@ -128,7 +145,8 @@ class ArloSensor(Entity):
if self._sensor_type == 'last_capture' or \
self._sensor_type == 'captured_today' or \
- self._sensor_type == 'battery_level':
+ self._sensor_type == 'battery_level' or \
+ self._sensor_type == 'signal_strength':
attrs['model'] = self._data.model_id
return attrs
diff --git a/homeassistant/components/sensor/haveibeenpwned.py b/homeassistant/components/sensor/haveibeenpwned.py
index 1c28db9a9df..3b041127a5b 100644
--- a/homeassistant/components/sensor/haveibeenpwned.py
+++ b/homeassistant/components/sensor/haveibeenpwned.py
@@ -7,24 +7,28 @@ https://home-assistant.io/components/sensor.haveibeenpwned/
from datetime import timedelta
import logging
-import voluptuous as vol
+from aiohttp.hdrs import USER_AGENT
import requests
+import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (STATE_UNKNOWN, CONF_EMAIL)
-from homeassistant.helpers.entity import Entity
+from homeassistant.const import CONF_EMAIL
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import track_point_in_time
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
-from homeassistant.helpers.event import track_point_in_time
_LOGGER = logging.getLogger(__name__)
DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
-USER_AGENT = "Home Assistant HaveIBeenPwned Sensor Component"
-MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
+HA_USER_AGENT = "Home Assistant HaveIBeenPwned Sensor Component"
+
MIN_TIME_BETWEEN_FORCED_UPDATES = timedelta(seconds=5)
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
+
+URL = 'https://haveibeenpwned.com/api/v2/breachedaccount/'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_EMAIL): vol.All(cv.ensure_list, [cv.string]),
@@ -33,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the HaveIBeenPwnedSensor sensor."""
+ """Set up the HaveIBeenPwned sensor."""
emails = config.get(CONF_EMAIL)
data = HaveIBeenPwnedData(emails)
@@ -50,11 +54,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class HaveIBeenPwnedSensor(Entity):
- """Implementation of a HaveIBeenPwnedSensor."""
+ """Implementation of a HaveIBeenPwned sensor."""
def __init__(self, data, hass, email):
- """Initialize the HaveIBeenPwnedSensor sensor."""
- self._state = STATE_UNKNOWN
+ """Initialize the HaveIBeenPwned sensor."""
+ self._state = None
self._data = data
self._hass = hass
self._email = email
@@ -77,7 +81,7 @@ class HaveIBeenPwnedSensor(Entity):
@property
def device_state_attributes(self):
- """Return the atrributes of the sensor."""
+ """Return the attributes of the sensor."""
val = {}
if self._email not in self._data.data:
return val
@@ -143,17 +147,16 @@ class HaveIBeenPwnedData(object):
def update(self, **kwargs):
"""Get the latest data for current email from REST service."""
try:
- url = "https://haveibeenpwned.com/api/v2/breachedaccount/{}". \
- format(self._email)
+ url = "{}{}".format(URL, self._email)
- _LOGGER.info("Checking for breaches for email %s", self._email)
+ _LOGGER.debug("Checking for breaches for email: %s", self._email)
- req = requests.get(url, headers={"User-agent": USER_AGENT},
- allow_redirects=True, timeout=5)
+ req = requests.get(
+ url, headers={USER_AGENT: HA_USER_AGENT}, allow_redirects=True,
+ timeout=5)
except requests.exceptions.RequestException:
- _LOGGER.error("Failed fetching HaveIBeenPwned Data for %s",
- self._email)
+ _LOGGER.error("Failed fetching data for %s", self._email)
return
if req.status_code == 200:
@@ -161,7 +164,7 @@ class HaveIBeenPwnedData(object):
key=lambda k: k["AddedDate"],
reverse=True)
- # only goto next email if we had data so that
+ # Only goto next email if we had data so that
# the forced updates try this current email again
self.set_next_email()
@@ -173,6 +176,6 @@ class HaveIBeenPwnedData(object):
self.set_next_email()
else:
- _LOGGER.error("Failed fetching HaveIBeenPwned Data for %s"
+ _LOGGER.error("Failed fetching data for %s"
"(HTTP Status_code = %d)", self._email,
req.status_code)
diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py
new file mode 100755
index 00000000000..28cba7da0b4
--- /dev/null
+++ b/homeassistant/components/sensor/lacrosse.py
@@ -0,0 +1,217 @@
+"""
+Support for LaCrosse sensor components.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.lacrosse/
+"""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.core import callback
+from homeassistant.components.sensor import (ENTITY_ID_FORMAT, PLATFORM_SCHEMA)
+from homeassistant.const import (
+ EVENT_HOMEASSISTANT_STOP, CONF_DEVICE, CONF_NAME, CONF_ID,
+ CONF_SENSORS, CONF_TYPE, TEMP_CELSIUS)
+from homeassistant.helpers.entity import Entity, async_generate_entity_id
+from homeassistant.helpers.event import async_track_point_in_utc_time
+from homeassistant.util import dt as dt_util
+
+REQUIREMENTS = ['pylacrosse==0.2.7']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_BAUD = 'baud'
+CONF_EXPIRE_AFTER = 'expire_after'
+
+DEFAULT_DEVICE = '/dev/ttyUSB0'
+DEFAULT_BAUD = '57600'
+DEFAULT_EXPIRE_AFTER = 300
+
+TYPES = ['battery', 'humidity', 'temperature']
+
+SENSOR_SCHEMA = vol.Schema({
+ vol.Required(CONF_ID): cv.positive_int,
+ vol.Required(CONF_TYPE): vol.In(TYPES),
+ vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
+ vol.Optional(CONF_NAME): cv.string,
+})
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
+ vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string,
+ vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the LaCrosse sensors."""
+ import pylacrosse
+ from serial import SerialException
+
+ usb_device = config.get(CONF_DEVICE)
+ baud = int(config.get(CONF_BAUD))
+ expire_after = config.get(CONF_EXPIRE_AFTER)
+
+ _LOGGER.debug("%s %s", usb_device, baud)
+
+ try:
+ lacrosse = pylacrosse.LaCrosse(usb_device, baud)
+ lacrosse.open()
+ except SerialException as exc:
+ _LOGGER.warning("Unable to open serial port: %s", exc)
+ return False
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lacrosse.close)
+
+ sensors = []
+ for device, device_config in config[CONF_SENSORS].items():
+ _LOGGER.debug("%s %s", device, device_config)
+
+ typ = device_config.get(CONF_TYPE)
+ sensor_class = TYPE_CLASSES[typ]
+ name = device_config.get(CONF_NAME, device)
+
+ sensors.append(
+ sensor_class(
+ hass, lacrosse, device, name, expire_after, device_config
+ )
+ )
+
+ add_devices(sensors)
+
+
+class LaCrosseSensor(Entity):
+ """Implementation of a Lacrosse sensor."""
+
+ _temperature = None
+ _humidity = None
+ _low_battery = None
+ _new_battery = None
+
+ def __init__(self, hass, lacrosse, device_id, name, expire_after, config):
+ """Initialize the sensor."""
+ self.hass = hass
+ self.entity_id = async_generate_entity_id(
+ ENTITY_ID_FORMAT, device_id, hass=hass)
+ self._config = config
+ self._name = name
+ self._value = None
+ self._expire_after = expire_after
+ self._expiration_trigger = None
+
+ lacrosse.register_callback(
+ int(self._config['id']), self._callback_lacrosse, None)
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ def update(self, *args):
+ """Get the latest data."""
+ pass
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ attributes = {
+ 'low_battery': self._low_battery,
+ 'new_battery': self._new_battery,
+ }
+ return attributes
+
+ def _callback_lacrosse(self, lacrosse_sensor, user_data):
+ """Callback function that is called from pylacrosse with new values."""
+ if self._expire_after is not None and self._expire_after > 0:
+ # Reset old trigger
+ if self._expiration_trigger:
+ self._expiration_trigger()
+ self._expiration_trigger = None
+
+ # Set new trigger
+ expiration_at = (
+ dt_util.utcnow() + timedelta(seconds=self._expire_after))
+
+ self._expiration_trigger = async_track_point_in_utc_time(
+ self.hass, self.value_is_expired, expiration_at)
+
+ self._temperature = round(lacrosse_sensor.temperature * 2) / 2
+ self._humidity = lacrosse_sensor.humidity
+ self._low_battery = lacrosse_sensor.low_battery
+ self._new_battery = lacrosse_sensor.new_battery
+
+ @callback
+ def value_is_expired(self, *_):
+ """Triggered when value is expired."""
+ self._expiration_trigger = None
+ self._value = None
+ self.async_schedule_update_ha_state()
+
+
+class LaCrosseTemperature(LaCrosseSensor):
+ """Implementation of a Lacrosse temperature sensor."""
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return TEMP_CELSIUS
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._temperature
+
+
+class LaCrosseHumidity(LaCrosseSensor):
+ """Implementation of a Lacrosse humidity sensor."""
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return '%'
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._humidity
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return 'mdi:water-percent'
+
+
+class LaCrosseBattery(LaCrosseSensor):
+ """Implementation of a Lacrosse battery sensor."""
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ if self._low_battery is None:
+ state = None
+ elif self._low_battery is True:
+ state = 'low'
+ else:
+ state = 'ok'
+ return state
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ if self._low_battery is None:
+ icon = 'mdi:battery-unknown'
+ elif self._low_battery is True:
+ icon = 'mdi:battery-alert'
+ else:
+ icon = 'mdi:battery'
+ return icon
+
+
+TYPE_CLASSES = {
+ 'temperature': LaCrosseTemperature,
+ 'humidity': LaCrosseHumidity,
+ 'battery': LaCrosseBattery
+}
diff --git a/homeassistant/components/sensor/london_air.py b/homeassistant/components/sensor/london_air.py
index 7a8ad4087b0..848e1255833 100644
--- a/homeassistant/components/sensor/london_air.py
+++ b/homeassistant/components/sensor/london_air.py
@@ -31,7 +31,6 @@ AUTHORITIES = [
'Enfield',
'Greenwich',
'Hackney',
- 'Hammersmith and Fulham',
'Haringey',
'Harrow',
'Havering',
diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py
index 0b2198bd396..c4014fbd1dd 100644
--- a/homeassistant/components/sensor/modbus.py
+++ b/homeassistant/components/sensor/modbus.py
@@ -11,7 +11,8 @@ import voluptuous as vol
import homeassistant.components.modbus as modbus
from homeassistant.const import (
- CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE)
+ CONF_NAME, CONF_OFFSET, CONF_UNIT_OF_MEASUREMENT, CONF_SLAVE,
+ CONF_STRUCTURE)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -21,6 +22,7 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['modbus']
CONF_COUNT = 'count'
+CONF_REVERSE_ORDER = 'reverse_order'
CONF_PRECISION = 'precision'
CONF_REGISTER = 'register'
CONF_REGISTERS = 'registers'
@@ -32,7 +34,9 @@ REGISTER_TYPE_HOLDING = 'holding'
REGISTER_TYPE_INPUT = 'input'
DATA_TYPE_INT = 'int'
+DATA_TYPE_UINT = 'uint'
DATA_TYPE_FLOAT = 'float'
+DATA_TYPE_CUSTOM = 'custom'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_REGISTERS): [{
@@ -41,12 +45,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING):
vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]),
vol.Optional(CONF_COUNT, default=1): cv.positive_int,
+ vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean,
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
vol.Optional(CONF_PRECISION, default=0): cv.positive_int,
vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
vol.Optional(CONF_SLAVE): cv.positive_int,
vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT):
- vol.In([DATA_TYPE_INT, DATA_TYPE_FLOAT]),
+ vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT,
+ DATA_TYPE_CUSTOM]),
+ vol.Optional(CONF_STRUCTURE): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string
}]
})
@@ -55,7 +62,37 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Modbus sensors."""
sensors = []
+ data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}}
+ data_types[DATA_TYPE_UINT] = {1: 'H', 2: 'I', 4: 'Q'}
+ data_types[DATA_TYPE_FLOAT] = {1: 'e', 2: 'f', 4: 'd'}
+
for register in config.get(CONF_REGISTERS):
+ structure = '>i'
+ if register.get(CONF_DATA_TYPE) != DATA_TYPE_CUSTOM:
+ try:
+ structure = '>{}'.format(data_types[
+ register.get(CONF_DATA_TYPE)][register.get(CONF_COUNT)])
+ except KeyError:
+ _LOGGER.error("Unable to detect data type for %s sensor, "
+ "try a custom type.", register.get(CONF_NAME))
+ continue
+ else:
+ structure = register.get(CONF_STRUCTURE)
+
+ try:
+ size = struct.calcsize(structure)
+ except struct.error as err:
+ _LOGGER.error(
+ "Error in sensor %s structure: %s",
+ register.get(CONF_NAME), err)
+ continue
+
+ if register.get(CONF_COUNT) * 2 != size:
+ _LOGGER.error(
+ "Structure size (%d bytes) mismatch registers count "
+ "(%d words)", size, register.get(CONF_COUNT))
+ continue
+
sensors.append(ModbusRegisterSensor(
register.get(CONF_NAME),
register.get(CONF_SLAVE),
@@ -63,10 +100,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
register.get(CONF_REGISTER_TYPE),
register.get(CONF_UNIT_OF_MEASUREMENT),
register.get(CONF_COUNT),
+ register.get(CONF_REVERSE_ORDER),
register.get(CONF_SCALE),
register.get(CONF_OFFSET),
- register.get(CONF_DATA_TYPE),
+ structure,
register.get(CONF_PRECISION)))
+
+ if not sensors:
+ return False
add_devices(sensors)
@@ -74,8 +115,8 @@ class ModbusRegisterSensor(Entity):
"""Modbus register sensor."""
def __init__(self, name, slave, register, register_type,
- unit_of_measurement, count, scale, offset, data_type,
- precision):
+ unit_of_measurement, count, reverse_order, scale, offset,
+ structure, precision):
"""Initialize the modbus register sensor."""
self._name = name
self._slave = int(slave) if slave else None
@@ -83,10 +124,11 @@ class ModbusRegisterSensor(Entity):
self._register_type = register_type
self._unit_of_measurement = unit_of_measurement
self._count = int(count)
+ self._reverse_order = reverse_order
self._scale = scale
self._offset = offset
self._precision = precision
- self._data_type = data_type
+ self._structure = structure
self._value = None
@property
@@ -120,17 +162,15 @@ class ModbusRegisterSensor(Entity):
try:
registers = result.registers
+ if self._reverse_order:
+ registers.reverse()
except AttributeError:
- _LOGGER.error("No response from modbus slave %s register %s",
+ _LOGGER.error("No response from modbus slave %s, register %s",
self._slave, self._register)
return
- if self._data_type == DATA_TYPE_FLOAT:
- byte_string = b''.join(
- [x.to_bytes(2, byteorder='big') for x in registers]
- )
- val = struct.unpack(">f", byte_string)[0]
- elif self._data_type == DATA_TYPE_INT:
- for i, res in enumerate(registers):
- val += res * (2**(i*16))
+ byte_string = b''.join(
+ [x.to_bytes(2, byteorder='big') for x in registers]
+ )
+ val = struct.unpack(self._structure, byte_string)[0]
self._value = format(
self._scale * val + self._offset, '.{}f'.format(self._precision))
diff --git a/homeassistant/components/sensor/neato.py b/homeassistant/components/sensor/neato.py
deleted file mode 100644
index 5179816eb35..00000000000
--- a/homeassistant/components/sensor/neato.py
+++ /dev/null
@@ -1,174 +0,0 @@
-"""
-Support for Neato Connected Vaccums sensors.
-
-For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/sensor.neato/
-"""
-import logging
-import requests
-from homeassistant.helpers.entity import Entity
-from homeassistant.components.neato import (
- NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS)
-
-_LOGGER = logging.getLogger(__name__)
-
-DEPENDENCIES = ['neato']
-
-SENSOR_TYPE_STATUS = 'status'
-SENSOR_TYPE_BATTERY = 'battery'
-
-SENSOR_TYPES = {
- SENSOR_TYPE_STATUS: ['Status'],
- SENSOR_TYPE_BATTERY: ['Battery']
-}
-
-ATTR_CLEAN_START = 'clean_start'
-ATTR_CLEAN_STOP = 'clean_stop'
-ATTR_CLEAN_AREA = 'clean_area'
-ATTR_CLEAN_BATTERY_START = 'battery_level_at_clean_start'
-ATTR_CLEAN_BATTERY_END = 'battery_level_at_clean_end'
-ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count'
-ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time'
-
-
-def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the Neato sensor platform."""
- dev = []
- for robot in hass.data[NEATO_ROBOTS]:
- for type_name in SENSOR_TYPES:
- dev.append(NeatoConnectedSensor(hass, robot, type_name))
- _LOGGER.debug("Adding sensors %s", dev)
- add_devices(dev)
-
-
-class NeatoConnectedSensor(Entity):
- """Neato Connected Sensor."""
-
- def __init__(self, hass, robot, sensor_type):
- """Initialize the Neato Connected sensor."""
- self.type = sensor_type
- self.robot = robot
- self.neato = hass.data[NEATO_LOGIN]
- self._robot_name = self.robot.name + ' ' + SENSOR_TYPES[self.type][0]
- self._status_state = None
- try:
- self._state = self.robot.state
- except (requests.exceptions.ConnectionError,
- requests.exceptions.HTTPError) as ex:
- self._state = None
- _LOGGER.warning("Neato connection error: %s", ex)
- self._mapdata = hass.data[NEATO_MAP_DATA]
- self.clean_time_start = None
- self.clean_time_stop = None
- self.clean_area = None
- self.clean_battery_start = None
- self.clean_battery_end = None
- self.clean_suspension_charge_count = None
- self.clean_suspension_time = None
- self._battery_state = None
-
- def update(self):
- """Update the properties of sensor."""
- _LOGGER.debug('Update of sensor')
- self.neato.update_robots()
- self._mapdata = self.hass.data[NEATO_MAP_DATA]
- try:
- self._state = self.robot.state
- except (requests.exceptions.ConnectionError,
- requests.exceptions.HTTPError) as ex:
- self._state = None
- self._status_state = 'Offline'
- _LOGGER.warning("Neato connection error: %s", ex)
- return
- if not self._state:
- return
- _LOGGER.debug('self._state=%s', self._state)
- if self.type == SENSOR_TYPE_STATUS:
- if self._state['state'] == 1:
- if self._state['details']['isCharging']:
- self._status_state = 'Charging'
- elif (self._state['details']['isDocked'] and
- not self._state['details']['isCharging']):
- self._status_state = 'Docked'
- else:
- self._status_state = 'Stopped'
- elif self._state['state'] == 2:
- if ALERTS.get(self._state['error']) is None:
- self._status_state = (
- MODE.get(self._state['cleaning']['mode'])
- + ' ' + ACTION.get(self._state['action']))
- else:
- self._status_state = ALERTS.get(self._state['error'])
- elif self._state['state'] == 3:
- self._status_state = 'Paused'
- elif self._state['state'] == 4:
- self._status_state = ERRORS.get(self._state['error'])
- if self.type == SENSOR_TYPE_BATTERY:
- self._battery_state = self._state['details']['charge']
- if not self._mapdata.get(self.robot.serial, {}).get('maps', []):
- return
- self.clean_time_start = (
- (self._mapdata[self.robot.serial]['maps'][0]['start_at']
- .strip('Z'))
- .replace('T', ' '))
- self.clean_time_stop = (
- (self._mapdata[self.robot.serial]['maps'][0]['end_at'].strip('Z'))
- .replace('T', ' '))
- self.clean_area = (
- self._mapdata[self.robot.serial]['maps'][0]['cleaned_area'])
- self.clean_suspension_charge_count = (
- self._mapdata[self.robot.serial]['maps'][0]
- ['suspended_cleaning_charging_count'])
- self.clean_suspension_time = (
- self._mapdata[self.robot.serial]['maps'][0]
- ['time_in_suspended_cleaning'])
- self.clean_battery_start = (
- self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_start'])
- self.clean_battery_end = (
- self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end'])
-
- @property
- def unit_of_measurement(self):
- """Return unit for the sensor."""
- if self.type == SENSOR_TYPE_BATTERY:
- return '%'
-
- @property
- def available(self):
- """Return True if sensor data is available."""
- return self._state
-
- @property
- def state(self):
- """Return the sensor state."""
- if self.type == SENSOR_TYPE_STATUS:
- return self._status_state
- if self.type == SENSOR_TYPE_BATTERY:
- return self._battery_state
-
- @property
- def name(self):
- """Return the name of the sensor."""
- return self._robot_name
-
- @property
- def device_state_attributes(self):
- """Return the device specific attributes."""
- data = {}
- if self.type is SENSOR_TYPE_STATUS:
- if self.clean_time_start:
- data[ATTR_CLEAN_START] = self.clean_time_start
- if self.clean_time_stop:
- data[ATTR_CLEAN_STOP] = self.clean_time_stop
- if self.clean_area:
- data[ATTR_CLEAN_AREA] = self.clean_area
- if self.clean_suspension_charge_count:
- data[ATTR_CLEAN_SUSP_COUNT] = (
- self.clean_suspension_charge_count)
- if self.clean_suspension_time:
- data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time
- if self.clean_battery_start:
- data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start
- if self.clean_battery_end:
- data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end
- return data
diff --git a/homeassistant/components/sensor/nederlandse_spoorwegen.py b/homeassistant/components/sensor/nederlandse_spoorwegen.py
index e8d3aa41c6c..3535e00d79b 100644
--- a/homeassistant/components/sensor/nederlandse_spoorwegen.py
+++ b/homeassistant/components/sensor/nederlandse_spoorwegen.py
@@ -135,6 +135,10 @@ class NSDepartureSensor(Entity):
'departure_delay':
self._trips[0].departure_time_planned !=
self._trips[0].departure_time_actual,
+ 'departure_platform':
+ self._trips[0].trip_parts[0].stops[0].platform,
+ 'departure_platform_changed':
+ self._trips[0].trip_parts[0].stops[0].platform_changed,
'arrival_time_planned':
self._trips[0].arrival_time_planned.strftime('%H:%M'),
'arrival_time_actual':
@@ -142,6 +146,10 @@ class NSDepartureSensor(Entity):
'arrival_delay':
self._trips[0].arrival_time_planned !=
self._trips[0].arrival_time_actual,
+ 'arrival_platform':
+ self._trips[0].trip_parts[0].stops[-1].platform,
+ 'arrival_platform_changed':
+ self._trips[0].trip_parts[0].stops[-1].platform_changed,
'next':
self._trips[1].departure_time_actual.strftime('%H:%M'),
'status': self._trips[0].status.lower(),
diff --git a/homeassistant/components/sensor/nzbget.py b/homeassistant/components/sensor/nzbget.py
index a440074b81b..b140d02af04 100644
--- a/homeassistant/components/sensor/nzbget.py
+++ b/homeassistant/components/sensor/nzbget.py
@@ -4,19 +4,20 @@ Support for monitoring NZBGet NZB client.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.nzbget/
"""
-import logging
from datetime import timedelta
+import logging
+from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT,
- CONF_SSL, CONTENT_TYPE_JSON, CONF_MONITORED_VARIABLES)
+ CONF_SSL, CONF_HOST, CONF_NAME, CONF_PORT, CONF_PASSWORD, CONF_USERNAME,
+ CONTENT_TYPE_JSON, CONF_MONITORED_VARIABLES)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -145,7 +146,7 @@ class NZBGetAPI(object):
"""Initialize NZBGet API and set headers needed later."""
self.api_url = api_url
self.status = None
- self.headers = {'content-type': CONTENT_TYPE_JSON}
+ self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON}
if username is not None and password is not None:
self.auth = (username, password)
@@ -155,7 +156,7 @@ class NZBGetAPI(object):
def post(self, method, params=None):
"""Send a POST request and return the response as a dict."""
- payload = {"method": method}
+ payload = {'method': method}
if params:
payload['params'] = params
diff --git a/homeassistant/components/sensor/pyload.py b/homeassistant/components/sensor/pyload.py
new file mode 100644
index 00000000000..9e1c0875169
--- /dev/null
+++ b/homeassistant/components/sensor/pyload.py
@@ -0,0 +1,170 @@
+"""
+Support for monitoring pyLoad.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.pyload/
+"""
+from datetime import timedelta
+import logging
+
+from aiohttp.hdrs import CONTENT_TYPE
+import requests
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_SSL, CONF_HOST, CONF_NAME, CONF_PORT, CONF_PASSWORD, CONF_USERNAME,
+ CONTENT_TYPE_JSON, CONF_MONITORED_VARIABLES)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_HOST = 'localhost'
+DEFAULT_NAME = 'pyLoad'
+DEFAULT_PORT = 8000
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15)
+
+SENSOR_TYPES = {
+ 'speed': ['speed', 'Speed', 'MB/s'],
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
+ vol.Optional(CONF_MONITORED_VARIABLES, default=['speed']):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_SSL, default=False): cv.boolean,
+ vol.Optional(CONF_USERNAME): cv.string,
+})
+
+
+# pylint: disable=unused-argument
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the pyLoad sensors."""
+ host = config.get(CONF_HOST)
+ port = config.get(CONF_PORT)
+ ssl = 's' if config.get(CONF_SSL) else ''
+ name = config.get(CONF_NAME)
+ username = config.get(CONF_USERNAME)
+ password = config.get(CONF_PASSWORD)
+ monitored_types = config.get(CONF_MONITORED_VARIABLES)
+ url = "http{}://{}:{}/api/".format(ssl, host, port)
+
+ try:
+ pyloadapi = PyLoadAPI(
+ api_url=url, username=username, password=password)
+ pyloadapi.update()
+ except (requests.exceptions.ConnectionError,
+ requests.exceptions.HTTPError) as conn_err:
+ _LOGGER.error("Error setting up pyLoad API: %s", conn_err)
+ return False
+
+ devices = []
+ for ng_type in monitored_types:
+ new_sensor = PyLoadSensor(
+ api=pyloadapi, sensor_type=SENSOR_TYPES.get(ng_type),
+ client_name=name)
+ devices.append(new_sensor)
+
+ add_devices(devices, True)
+
+
+class PyLoadSensor(Entity):
+ """Representation of a pyLoad sensor."""
+
+ def __init__(self, api, sensor_type, client_name):
+ """Initialize a new pyLoad sensor."""
+ self._name = '{} {}'.format(client_name, sensor_type[1])
+ self.type = sensor_type[0]
+ self.api = api
+ self._state = None
+ self._unit_of_measurement = sensor_type[2]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return self._unit_of_measurement
+
+ def update(self):
+ """Update state of sensor."""
+ try:
+ self.api.update()
+ except requests.exceptions.ConnectionError:
+ # Error calling the API, already logged in api.update()
+ return
+
+ if self.api.status is None:
+ _LOGGER.debug("Update of %s requested, but no status is available",
+ self._name)
+ return
+
+ value = self.api.status.get(self.type)
+ if value is None:
+ _LOGGER.warning("Unable to locate value for %s", self.type)
+ return
+
+ if "speed" in self.type and value > 0:
+ # Convert download rate from Bytes/s to MBytes/s
+ self._state = round(value / 2**20, 2)
+ else:
+ self._state = value
+
+
+class PyLoadAPI(object):
+ """Simple wrapper for pyLoad's API."""
+
+ def __init__(self, api_url, username=None, password=None):
+ """Initialize pyLoad API and set headers needed later."""
+ self.api_url = api_url
+ self.status = None
+ self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON}
+
+ if username is not None and password is not None:
+ self.payload = {'username': username, 'password': password}
+ self.login = requests.post(
+ '{}{}'.format(api_url, 'login'), data=self.payload, timeout=5)
+ self.update()
+
+ def post(self, method, params=None):
+ """Send a POST request and return the response as a dict."""
+ payload = {'method': method}
+
+ if params:
+ payload['params'] = params
+
+ try:
+ response = requests.post(
+ '{}{}'.format(self.api_url, 'statusServer'), json=payload,
+ cookies=self.login.cookies, headers=self.headers, timeout=5)
+ response.raise_for_status()
+ _LOGGER.debug("JSON Response: %s", response.json())
+ return response.json()
+
+ except requests.exceptions.ConnectionError as conn_exc:
+ _LOGGER.error("Failed to update pyLoad status. Error: %s",
+ conn_exc)
+ raise
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Update cached response."""
+ try:
+ self.status = self.post('speed')
+ except requests.exceptions.ConnectionError:
+ # Failed to update status - exception already logged in self.post
+ raise
diff --git a/homeassistant/components/sensor/serial.py b/homeassistant/components/sensor/serial.py
index ffa8bcc3070..df0f1e21625 100644
--- a/homeassistant/components/sensor/serial.py
+++ b/homeassistant/components/sensor/serial.py
@@ -6,12 +6,14 @@ https://home-assistant.io/components/sensor.serial/
"""
import asyncio
import logging
+import json
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
+from homeassistant.const import (
+ CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP)
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyserial-asyncio==0.4']
@@ -19,12 +21,17 @@ REQUIREMENTS = ['pyserial-asyncio==0.4']
_LOGGER = logging.getLogger(__name__)
CONF_SERIAL_PORT = 'serial_port'
+CONF_BAUDRATE = 'baudrate'
DEFAULT_NAME = "Serial Sensor"
+DEFAULT_BAUDRATE = 9600
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SERIAL_PORT): cv.string,
+ vol.Optional(CONF_BAUDRATE, default=DEFAULT_BAUDRATE):
+ cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
@@ -33,8 +40,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Serial sensor platform."""
name = config.get(CONF_NAME)
port = config.get(CONF_SERIAL_PORT)
+ baudrate = config.get(CONF_BAUDRATE)
- sensor = SerialSensor(name, port)
+ value_template = config.get(CONF_VALUE_TEMPLATE)
+ if value_template is not None:
+ value_template.hass = hass
+
+ sensor = SerialSensor(name, port, baudrate, value_template)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, sensor.stop_serial_read())
@@ -44,28 +56,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class SerialSensor(Entity):
"""Representation of a Serial sensor."""
- def __init__(self, name, port):
+ def __init__(self, name, port, baudrate, value_template):
"""Initialize the Serial sensor."""
self._name = name
self._state = None
self._port = port
+ self._baudrate = baudrate
self._serial_loop_task = None
+ self._template = value_template
+ self._attributes = []
@asyncio.coroutine
def async_added_to_hass(self):
"""Handle when an entity is about to be added to Home Assistant."""
self._serial_loop_task = self.hass.loop.create_task(
- self.serial_read(self._port))
+ self.serial_read(self._port, self._baudrate))
@asyncio.coroutine
- def serial_read(self, device, **kwargs):
+ def serial_read(self, device, rate, **kwargs):
"""Read the data from the port."""
import serial_asyncio
reader, _ = yield from serial_asyncio.open_serial_connection(
- url=device, **kwargs)
+ url=device, baudrate=rate, **kwargs)
while True:
line = yield from reader.readline()
- self._state = line.decode('utf-8').strip()
+ line = line.decode('utf-8').strip()
+
+ try:
+ data = json.loads(line)
+ if isinstance(data, dict):
+ self._attributes = data
+ except ValueError:
+ pass
+
+ if self._template is not None:
+ line = self._template.async_render_with_possible_json_value(
+ line)
+
+ self._state = line
self.async_schedule_update_ha_state()
@asyncio.coroutine
@@ -84,6 +112,11 @@ class SerialSensor(Entity):
"""No polling needed."""
return False
+ @property
+ def device_state_attributes(self):
+ """Return the attributes of the entity (if any JSON present)."""
+ return self._attributes
+
@property
def state(self):
"""Return the state of the sensor."""
diff --git a/homeassistant/components/sensor/snmp.py b/homeassistant/components/sensor/snmp.py
index 841ff107826..982e7d9559b 100644
--- a/homeassistant/components/sensor/snmp.py
+++ b/homeassistant/components/sensor/snmp.py
@@ -16,7 +16,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN,
CONF_VALUE_TEMPLATE)
-REQUIREMENTS = ['pysnmp==4.4.1']
+REQUIREMENTS = ['pysnmp==4.4.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py
index 0c9a21447a8..324d3029c99 100755
--- a/homeassistant/components/sensor/systemmonitor.py
+++ b/homeassistant/components/sensor/systemmonitor.py
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['psutil==5.4.0']
+REQUIREMENTS = ['psutil==5.4.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/thethingsnetwork.py b/homeassistant/components/sensor/thethingsnetwork.py
index 90b21cc19e5..28a3b48892b 100644
--- a/homeassistant/components/sensor/thethingsnetwork.py
+++ b/homeassistant/components/sensor/thethingsnetwork.py
@@ -8,15 +8,16 @@ import asyncio
import logging
import aiohttp
+from aiohttp.hdrs import ACCEPT, AUTHORIZATION
import async_timeout
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.components.thethingsnetwork import (
DATA_TTN, TTN_APP_ID, TTN_ACCESS_KEY, TTN_DATA_STORAGE_URL)
from homeassistant.const import CONTENT_TYPE_JSON
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -122,8 +123,8 @@ class TtnDataStorage(object):
self._url = TTN_DATA_STORAGE_URL.format(
app_id=app_id, endpoint='api/v2/query', device_id=device_id)
self._headers = {
- 'Accept': CONTENT_TYPE_JSON,
- 'Authorization': 'key {}'.format(access_key),
+ ACCEPT: CONTENT_TYPE_JSON,
+ AUTHORIZATION: 'key {}'.format(access_key),
}
@asyncio.coroutine
diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py
index f1edaa37f77..dd09b9f7891 100644
--- a/homeassistant/components/sensor/tibber.py
+++ b/homeassistant/components/sensor/tibber.py
@@ -18,7 +18,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util
-REQUIREMENTS = ['pyTibber==0.1.1']
+REQUIREMENTS = ['pyTibber==0.2.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/viaggiatreno.py b/homeassistant/components/sensor/viaggiatreno.py
new file mode 100644
index 00000000000..37e7e020cc9
--- /dev/null
+++ b/homeassistant/components/sensor/viaggiatreno.py
@@ -0,0 +1,187 @@
+"""
+Support for information about the Italian train system using ViaggiaTreno API.
+
+For more details about this platform please refer to the documentation at
+https://home-assistant.io/components/sensor.viaggiatreno
+"""
+import logging
+
+import asyncio
+import async_timeout
+import aiohttp
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.helpers.entity import Entity
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_ATTRIBUTION = "Powered by ViaggiaTreno Data"
+VIAGGIATRENO_ENDPOINT = ("http://www.viaggiatreno.it/viaggiatrenonew/"
+ "resteasy/viaggiatreno/andamentoTreno/"
+ "{station_id}/{train_id}")
+
+REQUEST_TIMEOUT = 5 # seconds
+ICON = 'mdi:train'
+MONITORED_INFO = [
+ 'categoria',
+ 'compOrarioArrivoZeroEffettivo',
+ 'compOrarioPartenzaZeroEffettivo',
+ 'destinazione',
+ 'numeroTreno',
+ 'orarioArrivo',
+ 'orarioPartenza',
+ 'origine',
+ 'subTitle',
+ ]
+
+DEFAULT_NAME = "Train {}"
+
+CONF_NAME = 'train_name'
+CONF_STATION_ID = 'station_id'
+CONF_STATION_NAME = 'station_name'
+CONF_TRAIN_ID = 'train_id'
+
+ARRIVED_STRING = 'Arrived'
+CANCELLED_STRING = 'Cancelled'
+NOT_DEPARTED_STRING = "Not departed yet"
+NO_INFORMATION_STRING = "No information for this train now"
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_TRAIN_ID): cv.string,
+ vol.Required(CONF_STATION_ID): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
+ })
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config,
+ async_add_devices, discovery_info=None):
+ """Setup the ViaggiaTreno platform."""
+ train_id = config.get(CONF_TRAIN_ID)
+ station_id = config.get(CONF_STATION_ID)
+ name = config.get(CONF_NAME)
+ if not name:
+ name = DEFAULT_NAME.format(train_id)
+ async_add_devices([ViaggiaTrenoSensor(train_id, station_id, name)])
+
+
+@asyncio.coroutine
+def async_http_request(hass, uri):
+ """Perform actual request."""
+ try:
+ session = hass.helpers.aiohttp_client.async_get_clientsession(hass)
+ with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
+ req = yield from session.get(uri)
+ if req.status != 200:
+ return {'error': req.status}
+ else:
+ json_response = yield from req.json()
+ return json_response
+ except (asyncio.TimeoutError, aiohttp.ClientError) as exc:
+ _LOGGER.error("Cannot connect to ViaggiaTreno API endpoint: %s", exc)
+ except ValueError:
+ _LOGGER.error("Received non-JSON data from ViaggiaTreno API endpoint")
+
+
+class ViaggiaTrenoSensor(Entity):
+ """Implementation of a ViaggiaTreno sensor."""
+
+ def __init__(self, train_id, station_id, name):
+ """Initialize the sensor."""
+ self._state = None
+ self._attributes = {}
+ self._unit = ''
+ self._icon = ICON
+ self._station_id = station_id
+ self._name = name
+
+ self.uri = VIAGGIATRENO_ENDPOINT.format(
+ station_id=station_id,
+ train_id=train_id)
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return self._icon
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit
+
+ @property
+ def device_state_attributes(self):
+ """Return extra attributes."""
+ self._attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
+ return self._attributes
+
+ @staticmethod
+ def has_departed(data):
+ """Check if the train has actually departed."""
+ try:
+ first_station = data['fermate'][0]
+ if data['oraUltimoRilevamento'] or first_station['effettiva']:
+ return True
+ except ValueError:
+ _LOGGER.error("Cannot fetch first station: %s", data)
+ return False
+
+ @staticmethod
+ def has_arrived(data):
+ """Check if the train has already arrived."""
+ last_station = data['fermate'][-1]
+ if not last_station['effettiva']:
+ return False
+ return True
+
+ @staticmethod
+ def is_cancelled(data):
+ """Check if the train is cancelled."""
+ if data['tipoTreno'] == 'ST' and data['provvedimento'] == 1:
+ return True
+ return False
+
+ @asyncio.coroutine
+ def async_update(self):
+ """Update state."""
+ uri = self.uri
+ res = yield from async_http_request(self.hass, uri)
+ if res.get('error', ''):
+ if res['error'] == 204:
+ self._state = NO_INFORMATION_STRING
+ self._unit = ''
+ else:
+ self._state = "Error: {}".format(res['error'])
+ self._unit = ''
+ else:
+ for i in MONITORED_INFO:
+ self._attributes[i] = res[i]
+
+ if self.is_cancelled(res):
+ self._state = CANCELLED_STRING
+ self._icon = 'mdi:cancel'
+ self._unit = ''
+ elif not self.has_departed(res):
+ self._state = NOT_DEPARTED_STRING
+ self._unit = ''
+ elif self.has_arrived(res):
+ self._state = ARRIVED_STRING
+ self._unit = ''
+ else:
+ self._state = res.get('ritardo')
+ self._unit = 'min'
+ self._icon = ICON
diff --git a/homeassistant/components/sensor/vultr.py b/homeassistant/components/sensor/vultr.py
new file mode 100644
index 00000000000..7a3db3895dc
--- /dev/null
+++ b/homeassistant/components/sensor/vultr.py
@@ -0,0 +1,115 @@
+"""
+Support for monitoring the state of Vultr Subscriptions.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/sensor.vultr/
+"""
+import logging
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.const import (
+ CONF_MONITORED_CONDITIONS, CONF_NAME)
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.components.vultr import (
+ CONF_SUBSCRIPTION, ATTR_CURRENT_BANDWIDTH_USED, ATTR_PENDING_CHARGES,
+ DATA_VULTR)
+
+# Name defaults to {subscription label} {sensor name}
+DEFAULT_NAME = 'Vultr {} {}'
+DEPENDENCIES = ['vultr']
+
+_LOGGER = logging.getLogger(__name__)
+
+# Monitored conditions: name, units, icon
+MONITORED_CONDITIONS = {
+ ATTR_CURRENT_BANDWIDTH_USED: ['Current Bandwidth Used', 'GB',
+ 'mdi:chart-histogram'],
+ ATTR_PENDING_CHARGES: ['Pending Charges', 'US$',
+ 'mdi:currency-usd']
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_SUBSCRIPTION): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=MONITORED_CONDITIONS):
+ vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)])
+})
+
+
+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)
+ monitored_conditions = config.get(CONF_MONITORED_CONDITIONS)
+
+ if subscription not in vultr.data:
+ _LOGGER.error("Subscription %s not found", subscription)
+ return False
+
+ sensors = []
+
+ for condition in monitored_conditions:
+ sensors.append(VultrSensor(vultr,
+ subscription,
+ condition,
+ name))
+
+ add_devices(sensors, True)
+
+
+class VultrSensor(Entity):
+ """Representation of a Vultr subscription sensor."""
+
+ def __init__(self, vultr, subscription, condition, name):
+ """Initialize a new Vultr sensor."""
+ self._vultr = vultr
+ self._condition = condition
+ self._name = name
+
+ self.subscription = subscription
+ self.data = None
+
+ condition_info = MONITORED_CONDITIONS[condition]
+
+ self._condition_name = condition_info[0]
+ self._units = condition_info[1]
+ self._icon = condition_info[2]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ try:
+ return self._name.format(self._condition_name)
+ except IndexError: # name contains more {} than fulfilled
+ try:
+ return self._name.format(self.data['label'],
+ self._condition_name)
+ except (KeyError, TypeError): # label key missing or data is None
+ return self._name
+
+ @property
+ def icon(self):
+ """Icon used in the frontend if any."""
+ return self._icon
+
+ @property
+ def unit_of_measurement(self):
+ """The unit of measurement to present the value in."""
+ return self._units
+
+ @property
+ def state(self):
+ """Return the value of this given sensor type."""
+ try:
+ return round(float(self.data.get(self._condition)), 2)
+ except (TypeError, ValueError):
+ return self.data.get(self._condition)
+
+ def update(self):
+ """Update state of sensor."""
+ self._vultr.update()
+ self.data = self._vultr.data[self.subscription]
diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py
index 2fcb13e13dd..c0763c4fefa 100644
--- a/homeassistant/components/sensor/wunderground.py
+++ b/homeassistant/components/sensor/wunderground.py
@@ -616,14 +616,13 @@ LANG_CODES = [
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PWS_ID): cv.string,
- vol.Optional(CONF_LANG, default=DEFAULT_LANG):
- vol.All(vol.In(LANG_CODES)),
+ vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.All(vol.In(LANG_CODES)),
vol.Inclusive(CONF_LATITUDE, 'coordinates',
'Latitude and longitude must exist together'): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
'Latitude and longitude must exist together'): cv.longitude,
- vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
- vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+ vol.Required(CONF_MONITORED_CONDITIONS):
+ vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]),
})
diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py
index 2883a396b77..873e27975db 100644
--- a/homeassistant/components/sensor/yweather.py
+++ b/homeassistant/components/sensor/yweather.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-REQUIREMENTS = ['yahooweather==0.8']
+REQUIREMENTS = ['yahooweather==0.9']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py
index 3eb677b4f02..4b63d769243 100644
--- a/homeassistant/components/sensor/zamg.py
+++ b/homeassistant/components/sensor/zamg.py
@@ -5,24 +5,25 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.zamg/
"""
import csv
+from datetime import datetime, timedelta
import gzip
import json
import logging
import os
-from datetime import datetime, timedelta
+from aiohttp.hdrs import USER_AGENT
import pytz
import requests
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.weather import (
- ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_PRESSURE,
- ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING,
- ATTR_WEATHER_WIND_SPEED)
+ ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_WIND_SPEED,
+ ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_TEMPERATURE,
+ ATTR_WEATHER_WIND_BEARING)
from homeassistant.const import (
- CONF_MONITORED_CONDITIONS, CONF_NAME, __version__,
- CONF_LATITUDE, CONF_LONGITUDE)
+ CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
+ __version__)
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
@@ -30,13 +31,12 @@ _LOGGER = logging.getLogger(__name__)
ATTR_STATION = 'station'
ATTR_UPDATED = 'updated'
-ATTRIBUTION = 'Data provided by ZAMG'
+ATTRIBUTION = "Data provided by ZAMG"
CONF_STATION_ID = 'station_id'
DEFAULT_NAME = 'zamg'
-# Data source updates once per hour, so we do nothing if it's been less time
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
SENSOR_TYPES = {
@@ -138,7 +138,7 @@ class ZamgData(object):
API_URL = 'http://www.zamg.ac.at/ogd/'
API_HEADERS = {
- 'User-Agent': '{} {}'.format('home-assistant.zamg/', __version__),
+ USER_AGENT: '{} {}'.format('home-assistant.zamg/', __version__),
}
def __init__(self, station_id):
@@ -162,8 +162,8 @@ class ZamgData(object):
cls.API_URL, headers=cls.API_HEADERS, timeout=15)
response.raise_for_status()
response.encoding = 'UTF8'
- return csv.DictReader(response.text.splitlines(),
- delimiter=';', quotechar='"')
+ return csv.DictReader(
+ response.text.splitlines(), delimiter=';', quotechar='"')
except requests.exceptions.HTTPError:
_LOGGER.error("While fetching data")
diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml
index 37829142e0c..c4e460fdb66 100644
--- a/homeassistant/components/services.yaml
+++ b/homeassistant/components/services.yaml
@@ -390,26 +390,6 @@ rflink:
description: The command to be sent.
example: 'on'
-counter:
- 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'
-
abode:
change_setting:
description: Change an Abode system setting.
diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py
index a271297d0fd..c186559c91a 100644
--- a/homeassistant/components/spc.py
+++ b/homeassistant/components/spc.py
@@ -87,9 +87,14 @@ def _async_process_message(sia_message, spc_registry):
# ZX - Zone Short
# ZD - Zone Disconnected
- if sia_code in ('BA', 'CG', 'NL', 'OG', 'OQ'):
+ extra = {}
+
+ if sia_code in ('BA', 'CG', 'NL', 'OG'):
# change in area status, notify alarm panel device
device = spc_registry.get_alarm_device(spc_id)
+ data = sia_message['description'].split('¦')
+ if len(data) == 3:
+ extra['changed_by'] = data[1]
else:
# change in zone status, notify sensor device
device = spc_registry.get_sensor_device(spc_id)
@@ -98,7 +103,6 @@ def _async_process_message(sia_message, spc_registry):
'CG': STATE_ALARM_ARMED_AWAY,
'NL': STATE_ALARM_ARMED_HOME,
'OG': STATE_ALARM_DISARMED,
- 'OQ': STATE_ALARM_DISARMED,
'ZO': STATE_ON,
'ZC': STATE_OFF,
'ZX': STATE_UNKNOWN,
@@ -110,7 +114,7 @@ def _async_process_message(sia_message, spc_registry):
_LOGGER.warning("No device mapping found for SPC area/zone id %s.",
spc_id)
elif new_state:
- yield from device.async_update_from_spc(new_state)
+ yield from device.async_update_from_spc(new_state, extra)
class SpcRegistry:
diff --git a/homeassistant/components/splunk.py b/homeassistant/components/splunk.py
index 38f8a91a917..a5b42eb9b5a 100644
--- a/homeassistant/components/splunk.py
+++ b/homeassistant/components/splunk.py
@@ -7,11 +7,12 @@ https://home-assistant.io/components/splunk/
import json
import logging
+from aiohttp.hdrs import AUTHORIZATION
import requests
import voluptuous as vol
from homeassistant.const import (
- CONF_NAME, CONF_HOST, CONF_PORT, CONF_SSL, CONF_TOKEN, EVENT_STATE_CHANGED)
+ CONF_SSL, CONF_HOST, CONF_NAME, CONF_PORT, CONF_TOKEN, EVENT_STATE_CHANGED)
from homeassistant.helpers import state as state_helper
import homeassistant.helpers.config_validation as cv
from homeassistant.remote import JSONEncoder
@@ -52,7 +53,7 @@ def setup(hass, config):
event_collector = '{}{}:{}/services/collector/event'.format(
uri_scheme, host, port)
- headers = {'Authorization': 'Splunk {}'.format(token)}
+ headers = {AUTHORIZATION: 'Splunk {}'.format(token)}
def splunk_event_listener(event):
"""Listen for new messages on the bus and sends them to Splunk."""
diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py
index c12d13860e2..8abdba31b67 100644
--- a/homeassistant/components/switch/broadlink.py
+++ b/homeassistant/components/switch/broadlink.py
@@ -117,6 +117,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for packet in packets:
for retry in range(DEFAULT_RETRY):
try:
+ extra = len(packet) % 4
+ if extra > 0:
+ packet = packet + ('=' * (4 - extra))
payload = b64decode(packet)
yield from hass.async_add_job(
broadlink_device.send_data, payload)
diff --git a/homeassistant/components/switch/hikvisioncam.py b/homeassistant/components/switch/hikvisioncam.py
index acb9af3cacb..c3e065abc0e 100644
--- a/homeassistant/components/switch/hikvisioncam.py
+++ b/homeassistant/components/switch/hikvisioncam.py
@@ -15,7 +15,8 @@ from homeassistant.const import (
from homeassistant.helpers.entity import ToggleEntity
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['hikvision==1.2']
+REQUIREMENTS = ['hikvision==0.4']
+# This is the last working version, please test before updating
_LOGGING = logging.getLogger(__name__)
diff --git a/homeassistant/components/switch/lutron_caseta.py b/homeassistant/components/switch/lutron_caseta.py
index daaba68dc5e..da36c76f41d 100644
--- a/homeassistant/components/switch/lutron_caseta.py
+++ b/homeassistant/components/switch/lutron_caseta.py
@@ -4,6 +4,7 @@ Support for Lutron Caseta switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sitch.lutron_caseta/
"""
+import asyncio
import logging
from homeassistant.components.lutron_caseta import (
@@ -16,7 +17,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 Lutron switch."""
devs = []
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
@@ -26,18 +28,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = LutronCasetaLight(switch_device, bridge)
devs.append(dev)
- add_devices(devs, True)
+ async_add_devices(devs, True)
return True
class LutronCasetaLight(LutronCasetaDevice, SwitchDevice):
"""Representation of a Lutron Caseta switch."""
- def turn_on(self, **kwargs):
+ @asyncio.coroutine
+ def async_turn_on(self, **kwargs):
"""Turn the switch on."""
self._smartbridge.turn_on(self._device_id)
- def turn_off(self, **kwargs):
+ @asyncio.coroutine
+ def async_turn_off(self, **kwargs):
"""Turn the switch off."""
self._smartbridge.turn_off(self._device_id)
@@ -46,7 +50,8 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchDevice):
"""Return true if device is on."""
return self._state["current_state"] > 0
- def update(self):
+ @asyncio.coroutine
+ def async_update(self):
"""Update when forcing a refresh of the device."""
self._state = self._smartbridge.get_device_by_id(self._device_id)
_LOGGER.debug(self._state)
diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py
index e6342617f28..c731b336dfb 100644
--- a/homeassistant/components/switch/modbus.py
+++ b/homeassistant/components/switch/modbus.py
@@ -8,7 +8,8 @@ import logging
import voluptuous as vol
import homeassistant.components.modbus as modbus
-from homeassistant.const import CONF_NAME, CONF_SLAVE
+from homeassistant.const import (
+ CONF_NAME, CONF_SLAVE, CONF_COMMAND_ON, CONF_COMMAND_OFF)
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers import config_validation as cv
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -18,32 +19,76 @@ DEPENDENCIES = ['modbus']
CONF_COIL = "coil"
CONF_COILS = "coils"
+CONF_REGISTER = "register"
+CONF_REGISTERS = "registers"
+CONF_VERIFY_STATE = "verify_state"
+CONF_VERIFY_REGISTER = "verify_register"
+CONF_REGISTER_TYPE = "register_type"
+CONF_STATE_ON = "state_on"
+CONF_STATE_OFF = "state_off"
-PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(CONF_COILS): [{
- vol.Required(CONF_COIL): cv.positive_int,
- vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_SLAVE): cv.positive_int,
- }]
+REGISTER_TYPE_HOLDING = 'holding'
+REGISTER_TYPE_INPUT = 'input'
+
+REGISTERS_SCHEMA = vol.Schema({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Optional(CONF_SLAVE): cv.positive_int,
+ vol.Required(CONF_REGISTER): cv.positive_int,
+ vol.Required(CONF_COMMAND_ON): cv.positive_int,
+ vol.Required(CONF_COMMAND_OFF): cv.positive_int,
+ vol.Optional(CONF_VERIFY_STATE, default=True): cv.boolean,
+ vol.Optional(CONF_VERIFY_REGISTER, default=None):
+ cv.positive_int,
+ vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING):
+ vol.In([REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT]),
+ vol.Optional(CONF_STATE_ON, default=None): cv.positive_int,
+ vol.Optional(CONF_STATE_OFF, default=None): cv.positive_int,
})
+COILS_SCHEMA = vol.Schema({
+ vol.Required(CONF_COIL): cv.positive_int,
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_SLAVE): cv.positive_int,
+})
+
+PLATFORM_SCHEMA = vol.All(
+ cv.has_at_least_one_key(CONF_COILS, CONF_REGISTERS),
+ PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_COILS): [COILS_SCHEMA],
+ vol.Optional(CONF_REGISTERS): [REGISTERS_SCHEMA]
+ }))
+
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Read configuration and create Modbus devices."""
switches = []
- for coil in config.get("coils"):
- switches.append(ModbusCoilSwitch(
- coil.get(CONF_NAME),
- coil.get(CONF_SLAVE),
- coil.get(CONF_COIL)))
+ if CONF_COILS in config:
+ for coil in config.get(CONF_COILS):
+ switches.append(ModbusCoilSwitch(
+ coil.get(CONF_NAME),
+ coil.get(CONF_SLAVE),
+ coil.get(CONF_COIL)))
+ if CONF_REGISTERS in config:
+ for register in config.get(CONF_REGISTERS):
+ switches.append(ModbusRegisterSwitch(
+ register.get(CONF_NAME),
+ register.get(CONF_SLAVE),
+ register.get(CONF_REGISTER),
+ register.get(CONF_COMMAND_ON),
+ register.get(CONF_COMMAND_OFF),
+ register.get(CONF_VERIFY_STATE),
+ register.get(CONF_VERIFY_REGISTER),
+ register.get(CONF_REGISTER_TYPE),
+ register.get(CONF_STATE_ON),
+ register.get(CONF_STATE_OFF)))
add_devices(switches)
class ModbusCoilSwitch(ToggleEntity):
- """Representation of a Modbus switch."""
+ """Representation of a Modbus coil switch."""
def __init__(self, name, slave, coil):
- """Initialize the switch."""
+ """Initialize the coil switch."""
self._name = name
self._slave = int(slave) if slave else None
self._coil = int(coil)
@@ -77,3 +122,82 @@ class ModbusCoilSwitch(ToggleEntity):
'No response from modbus slave %s coil %s',
self._slave,
self._coil)
+
+
+class ModbusRegisterSwitch(ModbusCoilSwitch):
+ """Representation of a Modbus register switch."""
+
+ # pylint: disable=super-init-not-called
+ def __init__(self, name, slave, register, command_on,
+ command_off, verify_state, verify_register,
+ register_type, state_on, state_off):
+ """Initialize the register switch."""
+ self._name = name
+ self._slave = slave
+ self._register = register
+ self._command_on = command_on
+ self._command_off = command_off
+ self._verify_state = verify_state
+ self._verify_register = (
+ verify_register if verify_register else self._register)
+ self._register_type = register_type
+ self._state_on = (
+ state_on if state_on else self._command_on)
+ self._state_off = (
+ state_off if state_off else self._command_off)
+ self._is_on = None
+
+ def turn_on(self, **kwargs):
+ """Set switch on."""
+ modbus.HUB.write_register(
+ self._slave,
+ self._register,
+ self._command_on)
+ if not self._verify_state:
+ self._is_on = True
+
+ def turn_off(self, **kwargs):
+ """Set switch off."""
+ modbus.HUB.write_register(
+ self._slave,
+ self._register,
+ self._command_off)
+ if not self._verify_state:
+ self._is_on = False
+
+ def update(self):
+ """Update the state of the switch."""
+ if not self._verify_state:
+ return
+
+ value = 0
+ if self._register_type == REGISTER_TYPE_INPUT:
+ result = modbus.HUB.read_input_registers(
+ self._slave,
+ self._register,
+ 1)
+ else:
+ result = modbus.HUB.read_holding_registers(
+ self._slave,
+ self._register,
+ 1)
+
+ try:
+ value = int(result.registers[0])
+ except AttributeError:
+ _LOGGER.error(
+ 'No response from modbus slave %s register %s',
+ self._slave,
+ self._verify_register)
+
+ if value == self._state_on:
+ self._is_on = True
+ elif value == self._state_off:
+ self._is_on = False
+ else:
+ _LOGGER.error(
+ 'Unexpected response from modbus slave %s '
+ 'register %s, got 0x%2x',
+ self._slave,
+ self._verify_register,
+ value)
diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py
index f29dc31eaf0..62bc5f99d01 100644
--- a/homeassistant/components/switch/neato.py
+++ b/homeassistant/components/switch/neato.py
@@ -14,11 +14,9 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['neato']
-SWITCH_TYPE_CLEAN = 'clean'
-SWITCH_TYPE_SCHEDULE = 'scedule'
+SWITCH_TYPE_SCHEDULE = 'schedule'
SWITCH_TYPES = {
- SWITCH_TYPE_CLEAN: ['Clean'],
SWITCH_TYPE_SCHEDULE: ['Schedule']
}
@@ -64,15 +62,6 @@ class NeatoConnectedSwitch(ToggleEntity):
self._state = None
return
_LOGGER.debug('self._state=%s', self._state)
- if self.type == SWITCH_TYPE_CLEAN:
- if (self.robot.state['action'] == 1 or
- self.robot.state['action'] == 2 or
- self.robot.state['action'] == 3 and
- self.robot.state['state'] == 2):
- self._clean_state = STATE_ON
- else:
- self._clean_state = STATE_OFF
- _LOGGER.debug("Clean state: %s", self._clean_state)
if self.type == SWITCH_TYPE_SCHEDULE:
_LOGGER.debug("State: %s", self._state)
if self.robot.schedule_enabled:
@@ -94,26 +83,17 @@ class NeatoConnectedSwitch(ToggleEntity):
@property
def is_on(self):
"""Return true if switch is on."""
- if self.type == SWITCH_TYPE_CLEAN:
- if self._clean_state == STATE_ON:
- return True
- return False
- elif self.type == SWITCH_TYPE_SCHEDULE:
+ if self.type == SWITCH_TYPE_SCHEDULE:
if self._schedule_state == STATE_ON:
return True
return False
def turn_on(self, **kwargs):
"""Turn the switch on."""
- if self.type == SWITCH_TYPE_CLEAN:
- self.robot.start_cleaning()
- elif self.type == SWITCH_TYPE_SCHEDULE:
+ if self.type == SWITCH_TYPE_SCHEDULE:
self.robot.enable_schedule()
def turn_off(self, **kwargs):
"""Turn the switch off."""
- if self.type == SWITCH_TYPE_CLEAN:
- self.robot.pause_cleaning()
- self.robot.send_to_base()
- elif self.type == SWITCH_TYPE_SCHEDULE:
+ if self.type == SWITCH_TYPE_SCHEDULE:
self.robot.disable_schedule()
diff --git a/homeassistant/components/switch/snmp.py b/homeassistant/components/switch/snmp.py
index d372991c3e2..99ba9d8cd54 100644
--- a/homeassistant/components/switch/snmp.py
+++ b/homeassistant/components/switch/snmp.py
@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pysnmp==4.4.1']
+REQUIREMENTS = ['pysnmp==4.4.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/switch/vultr.py b/homeassistant/components/switch/vultr.py
new file mode 100644
index 00000000000..888db754f01
--- /dev/null
+++ b/homeassistant/components/switch/vultr.py
@@ -0,0 +1,106 @@
+"""
+Support for interacting with Vultr subscriptions.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/switch.vultr/
+"""
+import logging
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_NAME
+from homeassistant.components.switch import (SwitchDevice, 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_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 switch."""
+ 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([VultrSwitch(vultr, subscription, name)], True)
+
+
+class VultrSwitch(SwitchDevice):
+ """Representation of a Vultr subscription switch."""
+
+ def __init__(self, vultr, subscription, name):
+ """Initialize a new Vultr switch."""
+ self._vultr = vultr
+ self._name = name
+
+ self.subscription = subscription
+ self.data = None
+
+ @property
+ def name(self):
+ """Return the name of the switch."""
+ try:
+ return self._name.format(self.data['label'])
+ except (TypeError, KeyError):
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self.data['power_status'] == 'running'
+
+ @property
+ def icon(self):
+ """Return the icon of this server."""
+ return 'mdi:server' if self.is_on else 'mdi:server-off'
+
+ @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 turn_on(self):
+ """Boot-up the subscription."""
+ if self.data['power_status'] != 'running':
+ self._vultr.start(self.subscription)
+
+ def turn_off(self):
+ """Halt the subscription."""
+ if self.data['power_status'] == 'running':
+ self._vultr.halt(self.subscription)
+
+ def update(self):
+ """Get the latest data from the device and update the data."""
+ self._vultr.update()
+ self.data = self._vultr.data[self.subscription]
diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py
index 1191322dce6..aaa37a24c0e 100644
--- a/homeassistant/components/switch/xiaomi_miio.py
+++ b/homeassistant/components/switch/xiaomi_miio.py
@@ -25,7 +25,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_POWER = 'power'
ATTR_TEMPERATURE = 'temperature'
@@ -68,8 +68,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
elif device_info.model in ['qmi.powerstrip.v1',
'zimi.powerstrip.v2']:
- from miio import Strip
- plug = Strip(host, token)
+ from miio import PowerStrip
+ plug = PowerStrip(host, token)
device = XiaomiPowerStripSwitch(name, plug, device_info)
devices.append(device)
elif device_info.model in ['chuangmi.plug.m1',
@@ -288,5 +288,9 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice):
else:
self._state = state.is_on
+ self._state_attrs.update({
+ ATTR_TEMPERATURE: state.temperature
+ })
+
except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)
diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py
new file mode 100644
index 00000000000..6505107d034
--- /dev/null
+++ b/homeassistant/components/system_log/__init__.py
@@ -0,0 +1,143 @@
+"""
+Support for system log.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/system_log/
+"""
+import os
+import re
+import asyncio
+import logging
+import traceback
+from io import StringIO
+from collections import deque
+
+import voluptuous as vol
+
+from homeassistant.config import load_yaml_config_file
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.http import HomeAssistantView
+
+DOMAIN = 'system_log'
+DEPENDENCIES = ['http']
+SERVICE_CLEAR = 'clear'
+
+CONF_MAX_ENTRIES = 'max_entries'
+
+DEFAULT_MAX_ENTRIES = 50
+
+DATA_SYSTEM_LOG = 'system_log'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Optional(CONF_MAX_ENTRIES,
+ default=DEFAULT_MAX_ENTRIES): cv.positive_int,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+SERVICE_CLEAR_SCHEMA = vol.Schema({})
+
+
+class LogErrorHandler(logging.Handler):
+ """Log handler for error messages."""
+
+ def __init__(self, maxlen):
+ """Initialize a new LogErrorHandler."""
+ super().__init__()
+ self.records = deque(maxlen=maxlen)
+
+ def emit(self, record):
+ """Save error and warning logs.
+
+ Everyhing logged with error or warning is saved in local buffer. A
+ default upper limit is set to 50 (older entries are discarded) but can
+ be changed if neeeded.
+ """
+ if record.levelno >= logging.WARN:
+ self.records.appendleft(record)
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Set up the logger component."""
+ conf = config.get(DOMAIN)
+
+ if conf is None:
+ conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
+
+ handler = LogErrorHandler(conf.get(CONF_MAX_ENTRIES))
+ logging.getLogger().addHandler(handler)
+
+ hass.http.register_view(AllErrorsView(handler))
+
+ @asyncio.coroutine
+ def async_service_handler(service):
+ """Handle logger services."""
+ # Only one service so far
+ handler.records.clear()
+
+ descriptions = yield from hass.async_add_job(
+ load_yaml_config_file, os.path.join(
+ os.path.dirname(__file__), 'services.yaml'))
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_CLEAR, async_service_handler,
+ descriptions[DOMAIN].get(SERVICE_CLEAR),
+ schema=SERVICE_CLEAR_SCHEMA)
+
+ return True
+
+
+def _figure_out_source(record):
+ # If a stack trace exists, extract filenames from the entire call stack.
+ # The other case is when a regular "log" is made (without an attached
+ # exception). In that case, just use the file where the log was made from.
+ if record.exc_info:
+ stack = [x[0] for x in traceback.extract_tb(record.exc_info[2])]
+ else:
+ stack = [record.pathname]
+
+ # Iterate through the stack call (in reverse) and find the last call from
+ # a file in HA. Try to figure out where error happened.
+ for pathname in reversed(stack):
+
+ # Try to match with a file within HA
+ match = re.match(r'.*/homeassistant/(.*)', pathname)
+ if match:
+ return match.group(1)
+
+ # Ok, we don't know what this is
+ return 'unknown'
+
+
+def _exception_as_string(exc_info):
+ buf = StringIO()
+ if exc_info:
+ traceback.print_exception(*exc_info, file=buf)
+ return buf.getvalue()
+
+
+def _convert(record):
+ return {
+ 'timestamp': record.created,
+ 'level': record.levelname,
+ 'message': record.getMessage(),
+ 'exception': _exception_as_string(record.exc_info),
+ 'source': _figure_out_source(record),
+ }
+
+
+class AllErrorsView(HomeAssistantView):
+ """Get all logged errors and warnings."""
+
+ url = "/api/error/all"
+ name = "api:error:all"
+
+ def __init__(self, handler):
+ """Initialize a new AllErrorsView."""
+ self.handler = handler
+
+ @asyncio.coroutine
+ def get(self, request):
+ """Get all errors and warnings."""
+ return self.json([_convert(x) for x in self.handler.records])
diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml
new file mode 100644
index 00000000000..98f86e12f8c
--- /dev/null
+++ b/homeassistant/components/system_log/services.yaml
@@ -0,0 +1,3 @@
+system_log:
+ clear:
+ description: Clear all log entries.
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index 896dbdc4399..dc9389b1144 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -65,6 +65,7 @@ DOMAIN = 'telegram_bot'
SERVICE_SEND_MESSAGE = 'send_message'
SERVICE_SEND_PHOTO = 'send_photo'
+SERVICE_SEND_VIDEO = 'send_video'
SERVICE_SEND_DOCUMENT = 'send_document'
SERVICE_SEND_LOCATION = 'send_location'
SERVICE_EDIT_MESSAGE = 'edit_message'
@@ -154,6 +155,7 @@ SERVICE_SCHEMA_DELETE_MESSAGE = vol.Schema({
SERVICE_MAP = {
SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE,
SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE,
+ SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE,
SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE,
SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION,
SERVICE_EDIT_MESSAGE: SERVICE_SCHEMA_EDIT_MESSAGE,
@@ -277,12 +279,11 @@ def async_setup(hass, config):
if msgtype == SERVICE_SEND_MESSAGE:
yield from hass.async_add_job(
partial(notify_service.send_message, **kwargs))
- elif msgtype == SERVICE_SEND_PHOTO:
+ elif (msgtype == SERVICE_SEND_PHOTO or
+ msgtype == SERVICE_SEND_VIDEO or
+ msgtype == SERVICE_SEND_DOCUMENT):
yield from hass.async_add_job(
- partial(notify_service.send_file, True, **kwargs))
- elif msgtype == SERVICE_SEND_DOCUMENT:
- yield from hass.async_add_job(
- partial(notify_service.send_file, False, **kwargs))
+ partial(notify_service.send_file, msgtype, **kwargs))
elif msgtype == SERVICE_SEND_LOCATION:
yield from hass.async_add_job(
partial(notify_service.send_location, **kwargs))
@@ -518,11 +519,15 @@ class TelegramNotificationService:
callback_query_id,
text=message, show_alert=show_alert, **params)
- def send_file(self, is_photo=True, target=None, **kwargs):
- """Send a photo or a document."""
+ def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs):
+ """Send a photo, video, or document."""
params = self._get_msg_kwargs(kwargs)
caption = kwargs.get(ATTR_CAPTION)
- func_send = self.bot.sendPhoto if is_photo else self.bot.sendDocument
+ func_send = {
+ SERVICE_SEND_PHOTO: self.bot.sendPhoto,
+ SERVICE_SEND_VIDEO: self.bot.sendVideo,
+ SERVICE_SEND_DOCUMENT: self.bot.sendDocument
+ }.get(file_type)
file_content = load_data(
self.hass,
url=kwargs.get(ATTR_URL),
diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py
index 4e26dfe3238..0ce11441843 100644
--- a/homeassistant/components/telegram_bot/polling.py
+++ b/homeassistant/components/telegram_bot/polling.py
@@ -10,6 +10,7 @@ import logging
import async_timeout
from aiohttp.client_exceptions import ClientError
+from aiohttp.hdrs import CONNECTION, KEEP_ALIVE
from homeassistant.components.telegram_bot import (
CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity,
@@ -22,6 +23,13 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = TELEGRAM_PLATFORM_SCHEMA
+RETRY_SLEEP = 10
+
+
+class WrongHttpStatus(Exception):
+ """Thrown when a wrong http status is received."""
+
+ pass
@asyncio.coroutine
@@ -41,20 +49,14 @@ def async_setup_platform(hass, config):
"""Stop the bot."""
pol.stop_polling()
- hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_START,
- _start_bot
- )
- hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_STOP,
- _stop_bot
- )
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _start_bot)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_bot)
return True
class TelegramPoll(BaseTelegramBotEntity):
- """asyncio telegram incoming message handler."""
+ """Asyncio telegram incoming message handler."""
def __init__(self, bot, hass, allowed_chat_ids):
"""Initialize the polling instance."""
@@ -62,9 +64,9 @@ class TelegramPoll(BaseTelegramBotEntity):
self.update_id = 0
self.websession = async_get_clientsession(hass)
self.update_url = '{0}/getUpdates'.format(bot.base_url)
- self.polling_task = None # The actuall polling task.
+ self.polling_task = None # The actual polling task.
self.timeout = 15 # async post timeout
- # polling timeout should always be less than async post timeout.
+ # Polling timeout should always be less than async post timeout.
self.post_data = {'timeout': self.timeout - 5}
def start_polling(self):
@@ -79,52 +81,48 @@ class TelegramPoll(BaseTelegramBotEntity):
def get_updates(self, offset):
"""Bypass the default long polling method to enable asyncio."""
resp = None
- _json = {'result': [], 'ok': True} # Empty result.
-
if offset:
self.post_data['offset'] = offset
try:
with async_timeout.timeout(self.timeout, loop=self.hass.loop):
resp = yield from self.websession.post(
self.update_url, data=self.post_data,
- headers={'connection': 'keep-alive'}
+ headers={CONNECTION: KEEP_ALIVE}
)
if resp.status == 200:
_json = yield from resp.json()
+ return _json
else:
- _LOGGER.error("Error %s on %s", resp.status, self.update_url)
-
- except ValueError:
- _LOGGER.error("Error parsing Json message")
- except (asyncio.TimeoutError, ClientError):
- _LOGGER.error("Client connection error")
+ raise WrongHttpStatus('wrong status %s', resp.status)
finally:
if resp is not None:
yield from resp.release()
- return _json
-
- @asyncio.coroutine
- def handle(self):
- """Receiving and processing incoming messages."""
- _updates = yield from self.get_updates(self.update_id)
- _updates = _updates.get('result')
- if _updates is None:
- _LOGGER.error("Incorrect result received.")
- else:
- for update in _updates:
- self.update_id = update['update_id'] + 1
- self.process_message(update)
-
@asyncio.coroutine
def check_incoming(self):
- """Loop which continuously checks for incoming telegram messages."""
+ """Continuously check for incoming telegram messages."""
try:
while True:
- # Each handle call sends a long polling post request
- # to the telegram server. If no incoming message it will return
- # an empty list. Calling self.handle() without any delay or
- # timeout will for this reason not really stress the processor.
- yield from self.handle()
+ try:
+ _updates = yield from self.get_updates(self.update_id)
+ except (WrongHttpStatus, ClientError) as err:
+ # WrongHttpStatus: Non-200 status code.
+ # Occurs at times (mainly 502) and recovers
+ # automatically. Pause for a while before retrying.
+ _LOGGER.error(err)
+ yield from asyncio.sleep(RETRY_SLEEP)
+ except (asyncio.TimeoutError, ValueError):
+ # Long polling timeout. Nothing serious.
+ # Json error. Just retry for the next message.
+ pass
+ else:
+ # no exception raised. update received data.
+ _updates = _updates.get('result')
+ if _updates is None:
+ _LOGGER.error("Incorrect result received.")
+ else:
+ for update in _updates:
+ self.update_id = update['update_id'] + 1
+ self.process_message(update)
except CancelledError:
_LOGGER.debug("Stopping Telegram polling bot")
diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml
index 3b86d97c310..dc864c9f61a 100644
--- a/homeassistant/components/telegram_bot/services.yaml
+++ b/homeassistant/components/telegram_bot/services.yaml
@@ -59,6 +59,37 @@ send_photo:
description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
+send_video:
+ description: Send a video.
+ fields:
+ url:
+ description: Remote path to a video.
+ example: 'http://example.org/path/to/the/video.mp4'
+ file:
+ description: Local path to an image.
+ example: '/path/to/the/video.mp4'
+ caption:
+ description: The title of the video.
+ example: 'My video'
+ username:
+ description: Username for a URL which require HTTP basic authentication.
+ example: myuser
+ password:
+ description: Password for a URL which require HTTP basic authentication.
+ example: myuser_pwd
+ target:
+ description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default.
+ example: '[12345, 67890] or 12345'
+ disable_notification:
+ description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound.
+ example: true
+ keyboard:
+ description: List of rows of commands, comma-separated, to make a custom keyboard.
+ example: '["/command1, /command2", "/command3"]'
+ inline_keyboard:
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
+
send_document:
description: Send a document.
fields:
diff --git a/homeassistant/components/timer/services.yaml b/homeassistant/components/timer/services.yaml
index f7d2c1a77b5..b299aaa8185 100644
--- a/homeassistant/components/timer/services.yaml
+++ b/homeassistant/components/timer/services.yaml
@@ -1,3 +1,5 @@
+# Describes the format for available timer services
+
start:
description: Start a timer.
diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py
index ead4924d599..53ea7eac997 100644
--- a/homeassistant/components/tradfri.py
+++ b/homeassistant/components/tradfri.py
@@ -5,9 +5,8 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ikea_tradfri/
"""
import asyncio
-import json
import logging
-import os
+from uuid import uuid4
import voluptuous as vol
@@ -15,6 +14,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import CONF_HOST
from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI
+from homeassistant.util.json import load_json, save_json
REQUIREMENTS = ['pytradfri==4.0.1',
'DTLSSocket==0.1.4',
@@ -58,26 +58,40 @@ def request_configuration(hass, config, host):
"""Handle the submitted configuration."""
try:
from pytradfri.api.aiocoap_api import APIFactory
+ from pytradfri import RequestError
except ImportError:
_LOGGER.exception("Looks like something isn't installed!")
return
- api_factory = APIFactory(host, psk_id=GATEWAY_IDENTITY)
- psk = yield from api_factory.generate_psk(callback_data.get('key'))
- res = yield from _setup_gateway(hass, config, host, psk,
+ identity = uuid4().hex
+ security_code = callback_data.get('security_code')
+
+ api_factory = APIFactory(host, psk_id=identity, loop=hass.loop)
+ # Need To Fix: currently entering a wrong security code sends
+ # pytradfri aiocoap API into an endless loop.
+ # Should just raise a requestError or something.
+ try:
+ key = yield from api_factory.generate_psk(security_code)
+ except RequestError:
+ configurator.async_notify_errors(hass, instance,
+ "Security Code not accepted.")
+ return
+
+ res = yield from _setup_gateway(hass, config, host, identity, key,
DEFAULT_ALLOW_TRADFRI_GROUPS)
if not res:
- hass.async_add_job(configurator.notify_errors, instance,
- "Unable to connect.")
+ configurator.async_notify_errors(hass, instance,
+ "Unable to connect.")
return
def success():
"""Set up was successful."""
- conf = _read_config(hass)
- conf[host] = {'key': psk}
- _write_config(hass, conf)
- hass.async_add_job(configurator.request_done, instance)
+ conf = load_json(hass.config.path(CONFIG_FILE))
+ conf[host] = {'identity': identity,
+ 'key': key}
+ save_json(hass.config.path(CONFIG_FILE), conf)
+ configurator.request_done(instance)
hass.async_add_job(success)
@@ -86,7 +100,8 @@ def request_configuration(hass, config, host):
description='Please enter the security code written at the bottom of '
'your IKEA Trådfri Gateway.',
submit_caption="Confirm",
- fields=[{'id': 'key', 'name': 'Security Code', 'type': 'password'}]
+ fields=[{'id': 'security_code', 'name': 'Security Code',
+ 'type': 'password'}]
)
@@ -96,35 +111,37 @@ def async_setup(hass, config):
conf = config.get(DOMAIN, {})
host = conf.get(CONF_HOST)
allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS)
- keys = yield from hass.async_add_job(_read_config, hass)
+ known_hosts = yield from hass.async_add_job(load_json,
+ hass.config.path(CONFIG_FILE))
@asyncio.coroutine
- def gateway_discovered(service, info):
+ def gateway_discovered(service, info,
+ allow_tradfri_groups=DEFAULT_ALLOW_TRADFRI_GROUPS):
"""Run when a gateway is discovered."""
host = info['host']
- if host in keys:
- yield from _setup_gateway(hass, config, host, keys[host]['key'],
+ if host in known_hosts:
+ # use fallbacks for old config style
+ # identity was hard coded as 'homeassistant'
+ identity = known_hosts[host].get('identity', 'homeassistant')
+ key = known_hosts[host].get('key')
+ yield from _setup_gateway(hass, config, host, identity, key,
allow_tradfri_groups)
else:
hass.async_add_job(request_configuration, hass, config, host)
discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered)
- if not host:
- return True
-
- if host and keys.get(host):
- return (yield from _setup_gateway(hass, config, host,
- keys[host]['key'],
- allow_tradfri_groups))
- else:
- hass.async_add_job(request_configuration, hass, config, host)
- return True
+ if host:
+ yield from gateway_discovered(None,
+ {'host': host},
+ allow_tradfri_groups)
+ return True
@asyncio.coroutine
-def _setup_gateway(hass, hass_config, host, key, allow_tradfri_groups):
+def _setup_gateway(hass, hass_config, host, identity, key,
+ allow_tradfri_groups):
"""Create a gateway."""
from pytradfri import Gateway, RequestError
try:
@@ -134,7 +151,7 @@ def _setup_gateway(hass, hass_config, host, key, allow_tradfri_groups):
return False
try:
- factory = APIFactory(host, psk_id=GATEWAY_IDENTITY, psk=key,
+ factory = APIFactory(host, psk_id=identity, psk=key,
loop=hass.loop)
api = factory.request
gateway = Gateway()
@@ -163,22 +180,3 @@ def _setup_gateway(hass, hass_config, host, key, allow_tradfri_groups):
hass.async_add_job(discovery.async_load_platform(
hass, 'sensor', DOMAIN, {'gateway': gateway_id}, hass_config))
return True
-
-
-def _read_config(hass):
- """Read tradfri config."""
- path = hass.config.path(CONFIG_FILE)
-
- if not os.path.isfile(path):
- return {}
-
- with open(path) as f_handle:
- # Guard against empty file
- return json.loads(f_handle.read() or '{}')
-
-
-def _write_config(hass, config):
- """Write tradfri config."""
- data = json.dumps(config)
- with open(hass.config.path(CONFIG_FILE), 'w', encoding='utf-8') as outfile:
- outfile.write(data)
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index 9f36b2fb78f..59090b98e94 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -286,10 +286,11 @@ class SpeechManager(object):
options = options or provider.default_options
if options is not None:
invalid_opts = [opt_name for opt_name in options.keys()
- if opt_name not in provider.supported_options]
+ if opt_name not in (provider.supported_options or
+ [])]
if invalid_opts:
raise HomeAssistantError(
- "Invalid options found: %s", invalid_opts)
+ "Invalid options found: {}".format(invalid_opts))
options_key = ctypes.c_size_t(hash(frozenset(options))).value
else:
options_key = '-'
diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py
index 3ddcc5c716a..e405e5be531 100644
--- a/homeassistant/components/tts/google.py
+++ b/homeassistant/components/tts/google.py
@@ -9,14 +9,15 @@ import logging
import re
import aiohttp
+from aiohttp.hdrs import REFERER, USER_AGENT
import async_timeout
import voluptuous as vol
import yarl
-from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG
+from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
from homeassistant.helpers.aiohttp_client import async_get_clientsession
-REQUIREMENTS = ["gTTS-token==1.1.1"]
+REQUIREMENTS = ['gTTS-token==1.1.1']
_LOGGER = logging.getLogger(__name__)
@@ -52,10 +53,10 @@ class GoogleProvider(Provider):
self.hass = hass
self._lang = lang
self.headers = {
- 'Referer': "http://translate.google.com/",
- 'User-Agent': ("Mozilla/5.0 (Windows NT 10.0; WOW64) "
- "AppleWebKit/537.36 (KHTML, like Gecko) "
- "Chrome/47.0.2526.106 Safari/537.36")
+ REFERER: "http://translate.google.com/",
+ 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.name = 'Google'
@@ -86,7 +87,7 @@ class GoogleProvider(Provider):
url_param = {
'ie': 'UTF-8',
'tl': language,
- 'q': yarl.quote(part, strict=False),
+ 'q': yarl.quote(part),
'tk': part_token,
'total': len(message_parts),
'idx': idx,
diff --git a/homeassistant/components/tts/yandextts.py b/homeassistant/components/tts/yandextts.py
index 05daad55412..b5e965a5b50 100644
--- a/homeassistant/components/tts/yandextts.py
+++ b/homeassistant/components/tts/yandextts.py
@@ -63,6 +63,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Range(min=MIN_SPEED, max=MAX_SPEED)
})
+SUPPORTED_OPTIONS = [
+ CONF_CODEC,
+ CONF_VOICE,
+ CONF_EMOTION,
+ CONF_SPEED,
+]
+
@asyncio.coroutine
def async_get_engine(hass, config):
@@ -94,11 +101,17 @@ class YandexSpeechKitProvider(Provider):
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
+ @property
+ def supported_options(self):
+ """Return list of supported options."""
+ return SUPPORTED_OPTIONS
+
@asyncio.coroutine
def async_get_tts_audio(self, message, language, options=None):
"""Load TTS from yandex."""
websession = async_get_clientsession(self.hass)
actual_language = language
+ options = options or {}
try:
with async_timeout.timeout(10, loop=self.hass.loop):
@@ -106,10 +119,10 @@ class YandexSpeechKitProvider(Provider):
'text': message,
'lang': actual_language,
'key': self._key,
- 'speaker': self._speaker,
- 'format': self._codec,
- 'emotion': self._emotion,
- 'speed': self._speed
+ 'speaker': options.get(CONF_VOICE, self._speaker),
+ 'format': options.get(CONF_CODEC, self._codec),
+ 'emotion': options.get(CONF_EMOTION, self._emotion),
+ 'speed': options.get(CONF_SPEED, self._speed)
}
request = yield from websession.get(
diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py
new file mode 100644
index 00000000000..e1c4a5952af
--- /dev/null
+++ b/homeassistant/components/vacuum/neato.py
@@ -0,0 +1,214 @@
+"""
+Support for Neato Connected Vaccums.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/vacuum.neato/
+"""
+import logging
+
+import requests
+
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.components.vacuum import (
+ VacuumDevice, SUPPORT_BATTERY, SUPPORT_PAUSE, SUPPORT_RETURN_HOME,
+ SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
+ SUPPORT_MAP, ATTR_STATUS, ATTR_BATTERY_LEVEL, ATTR_BATTERY_ICON)
+from homeassistant.components.neato import (
+ NEATO_ROBOTS, NEATO_LOGIN, NEATO_MAP_DATA, ACTION, ERRORS, MODE, ALERTS)
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['neato']
+
+SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \
+ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \
+ SUPPORT_STATUS | SUPPORT_MAP
+
+ICON = "mdi:roomba"
+
+ATTR_CLEAN_START = 'clean_start'
+ATTR_CLEAN_STOP = 'clean_stop'
+ATTR_CLEAN_AREA = 'clean_area'
+ATTR_CLEAN_BATTERY_START = 'battery_level_at_clean_start'
+ATTR_CLEAN_BATTERY_END = 'battery_level_at_clean_end'
+ATTR_CLEAN_SUSP_COUNT = 'clean_suspension_count'
+ATTR_CLEAN_SUSP_TIME = 'clean_suspension_time'
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Neato vacuum."""
+ dev = []
+ for robot in hass.data[NEATO_ROBOTS]:
+ dev.append(NeatoConnectedVacuum(hass, robot))
+ _LOGGER.debug("Adding vacuums %s", dev)
+ add_devices(dev, True)
+
+
+class NeatoConnectedVacuum(VacuumDevice):
+ """Neato Connected Vacuums."""
+
+ def __init__(self, hass, robot):
+ """Initialize the Neato Connected Vacuums."""
+ self.robot = robot
+ self.neato = hass.data[NEATO_LOGIN]
+ self._name = '{}'.format(self.robot.name)
+ self._status_state = None
+ self._clean_state = None
+ self._state = None
+ self._mapdata = hass.data[NEATO_MAP_DATA]
+ self.clean_time_start = None
+ self.clean_time_stop = None
+ self.clean_area = None
+ self.clean_battery_start = None
+ self.clean_battery_end = None
+ self.clean_suspension_charge_count = None
+ self.clean_suspension_time = None
+
+ def update(self):
+ """Update the states of Neato Vacuums."""
+ _LOGGER.debug("Running Vacuums update")
+ self.neato.update_robots()
+ try:
+ self._state = self.robot.state
+ except (requests.exceptions.ConnectionError,
+ requests.exceptions.HTTPError) as ex:
+ _LOGGER.warning("Neato connection error: %s", ex)
+ self._state = None
+ return
+ _LOGGER.debug('self._state=%s', self._state)
+ if self._state['state'] == 1:
+ if self._state['details']['isCharging']:
+ self._status_state = 'Charging'
+ elif (self._state['details']['isDocked'] and
+ not self._state['details']['isCharging']):
+ self._status_state = 'Docked'
+ else:
+ self._status_state = 'Stopped'
+ elif self._state['state'] == 2:
+ if ALERTS.get(self._state['error']) is None:
+ self._status_state = (
+ MODE.get(self._state['cleaning']['mode'])
+ + ' ' + ACTION.get(self._state['action']))
+ else:
+ self._status_state = ALERTS.get(self._state['error'])
+ elif self._state['state'] == 3:
+ self._status_state = 'Paused'
+ elif self._state['state'] == 4:
+ self._status_state = ERRORS.get(self._state['error'])
+
+ if (self.robot.state['action'] == 1 or
+ self.robot.state['action'] == 2 or
+ self.robot.state['action'] == 3 and
+ self.robot.state['state'] == 2):
+ self._clean_state = STATE_ON
+ else:
+ self._clean_state = STATE_OFF
+
+ if not self._mapdata.get(self.robot.serial, {}).get('maps', []):
+ return
+ self.clean_time_start = (
+ (self._mapdata[self.robot.serial]['maps'][0]['start_at']
+ .strip('Z'))
+ .replace('T', ' '))
+ self.clean_time_stop = (
+ (self._mapdata[self.robot.serial]['maps'][0]['end_at'].strip('Z'))
+ .replace('T', ' '))
+ self.clean_area = (
+ self._mapdata[self.robot.serial]['maps'][0]['cleaned_area'])
+ self.clean_suspension_charge_count = (
+ self._mapdata[self.robot.serial]['maps'][0]
+ ['suspended_cleaning_charging_count'])
+ self.clean_suspension_time = (
+ self._mapdata[self.robot.serial]['maps'][0]
+ ['time_in_suspended_cleaning'])
+ self.clean_battery_start = (
+ self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_start'])
+ self.clean_battery_end = (
+ self._mapdata[self.robot.serial]['maps'][0]['run_charge_at_end'])
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def icon(self):
+ """Return the icon to use for device."""
+ return ICON
+
+ @property
+ def supported_features(self):
+ """Flag vacuum cleaner robot features that are supported."""
+ return SUPPORT_NEATO
+
+ @property
+ def battery_level(self):
+ """Return the battery level of the vacuum cleaner."""
+ return self._state['details']['charge']
+
+ @property
+ def status(self):
+ """Return the status of the vacuum cleaner."""
+ return self._status_state
+
+ @property
+ def state_attributes(self):
+ """Return the state attributes of the vacuum cleaner."""
+ data = {}
+
+ if self.status is not None:
+ data[ATTR_STATUS] = self.status
+
+ if self.battery_level is not None:
+ data[ATTR_BATTERY_LEVEL] = self.battery_level
+ data[ATTR_BATTERY_ICON] = self.battery_icon
+
+ if self.clean_time_start is not None:
+ data[ATTR_CLEAN_START] = self.clean_time_start
+ if self.clean_time_stop is not None:
+ data[ATTR_CLEAN_STOP] = self.clean_time_stop
+ if self.clean_area is not None:
+ data[ATTR_CLEAN_AREA] = self.clean_area
+ if self.clean_suspension_charge_count is not None:
+ data[ATTR_CLEAN_SUSP_COUNT] = (
+ self.clean_suspension_charge_count)
+ if self.clean_suspension_time is not None:
+ data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time
+ if self.clean_battery_start is not None:
+ data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start
+ if self.clean_battery_end is not None:
+ data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end
+
+ return data
+
+ def turn_on(self, **kwargs):
+ """Turn the vacuum on and start cleaning."""
+ self.robot.start_cleaning()
+
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self._clean_state == STATE_ON
+
+ def turn_off(self, **kwargs):
+ """Turn the switch off."""
+ self.robot.pause_cleaning()
+ self.robot.send_to_base()
+
+ def return_to_base(self, **kwargs):
+ """Set the vacuum cleaner to return to the dock."""
+ self.robot.send_to_base()
+
+ def stop(self, **kwargs):
+ """Stop the vacuum cleaner."""
+ self.robot.stop_cleaning()
+
+ def start_pause(self, **kwargs):
+ """Start, pause or resume the cleaning task."""
+ if self._state['state'] == 1:
+ self.robot.start_cleaning()
+ elif self._state['state'] == 2 and\
+ ALERTS.get(self._state['error']) is None:
+ self.robot.pause_cleaning()
+ if self._state['state'] == 3:
+ self.robot.resume_cleaning()
diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py
index ed19e220008..829d0878ffe 100644
--- a/homeassistant/components/vacuum/xiaomi_miio.py
+++ b/homeassistant/components/vacuum/xiaomi_miio.py
@@ -21,7 +21,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-miio==0.3.0']
+REQUIREMENTS = ['python-miio==0.3.1']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py
index 3ed6efc25d7..94f712896cc 100644
--- a/homeassistant/components/verisure.py
+++ b/homeassistant/components/verisure.py
@@ -27,6 +27,7 @@ ATTR_DEVICE_SERIAL = 'device_serial'
CONF_ALARM = 'alarm'
CONF_CODE_DIGITS = 'code_digits'
CONF_DOOR_WINDOW = 'door_window'
+CONF_GIID = 'giid'
CONF_HYDROMETERS = 'hygrometers'
CONF_LOCKS = 'locks'
CONF_MOUSE = 'mouse'
@@ -47,6 +48,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_ALARM, default=True): cv.boolean,
vol.Optional(CONF_CODE_DIGITS, default=4): cv.positive_int,
vol.Optional(CONF_DOOR_WINDOW, default=True): cv.boolean,
+ vol.Optional(CONF_GIID): cv.string,
vol.Optional(CONF_HYDROMETERS, default=True): cv.boolean,
vol.Optional(CONF_LOCKS, default=True): cv.boolean,
vol.Optional(CONF_MOUSE, default=True): cv.boolean,
@@ -110,6 +112,8 @@ class VerisureHub(object):
domain_config[CONF_USERNAME],
domain_config[CONF_PASSWORD])
+ self.giid = domain_config.get(CONF_GIID)
+
import jsonpath
self.jsonpath = jsonpath.jsonpath
@@ -120,6 +124,8 @@ class VerisureHub(object):
except self._verisure.Error as ex:
_LOGGER.error('Could not log in to verisure, %s', ex)
return False
+ if self.giid:
+ return self.set_giid()
return True
def logout(self):
@@ -131,6 +137,15 @@ class VerisureHub(object):
return False
return True
+ def set_giid(self):
+ """Set installation GIID."""
+ try:
+ self.session.set_giid(self.giid)
+ except self._verisure.Error as ex:
+ _LOGGER.error('Could not set installation GIID, %s', ex)
+ return False
+ return True
+
@Throttle(timedelta(seconds=60))
def update_overview(self):
"""Update the overview."""
diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py
index 9c8366e7f7e..4cee6ea2139 100644
--- a/homeassistant/components/volvooncall.py
+++ b/homeassistant/components/volvooncall.py
@@ -22,13 +22,14 @@ DOMAIN = 'volvooncall'
DATA_KEY = DOMAIN
-REQUIREMENTS = ['volvooncall==0.3.3']
+REQUIREMENTS = ['volvooncall==0.4.0']
_LOGGER = logging.getLogger(__name__)
CONF_UPDATE_INTERVAL = 'update_interval'
MIN_UPDATE_INTERVAL = timedelta(minutes=1)
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
+CONF_REGION = 'region'
CONF_SERVICE_URL = 'service_url'
SIGNAL_VEHICLE_SEEN = '{}.vehicle_seen'.format(DOMAIN)
@@ -58,6 +59,7 @@ CONFIG_SCHEMA = vol.Schema({
{cv.slug: cv.string}),
vol.Optional(CONF_RESOURCES): vol.All(
cv.ensure_list, [vol.In(RESOURCES)]),
+ vol.Optional(CONF_REGION): cv.string,
vol.Optional(CONF_SERVICE_URL): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -65,11 +67,12 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config):
"""Set up the Volvo On Call component."""
- from volvooncall import Connection, DEFAULT_SERVICE_URL
+ from volvooncall import Connection
connection = Connection(
config[DOMAIN].get(CONF_USERNAME),
config[DOMAIN].get(CONF_PASSWORD),
- config[DOMAIN].get(CONF_SERVICE_URL, DEFAULT_SERVICE_URL))
+ config[DOMAIN].get(CONF_SERVICE_URL),
+ config[DOMAIN].get(CONF_REGION))
interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL)
diff --git a/homeassistant/components/vultr.py b/homeassistant/components/vultr.py
new file mode 100644
index 00000000000..59fc707bb28
--- /dev/null
+++ b/homeassistant/components/vultr.py
@@ -0,0 +1,105 @@
+"""
+Support for Vultr.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/vultr/
+"""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+
+from homeassistant.const import CONF_API_KEY
+from homeassistant.util import Throttle
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['vultr==0.1.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_AUTO_BACKUPS = 'auto_backups'
+ATTR_ALLOWED_BANDWIDTH = 'allowed_bandwidth_gb'
+ATTR_COST_PER_MONTH = 'cost_per_month'
+ATTR_CURRENT_BANDWIDTH_USED = 'current_bandwidth_gb'
+ATTR_CREATED_AT = 'created_at'
+ATTR_DISK = 'disk'
+ATTR_SUBSCRIPTION_ID = 'subid'
+ATTR_SUBSCRIPTION_NAME = 'label'
+ATTR_IPV4_ADDRESS = 'ipv4_address'
+ATTR_IPV6_ADDRESS = 'ipv6_address'
+ATTR_MEMORY = 'memory'
+ATTR_OS = 'os'
+ATTR_PENDING_CHARGES = 'pending_charges'
+ATTR_REGION = 'region'
+ATTR_VCPUS = 'vcpus'
+
+CONF_SUBSCRIPTION = 'subscription'
+
+DATA_VULTR = 'data_vultr'
+DOMAIN = 'vultr'
+
+NOTIFICATION_ID = 'vultr_notification'
+NOTIFICATION_TITLE = 'Vultr Setup'
+
+VULTR_PLATFORMS = ['binary_sensor', 'sensor', 'switch']
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_API_KEY): cv.string,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up the Vultr component."""
+ api_key = config[DOMAIN].get(CONF_API_KEY)
+
+ vultr = Vultr(api_key)
+
+ try:
+ vultr.update()
+ except RuntimeError as ex:
+ _LOGGER.error("Failed to make update API request because: %s",
+ ex)
+ hass.components.persistent_notification.create(
+ 'Error: {}'
+ ''.format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return False
+
+ hass.data[DATA_VULTR] = vultr
+ return True
+
+
+class Vultr(object):
+ """Handle all communication with the Vultr API."""
+
+ def __init__(self, api_key):
+ """Initialize the Vultr connection."""
+ from vultr import Vultr as VultrAPI
+
+ self._api_key = api_key
+ self.data = None
+ self.api = VultrAPI(self._api_key)
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Use the data from Vultr API."""
+ self.data = self.api.server_list()
+
+ def _force_update(self):
+ """Use the data from Vultr API."""
+ self.data = self.api.server_list()
+
+ def halt(self, subscription):
+ """Halt a subscription (hard power off)."""
+ self.api.server_halt(subscription)
+ self._force_update()
+
+ def start(self, subscription):
+ """Start a subscription."""
+ self.api.server_start(subscription)
+ self._force_update()
diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py
index 9e927da893e..acb95c17814 100644
--- a/homeassistant/components/weather/__init__.py
+++ b/homeassistant/components/weather/__init__.py
@@ -6,11 +6,10 @@ https://home-assistant.io/components/weather/
"""
import asyncio
import logging
-from numbers import Number
-from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity_component import EntityComponent
-from homeassistant.util.temperature import convert as convert_temperature
+from homeassistant.helpers.temperature import display_temp as show_temp
+from homeassistant.const import PRECISION_WHOLE, PRECISION_TENTHS, TEMP_CELSIUS
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
from homeassistant.helpers.entity import Entity
@@ -98,11 +97,19 @@ class WeatherEntity(Entity):
"""Return the forecast."""
return None
+ @property
+ def precision(self):
+ """Return the forecast."""
+ return PRECISION_TENTHS if self.temperature_unit == TEMP_CELSIUS \
+ else PRECISION_WHOLE
+
@property
def state_attributes(self):
"""Return the state attributes."""
data = {
- ATTR_WEATHER_TEMPERATURE: self._temp_for_display(self.temperature),
+ ATTR_WEATHER_TEMPERATURE: show_temp(
+ self.hass, self.temperature, self.temperature_unit,
+ self.precision),
ATTR_WEATHER_HUMIDITY: self.humidity,
}
@@ -134,8 +141,9 @@ class WeatherEntity(Entity):
forecast = []
for forecast_entry in self.forecast:
forecast_entry = dict(forecast_entry)
- forecast_entry[ATTR_FORECAST_TEMP] = self._temp_for_display(
- forecast_entry[ATTR_FORECAST_TEMP])
+ forecast_entry[ATTR_FORECAST_TEMP] = show_temp(
+ self.hass, forecast_entry[ATTR_FORECAST_TEMP],
+ self.temperature_unit, self.precision)
forecast.append(forecast_entry)
data[ATTR_FORECAST] = forecast
@@ -151,19 +159,3 @@ class WeatherEntity(Entity):
def condition(self):
"""Return the current condition."""
raise NotImplementedError()
-
- def _temp_for_display(self, temp):
- """Convert temperature into preferred units for display purposes."""
- unit = self.temperature_unit
- hass_unit = self.hass.config.units.temperature_unit
-
- if (temp is None or not isinstance(temp, Number) or
- unit == hass_unit):
- return temp
-
- value = convert_temperature(temp, unit, hass_unit)
-
- if hass_unit == TEMP_CELSIUS:
- return round(value, 1)
- # Users of fahrenheit generally expect integer units.
- return round(value)
diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py
index 0a404447346..02e07996213 100644
--- a/homeassistant/components/weather/demo.py
+++ b/homeassistant/components/weather/demo.py
@@ -31,7 +31,7 @@ CONDITION_CLASSES = {
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo weather."""
add_devices([
- DemoWeather('South', 'Sunshine', 21, 92, 1099, 0.5, TEMP_CELSIUS,
+ DemoWeather('South', 'Sunshine', 21.6414, 92, 1099, 0.5, TEMP_CELSIUS,
[22, 19, 15, 12, 14, 18, 21]),
DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT,
[-10, -13, -18, -23, -19, -14, -9])
diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py
index 12dc73af5cd..514eda0f09f 100644
--- a/homeassistant/components/weather/yweather.py
+++ b/homeassistant/components/weather/yweather.py
@@ -15,7 +15,7 @@ from homeassistant.components.weather import (
ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME)
from homeassistant.const import (TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN)
-REQUIREMENTS = ["yahooweather==0.8"]
+REQUIREMENTS = ["yahooweather==0.9"]
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
index e9f567c04d3..a1fb0ca9cac 100644
--- a/homeassistant/components/websocket_api.py
+++ b/homeassistant/components/websocket_api.py
@@ -202,15 +202,16 @@ class WebsocketAPIView(HomeAssistantView):
def get(self, request):
"""Handle an incoming websocket connection."""
# pylint: disable=no-self-use
- return ActiveConnection(request.app['hass']).handle(request)
+ return ActiveConnection(request.app['hass'], request).handle()
class ActiveConnection:
"""Handle an active websocket client connection."""
- def __init__(self, hass):
+ def __init__(self, hass, request):
"""Initialize an active connection."""
self.hass = hass
+ self.request = request
self.wsock = None
self.event_listeners = {}
self.to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG, loop=hass.loop)
@@ -259,8 +260,9 @@ class ActiveConnection:
self._writer_task.cancel()
@asyncio.coroutine
- def handle(self, request):
+ def handle(self):
"""Handle the websocket connection."""
+ request = self.request
wsock = self.wsock = web.WebSocketResponse()
yield from wsock.prepare(request)
self.debug("Connected")
@@ -350,7 +352,7 @@ class ActiveConnection:
if wsock.closed:
self.debug("Connection closed by client")
else:
- self.log_error("Unexpected TypeError", msg)
+ _LOGGER.exception("Unexpected TypeError: %s", msg)
except ValueError as err:
msg = "Received invalid JSON"
@@ -483,9 +485,14 @@ class ActiveConnection:
Async friendly.
"""
msg = GET_PANELS_MESSAGE_SCHEMA(msg)
+ panels = {
+ panel:
+ self.hass.data[frontend.DATA_PANELS][panel].to_response(
+ self.hass, self.request)
+ for panel in self.hass.data[frontend.DATA_PANELS]}
self.to_write.put_nowait(result_message(
- msg['id'], self.hass.data[frontend.DATA_PANELS]))
+ msg['id'], panels))
def handle_ping(self, msg):
"""Handle ping command.
diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py
index 0e6e41c63a5..2faeccde154 100755
--- a/homeassistant/components/zwave/__init__.py
+++ b/homeassistant/components/zwave/__init__.py
@@ -123,6 +123,17 @@ SET_WAKEUP_SCHEMA = vol.Schema({
vol.All(vol.Coerce(int), cv.positive_int),
})
+HEAL_NODE_SCHEMA = vol.Schema({
+ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
+ vol.Optional(const.ATTR_RETURN_ROUTES, default=False): cv.boolean,
+})
+
+TEST_NODE_SCHEMA = vol.Schema({
+ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int),
+ vol.Optional(const.ATTR_MESSAGES, default=1): cv.positive_int,
+})
+
+
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
vol.Optional(CONF_POLLING_INTENSITY): cv.positive_int,
vol.Optional(CONF_IGNORED, default=DEFAULT_CONF_IGNORED): cv.boolean,
@@ -564,6 +575,22 @@ def setup(hass, config):
_LOGGER.info("Node %s on instance %s does not have resettable "
"meters.", node_id, instance)
+ def heal_node(service):
+ """Heal a node on the network."""
+ node_id = service.data.get(const.ATTR_NODE_ID)
+ update_return_routes = service.data.get(const.ATTR_RETURN_ROUTES)
+ node = network.nodes[node_id]
+ _LOGGER.info("Z-Wave node heal running for node %s", node_id)
+ node.heal(update_return_routes)
+
+ def test_node(service):
+ """Send test messages to a node on the network."""
+ node_id = service.data.get(const.ATTR_NODE_ID)
+ messages = service.data.get(const.ATTR_MESSAGES)
+ node = network.nodes[node_id]
+ _LOGGER.info("Sending %s test-messages to node %s.", messages, node_id)
+ node.test(messages)
+
def start_zwave(_service_or_event):
"""Startup Z-Wave network."""
_LOGGER.info("Starting Z-Wave network...")
@@ -684,6 +711,16 @@ def setup(hass, config):
set_poll_intensity,
descriptions[const.SERVICE_SET_POLL_INTENSITY],
schema=SET_POLL_INTENSITY_SCHEMA)
+ hass.services.register(DOMAIN, const.SERVICE_HEAL_NODE,
+ heal_node,
+ descriptions[
+ const.SERVICE_HEAL_NODE],
+ schema=HEAL_NODE_SCHEMA)
+ hass.services.register(DOMAIN, const.SERVICE_TEST_NODE,
+ test_node,
+ descriptions[
+ const.SERVICE_TEST_NODE],
+ schema=TEST_NODE_SCHEMA)
# Setup autoheal
if autoheal:
diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py
index dced1689dba..5f0a7f4750b 100644
--- a/homeassistant/components/zwave/const.py
+++ b/homeassistant/components/zwave/const.py
@@ -8,7 +8,9 @@ ATTR_INSTANCE = "instance"
ATTR_GROUP = "group"
ATTR_VALUE_ID = "value_id"
ATTR_OBJECT_ID = "object_id"
+ATTR_MESSAGES = "messages"
ATTR_NAME = "name"
+ATTR_RETURN_ROUTES = "return_routes"
ATTR_SCENE_ID = "scene_id"
ATTR_SCENE_DATA = "scene_data"
ATTR_BASIC_LEVEL = "basic_level"
@@ -32,7 +34,9 @@ SERVICE_ADD_NODE_SECURE = "add_node_secure"
SERVICE_REMOVE_NODE = "remove_node"
SERVICE_CANCEL_COMMAND = "cancel_command"
SERVICE_HEAL_NETWORK = "heal_network"
+SERVICE_HEAL_NODE = "heal_node"
SERVICE_SOFT_RESET = "soft_reset"
+SERVICE_TEST_NODE = "test_node"
SERVICE_TEST_NETWORK = "test_network"
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"
SERVICE_PRINT_CONFIG_PARAMETER = "print_config_parameter"
diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py
index 44a30cdc529..04446cff9a1 100644
--- a/homeassistant/components/zwave/node_entity.py
+++ b/homeassistant/components/zwave/node_entity.py
@@ -69,11 +69,6 @@ class ZWaveBaseEntity(Entity):
self.hass.loop.call_later(0.1, do_update)
-def sub_status(status, stage):
- """Format sub-status."""
- return '{} ({})'.format(status, stage) if stage else status
-
-
class ZWaveNodeEntity(ZWaveBaseEntity):
"""Representation of a Z-Wave node."""
@@ -201,17 +196,17 @@ class ZWaveNodeEntity(ZWaveBaseEntity):
"""Return the state."""
if ATTR_READY not in self._attributes:
return None
- stage = ''
- if not self._attributes[ATTR_READY]:
- # If node is not ready use stage as sub-status.
- stage = self._attributes[ATTR_QUERY_STAGE]
+
if self._attributes[ATTR_FAILED]:
- return sub_status('Dead', stage)
+ return 'dead'
+ if self._attributes[ATTR_QUERY_STAGE] != 'Complete':
+ return 'initializing'
if not self._attributes[ATTR_AWAKE]:
- return sub_status('Sleeping', stage)
+ return 'sleeping'
if self._attributes[ATTR_READY]:
- return sub_status('Ready', stage)
- return stage
+ return 'ready'
+
+ return None
@property
def should_poll(self):
diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml
index 06e317333be..ba8e177c9f7 100644
--- a/homeassistant/components/zwave/services.yaml
+++ b/homeassistant/components/zwave/services.yaml
@@ -28,6 +28,17 @@ cancel_command:
heal_network:
description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for progress.
+ fields:
+ return_routes:
+ description: Wheter or not to update the return routes from the nodes to the controller. Defaults to False.
+ example: True
+
+heal_node:
+ description: Start a Z-Wave node heal. Refer to OZW.log for progress.
+ fields:
+ return_routes:
+ description: Wheter or not to update the return routes from the node to the controller. Defaults to False.
+ example: True
remove_node:
description: Remove a node from the Z-Wave network. Refer to OZW.log for progress.
@@ -120,6 +131,16 @@ soft_reset:
test_network:
description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW.log for progress.
+test_node:
+ description: This will send test messages to a node in the Z-Wave network. This could bring back dead nodes.
+ fields:
+ node_id:
+ description: ID of the node to send test messages to.
+ example: 10
+ messages:
+ description: Optional. Amount of test messages to send.
+ example: 3
+
rename_node:
description: Set the name of a node. This will also affect the IDs of all entities in the node.
fields:
diff --git a/homeassistant/const.py b/homeassistant/const.py
index fc471c6323d..d8b4dfcb044 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,45 +1,15 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 57
-PATCH_VERSION = '3'
+MINOR_VERSION = 58
+PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2)
REQUIRED_PYTHON_VER_WIN = (3, 5, 2)
CONSTRAINT_FILE = 'package_constraints.txt'
-PROJECT_NAME = 'Home Assistant'
-PROJECT_PACKAGE_NAME = 'homeassistant'
-PROJECT_LICENSE = 'Apache License 2.0'
-PROJECT_AUTHOR = 'The Home Assistant Authors'
-PROJECT_COPYRIGHT = ' 2013, {}'.format(PROJECT_AUTHOR)
-PROJECT_URL = 'https://home-assistant.io/'
-PROJECT_EMAIL = 'hello@home-assistant.io'
-PROJECT_DESCRIPTION = ('Open-source home automation platform '
- 'running on Python 3.')
-PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source '
- 'home automation platform running on Python 3. '
- 'Track and control all devices at home and '
- 'automate control. '
- 'Installation in less than a minute.')
-PROJECT_CLASSIFIERS = [
- 'Intended Audience :: End Users/Desktop',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: Apache Software License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python :: 3.4',
- 'Topic :: Home Automation'
-]
-
-PROJECT_GITHUB_USERNAME = 'home-assistant'
-PROJECT_GITHUB_REPOSITORY = 'home-assistant'
-
-PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
-GITHUB_PATH = '{}/{}'.format(PROJECT_GITHUB_USERNAME,
- PROJECT_GITHUB_REPOSITORY)
-GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
-
+# Format for platforms
PLATFORM_FORMAT = '{}.{}'
# Can be used to specify a catch all when registering state or event listeners.
@@ -48,8 +18,7 @@ MATCH_ALL = '*'
# If no name is specified
DEVICE_DEFAULT_NAME = 'Unnamed Device'
-WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
-
+# Sun events
SUN_EVENT_SUNSET = 'sunset'
SUN_EVENT_SUNRISE = 'sunrise'
@@ -157,6 +126,7 @@ CONF_SHOW_ON_MAP = 'show_on_map'
CONF_SLAVE = 'slave'
CONF_SSL = 'ssl'
CONF_STATE = 'state'
+CONF_STATE_TEMPLATE = 'state_template'
CONF_STRUCTURE = 'structure'
CONF_SWITCHES = 'switches'
CONF_TEMPERATURE_UNIT = 'temperature_unit'
@@ -429,24 +399,7 @@ HTTP_BASIC_AUTHENTICATION = 'basic'
HTTP_DIGEST_AUTHENTICATION = 'digest'
HTTP_HEADER_HA_AUTH = 'X-HA-access'
-HTTP_HEADER_ACCEPT_ENCODING = 'Accept-Encoding'
-HTTP_HEADER_AUTH = 'Authorization'
-HTTP_HEADER_USER_AGENT = 'User-Agent'
-HTTP_HEADER_CONTENT_TYPE = 'Content-type'
-HTTP_HEADER_CONTENT_ENCODING = 'Content-Encoding'
-HTTP_HEADER_VARY = 'Vary'
-HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
-HTTP_HEADER_CACHE_CONTROL = 'Cache-Control'
-HTTP_HEADER_EXPIRES = 'Expires'
-HTTP_HEADER_ORIGIN = 'Origin'
HTTP_HEADER_X_REQUESTED_WITH = 'X-Requested-With'
-HTTP_HEADER_ACCEPT = 'Accept'
-HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
-HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
-
-ALLOWED_CORS_HEADERS = [HTTP_HEADER_ORIGIN, HTTP_HEADER_ACCEPT,
- HTTP_HEADER_X_REQUESTED_WITH, HTTP_HEADER_CONTENT_TYPE,
- HTTP_HEADER_HA_AUTH]
CONTENT_TYPE_JSON = 'application/json'
CONTENT_TYPE_MULTIPART = 'multipart/x-mixed-replace; boundary={}'
@@ -463,3 +416,10 @@ VOLUME = 'volume' # type: str
TEMPERATURE = 'temperature' # type: str
SPEED_MS = 'speed_ms' # type: str
ILLUMINANCE = 'illuminance' # type: str
+
+WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
+
+# The degree of precision for platforms
+PRECISION_WHOLE = 1
+PRECISION_HALVES = 0.5
+PRECISION_TENTHS = 0.1
diff --git a/homeassistant/core.py b/homeassistant/core.py
index 31bb281aeaa..30be92af153 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -387,7 +387,7 @@ class EventBus(object):
@callback
def async_fire(self, event_type: str, event_data=None,
- origin=EventOrigin.local, wait=False):
+ origin=EventOrigin.local):
"""Fire an event.
This method must be run in the event loop.
@@ -395,8 +395,10 @@ class EventBus(object):
listeners = self._listeners.get(event_type, [])
# EVENT_HOMEASSISTANT_CLOSE should go only to his listeners
- if event_type != EVENT_HOMEASSISTANT_CLOSE:
- listeners = self._listeners.get(MATCH_ALL, []) + listeners
+ match_all_listeners = self._listeners.get(MATCH_ALL)
+ if (match_all_listeners is not None and
+ event_type != EVENT_HOMEASSISTANT_CLOSE):
+ listeners = match_all_listeners + listeners
event = Event(event_type, event_data, origin)
@@ -673,15 +675,6 @@ class StateMachine(object):
state_obj = self.get(entity_id)
return state_obj is not None and state_obj.state == state
- def is_state_attr(self, entity_id, name, value):
- """Test if entity exists and has a state attribute set to value.
-
- Async friendly.
- """
- state_obj = self.get(entity_id)
- return state_obj is not None and \
- state_obj.attributes.get(name, None) == value
-
def remove(self, entity_id):
"""Remove the state of an entity.
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index e5512b9140e..e5d0a34f76e 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -12,8 +12,7 @@ import voluptuous as vol
from homeassistant.loader import get_platform
from homeassistant.const import (
- CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, CONF_PLATFORM,
- CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT,
+ CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT,
CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS,
CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET,
SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC)
@@ -563,16 +562,3 @@ SCRIPT_SCHEMA = vol.All(
[vol.Any(SERVICE_SCHEMA, _SCRIPT_DELAY_SCHEMA,
_SCRIPT_WAIT_TEMPLATE_SCHEMA, EVENT_SCHEMA, CONDITION_SCHEMA)],
)
-
-FILTER_SCHEMA = vol.Schema({
- vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
- vol.Optional(CONF_ENTITIES, default=[]): entity_ids,
- vol.Optional(CONF_DOMAINS, default=[]):
- vol.All(ensure_list, [string])
- }),
- vol.Optional(CONF_INCLUDE, default={}): vol.Schema({
- vol.Optional(CONF_ENTITIES, default=[]): entity_ids,
- vol.Optional(CONF_DOMAINS, default=[]):
- vol.All(ensure_list, [string])
- })
-})
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index da82fc9202f..78db0890ab1 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -11,7 +11,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, STATE_ON,
STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT,
ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS)
-from homeassistant.core import HomeAssistant
+from homeassistant.core import HomeAssistant, callback
from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.exceptions import NoEntitySpecifiedError
from homeassistant.util import ensure_unique_string, slugify
@@ -41,6 +41,7 @@ def generate_entity_id(entity_id_format: str, name: Optional[str],
entity_id_format.format(slugify(name)), current_ids)
+@callback
def async_generate_entity_id(entity_id_format: str, name: Optional[str],
current_ids: Optional[List[str]]=None,
hass: Optional[HomeAssistant]=None) -> str:
@@ -168,15 +169,9 @@ class Entity(object):
def update(self):
"""Retrieve latest state.
- When not implemented, will forward call to async version if available.
+ For asyncio use coroutine async_update.
"""
- async_update = getattr(self, 'async_update', None)
-
- if async_update is None:
- return
-
- # pylint: disable=not-callable
- run_coroutine_threadsafe(async_update(), self.hass.loop).result()
+ pass
# DO NOT OVERWRITE
# These properties and methods are either managed by Home Assistant or they
@@ -240,10 +235,10 @@ class Entity(object):
if not self._slow_reported and end - start > 0.4:
self._slow_reported = True
- _LOGGER.warning("Updating state for %s took %.3f seconds. "
+ _LOGGER.warning("Updating state for %s (%s) took %.3f seconds. "
"Please report platform to the developers at "
"https://goo.gl/Nvioub", self.entity_id,
- end - start)
+ type(self), end - start)
# Overwrite properties that have been set in the config file.
if DATA_CUSTOMIZE in self.hass.data:
@@ -277,10 +272,12 @@ class Entity(object):
"""
self.hass.add_job(self.async_update_ha_state(force_refresh))
+ @callback
def async_schedule_update_ha_state(self, force_refresh=False):
"""Schedule a update ha state change task."""
self.hass.async_add_job(self.async_update_ha_state(force_refresh))
+ @asyncio.coroutine
def async_device_update(self, warning=True):
"""Process 'update' or 'async_update' from entity.
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index e805f277483..9b25b8ddbd4 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -97,6 +97,7 @@ class EntityComponent(object):
expand_group
).result()
+ @callback
def async_extract_from_service(self, service, expand_group=True):
"""Extract all known and available entities from a service call.
diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py
index d8d3f1c9325..f78c70e57d3 100644
--- a/homeassistant/helpers/entityfilter.py
+++ b/homeassistant/helpers/entityfilter.py
@@ -1,6 +1,30 @@
"""Helper class to implement include/exclude of entities and domains."""
+import voluptuous as vol
+
from homeassistant.core import split_entity_id
+from homeassistant.helpers import config_validation as cv
+
+CONF_INCLUDE_DOMAINS = 'include_domains'
+CONF_INCLUDE_ENTITIES = 'include_entities'
+CONF_EXCLUDE_DOMAINS = 'exclude_domains'
+CONF_EXCLUDE_ENTITIES = 'exclude_entities'
+
+FILTER_SCHEMA = vol.All(
+ vol.Schema({
+ vol.Optional(CONF_EXCLUDE_DOMAINS, default=[]):
+ vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_EXCLUDE_ENTITIES, default=[]): cv.entity_ids,
+ vol.Optional(CONF_INCLUDE_DOMAINS, default=[]):
+ vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_INCLUDE_ENTITIES, default=[]): cv.entity_ids,
+ }),
+ lambda config: generate_filter(
+ config[CONF_INCLUDE_DOMAINS],
+ config[CONF_INCLUDE_ENTITIES],
+ config[CONF_EXCLUDE_DOMAINS],
+ config[CONF_EXCLUDE_ENTITIES],
+ ))
def generate_filter(include_domains, include_entities,
diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py
new file mode 100644
index 00000000000..a4626c33210
--- /dev/null
+++ b/homeassistant/helpers/temperature.py
@@ -0,0 +1,33 @@
+"""Temperature helpers for Home Assistant."""
+from numbers import Number
+
+from homeassistant.core import HomeAssistant
+from homeassistant.util.temperature import convert as convert_temperature
+
+
+def display_temp(hass: HomeAssistant, temperature: float, unit: str,
+ precision: float) -> float:
+ """Convert temperature into preferred units for display purposes."""
+ temperature_unit = unit
+ ha_unit = hass.config.units.temperature_unit
+
+ if temperature is None:
+ return temperature
+
+ # If the temperature is not a number this can cause issues
+ # with Polymer components, so bail early there.
+ if not isinstance(temperature, Number):
+ raise TypeError(
+ "Temperature is not a number: {}".format(temperature))
+
+ if temperature_unit != ha_unit:
+ temperature = convert_temperature(
+ temperature, temperature_unit, ha_unit)
+
+ # Round in the units appropriate
+ if precision == 0.5:
+ return round(temperature * 2) / 2.0
+ elif precision == 0.1:
+ return round(temperature, 1)
+ # Integer as a fall back (PRECISION_WHOLE)
+ return round(temperature)
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 6f83688623a..bf1b88e1c3f 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -107,7 +107,8 @@ class Template(object):
This method must be run in the event loop.
"""
- self._ensure_compiled()
+ if self._compiled is None:
+ self._ensure_compiled()
if variables is not None:
kwargs.update(variables)
@@ -135,7 +136,8 @@ class Template(object):
This method must be run in the event loop.
"""
- self._ensure_compiled()
+ if self._compiled is None:
+ self._ensure_compiled()
variables = {
'value': value
@@ -154,20 +156,17 @@ class Template(object):
def _ensure_compiled(self):
"""Bind a template to a specific hass instance."""
- if self._compiled is not None:
- return
-
self.ensure_valid()
assert self.hass is not None, 'hass variable not set on template'
- location_methods = LocationMethods(self.hass)
+ template_methods = TemplateMethods(self.hass)
global_vars = ENV.make_globals({
- 'closest': location_methods.closest,
- 'distance': location_methods.distance,
+ 'closest': template_methods.closest,
+ 'distance': template_methods.distance,
'is_state': self.hass.states.is_state,
- 'is_state_attr': self.hass.states.is_state_attr,
+ 'is_state_attr': template_methods.is_state_attr,
'states': AllStates(self.hass),
})
@@ -272,11 +271,11 @@ def _wrap_state(state):
return None if state is None else TemplateState(state)
-class LocationMethods(object):
- """Class to expose distance helpers to templates."""
+class TemplateMethods(object):
+ """Class to expose helpers to templates."""
def __init__(self, hass):
- """Initialize the distance helpers."""
+ """Initialize the helpers."""
self._hass = hass
def closest(self, *args):
@@ -390,6 +389,12 @@ class LocationMethods(object):
return self._hass.config.units.length(
loc_util.distance(*locations[0] + locations[1]), 'm')
+ def is_state_attr(self, entity_id, name, value):
+ """Test if a state is a specific attribute."""
+ state_obj = self._hass.states.get(entity_id)
+ return state_obj is not None and \
+ state_obj.attributes.get(name) == value
+
def _resolve_state(self, entity_id_or_state):
"""Return state or entity_id if given."""
if isinstance(entity_id_or_state, State):
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index a2dc9572c81..056ed2f3fa6 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -5,8 +5,8 @@ pip>=8.0.3
jinja2>=2.9.6
voluptuous==0.10.5
typing>=3,<4
-aiohttp==2.2.5
-yarl==0.13
+aiohttp==2.3.2
+yarl==0.14.0
async_timeout==2.0.0
chardet==3.0.4
astral==1.4
diff --git a/homeassistant/remote.py b/homeassistant/remote.py
index c8fe62f64d9..7d032303548 100644
--- a/homeassistant/remote.py
+++ b/homeassistant/remote.py
@@ -15,22 +15,18 @@ import urllib.parse
from typing import Optional
+from aiohttp.hdrs import METH_GET, METH_POST, METH_DELETE, CONTENT_TYPE
import requests
from homeassistant import core as ha
from homeassistant.const import (
- HTTP_HEADER_HA_AUTH, SERVER_PORT, URL_API,
- URL_API_EVENTS, URL_API_EVENTS_EVENT, URL_API_SERVICES, URL_API_CONFIG,
- URL_API_SERVICES_SERVICE, URL_API_STATES, URL_API_STATES_ENTITY,
- HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON)
+ URL_API, SERVER_PORT, URL_API_CONFIG, URL_API_EVENTS, URL_API_STATES,
+ URL_API_SERVICES, CONTENT_TYPE_JSON, HTTP_HEADER_HA_AUTH,
+ URL_API_EVENTS_EVENT, URL_API_STATES_ENTITY, URL_API_SERVICES_SERVICE)
from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
-METHOD_GET = 'get'
-METHOD_POST = 'post'
-METHOD_DELETE = 'delete'
-
class APIStatus(enum.Enum):
"""Representation of an API status."""
@@ -67,9 +63,7 @@ class API(object):
self.base_url += ':{}'.format(port)
self.status = None
- self._headers = {
- HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON,
- }
+ self._headers = {CONTENT_TYPE: CONTENT_TYPE_JSON}
if api_password is not None:
self._headers[HTTP_HEADER_HA_AUTH] = api_password
@@ -89,7 +83,7 @@ class API(object):
url = urllib.parse.urljoin(self.base_url, path)
try:
- if method == METHOD_GET:
+ if method == METH_GET:
return requests.get(
url, params=data, timeout=timeout, headers=self._headers)
@@ -144,7 +138,7 @@ class JSONEncoder(json.JSONEncoder):
def validate_api(api):
"""Make a call to validate API."""
try:
- req = api(METHOD_GET, URL_API)
+ req = api(METH_GET, URL_API)
if req.status_code == 200:
return APIStatus.OK
@@ -161,7 +155,7 @@ def validate_api(api):
def get_event_listeners(api):
"""List of events that is being listened for."""
try:
- req = api(METHOD_GET, URL_API_EVENTS)
+ req = api(METH_GET, URL_API_EVENTS)
return req.json() if req.status_code == 200 else {}
@@ -175,7 +169,7 @@ def get_event_listeners(api):
def fire_event(api, event_type, data=None):
"""Fire an event at remote API."""
try:
- req = api(METHOD_POST, URL_API_EVENTS_EVENT.format(event_type), data)
+ req = api(METH_POST, URL_API_EVENTS_EVENT.format(event_type), data)
if req.status_code != 200:
_LOGGER.error("Error firing event: %d - %s",
@@ -188,7 +182,7 @@ def fire_event(api, event_type, data=None):
def get_state(api, entity_id):
"""Query given API for state of entity_id."""
try:
- req = api(METHOD_GET, URL_API_STATES_ENTITY.format(entity_id))
+ req = api(METH_GET, URL_API_STATES_ENTITY.format(entity_id))
# req.status_code == 422 if entity does not exist
@@ -205,7 +199,7 @@ def get_state(api, entity_id):
def get_states(api):
"""Query given API for all states."""
try:
- req = api(METHOD_GET,
+ req = api(METH_GET,
URL_API_STATES)
return [ha.State.from_dict(item) for
@@ -224,7 +218,7 @@ def remove_state(api, entity_id):
Return True if entity is gone (removed/never existed).
"""
try:
- req = api(METHOD_DELETE, URL_API_STATES_ENTITY.format(entity_id))
+ req = api(METH_DELETE, URL_API_STATES_ENTITY.format(entity_id))
if req.status_code in (200, 404):
return True
@@ -250,9 +244,7 @@ def set_state(api, entity_id, new_state, attributes=None, force_update=False):
'force_update': force_update}
try:
- req = api(METHOD_POST,
- URL_API_STATES_ENTITY.format(entity_id),
- data)
+ req = api(METH_POST, URL_API_STATES_ENTITY.format(entity_id), data)
if req.status_code not in (200, 201):
_LOGGER.error("Error changing state: %d - %s",
@@ -280,7 +272,7 @@ def get_services(api):
Each dict has a string "domain" and a list of strings "services".
"""
try:
- req = api(METHOD_GET, URL_API_SERVICES)
+ req = api(METH_GET, URL_API_SERVICES)
return req.json() if req.status_code == 200 else {}
@@ -294,7 +286,7 @@ def get_services(api):
def call_service(api, domain, service, service_data=None, timeout=5):
"""Call a service at the remote API."""
try:
- req = api(METHOD_POST,
+ req = api(METH_POST,
URL_API_SERVICES_SERVICE.format(domain, service),
service_data, timeout=timeout)
@@ -309,7 +301,7 @@ def call_service(api, domain, service, service_data=None, timeout=5):
def get_config(api):
"""Return configuration."""
try:
- req = api(METHOD_GET, URL_API_CONFIG)
+ req = api(METH_GET, URL_API_CONFIG)
if req.status_code != 200:
return {}
diff --git a/homeassistant/setup.py b/homeassistant/setup.py
index a7083d010e6..05a8ee1e2f1 100644
--- a/homeassistant/setup.py
+++ b/homeassistant/setup.py
@@ -1,6 +1,5 @@
"""All methods needed to bootstrap a Home Assistant instance."""
import asyncio
-import logging
import logging.handlers
import os
from timeit import default_timer as timer
@@ -9,13 +8,13 @@ from types import ModuleType
from typing import Optional, Dict
import homeassistant.config as conf_util
-from homeassistant.config import async_notify_setup_error
import homeassistant.core as core
import homeassistant.loader as loader
import homeassistant.util.package as pkg_util
-from homeassistant.util.async import run_coroutine_threadsafe
+from homeassistant.config import async_notify_setup_error
from homeassistant.const import (
EVENT_COMPONENT_LOADED, PLATFORM_FORMAT, CONSTRAINT_FILE)
+from homeassistant.util.async import run_coroutine_threadsafe
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py
index da97ed5662e..48d709bc549 100644
--- a/homeassistant/util/yaml.py
+++ b/homeassistant/util/yaml.py
@@ -78,7 +78,8 @@ def load_yaml(fname: str) -> Union[List, Dict]:
def dump(_dict: dict) -> str:
"""Dump YAML to a string and remove null."""
- return yaml.safe_dump(_dict, default_flow_style=False) \
+ return yaml.safe_dump(
+ _dict, default_flow_style=False, allow_unicode=True) \
.replace(': null\n', ':\n')
diff --git a/requirements_all.txt b/requirements_all.txt
index fb22149e54d..405bafaf13a 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -6,8 +6,8 @@ pip>=8.0.3
jinja2>=2.9.6
voluptuous==0.10.5
typing>=3,<4
-aiohttp==2.2.5
-yarl==0.13
+aiohttp==2.3.2
+yarl==0.14.0
async_timeout==2.0.0
chardet==3.0.4
astral==1.4
@@ -59,7 +59,7 @@ TwitterAPI==2.4.6
YesssSMS==0.1.1b3
# homeassistant.components.abode
-abodepy==0.12.1
+abodepy==0.12.2
# homeassistant.components.device_tracker.automatic
aioautomatic==0.6.4
@@ -96,7 +96,7 @@ anthemav==1.1.8
apcaccess==0.0.13
# homeassistant.components.notify.apns
-apns2==0.1.1
+apns2==0.3.0
# homeassistant.components.asterisk_mbox
asterisk_mbox==0.4.0
@@ -266,7 +266,7 @@ fixerio==0.1.1
flux_led==0.20
# homeassistant.components.notify.free_mobile
-freesms==0.1.1
+freesms==0.1.2
# homeassistant.components.device_tracker.fritz
# homeassistant.components.sensor.fritzbox_callmonitor
@@ -316,13 +316,13 @@ ha-philipsjs==0.0.1
haversine==0.4.5
# homeassistant.components.mqtt.server
-hbmqtt==0.8
+hbmqtt==0.9.1
# homeassistant.components.climate.heatmiser
heatmiserV3==0.9.1
# homeassistant.components.switch.hikvisioncam
-hikvision==1.2
+hikvision==0.4
# homeassistant.components.notify.hipchat
hipnotify==1.0.8
@@ -331,7 +331,7 @@ hipnotify==1.0.8
holidays==0.8.1
# homeassistant.components.frontend
-home-assistant-frontend==20171105.0
+home-assistant-frontend==20171118.0
# homeassistant.components.camera.onvif
http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a
@@ -559,7 +559,7 @@ proliphix==0.4.1
prometheus_client==0.0.19
# homeassistant.components.sensor.systemmonitor
-psutil==5.4.0
+psutil==5.4.1
# homeassistant.components.wink
pubnubsub-handler==1.0.2
@@ -591,7 +591,7 @@ pyHS100==0.3.0
pyRFXtrx==0.20.1
# homeassistant.components.sensor.tibber
-pyTibber==0.1.1
+pyTibber==0.2.1
# homeassistant.components.switch.dlink
pyW215==0.6.0
@@ -603,7 +603,7 @@ pyairvisual==1.0.0
pyalarmdotcom==0.3.0
# homeassistant.components.arlo
-pyarlo==0.0.7
+pyarlo==0.1.0
# homeassistant.components.notify.xmpp
pyasn1-modules==0.1.5
@@ -612,7 +612,7 @@ pyasn1-modules==0.1.5
pyasn1==0.3.7
# homeassistant.components.apple_tv
-pyatv==0.3.5
+pyatv==0.3.8
# homeassistant.components.device_tracker.bbox
# homeassistant.components.sensor.bbox
@@ -667,7 +667,7 @@ pyflexit==0.3
pyfttt==0.3
# homeassistant.components.remote.harmony
-pyharmony==1.0.16
+pyharmony==1.0.18
# homeassistant.components.binary_sensor.hikvision
pyhik==0.1.4
@@ -696,6 +696,9 @@ pykira==0.1.1
# homeassistant.components.sensor.kwb
pykwb==0.0.8
+# homeassistant.components.sensor.lacrosse
+pylacrosse==0.2.7
+
# homeassistant.components.sensor.lastfm
pylast==2.0.0
@@ -710,7 +713,7 @@ pylitejet==0.1
pyloopenergy==0.0.17
# homeassistant.components.lutron_caseta
-pylutron-caseta==0.2.8
+pylutron-caseta==0.3.0
# homeassistant.components.lutron
pylutron==0.1.0
@@ -728,7 +731,7 @@ pymodbus==1.3.1
pymonoprice==0.2
# homeassistant.components.media_player.yamaha_musiccast
-pymusiccast==0.1.3
+pymusiccast==0.1.5
# homeassistant.components.cover.myq
pymyq==0.0.8
@@ -786,7 +789,7 @@ pysma==0.1.3
# homeassistant.components.device_tracker.snmp
# homeassistant.components.sensor.snmp
# homeassistant.components.switch.snmp
-pysnmp==4.4.1
+pysnmp==4.4.2
# homeassistant.components.sensor.thinkingcleaner
# homeassistant.components.switch.thinkingcleaner
@@ -814,7 +817,7 @@ python-etherscan-api==0.0.1
python-forecastio==1.3.5
# homeassistant.components.gc100
-python-gc100==1.0.1a
+python-gc100==1.0.3a
# homeassistant.components.sensor.hp_ilo
python-hpilo==3.9
@@ -833,7 +836,7 @@ python-juicenet==0.0.5
# homeassistant.components.light.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
-python-miio==0.3.0
+python-miio==0.3.1
# homeassistant.components.media_player.mpd
python-mpd2==0.5.5
@@ -890,6 +893,9 @@ pythonegardia==1.0.22
# homeassistant.components.sensor.whois
pythonwhois==2.4.3
+# homeassistant.components.device_tracker.tile
+pytile==1.0.0
+
# homeassistant.components.device_tracker.trackr
pytrackr==0.0.5
@@ -912,7 +918,7 @@ pyvizio==0.0.2
pyvlx==0.1.3
# homeassistant.components.notify.html5
-pywebpush==1.1.0
+pywebpush==1.3.0
# homeassistant.components.wemo
pywemo==0.4.20
@@ -945,7 +951,7 @@ restrictedpython==4.0b2
rflink==0.0.34
# homeassistant.components.ring
-ring_doorbell==0.1.6
+ring_doorbell==0.1.7
# homeassistant.components.notify.rocketchat
rocketchat-API==0.6.1
@@ -991,7 +997,7 @@ sharp_aquos_rc==0.3.2
shodan==1.7.5
# homeassistant.components.notify.simplepush
-simplepush==1.1.3
+simplepush==1.1.4
# homeassistant.components.alarm_control_panel.simplisafe
simplisafe-python==1.0.5
@@ -1016,7 +1022,7 @@ sleepyq==0.6
# smbus-cffi==0.5.1
# homeassistant.components.media_player.snapcast
-snapcast==2.0.7
+snapcast==2.0.8
# homeassistant.components.climate.honeywell
somecomfort==0.4.1
@@ -1026,7 +1032,7 @@ speedtest-cli==1.0.7
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
-sqlalchemy==1.1.14
+sqlalchemy==1.1.15
# homeassistant.components.statsd
statsd==3.2.1
@@ -1072,7 +1078,7 @@ todoist-python==7.0.17
toonlib==1.0.2
# homeassistant.components.alarm_control_panel.totalconnect
-total_connect_client==0.12
+total_connect_client==0.13
# homeassistant.components.sensor.transmission
# homeassistant.components.switch.transmission
@@ -1091,7 +1097,7 @@ upsmychoice==1.0.6
uvcclient==0.10.1
# homeassistant.components.volvooncall
-volvooncall==0.3.3
+volvooncall==0.4.0
# homeassistant.components.verisure
vsure==1.3.7
@@ -1099,6 +1105,9 @@ vsure==1.3.7
# homeassistant.components.sensor.vasttrafik
vtjp==0.1.14
+# homeassistant.components.vultr
+vultr==0.1.2
+
# homeassistant.components.wake_on_lan
# homeassistant.components.media_player.panasonic_viera
# homeassistant.components.media_player.samsungtv
@@ -1126,7 +1135,7 @@ xbee-helper==0.0.7
xboxapi==0.1.1
# homeassistant.components.knx
-xknx==0.7.16
+xknx==0.7.18
# homeassistant.components.media_player.bluesound
# homeassistant.components.sensor.swiss_hydrological_data
@@ -1139,7 +1148,7 @@ yahoo-finance==1.4.0
# homeassistant.components.sensor.yweather
# homeassistant.components.weather.yweather
-yahooweather==0.8
+yahooweather==0.9
# homeassistant.components.light.yeelight
yeelight==0.3.3
@@ -1148,7 +1157,7 @@ yeelight==0.3.3
yeelightsunflower==0.0.8
# homeassistant.components.media_extractor
-youtube_dl==2017.10.29
+youtube_dl==2017.11.15
# homeassistant.components.light.zengge
zengge==0.2
diff --git a/requirements_test.txt b/requirements_test.txt
index 1aa909bc9bb..3edfa168f79 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -3,7 +3,7 @@
# new version
flake8==3.3
pylint==1.6.5
-mypy==0.540
+mypy==0.550
pydocstyle==1.1.1
coveralls>=1.1
pytest>=2.9.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index cb26016687d..c9ea20494d4 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -4,7 +4,7 @@
# new version
flake8==3.3
pylint==1.6.5
-mypy==0.540
+mypy==0.550
pydocstyle==1.1.1
coveralls>=1.1
pytest>=2.9.2
@@ -34,7 +34,7 @@ aioautomatic==0.6.4
aiohttp_cors==0.5.3
# homeassistant.components.notify.apns
-apns2==0.1.1
+apns2==0.3.0
# homeassistant.components.sensor.coinmarketcap
coinmarketcap==4.1.1
@@ -68,13 +68,13 @@ ha-ffmpeg==1.9
haversine==0.4.5
# homeassistant.components.mqtt.server
-hbmqtt==0.8
+hbmqtt==0.9.1
# homeassistant.components.binary_sensor.workday
holidays==0.8.1
# homeassistant.components.frontend
-home-assistant-frontend==20171105.0
+home-assistant-frontend==20171118.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@@ -134,7 +134,7 @@ pythonwhois==2.4.3
pyunifi==2.13
# homeassistant.components.notify.html5
-pywebpush==1.1.0
+pywebpush==1.3.0
# homeassistant.components.python_script
restrictedpython==4.0b2
@@ -143,7 +143,7 @@ restrictedpython==4.0b2
rflink==0.0.34
# homeassistant.components.ring
-ring_doorbell==0.1.6
+ring_doorbell==0.1.7
# homeassistant.components.media_player.yamaha
rxv==0.5.1
@@ -156,7 +156,7 @@ somecomfort==0.4.1
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
-sqlalchemy==1.1.14
+sqlalchemy==1.1.15
# homeassistant.components.statsd
statsd==3.2.1
@@ -164,6 +164,9 @@ statsd==3.2.1
# homeassistant.components.camera.uvc
uvcclient==0.10.1
+# homeassistant.components.vultr
+vultr==0.1.2
+
# homeassistant.components.wake_on_lan
# homeassistant.components.media_player.panasonic_viera
# homeassistant.components.media_player.samsungtv
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index d2ac40c2550..9d9725e9e6a 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -82,7 +82,8 @@ TEST_REQUIREMENTS = (
'warrant',
'yahoo-finance',
'pythonwhois',
- 'wakeonlan'
+ 'wakeonlan',
+ 'vultr'
)
IGNORE_PACKAGES = (
diff --git a/setup.py b/setup.py
index 3eb636d0801..f7a3e4ab8f3 100755
--- a/setup.py
+++ b/setup.py
@@ -2,15 +2,46 @@
"""Home Assistant setup script."""
import os
from setuptools import setup, find_packages
-from homeassistant.const import (__version__, PROJECT_PACKAGE_NAME,
- PROJECT_LICENSE, PROJECT_URL,
- PROJECT_EMAIL, PROJECT_DESCRIPTION,
- PROJECT_CLASSIFIERS, GITHUB_URL,
- PROJECT_AUTHOR)
+
+from homeassistant.const import __version__
+
+PROJECT_NAME = 'Home Assistant'
+PROJECT_PACKAGE_NAME = 'homeassistant'
+PROJECT_LICENSE = 'Apache License 2.0'
+PROJECT_AUTHOR = 'The Home Assistant Authors'
+PROJECT_COPYRIGHT = ' 2013-2017, {}'.format(PROJECT_AUTHOR)
+PROJECT_URL = 'https://home-assistant.io/'
+PROJECT_EMAIL = 'hello@home-assistant.io'
+PROJECT_DESCRIPTION = ('Open-source home automation platform '
+ 'running on Python 3.')
+PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source '
+ 'home automation platform running on Python 3. '
+ 'Track and control all devices at home and '
+ 'automate control. '
+ 'Installation in less than a minute.')
+PROJECT_CLASSIFIERS = [
+ 'Development Status :: 4 - Beta',
+ 'Intended Audience :: End Users/Desktop',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Apache Software License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Topic :: Home Automation'
+]
+
+PROJECT_GITHUB_USERNAME = 'home-assistant'
+PROJECT_GITHUB_REPOSITORY = 'home-assistant'
+
+PYPI_URL = 'https://pypi.python.org/pypi/{}'.format(PROJECT_PACKAGE_NAME)
+GITHUB_PATH = '{}/{}'.format(
+ PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY)
+GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
+
HERE = os.path.abspath(os.path.dirname(__file__))
-DOWNLOAD_URL = ('{}/archive/'
- '{}.zip'.format(GITHUB_URL, __version__))
+DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, __version__)
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
@@ -22,8 +53,8 @@ REQUIRES = [
'jinja2>=2.9.6',
'voluptuous==0.10.5',
'typing>=3,<4',
- 'aiohttp==2.2.5',
- 'yarl==0.13', # Update this whenever you update aiohttp
+ 'aiohttp==2.3.2', # If updated, check if yarl also needs an update!
+ 'yarl==0.14.0',
'async_timeout==2.0.0',
'chardet==3.0.4',
'astral==1.4',
diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py
index b5af01584d3..1b10b942281 100644
--- a/tests/components/alarm_control_panel/test_manual.py
+++ b/tests/components/alarm_control_panel/test_manual.py
@@ -72,10 +72,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_ARMED_HOME))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == STATE_ALARM_ARMED_HOME
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual.'
@@ -83,8 +81,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(STATE_ALARM_ARMED_HOME,
- self.hass.states.get(entity_id).state)
+ state = self.hass.states.get(entity_id)
+ assert state.state == STATE_ALARM_ARMED_HOME
def test_arm_home_with_invalid_code(self):
"""Attempt to arm home without a valid code."""
@@ -155,10 +153,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_ARMED_AWAY))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == STATE_ALARM_ARMED_AWAY
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual.'
@@ -166,8 +162,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(STATE_ALARM_ARMED_AWAY,
- self.hass.states.get(entity_id).state)
+ state = self.hass.states.get(entity_id)
+ assert state.state == STATE_ALARM_ARMED_AWAY
def test_arm_away_with_invalid_code(self):
"""Attempt to arm away without a valid code."""
@@ -238,10 +234,9 @@ class TestAlarmControlPanelManual(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_ARMED_NIGHT))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == \
+ STATE_ALARM_ARMED_NIGHT
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual.'
@@ -249,8 +244,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(STATE_ALARM_ARMED_NIGHT,
- self.hass.states.get(entity_id).state)
+ state = self.hass.states.get(entity_id)
+ assert state.state == STATE_ALARM_ARMED_NIGHT
def test_arm_night_with_invalid_code(self):
"""Attempt to night home without a valid code."""
@@ -329,10 +324,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_TRIGGERED))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(('homeassistant.components.alarm_control_panel.manual.'
@@ -340,8 +333,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(STATE_ALARM_TRIGGERED,
- self.hass.states.get(entity_id).state)
+ state = self.hass.states.get(entity_id)
+ assert state.state == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=5)
with patch(('homeassistant.components.alarm_control_panel.manual.'
@@ -349,8 +342,8 @@ class TestAlarmControlPanelManual(unittest.TestCase):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(STATE_ALARM_DISARMED,
- self.hass.states.get(entity_id).state)
+ state = self.hass.states.get(entity_id)
+ assert state.state == STATE_ALARM_DISARMED
def test_armed_home_with_specific_pending(self):
"""Test arm home method."""
diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py
index 5210c616f9c..e56b6865e6e 100644
--- a/tests/components/alarm_control_panel/test_manual_mqtt.py
+++ b/tests/components/alarm_control_panel/test_manual_mqtt.py
@@ -100,10 +100,8 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_ARMED_HOME))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == STATE_ALARM_ARMED_HOME
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
@@ -189,10 +187,8 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_ARMED_AWAY))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == STATE_ALARM_ARMED_AWAY
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
@@ -278,10 +274,9 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_ARMED_NIGHT))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == \
+ STATE_ALARM_ARMED_NIGHT
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
@@ -375,10 +370,8 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase):
self.assertEqual(STATE_ALARM_PENDING,
self.hass.states.get(entity_id).state)
- self.assertTrue(
- self.hass.states.is_state_attr(entity_id,
- 'post_pending_state',
- STATE_ALARM_TRIGGERED))
+ state = self.hass.states.get(entity_id)
+ assert state.attributes['post_pending_state'] == STATE_ALARM_TRIGGERED
future = dt_util.utcnow() + timedelta(seconds=2)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
diff --git a/tests/components/alarm_control_panel/test_spc.py b/tests/components/alarm_control_panel/test_spc.py
index 504b4e9237c..63b79781404 100644
--- a/tests/components/alarm_control_panel/test_spc.py
+++ b/tests/components/alarm_control_panel/test_spc.py
@@ -7,7 +7,7 @@ from homeassistant.components.spc import SpcRegistry
from homeassistant.components.alarm_control_panel import spc
from tests.common import async_test_home_assistant
from homeassistant.const import (
- STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED)
+ STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED)
@pytest.fixture
@@ -38,19 +38,19 @@ def test_setup_platform(hass):
'last_set_user_name': 'Pelle',
'last_unset_time': '1485800564',
'last_unset_user_id': '1',
- 'last_unset_user_name': 'Pelle',
+ 'last_unset_user_name': 'Lisa',
'last_alarm': '1478174896'
- }, {
+ }, {
'id': '3',
'name': 'Garage',
'mode': '0',
'last_set_time': '1483705803',
'last_set_user_id': '9998',
- 'last_set_user_name': 'Lisa',
+ 'last_set_user_name': 'Pelle',
'last_unset_time': '1483705808',
'last_unset_user_id': '9998',
'last_unset_user_name': 'Lisa'
- }]}
+ }]}
yield from spc.async_setup_platform(hass=hass,
config={},
@@ -58,7 +58,11 @@ def test_setup_platform(hass):
discovery_info=areas)
assert len(added_entities) == 2
+
assert added_entities[0].name == 'House'
assert added_entities[0].state == STATE_ALARM_ARMED_AWAY
+ assert added_entities[0].changed_by == 'Pelle'
+
assert added_entities[1].name == 'Garage'
assert added_entities[1].state == STATE_ALARM_DISARMED
+ assert added_entities[1].changed_by == 'Lisa'
diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py
index 565ebec64aa..a3587622b3d 100644
--- a/tests/components/alexa/test_intent.py
+++ b/tests/components/alexa/test_intent.py
@@ -13,6 +13,8 @@ from homeassistant.components.alexa import intent
SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000"
APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe"
REQUEST_ID = "amzn1.echo-api.request.0000000-0000-0000-0000-00000000000"
+AUTHORITY_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.ZODIAC"
+BUILTIN_AUTH_ID = "amzn1.er-authority.000000-d0ed-0000-ad00-000000d00ebe.TEST"
# pylint: disable=invalid-name
calls = []
@@ -90,7 +92,7 @@ def alexa_client(loop, hass, test_client):
"type": "plain",
"text": "LaunchRequest has been received.",
}
- }
+ },
}
}))
return loop.run_until_complete(test_client(hass.http.app))
@@ -207,6 +209,156 @@ def test_intent_request_with_slots(alexa_client):
assert text == "You told us your sign is virgo."
+@asyncio.coroutine
+def test_intent_request_with_slots_and_synonym_resolution(alexa_client):
+ """Test a request with slots and a name synonym."""
+ data = {
+ "version": "1.0",
+ "session": {
+ "new": False,
+ "sessionId": SESSION_ID,
+ "application": {
+ "applicationId": APPLICATION_ID
+ },
+ "attributes": {
+ "supportedHoroscopePeriods": {
+ "daily": True,
+ "weekly": False,
+ "monthly": False
+ }
+ },
+ "user": {
+ "userId": "amzn1.account.AM3B00000000000000000000000"
+ }
+ },
+ "request": {
+ "type": "IntentRequest",
+ "requestId": REQUEST_ID,
+ "timestamp": "2015-05-13T12:34:56Z",
+ "intent": {
+ "name": "GetZodiacHoroscopeIntent",
+ "slots": {
+ "ZodiacSign": {
+ "name": "ZodiacSign",
+ "value": "V zodiac",
+ "resolutions": {
+ "resolutionsPerAuthority": [
+ {
+ "authority": AUTHORITY_ID,
+ "status": {
+ "code": "ER_SUCCESS_MATCH"
+ },
+ "values": [
+ {
+ "value": {
+ "name": "Virgo"
+ }
+ }
+ ]
+ },
+ {
+ "authority": BUILTIN_AUTH_ID,
+ "status": {
+ "code": "ER_SUCCESS_NO_MATCH"
+ },
+ "values": [
+ {
+ "value": {
+ "name": "Test"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ req = yield from _intent_req(alexa_client, data)
+ assert req.status == 200
+ data = yield from req.json()
+ text = data.get("response", {}).get("outputSpeech",
+ {}).get("text")
+ assert text == "You told us your sign is Virgo."
+
+
+@asyncio.coroutine
+def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client):
+ """Test a request with slots and multiple name synonyms."""
+ data = {
+ "version": "1.0",
+ "session": {
+ "new": False,
+ "sessionId": SESSION_ID,
+ "application": {
+ "applicationId": APPLICATION_ID
+ },
+ "attributes": {
+ "supportedHoroscopePeriods": {
+ "daily": True,
+ "weekly": False,
+ "monthly": False
+ }
+ },
+ "user": {
+ "userId": "amzn1.account.AM3B00000000000000000000000"
+ }
+ },
+ "request": {
+ "type": "IntentRequest",
+ "requestId": REQUEST_ID,
+ "timestamp": "2015-05-13T12:34:56Z",
+ "intent": {
+ "name": "GetZodiacHoroscopeIntent",
+ "slots": {
+ "ZodiacSign": {
+ "name": "ZodiacSign",
+ "value": "V zodiac",
+ "resolutions": {
+ "resolutionsPerAuthority": [
+ {
+ "authority": AUTHORITY_ID,
+ "status": {
+ "code": "ER_SUCCESS_MATCH"
+ },
+ "values": [
+ {
+ "value": {
+ "name": "Virgo"
+ }
+ }
+ ]
+ },
+ {
+ "authority": BUILTIN_AUTH_ID,
+ "status": {
+ "code": "ER_SUCCESS_MATCH"
+ },
+ "values": [
+ {
+ "value": {
+ "name": "Test"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ req = yield from _intent_req(alexa_client, data)
+ assert req.status == 200
+ data = yield from req.json()
+ text = data.get("response", {}).get("outputSpeech",
+ {}).get("text")
+ assert text == "You told us your sign is V zodiac."
+
+
@asyncio.coroutine
def test_intent_request_with_slots_but_no_value(alexa_client):
"""Test a request with slots but no value."""
@@ -237,7 +389,7 @@ def test_intent_request_with_slots_but_no_value(alexa_client):
"name": "GetZodiacHoroscopeIntent",
"slots": {
"ZodiacSign": {
- "name": "ZodiacSign",
+ "name": "ZodiacSign"
}
}
}
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 4c79e95b324..55a412af1fd 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -5,9 +5,12 @@ from uuid import uuid4
import pytest
from homeassistant.components.alexa import smart_home
+from homeassistant.helpers import entityfilter
from tests.common import async_mock_service
+DEFAULT_CONFIG = smart_home.Config(filter=lambda entity_id: True)
+
def get_new_request(namespace, name, endpoint=None):
"""Generate a new API message."""
@@ -91,7 +94,7 @@ def test_wrong_version(hass):
msg['directive']['header']['payloadVersion'] = '2'
with pytest.raises(AssertionError):
- yield from smart_home.async_handle_message(hass, msg)
+ yield from smart_home.async_handle_message(hass, DEFAULT_CONFIG, msg)
@asyncio.coroutine
@@ -99,7 +102,7 @@ def test_discovery_request(hass):
"""Test alexa discovery request."""
request = get_new_request('Alexa.Discovery', 'Discover')
- # settup test devices
+ # setup test devices
hass.states.async_set(
'switch.test', 'on', {'friendly_name': "Test switch"})
@@ -114,12 +117,56 @@ def test_discovery_request(hass):
'friendly_name': "Test light 3", 'supported_features': 19
})
- msg = yield from smart_home.async_handle_message(hass, request)
+ hass.states.async_set(
+ 'script.test', 'off', {'friendly_name': "Test script"})
+
+ hass.states.async_set(
+ 'input_boolean.test', 'off', {'friendly_name': "Test input boolean"})
+
+ hass.states.async_set(
+ 'scene.test', 'off', {'friendly_name': "Test scene"})
+
+ hass.states.async_set(
+ 'fan.test_1', 'off', {'friendly_name': "Test fan 1"})
+
+ hass.states.async_set(
+ 'fan.test_2', 'off', {
+ 'friendly_name': "Test fan 2", 'supported_features': 1,
+ 'speed_list': ['low', 'medium', 'high']
+ })
+
+ hass.states.async_set(
+ 'lock.test', 'off', {'friendly_name': "Test lock"})
+
+ hass.states.async_set(
+ 'media_player.test', 'off', {
+ 'friendly_name': "Test media player",
+ 'supported_features': 20925,
+ 'volume_level': 1
+ })
+
+ hass.states.async_set(
+ 'alert.test', 'off', {'friendly_name': "Test alert"})
+
+ hass.states.async_set(
+ 'automation.test', 'off', {'friendly_name': "Test automation"})
+
+ hass.states.async_set(
+ 'group.test', 'off', {'friendly_name': "Test group"})
+
+ hass.states.async_set(
+ 'cover.test', 'off', {
+ 'friendly_name': "Test cover", 'supported_features': 255,
+ 'position': 85
+ })
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
- assert len(msg['payload']['endpoints']) == 4
+ assert len(msg['payload']['endpoints']) == 15
assert msg['header']['name'] == 'Discover.Response'
assert msg['header']['namespace'] == 'Alexa.Discovery'
@@ -170,9 +217,173 @@ def test_discovery_request(hass):
continue
+ if appliance['endpointId'] == 'script#test':
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test script"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.PowerController'
+ continue
+
+ if appliance['endpointId'] == 'input_boolean#test':
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test input boolean"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.PowerController'
+ continue
+
+ if appliance['endpointId'] == 'scene#test':
+ assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER"
+ assert appliance['friendlyName'] == "Test scene"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.SceneController'
+ continue
+
+ if appliance['endpointId'] == 'fan#test_1':
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test fan 1"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.PowerController'
+ continue
+
+ if appliance['endpointId'] == 'fan#test_2':
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test fan 2"
+ assert len(appliance['capabilities']) == 2
+
+ caps = set()
+ for feature in appliance['capabilities']:
+ caps.add(feature['interface'])
+
+ assert 'Alexa.PercentageController' in caps
+ assert 'Alexa.PowerController' in caps
+ continue
+
+ if appliance['endpointId'] == 'lock#test':
+ assert appliance['displayCategories'][0] == "SMARTLOCK"
+ assert appliance['friendlyName'] == "Test lock"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.LockController'
+ continue
+
+ if appliance['endpointId'] == 'media_player#test':
+ assert appliance['displayCategories'][0] == "TV"
+ assert appliance['friendlyName'] == "Test media player"
+ assert len(appliance['capabilities']) == 3
+ caps = set()
+ for feature in appliance['capabilities']:
+ caps.add(feature['interface'])
+
+ assert 'Alexa.PowerController' in caps
+ assert 'Alexa.Speaker' in caps
+ assert 'Alexa.PlaybackController' in caps
+ continue
+
+ if appliance['endpointId'] == 'alert#test':
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test alert"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.PowerController'
+ continue
+
+ if appliance['endpointId'] == 'automation#test':
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test automation"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.PowerController'
+ continue
+
+ if appliance['endpointId'] == 'group#test':
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test group"
+ assert len(appliance['capabilities']) == 1
+ assert appliance['capabilities'][-1]['interface'] == \
+ 'Alexa.PowerController'
+ continue
+
+ if appliance['endpointId'] == 'cover#test':
+ assert appliance['displayCategories'][0] == "DOOR"
+ assert appliance['friendlyName'] == "Test cover"
+ assert len(appliance['capabilities']) == 2
+
+ caps = set()
+ for feature in appliance['capabilities']:
+ caps.add(feature['interface'])
+
+ assert 'Alexa.PercentageController' in caps
+ assert 'Alexa.PowerController' in caps
+ continue
+
raise AssertionError("Unknown appliance!")
+@asyncio.coroutine
+def test_exclude_filters(hass):
+ """Test exclusion filters."""
+ request = get_new_request('Alexa.Discovery', 'Discover')
+
+ # setup test devices
+ hass.states.async_set(
+ 'switch.test', 'on', {'friendly_name': "Test switch"})
+
+ hass.states.async_set(
+ 'script.deny', 'off', {'friendly_name': "Blocked script"})
+
+ hass.states.async_set(
+ 'cover.deny', 'off', {'friendly_name': "Blocked cover"})
+
+ config = smart_home.Config(filter=entityfilter.generate_filter(
+ include_domains=[],
+ include_entities=[],
+ exclude_domains=['script'],
+ exclude_entities=['cover.deny'],
+ ))
+
+ msg = yield from smart_home.async_handle_message(hass, config, request)
+
+ msg = msg['event']
+
+ assert len(msg['payload']['endpoints']) == 1
+
+
+@asyncio.coroutine
+def test_include_filters(hass):
+ """Test inclusion filters."""
+ request = get_new_request('Alexa.Discovery', 'Discover')
+
+ # setup test devices
+ hass.states.async_set(
+ 'switch.deny', 'on', {'friendly_name': "Blocked switch"})
+
+ hass.states.async_set(
+ 'script.deny', 'off', {'friendly_name': "Blocked script"})
+
+ hass.states.async_set(
+ 'automation.allow', 'off', {'friendly_name': "Allowed automation"})
+
+ hass.states.async_set(
+ 'group.allow', 'off', {'friendly_name': "Allowed group"})
+
+ config = smart_home.Config(filter=entityfilter.generate_filter(
+ include_domains=['automation', 'group'],
+ include_entities=['script.deny'],
+ exclude_domains=[],
+ exclude_entities=[],
+ ))
+
+ msg = yield from smart_home.async_handle_message(hass, config, request)
+
+ msg = msg['event']
+
+ assert len(msg['payload']['endpoints']) == 3
+
+
@asyncio.coroutine
def test_api_entity_not_exists(hass):
"""Test api turn on process without entity."""
@@ -180,7 +391,8 @@ def test_api_entity_not_exists(hass):
call_switch = async_mock_service(hass, 'switch', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -195,7 +407,8 @@ def test_api_entity_not_exists(hass):
def test_api_function_not_implemented(hass):
"""Test api call that is not implemented to us."""
request = get_new_request('Alexa.HAHAAH', 'Sweet')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -206,21 +419,29 @@ def test_api_function_not_implemented(hass):
@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['light', 'switch'])
+@pytest.mark.parametrize("domain", ['alert', 'automation', 'group',
+ 'input_boolean', 'light', 'script',
+ 'switch'])
def test_api_turn_on(hass, domain):
"""Test api turn on process."""
request = get_new_request(
'Alexa.PowerController', 'TurnOn', '{}#test'.format(domain))
- # settup test devices
+ # setup test devices
hass.states.async_set(
'{}.test'.format(domain), 'off', {
'friendly_name': "Test {}".format(domain)
})
- call = async_mock_service(hass, domain, 'turn_on')
+ call_domain = domain
- msg = yield from smart_home.async_handle_message(hass, request)
+ if domain == 'group':
+ call_domain = 'homeassistant'
+
+ call = async_mock_service(hass, call_domain, 'turn_on')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -231,21 +452,29 @@ def test_api_turn_on(hass, domain):
@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['light', 'switch'])
+@pytest.mark.parametrize("domain", ['alert', 'automation', 'group',
+ 'input_boolean', 'light', 'script',
+ 'switch'])
def test_api_turn_off(hass, domain):
"""Test api turn on process."""
request = get_new_request(
'Alexa.PowerController', 'TurnOff', '{}#test'.format(domain))
- # settup test devices
+ # setup test devices
hass.states.async_set(
'{}.test'.format(domain), 'on', {
'friendly_name': "Test {}".format(domain)
})
- call = async_mock_service(hass, domain, 'turn_off')
+ call_domain = domain
- msg = yield from smart_home.async_handle_message(hass, request)
+ if domain == 'group':
+ call_domain = 'homeassistant'
+
+ call = async_mock_service(hass, call_domain, 'turn_off')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -264,13 +493,14 @@ def test_api_set_brightness(hass):
# add payload
request['directive']['payload']['brightness'] = '50'
- # settup test devices
+ # setup test devices
hass.states.async_set(
'light.test', 'off', {'friendly_name': "Test light"})
call_light = async_mock_service(hass, 'light', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -292,7 +522,7 @@ def test_api_adjust_brightness(hass, result, adjust):
# add payload
request['directive']['payload']['brightnessDelta'] = adjust
- # settup test devices
+ # setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'brightness': '77'
@@ -300,7 +530,8 @@ def test_api_adjust_brightness(hass, result, adjust):
call_light = async_mock_service(hass, 'light', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -324,7 +555,7 @@ def test_api_set_color_rgb(hass):
'brightness': '0.342',
}
- # settup test devices
+ # setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light",
@@ -333,7 +564,8 @@ def test_api_set_color_rgb(hass):
call_light = async_mock_service(hass, 'light', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -357,7 +589,7 @@ def test_api_set_color_xy(hass):
'brightness': '0.342',
}
- # settup test devices
+ # setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light",
@@ -366,7 +598,8 @@ def test_api_set_color_xy(hass):
call_light = async_mock_service(hass, 'light', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -388,13 +621,14 @@ def test_api_set_color_temperature(hass):
# add payload
request['directive']['payload']['colorTemperatureInKelvin'] = '7500'
- # settup test devices
+ # setup test devices
hass.states.async_set(
'light.test', 'off', {'friendly_name': "Test light"})
call_light = async_mock_service(hass, 'light', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -413,7 +647,7 @@ def test_api_decrease_color_temp(hass, result, initial):
'Alexa.ColorTemperatureController', 'DecreaseColorTemperature',
'light#test')
- # settup test devices
+ # setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'color_temp': initial,
@@ -422,7 +656,8 @@ def test_api_decrease_color_temp(hass, result, initial):
call_light = async_mock_service(hass, 'light', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -441,7 +676,7 @@ def test_api_increase_color_temp(hass, result, initial):
'Alexa.ColorTemperatureController', 'IncreaseColorTemperature',
'light#test')
- # settup test devices
+ # setup test devices
hass.states.async_set(
'light.test', 'off', {
'friendly_name': "Test light", 'color_temp': initial,
@@ -450,7 +685,8 @@ def test_api_increase_color_temp(hass, result, initial):
call_light = async_mock_service(hass, 'light', 'turn_on')
- msg = yield from smart_home.async_handle_message(hass, request)
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
assert 'event' in msg
msg = msg['event']
@@ -459,3 +695,392 @@ def test_api_increase_color_temp(hass, result, initial):
assert call_light[0].data['entity_id'] == 'light.test'
assert call_light[0].data['color_temp'] == result
assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['scene'])
+def test_api_activate(hass, domain):
+ """Test api activate process."""
+ request = get_new_request(
+ 'Alexa.SceneController', 'Activate', '{}#test'.format(domain))
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'turn_on')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+def test_api_set_percentage_fan(hass):
+ """Test api set percentage for fan process."""
+ request = get_new_request(
+ 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2')
+
+ # add payload
+ request['directive']['payload']['percentage'] = '50'
+
+ # setup test devices
+ hass.states.async_set(
+ 'fan.test_2', 'off', {'friendly_name': "Test fan"})
+
+ call_fan = async_mock_service(hass, 'fan', 'set_speed')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call_fan) == 1
+ assert call_fan[0].data['entity_id'] == 'fan.test_2'
+ assert call_fan[0].data['speed'] == 'medium'
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+def test_api_set_percentage_cover(hass):
+ """Test api set percentage for cover process."""
+ request = get_new_request(
+ 'Alexa.PercentageController', 'SetPercentage', 'cover#test')
+
+ # add payload
+ request['directive']['payload']['percentage'] = '50'
+
+ # setup test devices
+ hass.states.async_set(
+ 'cover.test', 'closed', {
+ 'friendly_name': "Test cover"
+ })
+
+ call_cover = async_mock_service(hass, 'cover', 'set_cover_position')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call_cover) == 1
+ assert call_cover[0].data['entity_id'] == 'cover.test'
+ assert call_cover[0].data['position'] == 50
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize(
+ "result,adjust", [('high', '-5'), ('off', '5'), ('low', '-80')])
+def test_api_adjust_percentage_fan(hass, result, adjust):
+ """Test api adjust percentage for fan process."""
+ request = get_new_request(
+ 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2')
+
+ # add payload
+ request['directive']['payload']['percentageDelta'] = adjust
+
+ # setup test devices
+ hass.states.async_set(
+ 'fan.test_2', 'on', {
+ 'friendly_name': "Test fan 2", 'speed': 'high'
+ })
+
+ call_fan = async_mock_service(hass, 'fan', 'set_speed')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call_fan) == 1
+ assert call_fan[0].data['entity_id'] == 'fan.test_2'
+ assert call_fan[0].data['speed'] == result
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize(
+ "result,adjust", [(25, '-5'), (35, '5'), (0, '-80')])
+def test_api_adjust_percentage_cover(hass, result, adjust):
+ """Test api adjust percentage for cover process."""
+ request = get_new_request(
+ 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test')
+
+ # add payload
+ request['directive']['payload']['percentageDelta'] = adjust
+
+ # setup test devices
+ hass.states.async_set(
+ 'cover.test', 'closed', {
+ 'friendly_name': "Test cover",
+ 'position': 30
+ })
+
+ call_cover = async_mock_service(hass, 'cover', 'set_cover_position')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call_cover) == 1
+ assert call_cover[0].data['entity_id'] == 'cover.test'
+ assert call_cover[0].data['position'] == result
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['lock'])
+def test_api_lock(hass, domain):
+ """Test api lock process."""
+ request = get_new_request(
+ 'Alexa.LockController', 'Lock', '{}#test'.format(domain))
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'lock')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['media_player'])
+def test_api_play(hass, domain):
+ """Test api play process."""
+ request = get_new_request(
+ 'Alexa.PlaybackController', 'Play', '{}#test'.format(domain))
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'media_play')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['media_player'])
+def test_api_pause(hass, domain):
+ """Test api pause process."""
+ request = get_new_request(
+ 'Alexa.PlaybackController', 'Pause', '{}#test'.format(domain))
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'media_pause')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['media_player'])
+def test_api_stop(hass, domain):
+ """Test api stop process."""
+ request = get_new_request(
+ 'Alexa.PlaybackController', 'Stop', '{}#test'.format(domain))
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'media_stop')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['media_player'])
+def test_api_next(hass, domain):
+ """Test api next process."""
+ request = get_new_request(
+ 'Alexa.PlaybackController', 'Next', '{}#test'.format(domain))
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'media_next_track')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['media_player'])
+def test_api_previous(hass, domain):
+ """Test api previous process."""
+ request = get_new_request(
+ 'Alexa.PlaybackController', 'Previous', '{}#test'.format(domain))
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'media_previous_track')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+def test_api_set_volume(hass):
+ """Test api set volume process."""
+ request = get_new_request(
+ 'Alexa.Speaker', 'SetVolume', 'media_player#test')
+
+ # add payload
+ request['directive']['payload']['volume'] = 50
+
+ # setup test devices
+ hass.states.async_set(
+ 'media_player.test', 'off', {
+ 'friendly_name': "Test media player", 'volume_level': 0
+ })
+
+ call_media_player = async_mock_service(hass, 'media_player', 'volume_set')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call_media_player) == 1
+ assert call_media_player[0].data['entity_id'] == 'media_player.test'
+ assert call_media_player[0].data['volume_level'] == 0.5
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize(
+ "result,adjust", [(0.7, '-5'), (0.8, '5'), (0, '-80')])
+def test_api_adjust_volume(hass, result, adjust):
+ """Test api adjust volume process."""
+ request = get_new_request(
+ 'Alexa.Speaker', 'AdjustVolume', 'media_player#test')
+
+ # add payload
+ request['directive']['payload']['volume'] = adjust
+
+ # setup test devices
+ hass.states.async_set(
+ 'media_player.test', 'off', {
+ 'friendly_name': "Test media player", 'volume_level': 0.75
+ })
+
+ call_media_player = async_mock_service(hass, 'media_player', 'volume_set')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call_media_player) == 1
+ assert call_media_player[0].data['entity_id'] == 'media_player.test'
+ assert call_media_player[0].data['volume_level'] == result
+ assert msg['header']['name'] == 'Response'
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize("domain", ['media_player'])
+def test_api_mute(hass, domain):
+ """Test api mute process."""
+ request = get_new_request(
+ 'Alexa.Speaker', 'SetMute', '{}#test'.format(domain))
+
+ request['directive']['payload']['mute'] = True
+
+ # setup test devices
+ hass.states.async_set(
+ '{}.test'.format(domain), 'off', {
+ 'friendly_name': "Test {}".format(domain)
+ })
+
+ call = async_mock_service(hass, domain, 'volume_mute')
+
+ msg = yield from smart_home.async_handle_message(
+ hass, DEFAULT_CONFIG, request)
+
+ assert 'event' in msg
+ msg = msg['event']
+
+ assert len(call) == 1
+ assert call[0].data['entity_id'] == '{}.test'.format(domain)
+ assert msg['header']['name'] == 'Response'
diff --git a/tests/components/binary_sensor/test_spc.py b/tests/components/binary_sensor/test_spc.py
index 5004ccd3210..d2299874527 100644
--- a/tests/components/binary_sensor/test_spc.py
+++ b/tests/components/binary_sensor/test_spc.py
@@ -30,7 +30,7 @@ def test_setup_platform(hass):
'area_name': 'House',
'input': '0',
'status': '0',
- }, {
+ }, {
'id': '3',
'type': '0',
'zone_name': 'Hallway PIR',
@@ -38,7 +38,7 @@ def test_setup_platform(hass):
'area_name': 'House',
'input': '0',
'status': '0',
- }, {
+ }, {
'id': '5',
'type': '1',
'zone_name': 'Front door',
@@ -46,7 +46,7 @@ def test_setup_platform(hass):
'area_name': 'House',
'input': '1',
'status': '0',
- }]}
+ }]}
def add_entities(entities):
nonlocal added_entities
diff --git a/tests/components/binary_sensor/test_vultr.py b/tests/components/binary_sensor/test_vultr.py
new file mode 100644
index 00000000000..2bcb220233b
--- /dev/null
+++ b/tests/components/binary_sensor/test_vultr.py
@@ -0,0 +1,165 @@
+"""Test the Vultr binary sensor platform."""
+import unittest
+import requests_mock
+import pytest
+import voluptuous as vol
+
+from homeassistant.components.binary_sensor import vultr
+from homeassistant.components import vultr as base_vultr
+from homeassistant.components.vultr import (
+ ATTR_ALLOWED_BANDWIDTH, ATTR_AUTO_BACKUPS, ATTR_IPV4_ADDRESS,
+ ATTR_COST_PER_MONTH, ATTR_CREATED_AT, ATTR_SUBSCRIPTION_ID,
+ CONF_SUBSCRIPTION)
+from homeassistant.const import (
+ CONF_PLATFORM, CONF_NAME)
+
+from tests.components.test_vultr import VALID_CONFIG
+from tests.common import (
+ get_test_home_assistant, load_fixture)
+
+
+class TestVultrBinarySensorSetup(unittest.TestCase):
+ """Test the Vultr binary sensor platform."""
+
+ DEVICES = []
+
+ def add_devices(self, devices, action):
+ """Mock add devices."""
+ for device in devices:
+ self.DEVICES.append(device)
+
+ def setUp(self):
+ """Init values for this testcase class."""
+ self.hass = get_test_home_assistant()
+ self.configs = [
+ {
+ CONF_SUBSCRIPTION: '576965',
+ CONF_NAME: "A Server"
+ },
+ {
+ CONF_SUBSCRIPTION: '123456',
+ CONF_NAME: "Failed Server"
+ },
+ {
+ CONF_SUBSCRIPTION: '555555',
+ CONF_NAME: vultr.DEFAULT_NAME
+ }
+ ]
+
+ def tearDown(self):
+ """Stop our started services."""
+ self.hass.stop()
+
+ def test_failed_hub(self):
+ """Test a hub setup failure."""
+ base_vultr.setup(self.hass, VALID_CONFIG)
+
+ @requests_mock.Mocker()
+ def test_binary_sensor(self, mock):
+ """Test successful instance."""
+ mock.get(
+ 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_account_info.json'))
+
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ # Setup hub
+ base_vultr.setup(self.hass, VALID_CONFIG)
+
+ # Setup each of our test configs
+ for config in self.configs:
+ vultr.setup_platform(self.hass,
+ config,
+ self.add_devices,
+ None)
+
+ self.assertEqual(len(self.DEVICES), 3)
+
+ for device in self.DEVICES:
+
+ # Test pre data retieval
+ if device.subscription == '555555':
+ self.assertEqual('Vultr {}', device.name)
+
+ device.update()
+ device_attrs = device.device_state_attributes
+
+ if device.subscription == '555555':
+ self.assertEqual('Vultr Another Server', device.name)
+
+ if device.name == 'A Server':
+ self.assertEqual(True, device.is_on)
+ self.assertEqual('power', device.device_class)
+ self.assertEqual('on', device.state)
+ self.assertEqual('mdi:server', device.icon)
+ self.assertEqual('1000',
+ device_attrs[ATTR_ALLOWED_BANDWIDTH])
+ self.assertEqual('yes',
+ device_attrs[ATTR_AUTO_BACKUPS])
+ self.assertEqual('123.123.123.123',
+ device_attrs[ATTR_IPV4_ADDRESS])
+ self.assertEqual('10.05',
+ device_attrs[ATTR_COST_PER_MONTH])
+ self.assertEqual('2013-12-19 14:45:41',
+ device_attrs[ATTR_CREATED_AT])
+ self.assertEqual('576965',
+ device_attrs[ATTR_SUBSCRIPTION_ID])
+ elif device.name == 'Failed Server':
+ self.assertEqual(False, device.is_on)
+ self.assertEqual('off', device.state)
+ self.assertEqual('mdi:server-off', device.icon)
+ self.assertEqual('1000',
+ device_attrs[ATTR_ALLOWED_BANDWIDTH])
+ self.assertEqual('no',
+ device_attrs[ATTR_AUTO_BACKUPS])
+ self.assertEqual('192.168.100.50',
+ device_attrs[ATTR_IPV4_ADDRESS])
+ self.assertEqual('73.25',
+ device_attrs[ATTR_COST_PER_MONTH])
+ self.assertEqual('2014-10-13 14:45:41',
+ device_attrs[ATTR_CREATED_AT])
+ self.assertEqual('123456',
+ device_attrs[ATTR_SUBSCRIPTION_ID])
+
+ def test_invalid_sensor_config(self):
+ """Test config type failures."""
+ with pytest.raises(vol.Invalid): # No subs
+ vultr.PLATFORM_SCHEMA({
+ CONF_PLATFORM: base_vultr.DOMAIN,
+ })
+
+ @requests_mock.Mocker()
+ def test_invalid_sensors(self, mock):
+ """Test the VultrBinarySensor fails."""
+ mock.get(
+ 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_account_info.json'))
+
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ base_vultr.setup(self.hass, VALID_CONFIG)
+
+ bad_conf = {} # No subscription
+
+ no_subs_setup = vultr.setup_platform(self.hass,
+ bad_conf,
+ self.add_devices,
+ None)
+
+ self.assertFalse(no_subs_setup)
+
+ bad_conf = {
+ CONF_NAME: "Missing Server",
+ CONF_SUBSCRIPTION: '555555'
+ } # Sub not associated with API key (not in server_list)
+
+ wrong_subs_setup = vultr.setup_platform(self.hass,
+ bad_conf,
+ self.add_devices,
+ None)
+
+ self.assertFalse(wrong_subs_setup)
diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py
index 74b2186b8d7..bb42ef177f0 100644
--- a/tests/components/climate/test_generic_thermostat.py
+++ b/tests/components/climate/test_generic_thermostat.py
@@ -1,9 +1,9 @@
"""The tests for the generic_thermostat."""
import asyncio
import datetime
-import pytz
import unittest
from unittest import mock
+import pytz
import homeassistant.core as ha
from homeassistant.core import callback
@@ -54,13 +54,16 @@ class TestSetupClimateGenericThermostat(unittest.TestCase):
'climate': config})
def test_valid_conf(self):
- """Test set up genreic_thermostat with valid config values."""
- self.assertTrue(setup_component(self.hass, 'climate',
- {'climate': {
- 'platform': 'generic_thermostat',
- 'name': 'test',
- 'heater': ENT_SWITCH,
- 'target_sensor': ENT_SENSOR}}))
+ """Test set up generic_thermostat with valid config values."""
+ self.assertTrue(
+ setup_component(self.hass, 'climate',
+ {'climate': {
+ 'platform': 'generic_thermostat',
+ 'name': 'test',
+ 'heater': ENT_SWITCH,
+ 'target_sensor': ENT_SENSOR
+ }})
+ )
def test_setup_with_sensor(self):
"""Test set up heat_control with sensor to trigger update at init."""
@@ -243,6 +246,31 @@ class TestClimateGenericThermostat(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))
+ @mock.patch('logging.Logger.error')
+ def test_invalid_operating_mode(self, log_mock):
+ """Test error handling for invalid operation mode."""
+ climate.set_operation_mode(self.hass, 'invalid mode')
+ self.hass.block_till_done()
+ self.assertEqual(log_mock.call_count, 1)
+
+ def test_operating_mode_auto(self):
+ """Test change mode from OFF to AUTO.
+
+ Switch turns on when temp below setpoint and mode changes.
+ """
+ climate.set_operation_mode(self.hass, STATE_OFF)
+ climate.set_temperature(self.hass, 30)
+ self._setup_sensor(25)
+ self.hass.block_till_done()
+ self._setup_switch(False)
+ climate.set_operation_mode(self.hass, climate.STATE_AUTO)
+ self.hass.block_till_done()
+ self.assertEqual(1, len(self.calls))
+ call = self.calls[0]
+ self.assertEqual('switch', call.domain)
+ self.assertEqual(SERVICE_TURN_ON, call.service)
+ self.assertEqual(ENT_SWITCH, call.data['entity_id'])
+
def _setup_sensor(self, temp, unit=TEMP_CELSIUS):
"""Setup the test sensor."""
self.hass.states.set(ENT_SENSOR, temp, {
diff --git a/tests/components/cloud/test_auth_api.py b/tests/components/cloud/test_auth_api.py
index d9f005fdcfa..20f9265a1c1 100644
--- a/tests/components/cloud/test_auth_api.py
+++ b/tests/components/cloud/test_auth_api.py
@@ -69,7 +69,6 @@ def test_login(mock_cognito):
auth_api.login(cloud, 'user', 'pass')
assert len(mock_cognito.authenticate.mock_calls) == 1
- assert cloud.email == 'user'
assert cloud.id_token == 'test_id_token'
assert cloud.access_token == 'test_access_token'
assert cloud.refresh_token == 'test_refresh_token'
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 1090acb01e9..296baa3f143 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -3,9 +3,10 @@ import asyncio
from unittest.mock import patch, MagicMock
import pytest
+from jose import jwt
from homeassistant.bootstrap import async_setup_component
-from homeassistant.components.cloud import DOMAIN, auth_api
+from homeassistant.components.cloud import DOMAIN, auth_api, iot
from tests.common import mock_coro
@@ -23,7 +24,8 @@ def cloud_client(hass, test_client):
'relayer': 'relayer',
}
}))
- return hass.loop.run_until_complete(test_client(hass.http.app))
+ with patch('homeassistant.components.cloud.Cloud.write_user_info'):
+ yield hass.loop.run_until_complete(test_client(hass.http.app))
@pytest.fixture
@@ -43,21 +45,35 @@ def test_account_view_no_account(cloud_client):
@asyncio.coroutine
def test_account_view(hass, cloud_client):
"""Test fetching account if no account available."""
- hass.data[DOMAIN].email = 'hello@home-assistant.io'
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': '2018-01-03'
+ }, 'test')
+ hass.data[DOMAIN].iot.state = iot.STATE_CONNECTED
req = yield from cloud_client.get('/api/cloud/account')
assert req.status == 200
result = yield from req.json()
- assert result == {'email': 'hello@home-assistant.io'}
+ assert result == {
+ 'email': 'hello@home-assistant.io',
+ 'sub_exp': '2018-01-03',
+ 'cloud': iot.STATE_CONNECTED,
+ }
@asyncio.coroutine
-def test_login_view(hass, cloud_client):
+def test_login_view(hass, cloud_client, mock_cognito):
"""Test logging in."""
- hass.data[DOMAIN].email = 'hello@home-assistant.io'
+ mock_cognito.id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': '2018-01-03'
+ }, 'test')
+ mock_cognito.access_token = 'access_token'
+ mock_cognito.refresh_token = 'refresh_token'
- with patch('homeassistant.components.cloud.iot.CloudIoT.connect'), \
- patch('homeassistant.components.cloud.'
- 'auth_api.login') as mock_login:
+ with patch('homeassistant.components.cloud.iot.CloudIoT.'
+ 'connect') as mock_connect, \
+ patch('homeassistant.components.cloud.auth_api._authenticate',
+ return_value=mock_cognito) as mock_auth:
req = yield from cloud_client.post('/api/cloud/login', json={
'email': 'my_username',
'password': 'my_password'
@@ -65,9 +81,13 @@ def test_login_view(hass, cloud_client):
assert req.status == 200
result = yield from req.json()
- assert result == {'email': 'hello@home-assistant.io'}
- assert len(mock_login.mock_calls) == 1
- cloud, result_user, result_pass = mock_login.mock_calls[0][1]
+ assert result['email'] == 'hello@home-assistant.io'
+ assert result['sub_exp'] == '2018-01-03'
+
+ assert len(mock_connect.mock_calls) == 1
+
+ assert len(mock_auth.mock_calls) == 1
+ cloud, result_user, result_pass = mock_auth.mock_calls[0][1]
assert result_user == 'my_username'
assert result_pass == 'my_password'
diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py
index 1eb1051520f..c05fdabf465 100644
--- a/tests/components/cloud/test_init.py
+++ b/tests/components/cloud/test_init.py
@@ -3,9 +3,11 @@ import asyncio
import json
from unittest.mock import patch, MagicMock, mock_open
+from jose import jwt
import pytest
from homeassistant.components import cloud
+from homeassistant.util.dt import utcnow
from tests.common import mock_coro
@@ -72,7 +74,6 @@ def test_initialize_loads_info(mock_os, hass):
"""Test initialize will load info from config file."""
mock_os.path.isfile.return_value = True
mopen = mock_open(read_data=json.dumps({
- 'email': 'test-email',
'id_token': 'test-id-token',
'access_token': 'test-access-token',
'refresh_token': 'test-refresh-token',
@@ -85,7 +86,6 @@ def test_initialize_loads_info(mock_os, hass):
with patch('homeassistant.components.cloud.open', mopen, create=True):
yield from cl.initialize()
- assert cl.email == 'test-email'
assert cl.id_token == 'test-id-token'
assert cl.access_token == 'test-access-token'
assert cl.refresh_token == 'test-refresh-token'
@@ -102,7 +102,6 @@ def test_logout_clears_info(mock_os, hass):
yield from cl.logout()
assert len(cl.iot.disconnect.mock_calls) == 1
- assert cl.email is None
assert cl.id_token is None
assert cl.access_token is None
assert cl.refresh_token is None
@@ -115,7 +114,6 @@ def test_write_user_info():
mopen = mock_open()
cl = cloud.Cloud(MagicMock(), cloud.MODE_DEV)
- cl.email = 'test-email'
cl.id_token = 'test-id-token'
cl.access_token = 'test-access-token'
cl.refresh_token = 'test-refresh-token'
@@ -129,7 +127,41 @@ def test_write_user_info():
data = json.loads(handle.write.mock_calls[0][1][0])
assert data == {
'access_token': 'test-access-token',
- 'email': 'test-email',
'id_token': 'test-id-token',
'refresh_token': 'test-refresh-token',
}
+
+
+@asyncio.coroutine
+def test_subscription_not_expired_without_sub_in_claim():
+ """Test that we do not enforce subscriptions yet."""
+ cl = cloud.Cloud(None, cloud.MODE_DEV)
+ cl.id_token = jwt.encode({}, 'test')
+
+ assert not cl.subscription_expired
+
+
+@asyncio.coroutine
+def test_subscription_expired():
+ """Test subscription being expired."""
+ cl = cloud.Cloud(None, cloud.MODE_DEV)
+ cl.id_token = jwt.encode({
+ 'custom:sub-exp': '2017-11-13'
+ }, 'test')
+
+ with patch('homeassistant.util.dt.utcnow',
+ return_value=utcnow().replace(year=2018)):
+ assert cl.subscription_expired
+
+
+@asyncio.coroutine
+def test_subscription_not_expired():
+ """Test subscription not being expired."""
+ cl = cloud.Cloud(None, cloud.MODE_DEV)
+ cl.id_token = jwt.encode({
+ 'custom:sub-exp': '2017-11-13'
+ }, 'test')
+
+ with patch('homeassistant.util.dt.utcnow',
+ return_value=utcnow().replace(year=2017, month=11, day=9)):
+ assert not cl.subscription_expired
diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py
index f1254cdb3c7..be5a93c9e47 100644
--- a/tests/components/cloud/test_iot.py
+++ b/tests/components/cloud/test_iot.py
@@ -30,11 +30,16 @@ def mock_handle_message():
yield mock
+@pytest.fixture
+def mock_cloud():
+ """Mock cloud class."""
+ return MagicMock(subscription_expired=False)
+
+
@asyncio.coroutine
-def test_cloud_calling_handler(mock_client, mock_handle_message):
+def test_cloud_calling_handler(mock_client, mock_handle_message, mock_cloud):
"""Test we call handle message with correct info."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.text,
json=MagicMock(return_value={
@@ -53,8 +58,8 @@ def test_cloud_calling_handler(mock_client, mock_handle_message):
p_hass, p_cloud, handler_name, payload = \
mock_handle_message.mock_calls[0][1]
- assert p_hass is cloud.hass
- assert p_cloud is cloud
+ assert p_hass is mock_cloud.hass
+ assert p_cloud is mock_cloud
assert handler_name == 'test-handler'
assert payload == 'test-payload'
@@ -67,10 +72,9 @@ def test_cloud_calling_handler(mock_client, mock_handle_message):
@asyncio.coroutine
-def test_connection_msg_for_unknown_handler(mock_client):
+def test_connection_msg_for_unknown_handler(mock_client, mock_cloud):
"""Test a msg for an unknown handler."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.text,
json=MagicMock(return_value={
@@ -92,10 +96,10 @@ def test_connection_msg_for_unknown_handler(mock_client):
@asyncio.coroutine
-def test_connection_msg_for_handler_raising(mock_client, mock_handle_message):
+def test_connection_msg_for_handler_raising(mock_client, mock_handle_message,
+ mock_cloud):
"""Test we sent error when handler raises exception."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.text,
json=MagicMock(return_value={
@@ -136,37 +140,34 @@ def test_handler_forwarding():
@asyncio.coroutine
-def test_handling_core_messages(hass):
+def test_handling_core_messages(hass, mock_cloud):
"""Test handling core messages."""
- cloud = MagicMock()
- cloud.logout.return_value = mock_coro()
- yield from iot.async_handle_cloud(hass, cloud, {
+ mock_cloud.logout.return_value = mock_coro()
+ yield from iot.async_handle_cloud(hass, mock_cloud, {
'action': 'logout',
'reason': 'Logged in at two places.'
})
- assert len(cloud.logout.mock_calls) == 1
+ assert len(mock_cloud.logout.mock_calls) == 1
@asyncio.coroutine
-def test_cloud_getting_disconnected_by_server(mock_client, caplog):
+def test_cloud_getting_disconnected_by_server(mock_client, caplog, mock_cloud):
"""Test server disconnecting instance."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.CLOSING,
))
yield from conn.connect()
- assert 'Connection closed: Closed by server' in caplog.text
- assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
+ assert 'Connection closed: Connection cancelled.' in caplog.text
+ assert 'connect' in str(mock_cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
-def test_cloud_receiving_bytes(mock_client, caplog):
+def test_cloud_receiving_bytes(mock_client, caplog, mock_cloud):
"""Test server disconnecting instance."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.BINARY,
))
@@ -174,14 +175,13 @@ def test_cloud_receiving_bytes(mock_client, caplog):
yield from conn.connect()
assert 'Connection closed: Received non-Text message' in caplog.text
- assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
+ assert 'connect' in str(mock_cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
-def test_cloud_sending_invalid_json(mock_client, caplog):
+def test_cloud_sending_invalid_json(mock_client, caplog, mock_cloud):
"""Test cloud sending invalid JSON."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.return_value = mock_coro(MagicMock(
type=WSMsgType.TEXT,
json=MagicMock(side_effect=ValueError)
@@ -190,27 +190,25 @@ def test_cloud_sending_invalid_json(mock_client, caplog):
yield from conn.connect()
assert 'Connection closed: Received invalid JSON.' in caplog.text
- assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
+ assert 'connect' in str(mock_cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
-def test_cloud_check_token_raising(mock_client, caplog):
+def test_cloud_check_token_raising(mock_client, caplog, mock_cloud):
"""Test cloud sending invalid JSON."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.side_effect = auth_api.CloudError
yield from conn.connect()
assert 'Unable to connect: Unable to refresh token.' in caplog.text
- assert 'connect' in str(cloud.hass.async_add_job.mock_calls[-1][1][0])
+ assert 'connect' in str(mock_cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
-def test_cloud_connect_invalid_auth(mock_client, caplog):
+def test_cloud_connect_invalid_auth(mock_client, caplog, mock_cloud):
"""Test invalid auth detected by server."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.side_effect = \
client_exceptions.WSServerHandshakeError(None, None, code=401)
@@ -220,10 +218,9 @@ def test_cloud_connect_invalid_auth(mock_client, caplog):
@asyncio.coroutine
-def test_cloud_unable_to_connect(mock_client, caplog):
+def test_cloud_unable_to_connect(mock_client, caplog, mock_cloud):
"""Test unable to connect error."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.side_effect = client_exceptions.ClientError(None, None)
yield from conn.connect()
@@ -232,12 +229,28 @@ def test_cloud_unable_to_connect(mock_client, caplog):
@asyncio.coroutine
-def test_cloud_random_exception(mock_client, caplog):
+def test_cloud_random_exception(mock_client, caplog, mock_cloud):
"""Test random exception."""
- cloud = MagicMock()
- conn = iot.CloudIoT(cloud)
+ conn = iot.CloudIoT(mock_cloud)
mock_client.receive.side_effect = Exception
yield from conn.connect()
assert 'Unexpected error' in caplog.text
+
+
+@asyncio.coroutine
+def test_refresh_token_before_expiration_fails(hass, mock_cloud):
+ """Test that we don't connect if token is expired."""
+ mock_cloud.subscription_expired = True
+ mock_cloud.hass = hass
+ conn = iot.CloudIoT(mock_cloud)
+
+ with patch('homeassistant.components.cloud.auth_api.check_token',
+ return_value=mock_coro()) as mock_check_token, \
+ patch.object(hass.components.persistent_notification,
+ 'async_create') as mock_create:
+ yield from conn.connect()
+
+ assert len(mock_check_token.mock_calls) == 1
+ assert len(mock_create.mock_calls) == 1
diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py
index fc359dc7ff7..81800d709e3 100644
--- a/tests/components/config/test_zwave.py
+++ b/tests/components/config/test_zwave.py
@@ -9,7 +9,7 @@ from homeassistant.components import config
from homeassistant.components.zwave import DATA_NETWORK, const
from homeassistant.components.config.zwave import (
ZWaveNodeValueView, ZWaveNodeGroupView, ZWaveNodeConfigView,
- ZWaveUserCodeView)
+ ZWaveUserCodeView, ZWaveConfigWriteView)
from tests.common import mock_http_component_app
from tests.mock.zwave import MockNode, MockValue, MockEntityValues
@@ -417,3 +417,36 @@ def test_get_usercodes_no_genreuser(hass, test_client):
result = yield from resp.json()
assert result == {}
+
+
+@asyncio.coroutine
+def test_save_config_no_network(hass, test_client):
+ """Test saving configuration without network data."""
+ app = mock_http_component_app(hass)
+ ZWaveConfigWriteView().register(app.router)
+
+ client = yield from test_client(app)
+
+ resp = yield from client.post('/api/zwave/saveconfig')
+
+ assert resp.status == 404
+ result = yield from resp.json()
+ assert result == {'message': 'No Z-Wave network data found'}
+
+
+@asyncio.coroutine
+def test_save_config(hass, test_client):
+ """Test saving configuration."""
+ app = mock_http_component_app(hass)
+ ZWaveConfigWriteView().register(app.router)
+
+ network = hass.data[DATA_NETWORK] = MagicMock()
+
+ client = yield from test_client(app)
+
+ resp = yield from client.post('/api/zwave/saveconfig')
+
+ assert resp.status == 200
+ result = yield from resp.json()
+ assert network.write_config.called
+ assert result == {'message': 'Z-Wave configuration saved to file.'}
diff --git a/tests/components/counter/__init__.py b/tests/components/counter/__init__.py
new file mode 100644
index 00000000000..7ebe8e7d7b5
--- /dev/null
+++ b/tests/components/counter/__init__.py
@@ -0,0 +1 @@
+"""Tests for the counter component."""
diff --git a/tests/components/test_counter.py b/tests/components/counter/test_init.py
similarity index 100%
rename from tests/components/test_counter.py
rename to tests/components/counter/test_init.py
index 8dc04f0e76a..f4c6ee9c7da 100644
--- a/tests/components/test_counter.py
+++ b/tests/components/counter/test_init.py
@@ -42,6 +42,47 @@ class TestCounter(unittest.TestCase):
self.assertFalse(
setup_component(self.hass, DOMAIN, {DOMAIN: cfg}))
+ def test_config_options(self):
+ """Test configuration options."""
+ count_start = len(self.hass.states.entity_ids())
+
+ _LOGGER.debug('ENTITIES @ start: %s', self.hass.states.entity_ids())
+
+ config = {
+ DOMAIN: {
+ 'test_1': {},
+ 'test_2': {
+ CONF_NAME: 'Hello World',
+ CONF_ICON: 'mdi:work',
+ CONF_INITIAL: 10,
+ CONF_STEP: 5,
+ }
+ }
+ }
+
+ assert setup_component(self.hass, 'counter', config)
+ self.hass.block_till_done()
+
+ _LOGGER.debug('ENTITIES: %s', self.hass.states.entity_ids())
+
+ self.assertEqual(count_start + 2, len(self.hass.states.entity_ids()))
+ self.hass.block_till_done()
+
+ state_1 = self.hass.states.get('counter.test_1')
+ state_2 = self.hass.states.get('counter.test_2')
+
+ self.assertIsNotNone(state_1)
+ self.assertIsNotNone(state_2)
+
+ self.assertEqual(0, int(state_1.state))
+ self.assertNotIn(ATTR_ICON, state_1.attributes)
+ self.assertNotIn(ATTR_FRIENDLY_NAME, state_1.attributes)
+
+ self.assertEqual(10, int(state_2.state))
+ self.assertEqual('Hello World',
+ state_2.attributes.get(ATTR_FRIENDLY_NAME))
+ self.assertEqual('mdi:work', state_2.attributes.get(ATTR_ICON))
+
def test_methods(self):
"""Test increment, decrement, and reset methods."""
config = {
@@ -118,47 +159,6 @@ class TestCounter(unittest.TestCase):
state = self.hass.states.get(entity_id)
self.assertEqual(15, int(state.state))
- def test_config_options(self):
- """Test configuration options."""
- count_start = len(self.hass.states.entity_ids())
-
- _LOGGER.debug('ENTITIES @ start: %s', self.hass.states.entity_ids())
-
- config = {
- DOMAIN: {
- 'test_1': {},
- 'test_2': {
- CONF_NAME: 'Hello World',
- CONF_ICON: 'mdi:work',
- CONF_INITIAL: 10,
- CONF_STEP: 5,
- }
- }
- }
-
- assert setup_component(self.hass, 'counter', config)
- self.hass.block_till_done()
-
- _LOGGER.debug('ENTITIES: %s', self.hass.states.entity_ids())
-
- self.assertEqual(count_start + 2, len(self.hass.states.entity_ids()))
- self.hass.block_till_done()
-
- state_1 = self.hass.states.get('counter.test_1')
- state_2 = self.hass.states.get('counter.test_2')
-
- self.assertIsNotNone(state_1)
- self.assertIsNotNone(state_2)
-
- self.assertEqual(0, int(state_1.state))
- self.assertNotIn(ATTR_ICON, state_1.attributes)
- self.assertNotIn(ATTR_FRIENDLY_NAME, state_1.attributes)
-
- self.assertEqual(10, int(state_2.state))
- self.assertEqual('Hello World',
- state_2.attributes.get(ATTR_FRIENDLY_NAME))
- self.assertEqual('mdi:work', state_2.attributes.get(ATTR_ICON))
-
@asyncio.coroutine
def test_initial_state_overrules_restore_state(hass):
diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py
index a8531e2aa69..704b2590f12 100644
--- a/tests/components/device_tracker/test_init.py
+++ b/tests/components/device_tracker/test_init.py
@@ -481,6 +481,8 @@ class TestComponentsDeviceTracker(unittest.TestCase):
assert test_events[0].data == {
'entity_id': 'device_tracker.hello',
'host_name': 'hello',
+ 'mac': 'MAC_1',
+ 'vendor': 'unknown',
}
# pylint: disable=invalid-name
diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py
index a06adcb286a..4f5efb9d09d 100644
--- a/tests/components/device_tracker/test_owntracks.py
+++ b/tests/components/device_tracker/test_owntracks.py
@@ -18,10 +18,13 @@ DEVICE = 'phone'
LOCATION_TOPIC = 'owntracks/{}/{}'.format(USER, DEVICE)
EVENT_TOPIC = 'owntracks/{}/{}/event'.format(USER, DEVICE)
-WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoints'.format(USER, DEVICE)
+WAYPOINTS_TOPIC = 'owntracks/{}/{}/waypoints'.format(USER, DEVICE)
+WAYPOINT_TOPIC = 'owntracks/{}/{}/waypoint'.format(USER, DEVICE)
USER_BLACKLIST = 'ram'
-WAYPOINT_TOPIC_BLOCKED = 'owntracks/{}/{}/waypoints'.format(
+WAYPOINTS_TOPIC_BLOCKED = 'owntracks/{}/{}/waypoints'.format(
USER_BLACKLIST, DEVICE)
+LWT_TOPIC = 'owntracks/{}/{}/lwt'.format(USER, DEVICE)
+BAD_TOPIC = 'owntracks/{}/{}/unsupported'.format(USER, DEVICE)
DEVICE_TRACKER_STATE = 'device_tracker.{}_{}'.format(USER, DEVICE)
@@ -232,6 +235,15 @@ WAYPOINTS_UPDATED_MESSAGE = {
]
}
+WAYPOINT_MESSAGE = {
+ "_type": "waypoint",
+ "tst": 4,
+ "lat": 9,
+ "lon": 47,
+ "rad": 50,
+ "desc": "exp_wayp1"
+}
+
WAYPOINT_ENTITY_NAMES = [
'zone.greg_phone__exp_wayp1',
'zone.greg_phone__exp_wayp2',
@@ -239,10 +251,26 @@ WAYPOINT_ENTITY_NAMES = [
'zone.ram_phone__exp_wayp2',
]
+LWT_MESSAGE = {
+ "_type": "lwt",
+ "tst": 1
+}
+
+BAD_MESSAGE = {
+ "_type": "unsupported",
+ "tst": 1
+}
+
BAD_JSON_PREFIX = '--$this is bad json#--'
BAD_JSON_SUFFIX = '** and it ends here ^^'
+# def raise_on_not_implemented(hass, context, message):
+def raise_on_not_implemented():
+ """Throw NotImplemented."""
+ raise NotImplementedError("oopsie")
+
+
class BaseMQTT(unittest.TestCase):
"""Base MQTT assert functions."""
@@ -1056,7 +1084,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_waypoint_import_simple(self):
"""Test a simple import of list of waypoints."""
waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy()
- self.send_message(WAYPOINT_TOPIC, waypoints_message)
+ self.send_message(WAYPOINTS_TOPIC, waypoints_message)
# Check if it made it into states
wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[0])
self.assertTrue(wayp is not None)
@@ -1066,7 +1094,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_waypoint_import_blacklist(self):
"""Test import of list of waypoints for blacklisted user."""
waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy()
- self.send_message(WAYPOINT_TOPIC_BLOCKED, waypoints_message)
+ self.send_message(WAYPOINTS_TOPIC_BLOCKED, waypoints_message)
# Check if it made it into states
wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[2])
self.assertTrue(wayp is None)
@@ -1088,7 +1116,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
run_coroutine_threadsafe(owntracks.async_setup_scanner(
self.hass, test_config, mock_see), self.hass.loop).result()
waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy()
- self.send_message(WAYPOINT_TOPIC_BLOCKED, waypoints_message)
+ self.send_message(WAYPOINTS_TOPIC_BLOCKED, waypoints_message)
# Check if it made it into states
wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[2])
self.assertTrue(wayp is not None)
@@ -1098,7 +1126,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_waypoint_import_bad_json(self):
"""Test importing a bad JSON payload."""
waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy()
- self.send_message(WAYPOINT_TOPIC, waypoints_message, True)
+ self.send_message(WAYPOINTS_TOPIC, waypoints_message, True)
# Check if it made it into states
wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[2])
self.assertTrue(wayp is None)
@@ -1108,15 +1136,40 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_waypoint_import_existing(self):
"""Test importing a zone that exists."""
waypoints_message = WAYPOINTS_EXPORTED_MESSAGE.copy()
- self.send_message(WAYPOINT_TOPIC, waypoints_message)
+ self.send_message(WAYPOINTS_TOPIC, waypoints_message)
# Get the first waypoint exported
wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[0])
# Send an update
waypoints_message = WAYPOINTS_UPDATED_MESSAGE.copy()
- self.send_message(WAYPOINT_TOPIC, waypoints_message)
+ self.send_message(WAYPOINTS_TOPIC, waypoints_message)
new_wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[0])
self.assertTrue(wayp == new_wayp)
+ def test_single_waypoint_import(self):
+ """Test single waypoint message."""
+ waypoint_message = WAYPOINT_MESSAGE.copy()
+ self.send_message(WAYPOINT_TOPIC, waypoint_message)
+ wayp = self.hass.states.get(WAYPOINT_ENTITY_NAMES[0])
+ self.assertTrue(wayp is not None)
+
+ def test_not_implemented_message(self):
+ """Handle not implemented message type."""
+ patch_handler = patch('homeassistant.components.device_tracker.'
+ 'owntracks.async_handle_not_impl_msg',
+ return_value=mock_coro(False))
+ patch_handler.start()
+ self.assertFalse(self.send_message(LWT_TOPIC, LWT_MESSAGE))
+ patch_handler.stop()
+
+ def test_unsupported_message(self):
+ """Handle not implemented message type."""
+ patch_handler = patch('homeassistant.components.device_tracker.'
+ 'owntracks.async_handle_unsupported_msg',
+ return_value=mock_coro(False))
+ patch_handler.start()
+ self.assertFalse(self.send_message(BAD_TOPIC, BAD_MESSAGE))
+ patch_handler.stop()
+
def generate_ciphers(secret):
"""Generate test ciphers for the DEFAULT_LOCATION_MESSAGE."""
@@ -1143,7 +1196,7 @@ def generate_ciphers(secret):
json.dumps(DEFAULT_LOCATION_MESSAGE).encode("utf-8"))
)
).decode("utf-8")
- return (ctxt, mctxt)
+ return ctxt, mctxt
TEST_SECRET_KEY = 's3cretkey'
@@ -1172,7 +1225,7 @@ def mock_cipher():
if key != mkey:
raise ValueError()
return plaintext
- return (len(TEST_SECRET_KEY), mock_decrypt)
+ return len(TEST_SECRET_KEY), mock_decrypt
class TestDeviceTrackerOwnTrackConfigs(BaseMQTT):
diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py
index cc03324a638..383b4f7165d 100644
--- a/tests/components/emulated_hue/test_hue_api.py
+++ b/tests/components/emulated_hue/test_hue_api.py
@@ -1,33 +1,32 @@
"""The tests for the emulated Hue component."""
import asyncio
import json
-
from unittest.mock import patch
-import pytest
-from homeassistant import setup, const, core
+from aiohttp.hdrs import CONTENT_TYPE
+import pytest
+from tests.common import get_test_instance_port
+
+from homeassistant import core, const, setup
import homeassistant.components as core_components
from homeassistant.components import (
- emulated_hue, http, light, script, media_player, fan
-)
-from homeassistant.const import STATE_ON, STATE_OFF
-from homeassistant.components.emulated_hue.hue_api import (
- HUE_API_STATE_ON, HUE_API_STATE_BRI, HueUsernameView,
- HueAllLightsStateView, HueOneLightStateView, HueOneLightChangeView)
+ fan, http, light, script, emulated_hue, media_player)
from homeassistant.components.emulated_hue import Config
-
-from tests.common import get_test_instance_port
+from homeassistant.components.emulated_hue.hue_api import (
+ HUE_API_STATE_ON, HUE_API_STATE_BRI, HueUsernameView, HueOneLightStateView,
+ HueAllLightsStateView, HueOneLightChangeView)
+from homeassistant.const import STATE_ON, STATE_OFF
HTTP_SERVER_PORT = get_test_instance_port()
BRIDGE_SERVER_PORT = get_test_instance_port()
BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
-JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
+JSON_HEADERS = {CONTENT_TYPE: const.CONTENT_TYPE_JSON}
@pytest.fixture
def hass_hue(loop, hass):
- """Setup a hass instance for these tests."""
+ """Setup a Home Assistant instance for these tests."""
# We need to do this to get access to homeassistant/turn_(on,off)
loop.run_until_complete(
core_components.async_setup(hass, {core.DOMAIN: {}}))
diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py
index 3706ce224be..1cd895954de 100644
--- a/tests/components/emulated_hue/test_upnp.py
+++ b/tests/components/emulated_hue/test_upnp.py
@@ -4,6 +4,7 @@ import json
import unittest
from unittest.mock import patch
import requests
+from aiohttp.hdrs import CONTENT_TYPE
from homeassistant import setup, const, core
import homeassistant.components as core_components
@@ -16,7 +17,7 @@ HTTP_SERVER_PORT = get_test_instance_port()
BRIDGE_SERVER_PORT = get_test_instance_port()
BRIDGE_URL_BASE = 'http://127.0.0.1:{}'.format(BRIDGE_SERVER_PORT) + '{}'
-JSON_HEADERS = {const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON}
+JSON_HEADERS = {CONTENT_TYPE: const.CONTENT_TYPE_JSON}
def setup_hass_instance(emulated_hue_config):
diff --git a/tests/components/fan/__init__.py b/tests/components/fan/__init__.py
index 54ed1fcc505..28ae7f4e249 100644
--- a/tests/components/fan/__init__.py
+++ b/tests/components/fan/__init__.py
@@ -1,4 +1,4 @@
-"""Test fan component plaforms."""
+"""Tests for fan platforms."""
import unittest
diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py
index 35be79469a9..dba10608991 100644
--- a/tests/components/google_assistant/test_google_assistant.py
+++ b/tests/components/google_assistant/test_google_assistant.py
@@ -1,18 +1,19 @@
-"""The tests for the Google Actions component."""
+"""The tests for the Google Assistant component."""
# pylint: disable=protected-access
-import json
import asyncio
-import pytest
+import json
-from homeassistant import setup, const, core
-from homeassistant.components import (
- http, async_setup, light, cover, media_player, fan, switch, climate
-)
-from homeassistant.components import google_assistant as ga
+from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
+import pytest
from tests.common import get_test_instance_port
-from . import DEMO_DEVICES
+from homeassistant import core, const, setup
+from homeassistant.components import (
+ fan, http, cover, light, switch, climate, async_setup, media_player)
+from homeassistant.components import google_assistant as ga
+from homeassistant.util.unit_system import IMPERIAL_SYSTEM
+from . import DEMO_DEVICES
API_PASSWORD = "test1234"
SERVER_PORT = get_test_instance_port()
@@ -20,7 +21,7 @@ BASE_API_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
HA_HEADERS = {
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
- const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON,
+ CONTENT_TYPE: const.CONTENT_TYPE_JSON,
}
AUTHCFG = {
@@ -28,12 +29,12 @@ AUTHCFG = {
'client_id': 'helloworld',
'access_token': 'superdoublesecret'
}
-AUTH_HEADER = {'Authorization': 'Bearer {}'.format(AUTHCFG['access_token'])}
+AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(AUTHCFG['access_token'])}
@pytest.fixture
def assistant_client(loop, hass_fixture, test_client):
- """Create web client for emulated hue api."""
+ """Create web client for the Google Assistant API."""
hass = hass_fixture
web_app = hass.http.app
@@ -45,7 +46,7 @@ def assistant_client(loop, hass_fixture, test_client):
@pytest.fixture
def hass_fixture(loop, hass):
- """Set up a hass instance for these tests."""
+ """Set up a HOme Assistant instance for these tests."""
# We need to do this to get access to homeassistant/turn_(on,off)
loop.run_until_complete(async_setup(hass, {core.DOMAIN: {}}))
@@ -180,6 +181,8 @@ def test_query_request(hass_fixture, assistant_client):
'id': "light.ceiling_lights",
}, {
'id': "light.bed_light",
+ }, {
+ 'id': "light.kitchen_lights",
}]
}
}]
@@ -192,10 +195,107 @@ def test_query_request(hass_fixture, assistant_client):
body = yield from result.json()
assert body.get('requestId') == reqid
devices = body['payload']['devices']
- assert len(devices) == 2
+ assert len(devices) == 3
assert devices['light.bed_light']['on'] is False
assert devices['light.ceiling_lights']['on'] is True
assert devices['light.ceiling_lights']['brightness'] == 70
+ assert devices['light.kitchen_lights']['color']['spectrumRGB'] == 16727919
+ assert devices['light.kitchen_lights']['color']['temperature'] == 4166
+
+
+@asyncio.coroutine
+def test_query_climate_request(hass_fixture, assistant_client):
+ """Test a query request."""
+ reqid = '5711642932632160984'
+ data = {
+ 'requestId':
+ reqid,
+ 'inputs': [{
+ 'intent': 'action.devices.QUERY',
+ 'payload': {
+ 'devices': [
+ {'id': 'climate.hvac'},
+ {'id': 'climate.heatpump'},
+ {'id': 'climate.ecobee'},
+ ]
+ }
+ }]
+ }
+ result = yield from assistant_client.post(
+ ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
+ data=json.dumps(data),
+ headers=AUTH_HEADER)
+ assert result.status == 200
+ body = yield from result.json()
+ assert body.get('requestId') == reqid
+ devices = body['payload']['devices']
+ assert devices == {
+ 'climate.heatpump': {
+ 'thermostatTemperatureSetpoint': 20.0,
+ 'thermostatTemperatureAmbient': 25.0,
+ 'thermostatMode': 'heat',
+ },
+ 'climate.ecobee': {
+ 'thermostatTemperatureSetpointHigh': 24,
+ 'thermostatTemperatureAmbient': 23,
+ 'thermostatMode': 'on',
+ 'thermostatTemperatureSetpointLow': 21
+ },
+ 'climate.hvac': {
+ 'thermostatTemperatureSetpoint': 21,
+ 'thermostatTemperatureAmbient': 22,
+ 'thermostatMode': 'cool',
+ 'thermostatHumidityAmbient': 54,
+ }
+ }
+
+
+@asyncio.coroutine
+def test_query_climate_request_f(hass_fixture, assistant_client):
+ """Test a query request."""
+ hass_fixture.config.units = IMPERIAL_SYSTEM
+ reqid = '5711642932632160984'
+ data = {
+ 'requestId':
+ reqid,
+ 'inputs': [{
+ 'intent': 'action.devices.QUERY',
+ 'payload': {
+ 'devices': [
+ {'id': 'climate.hvac'},
+ {'id': 'climate.heatpump'},
+ {'id': 'climate.ecobee'},
+ ]
+ }
+ }]
+ }
+ result = yield from assistant_client.post(
+ ga.const.GOOGLE_ASSISTANT_API_ENDPOINT,
+ data=json.dumps(data),
+ headers=AUTH_HEADER)
+ assert result.status == 200
+ body = yield from result.json()
+ assert body.get('requestId') == reqid
+ devices = body['payload']['devices']
+ assert devices == {
+ 'climate.heatpump': {
+ 'thermostatTemperatureSetpoint': -6.7,
+ 'thermostatTemperatureAmbient': -3.9,
+ 'thermostatMode': 'heat',
+ },
+ 'climate.ecobee': {
+ 'thermostatTemperatureSetpointHigh': -4.4,
+ 'thermostatTemperatureAmbient': -5,
+ 'thermostatMode': 'on',
+ 'thermostatTemperatureSetpointLow': -6.1,
+ },
+ 'climate.hvac': {
+ 'thermostatTemperatureSetpoint': -6.1,
+ 'thermostatTemperatureAmbient': -5.6,
+ 'thermostatMode': 'cool',
+ 'thermostatHumidityAmbient': 54,
+ }
+ }
@asyncio.coroutine
@@ -225,6 +325,31 @@ def test_execute_request(hass_fixture, assistant_client):
"on": False
}
}]
+ }, {
+ "devices": [{
+ "id": "light.kitchen_lights",
+ }],
+ "execution": [{
+ "command": "action.devices.commands.ColorAbsolute",
+ "params": {
+ "color": {
+ "spectrumRGB": 16711680,
+ "temperature": 2100
+ }
+ }
+ }]
+ }, {
+ "devices": [{
+ "id": "light.kitchen_lights",
+ }],
+ "execution": [{
+ "command": "action.devices.commands.ColorAbsolute",
+ "params": {
+ "color": {
+ "spectrumRGB": 16711680
+ }
+ }
+ }]
}]
}
}]
@@ -237,7 +362,10 @@ def test_execute_request(hass_fixture, assistant_client):
body = yield from result.json()
assert body.get('requestId') == reqid
commands = body['payload']['commands']
- assert len(commands) == 3
+ assert len(commands) == 5
ceiling = hass_fixture.states.get('light.ceiling_lights')
assert ceiling.state == 'off'
+ kitchen = hass_fixture.states.get('light.kitchen_lights')
+ assert kitchen.attributes.get(light.ATTR_COLOR_TEMP) == 476
+ assert kitchen.attributes.get(light.ATTR_RGB_COLOR) == (255, 0, 0)
assert hass_fixture.states.get('switch.decorative_lights').state == 'off'
diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py
new file mode 100644
index 00000000000..9ced9fc329d
--- /dev/null
+++ b/tests/components/google_assistant/test_init.py
@@ -0,0 +1,31 @@
+"""The tests for google-assistant init."""
+import asyncio
+
+from homeassistant.setup import async_setup_component
+from homeassistant.components import google_assistant as ga
+
+GA_API_KEY = "Agdgjsj399sdfkosd932ksd"
+GA_AGENT_USER_ID = "testid"
+
+
+@asyncio.coroutine
+def test_request_sync_service(aioclient_mock, hass):
+ """Test that it posts to the request_sync url."""
+ aioclient_mock.post(
+ ga.const.REQUEST_SYNC_BASE_URL, status=200)
+
+ yield from async_setup_component(hass, 'google_assistant', {
+ 'google_assistant': {
+ 'project_id': 'test_project',
+ 'client_id': 'r7328kwdsdfsdf03223409',
+ 'access_token': '8wdsfjsf932492342349234',
+ 'agent_user_id': GA_AGENT_USER_ID,
+ 'api_key': GA_API_KEY
+ }})
+
+ assert aioclient_mock.call_count == 0
+ yield from hass.services.async_call(ga.const.DOMAIN,
+ ga.const.SERVICE_REQUEST_SYNC,
+ blocking=True)
+
+ assert aioclient_mock.call_count == 1
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index 20db85b998e..2668c0cecfc 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -5,6 +5,7 @@ import asyncio
from homeassistant import const
from homeassistant.components import climate
from homeassistant.components import google_assistant as ga
+from homeassistant.util.unit_system import (IMPERIAL_SYSTEM, METRIC_SYSTEM)
DETERMINE_SERVICE_TESTS = [{ # Test light brightness
'entity_id': 'light.test',
@@ -16,6 +17,57 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness
const.SERVICE_TURN_ON,
{'entity_id': 'light.test', 'brightness': 242}
)
+}, { # Test light color temperature
+ 'entity_id': 'light.test',
+ 'command': ga.const.COMMAND_COLOR,
+ 'params': {
+ 'color': {
+ 'temperature': 2300,
+ 'name': 'warm white'
+ }
+ },
+ 'expected': (
+ const.SERVICE_TURN_ON,
+ {'entity_id': 'light.test', 'kelvin': 2300}
+ )
+}, { # Test light color blue
+ 'entity_id': 'light.test',
+ 'command': ga.const.COMMAND_COLOR,
+ 'params': {
+ 'color': {
+ 'spectrumRGB': 255,
+ 'name': 'blue'
+ }
+ },
+ 'expected': (
+ const.SERVICE_TURN_ON,
+ {'entity_id': 'light.test', 'rgb_color': [0, 0, 255]}
+ )
+}, { # Test light color yellow
+ 'entity_id': 'light.test',
+ 'command': ga.const.COMMAND_COLOR,
+ 'params': {
+ 'color': {
+ 'spectrumRGB': 16776960,
+ 'name': 'yellow'
+ }
+ },
+ 'expected': (
+ const.SERVICE_TURN_ON,
+ {'entity_id': 'light.test', 'rgb_color': [255, 255, 0]}
+ )
+}, { # Test unhandled action/service
+ 'entity_id': 'light.test',
+ 'command': ga.const.COMMAND_COLOR,
+ 'params': {
+ 'color': {
+ 'unhandled': 2300
+ }
+ },
+ 'expected': (
+ None,
+ {'entity_id': 'light.test'}
+ )
}, { # Test switch to light custom type
'entity_id': 'switch.decorative_lights',
'command': ga.const.COMMAND_ONOFF,
@@ -82,6 +134,15 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness
climate.SERVICE_SET_TEMPERATURE,
{'entity_id': 'climate.living_room', 'temperature': 24.5}
),
+}, { # Test climate temperature Fahrenheit
+ 'entity_id': 'climate.living_room',
+ 'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT,
+ 'params': {'thermostatTemperatureSetpoint': 24.5},
+ 'units': IMPERIAL_SYSTEM,
+ 'expected': (
+ climate.SERVICE_SET_TEMPERATURE,
+ {'entity_id': 'climate.living_room', 'temperature': 76.1}
+ ),
}, { # Test climate temperature range
'entity_id': 'climate.living_room',
'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
@@ -94,6 +155,19 @@ DETERMINE_SERVICE_TESTS = [{ # Test light brightness
{'entity_id': 'climate.living_room',
'target_temp_high': 24.5, 'target_temp_low': 20.5}
),
+}, { # Test climate temperature range Fahrenheit
+ 'entity_id': 'climate.living_room',
+ 'command': ga.const.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE,
+ 'params': {
+ 'thermostatTemperatureSetpointHigh': 24.5,
+ 'thermostatTemperatureSetpointLow': 20.5,
+ },
+ 'units': IMPERIAL_SYSTEM,
+ 'expected': (
+ climate.SERVICE_SET_TEMPERATURE,
+ {'entity_id': 'climate.living_room',
+ 'target_temp_high': 76.1, 'target_temp_low': 68.9}
+ ),
}, { # Test climate operation mode
'entity_id': 'climate.living_room',
'command': ga.const.COMMAND_THERMOSTAT_SET_MODE,
@@ -122,5 +196,6 @@ def test_determine_service():
result = ga.smart_home.determine_service(
test['entity_id'],
test['command'],
- test['params'])
+ test['params'],
+ test.get('units', METRIC_SYSTEM))
assert result == test['expected']
diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py
index 4428b5043fd..4ff87efd137 100644
--- a/tests/components/http/test_init.py
+++ b/tests/components/http/test_init.py
@@ -1,19 +1,23 @@
"""The tests for the Home Assistant HTTP component."""
import asyncio
+
+from aiohttp.hdrs import (
+ ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_HEADERS,
+ ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS,
+ CONTENT_TYPE)
import requests
-
-from homeassistant import setup, const
-import homeassistant.components.http as http
-
from tests.common import get_test_instance_port, get_test_home_assistant
+from homeassistant import const, setup
+import homeassistant.components.http as http
+
API_PASSWORD = 'test1234'
SERVER_PORT = get_test_instance_port()
HTTP_BASE = '127.0.0.1:{}'.format(SERVER_PORT)
HTTP_BASE_URL = 'http://{}'.format(HTTP_BASE)
HA_HEADERS = {
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
- const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON,
+ CONTENT_TYPE: const.CONTENT_TYPE_JSON,
}
CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE]
@@ -64,9 +68,9 @@ class TestCors:
"""Test cross origin resource sharing with password in url."""
req = requests.get(_url(const.URL_API),
params={'api_password': API_PASSWORD},
- headers={const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL})
+ headers={ORIGIN: HTTP_BASE_URL})
- allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
+ allow_origin = ACCESS_CONTROL_ALLOW_ORIGIN
assert req.status_code == 200
assert req.headers.get(allow_origin) == HTTP_BASE_URL
@@ -75,11 +79,11 @@ class TestCors:
"""Test cross origin resource sharing with password in header."""
headers = {
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
- const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL
+ ORIGIN: HTTP_BASE_URL
}
req = requests.get(_url(const.URL_API), headers=headers)
- allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
+ allow_origin = ACCESS_CONTROL_ALLOW_ORIGIN
assert req.status_code == 200
assert req.headers.get(allow_origin) == HTTP_BASE_URL
@@ -91,8 +95,8 @@ class TestCors:
}
req = requests.get(_url(const.URL_API), headers=headers)
- allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
- allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
+ allow_origin = ACCESS_CONTROL_ALLOW_ORIGIN
+ allow_headers = ACCESS_CONTROL_ALLOW_HEADERS
assert req.status_code == 200
assert allow_origin not in req.headers
@@ -101,14 +105,14 @@ class TestCors:
def test_cors_preflight_allowed(self):
"""Test cross origin resource sharing preflight (OPTIONS) request."""
headers = {
- const.HTTP_HEADER_ORIGIN: HTTP_BASE_URL,
- 'Access-Control-Request-Method': 'GET',
- 'Access-Control-Request-Headers': 'x-ha-access'
+ ORIGIN: HTTP_BASE_URL,
+ ACCESS_CONTROL_REQUEST_METHOD: 'GET',
+ ACCESS_CONTROL_REQUEST_HEADERS: 'x-ha-access'
}
req = requests.options(_url(const.URL_API), headers=headers)
- allow_origin = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN
- allow_headers = const.HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS
+ allow_origin = ACCESS_CONTROL_ALLOW_ORIGIN
+ allow_headers = ACCESS_CONTROL_ALLOW_HEADERS
assert req.status_code == 200
assert req.headers.get(allow_origin) == HTTP_BASE_URL
@@ -139,26 +143,14 @@ def test_registering_view_while_running(hass, test_client):
}
)
- yield from setup.async_setup_component(hass, 'api')
-
yield from hass.async_start()
-
- yield from hass.async_block_till_done()
-
+ # This raises a RuntimeError if app is frozen
hass.http.register_view(TestView)
- client = yield from test_client(hass.http.app)
-
- resp = yield from client.get('/hello')
- assert resp.status == 200
-
- text = yield from resp.text()
- assert text == 'hello'
-
@asyncio.coroutine
def test_api_base_url_with_domain(hass):
- """Test setting api url."""
+ """Test setting API URL."""
result = yield from setup.async_setup_component(hass, 'http', {
'http': {
'base_url': 'example.com'
diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py
index b4576b174d6..8a7d648e6f2 100644
--- a/tests/components/light/test_demo.py
+++ b/tests/components/light/test_demo.py
@@ -1,14 +1,11 @@
"""The tests for the demo light component."""
# pylint: disable=protected-access
-import asyncio
import unittest
-from homeassistant.core import State, CoreState
-from homeassistant.setup import setup_component, async_setup_component
+from homeassistant.setup import setup_component
import homeassistant.components.light as light
-from homeassistant.helpers.restore_state import DATA_RESTORE_CACHE
-from tests.common import get_test_home_assistant, mock_component
+from tests.common import get_test_home_assistant
ENTITY_LIGHT = 'light.bed_light'
@@ -79,36 +76,3 @@ class TestDemoLight(unittest.TestCase):
light.turn_off(self.hass)
self.hass.block_till_done()
self.assertFalse(light.is_on(self.hass, ENTITY_LIGHT))
-
-
-@asyncio.coroutine
-def test_restore_state(hass):
- """Test state gets restored."""
- mock_component(hass, 'recorder')
- hass.state = CoreState.starting
- hass.data[DATA_RESTORE_CACHE] = {
- 'light.bed_light': State('light.bed_light', 'on', {
- 'brightness': 'value-brightness',
- 'color_temp': 'value-color_temp',
- 'rgb_color': 'value-rgb_color',
- 'xy_color': 'value-xy_color',
- 'white_value': 'value-white_value',
- 'effect': 'value-effect',
- }),
- }
-
- yield from async_setup_component(hass, 'light', {
- 'light': {
- 'platform': 'demo',
- }})
-
- state = hass.states.get('light.bed_light')
- assert state is not None
- assert state.entity_id == 'light.bed_light'
- assert state.state == 'on'
- assert state.attributes.get('brightness') == 'value-brightness'
- assert state.attributes.get('color_temp') == 'value-color_temp'
- assert state.attributes.get('rgb_color') == 'value-rgb_color'
- assert state.attributes.get('xy_color') == 'value-xy_color'
- assert state.attributes.get('white_value') == 'value-white_value'
- assert state.attributes.get('effect') == 'value-effect'
diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py
index 439b272fd4a..2bcd02e69aa 100644
--- a/tests/components/media_player/test_monoprice.py
+++ b/tests/components/media_player/test_monoprice.py
@@ -9,7 +9,8 @@ from homeassistant.components.media_player import (
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE)
from homeassistant.const import STATE_ON, STATE_OFF
-from components.media_player.monoprice import MonopriceZone, PLATFORM_SCHEMA
+from homeassistant.components.media_player.monoprice import (
+ MonopriceZone, PLATFORM_SCHEMA)
class MockState(object):
diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py
index 01281d189b4..ffd4008f385 100644
--- a/tests/components/media_player/test_universal.py
+++ b/tests/components/media_player/test_universal.py
@@ -2,6 +2,8 @@
from copy import copy
import unittest
+from voluptuous.error import MultipleInvalid
+
from homeassistant.const import (
STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED)
import homeassistant.components.switch as switch
@@ -14,6 +16,13 @@ from homeassistant.util.async import run_coroutine_threadsafe
from tests.common import mock_service, get_test_home_assistant
+def validate_config(config):
+ """Use the platform schema to validate configuration."""
+ validated_config = universal.PLATFORM_SCHEMA(config)
+ validated_config.pop('platform')
+ return validated_config
+
+
class MockMediaPlayer(media_player.MediaPlayerDevice):
"""Mock media player for testing."""
@@ -116,9 +125,9 @@ class MockMediaPlayer(media_player.MediaPlayerDevice):
"""Mock turn_off function."""
self._state = STATE_OFF
- def mute_volume(self):
+ def mute_volume(self, mute):
"""Mock mute function."""
- self._is_volume_muted = ~self._is_volume_muted
+ self._is_volume_muted = mute
def set_volume_level(self, volume):
"""Mock set volume level."""
@@ -210,10 +219,8 @@ class TestMediaPlayer(unittest.TestCase):
config_start['commands'] = {}
config_start['attributes'] = {}
- response = universal.validate_config(self.config_children_only)
-
- self.assertTrue(response)
- self.assertEqual(config_start, self.config_children_only)
+ config = validate_config(self.config_children_only)
+ self.assertEqual(config_start, config)
def test_config_children_and_attr(self):
"""Check config with children and attributes."""
@@ -221,15 +228,16 @@ class TestMediaPlayer(unittest.TestCase):
del config_start['platform']
config_start['commands'] = {}
- response = universal.validate_config(self.config_children_and_attr)
-
- self.assertTrue(response)
- self.assertEqual(config_start, self.config_children_and_attr)
+ config = validate_config(self.config_children_and_attr)
+ self.assertEqual(config_start, config)
def test_config_no_name(self):
"""Check config with no Name entry."""
- response = universal.validate_config({'platform': 'universal'})
-
+ response = True
+ try:
+ validate_config({'platform': 'universal'})
+ except MultipleInvalid:
+ response = False
self.assertFalse(response)
def test_config_bad_children(self):
@@ -238,36 +246,31 @@ class TestMediaPlayer(unittest.TestCase):
config_bad_children = {'name': 'test', 'children': {},
'platform': 'universal'}
- response = universal.validate_config(config_no_children)
- self.assertTrue(response)
+ config_no_children = validate_config(config_no_children)
self.assertEqual([], config_no_children['children'])
- response = universal.validate_config(config_bad_children)
- self.assertTrue(response)
+ config_bad_children = validate_config(config_bad_children)
self.assertEqual([], config_bad_children['children'])
def test_config_bad_commands(self):
"""Check config with bad commands entry."""
- config = {'name': 'test', 'commands': [], 'platform': 'universal'}
+ config = {'name': 'test', 'platform': 'universal'}
- response = universal.validate_config(config)
- self.assertTrue(response)
+ config = validate_config(config)
self.assertEqual({}, config['commands'])
def test_config_bad_attributes(self):
"""Check config with bad attributes."""
- config = {'name': 'test', 'attributes': [], 'platform': 'universal'}
+ config = {'name': 'test', 'platform': 'universal'}
- response = universal.validate_config(config)
- self.assertTrue(response)
+ config = validate_config(config)
self.assertEqual({}, config['attributes'])
def test_config_bad_key(self):
"""Check config with bad key."""
config = {'name': 'test', 'asdf': 5, 'platform': 'universal'}
- response = universal.validate_config(config)
- self.assertTrue(response)
+ config = validate_config(config)
self.assertFalse('asdf' in config)
def test_platform_setup(self):
@@ -281,21 +284,27 @@ class TestMediaPlayer(unittest.TestCase):
for dev in new_entities:
entities.append(dev)
- run_coroutine_threadsafe(
- universal.async_setup_platform(self.hass, bad_config, add_devices),
- self.hass.loop).result()
+ setup_ok = True
+ try:
+ run_coroutine_threadsafe(
+ universal.async_setup_platform(
+ self.hass, validate_config(bad_config), add_devices),
+ self.hass.loop).result()
+ except MultipleInvalid:
+ setup_ok = False
+ self.assertFalse(setup_ok)
self.assertEqual(0, len(entities))
run_coroutine_threadsafe(
- universal.async_setup_platform(self.hass, config, add_devices),
+ universal.async_setup_platform(
+ self.hass, validate_config(config), add_devices),
self.hass.loop).result()
self.assertEqual(1, len(entities))
self.assertEqual('test', entities[0].name)
def test_master_state(self):
"""Test master state property."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -303,8 +312,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_master_state_with_attrs(self):
"""Test master state property."""
- config = self.config_children_and_attr
- universal.validate_config(config)
+ config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -312,11 +320,26 @@ class TestMediaPlayer(unittest.TestCase):
self.hass.states.set(self.mock_state_switch_id, STATE_ON)
self.assertEqual(STATE_ON, ump.master_state)
+ def test_master_state_with_template(self):
+ """Test the state_template option."""
+ config = copy(self.config_children_and_attr)
+ self.hass.states.set('input_boolean.test', STATE_OFF)
+ templ = '{% if states.input_boolean.test.state == "off" %}on' \
+ '{% else %}{{ states.media_player.mock1.state }}{% endif %}'
+ config['state_template'] = templ
+ config = validate_config(config)
+
+ ump = universal.UniversalMediaPlayer(self.hass, **config)
+
+ self.assertEqual(STATE_ON, ump.master_state)
+ self.hass.states.set('input_boolean.test', STATE_ON)
+ self.assertEqual(STATE_OFF, ump.master_state)
+
def test_master_state_with_bad_attrs(self):
"""Test master state property."""
- config = self.config_children_and_attr
+ config = copy(self.config_children_and_attr)
config['attributes']['state'] = 'bad.entity_id'
- universal.validate_config(config)
+ config = validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -324,8 +347,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_active_child_state(self):
"""Test active child state property."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -356,8 +378,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_name(self):
"""Test name property."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -365,8 +386,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_polling(self):
"""Test should_poll property."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -374,8 +394,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_state_children_only(self):
"""Test media player state with only children."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -391,8 +410,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_state_with_children_and_attrs(self):
"""Test media player with children and master state."""
- config = self.config_children_and_attr
- universal.validate_config(config)
+ config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -416,8 +434,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_volume_level(self):
"""Test volume level property."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -439,9 +456,8 @@ class TestMediaPlayer(unittest.TestCase):
def test_media_image_url(self):
"""Test media_image_url property."""
- TEST_URL = "test_url"
- config = self.config_children_only
- universal.validate_config(config)
+ test_url = "test_url"
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -450,7 +466,7 @@ class TestMediaPlayer(unittest.TestCase):
self.assertEqual(None, ump.media_image_url)
self.mock_mp_1._state = STATE_PLAYING
- self.mock_mp_1._media_image_url = TEST_URL
+ self.mock_mp_1._media_image_url = test_url
self.mock_mp_1.schedule_update_ha_state()
self.hass.block_till_done()
run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result()
@@ -460,8 +476,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_is_volume_muted_children_only(self):
"""Test is volume muted property w/ children only."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -483,8 +498,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_source_list_children_and_attr(self):
"""Test source list property w/ children and attrs."""
- config = self.config_children_and_attr
- universal.validate_config(config)
+ config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -495,8 +509,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_source_children_and_attr(self):
"""Test source property w/ children and attrs."""
- config = self.config_children_and_attr
- universal.validate_config(config)
+ config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -507,8 +520,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_volume_level_children_and_attr(self):
"""Test volume level property w/ children and attrs."""
- config = self.config_children_and_attr
- universal.validate_config(config)
+ config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -519,8 +531,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_is_volume_muted_children_and_attr(self):
"""Test is volume muted property w/ children and attrs."""
- config = self.config_children_and_attr
- universal.validate_config(config)
+ config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
@@ -531,8 +542,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_supported_features_children_only(self):
"""Test supported media commands with only children."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -549,16 +559,19 @@ class TestMediaPlayer(unittest.TestCase):
def test_supported_features_children_and_cmds(self):
"""Test supported media commands with children and attrs."""
- config = self.config_children_and_attr
- universal.validate_config(config)
- config['commands']['turn_on'] = 'test'
- config['commands']['turn_off'] = 'test'
- config['commands']['volume_up'] = 'test'
- config['commands']['volume_down'] = 'test'
- config['commands']['volume_mute'] = 'test'
- config['commands']['volume_set'] = 'test'
- config['commands']['select_source'] = 'test'
- config['commands']['shuffle_set'] = 'test'
+ config = copy(self.config_children_and_attr)
+ excmd = {'service': 'media_player.test', 'data': {'entity_id': 'test'}}
+ config['commands'] = {
+ 'turn_on': excmd,
+ 'turn_off': excmd,
+ 'volume_up': excmd,
+ 'volume_down': excmd,
+ 'volume_mute': excmd,
+ 'volume_set': excmd,
+ 'select_source': excmd,
+ 'shuffle_set': excmd
+ }
+ config = validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -577,8 +590,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_service_call_no_active_child(self):
"""Test a service call to children with no active child."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_and_attr)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -599,8 +611,7 @@ class TestMediaPlayer(unittest.TestCase):
def test_service_call_to_child(self):
"""Test service calls that should be routed to a child."""
- config = self.config_children_only
- universal.validate_config(config)
+ config = validate_config(self.config_children_only)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
@@ -699,10 +710,10 @@ class TestMediaPlayer(unittest.TestCase):
def test_service_call_to_command(self):
"""Test service call to command."""
- config = self.config_children_only
+ config = copy(self.config_children_only)
config['commands'] = {'turn_off': {
'service': 'test.turn_off', 'data': {}}}
- universal.validate_config(config)
+ config = validate_config(config)
service = mock_service(self.hass, 'test', 'turn_off')
diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py
index 3d068224243..55ff0e9ff05 100644
--- a/tests/components/mqtt/test_init.py
+++ b/tests/components/mqtt/test_init.py
@@ -388,9 +388,12 @@ class TestMQTTCallbacks(unittest.TestCase):
@mock.patch('homeassistant.components.mqtt.time.sleep')
def test_mqtt_disconnect_tries_reconnect(self, mock_sleep):
"""Test the re-connect tries."""
- self.hass.data['mqtt'].topics = {
+ self.hass.data['mqtt'].subscribed_topics = {
'test/topic': 1,
- 'test/progress': None
+ }
+ self.hass.data['mqtt'].wanted_topics = {
+ 'test/progress': 0,
+ 'test/topic': 2,
}
self.hass.data['mqtt'].progress = {
1: 'test/progress'
@@ -403,7 +406,9 @@ class TestMQTTCallbacks(unittest.TestCase):
self.assertEqual([1, 2, 4],
[call[1][0] for call in mock_sleep.mock_calls])
- self.assertEqual({'test/topic': 1}, self.hass.data['mqtt'].topics)
+ self.assertEqual({'test/topic': 2, 'test/progress': 0},
+ self.hass.data['mqtt'].wanted_topics)
+ self.assertEqual({}, self.hass.data['mqtt'].subscribed_topics)
self.assertEqual({}, self.hass.data['mqtt'].progress)
def test_invalid_mqtt_topics(self):
@@ -556,12 +561,15 @@ def test_mqtt_subscribes_topics_on_connect(hass):
"""Test subscription to topic on connect."""
mqtt_client = yield from mock_mqtt_client(hass)
- prev_topics = OrderedDict()
- prev_topics['topic/test'] = 1,
- prev_topics['home/sensor'] = 2,
- prev_topics['still/pending'] = None
+ subscribed_topics = OrderedDict()
+ subscribed_topics['topic/test'] = 1
+ subscribed_topics['home/sensor'] = 2
- hass.data['mqtt'].topics = prev_topics
+ wanted_topics = subscribed_topics.copy()
+ wanted_topics['still/pending'] = 0
+
+ hass.data['mqtt'].wanted_topics = wanted_topics
+ hass.data['mqtt'].subscribed_topics = subscribed_topics
hass.data['mqtt'].progress = {1: 'still/pending'}
# Return values for subscribe calls (rc, mid)
@@ -574,7 +582,7 @@ def test_mqtt_subscribes_topics_on_connect(hass):
assert not mqtt_client.disconnect.called
- expected = [(topic, qos) for topic, qos in prev_topics.items()
- if qos is not None]
+ expected = [(topic, qos) for topic, qos in wanted_topics.items()]
assert [call[1][1:] for call in hass.add_job.mock_calls] == expected
+ assert hass.data['mqtt'].progress == {}
diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py
index 5aa8afb4f7d..c3998b6db64 100644
--- a/tests/components/notify/test_html5.py
+++ b/tests/components/notify/test_html5.py
@@ -2,6 +2,7 @@
import asyncio
import json
from unittest.mock import patch, MagicMock, mock_open
+from aiohttp.hdrs import AUTHORIZATION
from homeassistant.components.notify import html5
@@ -56,24 +57,13 @@ class TestHtml5Notify(object):
m = mock_open()
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
service = html5.get_service(hass, {})
assert service is not None
- def test_get_service_with_bad_json(self):
- """Test ."""
- hass = MagicMock()
-
- m = mock_open(read_data='I am not JSON')
- with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
- ):
- service = html5.get_service(hass, {})
-
- assert service is None
-
@patch('pywebpush.WebPusher')
def test_sending_message(self, mock_wp):
"""Test sending message."""
@@ -85,7 +75,8 @@ class TestHtml5Notify(object):
m = mock_open(read_data=json.dumps(data))
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
service = html5.get_service(hass, {'gcm_sender_id': '100'})
@@ -119,7 +110,8 @@ class TestHtml5Notify(object):
m = mock_open()
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {})
@@ -157,7 +149,8 @@ class TestHtml5Notify(object):
m = mock_open()
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {})
@@ -192,7 +185,8 @@ class TestHtml5Notify(object):
m = mock_open()
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {})
@@ -221,7 +215,7 @@ class TestHtml5Notify(object):
}))
assert resp.status == 400
- with patch('homeassistant.components.notify.html5._save_config',
+ with patch('homeassistant.components.notify.html5.save_json',
return_value=False):
# resp = view.post(Request(builder.get_environ()))
resp = yield from client.post(REGISTER_URL, data=json.dumps({
@@ -242,14 +236,12 @@ class TestHtml5Notify(object):
}
m = mock_open(read_data=json.dumps(config))
-
- with patch('homeassistant.components.notify.html5.open', m,
- create=True):
+ with patch(
+ 'homeassistant.util.json.open',
+ m, create=True
+ ):
hass.config.path.return_value = 'file.conf'
-
- with patch('homeassistant.components.notify.html5.os.path.isfile',
- return_value=True):
- service = html5.get_service(hass, {})
+ service = html5.get_service(hass, {})
assert service is not None
@@ -278,8 +270,8 @@ class TestHtml5Notify(object):
assert json.loads(handle.write.call_args[0][0]) == config
@asyncio.coroutine
- def test_unregister_device_view_handle_unknown_subscription(self, loop,
- test_client):
+ def test_unregister_device_view_handle_unknown_subscription(
+ self, loop, test_client):
"""Test that the HTML unregister view handles unknown subscriptions."""
hass = MagicMock()
@@ -290,12 +282,11 @@ class TestHtml5Notify(object):
m = mock_open(read_data=json.dumps(config))
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
hass.config.path.return_value = 'file.conf'
- with patch('homeassistant.components.notify.html5.os.path.isfile',
- return_value=True):
- service = html5.get_service(hass, {})
+ service = html5.get_service(hass, {})
assert service is not None
@@ -322,8 +313,8 @@ class TestHtml5Notify(object):
assert handle.write.call_count == 0
@asyncio.coroutine
- def test_unregistering_device_view_handles_json_safe_error(self, loop,
- test_client):
+ def test_unregistering_device_view_handles_json_safe_error(
+ self, loop, test_client):
"""Test that the HTML unregister view handles JSON write errors."""
hass = MagicMock()
@@ -334,12 +325,11 @@ class TestHtml5Notify(object):
m = mock_open(read_data=json.dumps(config))
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
hass.config.path.return_value = 'file.conf'
- with patch('homeassistant.components.notify.html5.os.path.isfile',
- return_value=True):
- service = html5.get_service(hass, {})
+ service = html5.get_service(hass, {})
assert service is not None
@@ -356,7 +346,7 @@ class TestHtml5Notify(object):
client = yield from test_client(app)
hass.http.is_banned_ip.return_value = False
- with patch('homeassistant.components.notify.html5._save_config',
+ with patch('homeassistant.components.notify.html5.save_json',
return_value=False):
resp = yield from client.delete(REGISTER_URL, data=json.dumps({
'subscription': SUBSCRIPTION_1['subscription'],
@@ -374,7 +364,8 @@ class TestHtml5Notify(object):
m = mock_open()
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
hass.config.path.return_value = 'file.conf'
service = html5.get_service(hass, {})
@@ -405,17 +396,16 @@ class TestHtml5Notify(object):
hass = MagicMock()
data = {
- 'device': SUBSCRIPTION_1,
+ 'device': SUBSCRIPTION_1
}
m = mock_open(read_data=json.dumps(data))
with patch(
- 'homeassistant.components.notify.html5.open', m, create=True
+ 'homeassistant.util.json.open',
+ m, create=True
):
hass.config.path.return_value = 'file.conf'
- with patch('homeassistant.components.notify.html5.os.path.isfile',
- return_value=True):
- service = html5.get_service(hass, {'gcm_sender_id': '100'})
+ service = html5.get_service(hass, {'gcm_sender_id': '100'})
assert service is not None
@@ -423,8 +413,8 @@ class TestHtml5Notify(object):
assert len(hass.mock_calls) == 3
with patch('pywebpush.WebPusher') as mock_wp:
- service.send_message('Hello', target=['device'],
- data={'icon': 'beer.png'})
+ service.send_message(
+ 'Hello', target=['device'], data={'icon': 'beer.png'})
assert len(mock_wp.mock_calls) == 3
@@ -453,7 +443,7 @@ class TestHtml5Notify(object):
resp = yield from client.post(PUBLISH_URL, data=json.dumps({
'type': 'push',
- }), headers={'Authorization': bearer_token})
+ }), headers={AUTHORIZATION: bearer_token})
assert resp.status == 200
body = yield from resp.json()
diff --git a/tests/components/sensor/test_vultr.py b/tests/components/sensor/test_vultr.py
new file mode 100644
index 00000000000..a4e5edc5800
--- /dev/null
+++ b/tests/components/sensor/test_vultr.py
@@ -0,0 +1,165 @@
+"""The tests for the Vultr sensor platform."""
+import pytest
+import unittest
+import requests_mock
+import voluptuous as vol
+
+from homeassistant.components.sensor import vultr
+from homeassistant.components import vultr as base_vultr
+from homeassistant.components.vultr import CONF_SUBSCRIPTION
+from homeassistant.const import (
+ CONF_NAME, CONF_MONITORED_CONDITIONS, CONF_PLATFORM)
+
+from tests.components.test_vultr import VALID_CONFIG
+from tests.common import (
+ get_test_home_assistant, load_fixture)
+
+
+class TestVultrSensorSetup(unittest.TestCase):
+ """Test the Vultr platform."""
+
+ DEVICES = []
+
+ def add_devices(self, devices, action):
+ """Mock add devices."""
+ for device in devices:
+ self.DEVICES.append(device)
+
+ def setUp(self):
+ """Initialize values for this testcase class."""
+ self.hass = get_test_home_assistant()
+ self.configs = [
+ {
+ CONF_NAME: vultr.DEFAULT_NAME,
+ CONF_SUBSCRIPTION: '576965',
+ CONF_MONITORED_CONDITIONS: vultr.MONITORED_CONDITIONS
+ },
+ {
+ CONF_NAME: 'Server {}',
+ CONF_SUBSCRIPTION: '123456',
+ CONF_MONITORED_CONDITIONS: vultr.MONITORED_CONDITIONS
+ },
+ {
+ CONF_NAME: 'VPS Charges',
+ CONF_SUBSCRIPTION: '555555',
+ CONF_MONITORED_CONDITIONS: [
+ 'pending_charges'
+ ]
+ }
+ ]
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @requests_mock.Mocker()
+ def test_sensor(self, mock):
+ """Test the Vultr sensor class and methods."""
+ mock.get(
+ 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_account_info.json'))
+
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ base_vultr.setup(self.hass, VALID_CONFIG)
+
+ for config in self.configs:
+ setup = vultr.setup_platform(self.hass,
+ config,
+ self.add_devices,
+ None)
+
+ self.assertIsNone(setup)
+
+ self.assertEqual(5, len(self.DEVICES))
+
+ tested = 0
+
+ for device in self.DEVICES:
+
+ # Test pre update
+ if device.subscription == '576965':
+ self.assertEqual(vultr.DEFAULT_NAME, device.name)
+
+ device.update()
+
+ if device.unit_of_measurement == 'GB': # Test Bandwidth Used
+ if device.subscription == '576965':
+ self.assertEqual(
+ 'Vultr my new server Current Bandwidth Used',
+ device.name)
+ self.assertEqual('mdi:chart-histogram', device.icon)
+ self.assertEqual(131.51, device.state)
+ self.assertEqual('mdi:chart-histogram', device.icon)
+ tested += 1
+
+ elif device.subscription == '123456':
+ self.assertEqual('Server Current Bandwidth Used',
+ device.name)
+ self.assertEqual(957.46, device.state)
+ tested += 1
+
+ elif device.unit_of_measurement == 'US$': # Test Pending Charges
+
+ if device.subscription == '576965': # Default 'Vultr {} {}'
+ self.assertEqual('Vultr my new server Pending Charges',
+ device.name)
+ self.assertEqual('mdi:currency-usd', device.icon)
+ self.assertEqual(46.67, device.state)
+ self.assertEqual('mdi:currency-usd', device.icon)
+ tested += 1
+
+ elif device.subscription == '123456': # Custom name with 1 {}
+ self.assertEqual('Server Pending Charges', device.name)
+ self.assertEqual('not a number', device.state)
+ tested += 1
+
+ elif device.subscription == '555555': # No {} in name
+ self.assertEqual('VPS Charges', device.name)
+ self.assertEqual(5.45, device.state)
+ tested += 1
+
+ self.assertEqual(tested, 5)
+
+ def test_invalid_sensor_config(self):
+ """Test config type failures."""
+ with pytest.raises(vol.Invalid): # No subscription
+ vultr.PLATFORM_SCHEMA({
+ CONF_PLATFORM: base_vultr.DOMAIN,
+ CONF_MONITORED_CONDITIONS: vultr.MONITORED_CONDITIONS
+ })
+ with pytest.raises(vol.Invalid): # Bad monitored_conditions
+ vultr.PLATFORM_SCHEMA({
+ CONF_PLATFORM: base_vultr.DOMAIN,
+ CONF_SUBSCRIPTION: '123456',
+ CONF_MONITORED_CONDITIONS: [
+ 'non-existent-condition',
+ ]
+ })
+
+ @requests_mock.Mocker()
+ def test_invalid_sensors(self, mock):
+ """Test the VultrSensor fails."""
+ mock.get(
+ 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_account_info.json'))
+
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ base_vultr.setup(self.hass, VALID_CONFIG)
+
+ bad_conf = {
+ CONF_MONITORED_CONDITIONS: vultr.MONITORED_CONDITIONS,
+ } # No subs at all
+
+ no_sub_setup = vultr.setup_platform(self.hass,
+ bad_conf,
+ self.add_devices,
+ None)
+
+ self.assertIsNotNone(no_sub_setup)
+ self.assertEqual(0, len(self.DEVICES))
diff --git a/tests/components/switch/test_vultr.py b/tests/components/switch/test_vultr.py
new file mode 100644
index 00000000000..53bf6fbec85
--- /dev/null
+++ b/tests/components/switch/test_vultr.py
@@ -0,0 +1,201 @@
+"""Test the Vultr switch platform."""
+import unittest
+import requests_mock
+import pytest
+import voluptuous as vol
+
+from homeassistant.components.switch import vultr
+from homeassistant.components import vultr as base_vultr
+from homeassistant.components.vultr import (
+ ATTR_ALLOWED_BANDWIDTH, ATTR_AUTO_BACKUPS, ATTR_IPV4_ADDRESS,
+ ATTR_COST_PER_MONTH, ATTR_CREATED_AT, ATTR_SUBSCRIPTION_ID,
+ CONF_SUBSCRIPTION)
+from homeassistant.const import (
+ CONF_PLATFORM, CONF_NAME)
+
+from tests.components.test_vultr import VALID_CONFIG
+from tests.common import (
+ get_test_home_assistant, load_fixture)
+
+
+class TestVultrSwitchSetup(unittest.TestCase):
+ """Test the Vultr switch platform."""
+
+ DEVICES = []
+
+ def add_devices(self, devices, action):
+ """Mock add devices."""
+ for device in devices:
+ self.DEVICES.append(device)
+
+ def setUp(self):
+ """Init values for this testcase class."""
+ self.hass = get_test_home_assistant()
+ self.configs = [
+ {
+ CONF_SUBSCRIPTION: '576965',
+ CONF_NAME: "A Server"
+ },
+ {
+ CONF_SUBSCRIPTION: '123456',
+ CONF_NAME: "Failed Server"
+ },
+ {
+ CONF_SUBSCRIPTION: '555555',
+ CONF_NAME: vultr.DEFAULT_NAME
+ }
+ ]
+
+ def tearDown(self):
+ """Stop our started services."""
+ self.hass.stop()
+
+ @requests_mock.Mocker()
+ def test_switch(self, mock):
+ """Test successful instance."""
+ mock.get(
+ 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_account_info.json'))
+
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ # Setup hub
+ base_vultr.setup(self.hass, VALID_CONFIG)
+
+ # Setup each of our test configs
+ for config in self.configs:
+ vultr.setup_platform(self.hass,
+ config,
+ self.add_devices,
+ None)
+
+ self.assertEqual(len(self.DEVICES), 3)
+
+ tested = 0
+
+ for device in self.DEVICES:
+ if device.subscription == '555555':
+ self.assertEqual('Vultr {}', device.name)
+ tested += 1
+
+ device.update()
+ device_attrs = device.device_state_attributes
+
+ if device.subscription == '555555':
+ self.assertEqual('Vultr Another Server', device.name)
+ tested += 1
+
+ if device.name == 'A Server':
+ self.assertEqual(True, device.is_on)
+ self.assertEqual('on', device.state)
+ self.assertEqual('mdi:server', device.icon)
+ self.assertEqual('1000',
+ device_attrs[ATTR_ALLOWED_BANDWIDTH])
+ self.assertEqual('yes',
+ device_attrs[ATTR_AUTO_BACKUPS])
+ self.assertEqual('123.123.123.123',
+ device_attrs[ATTR_IPV4_ADDRESS])
+ self.assertEqual('10.05',
+ device_attrs[ATTR_COST_PER_MONTH])
+ self.assertEqual('2013-12-19 14:45:41',
+ device_attrs[ATTR_CREATED_AT])
+ self.assertEqual('576965',
+ device_attrs[ATTR_SUBSCRIPTION_ID])
+ tested += 1
+
+ elif device.name == 'Failed Server':
+ self.assertEqual(False, device.is_on)
+ self.assertEqual('off', device.state)
+ self.assertEqual('mdi:server-off', device.icon)
+ self.assertEqual('1000',
+ device_attrs[ATTR_ALLOWED_BANDWIDTH])
+ self.assertEqual('no',
+ device_attrs[ATTR_AUTO_BACKUPS])
+ self.assertEqual('192.168.100.50',
+ device_attrs[ATTR_IPV4_ADDRESS])
+ self.assertEqual('73.25',
+ device_attrs[ATTR_COST_PER_MONTH])
+ self.assertEqual('2014-10-13 14:45:41',
+ device_attrs[ATTR_CREATED_AT])
+ self.assertEqual('123456',
+ device_attrs[ATTR_SUBSCRIPTION_ID])
+ tested += 1
+
+ self.assertEqual(4, tested)
+
+ @requests_mock.Mocker()
+ def test_turn_on(self, mock):
+ """Test turning a subscription on."""
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ mock.post(
+ 'https://api.vultr.com/v1/server/start?api_key=ABCDEFG1234567')
+
+ for device in self.DEVICES:
+ if device.name == 'Failed Server':
+ device.turn_on()
+
+ # Turn on, force date update
+ self.assertEqual(2, mock.call_count)
+
+ @requests_mock.Mocker()
+ def test_turn_off(self, mock):
+ """Test turning a subscription off."""
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ mock.post(
+ 'https://api.vultr.com/v1/server/halt?api_key=ABCDEFG1234567')
+
+ for device in self.DEVICES:
+ if device.name == 'A Server':
+ device.turn_off()
+
+ # Turn off, force update
+ self.assertEqual(2, mock.call_count)
+
+ def test_invalid_switch_config(self):
+ """Test config type failures."""
+ with pytest.raises(vol.Invalid): # No subscription
+ vultr.PLATFORM_SCHEMA({
+ CONF_PLATFORM: base_vultr.DOMAIN,
+ })
+
+ @requests_mock.Mocker()
+ def test_invalid_switches(self, mock):
+ """Test the VultrSwitch fails."""
+ mock.get(
+ 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_account_info.json'))
+
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ base_vultr.setup(self.hass, VALID_CONFIG)
+
+ bad_conf = {} # No subscription
+
+ no_subs_setup = vultr.setup_platform(self.hass,
+ bad_conf,
+ self.add_devices,
+ None)
+
+ self.assertIsNotNone(no_subs_setup)
+
+ bad_conf = {
+ CONF_NAME: "Missing Server",
+ CONF_SUBSCRIPTION: '665544'
+ } # Sub not associated with API key (not in server_list)
+
+ wrong_subs_setup = vultr.setup_platform(self.hass,
+ bad_conf,
+ self.add_devices,
+ None)
+
+ self.assertIsNotNone(wrong_subs_setup)
diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py
index 063cf93d871..3042535ff42 100644
--- a/tests/components/switch/test_wake_on_lan.py
+++ b/tests/components/switch/test_wake_on_lan.py
@@ -6,7 +6,7 @@ from homeassistant.setup import setup_component
from homeassistant.const import STATE_ON, STATE_OFF
import homeassistant.components.switch as switch
-from tests.common import get_test_home_assistant
+from tests.common import get_test_home_assistant, mock_service
TEST_STATE = None
@@ -141,6 +141,7 @@ class TestWOLSwitch(unittest.TestCase):
},
}
}))
+ calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET')
state = self.hass.states.get('switch.wake_on_lan')
self.assertEqual(STATE_OFF, state.state)
@@ -152,6 +153,7 @@ class TestWOLSwitch(unittest.TestCase):
state = self.hass.states.get('switch.wake_on_lan')
self.assertEqual(STATE_ON, state.state)
+ assert len(calls) == 0
TEST_STATE = False
@@ -160,6 +162,7 @@ class TestWOLSwitch(unittest.TestCase):
state = self.hass.states.get('switch.wake_on_lan')
self.assertEqual(STATE_OFF, state.state)
+ assert len(calls) == 1
@patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet)
@patch('subprocess.call', new=call)
diff --git a/tests/components/test_dialogflow.py b/tests/components/test_dialogflow.py
index 8275534123c..a52c841e0cc 100644
--- a/tests/components/test_dialogflow.py
+++ b/tests/components/test_dialogflow.py
@@ -4,6 +4,7 @@ import json
import unittest
import requests
+from aiohttp.hdrs import CONTENT_TYPE
from homeassistant.core import callback
from homeassistant import setup, const
@@ -18,7 +19,7 @@ INTENTS_API_URL = "{}{}".format(BASE_API_URL, dialogflow.INTENTS_API_ENDPOINT)
HA_HEADERS = {
const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
- const.HTTP_HEADER_CONTENT_TYPE: const.CONTENT_TYPE_JSON,
+ CONTENT_TYPE: const.CONTENT_TYPE_JSON,
}
SESSION_ID = "a9b84cec-46b6-484e-8f31-f65dba03ae6d"
diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py
index 1b034cfe940..3d8d2b62a2b 100644
--- a/tests/components/test_frontend.py
+++ b/tests/components/test_frontend.py
@@ -52,7 +52,7 @@ def test_frontend_and_static(mock_http_client):
# Test we can retrieve frontend.js
frontendjs = re.search(
- r'(?P\/static\/frontend-[A-Za-z0-9]{32}.html)', text)
+ r'(?P\/frontend_es5\/frontend-[A-Za-z0-9]{32}.html)', text)
assert frontendjs is not None
resp = yield from mock_http_client.get(frontendjs.groups(0)[0])
@@ -63,6 +63,10 @@ def test_frontend_and_static(mock_http_client):
@asyncio.coroutine
def test_dont_cache_service_worker(mock_http_client):
"""Test that we don't cache the service worker."""
+ resp = yield from mock_http_client.get('/service_worker_es5.js')
+ assert resp.status == 200
+ assert 'cache-control' not in resp.headers
+
resp = yield from mock_http_client.get('/service_worker.js')
assert resp.status == 200
assert 'cache-control' not in resp.headers
diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py
index 761ba29e403..3704c486a2a 100644
--- a/tests/components/test_hassio.py
+++ b/tests/components/test_hassio.py
@@ -231,7 +231,8 @@ def test_auth_required_forward_request(hassio_client):
@asyncio.coroutine
-def test_forward_request_no_auth_for_panel(hassio_client):
+@pytest.mark.parametrize('build_type', ['es5', 'latest'])
+def test_forward_request_no_auth_for_panel(hassio_client, build_type):
"""Test no auth needed for ."""
response = MagicMock()
response.read.return_value = mock_coro('data')
@@ -240,7 +241,8 @@ def test_forward_request_no_auth_for_panel(hassio_client):
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio._create_response') as mresp:
mresp.return_value = 'response'
- resp = yield from hassio_client.get('/api/hassio/panel')
+ resp = yield from hassio_client.get(
+ '/api/hassio/panel_{}'.format(build_type))
# Check we got right response
assert resp.status == 200
diff --git a/tests/components/test_input_datetime.py b/tests/components/test_input_datetime.py
index af664f36a53..5d3f1782831 100644
--- a/tests/components/test_input_datetime.py
+++ b/tests/components/test_input_datetime.py
@@ -102,7 +102,7 @@ def test_set_datetime_time(hass):
@asyncio.coroutine
def test_set_invalid(hass):
"""Test set_datetime method with only time."""
- initial = datetime.datetime(2017, 1, 1, 0, 0)
+ initial = '2017-01-01'
yield from async_setup_component(hass, DOMAIN, {
DOMAIN: {
'test_date': {
@@ -124,7 +124,7 @@ def test_set_invalid(hass):
yield from hass.async_block_till_done()
state = hass.states.get(entity_id)
- assert state.state == str(initial.date())
+ assert state.state == initial
@asyncio.coroutine
@@ -159,8 +159,8 @@ def test_set_datetime_date(hass):
def test_restore_state(hass):
"""Ensure states are restored on startup."""
mock_restore_cache(hass, (
- State('input_datetime.test_time', '2017-09-07 19:46:00'),
- State('input_datetime.test_date', '2017-09-07 19:46:00'),
+ State('input_datetime.test_time', '19:46:00'),
+ State('input_datetime.test_date', '2017-09-07'),
State('input_datetime.test_datetime', '2017-09-07 19:46:00'),
State('input_datetime.test_bogus_data', 'this is not a date'),
))
diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py
index cc1ea277a34..76d8e48d03a 100644
--- a/tests/components/test_mqtt_statestream.py
+++ b/tests/components/test_mqtt_statestream.py
@@ -25,7 +25,8 @@ class TestMqttStateStream(object):
self.hass.stop()
def add_statestream(self, base_topic=None, publish_attributes=None,
- publish_timestamps=None):
+ publish_timestamps=None, publish_include=None,
+ publish_exclude=None):
"""Add a mqtt_statestream component."""
config = {}
if base_topic:
@@ -34,7 +35,10 @@ class TestMqttStateStream(object):
config['publish_attributes'] = publish_attributes
if publish_timestamps:
config['publish_timestamps'] = publish_timestamps
- print("Publishing timestamps")
+ if publish_include:
+ config['include'] = publish_include
+ if publish_exclude:
+ config['exclude'] = publish_exclude
return setup_component(self.hass, statestream.DOMAIN, {
statestream.DOMAIN: config})
@@ -152,3 +156,237 @@ class TestMqttStateStream(object):
mock_pub.assert_has_calls(calls, any_order=True)
assert mock_pub.called
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ @patch('homeassistant.core.dt_util.utcnow')
+ def test_state_changed_event_include_domain(self, mock_utcnow, mock_pub):
+ """"Test that filtering on included domain works as expected."""
+ base_topic = 'pub'
+
+ incl = {
+ 'domains': ['fake']
+ }
+ excl = {}
+
+ # Add the statestream component for publishing state updates
+ # Set the filter to allow fake.* items
+ assert self.add_statestream(base_topic=base_topic,
+ publish_include=incl,
+ publish_exclude=excl)
+ self.hass.block_till_done()
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_statestream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State('fake.entity', 'on'))
+ self.hass.block_till_done()
+
+ # Make sure 'on' was published to pub/fake/entity/state
+ mock_pub.assert_called_with(self.hass, 'pub/fake/entity/state', 'on',
+ 1, True)
+ assert mock_pub.called
+
+ mock_pub.reset_mock()
+ # Set a state of an entity that shouldn't be included
+ mock_state_change_event(self.hass, State('fake2.entity', 'on'))
+ self.hass.block_till_done()
+
+ assert not mock_pub.called
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ @patch('homeassistant.core.dt_util.utcnow')
+ def test_state_changed_event_include_entity(self, mock_utcnow, mock_pub):
+ """"Test that filtering on included entity works as expected."""
+ base_topic = 'pub'
+
+ incl = {
+ 'entities': ['fake.entity']
+ }
+ excl = {}
+
+ # Add the statestream component for publishing state updates
+ # Set the filter to allow fake.* items
+ assert self.add_statestream(base_topic=base_topic,
+ publish_include=incl,
+ publish_exclude=excl)
+ self.hass.block_till_done()
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_statestream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State('fake.entity', 'on'))
+ self.hass.block_till_done()
+
+ # Make sure 'on' was published to pub/fake/entity/state
+ mock_pub.assert_called_with(self.hass, 'pub/fake/entity/state', 'on',
+ 1, True)
+ assert mock_pub.called
+
+ mock_pub.reset_mock()
+ # Set a state of an entity that shouldn't be included
+ mock_state_change_event(self.hass, State('fake.entity2', 'on'))
+ self.hass.block_till_done()
+
+ assert not mock_pub.called
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ @patch('homeassistant.core.dt_util.utcnow')
+ def test_state_changed_event_exclude_domain(self, mock_utcnow, mock_pub):
+ """"Test that filtering on excluded domain works as expected."""
+ base_topic = 'pub'
+
+ incl = {}
+ excl = {
+ 'domains': ['fake2']
+ }
+
+ # Add the statestream component for publishing state updates
+ # Set the filter to allow fake.* items
+ assert self.add_statestream(base_topic=base_topic,
+ publish_include=incl,
+ publish_exclude=excl)
+ self.hass.block_till_done()
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_statestream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State('fake.entity', 'on'))
+ self.hass.block_till_done()
+
+ # Make sure 'on' was published to pub/fake/entity/state
+ mock_pub.assert_called_with(self.hass, 'pub/fake/entity/state', 'on',
+ 1, True)
+ assert mock_pub.called
+
+ mock_pub.reset_mock()
+ # Set a state of an entity that shouldn't be included
+ mock_state_change_event(self.hass, State('fake2.entity', 'on'))
+ self.hass.block_till_done()
+
+ assert not mock_pub.called
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ @patch('homeassistant.core.dt_util.utcnow')
+ def test_state_changed_event_exclude_entity(self, mock_utcnow, mock_pub):
+ """"Test that filtering on excluded entity works as expected."""
+ base_topic = 'pub'
+
+ incl = {}
+ excl = {
+ 'entities': ['fake.entity2']
+ }
+
+ # Add the statestream component for publishing state updates
+ # Set the filter to allow fake.* items
+ assert self.add_statestream(base_topic=base_topic,
+ publish_include=incl,
+ publish_exclude=excl)
+ self.hass.block_till_done()
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_statestream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State('fake.entity', 'on'))
+ self.hass.block_till_done()
+
+ # Make sure 'on' was published to pub/fake/entity/state
+ mock_pub.assert_called_with(self.hass, 'pub/fake/entity/state', 'on',
+ 1, True)
+ assert mock_pub.called
+
+ mock_pub.reset_mock()
+ # Set a state of an entity that shouldn't be included
+ mock_state_change_event(self.hass, State('fake.entity2', 'on'))
+ self.hass.block_till_done()
+
+ assert not mock_pub.called
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ @patch('homeassistant.core.dt_util.utcnow')
+ def test_state_changed_event_exclude_domain_include_entity(
+ self, mock_utcnow, mock_pub):
+ """"Test filtering with excluded domain and included entity."""
+ base_topic = 'pub'
+
+ incl = {
+ 'entities': ['fake.entity']
+ }
+ excl = {
+ 'domains': ['fake']
+ }
+
+ # Add the statestream component for publishing state updates
+ # Set the filter to allow fake.* items
+ assert self.add_statestream(base_topic=base_topic,
+ publish_include=incl,
+ publish_exclude=excl)
+ self.hass.block_till_done()
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_statestream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State('fake.entity', 'on'))
+ self.hass.block_till_done()
+
+ # Make sure 'on' was published to pub/fake/entity/state
+ mock_pub.assert_called_with(self.hass, 'pub/fake/entity/state', 'on',
+ 1, True)
+ assert mock_pub.called
+
+ mock_pub.reset_mock()
+ # Set a state of an entity that shouldn't be included
+ mock_state_change_event(self.hass, State('fake.entity2', 'on'))
+ self.hass.block_till_done()
+
+ assert not mock_pub.called
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ @patch('homeassistant.core.dt_util.utcnow')
+ def test_state_changed_event_include_domain_exclude_entity(
+ self, mock_utcnow, mock_pub):
+ """"Test filtering with included domain and excluded entity."""
+ base_topic = 'pub'
+
+ incl = {
+ 'domains': ['fake']
+ }
+ excl = {
+ 'entities': ['fake.entity2']
+ }
+
+ # Add the statestream component for publishing state updates
+ # Set the filter to allow fake.* items
+ assert self.add_statestream(base_topic=base_topic,
+ publish_include=incl,
+ publish_exclude=excl)
+ self.hass.block_till_done()
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_statestream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State('fake.entity', 'on'))
+ self.hass.block_till_done()
+
+ # Make sure 'on' was published to pub/fake/entity/state
+ mock_pub.assert_called_with(self.hass, 'pub/fake/entity/state', 'on',
+ 1, True)
+ assert mock_pub.called
+
+ mock_pub.reset_mock()
+ # Set a state of an entity that shouldn't be included
+ mock_state_change_event(self.hass, State('fake.entity2', 'on'))
+ self.hass.block_till_done()
+
+ assert not mock_pub.called
diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py
index 00c824418be..805d73e1820 100644
--- a/tests/components/test_panel_iframe.py
+++ b/tests/components/test_panel_iframe.py
@@ -33,8 +33,8 @@ class TestPanelIframe(unittest.TestCase):
'panel_iframe': conf
})
- @patch.dict('hass_frontend.FINGERPRINTS',
- {'panels/ha-panel-iframe.html': 'md5md5'})
+ @patch.dict('hass_frontend_es5.FINGERPRINTS',
+ {'iframe': 'md5md5'})
def test_correct_config(self):
"""Test correct config."""
assert setup.setup_component(
@@ -55,20 +55,20 @@ class TestPanelIframe(unittest.TestCase):
panels = self.hass.data[frontend.DATA_PANELS]
- assert panels.get('router').as_dict() == {
+ assert panels.get('router').to_response(self.hass, None) == {
'component_name': 'iframe',
'config': {'url': 'http://192.168.1.1'},
'icon': 'mdi:network-wireless',
'title': 'Router',
- 'url': '/static/panels/ha-panel-iframe-md5md5.html',
+ 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'router'
}
- assert panels.get('weather').as_dict() == {
+ assert panels.get('weather').to_response(self.hass, None) == {
'component_name': 'iframe',
'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'},
'icon': 'mdi:weather',
'title': 'Weather',
- 'url': '/static/panels/ha-panel-iframe-md5md5.html',
+ 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'weather',
}
diff --git a/tests/components/test_python_script.py b/tests/components/test_python_script.py
index e5d6b0c4aad..8a7f94d7dcd 100644
--- a/tests/components/test_python_script.py
+++ b/tests/components/test_python_script.py
@@ -209,6 +209,27 @@ hass.states.set('hello.ab_list', '{}'.format(ab_list))
assert caplog.text == ''
+@asyncio.coroutine
+def test_execute_sorted(hass, caplog):
+ """Test sorted() function."""
+ caplog.set_level(logging.ERROR)
+ source = """
+a = sorted([3,1,2])
+assert(a == [1,2,3])
+hass.states.set('hello.a', a[0])
+hass.states.set('hello.b', a[1])
+hass.states.set('hello.c', a[2])
+"""
+ hass.async_add_job(execute, hass, 'test.py', source, {})
+ yield from hass.async_block_till_done()
+
+ assert hass.states.is_state('hello.a', '1')
+ assert hass.states.is_state('hello.b', '2')
+ assert hass.states.is_state('hello.c', '3')
+ # No errors logged = good
+ assert caplog.text == ''
+
+
@asyncio.coroutine
def test_exposed_modules(hass, caplog):
"""Test datetime and time modules exposed."""
diff --git a/tests/components/test_spc.py b/tests/components/test_spc.py
index 6fae8d821c2..7837abd8007 100644
--- a/tests/components/test_spc.py
+++ b/tests/components/test_spc.py
@@ -7,7 +7,9 @@ from homeassistant.components import spc
from homeassistant.bootstrap import async_setup_component
from tests.common import async_test_home_assistant
from tests.test_util.aiohttp import mock_aiohttp_client
-from homeassistant.const import (STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
+from homeassistant.const import (
+ STATE_ON, STATE_OFF, STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
@pytest.fixture
@@ -57,7 +59,13 @@ def aioclient_mock():
@asyncio.coroutine
-def test_update_alarm_device(hass, aioclient_mock, monkeypatch):
+@pytest.mark.parametrize("sia_code,state", [
+ ('NL', STATE_ALARM_ARMED_HOME),
+ ('CG', STATE_ALARM_ARMED_AWAY),
+ ('OG', STATE_ALARM_DISARMED)
+])
+def test_update_alarm_device(hass, aioclient_mock, monkeypatch,
+ sia_code, state):
"""Test that alarm panel state changes on incoming websocket data."""
monkeypatch.setattr("homeassistant.components.spc.SpcWebGateway."
"start_listener", lambda x, *args: None)
@@ -65,8 +73,8 @@ def test_update_alarm_device(hass, aioclient_mock, monkeypatch):
'spc': {
'api_url': 'http://localhost/',
'ws_url': 'ws://localhost/'
- }
}
+ }
yield from async_setup_component(hass, 'spc', config)
yield from hass.async_block_till_done()
@@ -74,38 +82,48 @@ def test_update_alarm_device(hass, aioclient_mock, monkeypatch):
assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
- msg = {"sia_code": "NL", "sia_address": "1", "description": "House|Sam|1"}
+ msg = {"sia_code": sia_code, "sia_address": "1",
+ "description": "House¦Sam¦1"}
yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY])
- assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME
+ yield from hass.async_block_till_done()
- msg = {"sia_code": "OQ", "sia_address": "1", "description": "Sam"}
- yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY])
- assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED
+ state_obj = hass.states.get(entity_id)
+ assert state_obj.state == state
+ assert state_obj.attributes['changed_by'] == 'Sam'
@asyncio.coroutine
-def test_update_sensor_device(hass, aioclient_mock, monkeypatch):
- """Test that sensors change state on incoming websocket data."""
+@pytest.mark.parametrize("sia_code,state", [
+ ('ZO', STATE_ON),
+ ('ZC', STATE_OFF)
+])
+def test_update_sensor_device(hass, aioclient_mock, monkeypatch,
+ sia_code, state):
+ """
+ Test that sensors change state on incoming websocket data.
+
+ Note that we don't test for the ZD (disconnected) and ZX (problem/short)
+ codes since the binary sensor component is hardcoded to only
+ let on/off states through.
+ """
monkeypatch.setattr("homeassistant.components.spc.SpcWebGateway."
"start_listener", lambda x, *args: None)
config = {
'spc': {
'api_url': 'http://localhost/',
'ws_url': 'ws://localhost/'
- }
}
+ }
yield from async_setup_component(hass, 'spc', config)
yield from hass.async_block_till_done()
- assert hass.states.get('binary_sensor.hallway_pir').state == 'off'
+ assert hass.states.get('binary_sensor.hallway_pir').state == STATE_OFF
- msg = {"sia_code": "ZO", "sia_address": "3", "description": "Hallway PIR"}
+ msg = {"sia_code": sia_code, "sia_address": "3",
+ "description": "Hallway PIR"}
yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY])
- assert hass.states.get('binary_sensor.hallway_pir').state == 'on'
-
- msg = {"sia_code": "ZC", "sia_address": "3", "description": "Hallway PIR"}
- yield from spc._async_process_message(msg, hass.data[spc.DATA_REGISTRY])
- assert hass.states.get('binary_sensor.hallway_pir').state == 'off'
+ yield from hass.async_block_till_done()
+ assert hass.states.get('binary_sensor.hallway_pir').state == state
class TestSpcRegistry:
@@ -139,7 +157,7 @@ class TestSpcWebGateway:
('set', spc.SpcWebGateway.AREA_COMMAND_SET),
('unset', spc.SpcWebGateway.AREA_COMMAND_UNSET),
('set_a', spc.SpcWebGateway.AREA_COMMAND_PART_SET)
- ])
+ ])
def test_area_commands(self, spcwebgw, url_command, command):
"""Test alarm arming/disarming."""
with mock_aiohttp_client() as aioclient_mock:
diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py
new file mode 100644
index 00000000000..b86c768fb42
--- /dev/null
+++ b/tests/components/test_system_log.py
@@ -0,0 +1,112 @@
+"""Test system log component."""
+import asyncio
+import logging
+import pytest
+
+from homeassistant.bootstrap import async_setup_component
+from homeassistant.components import system_log
+
+_LOGGER = logging.getLogger('test_logger')
+
+
+@pytest.fixture(autouse=True)
+@asyncio.coroutine
+def setup_test_case(hass):
+ """Setup system_log component before test case."""
+ config = {'system_log': {'max_entries': 2}}
+ yield from async_setup_component(hass, system_log.DOMAIN, config)
+
+
+@asyncio.coroutine
+def get_error_log(hass, test_client, expected_count):
+ """Fetch all entries from system_log via the API."""
+ client = yield from test_client(hass.http.app)
+ resp = yield from client.get('/api/error/all')
+ assert resp.status == 200
+
+ data = yield from resp.json()
+ assert len(data) == expected_count
+ return data
+
+
+def _generate_and_log_exception(exception, log):
+ try:
+ raise Exception(exception)
+ except: # pylint: disable=bare-except
+ _LOGGER.exception(log)
+
+
+def assert_log(log, exception, message, level):
+ """Assert that specified values are in a specific log entry."""
+ assert exception in log['exception']
+ assert message == log['message']
+ assert level == log['level']
+ assert log['source'] == 'unknown' # always unkown in tests
+ assert 'timestamp' in log
+
+
+@asyncio.coroutine
+def test_normal_logs(hass, test_client):
+ """Test that debug and info are not logged."""
+ _LOGGER.debug('debug')
+ _LOGGER.info('info')
+
+ # Assert done by get_error_log
+ yield from get_error_log(hass, test_client, 0)
+
+
+@asyncio.coroutine
+def test_exception(hass, test_client):
+ """Test that exceptions are logged and retrieved correctly."""
+ _generate_and_log_exception('exception message', 'log message')
+ log = (yield from get_error_log(hass, test_client, 1))[0]
+ assert_log(log, 'exception message', 'log message', 'ERROR')
+
+
+@asyncio.coroutine
+def test_warning(hass, test_client):
+ """Test that warning are logged and retrieved correctly."""
+ _LOGGER.warning('warning message')
+ log = (yield from get_error_log(hass, test_client, 1))[0]
+ assert_log(log, '', 'warning message', 'WARNING')
+
+
+@asyncio.coroutine
+def test_error(hass, test_client):
+ """Test that errors are logged and retrieved correctly."""
+ _LOGGER.error('error message')
+ log = (yield from get_error_log(hass, test_client, 1))[0]
+ assert_log(log, '', 'error message', 'ERROR')
+
+
+@asyncio.coroutine
+def test_critical(hass, test_client):
+ """Test that critical are logged and retrieved correctly."""
+ _LOGGER.critical('critical message')
+ log = (yield from get_error_log(hass, test_client, 1))[0]
+ assert_log(log, '', 'critical message', 'CRITICAL')
+
+
+@asyncio.coroutine
+def test_remove_older_logs(hass, test_client):
+ """Test that older logs are rotated out."""
+ _LOGGER.error('error message 1')
+ _LOGGER.error('error message 2')
+ _LOGGER.error('error message 3')
+ log = yield from get_error_log(hass, test_client, 2)
+ assert_log(log[0], '', 'error message 3', 'ERROR')
+ assert_log(log[1], '', 'error message 2', 'ERROR')
+
+
+@asyncio.coroutine
+def test_clear_logs(hass, test_client):
+ """Test that the log can be cleared via a service call."""
+ _LOGGER.error('error message')
+
+ hass.async_add_job(
+ hass.services.async_call(
+ system_log.DOMAIN, system_log.SERVICE_CLEAR, {}))
+ yield from hass.async_block_till_done()
+
+ # Assert done by get_error_log
+ yield from get_error_log(hass, test_client, 0)
diff --git a/tests/components/test_vultr.py b/tests/components/test_vultr.py
new file mode 100644
index 00000000000..b504c320dc8
--- /dev/null
+++ b/tests/components/test_vultr.py
@@ -0,0 +1,48 @@
+"""The tests for the Vultr component."""
+import unittest
+import requests_mock
+
+from copy import deepcopy
+from homeassistant import setup
+import homeassistant.components.vultr as vultr
+
+from tests.common import (
+ get_test_home_assistant, load_fixture)
+
+VALID_CONFIG = {
+ 'vultr': {
+ 'api_key': 'ABCDEFG1234567'
+ }
+}
+
+
+class TestVultr(unittest.TestCase):
+ """Tests the Vultr component."""
+
+ def setUp(self):
+ """Initialize values for this test case class."""
+ self.hass = get_test_home_assistant()
+ self.config = VALID_CONFIG
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop everything that we started."""
+ self.hass.stop()
+
+ @requests_mock.Mocker()
+ def test_setup(self, mock):
+ """Test successful setup."""
+ mock.get(
+ 'https://api.vultr.com/v1/account/info?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_account_info.json'))
+ mock.get(
+ 'https://api.vultr.com/v1/server/list?api_key=ABCDEFG1234567',
+ text=load_fixture('vultr_server_list.json'))
+
+ response = vultr.setup(self.hass, self.config)
+ self.assertTrue(response)
+
+ def test_setup_no_api_key(self):
+ """Test failed setup with missing API Key."""
+ conf = deepcopy(self.config)
+ del conf['vultr']['api_key']
+ assert not setup.setup_component(self.hass, vultr.DOMAIN, conf)
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
index c310b0d5445..8b6c7494214 100644
--- a/tests/components/test_websocket_api.py
+++ b/tests/components/test_websocket_api.py
@@ -290,7 +290,7 @@ def test_get_panels(hass, websocket_client):
"""Test get_panels command."""
yield from hass.components.frontend.async_register_built_in_panel(
'map', 'Map', 'mdi:account-location')
-
+ hass.data[frontend.DATA_JS_VERSION] = 'es5'
websocket_client.send_json({
'id': 5,
'type': wapi.TYPE_GET_PANELS,
@@ -300,8 +300,14 @@ def test_get_panels(hass, websocket_client):
assert msg['id'] == 5
assert msg['type'] == wapi.TYPE_RESULT
assert msg['success']
- assert msg['result'] == {url: panel.as_dict() for url, panel
- in hass.data[frontend.DATA_PANELS].items()}
+ assert msg['result'] == {'map': {
+ 'component_name': 'map',
+ 'url_path': 'map',
+ 'config': None,
+ 'url': None,
+ 'icon': 'mdi:account-location',
+ 'title': 'Map',
+ }}
@asyncio.coroutine
diff --git a/tests/components/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py
index 1ed92f34ebe..e08229631cf 100644
--- a/tests/components/tts/test_yandextts.py
+++ b/tests/components/tts/test_yandextts.py
@@ -363,3 +363,40 @@ class TestTTSYandexPlatform(object):
assert len(aioclient_mock.mock_calls) == 1
assert len(calls) == 1
+
+ def test_service_say_specified_options(self, aioclient_mock):
+ """Test service call say with options."""
+ calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
+
+ url_param = {
+ 'text': 'HomeAssistant',
+ 'lang': 'en-US',
+ 'key': '1234567xx',
+ 'speaker': 'zahar',
+ 'format': 'mp3',
+ 'emotion': 'evil',
+ 'speed': 2
+ }
+ aioclient_mock.get(
+ self._base_url, status=200, content=b'test', params=url_param)
+ config = {
+ tts.DOMAIN: {
+ 'platform': 'yandextts',
+ 'api_key': '1234567xx',
+ }
+ }
+
+ with assert_setup_component(1, tts.DOMAIN):
+ setup_component(self.hass, tts.DOMAIN, config)
+
+ self.hass.services.call(tts.DOMAIN, 'yandextts_say', {
+ tts.ATTR_MESSAGE: "HomeAssistant",
+ 'options': {
+ 'emotion': 'evil',
+ 'speed': 2,
+ }
+ })
+ self.hass.block_till_done()
+
+ assert len(aioclient_mock.mock_calls) == 1
+ assert len(calls) == 1
diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py
index 1563dd377c4..9d22b1ad0ae 100644
--- a/tests/components/weather/test_weather.py
+++ b/tests/components/weather/test_weather.py
@@ -37,7 +37,7 @@ class TestWeather(unittest.TestCase):
assert state.state == 'sunny'
data = state.attributes
- assert data.get(ATTR_WEATHER_TEMPERATURE) == 21
+ assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6
assert data.get(ATTR_WEATHER_HUMIDITY) == 92
assert data.get(ATTR_WEATHER_PRESSURE) == 1099
assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5
@@ -57,4 +57,4 @@ class TestWeather(unittest.TestCase):
assert state.state == 'rainy'
data = state.attributes
- assert data.get(ATTR_WEATHER_TEMPERATURE) == -24.4
+ assert data.get(ATTR_WEATHER_TEMPERATURE) == -24
diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py
index 1e759949a46..ce2795297a2 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -1253,3 +1253,27 @@ class TestZWaveServices(unittest.TestCase):
assert node.refresh_info.called
assert len(node.refresh_info.mock_calls) == 1
+
+ def test_heal_node(self):
+ """Test zwave heal_node service."""
+ node = MockNode(node_id=19)
+ self.zwave_network.nodes = {19: node}
+ self.hass.services.call('zwave', 'heal_node', {
+ const.ATTR_NODE_ID: 19,
+ })
+ self.hass.block_till_done()
+
+ assert node.heal.called
+ assert len(node.heal.mock_calls) == 1
+
+ def test_test_node(self):
+ """Test the zwave test_node service."""
+ node = MockNode(node_id=19)
+ self.zwave_network.nodes = {19: node}
+ self.hass.services.call('zwave', 'test_node', {
+ const.ATTR_NODE_ID: 19,
+ })
+ self.hass.block_till_done()
+
+ assert node.test.called
+ assert len(node.test.mock_calls) == 1
diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py
index 32351234ad3..e4afca31740 100644
--- a/tests/components/zwave/test_node_entity.py
+++ b/tests/components/zwave/test_node_entity.py
@@ -330,38 +330,34 @@ class TestZWaveNodeEntity(unittest.TestCase):
"""Test state property."""
self.node.is_ready = False
self.entity.node_changed()
- self.assertEqual('Dynamic', self.entity.state)
+ self.assertEqual('initializing', self.entity.state)
self.node.is_failed = True
+ self.node.query_stage = 'Complete'
self.entity.node_changed()
- self.assertEqual('Dead (Dynamic)', self.entity.state)
+ self.assertEqual('dead', self.entity.state)
self.node.is_failed = False
self.node.is_awake = False
self.entity.node_changed()
- self.assertEqual('Sleeping (Dynamic)', self.entity.state)
+ self.assertEqual('sleeping', self.entity.state)
def test_state_ready(self):
"""Test state property."""
+ self.node.query_stage = 'Complete'
self.node.is_ready = True
self.entity.node_changed()
- self.assertEqual('Ready', self.entity.state)
+ self.assertEqual('ready', self.entity.state)
self.node.is_failed = True
self.entity.node_changed()
- self.assertEqual('Dead', self.entity.state)
+ self.assertEqual('dead', self.entity.state)
self.node.is_failed = False
self.node.is_awake = False
self.entity.node_changed()
- self.assertEqual('Sleeping', self.entity.state)
+ self.assertEqual('sleeping', self.entity.state)
def test_not_polled(self):
"""Test should_poll property."""
self.assertFalse(self.entity.should_poll)
-
-
-def test_sub_status():
- """Test sub_status function."""
- assert node_entity.sub_status('Status', 'Stage') == 'Status (Stage)'
- assert node_entity.sub_status('Status', '') == 'Status'
diff --git a/tests/fixtures/vultr_account_info.json b/tests/fixtures/vultr_account_info.json
new file mode 100644
index 00000000000..beab9534fc3
--- /dev/null
+++ b/tests/fixtures/vultr_account_info.json
@@ -0,0 +1 @@
+{"balance":"-123.00","pending_charges":"3.38","last_payment_date":"2017-08-11 15:04:04","last_payment_amount":"-10.00"}
diff --git a/tests/fixtures/vultr_server_list.json b/tests/fixtures/vultr_server_list.json
new file mode 100644
index 00000000000..99955e332ec
--- /dev/null
+++ b/tests/fixtures/vultr_server_list.json
@@ -0,0 +1,122 @@
+{
+ "576965": {
+ "SUBID": "576965",
+ "os": "CentOS 6 x64",
+ "ram": "4096 MB",
+ "disk": "Virtual 60 GB",
+ "main_ip": "123.123.123.123",
+ "vcpu_count": "2",
+ "location": "New Jersey",
+ "DCID": "1",
+ "default_password": "nreqnusibni",
+ "date_created": "2013-12-19 14:45:41",
+ "pending_charges": "46.67",
+ "status": "active",
+ "cost_per_month": "10.05",
+ "current_bandwidth_gb": 131.512,
+ "allowed_bandwidth_gb": "1000",
+ "netmask_v4": "255.255.255.248",
+ "gateway_v4": "123.123.123.1",
+ "power_status": "running",
+ "server_state": "ok",
+ "VPSPLANID": "28",
+ "v6_network": "2001:DB8:1000::",
+ "v6_main_ip": "2001:DB8:1000::100",
+ "v6_network_size": "64",
+ "v6_networks": [
+ {
+ "v6_network": "2001:DB8:1000::",
+ "v6_main_ip": "2001:DB8:1000::100",
+ "v6_network_size": "64"
+ }
+ ],
+ "label": "my new server",
+ "internal_ip": "10.99.0.10",
+ "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=eawxFVZw2mXnhGUV",
+ "auto_backups": "yes",
+ "tag": "mytag",
+ "OSID": "127",
+ "APPID": "0",
+ "FIREWALLGROUPID": "0"
+ },
+ "123456": {
+ "SUBID": "123456",
+ "os": "CentOS 6 x64",
+ "ram": "4096 MB",
+ "disk": "Virtual 60 GB",
+ "main_ip": "192.168.100.50",
+ "vcpu_count": "2",
+ "location": "New Jersey",
+ "DCID": "1",
+ "default_password": "nreqnusibni",
+ "date_created": "2014-10-13 14:45:41",
+ "pending_charges": "not a number",
+ "status": "active",
+ "cost_per_month": "73.25",
+ "current_bandwidth_gb": 957.457,
+ "allowed_bandwidth_gb": "1000",
+ "netmask_v4": "255.255.255.248",
+ "gateway_v4": "123.123.123.1",
+ "power_status": "halted",
+ "server_state": "ok",
+ "VPSPLANID": "28",
+ "v6_network": "2001:DB8:1000::",
+ "v6_main_ip": "2001:DB8:1000::100",
+ "v6_network_size": "64",
+ "v6_networks": [
+ {
+ "v6_network": "2001:DB8:1000::",
+ "v6_main_ip": "2001:DB8:1000::100",
+ "v6_network_size": "64"
+ }
+ ],
+ "label": "my failed server",
+ "internal_ip": "10.99.0.10",
+ "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=eawxFVZw2mXnhGUV",
+ "auto_backups": "no",
+ "tag": "mytag",
+ "OSID": "127",
+ "APPID": "0",
+ "FIREWALLGROUPID": "0"
+ },
+ "555555": {
+ "SUBID": "555555",
+ "os": "CentOS 7 x64",
+ "ram": "1024 MB",
+ "disk": "Virtual 30 GB",
+ "main_ip": "192.168.250.50",
+ "vcpu_count": "1",
+ "location": "London",
+ "DCID": "7",
+ "default_password": "password",
+ "date_created": "2014-10-15 14:45:41",
+ "pending_charges": "5.45",
+ "status": "active",
+ "cost_per_month": "73.25",
+ "current_bandwidth_gb": 57.457,
+ "allowed_bandwidth_gb": "100",
+ "netmask_v4": "255.255.255.248",
+ "gateway_v4": "123.123.123.1",
+ "power_status": "halted",
+ "server_state": "ok",
+ "VPSPLANID": "28",
+ "v6_network": "2001:DB8:1000::",
+ "v6_main_ip": "2001:DB8:1000::100",
+ "v6_network_size": "64",
+ "v6_networks": [
+ {
+ "v6_network": "2001:DB8:1000::",
+ "v6_main_ip": "2001:DB8:1000::100",
+ "v6_network_size": "64"
+ }
+ ],
+ "label": "Another Server",
+ "internal_ip": "10.99.0.10",
+ "kvm_url": "https://my.vultr.com/subs/novnc/api.php?data=eawxFVZw2mXnhGUV",
+ "auto_backups": "no",
+ "tag": "mytag",
+ "OSID": "127",
+ "APPID": "0",
+ "FIREWALLGROUPID": "0"
+ }
+}
diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py
index d7f518f489e..a4c8b03daa0 100644
--- a/tests/helpers/test_entity.py
+++ b/tests/helpers/test_entity.py
@@ -100,22 +100,6 @@ class TestHelpersEntity(object):
fmt, 'overwrite hidden true',
hass=self.hass) == 'test.overwrite_hidden_true_2'
- def test_update_calls_async_update_if_available(self):
- """Test async update getting called."""
- async_update = []
-
- class AsyncEntity(entity.Entity):
- hass = self.hass
- entity_id = 'sensor.test'
-
- @asyncio.coroutine
- def async_update(self):
- async_update.append([1])
-
- ent = AsyncEntity()
- ent.update()
- assert len(async_update) == 1
-
def test_device_class(self):
"""Test device class attribute."""
state = self.hass.states.get(self.entity.entity_id)
diff --git a/tests/helpers/test_temperature.py b/tests/helpers/test_temperature.py
new file mode 100644
index 00000000000..96e7bd6c74f
--- /dev/null
+++ b/tests/helpers/test_temperature.py
@@ -0,0 +1,49 @@
+"""Tests Home Assistant temperature helpers."""
+import unittest
+
+from tests.common import get_test_home_assistant
+
+from homeassistant.const import (
+ TEMP_CELSIUS, PRECISION_WHOLE, TEMP_FAHRENHEIT, PRECISION_HALVES,
+ PRECISION_TENTHS)
+from homeassistant.helpers.temperature import display_temp
+from homeassistant.util.unit_system import METRIC_SYSTEM
+
+TEMP = 24.636626
+
+
+class TestHelpersTemperature(unittest.TestCase):
+ """Setup the temperature tests."""
+
+ def setUp(self):
+ """Setup the tests."""
+ self.hass = get_test_home_assistant()
+ self.hass.config.unit_system = METRIC_SYSTEM
+
+ def tearDown(self):
+ """Stop down stuff we started."""
+ self.hass.stop()
+
+ def test_temperature_not_a_number(self):
+ """Test that temperature is a number."""
+ temp = "Temperature"
+ with self.assertRaises(Exception) as context:
+ display_temp(self.hass, temp, TEMP_CELSIUS, PRECISION_HALVES)
+
+ self.assertTrue("Temperature is not a number: {}".format(temp)
+ in str(context.exception))
+
+ def test_celsius_halves(self):
+ """Test temperature to celsius rounding to halves."""
+ self.assertEqual(24.5, display_temp(
+ self.hass, TEMP, TEMP_CELSIUS, PRECISION_HALVES))
+
+ def test_celsius_tenths(self):
+ """Test temperature to celsius rounding to tenths."""
+ self.assertEqual(24.6, display_temp(
+ self.hass, TEMP, TEMP_CELSIUS, PRECISION_TENTHS))
+
+ def test_fahrenheit_wholes(self):
+ """Test temperature to fahrenheit rounding to wholes."""
+ self.assertEqual(-4, display_temp(
+ self.hass, TEMP, TEMP_FAHRENHEIT, PRECISION_WHOLE))
diff --git a/tests/test_core.py b/tests/test_core.py
index c3fea749f5d..09ddf721628 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -495,18 +495,6 @@ class TestStateMachine(unittest.TestCase):
self.assertFalse(self.states.is_state('light.Bowl', 'off'))
self.assertFalse(self.states.is_state('light.Non_existing', 'on'))
- def test_is_state_attr(self):
- """Test is_state_attr method."""
- self.states.set("light.Bowl", "on", {"brightness": 100})
- self.assertTrue(
- self.states.is_state_attr('light.Bowl', 'brightness', 100))
- self.assertFalse(
- self.states.is_state_attr('light.Bowl', 'friendly_name', 200))
- self.assertFalse(
- self.states.is_state_attr('light.Bowl', 'friendly_name', 'Bowl'))
- self.assertFalse(
- self.states.is_state_attr('light.Non_existing', 'brightness', 100))
-
def test_entity_ids(self):
"""Test get_entity_ids method."""
ent_ids = self.states.entity_ids()
diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py
index 50e271008a2..38b957ad102 100644
--- a/tests/util/test_yaml.py
+++ b/tests/util/test_yaml.py
@@ -267,6 +267,10 @@ class TestYaml(unittest.TestCase):
"""The that the dump method returns empty None values."""
assert yaml.dump({'a': None, 'b': 'b'}) == 'a:\nb: b\n'
+ def test_dump_unicode(self):
+ """The that the dump method returns empty None values."""
+ assert yaml.dump({'a': None, 'b': 'привет'}) == 'a:\nb: привет\n'
+
FILES = {}