diff --git a/.coveragerc b/.coveragerc
index 4b19519038f..bd99e3ac2e2 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -29,6 +29,9 @@ omit =
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py
+ homeassistant/components/bmw_connected_drive.py
+ homeassistant/components/*/bmw_connected_drive.py
+
homeassistant/components/android_ip_webcam.py
homeassistant/components/*/android_ip_webcam.py
@@ -38,6 +41,9 @@ omit =
homeassistant/components/asterisk_mbox.py
homeassistant/components/*/asterisk_mbox.py
+ homeassistant/components/august.py
+ homeassistant/components/*/august.py
+
homeassistant/components/axis.py
homeassistant/components/*/axis.py
@@ -205,6 +211,9 @@ omit =
homeassistant/components/skybell.py
homeassistant/components/*/skybell.py
+ homeassistant/components/smappee.py
+ homeassistant/components/*/smappee.py
+
homeassistant/components/tado.py
homeassistant/components/*/tado.py
@@ -462,6 +471,7 @@ omit =
homeassistant/components/media_player/vizio.py
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py
+ homeassistant/components/media_player/xiaomi_tv.py
homeassistant/components/media_player/yamaha.py
homeassistant/components/media_player/yamaha_musiccast.py
homeassistant/components/media_player/ziggo_mediabox_xl.py
@@ -551,8 +561,10 @@ omit =
homeassistant/components/sensor/etherscan.py
homeassistant/components/sensor/fastdotcom.py
homeassistant/components/sensor/fedex.py
+ homeassistant/components/sensor/filesize.py
homeassistant/components/sensor/fitbit.py
homeassistant/components/sensor/fixer.py
+ homeassistant/components/sensor/folder.py
homeassistant/components/sensor/fritzbox_callmonitor.py
homeassistant/components/sensor/fritzbox_netmonitor.py
homeassistant/components/sensor/gearbest.py
@@ -617,6 +629,7 @@ omit =
homeassistant/components/sensor/sochain.py
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py
+ homeassistant/components/sensor/spotcrime.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
diff --git a/.gitattributes b/.gitattributes
index 214efef6e4d..caff2fc5c1f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,3 +1,10 @@
# Ensure Docker script files uses LF to support Docker for Windows.
-setup_docker_prereqs eol=lf
-/virtualization/Docker/scripts/* eol=lf
\ No newline at end of file
+# Ensure "git config --global core.autocrlf input" before you clone
+* text eol=lf
+*.py whitespace=error
+
+*.ico binary
+*.jpg binary
+*.png binary
+*.zip binary
+*.mp3 binary
diff --git a/CODEOWNERS b/CODEOWNERS
old mode 100644
new mode 100755
index 6e088a84e5d..a5b5cfcb32c
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -43,6 +43,7 @@ homeassistant/components/hassio.py @home-assistant/hassio
# Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
+homeassistant/components/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50
homeassistant/components/climate/eq3btsmart.py @rytilahti
@@ -54,7 +55,10 @@ homeassistant/components/history_graph.py @andrey-git
homeassistant/components/light/tplink.py @rytilahti
homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/media_player/kodi.py @armills
+homeassistant/components/media_player/mediaroom.py @dgomes
homeassistant/components/media_player/monoprice.py @etsinko
+homeassistant/components/media_player/sonos.py @amelchio
+homeassistant/components/media_player/xiaomi_tv.py @fattdev
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
homeassistant/components/plant.py @ChristianKuehnel
homeassistant/components/sensor/airvisual.py @bachya
@@ -63,6 +67,7 @@ homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/pollen.py @bachya
homeassistant/components/sensor/sytadin.py @gautric
+homeassistant/components/sensor/sql.py @dgomes
homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git
homeassistant/components/switch/rainmachine.py @bachya
@@ -70,9 +75,11 @@ homeassistant/components/switch/tplink.py @rytilahti
homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/axis.py @kane610
+homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
homeassistant/components/*/broadlink.py @danielhiversen
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
+homeassistant/components/homekit/* @cdce8p
homeassistant/components/*/deconz.py @kane610
homeassistant/components/*/rfxtrx.py @danielhiversen
homeassistant/components/velux.py @Julius2342
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 595c15717eb..b5428ede8fa 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -22,10 +22,23 @@ import os
import inspect
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)
+
+PROJECT_NAME = 'Home Assistant'
+PROJECT_PACKAGE_NAME = 'homeassistant'
+PROJECT_AUTHOR = 'The Home Assistant Authors'
+PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR)
+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_GITHUB_USERNAME = 'home-assistant'
+PROJECT_GITHUB_REPOSITORY = 'home-assistant'
+
+GITHUB_PATH = '{}/{}'.format(
+ PROJECT_GITHUB_USERNAME, PROJECT_GITHUB_REPOSITORY)
+GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
+
sys.path.insert(0, os.path.abspath('_ext'))
sys.path.insert(0, os.path.abspath('../homeassistant'))
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index 64ad88f8c8b..4971cbccc9c 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -12,7 +12,8 @@ from typing import Any, Optional, Dict
import voluptuous as vol
from homeassistant import (
- core, config as conf_util, loader, components as core_components)
+ core, config as conf_util, config_entries, loader,
+ components as core_components)
from homeassistant.components import persistent_notification
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.setup import async_setup_component
@@ -35,13 +36,13 @@ FIRST_INIT_COMPONENT = set((
def from_config_dict(config: Dict[str, Any],
- hass: Optional[core.HomeAssistant]=None,
- config_dir: Optional[str]=None,
- enable_log: bool=True,
- verbose: bool=False,
- skip_pip: bool=False,
- log_rotate_days: Any=None,
- log_file: Any=None) \
+ hass: Optional[core.HomeAssistant] = None,
+ config_dir: Optional[str] = None,
+ enable_log: bool = True,
+ verbose: bool = False,
+ skip_pip: bool = False,
+ log_rotate_days: Any = None,
+ log_file: Any = None) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@@ -68,12 +69,12 @@ def from_config_dict(config: Dict[str, Any],
@asyncio.coroutine
def async_from_config_dict(config: Dict[str, Any],
hass: core.HomeAssistant,
- config_dir: Optional[str]=None,
- enable_log: bool=True,
- verbose: bool=False,
- skip_pip: bool=False,
- log_rotate_days: Any=None,
- log_file: Any=None) \
+ config_dir: Optional[str] = None,
+ enable_log: bool = True,
+ verbose: bool = False,
+ skip_pip: bool = False,
+ log_rotate_days: Any = None,
+ log_file: Any = None) \
-> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
@@ -123,9 +124,13 @@ def async_from_config_dict(config: Dict[str, Any],
new_config[key] = value or {}
config = new_config
+ hass.config_entries = config_entries.ConfigEntries(hass, config)
+ yield from hass.config_entries.async_load()
+
# Filter out the repeating and common config section [homeassistant]
components = set(key.split(' ')[0] for key in config.keys()
if key != core.DOMAIN)
+ components.update(hass.config_entries.async_domains())
# setup components
# pylint: disable=not-an-iterable
@@ -163,11 +168,11 @@ def async_from_config_dict(config: Dict[str, Any],
def from_config_file(config_path: str,
- hass: Optional[core.HomeAssistant]=None,
- verbose: bool=False,
- skip_pip: bool=True,
- log_rotate_days: Any=None,
- log_file: Any=None):
+ hass: Optional[core.HomeAssistant] = None,
+ verbose: bool = False,
+ skip_pip: bool = True,
+ log_rotate_days: Any = None,
+ log_file: Any = None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter if given,
@@ -188,10 +193,10 @@ def from_config_file(config_path: str,
@asyncio.coroutine
def async_from_config_file(config_path: str,
hass: core.HomeAssistant,
- verbose: bool=False,
- skip_pip: bool=True,
- log_rotate_days: Any=None,
- log_file: Any=None):
+ verbose: bool = False,
+ skip_pip: bool = True,
+ log_rotate_days: Any = None,
+ log_file: Any = None):
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@@ -219,7 +224,7 @@ def async_from_config_file(config_path: str,
@core.callback
-def async_enable_logging(hass: core.HomeAssistant, verbose: bool=False,
+def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False,
log_rotate_days=None, log_file=None) -> None:
"""Set up the logging.
diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py
index a1c6811afe7..6b306adad5b 100644
--- a/homeassistant/components/__init__.py
+++ b/homeassistant/components/__init__.py
@@ -15,6 +15,7 @@ import homeassistant.core as ha
import homeassistant.config as conf_util
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service import extract_entity_ids
+from homeassistant.helpers import intent
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
@@ -154,6 +155,12 @@ def async_setup(hass, config):
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
hass.services.async_register(
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
+ hass.helpers.intent.async_register(intent.ServiceIntentHandler(
+ intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned on {}"))
+ hass.helpers.intent.async_register(intent.ServiceIntentHandler(
+ intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned off {}"))
+ hass.helpers.intent.async_register(intent.ServiceIntentHandler(
+ intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
@asyncio.coroutine
def async_handle_core_service(call):
diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py
index cbfee2ae215..fde21a265b0 100644
--- a/homeassistant/components/abode.py
+++ b/homeassistant/components/abode.py
@@ -7,6 +7,7 @@ https://home-assistant.io/components/abode/
import asyncio
import logging
from functools import partial
+from requests.exceptions import HTTPError, ConnectTimeout
import voluptuous as vol
@@ -17,7 +18,6 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-from requests.exceptions import HTTPError, ConnectTimeout
REQUIREMENTS = ['abodepy==0.12.2']
diff --git a/homeassistant/components/alarm_control_panel/canary.py b/homeassistant/components/alarm_control_panel/canary.py
index fb5c4c37e8d..2e0e9994e10 100644
--- a/homeassistant/components/alarm_control_panel/canary.py
+++ b/homeassistant/components/alarm_control_panel/canary.py
@@ -59,8 +59,7 @@ class CanaryAlarm(AlarmControlPanel):
return STATE_ALARM_ARMED_HOME
elif mode.name == LOCATION_MODE_NIGHT:
return STATE_ALARM_ARMED_NIGHT
- else:
- return None
+ return None
@property
def device_state_attributes(self):
diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py
index 35b255d4b57..5beb5261607 100644
--- a/homeassistant/components/alarm_control_panel/manual.py
+++ b/homeassistant/components/alarm_control_panel/manual.py
@@ -172,9 +172,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
- else:
- self._state = self._previous_state
- return self._state
+ self._state = self._previous_state
+ return self._state
if self._state in SUPPORTED_PENDING_STATES and \
self._within_pending_time(self._state):
@@ -187,8 +186,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
"""Get the current state."""
if self.state == STATE_ALARM_PENDING:
return self._previous_state
- else:
- return self._state
+ return self._state
def _pending_time(self, state):
"""Get the pending time."""
diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py
index ef12cbe365f..4b08ad67292 100644
--- a/homeassistant/components/alarm_control_panel/manual_mqtt.py
+++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py
@@ -208,9 +208,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
trigger_time) < dt_util.utcnow():
if self._disarm_after_trigger:
return STATE_ALARM_DISARMED
- else:
- self._state = self._previous_state
- return self._state
+ self._state = self._previous_state
+ return self._state
if self._state in SUPPORTED_PENDING_STATES and \
self._within_pending_time(self._state):
@@ -223,8 +222,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
"""Get the current state."""
if self.state == STATE_ALARM_PENDING:
return self._previous_state
- else:
- return self._state
+ return self._state
def _pending_time(self, state):
"""Get the pending time."""
diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml
index bfd38c902d0..72784c8178c 100644
--- a/homeassistant/components/alarm_control_panel/services.yaml
+++ b/homeassistant/components/alarm_control_panel/services.yaml
@@ -1,71 +1,71 @@
-# Describes the format for available alarm control panel services
-
-alarm_disarm:
- description: Send the alarm the command for disarm.
- fields:
- entity_id:
- description: Name of alarm control panel to disarm.
- example: 'alarm_control_panel.downstairs'
- code:
- description: An optional code to disarm the alarm control panel with.
- example: 1234
-
-alarm_arm_home:
- description: Send the alarm the command for arm home.
- fields:
- entity_id:
- description: Name of alarm control panel to arm home.
- example: 'alarm_control_panel.downstairs'
- code:
- description: An optional code to arm home the alarm control panel with.
- example: 1234
-
-alarm_arm_away:
- description: Send the alarm the command for arm away.
- fields:
- entity_id:
- description: Name of alarm control panel to arm away.
- example: 'alarm_control_panel.downstairs'
- code:
- description: An optional code to arm away the alarm control panel with.
- example: 1234
-
-alarm_arm_night:
- description: Send the alarm the command for arm night.
- fields:
- entity_id:
- description: Name of alarm control panel to arm night.
- example: 'alarm_control_panel.downstairs'
- code:
- description: An optional code to arm night the alarm control panel with.
- example: 1234
-
-alarm_trigger:
- description: Send the alarm the command for trigger.
- fields:
- entity_id:
- description: Name of alarm control panel to trigger.
- example: 'alarm_control_panel.downstairs'
- code:
- description: An optional code to trigger the alarm control panel with.
- example: 1234
-
-envisalink_alarm_keypress:
- description: Send custom keypresses to the alarm.
- fields:
- entity_id:
- description: Name of the alarm control panel to trigger.
- example: 'alarm_control_panel.downstairs'
- keypress:
- description: 'String to send to the alarm panel (1-6 characters).'
- example: '*71'
-
-alarmdecoder_alarm_toggle_chime:
- description: Send the alarm the toggle chime command.
- fields:
- entity_id:
- description: Name of the alarm control panel to trigger.
- example: 'alarm_control_panel.downstairs'
- code:
- description: A required code to toggle the alarm control panel chime with.
- example: 1234
+# Describes the format for available alarm control panel services
+
+alarm_disarm:
+ description: Send the alarm the command for disarm.
+ fields:
+ entity_id:
+ description: Name of alarm control panel to disarm.
+ example: 'alarm_control_panel.downstairs'
+ code:
+ description: An optional code to disarm the alarm control panel with.
+ example: 1234
+
+alarm_arm_home:
+ description: Send the alarm the command for arm home.
+ fields:
+ entity_id:
+ description: Name of alarm control panel to arm home.
+ example: 'alarm_control_panel.downstairs'
+ code:
+ description: An optional code to arm home the alarm control panel with.
+ example: 1234
+
+alarm_arm_away:
+ description: Send the alarm the command for arm away.
+ fields:
+ entity_id:
+ description: Name of alarm control panel to arm away.
+ example: 'alarm_control_panel.downstairs'
+ code:
+ description: An optional code to arm away the alarm control panel with.
+ example: 1234
+
+alarm_arm_night:
+ description: Send the alarm the command for arm night.
+ fields:
+ entity_id:
+ description: Name of alarm control panel to arm night.
+ example: 'alarm_control_panel.downstairs'
+ code:
+ description: An optional code to arm night the alarm control panel with.
+ example: 1234
+
+alarm_trigger:
+ description: Send the alarm the command for trigger.
+ fields:
+ entity_id:
+ description: Name of alarm control panel to trigger.
+ example: 'alarm_control_panel.downstairs'
+ code:
+ description: An optional code to trigger the alarm control panel with.
+ example: 1234
+
+envisalink_alarm_keypress:
+ description: Send custom keypresses to the alarm.
+ fields:
+ entity_id:
+ description: Name of the alarm control panel to trigger.
+ example: 'alarm_control_panel.downstairs'
+ keypress:
+ description: 'String to send to the alarm panel (1-6 characters).'
+ example: '*71'
+
+alarmdecoder_alarm_toggle_chime:
+ description: Send the alarm the toggle chime command.
+ fields:
+ entity_id:
+ description: Name of the alarm control panel to trigger.
+ example: 'alarm_control_panel.downstairs'
+ code:
+ description: A required code to toggle the alarm control panel chime with.
+ example: 1234
diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py
index eb941e22877..9d47e4bd322 100644
--- a/homeassistant/components/alert.py
+++ b/homeassistant/components/alert.py
@@ -34,7 +34,7 @@ DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_DONE_MESSAGE, default=None): cv.string,
+ vol.Optional(CONF_DONE_MESSAGE): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
@@ -121,7 +121,7 @@ def async_setup(hass, config):
# Setup alerts
for entity_id, alert in alerts.items():
entity = Alert(hass, entity_id,
- alert[CONF_NAME], alert[CONF_DONE_MESSAGE],
+ alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE),
alert[CONF_ENTITY_ID], alert[CONF_STATE],
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py
index b683f5cfc7c..d120270650f 100644
--- a/homeassistant/components/alexa/__init__.py
+++ b/homeassistant/components/alexa/__init__.py
@@ -31,10 +31,7 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({
})
SMART_HOME_SCHEMA = vol.Schema({
- vol.Optional(
- CONF_FILTER,
- default=lambda: entityfilter.generate_filter([], [], [], [])
- ): entityfilter.FILTER_SCHEMA,
+ vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
})
diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py
index a4f0225d22d..0d325534266 100644
--- a/homeassistant/components/alexa/smart_home.py
+++ b/homeassistant/components/alexa/smart_home.py
@@ -391,6 +391,7 @@ class _AlexaTemperatureSensor(_AlexaInterface):
@ENTITY_ADAPTERS.register(alert.DOMAIN)
@ENTITY_ADAPTERS.register(automation.DOMAIN)
+@ENTITY_ADAPTERS.register(group.DOMAIN)
@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
class _GenericCapabilities(_AlexaEntity):
"""A generic, on/off device.
@@ -521,16 +522,6 @@ class _ScriptCapabilities(_AlexaEntity):
supports_deactivation=can_cancel)]
-@ENTITY_ADAPTERS.register(group.DOMAIN)
-class _GroupCapabilities(_AlexaEntity):
- def default_display_categories(self):
- return [_DisplayCategory.SCENE_TRIGGER]
-
- def interfaces(self):
- return [_AlexaSceneController(self.entity,
- supports_deactivation=True)]
-
-
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
class _SensorCapabilities(_AlexaEntity):
def default_display_categories(self):
@@ -773,6 +764,8 @@ def extract_entity(funct):
def async_api_turn_on(hass, config, request, entity):
"""Process a turn on request."""
domain = entity.domain
+ if entity.domain == group.DOMAIN:
+ domain = ha.DOMAIN
service = SERVICE_TURN_ON
if entity.domain == cover.DOMAIN:
@@ -928,10 +921,7 @@ def async_api_increase_color_temp(hass, config, request, entity):
@asyncio.coroutine
def async_api_activate(hass, config, request, entity):
"""Process an activate request."""
- if entity.domain == group.DOMAIN:
- domain = ha.DOMAIN
- else:
- domain = entity.domain
+ domain = entity.domain
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
@@ -955,10 +945,7 @@ def async_api_activate(hass, config, request, entity):
@asyncio.coroutine
def async_api_deactivate(hass, config, request, entity):
"""Process a deactivate request."""
- if entity.domain == group.DOMAIN:
- domain = ha.DOMAIN
- else:
- domain = entity.domain
+ domain = entity.domain
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id
@@ -1178,20 +1165,24 @@ def async_api_adjust_volume(hass, config, request, entity):
@asyncio.coroutine
def async_api_adjust_volume_step(hass, config, request, entity):
"""Process an adjust volume step request."""
- volume_step = round(float(request[API_PAYLOAD]['volumeSteps'] / 100), 2)
-
- current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
-
- volume = current_level + volume_step
+ # media_player volume up/down service does not support specifying steps
+ # each component handles it differently e.g. via config.
+ # For now we use the volumeSteps returned to figure out if we
+ # should step up/down
+ volume_step = request[API_PAYLOAD]['volumeSteps']
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=False)
+ if volume_step > 0:
+ yield from hass.services.async_call(
+ entity.domain, media_player.SERVICE_VOLUME_UP,
+ data, blocking=False)
+ elif volume_step < 0:
+ yield from hass.services.async_call(
+ entity.domain, media_player.SERVICE_VOLUME_DOWN,
+ data, blocking=False)
return api_message(request)
diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py
index 9205846462f..b91f1fae565 100644
--- a/homeassistant/components/amcrest.py
+++ b/homeassistant/components/amcrest.py
@@ -79,7 +79,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
- vol.Optional(CONF_SENSORS, default=None):
+ vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
})])
}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py
index 5fbd5a764e9..13fa64438d3 100644
--- a/homeassistant/components/android_ip_webcam.py
+++ b/homeassistant/components/android_ip_webcam.py
@@ -140,11 +140,11 @@ CONFIG_SCHEMA = vol.Schema({
cv.time_period,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
- vol.Optional(CONF_SWITCHES, default=None):
+ vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
- vol.Optional(CONF_SENSORS, default=None):
+ vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
- vol.Optional(CONF_MOTION_SENSOR, default=None): cv.boolean,
+ vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA)
@@ -165,9 +165,9 @@ def async_setup(hass, config):
password = cam_config.get(CONF_PASSWORD)
name = cam_config[CONF_NAME]
interval = cam_config[CONF_SCAN_INTERVAL]
- switches = cam_config[CONF_SWITCHES]
- sensors = cam_config[CONF_SENSORS]
- motion = cam_config[CONF_MOTION_SENSOR]
+ switches = cam_config.get(CONF_SWITCHES)
+ sensors = cam_config.get(CONF_SENSORS)
+ motion = cam_config.get(CONF_MOTION_SENSOR)
# Init ip webcam
cam = PyDroidIPCam(
diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py
index 230b0ea8a1b..a9bd5c9c8bc 100644
--- a/homeassistant/components/apple_tv.py
+++ b/homeassistant/components/apple_tv.py
@@ -60,7 +60,7 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(ensure_list, [vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_LOGIN_ID): cv.string,
- vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
+ vol.Optional(CONF_CREDENTIALS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
})])
diff --git a/homeassistant/components/august.py b/homeassistant/components/august.py
new file mode 100644
index 00000000000..c12e18ef09c
--- /dev/null
+++ b/homeassistant/components/august.py
@@ -0,0 +1,257 @@
+"""
+Support for August devices.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/august/
+"""
+
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+from requests import RequestException
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (
+ CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT)
+from homeassistant.helpers import discovery
+from homeassistant.util import Throttle
+
+_LOGGER = logging.getLogger(__name__)
+
+_CONFIGURING = {}
+
+REQUIREMENTS = ['py-august==0.3.0']
+
+DEFAULT_TIMEOUT = 10
+ACTIVITY_FETCH_LIMIT = 10
+ACTIVITY_INITIAL_FETCH_LIMIT = 20
+
+CONF_LOGIN_METHOD = 'login_method'
+CONF_INSTALL_ID = 'install_id'
+
+NOTIFICATION_ID = 'august_notification'
+NOTIFICATION_TITLE = "August Setup"
+
+AUGUST_CONFIG_FILE = '.august.conf'
+
+DATA_AUGUST = 'august'
+DOMAIN = 'august'
+DEFAULT_ENTITY_NAMESPACE = 'august'
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
+DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
+LOGIN_METHODS = ['phone', 'email']
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_INSTALL_ID): cv.string,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+AUGUST_COMPONENTS = [
+ 'camera', 'binary_sensor', 'lock'
+]
+
+
+def request_configuration(hass, config, api, authenticator):
+ """Request configuration steps from the user."""
+ configurator = hass.components.configurator
+
+ def august_configuration_callback(data):
+ """Run when the configuration callback is called."""
+ from august.authenticator import ValidationResult
+
+ result = authenticator.validate_verification_code(
+ data.get('verification_code'))
+
+ if result == ValidationResult.INVALID_VERIFICATION_CODE:
+ configurator.notify_errors(_CONFIGURING[DOMAIN],
+ "Invalid verification code")
+ elif result == ValidationResult.VALIDATED:
+ setup_august(hass, config, api, authenticator)
+
+ if DOMAIN not in _CONFIGURING:
+ authenticator.send_verification_code()
+
+ conf = config[DOMAIN]
+ username = conf.get(CONF_USERNAME)
+ login_method = conf.get(CONF_LOGIN_METHOD)
+
+ _CONFIGURING[DOMAIN] = configurator.request_config(
+ NOTIFICATION_TITLE,
+ august_configuration_callback,
+ description="Please check your {} ({}) and enter the verification "
+ "code below".format(login_method, username),
+ submit_caption='Verify',
+ fields=[{
+ 'id': 'verification_code',
+ 'name': "Verification code",
+ 'type': 'string'}]
+ )
+
+
+def setup_august(hass, config, api, authenticator):
+ """Set up the August component."""
+ from august.authenticator import AuthenticationState
+
+ authentication = None
+ try:
+ authentication = authenticator.authenticate()
+ except RequestException as ex:
+ _LOGGER.error("Unable to connect to August service: %s", str(ex))
+
+ hass.components.persistent_notification.create(
+ "Error: {}
"
+ "You will need to restart hass after fixing."
+ "".format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+
+ state = authentication.state
+
+ if state == AuthenticationState.AUTHENTICATED:
+ if DOMAIN in _CONFIGURING:
+ hass.components.configurator.request_done(_CONFIGURING.pop(DOMAIN))
+
+ hass.data[DATA_AUGUST] = AugustData(api, authentication.access_token)
+
+ for component in AUGUST_COMPONENTS:
+ discovery.load_platform(hass, component, DOMAIN, {}, config)
+
+ return True
+ elif state == AuthenticationState.BAD_PASSWORD:
+ return False
+ elif state == AuthenticationState.REQUIRES_VALIDATION:
+ request_configuration(hass, config, api, authenticator)
+ return True
+
+ return False
+
+
+def setup(hass, config):
+ """Set up the August component."""
+ from august.api import Api
+ from august.authenticator import Authenticator
+
+ conf = config[DOMAIN]
+ api = Api(timeout=conf.get(CONF_TIMEOUT))
+
+ authenticator = Authenticator(
+ api,
+ conf.get(CONF_LOGIN_METHOD),
+ conf.get(CONF_USERNAME),
+ conf.get(CONF_PASSWORD),
+ install_id=conf.get(CONF_INSTALL_ID),
+ access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))
+
+ return setup_august(hass, config, api, authenticator)
+
+
+class AugustData:
+ """August data object."""
+
+ def __init__(self, api, access_token):
+ """Init August data object."""
+ self._api = api
+ self._access_token = access_token
+ self._doorbells = self._api.get_doorbells(self._access_token) or []
+ self._locks = self._api.get_locks(self._access_token) or []
+ self._house_ids = [d.house_id for d in self._doorbells + self._locks]
+
+ self._doorbell_detail_by_id = {}
+ self._lock_status_by_id = {}
+ self._lock_detail_by_id = {}
+ self._activities_by_id = {}
+
+ @property
+ def house_ids(self):
+ """Return a list of house_ids."""
+ return self._house_ids
+
+ @property
+ def doorbells(self):
+ """Return a list of doorbells."""
+ return self._doorbells
+
+ @property
+ def locks(self):
+ """Return a list of locks."""
+ return self._locks
+
+ def get_device_activities(self, device_id, *activity_types):
+ """Return a list of activities."""
+ self._update_device_activities()
+
+ activities = self._activities_by_id.get(device_id, [])
+ if activity_types:
+ return [a for a in activities if a.activity_type in activity_types]
+ return activities
+
+ def get_latest_device_activity(self, device_id, *activity_types):
+ """Return latest activity."""
+ activities = self.get_device_activities(device_id, *activity_types)
+ return next(iter(activities or []), None)
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
+ """Update data object with latest from August API."""
+ for house_id in self.house_ids:
+ activities = self._api.get_house_activities(self._access_token,
+ house_id,
+ limit=limit)
+
+ device_ids = {a.device_id for a in activities}
+ for device_id in device_ids:
+ self._activities_by_id[device_id] = [a for a in activities if
+ a.device_id == device_id]
+
+ def get_doorbell_detail(self, doorbell_id):
+ """Return doorbell detail."""
+ self._update_doorbells()
+ return self._doorbell_detail_by_id.get(doorbell_id)
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def _update_doorbells(self):
+ detail_by_id = {}
+
+ for doorbell in self._doorbells:
+ detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
+ self._access_token, doorbell.device_id)
+
+ self._doorbell_detail_by_id = detail_by_id
+
+ def get_lock_status(self, lock_id):
+ """Return lock status."""
+ self._update_locks()
+ return self._lock_status_by_id.get(lock_id)
+
+ def get_lock_detail(self, lock_id):
+ """Return lock detail."""
+ self._update_locks()
+ return self._lock_detail_by_id.get(lock_id)
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def _update_locks(self):
+ status_by_id = {}
+ detail_by_id = {}
+
+ for lock in self._locks:
+ status_by_id[lock.device_id] = self._api.get_lock_status(
+ self._access_token, lock.device_id)
+ detail_by_id[lock.device_id] = self._api.get_lock_detail(
+ self._access_token, lock.device_id)
+
+ self._lock_status_by_id = status_by_id
+ self._lock_detail_by_id = detail_by_id
+
+ def lock(self, device_id):
+ """Lock the device."""
+ return self._api.lock(self._access_token, device_id)
+
+ def unlock(self, device_id):
+ """Unlock the device."""
+ return self._api.unlock(self._access_token, device_id)
diff --git a/homeassistant/components/binary_sensor/august.py b/homeassistant/components/binary_sensor/august.py
new file mode 100644
index 00000000000..8df50a1bfb6
--- /dev/null
+++ b/homeassistant/components/binary_sensor/august.py
@@ -0,0 +1,97 @@
+"""
+Support for August binary sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.august/
+"""
+from datetime import timedelta, datetime
+
+from homeassistant.components.august import DATA_AUGUST
+from homeassistant.components.binary_sensor import (BinarySensorDevice)
+
+DEPENDENCIES = ['august']
+
+SCAN_INTERVAL = timedelta(seconds=5)
+
+
+def _retrieve_online_state(data, doorbell):
+ """Get the latest state of the sensor."""
+ detail = data.get_doorbell_detail(doorbell.device_id)
+ return detail.is_online
+
+
+def _retrieve_motion_state(data, doorbell):
+ from august.activity import ActivityType
+ return _activity_time_based_state(data, doorbell,
+ [ActivityType.DOORBELL_MOTION,
+ ActivityType.DOORBELL_DING])
+
+
+def _retrieve_ding_state(data, doorbell):
+ from august.activity import ActivityType
+ return _activity_time_based_state(data, doorbell,
+ [ActivityType.DOORBELL_DING])
+
+
+def _activity_time_based_state(data, doorbell, activity_types):
+ """Get the latest state of the sensor."""
+ latest = data.get_latest_device_activity(doorbell.device_id,
+ *activity_types)
+
+ if latest is not None:
+ start = latest.activity_start_time
+ end = latest.activity_end_time + timedelta(seconds=30)
+ return start <= datetime.now() <= end
+ return None
+
+
+# Sensor types: Name, device_class, state_provider
+SENSOR_TYPES = {
+ 'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
+ 'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
+ 'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
+}
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the August binary sensors."""
+ data = hass.data[DATA_AUGUST]
+ devices = []
+
+ for doorbell in data.doorbells:
+ for sensor_type in SENSOR_TYPES:
+ devices.append(AugustBinarySensor(data, sensor_type, doorbell))
+
+ add_devices(devices, True)
+
+
+class AugustBinarySensor(BinarySensorDevice):
+ """Representation of an August binary sensor."""
+
+ def __init__(self, data, sensor_type, doorbell):
+ """Initialize the sensor."""
+ self._data = data
+ self._sensor_type = sensor_type
+ self._doorbell = doorbell
+ self._state = None
+
+ @property
+ def is_on(self):
+ """Return true if the binary sensor is on."""
+ return self._state
+
+ @property
+ def device_class(self):
+ """Return the class of this device, from component DEVICE_CLASSES."""
+ return SENSOR_TYPES[self._sensor_type][1]
+
+ @property
+ def name(self):
+ """Return the name of the binary sensor."""
+ return "{} {}".format(self._doorbell.device_name,
+ SENSOR_TYPES[self._sensor_type][0])
+
+ def update(self):
+ """Get the latest state of the sensor."""
+ state_provider = SENSOR_TYPES[self._sensor_type][2]
+ self._state = state_provider(self._data, self._doorbell)
diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py
index 1d0849b255e..53f148fe97f 100644
--- a/homeassistant/components/binary_sensor/bloomsky.py
+++ b/homeassistant/components/binary_sensor/bloomsky.py
@@ -24,7 +24,7 @@ SENSOR_TYPES = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES):
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py
index 0d7c3e086bb..8fea7891c3d 100644
--- a/homeassistant/components/binary_sensor/deconz.py
+++ b/homeassistant/components/binary_sensor/deconz.py
@@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
import asyncio
from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
+from homeassistant.components.deconz import (
+ DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
@@ -21,7 +22,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return
from pydeconz.sensor import DECONZ_BINARY_SENSOR
- sensors = hass.data[DECONZ_DATA].sensors
+ sensors = hass.data[DATA_DECONZ].sensors
entities = []
for key in sorted(sensors.keys(), key=int):
@@ -42,6 +43,7 @@ class DeconzBinarySensor(BinarySensorDevice):
def async_added_to_hass(self):
"""Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
+ self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
@callback
def async_update_callback(self, reason):
diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py
index 7d35c0c9e94..0aadcc247ea 100644
--- a/homeassistant/components/binary_sensor/envisalink.py
+++ b/homeassistant/components/binary_sensor/envisalink.py
@@ -50,7 +50,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
self._zone_type = zone_type
self._zone_number = zone_number
- _LOGGER.debug('Setting up zone: ' + zone_name)
+ _LOGGER.debug('Setting up zone: %s', zone_name)
super().__init__(zone_name, info, controller)
@asyncio.coroutine
diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py
index ec64bdf07b8..36ec8b7b61a 100644
--- a/homeassistant/components/binary_sensor/hikvision.py
+++ b/homeassistant/components/binary_sensor/hikvision.py
@@ -56,7 +56,7 @@ CUSTOMIZE_SCHEMA = vol.Schema({
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_NAME, default=None): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SSL, default=False): cv.boolean,
diff --git a/homeassistant/components/binary_sensor/ihc.py b/homeassistant/components/binary_sensor/ihc.py
index 04f8c0d00dd..96efa6e6c19 100644
--- a/homeassistant/components/binary_sensor/ihc.py
+++ b/homeassistant/components/binary_sensor/ihc.py
@@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All({
vol.Required(CONF_ID): cv.positive_int,
vol.Optional(CONF_NAME): cv.string,
- vol.Optional(CONF_TYPE, default=None): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_TYPE): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_INVERTING, default=False): cv.boolean,
}, validate_name)
])
@@ -43,7 +43,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
product_cfg = device['product_cfg']
product = device['product']
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
- product_cfg[CONF_TYPE],
+ product_cfg.get(CONF_TYPE),
product_cfg[CONF_INVERTING],
product)
devices.append(sensor)
@@ -52,7 +52,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for sensor_cfg in binary_sensors:
ihc_id = sensor_cfg[CONF_ID]
name = sensor_cfg[CONF_NAME]
- sensor_type = sensor_cfg[CONF_TYPE]
+ sensor_type = sensor_cfg.get(CONF_TYPE)
inverting = sensor_cfg[CONF_INVERTING]
sensor = IHCBinarySensor(ihc_controller, name, ihc_id, info,
sensor_type, inverting)
@@ -70,7 +70,7 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice):
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
sensor_type: str, inverting: bool,
- product: Element=None) -> None:
+ product: Element = None) -> None:
"""Initialize the IHC binary sensor."""
super().__init__(ihc_controller, name, ihc_id, info, product)
self._state = None
diff --git a/homeassistant/components/binary_sensor/knx.py b/homeassistant/components/binary_sensor/knx.py
index c01654a3663..2b33d6850d6 100644
--- a/homeassistant/components/binary_sensor/knx.py
+++ b/homeassistant/components/binary_sensor/knx.py
@@ -35,7 +35,7 @@ DEPENDENCIES = ['knx']
AUTOMATION_SCHEMA = vol.Schema({
vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
- vol.Required(CONF_ACTION, default=None): cv.SCRIPT_SCHEMA
+ vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA
})
AUTOMATIONS_SCHEMA = vol.All(
@@ -49,16 +49,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICE_CLASS): cv.string,
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
cv.positive_int,
- vol.Optional(CONF_AUTOMATION, default=None): AUTOMATIONS_SCHEMA,
+ vol.Optional(CONF_AUTOMATION): AUTOMATIONS_SCHEMA,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up binary sensor(s) for KNX platform."""
- if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
- return
-
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
else:
diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py
index dd7e0ee8d50..7997e4e60db 100644
--- a/homeassistant/components/binary_sensor/netatmo.py
+++ b/homeassistant/components/binary_sensor/netatmo.py
@@ -50,10 +50,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_CAMERAS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_HOME): cv.string,
- vol.Optional(CONF_PRESENCE_SENSORS, default=PRESENCE_SENSOR_TYPES):
+ vol.Optional(CONF_PRESENCE_SENSORS, default=list(PRESENCE_SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]),
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
- vol.Optional(CONF_WELCOME_SENSORS, default=WELCOME_SENSOR_TYPES):
+ vol.Optional(CONF_WELCOME_SENSORS, default=list(WELCOME_SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)]),
})
diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py
index 129b5250431..265fcec66fa 100644
--- a/homeassistant/components/binary_sensor/octoprint.py
+++ b/homeassistant/components/binary_sensor/octoprint.py
@@ -27,7 +27,7 @@ SENSOR_TYPES = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES):
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py
index 2cc0aee2c7b..aedfc3364db 100644
--- a/homeassistant/components/binary_sensor/rfxtrx.py
+++ b/homeassistant/components/binary_sensor/rfxtrx.py
@@ -28,15 +28,15 @@ DEPENDENCIES = ['rfxtrx']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_DEVICES, default={}): {
cv.string: vol.Schema({
- vol.Optional(CONF_NAME, default=None): cv.string,
- vol.Optional(CONF_DEVICE_CLASS, default=None):
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_DEVICE_CLASS):
DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean,
- vol.Optional(CONF_OFF_DELAY, default=None):
+ vol.Optional(CONF_OFF_DELAY):
vol.Any(cv.time_period, cv.positive_timedelta),
- vol.Optional(CONF_DATA_BITS, default=None): cv.positive_int,
- vol.Optional(CONF_COMMAND_ON, default=None): cv.byte,
- vol.Optional(CONF_COMMAND_OFF, default=None): cv.byte
+ vol.Optional(CONF_DATA_BITS): cv.positive_int,
+ vol.Optional(CONF_COMMAND_ON): cv.byte,
+ vol.Optional(CONF_COMMAND_OFF): cv.byte
})
},
vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean,
@@ -48,7 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import RFXtrx as rfxtrxmod
sensors = []
- for packet_id, entity in config['devices'].items():
+ for packet_id, entity in config[CONF_DEVICES].items():
event = rfxtrx.get_rfx_object(packet_id)
device_id = slugify(event.device.id_string.lower())
@@ -64,10 +64,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
entity[ATTR_NAME], entity[CONF_DEVICE_CLASS])
device = RfxtrxBinarySensor(
- event, entity[ATTR_NAME], entity[CONF_DEVICE_CLASS],
- entity[CONF_FIRE_EVENT], entity[CONF_OFF_DELAY],
- entity[CONF_DATA_BITS], entity[CONF_COMMAND_ON],
- entity[CONF_COMMAND_OFF])
+ event, entity.get(CONF_NAME), entity.get(CONF_DEVICE_CLASS),
+ entity[CONF_FIRE_EVENT], entity.get(CONF_OFF_DELAY),
+ entity.get(CONF_DATA_BITS), entity.get(CONF_COMMAND_ON),
+ entity.get(CONF_COMMAND_OFF))
device.hass = hass
sensors.append(device)
rfxtrx.RFX_DEVICES[device_id] = device
diff --git a/homeassistant/components/binary_sensor/rpi_pfio.py b/homeassistant/components/binary_sensor/rpi_pfio.py
index 7acbadf873a..1abfa25c82b 100644
--- a/homeassistant/components/binary_sensor/rpi_pfio.py
+++ b/homeassistant/components/binary_sensor/rpi_pfio.py
@@ -26,7 +26,7 @@ DEFAULT_SETTLE_TIME = 20
DEPENDENCIES = ['rpi_pfio']
PORT_SCHEMA = vol.Schema({
- vol.Optional(CONF_NAME, default=None): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_SETTLE_TIME, default=DEFAULT_SETTLE_TIME):
cv.positive_int,
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
binary_sensors = []
ports = config.get(CONF_PORTS)
for port, port_entity in ports.items():
- name = port_entity[CONF_NAME]
+ name = port_entity.get(CONF_NAME)
settle_time = port_entity[CONF_SETTLE_TIME] / 1000
invert_logic = port_entity[CONF_INVERT_LOGIC]
diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py
index af814cfd464..58599d3d3de 100644
--- a/homeassistant/components/binary_sensor/workday.py
+++ b/homeassistant/components/binary_sensor/workday.py
@@ -47,7 +47,7 @@ DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
- vol.Optional(CONF_PROVINCE, default=None): cv.string,
+ vol.Optional(CONF_PROVINCE): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py
new file mode 100644
index 00000000000..98c25df79f6
--- /dev/null
+++ b/homeassistant/components/bmw_connected_drive.py
@@ -0,0 +1,105 @@
+"""
+Reads vehicle status from BMW connected drive portal.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/bmw_connected_drive/
+"""
+import logging
+import datetime
+
+import voluptuous as vol
+from homeassistant.helpers import discovery
+from homeassistant.helpers.event import track_utc_time_change
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (
+ CONF_USERNAME, CONF_PASSWORD
+)
+
+REQUIREMENTS = ['bimmer_connected==0.3.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'bmw_connected_drive'
+CONF_VALUES = 'values'
+CONF_COUNTRY = 'country'
+
+ACCOUNT_SCHEMA = vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Required(CONF_COUNTRY): cv.string,
+})
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: {
+ cv.string: ACCOUNT_SCHEMA
+ },
+}, extra=vol.ALLOW_EXTRA)
+
+
+BMW_COMPONENTS = ['device_tracker', 'sensor']
+UPDATE_INTERVAL = 5 # in minutes
+
+
+def setup(hass, config):
+ """Set up the BMW connected drive components."""
+ accounts = []
+ for name, account_config in config[DOMAIN].items():
+ username = account_config[CONF_USERNAME]
+ password = account_config[CONF_PASSWORD]
+ country = account_config[CONF_COUNTRY]
+ _LOGGER.debug('Adding new account %s', name)
+ bimmer = BMWConnectedDriveAccount(username, password, country, name)
+ accounts.append(bimmer)
+
+ # update every UPDATE_INTERVAL minutes, starting now
+ # this should even out the load on the servers
+
+ now = datetime.datetime.now()
+ track_utc_time_change(
+ hass, bimmer.update,
+ minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
+ second=now.second)
+
+ hass.data[DOMAIN] = accounts
+
+ for account in accounts:
+ account.update()
+
+ for component in BMW_COMPONENTS:
+ discovery.load_platform(hass, component, DOMAIN, {}, config)
+
+ return True
+
+
+class BMWConnectedDriveAccount(object):
+ """Representation of a BMW vehicle."""
+
+ def __init__(self, username: str, password: str, country: str,
+ name: str) -> None:
+ """Constructor."""
+ from bimmer_connected.account import ConnectedDriveAccount
+
+ self.account = ConnectedDriveAccount(username, password, country)
+ self.name = name
+ self._update_listeners = []
+
+ def update(self, *_):
+ """Update the state of all vehicles.
+
+ Notify all listeners about the update.
+ """
+ _LOGGER.debug('Updating vehicle state for account %s, '
+ 'notifying %d listeners',
+ self.name, len(self._update_listeners))
+ try:
+ self.account.update_vehicle_states()
+ for listener in self._update_listeners:
+ listener()
+ except IOError as exception:
+ _LOGGER.error('Error updating the vehicle state.')
+ _LOGGER.exception(exception)
+
+ def add_update_listener(self, listener):
+ """Add a listener for update notifications."""
+ self._update_listeners.append(listener)
diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py
index ba798ce7902..d70e7ff8946 100644
--- a/homeassistant/components/calendar/caldav.py
+++ b/homeassistant/components/calendar/caldav.py
@@ -166,7 +166,7 @@ class WebDavCalendarData(object):
self.event = {
"summary": vevent.summary.value,
"start": self.get_hass_date(vevent.dtstart.value),
- "end": self.get_hass_date(vevent.dtend.value),
+ "end": self.get_hass_date(self.get_end_date(vevent)),
"location": self.get_attr_value(vevent, "location"),
"description": self.get_attr_value(vevent, "description")
}
@@ -194,7 +194,7 @@ class WebDavCalendarData(object):
@staticmethod
def is_over(vevent):
"""Return if the event is over."""
- return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
+ return dt.now() > WebDavCalendarData.get_end_date(vevent)
@staticmethod
def get_hass_date(obj):
@@ -217,3 +217,17 @@ class WebDavCalendarData(object):
if hasattr(obj, attribute):
return getattr(obj, attribute).value
return None
+
+ @staticmethod
+ def get_end_date(obj):
+ """Return the end datetime as determined by dtend or duration."""
+ if hasattr(obj, "dtend"):
+ enddate = obj.dtend.value
+
+ elif hasattr(obj, "duration"):
+ enddate = obj.dtstart.value + obj.duration.value
+
+ else:
+ enddate = obj.dtstart.value + timedelta(days=1)
+
+ return WebDavCalendarData.to_datetime(enddate)
diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py
index f1c80612f3b..c5ae1dd3c11 100644
--- a/homeassistant/components/calendar/todoist.py
+++ b/homeassistant/components/calendar/todoist.py
@@ -498,7 +498,7 @@ class TodoistProjectData(object):
# Organize the best tasks (so users can see all the tasks
# they have, organized)
- while len(project_tasks) > 0:
+ while project_tasks:
best_task = self.select_best_task(project_tasks)
_LOGGER.debug("Found Todoist Task: %s", best_task[SUMMARY])
project_tasks.remove(best_task)
diff --git a/homeassistant/components/camera/august.py b/homeassistant/components/camera/august.py
new file mode 100644
index 00000000000..d3bc080bfc6
--- /dev/null
+++ b/homeassistant/components/camera/august.py
@@ -0,0 +1,76 @@
+"""
+Support for August camera.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/camera.august/
+"""
+from datetime import timedelta
+
+import requests
+
+from homeassistant.components.august import DATA_AUGUST, DEFAULT_TIMEOUT
+from homeassistant.components.camera import Camera
+
+DEPENDENCIES = ['august']
+
+SCAN_INTERVAL = timedelta(seconds=5)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up August cameras."""
+ data = hass.data[DATA_AUGUST]
+ devices = []
+
+ for doorbell in data.doorbells:
+ devices.append(AugustCamera(data, doorbell, DEFAULT_TIMEOUT))
+
+ add_devices(devices, True)
+
+
+class AugustCamera(Camera):
+ """An implementation of a Canary security camera."""
+
+ def __init__(self, data, doorbell, timeout):
+ """Initialize a Canary security camera."""
+ super().__init__()
+ self._data = data
+ self._doorbell = doorbell
+ self._timeout = timeout
+ self._image_url = None
+ self._image_content = None
+
+ @property
+ def name(self):
+ """Return the name of this device."""
+ return self._doorbell.device_name
+
+ @property
+ def is_recording(self):
+ """Return true if the device is recording."""
+ return self._doorbell.has_subscription
+
+ @property
+ def motion_detection_enabled(self):
+ """Return the camera motion detection status."""
+ return True
+
+ @property
+ def brand(self):
+ """Return the camera brand."""
+ return 'August'
+
+ @property
+ def model(self):
+ """Return the camera model."""
+ return 'Doorbell'
+
+ def camera_image(self):
+ """Return bytes of camera image."""
+ latest = self._data.get_doorbell_detail(self._doorbell.device_id)
+
+ if self._image_url is not latest.image_url:
+ self._image_url = latest.image_url
+ self._image_content = requests.get(self._image_url,
+ timeout=self._timeout).content
+
+ return self._image_content
diff --git a/homeassistant/components/camera/doorbird.py b/homeassistant/components/camera/doorbird.py
index 2ca962a8450..034ddc2fabb 100644
--- a/homeassistant/components/camera/doorbird.py
+++ b/homeassistant/components/camera/doorbird.py
@@ -18,8 +18,10 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
DEPENDENCIES = ['doorbird']
_CAMERA_LAST_VISITOR = "DoorBird Last Ring"
+_CAMERA_LAST_MOTION = "DoorBird Last Motion"
_CAMERA_LIVE = "DoorBird Live"
_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1)
+_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1)
_LIVE_INTERVAL = datetime.timedelta(seconds=1)
_LOGGER = logging.getLogger(__name__)
_TIMEOUT = 10 # seconds
@@ -34,6 +36,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
DoorBirdCamera(
device.history_image_url(1, 'doorbell'), _CAMERA_LAST_VISITOR,
_LAST_VISITOR_INTERVAL),
+ DoorBirdCamera(
+ device.history_image_url(1, 'motionsensor'), _CAMERA_LAST_MOTION,
+ _LAST_MOTION_INTERVAL),
])
diff --git a/homeassistant/components/camera/mjpeg.py b/homeassistant/components/camera/mjpeg.py
index 6168eb81939..35d30104f6e 100644
--- a/homeassistant/components/camera/mjpeg.py
+++ b/homeassistant/components/camera/mjpeg.py
@@ -119,6 +119,8 @@ class MjpegCamera(Camera):
else:
req = requests.get(self._mjpeg_url, stream=True, timeout=10)
+ # https://github.com/PyCQA/pylint/issues/1437
+ # pylint: disable=no-member
with closing(req) as response:
return extract_image_from_mjpeg(response.iter_content(102400))
diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py
index 65f291bf41d..1340c52459d 100644
--- a/homeassistant/components/camera/onvif.py
+++ b/homeassistant/components/camera/onvif.py
@@ -6,18 +6,19 @@ https://home-assistant.io/components/camera.onvif/
"""
import asyncio
import logging
-import os
import voluptuous as vol
from homeassistant.const import (
- CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT)
-from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
+ CONF_NAME, CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT,
+ ATTR_ENTITY_ID)
+from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, DOMAIN
from homeassistant.components.ffmpeg import (
DATA_FFMPEG, CONF_EXTRA_ARGUMENTS)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream)
+from homeassistant.helpers.service import extract_entity_ids
_LOGGER = logging.getLogger(__name__)
@@ -33,6 +34,22 @@ DEFAULT_USERNAME = 'admin'
DEFAULT_PASSWORD = '888888'
DEFAULT_ARGUMENTS = '-q:v 2'
+ATTR_PAN = "pan"
+ATTR_TILT = "tilt"
+ATTR_ZOOM = "zoom"
+
+DIR_UP = "UP"
+DIR_DOWN = "DOWN"
+DIR_LEFT = "LEFT"
+DIR_RIGHT = "RIGHT"
+ZOOM_OUT = "ZOOM_OUT"
+ZOOM_IN = "ZOOM_IN"
+
+SERVICE_PTZ = "onvif_ptz"
+
+ONVIF_DATA = "onvif"
+ENTITIES = "entities"
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@@ -42,36 +59,98 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EXTRA_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
})
+SERVICE_PTZ_SCHEMA = vol.Schema({
+ ATTR_ENTITY_ID: cv.entity_ids,
+ ATTR_PAN: vol.In([DIR_LEFT, DIR_RIGHT]),
+ ATTR_TILT: vol.In([DIR_UP, DIR_DOWN]),
+ ATTR_ZOOM: vol.In([ZOOM_OUT, ZOOM_IN])
+})
+
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up a ONVIF camera."""
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_HOST)):
return
- async_add_devices([ONVIFCamera(hass, config)])
+
+ def handle_ptz(service):
+ """Handle PTZ service call."""
+ pan = service.data.get(ATTR_PAN, None)
+ tilt = service.data.get(ATTR_TILT, None)
+ zoom = service.data.get(ATTR_ZOOM, None)
+ all_cameras = hass.data[ONVIF_DATA][ENTITIES]
+ entity_ids = extract_entity_ids(hass, service)
+ target_cameras = []
+ if not entity_ids:
+ target_cameras = all_cameras
+ else:
+ target_cameras = [camera for camera in all_cameras
+ if camera.entity_id in entity_ids]
+ for camera in target_cameras:
+ camera.perform_ptz(pan, tilt, zoom)
+
+ hass.services.async_register(DOMAIN, SERVICE_PTZ, handle_ptz,
+ schema=SERVICE_PTZ_SCHEMA)
+ async_add_devices([ONVIFHassCamera(hass, config)])
-class ONVIFCamera(Camera):
+class ONVIFHassCamera(Camera):
"""An implementation of an ONVIF camera."""
def __init__(self, hass, config):
"""Initialize a ONVIF camera."""
- from onvif import ONVIFService
- import onvif
+ from onvif import ONVIFCamera, exceptions
super().__init__()
self._name = config.get(CONF_NAME)
self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS)
- media = ONVIFService(
- 'http://{}:{}/onvif/device_service'.format(
- config.get(CONF_HOST), config.get(CONF_PORT)),
- config.get(CONF_USERNAME),
- config.get(CONF_PASSWORD),
- '{}/wsdl/media.wsdl'.format(os.path.dirname(onvif.__file__))
- )
- self._input = media.GetStreamUri().Uri
- _LOGGER.debug("ONVIF Camera Using the following URL for %s: %s",
- self._name, self._input)
+ self._input = None
+ camera = None
+ try:
+ _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s",
+ config.get(CONF_HOST), config.get(CONF_PORT))
+ camera = ONVIFCamera(
+ config.get(CONF_HOST), config.get(CONF_PORT),
+ config.get(CONF_USERNAME), config.get(CONF_PASSWORD)
+ )
+ media_service = camera.create_media_service()
+ stream_uri = media_service.GetStreamUri(
+ {'StreamSetup': {'Stream': 'RTP-Unicast', 'Transport': 'RTSP'}}
+ )
+ self._input = stream_uri.Uri.replace(
+ 'rtsp://', 'rtsp://{}:{}@'.format(
+ config.get(CONF_USERNAME),
+ config.get(CONF_PASSWORD)), 1)
+ _LOGGER.debug(
+ "ONVIF Camera Using the following URL for %s: %s",
+ self._name, self._input)
+ except Exception as err:
+ _LOGGER.error("Unable to communicate with ONVIF Camera: %s", err)
+ raise
+ try:
+ self._ptz = camera.create_ptz_service()
+ except exceptions.ONVIFError as err:
+ self._ptz = None
+ _LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err)
+
+ def perform_ptz(self, pan, tilt, zoom):
+ """Perform a PTZ action on the camera."""
+ if self._ptz:
+ pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0
+ tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0
+ zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0
+ req = {"Velocity": {
+ "PanTilt": {"_x": pan_val, "_y": tilt_val},
+ "Zoom": {"_x": zoom_val}}}
+ self._ptz.ContinuousMove(req)
+
+ @asyncio.coroutine
+ def async_added_to_hass(self):
+ """Callback when entity is added to hass."""
+ if ONVIF_DATA not in self.hass.data:
+ self.hass.data[ONVIF_DATA] = {}
+ self.hass.data[ONVIF_DATA][ENTITIES] = []
+ self.hass.data[ONVIF_DATA][ENTITIES].append(self)
@asyncio.coroutine
def async_camera_image(self):
diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py
index f37e7778414..f1f110d7c6a 100644
--- a/homeassistant/components/camera/rpi_camera.py
+++ b/homeassistant/components/camera/rpi_camera.py
@@ -8,6 +8,7 @@ import os
import subprocess
import logging
import shutil
+from tempfile import NamedTemporaryFile
import voluptuous as vol
@@ -36,7 +37,7 @@ DEFAULT_TIMELAPSE = 1000
DEFAULT_VERTICAL_FLIP = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_FILE_PATH): cv.string,
+ vol.Optional(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_HORIZONTAL_FLIP, default=DEFAULT_HORIZONTAL_FLIP):
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
@@ -77,25 +78,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
CONF_TIMELAPSE: config.get(CONF_TIMELAPSE),
CONF_HORIZONTAL_FLIP: config.get(CONF_HORIZONTAL_FLIP),
CONF_VERTICAL_FLIP: config.get(CONF_VERTICAL_FLIP),
- CONF_FILE_PATH: config.get(CONF_FILE_PATH,
- os.path.join(os.path.dirname(__file__),
- 'image.jpg'))
+ CONF_FILE_PATH: config.get(CONF_FILE_PATH)
}
)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, kill_raspistill)
- try:
- # Try to create an empty file (or open existing) to ensure we have
- # proper permissions.
- open(setup_config[CONF_FILE_PATH], 'a').close()
+ file_path = setup_config[CONF_FILE_PATH]
- add_devices([RaspberryCamera(setup_config)])
- except PermissionError:
- _LOGGER.error("File path is not writable")
- return False
- except FileNotFoundError:
- _LOGGER.error("Could not create output file (missing directory?)")
+ def delete_temp_file(*args):
+ """Delete the temporary file to prevent saving multiple temp images.
+
+ Only used when no path is defined
+ """
+ os.remove(file_path)
+
+ # If no file path is defined, use a temporary file
+ if file_path is None:
+ temp_file = NamedTemporaryFile(suffix='.jpg', delete=False)
+ temp_file.close()
+ file_path = temp_file.name
+ setup_config[CONF_FILE_PATH] = file_path
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, delete_temp_file)
+
+ # Check whether the file path has been whitelisted
+ elif not hass.config.is_allowed_path(file_path):
+ _LOGGER.error("'%s' is not a whitelisted directory", file_path)
return False
diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml
index 926af582cc7..b548f3d1ada 100644
--- a/homeassistant/components/camera/services.yaml
+++ b/homeassistant/components/camera/services.yaml
@@ -23,3 +23,20 @@ snapshot:
filename:
description: Template of a Filename. Variable is entity_id.
example: '/tmp/snapshot_{{ entity_id }}'
+
+onvif_ptz:
+ description: Pan/Tilt/Zoom service for ONVIF camera.
+ fields:
+ entity_id:
+ description: Name(s) of entities to pan, tilt or zoom.
+ example: 'camera.living_room_camera'
+ pan:
+ description: "Direction of pan. Allowed values: LEFT, RIGHT."
+ example: 'LEFT'
+ tilt:
+ description: "Direction of tilt. Allowed values: DOWN, UP."
+ example: 'DOWN'
+ zoom:
+ description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
+ example: "ZOOM_IN"
+
diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py
index f7dc4cfd973..20dceb8a1c5 100644
--- a/homeassistant/components/camera/uvc.py
+++ b/homeassistant/components/camera/uvc.py
@@ -188,7 +188,7 @@ class UnifiVideoCamera(Camera):
self._nvr.set_recordmode(self._uuid, set_mode)
self._motion_status = mode
except NvrError as err:
- _LOGGER.error("Unable to set recordmode to " + set_mode)
+ _LOGGER.error("Unable to set recordmode to %s", set_mode)
_LOGGER.debug(err)
def enable_motion_detection(self):
diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py
index b4bcad0064d..5836a9c94dc 100644
--- a/homeassistant/components/camera/xeoma.py
+++ b/homeassistant/components/camera/xeoma.py
@@ -33,7 +33,7 @@ CAMERAS_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_CAMERAS, default={}):
+ vol.Optional(CONF_CAMERAS):
vol.Schema(vol.All(cv.ensure_list, [CAMERAS_SCHEMA])),
vol.Optional(CONF_NEW_VERSION, default=True): cv.boolean,
vol.Optional(CONF_PASSWORD): cv.string,
@@ -42,7 +42,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
-# pylint: disable=unused-argument
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Discover and setup Xeoma Cameras."""
from pyxeoma.xeoma import Xeoma, XeomaError
@@ -68,7 +67,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
for image_name, username, pw in discovered_image_names
]
- for cam in config[CONF_CAMERAS]:
+ for cam in config.get(CONF_CAMERAS, []):
+ # https://github.com/PyCQA/pylint/issues/1830
+ # pylint: disable=stop-iteration-return
camera = next(
(dc for dc in discovered_cameras
if dc[CONF_IMAGE_NAME] == cam[CONF_IMAGE_NAME]), None)
diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py
index ce656eb96e8..e1a5f71af83 100644
--- a/homeassistant/components/climate/__init__.py
+++ b/homeassistant/components/climate/__init__.py
@@ -669,16 +669,16 @@ class ClimateDevice(Entity):
"""
return self.hass.async_add_job(self.set_humidity, humidity)
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
raise NotImplementedError()
- def async_set_fan_mode(self, fan):
+ def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode.
This method must be run in the event loop and returns a coroutine.
"""
- return self.hass.async_add_job(self.set_fan_mode, fan)
+ return self.hass.async_add_job(self.set_fan_mode, fan_mode)
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py
index 0ed4ebe8942..2c49b25a39d 100644
--- a/homeassistant/components/climate/daikin.py
+++ b/homeassistant/components/climate/daikin.py
@@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_NAME, default=None): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
})
HA_STATE_TO_DAIKIN = {
@@ -236,9 +236,9 @@ class DaikinClimate(ClimateDevice):
"""Return the fan setting."""
return self.get(ATTR_FAN_MODE)
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set fan mode."""
- self.set({ATTR_FAN_MODE: fan})
+ self.set({ATTR_FAN_MODE: fan_mode})
@property
def fan_list(self):
diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py
index 102155babea..44491b8cd21 100644
--- a/homeassistant/components/climate/demo.py
+++ b/homeassistant/components/climate/demo.py
@@ -195,9 +195,9 @@ class DemoClimate(ClimateDevice):
self._current_swing_mode = swing_mode
self.schedule_update_ha_state()
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set new target temperature."""
- self._current_fan_mode = fan
+ self._current_fan_mode = fan_mode
self.schedule_update_ha_state()
def set_operation_mode(self, operation_mode):
@@ -225,9 +225,9 @@ class DemoClimate(ClimateDevice):
self._away = False
self.schedule_update_ha_state()
- def set_hold_mode(self, hold):
- """Update hold mode on."""
- self._hold = hold
+ def set_hold_mode(self, hold_mode):
+ """Update hold_mode on."""
+ self._hold = hold_mode
self.schedule_update_ha_state()
def turn_aux_heat_on(self):
diff --git a/homeassistant/components/climate/ephember.py b/homeassistant/components/climate/ephember.py
index e1f1ab7d448..419237b4645 100644
--- a/homeassistant/components/climate/ephember.py
+++ b/homeassistant/components/climate/ephember.py
@@ -98,8 +98,7 @@ class EphEmberThermostat(ClimateDevice):
"""Return current operation ie. heat, cool, idle."""
if self._zone['isCurrentlyActive']:
return STATE_HEAT
- else:
- return STATE_IDLE
+ return STATE_IDLE
@property
def is_aux_heat_on(self):
diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py
index 9c712c632e6..5c0a3530006 100644
--- a/homeassistant/components/climate/eq3btsmart.py
+++ b/homeassistant/components/climate/eq3btsmart.py
@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-eq3bt==0.1.8']
+REQUIREMENTS = ['python-eq3bt==0.1.9']
_LOGGER = logging.getLogger(__name__)
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices)
-# pylint: disable=import-error
+# pylint: disable=import-error, no-name-in-module
class EQ3BTSmartThermostat(ClimateDevice):
"""Representation of an eQ-3 Bluetooth Smart thermostat."""
@@ -75,6 +75,8 @@ class EQ3BTSmartThermostat(ClimateDevice):
self._name = _name
self._thermostat = eq3.Thermostat(_mac)
+ self._target_temperature = None
+ self._target_mode = None
@property
def supported_features(self):
@@ -116,6 +118,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None:
return
+ self._target_temperature = temperature
self._thermostat.target_temperature = temperature
@property
@@ -132,6 +135,7 @@ class EQ3BTSmartThermostat(ClimateDevice):
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
+ self._target_mode = operation_mode
self._thermostat.mode = self.reverse_modes[operation_mode]
def turn_away_mode_off(self):
@@ -177,3 +181,15 @@ class EQ3BTSmartThermostat(ClimateDevice):
self._thermostat.update()
except BTLEException as ex:
_LOGGER.warning("Updating the state failed: %s", ex)
+
+ if (self._target_temperature and
+ self._thermostat.target_temperature
+ != self._target_temperature):
+ self.set_temperature(temperature=self._target_temperature)
+ else:
+ self._target_temperature = None
+ if (self._target_mode and
+ self.modes[self._thermostat.mode] != self._target_mode):
+ self.set_operation_mode(operation_mode=self._target_mode)
+ else:
+ self._target_mode = None
diff --git a/homeassistant/components/climate/flexit.py b/homeassistant/components/climate/flexit.py
index 98c03217509..565e913319f 100644
--- a/homeassistant/components/climate/flexit.py
+++ b/homeassistant/components/climate/flexit.py
@@ -152,6 +152,6 @@ class Flexit(ClimateDevice):
self._target_temperature = kwargs.get(ATTR_TEMPERATURE)
self.unit.set_temp(self._target_temperature)
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set new fan mode."""
- self.unit.set_fan_speed(self._fan_list.index(fan))
+ self.unit.set_fan_speed(self._fan_list.index(fan_mode))
diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py
index c66e611c8e9..b97dc221298 100644
--- a/homeassistant/components/climate/generic_thermostat.py
+++ b/homeassistant/components/climate/generic_thermostat.py
@@ -190,11 +190,9 @@ class GenericThermostat(ClimateDevice):
"""Return the current state."""
if self._is_device_active:
return self.current_operation
- else:
- if self._enabled:
- return STATE_IDLE
- else:
- return STATE_OFF
+ if self._enabled:
+ return STATE_IDLE
+ return STATE_OFF
@property
def should_poll(self):
diff --git a/homeassistant/components/climate/knx.py b/homeassistant/components/climate/knx.py
index a78c277fa33..1bbc5b789fb 100644
--- a/homeassistant/components/climate/knx.py
+++ b/homeassistant/components/climate/knx.py
@@ -48,9 +48,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
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_SETPOINT_SHIFT_MIN, default=DEFAULT_SETPOINT_SHIFT_MIN):
+ vol.All(int, vol.Range(min=-32, max=0)),
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,
@@ -64,9 +64,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up climate(s) for KNX platform."""
- if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
- return
-
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
else:
diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py
index 2b3b3bfbab1..9c005b62dcc 100644
--- a/homeassistant/components/climate/melissa.py
+++ b/homeassistant/components/climate/melissa.py
@@ -26,7 +26,7 @@ SUPPORT_FLAGS = (SUPPORT_FAN_MODE | SUPPORT_OPERATION_MODE |
SUPPORT_ON_OFF | SUPPORT_TARGET_TEMPERATURE)
OP_MODES = [
- STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT
+ STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT
]
FAN_MODES = [
@@ -42,8 +42,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
all_devices = []
for device in devices:
- all_devices.append(MelissaClimate(
- api, device['serial_number'], device))
+ if device['type'] == 'melissa':
+ all_devices.append(MelissaClimate(
+ api, device['serial_number'], device))
add_devices(all_devices)
@@ -146,10 +147,10 @@ class MelissaClimate(ClimateDevice):
temp = kwargs.get(ATTR_TEMPERATURE)
self.send({self._api.TEMP: temp})
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set fan mode."""
- fan_mode = self.hass_fan_to_melissa(fan)
- self.send({self._api.FAN: fan_mode})
+ melissa_fan_mode = self.hass_fan_to_melissa(fan_mode)
+ self.send({self._api.FAN: melissa_fan_mode})
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
@@ -174,8 +175,7 @@ class MelissaClimate(ClimateDevice):
if not self._api.send(self._serial_number, self._cur_settings):
self._cur_settings = old_value
return False
- else:
- return True
+ return True
def update(self):
"""Get latest data from Melissa."""
@@ -196,14 +196,11 @@ class MelissaClimate(ClimateDevice):
return STATE_OFF
elif state == self._api.STATE_IDLE:
return STATE_IDLE
- else:
- return None
+ return None
def melissa_op_to_hass(self, mode):
"""Translate Melissa modes to hass states."""
- if mode == self._api.MODE_AUTO:
- return STATE_AUTO
- elif mode == self._api.MODE_HEAT:
+ if mode == self._api.MODE_HEAT:
return STATE_HEAT
elif mode == self._api.MODE_COOL:
return STATE_COOL
@@ -211,10 +208,9 @@ class MelissaClimate(ClimateDevice):
return STATE_DRY
elif mode == self._api.MODE_FAN:
return STATE_FAN_ONLY
- else:
- _LOGGER.warning(
- "Operation mode %s could not be mapped to hass", mode)
- return None
+ _LOGGER.warning(
+ "Operation mode %s could not be mapped to hass", mode)
+ return None
def melissa_fan_to_hass(self, fan):
"""Translate Melissa fan modes to hass modes."""
@@ -226,15 +222,12 @@ class MelissaClimate(ClimateDevice):
return SPEED_MEDIUM
elif fan == self._api.FAN_HIGH:
return SPEED_HIGH
- else:
- _LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
- return None
+ _LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
+ return None
def hass_mode_to_melissa(self, mode):
"""Translate hass states to melissa modes."""
- if mode == STATE_AUTO:
- return self._api.MODE_AUTO
- elif mode == STATE_HEAT:
+ if mode == STATE_HEAT:
return self._api.MODE_HEAT
elif mode == STATE_COOL:
return self._api.MODE_COOL
diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py
index 5929cec3b05..1d98a5733f7 100644
--- a/homeassistant/components/climate/mqtt.py
+++ b/homeassistant/components/climate/mqtt.py
@@ -482,15 +482,15 @@ class MqttClimate(MqttAvailability, ClimateDevice):
self.async_schedule_update_ha_state()
@asyncio.coroutine
- def async_set_fan_mode(self, fan):
+ def async_set_fan_mode(self, fan_mode):
"""Set new target temperature."""
if self._send_if_off or self._current_operation != STATE_OFF:
mqtt.async_publish(
self.hass, self._topic[CONF_FAN_MODE_COMMAND_TOPIC],
- fan, self._qos, self._retain)
+ fan_mode, self._qos, self._retain)
if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None:
- self._current_fan_mode = fan
+ self._current_fan_mode = fan_mode
self.async_schedule_update_ha_state()
@asyncio.coroutine
@@ -552,15 +552,15 @@ class MqttClimate(MqttAvailability, ClimateDevice):
self.async_schedule_update_ha_state()
@asyncio.coroutine
- def async_set_hold_mode(self, hold):
+ def async_set_hold_mode(self, hold_mode):
"""Update hold mode on."""
if self._topic[CONF_HOLD_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass,
self._topic[CONF_HOLD_COMMAND_TOPIC],
- hold, self._qos, self._retain)
+ hold_mode, self._qos, self._retain)
if self._topic[CONF_HOLD_STATE_TOPIC] is None:
- self._hold = hold
+ self._hold = hold_mode
self.async_schedule_update_ha_state()
@asyncio.coroutine
diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py
index 5553db70f0d..b526d8b066c 100644
--- a/homeassistant/components/climate/mysensors.py
+++ b/homeassistant/components/climate/mysensors.py
@@ -143,14 +143,14 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
self._values[value_type] = value
self.schedule_update_ha_state()
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set new target temperature."""
set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
- self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan)
+ self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode)
if self.gateway.optimistic:
# Optimistically assume that device has changed state
- self._values[set_req.V_HVAC_SPEED] = fan
+ self._values[set_req.V_HVAC_SPEED] = fan_mode
self.schedule_update_ha_state()
def set_operation_mode(self, operation_mode):
diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py
index d8d7d6c901a..0427514a7b5 100644
--- a/homeassistant/components/climate/nest.py
+++ b/homeassistant/components/climate/nest.py
@@ -207,9 +207,9 @@ class NestThermostat(ClimateDevice):
"""List of available fan modes."""
return self._fan_list
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
- self.device.fan = fan.lower()
+ self.device.fan = fan_mode.lower()
@property
def min_temp(self):
@@ -225,7 +225,7 @@ class NestThermostat(ClimateDevice):
"""Cache value from Python-nest."""
self._location = self.device.where
self._name = self.device.name
- self._humidity = self.device.humidity,
+ self._humidity = self.device.humidity
self._temperature = self.device.temperature
self._mode = self.device.mode
self._target_temperature = self.device.target
diff --git a/homeassistant/components/climate/nuheat.py b/homeassistant/components/climate/nuheat.py
index f41812dbaae..39c66ff94f2 100644
--- a/homeassistant/components/climate/nuheat.py
+++ b/homeassistant/components/climate/nuheat.py
@@ -185,7 +185,7 @@ class NuHeatThermostat(ClimateDevice):
self._thermostat.resume_schedule()
self._force_update = True
- def set_hold_mode(self, hold_mode, **kwargs):
+ def set_hold_mode(self, hold_mode):
"""Update the hold mode of the thermostat."""
if hold_mode == MODE_AUTO:
schedule_mode = SCHEDULE_RUN
diff --git a/homeassistant/components/climate/radiotherm.py b/homeassistant/components/climate/radiotherm.py
index 2b31ca93d22..032d85637ef 100644
--- a/homeassistant/components/climate/radiotherm.py
+++ b/homeassistant/components/climate/radiotherm.py
@@ -183,17 +183,16 @@ class RadioThermostat(ClimateDevice):
"""List of available fan modes."""
if self._is_model_ct80:
return CT80_FAN_OPERATION_LIST
- else:
- return CT30_FAN_OPERATION_LIST
+ return CT30_FAN_OPERATION_LIST
@property
def current_fan_mode(self):
"""Return whether the fan is on."""
return self._fmode
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
- code = FAN_MODE_TO_CODE.get(fan, None)
+ code = FAN_MODE_TO_CODE.get(fan_mode, None)
if code is not None:
self.device.fmode = code
diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py
index 67113e7c48a..b49d379592f 100644
--- a/homeassistant/components/climate/sensibo.py
+++ b/homeassistant/components/climate/sensibo.py
@@ -240,13 +240,13 @@ class SensiboClimate(ClimateDevice):
def min_temp(self):
"""Return the minimum temperature."""
return self._temperatures_list[0] \
- if len(self._temperatures_list) else super().min_temp()
+ if self._temperatures_list else super().min_temp()
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._temperatures_list[-1] \
- if len(self._temperatures_list) else super().max_temp()
+ if self._temperatures_list else super().max_temp()
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
@@ -273,11 +273,11 @@ class SensiboClimate(ClimateDevice):
self._id, 'targetTemperature', temperature, self._ac_states)
@asyncio.coroutine
- def async_set_fan_mode(self, fan):
+ def async_set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
with async_timeout.timeout(TIMEOUT):
yield from self._client.async_set_ac_state_property(
- self._id, 'fanLevel', fan, self._ac_states)
+ self._id, 'fanLevel', fan_mode, self._ac_states)
@asyncio.coroutine
def async_set_operation_mode(self, operation_mode):
diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py
index 868511c0ac4..437c8ec3371 100644
--- a/homeassistant/components/climate/tado.py
+++ b/homeassistant/components/climate/tado.py
@@ -213,6 +213,7 @@ class TadoClimate(ClimateDevice):
self._target_temp = temperature
self._control_heating()
+ # pylint: disable=arguments-differ
def set_operation_mode(self, readable_operation_mode):
"""Set new operation mode."""
operation_mode = CONST_MODE_SMART_SCHEDULE
diff --git a/homeassistant/components/climate/tesla.py b/homeassistant/components/climate/tesla.py
index 459d9c666fd..225c13d975d 100644
--- a/homeassistant/components/climate/tesla.py
+++ b/homeassistant/components/climate/tesla.py
@@ -51,8 +51,7 @@ class TeslaThermostat(TeslaDevice, ClimateDevice):
mode = self.tesla_device.is_hvac_enabled()
if mode:
return OPERATION_LIST[0] # On
- else:
- return OPERATION_LIST[1] # Off
+ return OPERATION_LIST[1] # Off
@property
def operation_list(self):
diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py
index 6db1d53bc50..6e63cc4092b 100644
--- a/homeassistant/components/climate/venstar.py
+++ b/homeassistant/components/climate/venstar.py
@@ -111,8 +111,7 @@ class VenstarThermostat(ClimateDevice):
"""Return the unit of measurement, as defined by the API."""
if self._client.tempunits == self._client.TEMPUNITS_F:
return TEMP_FAHRENHEIT
- else:
- return TEMP_CELSIUS
+ return TEMP_CELSIUS
@property
def fan_list(self):
@@ -143,16 +142,14 @@ class VenstarThermostat(ClimateDevice):
return STATE_COOL
elif self._client.mode == self._client.MODE_AUTO:
return STATE_AUTO
- else:
- return STATE_OFF
+ return STATE_OFF
@property
def current_fan_mode(self):
"""Return the fan setting."""
if self._client.fan == self._client.FAN_AUTO:
return STATE_AUTO
- else:
- return STATE_ON
+ return STATE_ON
@property
def device_state_attributes(self):
@@ -169,24 +166,21 @@ class VenstarThermostat(ClimateDevice):
return self._client.heattemp
elif self._client.mode == self._client.MODE_COOL:
return self._client.cooltemp
- else:
- return None
+ return None
@property
def target_temperature_low(self):
"""Return the lower bound temp if auto mode is on."""
if self._client.mode == self._client.MODE_AUTO:
return self._client.heattemp
- else:
- return None
+ return None
@property
def target_temperature_high(self):
"""Return the upper bound temp if auto mode is on."""
if self._client.mode == self._client.MODE_AUTO:
return self._client.cooltemp
- else:
- return None
+ return None
@property
def target_humidity(self):
@@ -245,9 +239,9 @@ class VenstarThermostat(ClimateDevice):
if not success:
_LOGGER.error("Failed to change the temperature")
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
- if fan == STATE_ON:
+ if fan_mode == STATE_ON:
success = self._client.set_fan(self._client.FAN_ON)
else:
success = self._client.set_fan(self._client.FAN_AUTO)
diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py
index c9d22e41d81..6fb6bc0ff48 100644
--- a/homeassistant/components/climate/vera.py
+++ b/homeassistant/components/climate/vera.py
@@ -85,13 +85,13 @@ class VeraThermostat(VeraDevice, ClimateDevice):
"""Return a list of available fan modes."""
return FAN_OPERATION_LIST
- def set_fan_mode(self, mode):
+ def set_fan_mode(self, fan_mode):
"""Set new target temperature."""
- if mode == FAN_OPERATION_LIST[0]:
+ if fan_mode == FAN_OPERATION_LIST[0]:
self.vera_device.fan_on()
- elif mode == FAN_OPERATION_LIST[1]:
+ elif fan_mode == FAN_OPERATION_LIST[1]:
self.vera_device.fan_auto()
- elif mode == FAN_OPERATION_LIST[2]:
+ elif fan_mode == FAN_OPERATION_LIST[2]:
return self.vera_device.fan_cycle()
@property
diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py
index 50374a32807..8c66567a4aa 100644
--- a/homeassistant/components/climate/wink.py
+++ b/homeassistant/components/climate/wink.py
@@ -324,9 +324,9 @@ class WinkThermostat(WinkDevice, ClimateDevice):
return self.wink.fan_modes()
return None
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Turn fan on/off."""
- self.wink.set_fan_mode(fan.lower())
+ self.wink.set_fan_mode(fan_mode.lower())
def turn_aux_heat_on(self):
"""Turn auxiliary heater on."""
@@ -486,26 +486,25 @@ class WinkAC(WinkDevice, ClimateDevice):
return SPEED_LOW
elif speed <= 0.66:
return SPEED_MEDIUM
- else:
- return SPEED_HIGH
+ return SPEED_HIGH
@property
def fan_list(self):
"""Return a list of available fan modes."""
return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""
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:
+ if fan_mode == SPEED_LOW:
speed = 0.33
- elif fan == SPEED_MEDIUM:
+ elif fan_mode == SPEED_MEDIUM:
speed = 0.66
- elif fan == SPEED_HIGH:
+ elif fan_mode == SPEED_HIGH:
speed = 1.0
self.wink.set_ac_fan_speed(speed)
diff --git a/homeassistant/components/climate/zwave.py b/homeassistant/components/climate/zwave.py
index acc3eda1194..1eec9c82f3c 100644
--- a/homeassistant/components/climate/zwave.py
+++ b/homeassistant/components/climate/zwave.py
@@ -198,10 +198,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice):
self.values.primary.data = temperature
- def set_fan_mode(self, fan):
+ def set_fan_mode(self, fan_mode):
"""Set new target fan mode."""
if self.values.fan_mode:
- self.values.fan_mode.data = fan
+ self.values.fan_mode.data = fan_mode
def set_operation_mode(self, operation_mode):
"""Set new target operation mode."""
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index e17c9ee1b1e..3657b64b989 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -56,10 +56,7 @@ GOOGLE_ENTITY_SCHEMA = vol.Schema({
})
ASSISTANT_SCHEMA = vol.Schema({
- vol.Optional(
- CONF_FILTER,
- default=lambda: entityfilter.generate_filter([], [], [], [])
- ): entityfilter.FILTER_SCHEMA,
+ vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
})
ALEXA_SCHEMA = ASSISTANT_SCHEMA.extend({
@@ -222,7 +219,7 @@ class Cloud:
# Fetching keyset can fail if internet is not up yet.
if not success:
- self.hass.helpers.async_call_later(5, self.async_start)
+ self.hass.helpers.event.async_call_later(5, self.async_start)
return
def load_config():
diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py
index e96f2a2d8a5..118a9857158 100644
--- a/homeassistant/components/cloud/auth_api.py
+++ b/homeassistant/components/cloud/auth_api.py
@@ -1,7 +1,4 @@
"""Package to communicate with the authentication API."""
-import logging
-
-_LOGGER = logging.getLogger(__name__)
class CloudError(Exception):
@@ -31,6 +28,8 @@ class InvalidCode(CloudError):
class PasswordChangeRequired(CloudError):
"""Raised when a password change is required."""
+ # https://github.com/PyCQA/pylint/issues/1085
+ # pylint: disable=useless-super-delegation
def __init__(self, message='Password change required.'):
"""Initialize a password change required error."""
super().__init__(message)
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index af966e180eb..f7f327f2f2c 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -6,8 +6,9 @@ import logging
import async_timeout
import voluptuous as vol
-from homeassistant.components.http import (
- HomeAssistantView, RequestDataValidator)
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.http.data_validator import (
+ RequestDataValidator)
from . import auth_api
from .const import DOMAIN, REQUEST_TIMEOUT
diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py
index 2d3ab025e43..3220fc372f7 100644
--- a/homeassistant/components/cloud/iot.py
+++ b/homeassistant/components/cloud/iot.py
@@ -44,20 +44,13 @@ class CloudIoT:
@asyncio.coroutine
def connect(self):
"""Connect to the IoT broker."""
+ if self.state != STATE_DISCONNECTED:
+ raise RuntimeError('Connect called while not disconnected')
+
hass = self.cloud.hass
- 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.close_requested = False
+ self.state = STATE_CONNECTING
+ self.tries = 0
@asyncio.coroutine
def _handle_hass_stop(event):
@@ -66,17 +59,60 @@ class CloudIoT:
remove_hass_stop_listener = None
yield from self.disconnect()
- self.state = STATE_CONNECTING
- self.close_requested = False
remove_hass_stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _handle_hass_stop)
+
+ while True:
+ try:
+ yield from self._handle_connection()
+ except Exception: # pylint: disable=broad-except
+ # Safety net. This should never hit.
+ # Still adding it here to make sure we can always reconnect
+ _LOGGER.exception("Unexpected error")
+
+ if self.close_requested:
+ break
+
+ self.state = STATE_CONNECTING
+ self.tries += 1
+
+ try:
+ # Sleep 0, 5, 10, 15 ... 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
+ except asyncio.CancelledError:
+ # Happens if disconnect called
+ break
+
+ self.state = STATE_DISCONNECTED
+ if remove_hass_stop_listener is not None:
+ remove_hass_stop_listener()
+
+ @asyncio.coroutine
+ def _handle_connection(self):
+ """Connect to the IoT broker."""
+ hass = self.cloud.hass
+
+ try:
+ yield from hass.async_add_job(auth_api.check_token, self.cloud)
+ except auth_api.CloudError as err:
+ _LOGGER.warning("Unable to connect: %s", err)
+ return
+
+ if self.cloud.subscription_expired:
+ hass.components.persistent_notification.async_create(
+ MESSAGE_EXPIRATION, 'Subscription expired',
+ 'cloud_subscription_expired')
+ self.close_requested = True
+ return
+
session = async_get_clientsession(self.cloud.hass)
client = None
disconnect_warn = None
try:
- yield from hass.async_add_job(auth_api.check_token, self.cloud)
-
self.client = client = yield from session.ws_connect(
self.cloud.relayer, heartbeat=55, headers={
hdrs.AUTHORIZATION:
@@ -90,9 +126,11 @@ class CloudIoT:
while not client.closed:
msg = yield from client.receive()
- if msg.type in (WSMsgType.ERROR, WSMsgType.CLOSED,
- WSMsgType.CLOSING):
- disconnect_warn = 'Connection cancelled.'
+ if msg.type in (WSMsgType.CLOSED, WSMsgType.CLOSING):
+ break
+
+ elif msg.type == WSMsgType.ERROR:
+ disconnect_warn = 'Connection error'
break
elif msg.type != WSMsgType.TEXT:
@@ -131,9 +169,6 @@ class CloudIoT:
_LOGGER.debug("Publishing message: %s", response)
yield from client.send_json(response)
- except auth_api.CloudError:
- _LOGGER.warning("Unable to connect: Unable to refresh token.")
-
except client_exceptions.WSServerHandshakeError as err:
if err.code == 401:
disconnect_warn = 'Invalid auth.'
@@ -145,38 +180,11 @@ class CloudIoT:
except client_exceptions.ClientError as err:
_LOGGER.warning("Unable to connect: %s", err)
- except Exception: # pylint: disable=broad-except
- if not self.close_requested:
- _LOGGER.exception("Unexpected error")
-
finally:
- if disconnect_warn is not None:
- _LOGGER.warning("Connection closed: %s", disconnect_warn)
-
- if remove_hass_stop_listener is not None:
- remove_hass_stop_listener()
-
- if client is not None:
- self.client = None
- yield from client.close()
-
- if self.close_requested:
- self.state = STATE_DISCONNECTED
-
+ if disconnect_warn is None:
+ _LOGGER.info("Connection closed")
else:
- self.state = STATE_CONNECTING
- self.tries += 1
-
- 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
+ _LOGGER.warning("Connection closed: %s", disconnect_warn)
@asyncio.coroutine
def disconnect(self):
diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py
index c45e3561c47..39c35205619 100644
--- a/homeassistant/components/config/__init__.py
+++ b/homeassistant/components/config/__init__.py
@@ -14,15 +14,23 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
-ON_DEMAND = ('zwave')
+ON_DEMAND = ('zwave',)
+FEATURE_FLAGS = ('config_entries',)
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the config component."""
+ global SECTIONS
+
yield from hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'mdi:settings')
+ # Temporary way of allowing people to opt-in for unreleased config sections
+ for key, value in config.get(DOMAIN, {}).items():
+ if key in FEATURE_FLAGS and value:
+ SECTIONS += (key,)
+
@asyncio.coroutine
def setup_panel(panel_name):
"""Set up a panel."""
@@ -151,7 +159,7 @@ class EditKeyBasedConfigView(BaseEditConfigView):
def _get_value(self, hass, data, config_key):
"""Get value."""
- return data.get(config_key, {})
+ return data.get(config_key)
def _write_value(self, hass, data, config_key, new_value):
"""Set value."""
diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py
new file mode 100644
index 00000000000..ebfa095372a
--- /dev/null
+++ b/homeassistant/components/config/config_entries.py
@@ -0,0 +1,182 @@
+"""Http views to control the config manager."""
+import asyncio
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.http.data_validator import RequestDataValidator
+
+
+REQUIREMENTS = ['voluptuous-serialize==1']
+
+
+@asyncio.coroutine
+def async_setup(hass):
+ """Enable the Home Assistant views."""
+ hass.http.register_view(ConfigManagerEntryIndexView)
+ hass.http.register_view(ConfigManagerEntryResourceView)
+ hass.http.register_view(ConfigManagerFlowIndexView)
+ hass.http.register_view(ConfigManagerFlowResourceView)
+ hass.http.register_view(ConfigManagerAvailableFlowView)
+ return True
+
+
+def _prepare_json(result):
+ """Convert result for JSON."""
+ if result['type'] != config_entries.RESULT_TYPE_FORM:
+ return result
+
+ import voluptuous_serialize
+
+ data = result.copy()
+
+ schema = data['data_schema']
+ if schema is None:
+ data['data_schema'] = []
+ else:
+ data['data_schema'] = voluptuous_serialize.convert(schema)
+
+ return data
+
+
+class ConfigManagerEntryIndexView(HomeAssistantView):
+ """View to get available config entries."""
+
+ url = '/api/config/config_entries/entry'
+ name = 'api:config:config_entries:entry'
+
+ @asyncio.coroutine
+ def get(self, request):
+ """List flows in progress."""
+ hass = request.app['hass']
+ return self.json([{
+ 'entry_id': entry.entry_id,
+ 'domain': entry.domain,
+ 'title': entry.title,
+ 'source': entry.source,
+ 'state': entry.state,
+ } for entry in hass.config_entries.async_entries()])
+
+
+class ConfigManagerEntryResourceView(HomeAssistantView):
+ """View to interact with a config entry."""
+
+ url = '/api/config/config_entries/entry/{entry_id}'
+ name = 'api:config:config_entries:entry:resource'
+
+ @asyncio.coroutine
+ def delete(self, request, entry_id):
+ """Delete a config entry."""
+ hass = request.app['hass']
+
+ try:
+ result = yield from hass.config_entries.async_remove(entry_id)
+ except config_entries.UnknownEntry:
+ return self.json_message('Invalid entry specified', 404)
+
+ return self.json(result)
+
+
+class ConfigManagerFlowIndexView(HomeAssistantView):
+ """View to create config flows."""
+
+ url = '/api/config/config_entries/flow'
+ name = 'api:config:config_entries:flow'
+
+ @asyncio.coroutine
+ def get(self, request):
+ """List flows that are in progress but not started by a user.
+
+ Example of a non-user initiated flow is a discovered Hue hub that
+ requires user interaction to finish setup.
+ """
+ hass = request.app['hass']
+
+ return self.json([
+ flow for flow in hass.config_entries.flow.async_progress()
+ if flow['source'] != config_entries.SOURCE_USER])
+
+ @asyncio.coroutine
+ @RequestDataValidator(vol.Schema({
+ vol.Required('domain'): str,
+ }))
+ def post(self, request, data):
+ """Handle a POST request."""
+ hass = request.app['hass']
+
+ try:
+ result = yield from hass.config_entries.flow.async_init(
+ data['domain'])
+ except config_entries.UnknownHandler:
+ return self.json_message('Invalid handler specified', 404)
+ except config_entries.UnknownStep:
+ return self.json_message('Handler does not support init', 400)
+
+ result = _prepare_json(result)
+
+ return self.json(result)
+
+
+class ConfigManagerFlowResourceView(HomeAssistantView):
+ """View to interact with the flow manager."""
+
+ url = '/api/config/config_entries/flow/{flow_id}'
+ name = 'api:config:config_entries:flow:resource'
+
+ @asyncio.coroutine
+ def get(self, request, flow_id):
+ """Get the current state of a flow."""
+ hass = request.app['hass']
+
+ try:
+ result = yield from hass.config_entries.flow.async_configure(
+ flow_id)
+ except config_entries.UnknownFlow:
+ return self.json_message('Invalid flow specified', 404)
+
+ result = _prepare_json(result)
+
+ return self.json(result)
+
+ @asyncio.coroutine
+ @RequestDataValidator(vol.Schema(dict), allow_empty=True)
+ def post(self, request, flow_id, data):
+ """Handle a POST request."""
+ hass = request.app['hass']
+
+ try:
+ result = yield from hass.config_entries.flow.async_configure(
+ flow_id, data)
+ except config_entries.UnknownFlow:
+ return self.json_message('Invalid flow specified', 404)
+ except vol.Invalid:
+ return self.json_message('User input malformed', 400)
+
+ result = _prepare_json(result)
+
+ return self.json(result)
+
+ @asyncio.coroutine
+ def delete(self, request, flow_id):
+ """Cancel a flow in progress."""
+ hass = request.app['hass']
+
+ try:
+ hass.config_entries.async_abort(flow_id)
+ except config_entries.UnknownFlow:
+ return self.json_message('Invalid flow specified', 404)
+
+ return self.json_message('Flow aborted')
+
+
+class ConfigManagerAvailableFlowView(HomeAssistantView):
+ """View to query available flows."""
+
+ url = '/api/config/config_entries/flow_handlers'
+ name = 'api:config:config_entries:flow_handlers'
+
+ @asyncio.coroutine
+ def get(self, request):
+ """List available flow handlers."""
+ return self.json(config_entries.FLOWS)
diff --git a/homeassistant/components/config_entry_example.py b/homeassistant/components/config_entry_example.py
new file mode 100644
index 00000000000..2d5ea728ff3
--- /dev/null
+++ b/homeassistant/components/config_entry_example.py
@@ -0,0 +1,102 @@
+"""Example component to show how config entries work."""
+
+import asyncio
+
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.const import ATTR_FRIENDLY_NAME
+from homeassistant.util import slugify
+
+
+DOMAIN = 'config_entry_example'
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Setup for our example component."""
+ return True
+
+
+@asyncio.coroutine
+def async_setup_entry(hass, entry):
+ """Initialize an entry."""
+ entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
+ hass.states.async_set(entity_id, 'loaded', {
+ ATTR_FRIENDLY_NAME: entry.data['name']
+ })
+
+ # Indicate setup was successful.
+ return True
+
+
+@asyncio.coroutine
+def async_unload_entry(hass, entry):
+ """Unload an entry."""
+ entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id'])
+ hass.states.async_remove(entity_id)
+
+ # Indicate unload was successful.
+ return True
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class ExampleConfigFlow(config_entries.ConfigFlowHandler):
+ """Handle an example configuration flow."""
+
+ VERSION = 1
+
+ def __init__(self):
+ """Initialize a Hue config handler."""
+ self.object_id = None
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ """Start config flow."""
+ errors = None
+ if user_input is not None:
+ object_id = user_input['object_id']
+
+ if object_id != '' and object_id == slugify(object_id):
+ self.object_id = user_input['object_id']
+ return (yield from self.async_step_name())
+
+ errors = {
+ 'object_id': 'Invalid object id.'
+ }
+
+ return self.async_show_form(
+ title='Pick object id',
+ step_id='init',
+ description="Please enter an object_id for the test entity.",
+ data_schema=vol.Schema({
+ 'object_id': str
+ }),
+ errors=errors
+ )
+
+ @asyncio.coroutine
+ def async_step_name(self, user_input=None):
+ """Ask user to enter the name."""
+ errors = None
+ if user_input is not None:
+ name = user_input['name']
+
+ if name != '':
+ return self.async_create_entry(
+ title=name,
+ data={
+ 'name': name,
+ 'object_id': self.object_id,
+ }
+ )
+
+ return self.async_show_form(
+ title='Name of the entity',
+ step_id='name',
+ description="Please enter a name for the test entity.",
+ data_schema=vol.Schema({
+ 'name': str
+ }),
+ errors=errors
+ )
diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py
index 5187b4782ef..9f325f3eb89 100644
--- a/homeassistant/components/conversation.py
+++ b/homeassistant/components/conversation.py
@@ -7,19 +7,17 @@ https://home-assistant.io/components/conversation/
import asyncio
import logging
import re
-import warnings
import voluptuous as vol
from homeassistant import core
from homeassistant.components import http
-from homeassistant.const import (
- ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
+from homeassistant.components.http.data_validator import (
+ RequestDataValidator)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import intent
-from homeassistant.loader import bind_hass
-REQUIREMENTS = ['fuzzywuzzy==0.16.0']
+from homeassistant.loader import bind_hass
_LOGGER = logging.getLogger(__name__)
@@ -28,9 +26,6 @@ ATTR_TEXT = 'text'
DEPENDENCIES = ['http']
DOMAIN = 'conversation'
-INTENT_TURN_OFF = 'HassTurnOff'
-INTENT_TURN_ON = 'HassTurnOn'
-
REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)')
REGEX_TYPE = type(re.compile(''))
@@ -50,7 +45,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({
@core.callback
@bind_hass
def async_register(hass, intent_type, utterances):
- """Register an intent.
+ """Register utterances and any custom intents.
Registrations don't require conversations to be loaded. They will become
active once the conversation component is loaded.
@@ -75,8 +70,6 @@ def async_register(hass, intent_type, utterances):
@asyncio.coroutine
def async_setup(hass, config):
"""Register the process service."""
- warnings.filterwarnings('ignore', module='fuzzywuzzy')
-
config = config.get(DOMAIN, {})
intents = hass.data.get(DOMAIN)
@@ -102,12 +95,12 @@ def async_setup(hass, config):
hass.http.register_view(ConversationProcessView)
- hass.helpers.intent.async_register(TurnOnIntent())
- hass.helpers.intent.async_register(TurnOffIntent())
- async_register(hass, INTENT_TURN_ON,
+ async_register(hass, intent.INTENT_TURN_ON,
['Turn {name} on', 'Turn on {name}'])
- async_register(hass, INTENT_TURN_OFF, [
- 'Turn {name} off', 'Turn off {name}'])
+ async_register(hass, intent.INTENT_TURN_OFF,
+ ['Turn {name} off', 'Turn off {name}'])
+ async_register(hass, intent.INTENT_TOGGLE,
+ ['Toggle {name}', '{name} toggle'])
return True
@@ -151,86 +144,13 @@ def _process(hass, text):
return response
-@core.callback
-def _match_entity(hass, name):
- """Match a name to an entity."""
- from fuzzywuzzy import process as fuzzyExtract
- entities = {state.entity_id: state.name for state
- in hass.states.async_all()}
- entity_id = fuzzyExtract.extractOne(
- name, entities, score_cutoff=65)[2]
- return hass.states.get(entity_id) if entity_id else None
-
-
-class TurnOnIntent(intent.IntentHandler):
- """Handle turning item on intents."""
-
- intent_type = INTENT_TURN_ON
- slot_schema = {
- 'name': cv.string,
- }
-
- @asyncio.coroutine
- def async_handle(self, intent_obj):
- """Handle turn on intent."""
- hass = intent_obj.hass
- slots = self.async_validate_slots(intent_obj.slots)
- name = slots['name']['value']
- entity = _match_entity(hass, name)
-
- if not entity:
- _LOGGER.error("Could not find entity id for %s", name)
- return None
-
- yield from hass.services.async_call(
- core.DOMAIN, SERVICE_TURN_ON, {
- ATTR_ENTITY_ID: entity.entity_id,
- }, blocking=True)
-
- response = intent_obj.create_response()
- response.async_set_speech(
- 'Turned on {}'.format(entity.name))
- return response
-
-
-class TurnOffIntent(intent.IntentHandler):
- """Handle turning item off intents."""
-
- intent_type = INTENT_TURN_OFF
- slot_schema = {
- 'name': cv.string,
- }
-
- @asyncio.coroutine
- def async_handle(self, intent_obj):
- """Handle turn off intent."""
- hass = intent_obj.hass
- slots = self.async_validate_slots(intent_obj.slots)
- name = slots['name']['value']
- entity = _match_entity(hass, name)
-
- if not entity:
- _LOGGER.error("Could not find entity id for %s", name)
- return None
-
- yield from hass.services.async_call(
- core.DOMAIN, SERVICE_TURN_OFF, {
- ATTR_ENTITY_ID: entity.entity_id,
- }, blocking=True)
-
- response = intent_obj.create_response()
- response.async_set_speech(
- 'Turned off {}'.format(entity.name))
- return response
-
-
class ConversationProcessView(http.HomeAssistantView):
"""View to retrieve shopping list content."""
url = '/api/conversation/process'
name = "api:conversation:process"
- @http.RequestDataValidator(vol.Schema({
+ @RequestDataValidator(vol.Schema({
vol.Required('text'): str,
}))
@asyncio.coroutine
diff --git a/homeassistant/components/cover/demo.py b/homeassistant/components/cover/demo.py
index 827b50c8af9..70e681f1120 100644
--- a/homeassistant/components/cover/demo.py
+++ b/homeassistant/components/cover/demo.py
@@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/
"""
from homeassistant.components.cover import (
- CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE)
+ CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION,
+ ATTR_TILT_POSITION)
from homeassistant.helpers.event import track_utc_time_change
@@ -137,8 +138,9 @@ class DemoCover(CoverDevice):
self._listen_cover_tilt()
self._requested_closing_tilt = False
- def set_cover_position(self, position, **kwargs):
+ def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
+ position = kwargs.get(ATTR_POSITION)
self._set_position = round(position, -1)
if self._position == position:
return
@@ -146,8 +148,9 @@ class DemoCover(CoverDevice):
self._listen_cover()
self._requested_closing = position < self._position
- def set_cover_tilt_position(self, tilt_position, **kwargs):
+ def set_cover_tilt_position(self, **kwargs):
"""Move the cover til to a specific position."""
+ tilt_position = kwargs.get(ATTR_TILT_POSITION)
self._set_tilt_position = round(tilt_position, -1)
if self._tilt_position == tilt_position:
return
diff --git a/homeassistant/components/cover/garadget.py b/homeassistant/components/cover/garadget.py
index 22f5fd889a2..c19aa69c8f0 100644
--- a/homeassistant/components/cover/garadget.py
+++ b/homeassistant/components/cover/garadget.py
@@ -201,21 +201,21 @@ class GaradgetCover(CoverDevice):
"""Check the state of the service during an operation."""
self.schedule_update_ha_state(True)
- def close_cover(self):
+ def close_cover(self, **kwargs):
"""Close the cover."""
if self._state not in ['close', 'closing']:
ret = self._put_command('setState', 'close')
self._start_watcher('close')
return ret.get('return_value') == 1
- def open_cover(self):
+ def open_cover(self, **kwargs):
"""Open the cover."""
if self._state not in ['open', 'opening']:
ret = self._put_command('setState', 'open')
self._start_watcher('open')
return ret.get('return_value') == 1
- def stop_cover(self):
+ def stop_cover(self, **kwargs):
"""Stop the door where it is."""
if self._state not in ['stopped']:
ret = self._put_command('setState', 'stop')
diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py
index 7d77b1bc3be..82ca60e84e6 100644
--- a/homeassistant/components/cover/isy994.py
+++ b/homeassistant/components/cover/isy994.py
@@ -42,10 +42,6 @@ def setup_platform(hass, config: ConfigType,
class ISYCoverDevice(ISYDevice, CoverDevice):
"""Representation of an ISY994 cover device."""
- def __init__(self, node: object) -> None:
- """Initialize the ISY994 cover device."""
- super().__init__(node)
-
@property
def current_cover_position(self) -> int:
"""Return the current cover position."""
@@ -61,8 +57,7 @@ class ISYCoverDevice(ISYDevice, CoverDevice):
"""Get the state of the ISY994 cover device."""
if self.is_unknown():
return None
- else:
- return VALUE_TO_STATE.get(self.value, STATE_OPEN)
+ return VALUE_TO_STATE.get(self.value, STATE_OPEN)
def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover device."""
diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py
index a6cd1263a73..730a2b29a2e 100644
--- a/homeassistant/components/cover/knx.py
+++ b/homeassistant/components/cover/knx.py
@@ -53,9 +53,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up cover(s) for KNX platform."""
- if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
- return
-
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
else:
diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py
index e55072dbc73..0f31d3a9fe0 100644
--- a/homeassistant/components/cover/mqtt.py
+++ b/homeassistant/components/cover/mqtt.py
@@ -65,9 +65,9 @@ TILT_FEATURES = (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT |
SUPPORT_SET_TILT_POSITION)
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_COMMAND_TOPIC, default=None): valid_publish_topic,
- vol.Optional(CONF_POSITION_TOPIC, default=None): valid_publish_topic,
- vol.Optional(CONF_SET_POSITION_TEMPLATE, default=None): cv.template,
+ vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
+ vol.Optional(CONF_POSITION_TOPIC): valid_publish_topic,
+ vol.Optional(CONF_SET_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
@@ -78,8 +78,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_STATE_OPEN, default=STATE_OPEN): cv.string,
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
- vol.Optional(CONF_TILT_COMMAND_TOPIC, default=None): valid_publish_topic,
- vol.Optional(CONF_TILT_STATUS_TOPIC, default=None): valid_subscribe_topic,
+ vol.Optional(CONF_TILT_COMMAND_TOPIC): valid_publish_topic,
+ vol.Optional(CONF_TILT_STATUS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_TILT_CLOSED_POSITION,
default=DEFAULT_TILT_CLOSED_POSITION): int,
vol.Optional(CONF_TILT_OPEN_POSITION,
diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py
index 8d59a90278c..f07d3849fae 100644
--- a/homeassistant/components/cover/myq.py
+++ b/homeassistant/components/cover/myq.py
@@ -84,11 +84,11 @@ class MyQDevice(CoverDevice):
"""Return true if cover is closed, else False."""
return self._status == STATE_CLOSED
- def close_cover(self):
+ def close_cover(self, **kwargs):
"""Issue close command to cover."""
self.myq.close_device(self.device_id)
- def open_cover(self):
+ def open_cover(self, **kwargs):
"""Issue open command to cover."""
self.myq.open_device(self.device_id)
diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py
index d98c71e25fb..d68021d7db3 100644
--- a/homeassistant/components/cover/opengarage.py
+++ b/homeassistant/components/cover/opengarage.py
@@ -115,18 +115,18 @@ class OpenGarageCover(CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed."""
- if self._state == STATE_UNKNOWN:
+ if self._state in [STATE_UNKNOWN, STATE_OFFLINE]:
return None
return self._state in [STATE_CLOSED, STATE_OPENING]
- def close_cover(self):
+ def close_cover(self, **kwargs):
"""Close the cover."""
if self._state not in [STATE_CLOSED, STATE_CLOSING]:
self._state_before_move = self._state
self._state = STATE_CLOSING
self._push_button()
- def open_cover(self):
+ def open_cover(self, **kwargs):
"""Open the cover."""
if self._state not in [STATE_OPEN, STATE_OPENING]:
self._state_before_move = self._state
diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py
index 981312140eb..77cd0b0f7e2 100644
--- a/homeassistant/components/cover/rpi_gpio.py
+++ b/homeassistant/components/cover/rpi_gpio.py
@@ -109,12 +109,12 @@ class RPiGPIOCover(CoverDevice):
sleep(self._relay_time)
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
- def close_cover(self):
+ def close_cover(self, **kwargs):
"""Close the cover."""
if not self.is_closed:
self._trigger()
- def open_cover(self):
+ def open_cover(self, **kwargs):
"""Open the cover."""
if self.is_closed:
self._trigger()
diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml
index 1a3e020ed87..79f00180a89 100644
--- a/homeassistant/components/cover/services.yaml
+++ b/homeassistant/components/cover/services.yaml
@@ -1,63 +1,63 @@
-# Describes the format for available cover services
-
-open_cover:
- description: Open all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to open.
- example: 'cover.living_room'
-
-close_cover:
- description: Close all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to close.
- example: 'cover.living_room'
-
-set_cover_position:
- description: Move to specific position all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to set cover position.
- example: 'cover.living_room'
- position:
- description: Position of the cover (0 to 100).
- example: 30
-
-stop_cover:
- description: Stop all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to stop.
- example: 'cover.living_room'
-
-open_cover_tilt:
- description: Open all or specified cover tilt.
- fields:
- entity_id:
- description: Name(s) of cover(s) tilt to open.
- example: 'cover.living_room'
-
-close_cover_tilt:
- description: Close all or specified cover tilt.
- fields:
- entity_id:
- description: Name(s) of cover(s) to close tilt.
- example: 'cover.living_room'
-
-set_cover_tilt_position:
- description: Move to specific position all or specified cover tilt.
- fields:
- entity_id:
- description: Name(s) of cover(s) to set cover tilt position.
- example: 'cover.living_room'
- tilt_position:
- description: Tilt position of the cover (0 to 100).
- example: 30
-
-stop_cover_tilt:
- description: Stop all or specified cover.
- fields:
- entity_id:
- description: Name(s) of cover(s) to stop.
- example: 'cover.living_room'
+# Describes the format for available cover services
+
+open_cover:
+ description: Open all or specified cover.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) to open.
+ example: 'cover.living_room'
+
+close_cover:
+ description: Close all or specified cover.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) to close.
+ example: 'cover.living_room'
+
+set_cover_position:
+ description: Move to specific position all or specified cover.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) to set cover position.
+ example: 'cover.living_room'
+ position:
+ description: Position of the cover (0 to 100).
+ example: 30
+
+stop_cover:
+ description: Stop all or specified cover.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) to stop.
+ example: 'cover.living_room'
+
+open_cover_tilt:
+ description: Open all or specified cover tilt.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) tilt to open.
+ example: 'cover.living_room'
+
+close_cover_tilt:
+ description: Close all or specified cover tilt.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) to close tilt.
+ example: 'cover.living_room'
+
+set_cover_tilt_position:
+ description: Move to specific position all or specified cover tilt.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) to set cover tilt position.
+ example: 'cover.living_room'
+ tilt_position:
+ description: Tilt position of the cover (0 to 100).
+ example: 30
+
+stop_cover_tilt:
+ description: Stop all or specified cover.
+ fields:
+ entity_id:
+ description: Name(s) of cover(s) to stop.
+ example: 'cover.living_room'
diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py
index 19bd9f01417..6fb8e92e051 100644
--- a/homeassistant/components/cover/tahoma.py
+++ b/homeassistant/components/cover/tahoma.py
@@ -6,7 +6,7 @@ https://home-assistant.io/components/cover.tahoma/
"""
import logging
-from homeassistant.components.cover import CoverDevice
+from homeassistant.components.cover import CoverDevice, ATTR_POSITION
from homeassistant.components.tahoma import (
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
@@ -49,9 +49,9 @@ class TahomaCover(TahomaDevice, CoverDevice):
except KeyError:
return None
- def set_cover_position(self, position, **kwargs):
+ def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
- self.apply_action('setPosition', 100 - position)
+ self.apply_action('setPosition', 100 - kwargs.get(ATTR_POSITION))
@property
def is_closed(self):
@@ -64,8 +64,7 @@ class TahomaCover(TahomaDevice, CoverDevice):
"""Return the class of the device."""
if self.tahoma_device.type == 'io:WindowOpenerVeluxIOComponent':
return 'window'
- else:
- return None
+ return None
def open_cover(self, **kwargs):
"""Open the cover."""
diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py
index 6cf269b75b3..ff9ba6f762b 100644
--- a/homeassistant/components/cover/vera.py
+++ b/homeassistant/components/cover/vera.py
@@ -6,7 +6,8 @@ https://home-assistant.io/components/cover.vera/
"""
import logging
-from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT
+from homeassistant.components.cover import CoverDevice, ENTITY_ID_FORMAT, \
+ ATTR_POSITION
from homeassistant.components.vera import (
VERA_CONTROLLER, VERA_DEVICES, VeraDevice)
@@ -44,9 +45,9 @@ class VeraCover(VeraDevice, CoverDevice):
return 100
return position
- def set_cover_position(self, position, **kwargs):
+ def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
- self.vera_device.set_level(position)
+ self.vera_device.set_level(kwargs.get(ATTR_POSITION))
self.schedule_update_ha_state()
@property
diff --git a/homeassistant/components/cover/wink.py b/homeassistant/components/cover/wink.py
index 35f14e80b5b..093ccd43473 100644
--- a/homeassistant/components/cover/wink.py
+++ b/homeassistant/components/cover/wink.py
@@ -6,7 +6,8 @@ https://home-assistant.io/components/cover.wink/
"""
import asyncio
-from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN
+from homeassistant.components.cover import CoverDevice, STATE_UNKNOWN, \
+ ATTR_POSITION
from homeassistant.components.wink import WinkDevice, DOMAIN
DEPENDENCIES = ['wink']
@@ -42,17 +43,17 @@ class WinkCoverDevice(WinkDevice, CoverDevice):
"""Open the cover."""
self.wink.set_state(1)
- def set_cover_position(self, position, **kwargs):
+ def set_cover_position(self, **kwargs):
"""Move the cover shutter to a specific position."""
- self.wink.set_state(float(position)/100)
+ position = kwargs.get(ATTR_POSITION)
+ self.wink.set_state(position/100)
@property
def current_cover_position(self):
"""Return the current position of cover shutter."""
if self.wink.state() is not None:
return int(self.wink.state()*100)
- else:
- return STATE_UNKNOWN
+ return STATE_UNKNOWN
@property
def is_closed(self):
diff --git a/homeassistant/components/cover/xiaomi_aqara.py b/homeassistant/components/cover/xiaomi_aqara.py
index 29cb707fef5..14321149148 100644
--- a/homeassistant/components/cover/xiaomi_aqara.py
+++ b/homeassistant/components/cover/xiaomi_aqara.py
@@ -1,7 +1,7 @@
"""Support for Xiaomi curtain."""
import logging
-from homeassistant.components.cover import CoverDevice
+from homeassistant.components.cover import CoverDevice, ATTR_POSITION
from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY,
XiaomiDevice)
@@ -55,8 +55,9 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
"""Stop the cover."""
self._write_to_hub(self._sid, **{self._data_key['status']: 'stop'})
- def set_cover_position(self, position, **kwargs):
+ def set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
+ position = kwargs.get(ATTR_POSITION)
self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)})
def parse_data(self, data, raw_data):
diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py
index 15100957242..6f4a11684bd 100644
--- a/homeassistant/components/cover/zwave.py
+++ b/homeassistant/components/cover/zwave.py
@@ -8,7 +8,7 @@ https://home-assistant.io/components/cover.zwave/
# pylint: disable=import-error
import logging
from homeassistant.components.cover import (
- DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE)
+ DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION)
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
@@ -97,9 +97,10 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, CoverDevice):
"""Move the roller shutter down."""
self._network.manager.pressButton(self._close_id)
- def set_cover_position(self, position, **kwargs):
+ def set_cover_position(self, **kwargs):
"""Move the roller shutter to a specific position."""
- self.node.set_dimmer(self.values.primary.value_id, position)
+ self.node.set_dimmer(self.values.primary.value_id,
+ kwargs.get(ATTR_POSITION))
def stop_cover(self, **kwargs):
"""Stop the roller shutter."""
@@ -139,11 +140,11 @@ class ZwaveGarageDoorSwitch(ZwaveGarageDoorBase):
"""Return the current position of Zwave garage door."""
return not self._state
- def close_cover(self):
+ def close_cover(self, **kwargs):
"""Close the garage door."""
self.values.primary.data = False
- def open_cover(self):
+ def open_cover(self, **kwargs):
"""Open the garage door."""
self.values.primary.data = True
@@ -166,10 +167,10 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase):
"""Return the current position of Zwave garage door."""
return self._state == "Closed"
- def close_cover(self):
+ def close_cover(self, **kwargs):
"""Close the garage door."""
self.values.primary.data = "Closed"
- def open_cover(self):
+ def open_cover(self, **kwargs):
"""Open the garage door."""
self.values.primary.data = "Opened"
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 9d7d253c328..693f3e4470a 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -4,6 +4,7 @@ Support for deCONZ devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/deconz/
"""
+
import asyncio
import logging
@@ -17,11 +18,12 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.json import load_json, save_json
-REQUIREMENTS = ['pydeconz==27']
+REQUIREMENTS = ['pydeconz==30']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'deconz'
+DATA_DECONZ_ID = 'deconz_entities'
CONFIG_FILE = 'deconz.conf'
@@ -34,13 +36,16 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
SERVICE_FIELD = 'field'
+SERVICE_ENTITY = 'entity'
SERVICE_DATA = 'data'
SERVICE_SCHEMA = vol.Schema({
- vol.Required(SERVICE_FIELD): cv.string,
+ vol.Exclusive(SERVICE_FIELD, 'deconz_id'): cv.string,
+ vol.Exclusive(SERVICE_ENTITY, 'deconz_id'): cv.entity_id,
vol.Required(SERVICE_DATA): dict,
})
+
CONFIG_INSTRUCTIONS = """
Unlock your deCONZ gateway to register with Home Assistant.
@@ -100,6 +105,7 @@ def async_setup_deconz(hass, config, deconz_config):
return False
hass.data[DOMAIN] = deconz
+ hass.data[DATA_DECONZ_ID] = {}
for component in ['binary_sensor', 'light', 'scene', 'sensor']:
hass.async_add_job(discovery.async_load_platform(
@@ -112,6 +118,7 @@ def async_setup_deconz(hass, config, deconz_config):
Field is a string representing a specific device in deCONZ
e.g. field='/lights/1/state'.
+ Entity_id can be used to retrieve the proper field.
Data is a json object with what data you want to alter
e.g. data={'on': true}.
{
@@ -121,9 +128,17 @@ def async_setup_deconz(hass, config, deconz_config):
See Dresden Elektroniks REST API documentation for details:
http://dresden-elektronik.github.io/deconz-rest-doc/rest/
"""
- deconz = hass.data[DOMAIN]
field = call.data.get(SERVICE_FIELD)
+ entity_id = call.data.get(SERVICE_ENTITY)
data = call.data.get(SERVICE_DATA)
+ deconz = hass.data[DOMAIN]
+ if entity_id:
+ entities = hass.data.get(DATA_DECONZ_ID)
+ if entities:
+ field = entities.get(entity_id)
+ if field is None:
+ _LOGGER.error('Could not find the entity %s', entity_id)
+ return
yield from deconz.async_put_state(field, data)
hass.services.async_register(
DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA)
diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml
index 2e6593c6ea0..78bf7041a93 100644
--- a/homeassistant/components/deconz/services.yaml
+++ b/homeassistant/components/deconz/services.yaml
@@ -1,10 +1,13 @@
configure:
- description: Set attribute of device in Deconz. See Dresden Elektroniks REST API documentation for details http://dresden-elektronik.github.io/deconz-rest-doc/rest/
+ description: Set attribute of device in deCONZ. See https://home-assistant.io/components/deconz/#device-services for details.
fields:
field:
- description: Field is a string representing a specific device in Deconz.
+ description: Field is a string representing a specific device in deCONZ.
example: '/lights/1/state'
+ entity:
+ description: Entity id representing a specific device in deCONZ.
+ example: 'light.rgb_light'
data:
description: Data is a json object with what data you want to alter.
example: '{"on": true}'
diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py
index 2adee1e2330..19ab77350f3 100644
--- a/homeassistant/components/device_tracker/__init__.py
+++ b/homeassistant/components/device_tracker/__init__.py
@@ -99,17 +99,17 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
@bind_hass
-def is_on(hass: HomeAssistantType, entity_id: str=None):
+def is_on(hass: HomeAssistantType, entity_id: str = None):
"""Return the state if any or a specified device is home."""
entity = entity_id or ENTITY_ID_ALL_DEVICES
return hass.states.is_state(entity, STATE_HOME)
-def see(hass: HomeAssistantType, mac: str=None, dev_id: str=None,
- host_name: str=None, location_name: str=None,
- gps: GPSType=None, gps_accuracy=None,
- battery=None, attributes: dict=None):
+def see(hass: HomeAssistantType, mac: str = None, dev_id: str = None,
+ host_name: str = None, location_name: str = None,
+ gps: GPSType = None, gps_accuracy=None,
+ battery=None, attributes: dict = None):
"""Call service to notify you see device."""
data = {key: value for key, value in
((ATTR_MAC, mac),
@@ -239,11 +239,11 @@ class DeviceTracker(object):
_LOGGER.warning('Duplicate device MAC addresses detected %s',
dev.mac)
- def see(self, mac: str=None, dev_id: str=None, host_name: str=None,
- location_name: str=None, gps: GPSType=None, gps_accuracy=None,
- battery: str=None, attributes: dict=None,
- source_type: str=SOURCE_TYPE_GPS, picture: str=None,
- icon: str=None):
+ def see(self, mac: str = None, dev_id: str = None, host_name: str = None,
+ location_name: str = None, gps: GPSType = None, gps_accuracy=None,
+ battery: str = None, attributes: dict = None,
+ source_type: str = SOURCE_TYPE_GPS, picture: str = None,
+ icon: str = None):
"""Notify the device tracker that you see a device."""
self.hass.add_job(
self.async_see(mac, dev_id, host_name, location_name, gps,
@@ -252,11 +252,11 @@ class DeviceTracker(object):
)
@asyncio.coroutine
- def async_see(self, mac: str=None, dev_id: str=None, host_name: str=None,
- location_name: str=None, gps: GPSType=None,
- gps_accuracy=None, battery: str=None, attributes: dict=None,
- source_type: str=SOURCE_TYPE_GPS, picture: str=None,
- icon: str=None):
+ def async_see(self, mac: str = None, dev_id: str = None,
+ host_name: str = None, location_name: str = None,
+ gps: GPSType = None, gps_accuracy=None, battery: str = None,
+ attributes: dict = None, source_type: str = SOURCE_TYPE_GPS,
+ picture: str = None, icon: str = None):
"""Notify the device tracker that you see a device.
This method is a coroutine.
@@ -396,9 +396,9 @@ class Device(Entity):
_state = STATE_NOT_HOME
def __init__(self, hass: HomeAssistantType, consider_home: timedelta,
- track: bool, dev_id: str, mac: str, name: str=None,
- picture: str=None, gravatar: str=None, icon: str=None,
- hide_if_away: bool=False, vendor: str=None) -> None:
+ track: bool, dev_id: str, mac: str, name: str = None,
+ picture: str = None, gravatar: str = None, icon: str = None,
+ hide_if_away: bool = False, vendor: str = None) -> None:
"""Initialize a device."""
self.hass = hass
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@@ -475,9 +475,10 @@ class Device(Entity):
return self.away_hide and self.state != STATE_HOME
@asyncio.coroutine
- def async_seen(self, host_name: str=None, location_name: str=None,
- gps: GPSType=None, gps_accuracy=0, battery: str=None,
- attributes: dict=None, source_type: str=SOURCE_TYPE_GPS):
+ def async_seen(self, host_name: str = None, location_name: str = None,
+ gps: GPSType = None, gps_accuracy=0, battery: str = None,
+ attributes: dict = None,
+ source_type: str = SOURCE_TYPE_GPS):
"""Mark the device as seen."""
self.source_type = source_type
self.last_seen = dt_util.utcnow()
@@ -504,7 +505,7 @@ class Device(Entity):
# pylint: disable=not-an-iterable
yield from self.async_update()
- def stale(self, now: dt_util.dt.datetime=None):
+ def stale(self, now: dt_util.dt.datetime = None):
"""Return if device state is stale.
Async friendly.
@@ -621,16 +622,16 @@ class DeviceScanner(object):
"""
return self.hass.async_add_job(self.scan_devices)
- def get_device_name(self, mac: str) -> str:
- """Get device name from mac."""
+ def get_device_name(self, device: str) -> str:
+ """Get the name of a device."""
raise NotImplementedError()
- def async_get_device_name(self, mac: str) -> Any:
- """Get device name from mac.
+ def async_get_device_name(self, device: str) -> Any:
+ """Get the name of a device.
This method must be run in the event loop and returns a coroutine.
"""
- return self.hass.async_add_job(self.get_device_name, mac)
+ return self.hass.async_add_job(self.get_device_name, device)
def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta):
@@ -648,8 +649,7 @@ def async_load_config(path: str, hass: HomeAssistantType,
"""
dev_schema = vol.Schema({
vol.Required(CONF_NAME): cv.string,
- vol.Optional(CONF_ICON, default=False):
- vol.Any(None, cv.icon),
+ vol.Optional(CONF_ICON, default=None): vol.Any(None, cv.icon),
vol.Optional('track', default=False): cv.boolean,
vol.Optional(CONF_MAC, default=None):
vol.Any(None, vol.All(cv.string, vol.Upper)),
diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py
index fb47b26a687..1956e42cb78 100644
--- a/homeassistant/components/device_tracker/asuswrt.py
+++ b/homeassistant/components/device_tracker/asuswrt.py
@@ -63,6 +63,7 @@ _IP_NEIGH_REGEX = re.compile(
r'\w+\s'
r'(\w+\s(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s'
r'\s?(router)?'
+ r'\s?(nud)?'
r'(?P(\w+))')
_ARP_CMD = 'arp -n'
@@ -118,11 +119,10 @@ class AsusWrtDeviceScanner(DeviceScanner):
if self.protocol == 'ssh':
self.connection = SshConnection(
self.host, self.port, self.username, self.password,
- self.ssh_key, self.mode == 'ap')
+ self.ssh_key)
else:
self.connection = TelnetConnection(
- self.host, self.port, self.username, self.password,
- self.mode == 'ap')
+ self.host, self.port, self.username, self.password)
self.last_results = {}
@@ -212,6 +212,9 @@ class AsusWrtDeviceScanner(DeviceScanner):
result = _parse_lines(lines, _IP_NEIGH_REGEX)
devices = {}
for device in result:
+ status = device['status']
+ if status is None or status.upper() != 'REACHABLE':
+ continue
if device['mac'] is not None:
mac = device['mac'].upper()
old_device = cur_devices.get(mac)
@@ -226,7 +229,7 @@ class AsusWrtDeviceScanner(DeviceScanner):
result = _parse_lines(lines, _ARP_REGEX)
devices = {}
for device in result:
- if device['mac']:
+ if device['mac'] is not None:
mac = device['mac'].upper()
devices[mac] = Device(mac, device['ip'], None)
return devices
@@ -253,7 +256,7 @@ class _Connection:
class SshConnection(_Connection):
"""Maintains an SSH connection to an ASUS-WRT router."""
- def __init__(self, host, port, username, password, ssh_key, ap):
+ def __init__(self, host, port, username, password, ssh_key):
"""Initialize the SSH connection properties."""
super().__init__()
@@ -263,7 +266,6 @@ class SshConnection(_Connection):
self._username = username
self._password = password
self._ssh_key = ssh_key
- self._ap = ap
def run_command(self, command):
"""Run commands through an SSH connection.
@@ -323,7 +325,7 @@ class SshConnection(_Connection):
class TelnetConnection(_Connection):
"""Maintains a Telnet connection to an ASUS-WRT router."""
- def __init__(self, host, port, username, password, ap):
+ def __init__(self, host, port, username, password):
"""Initialize the Telnet connection properties."""
super().__init__()
@@ -332,7 +334,6 @@ class TelnetConnection(_Connection):
self._port = port
self._username = username
self._password = password
- self._ap = ap
self._prompt_string = None
def run_command(self, command):
diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py
index 5ad3995ad2a..607f236f920 100644
--- a/homeassistant/components/device_tracker/automatic.py
+++ b/homeassistant/components/device_tracker/automatic.py
@@ -23,7 +23,7 @@ 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
-REQUIREMENTS = ['aioautomatic==0.6.4']
+REQUIREMENTS = ['aioautomatic==0.6.5']
_LOGGER = logging.getLogger(__name__)
@@ -49,8 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_SECRET): cv.string,
vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean,
- vol.Optional(CONF_DEVICES, default=None):
- vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_DEVICES): vol.All(cv.ensure_list, [cv.string]),
})
@@ -109,7 +108,7 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
_write_refresh_token_to_file, hass, filename,
session.refresh_token)
data = AutomaticData(
- hass, client, session, config[CONF_DEVICES], async_see)
+ hass, client, session, config.get(CONF_DEVICES), async_see)
# Load the initial vehicle data
vehicles = yield from session.get_vehicles()
@@ -177,10 +176,9 @@ class AutomaticAuthCallbackView(HomeAssistantView):
_LOGGER.error(
"Error authorizing Automatic: %s", params['error'])
return response
- else:
- _LOGGER.error(
- "Error authorizing Automatic. Invalid response returned")
- return response
+ _LOGGER.error(
+ "Error authorizing Automatic. Invalid response returned")
+ return response
if DATA_CONFIGURING not in hass.data or \
params['state'] not in hass.data[DATA_CONFIGURING]:
diff --git a/homeassistant/components/device_tracker/bbox.py b/homeassistant/components/device_tracker/bbox.py
index 23a94d093e2..6d870364dcb 100644
--- a/homeassistant/components/device_tracker/bbox.py
+++ b/homeassistant/components/device_tracker/bbox.py
@@ -45,10 +45,10 @@ class BboxDeviceScanner(DeviceScanner):
return [device.mac for device in self.last_results]
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
- filter_named = [device.name for device in self.last_results if
- device.mac == mac]
+ filter_named = [result.name for result in self.last_results if
+ result.mac == device]
if filter_named:
return filter_named[0]
diff --git a/homeassistant/components/device_tracker/bluetooth_le_tracker.py b/homeassistant/components/device_tracker/bluetooth_le_tracker.py
index 19582822913..d9cda24b699 100644
--- a/homeassistant/components/device_tracker/bluetooth_le_tracker.py
+++ b/homeassistant/components/device_tracker/bluetooth_le_tracker.py
@@ -102,7 +102,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
"""Lookup Bluetooth LE devices and update status."""
devs = discover_ble_devices()
for mac in devs_to_track:
- _LOGGER.debug("Checking " + mac)
+ _LOGGER.debug("Checking %s", mac)
result = mac in devs
if not result:
# Could not lookup device name
diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py
index a535d87105e..9d41611d9a2 100644
--- a/homeassistant/components/device_tracker/bluetooth_tracker.py
+++ b/homeassistant/components/device_tracker/bluetooth_tracker.py
@@ -41,7 +41,7 @@ def setup_scanner(hass, config, see, discovery_info=None):
result = bluetooth.discover_devices(
duration=8, lookup_names=True, flush_cache=True,
lookup_class=False)
- _LOGGER.debug("Bluetooth devices discovered = " + str(len(result)))
+ _LOGGER.debug("Bluetooth devices discovered = %d", len(result))
return result
yaml_path = hass.config.path(YAML_DEVICES)
diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py
new file mode 100644
index 00000000000..6ba2681e4cd
--- /dev/null
+++ b/homeassistant/components/device_tracker/bmw_connected_drive.py
@@ -0,0 +1,51 @@
+"""Device tracker for BMW Connected Drive vehicles.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/device_tracker.bmw_connected_drive/
+"""
+import logging
+
+from homeassistant.components.bmw_connected_drive import DOMAIN \
+ as BMW_DOMAIN
+from homeassistant.util import slugify
+
+DEPENDENCIES = ['bmw_connected_drive']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_scanner(hass, config, see, discovery_info=None):
+ """Set up the BMW tracker."""
+ accounts = hass.data[BMW_DOMAIN]
+ _LOGGER.debug('Found BMW accounts: %s',
+ ', '.join([a.name for a in accounts]))
+ for account in accounts:
+ for vehicle in account.account.vehicles:
+ tracker = BMWDeviceTracker(see, vehicle)
+ account.add_update_listener(tracker.update)
+ tracker.update()
+ return True
+
+
+class BMWDeviceTracker(object):
+ """BMW Connected Drive device tracker."""
+
+ def __init__(self, see, vehicle):
+ """Initialize the Tracker."""
+ self._see = see
+ self.vehicle = vehicle
+
+ def update(self) -> None:
+ """Update the device info."""
+ dev_id = slugify(self.vehicle.modelName)
+ _LOGGER.debug('Updating %s', dev_id)
+ attrs = {
+ 'trackr_id': dev_id,
+ 'id': dev_id,
+ 'name': self.vehicle.modelName
+ }
+ self._see(
+ dev_id=dev_id, host_name=self.vehicle.modelName,
+ gps=self.vehicle.state.gps_position, attributes=attrs,
+ icon='mdi:car'
+ )
diff --git a/homeassistant/components/device_tracker/fritz.py b/homeassistant/components/device_tracker/fritz.py
index 58c23cb7d76..8c9d1988a71 100644
--- a/homeassistant/components/device_tracker/fritz.py
+++ b/homeassistant/components/device_tracker/fritz.py
@@ -75,9 +75,9 @@ class FritzBoxScanner(DeviceScanner):
active_hosts.append(known_host['mac'])
return active_hosts
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name of the given device or None if is not known."""
- ret = self.fritz_box.get_specific_host_entry(mac).get(
+ ret = self.fritz_box.get_specific_host_entry(device).get(
'NewHostName'
)
if ret == {}:
diff --git a/homeassistant/components/device_tracker/geofency.py b/homeassistant/components/device_tracker/geofency.py
index 58d69f39a1d..adb5c6f6d28 100644
--- a/homeassistant/components/device_tracker/geofency.py
+++ b/homeassistant/components/device_tracker/geofency.py
@@ -120,8 +120,7 @@ class GeofencyView(HomeAssistantView):
"""Return name of device tracker."""
if 'beaconUUID' in data:
return "{}_{}".format(BEACON_DEV_PREFIX, data['name'])
- else:
- return data['device']
+ return data['device']
@asyncio.coroutine
def _set_location(self, hass, data, location_name):
diff --git a/homeassistant/components/device_tracker/hitron_coda.py b/homeassistant/components/device_tracker/hitron_coda.py
index 17dc34d1040..aa437eeef86 100644
--- a/homeassistant/components/device_tracker/hitron_coda.py
+++ b/homeassistant/components/device_tracker/hitron_coda.py
@@ -60,11 +60,11 @@ class HitronCODADeviceScanner(DeviceScanner):
return [device.mac for device in self.last_results]
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""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)
+ result.name for result in self.last_results
+ if result.mac == device), None)
return name
def _login(self):
diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py
index 357dd0d36cf..775075b8a4a 100644
--- a/homeassistant/components/device_tracker/huawei_router.py
+++ b/homeassistant/components/device_tracker/huawei_router.py
@@ -86,6 +86,7 @@ class HuaweiDeviceScanner(DeviceScanner):
active_clients = [client for client in data if client.state]
self.last_results = active_clients
+ # pylint: disable=logging-not-lazy
_LOGGER.debug("Active clients: " + "\n"
.join((client.mac + " " + client.name)
for client in active_clients))
diff --git a/homeassistant/components/device_tracker/keenetic_ndms2.py b/homeassistant/components/device_tracker/keenetic_ndms2.py
index 5a7db36e479..36dc1182a92 100644
--- a/homeassistant/components/device_tracker/keenetic_ndms2.py
+++ b/homeassistant/components/device_tracker/keenetic_ndms2.py
@@ -67,10 +67,10 @@ class KeeneticNDMS2DeviceScanner(DeviceScanner):
return [device.mac for device in self.last_results]
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
- filter_named = [device.name for device in self.last_results
- if device.mac == mac]
+ filter_named = [result.name for result in self.last_results
+ if result.mac == device]
if filter_named:
return filter_named[0]
diff --git a/homeassistant/components/device_tracker/linksys_ap.py b/homeassistant/components/device_tracker/linksys_ap.py
index 20dc9052e11..8837b628b32 100644
--- a/homeassistant/components/device_tracker/linksys_ap.py
+++ b/homeassistant/components/device_tracker/linksys_ap.py
@@ -62,7 +62,7 @@ class LinksysAPDeviceScanner(DeviceScanner):
return self.last_results
# pylint: disable=no-self-use
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""
Return the name (if known) of the device.
diff --git a/homeassistant/components/device_tracker/linksys_smart.py b/homeassistant/components/device_tracker/linksys_smart.py
index 4bcbb600b8b..c92f940f526 100644
--- a/homeassistant/components/device_tracker/linksys_smart.py
+++ b/homeassistant/components/device_tracker/linksys_smart.py
@@ -45,9 +45,9 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner):
return self.last_results.keys()
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name (if known) of the device."""
- return self.last_results.get(mac)
+ return self.last_results.get(device)
def _update_info(self):
"""Check for connected devices."""
diff --git a/homeassistant/components/device_tracker/meraki.py b/homeassistant/components/device_tracker/meraki.py
index 9437486a0aa..9bbc6bf9ffe 100644
--- a/homeassistant/components/device_tracker/meraki.py
+++ b/homeassistant/components/device_tracker/meraki.py
@@ -85,7 +85,7 @@ class MerakiView(HomeAssistantView):
return self.json_message('Invalid device type',
HTTP_UNPROCESSABLE_ENTITY)
_LOGGER.debug("Processing %s", data['type'])
- if len(data["data"]["observations"]) == 0:
+ if not data["data"]["observations"]:
_LOGGER.debug("No observations found")
return
self._handle(request.app['hass'], data)
@@ -107,8 +107,7 @@ class MerakiView(HomeAssistantView):
if lat == "NaN" or lng == "NaN":
_LOGGER.debug(
- "No coordinates received, skipping location for: " + mac
- )
+ "No coordinates received, skipping location for: %s", mac)
gps_location = None
accuracy = None
else:
diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py
index 1805559c252..1d9161c0d45 100644
--- a/homeassistant/components/device_tracker/mikrotik.py
+++ b/homeassistant/components/device_tracker/mikrotik.py
@@ -137,9 +137,9 @@ class MikrotikScanner(DeviceScanner):
self._update_info()
return [device for device in self.last_results]
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
- return self.last_results.get(mac)
+ return self.last_results.get(device)
def _update_info(self):
"""Retrieve latest information from the Mikrotik box."""
diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py
index 7bcad60236a..9a5532fc9f4 100644
--- a/homeassistant/components/device_tracker/mqtt_json.py
+++ b/homeassistant/components/device_tracker/mqtt_json.py
@@ -26,8 +26,8 @@ _LOGGER = logging.getLogger(__name__)
GPS_JSON_PAYLOAD_SCHEMA = vol.Schema({
vol.Required(ATTR_LATITUDE): vol.Coerce(float),
vol.Required(ATTR_LONGITUDE): vol.Coerce(float),
- vol.Optional(ATTR_GPS_ACCURACY, default=None): vol.Coerce(int),
- vol.Optional(ATTR_BATTERY_LEVEL, default=None): vol.Coerce(str),
+ vol.Optional(ATTR_GPS_ACCURACY): vol.Coerce(int),
+ vol.Optional(ATTR_BATTERY_LEVEL): vol.Coerce(str),
}, extra=vol.ALLOW_EXTRA)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend({
diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py
index d2b8bc274ca..25d5d38b2a7 100644
--- a/homeassistant/components/device_tracker/netgear.py
+++ b/homeassistant/components/device_tracker/netgear.py
@@ -70,11 +70,11 @@ class NetgearDeviceScanner(DeviceScanner):
return (device.mac for device in self.last_results)
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
try:
- return next(device.name for device in self.last_results
- if device.mac == mac)
+ return next(result.name for result in self.last_results
+ if result.mac == device)
except StopIteration:
return None
diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py
index e9d70142ad1..23cb7ea8f9d 100644
--- a/homeassistant/components/device_tracker/nmap_tracker.py
+++ b/homeassistant/components/device_tracker/nmap_tracker.py
@@ -33,7 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOSTS): cv.ensure_list,
vol.Required(CONF_HOME_INTERVAL, default=0): cv.positive_int,
vol.Optional(CONF_EXCLUDE, default=[]):
- vol.All(cv.ensure_list, vol.Length(min=1)),
+ vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_OPTIONS, default=DEFAULT_OPTIONS):
cv.string
})
@@ -41,9 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_scanner(hass, config):
"""Validate the configuration and return a Nmap scanner."""
- scanner = NmapDeviceScanner(config[DOMAIN])
-
- return scanner if scanner.success_init else None
+ return NmapDeviceScanner(config[DOMAIN])
Device = namedtuple('Device', ['mac', 'name', 'ip', 'last_update'])
@@ -76,7 +74,6 @@ class NmapDeviceScanner(DeviceScanner):
self._options = config[CONF_OPTIONS]
self.home_interval = timedelta(minutes=minutes)
- self.success_init = self._update_info()
_LOGGER.info("Scanner initialized")
def scan_devices(self):
@@ -85,10 +82,10 @@ class NmapDeviceScanner(DeviceScanner):
return [device.mac for device in self.last_results]
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
- filter_named = [device.name for device in self.last_results
- if device.mac == mac]
+ filter_named = [result.name for result in self.last_results
+ if result.mac == device]
if filter_named:
return filter_named[0]
diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py
index fca4998f7b5..dcf06036ea0 100644
--- a/homeassistant/components/device_tracker/tado.py
+++ b/homeassistant/components/device_tracker/tado.py
@@ -83,10 +83,10 @@ class TadoDeviceScanner(DeviceScanner):
return [device.mac for device in self.last_results]
@asyncio.coroutine
- def async_get_device_name(self, mac):
+ def async_get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
- filter_named = [device.name for device in self.last_results
- if device.mac == mac]
+ filter_named = [result.name for result in self.last_results
+ if result.mac == device]
if filter_named:
return filter_named[0]
diff --git a/homeassistant/components/device_tracker/tomato.py b/homeassistant/components/device_tracker/tomato.py
index 7cebf0abdf4..01ae2977f6d 100644
--- a/homeassistant/components/device_tracker/tomato.py
+++ b/homeassistant/components/device_tracker/tomato.py
@@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_PORT, default=-1): cv.port,
+ vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=True): vol.Any(
cv.boolean, cv.isfile),
@@ -45,13 +45,11 @@ class TomatoDeviceScanner(DeviceScanner):
def __init__(self, config):
"""Initialize the scanner."""
host, http_id = config[CONF_HOST], config[CONF_HTTP_ID]
- port = config[CONF_PORT]
+ port = config.get(CONF_PORT)
username, password = config[CONF_USERNAME], config[CONF_PASSWORD]
self.ssl, self.verify_ssl = config[CONF_SSL], config[CONF_VERIFY_SSL]
- if port == -1:
- port = 80
- if self.ssl:
- port = 443
+ if port is None:
+ port = 443 if self.ssl else 80
self.req = requests.Request(
'POST', 'http{}://{}:{}/update.cgi'.format(
diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py
index e66bb95a11a..946aae5fe56 100644
--- a/homeassistant/components/device_tracker/ubus.py
+++ b/homeassistant/components/device_tracker/ubus.py
@@ -96,11 +96,11 @@ class UbusDeviceScanner(DeviceScanner):
raise NotImplementedError
@_refresh_on_access_denied
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if self.mac2name is None:
self._generate_mac2name()
- name = self.mac2name.get(mac.upper(), None)
+ name = self.mac2name.get(device.upper(), None)
return name
@_refresh_on_access_denied
diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py
index d5b6b044f1f..8663930c4e6 100644
--- a/homeassistant/components/device_tracker/unifi.py
+++ b/homeassistant/components/device_tracker/unifi.py
@@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'
CONF_SITE_ID = 'site_id'
CONF_DETECTION_TIME = 'detection_time'
+CONF_SSID_FILTER = 'ssid_filter'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8443
@@ -39,7 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any(
cv.boolean, cv.isfile),
vol.Optional(CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME): vol.All(
- cv.time_period, cv.positive_timedelta)
+ cv.time_period, cv.positive_timedelta),
+ vol.Optional(CONF_SSID_FILTER): vol.All(cv.ensure_list, [cv.string])
})
@@ -54,6 +56,7 @@ def get_scanner(hass, config):
port = config[DOMAIN].get(CONF_PORT)
verify_ssl = config[DOMAIN].get(CONF_VERIFY_SSL)
detection_time = config[DOMAIN].get(CONF_DETECTION_TIME)
+ ssid_filter = config[DOMAIN].get(CONF_SSID_FILTER)
try:
ctrl = Controller(host, username, password, port, version='v4',
@@ -69,16 +72,18 @@ def get_scanner(hass, config):
notification_id=NOTIFICATION_ID)
return False
- return UnifiScanner(ctrl, detection_time)
+ return UnifiScanner(ctrl, detection_time, ssid_filter)
class UnifiScanner(DeviceScanner):
"""Provide device_tracker support from Unifi WAP client data."""
- def __init__(self, controller, detection_time: timedelta) -> None:
+ def __init__(self, controller, detection_time: timedelta,
+ ssid_filter) -> None:
"""Initialize the scanner."""
self._detection_time = detection_time
self._controller = controller
+ self._ssid_filter = ssid_filter
self._update()
def _update(self):
@@ -90,6 +95,11 @@ class UnifiScanner(DeviceScanner):
_LOGGER.error("Failed to scan clients: %s", ex)
clients = []
+ # Filter clients to provided SSID list
+ if self._ssid_filter:
+ clients = [client for client in clients
+ if client['essid'] in self._ssid_filter]
+
self._clients = {
client['mac']: client
for client in clients
@@ -101,13 +111,13 @@ class UnifiScanner(DeviceScanner):
self._update()
return self._clients.keys()
- def get_device_name(self, mac):
+ def get_device_name(self, device):
"""Return the name (if known) of the device.
If a name has been set in Unifi, then return that, else
return the hostname if it has been detected.
"""
- client = self._clients.get(mac, {})
+ client = self._clients.get(device, {})
name = client.get('name') or client.get('hostname')
- _LOGGER.debug("Device mac %s name %s", mac, name)
+ _LOGGER.debug("Device mac %s name %s", device, name)
return name
diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos.py
index 4bdb4c80add..2c9f763aaa8 100644
--- a/homeassistant/components/dominos.py
+++ b/homeassistant/components/dominos.py
@@ -140,21 +140,20 @@ class Dominos():
if self.closest_store is None:
_LOGGER.warning('Cannot get menu. Store may be closed')
return []
- else:
- menu = self.closest_store.get_menu()
- product_entries = []
+ menu = self.closest_store.get_menu()
+ product_entries = []
- for product in menu.products:
- item = {}
- if isinstance(product.menu_data['Variants'], list):
- variants = ', '.join(product.menu_data['Variants'])
- else:
- variants = product.menu_data['Variants']
- item['name'] = product.name
- item['variants'] = variants
- product_entries.append(item)
+ for product in menu.products:
+ item = {}
+ if isinstance(product.menu_data['Variants'], list):
+ variants = ', '.join(product.menu_data['Variants'])
+ else:
+ variants = product.menu_data['Variants']
+ item['name'] = product.name
+ item['variants'] = variants
+ product_entries.append(item)
- return product_entries
+ return product_entries
class DominosProductListView(http.HomeAssistantView):
@@ -203,8 +202,7 @@ class DominosOrder(Entity):
"""Return the state either closed, orderable or unorderable."""
if self.dominos.closest_store is None:
return 'closed'
- else:
- return 'orderable' if self._orderable else 'unorderable'
+ return 'orderable' if self._orderable else 'unorderable'
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py
index 9fba21b81dc..c89e4fda358 100644
--- a/homeassistant/components/emulated_hue/__init__.py
+++ b/homeassistant/components/emulated_hue/__init__.py
@@ -14,7 +14,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.components.http import REQUIREMENTS # NOQA
-from homeassistant.components.http import HomeAssistantWSGI
+from homeassistant.components.http import HomeAssistantHTTP
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.deprecation import get_deprecated
import homeassistant.helpers.config_validation as cv
@@ -86,7 +86,7 @@ def setup(hass, yaml_config):
"""Activate the emulated_hue component."""
config = Config(hass, yaml_config.get(DOMAIN, {}))
- server = HomeAssistantWSGI(
+ server = HomeAssistantHTTP(
hass,
server_host=config.host_ip_addr,
server_port=config.listen_port,
diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py
index 6e6d377986d..66790d02687 100644
--- a/homeassistant/components/fan/__init__.py
+++ b/homeassistant/components/fan/__init__.py
@@ -118,7 +118,7 @@ SERVICE_TO_METHOD = {
@bind_hass
-def is_on(hass, entity_id: str=None) -> bool:
+def is_on(hass, entity_id: str = None) -> bool:
"""Return if the fans are on based on the statemachine."""
entity_id = entity_id or ENTITY_ID_ALL_FANS
state = hass.states.get(entity_id)
@@ -126,7 +126,7 @@ def is_on(hass, entity_id: str=None) -> bool:
@bind_hass
-def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
+def turn_on(hass, entity_id: str = None, speed: str = None) -> None:
"""Turn all or specified fan on."""
data = {
key: value for key, value in [
@@ -139,7 +139,7 @@ def turn_on(hass, entity_id: str=None, speed: str=None) -> None:
@bind_hass
-def turn_off(hass, entity_id: str=None) -> None:
+def turn_off(hass, entity_id: str = None) -> None:
"""Turn all or specified fan off."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
@@ -147,7 +147,7 @@ def turn_off(hass, entity_id: str=None) -> None:
@bind_hass
-def toggle(hass, entity_id: str=None) -> None:
+def toggle(hass, entity_id: str = None) -> None:
"""Toggle all or specified fans."""
data = {
ATTR_ENTITY_ID: entity_id
@@ -157,7 +157,8 @@ def toggle(hass, entity_id: str=None) -> None:
@bind_hass
-def oscillate(hass, entity_id: str=None, should_oscillate: bool=True) -> None:
+def oscillate(hass, entity_id: str = None,
+ should_oscillate: bool = True) -> None:
"""Set oscillation on all or specified fan."""
data = {
key: value for key, value in [
@@ -170,7 +171,7 @@ def oscillate(hass, entity_id: str=None, should_oscillate: bool=True) -> None:
@bind_hass
-def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
+def set_speed(hass, entity_id: str = None, speed: str = None) -> None:
"""Set speed for all or specified fan."""
data = {
key: value for key, value in [
@@ -183,7 +184,7 @@ def set_speed(hass, entity_id: str=None, speed: str=None) -> None:
@bind_hass
-def set_direction(hass, entity_id: str=None, direction: str=None) -> None:
+def set_direction(hass, entity_id: str = None, direction: str = None) -> None:
"""Set direction for all or specified fan."""
data = {
key: value for key, value in [
@@ -258,11 +259,13 @@ class FanEntity(ToggleEntity):
"""
return self.hass.async_add_job(self.set_direction, direction)
- def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
+ # pylint: disable=arguments-differ
+ def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
"""Turn on the fan."""
raise NotImplementedError()
- def async_turn_on(self: ToggleEntity, speed: str=None, **kwargs):
+ # pylint: disable=arguments-differ
+ def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs):
"""Turn on the fan.
This method must be run in the event loop and returns a coroutine.
diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py
index c6d1232801f..12dc0b1104f 100644
--- a/homeassistant/components/fan/comfoconnect.py
+++ b/homeassistant/components/fan/comfoconnect.py
@@ -87,7 +87,7 @@ class ComfoConnectFan(FanEntity):
"""List of available fan modes."""
return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
- def turn_on(self, speed: str=None, **kwargs) -> None:
+ def turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on the fan."""
if speed is None:
speed = SPEED_LOW
@@ -97,21 +97,21 @@ class ComfoConnectFan(FanEntity):
"""Turn off the fan (to away)."""
self.set_speed(SPEED_OFF)
- def set_speed(self, mode):
+ def set_speed(self, speed: str):
"""Set fan speed."""
- _LOGGER.debug('Changing fan mode to %s.', mode)
+ _LOGGER.debug('Changing fan speed to %s.', speed)
from pycomfoconnect import (
CMD_FAN_MODE_AWAY, CMD_FAN_MODE_LOW, CMD_FAN_MODE_MEDIUM,
CMD_FAN_MODE_HIGH)
- if mode == SPEED_OFF:
+ if speed == SPEED_OFF:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY)
- elif mode == SPEED_LOW:
+ elif speed == SPEED_LOW:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_LOW)
- elif mode == SPEED_MEDIUM:
+ elif speed == SPEED_MEDIUM:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_MEDIUM)
- elif mode == SPEED_HIGH:
+ elif speed == SPEED_HIGH:
self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_HIGH)
# Update current mode
diff --git a/homeassistant/components/fan/demo.py b/homeassistant/components/fan/demo.py
index bdb1b784c8b..b328ebb3101 100644
--- a/homeassistant/components/fan/demo.py
+++ b/homeassistant/components/fan/demo.py
@@ -59,13 +59,13 @@ class DemoFan(FanEntity):
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
- def turn_on(self, speed: str=None) -> None:
+ def turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on the entity."""
if speed is None:
speed = SPEED_MEDIUM
self.set_speed(speed)
- def turn_off(self) -> None:
+ def turn_off(self, **kwargs) -> None:
"""Turn off the entity."""
self.oscillate(False)
self.set_speed(STATE_OFF)
diff --git a/homeassistant/components/fan/dyson.py b/homeassistant/components/fan/dyson.py
index c5e5b8736ae..5b689ece6ed 100644
--- a/homeassistant/components/fan/dyson.py
+++ b/homeassistant/components/fan/dyson.py
@@ -113,7 +113,7 @@ class DysonPureCoolLinkDevice(FanEntity):
self._device.set_configuration(
fan_mode=FanMode.FAN, fan_speed=fan_speed)
- def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
+ def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
"""Turn on the fan."""
from libpurecoollink.const import FanSpeed, FanMode
diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py
index e6f9424d852..b8a5c99add4 100644
--- a/homeassistant/components/fan/insteon_local.py
+++ b/homeassistant/components/fan/insteon_local.py
@@ -91,7 +91,7 @@ class InsteonLocalFanDevice(FanEntity):
"""Flag supported features."""
return SUPPORT_INSTEON_LOCAL
- def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
+ def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
"""Turn device on."""
if speed is None:
if ATTR_SPEED in kwargs:
diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py
index 137bc400d0d..847ca3b325b 100644
--- a/homeassistant/components/fan/isy994.py
+++ b/homeassistant/components/fan/isy994.py
@@ -48,10 +48,6 @@ def setup_platform(hass, config: ConfigType,
class ISYFanDevice(ISYDevice, FanEntity):
"""Representation of an ISY994 fan device."""
- def __init__(self, node) -> None:
- """Initialize the ISY994 fan device."""
- super().__init__(node)
-
@property
def speed(self) -> str:
"""Return the current speed."""
@@ -66,7 +62,7 @@ class ISYFanDevice(ISYDevice, FanEntity):
"""Send the set speed command to the ISY994 fan device."""
self._node.on(val=STATE_TO_VALUE.get(speed, 255))
- def turn_on(self, speed: str=None, **kwargs) -> None:
+ def turn_on(self, speed: str = None, **kwargs) -> None:
"""Send the turn on command to the ISY994 fan device."""
self.set_speed(speed)
@@ -99,7 +95,7 @@ class ISYFanProgram(ISYFanDevice):
if not self._actions.runThen():
_LOGGER.error("Unable to turn off the fan")
- def turn_on(self, **kwargs) -> None:
+ def turn_on(self, speed: str = None, **kwargs) -> None:
"""Send the turn off command to ISY994 fan program."""
if not self._actions.runElse():
_LOGGER.error("Unable to turn on the fan")
diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py
index 1ecbb12bcb4..95ff587c613 100644
--- a/homeassistant/components/fan/mqtt.py
+++ b/homeassistant/components/fan/mqtt.py
@@ -252,7 +252,7 @@ class MqttFan(MqttAvailability, FanEntity):
return self._oscillation
@asyncio.coroutine
- def async_turn_on(self, speed: str=None) -> None:
+ def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on the entity.
This method is a coroutine.
@@ -264,7 +264,7 @@ class MqttFan(MqttAvailability, FanEntity):
yield from self.async_set_speed(speed)
@asyncio.coroutine
- def async_turn_off(self) -> None:
+ def async_turn_off(self, **kwargs) -> None:
"""Turn off the entity.
This method is a coroutine.
diff --git a/homeassistant/components/fan/velbus.py b/homeassistant/components/fan/velbus.py
index c0d125aa5ab..e8208d1c990 100644
--- a/homeassistant/components/fan/velbus.py
+++ b/homeassistant/components/fan/velbus.py
@@ -128,13 +128,13 @@ class VelbusFan(FanEntity):
"""Get the list of available speeds."""
return [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
- def turn_on(self, speed, **kwargs):
+ def turn_on(self, speed=None, **kwargs):
"""Turn on the entity."""
if speed is None:
speed = SPEED_MEDIUM
self.set_speed(speed)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn off the entity."""
self.set_speed(STATE_OFF)
diff --git a/homeassistant/components/fan/wink.py b/homeassistant/components/fan/wink.py
index 827f134cc08..0cebd9cb9f8 100644
--- a/homeassistant/components/fan/wink.py
+++ b/homeassistant/components/fan/wink.py
@@ -47,7 +47,7 @@ class WinkFanDevice(WinkDevice, FanEntity):
"""Set the speed of the fan."""
self.wink.set_state(True, speed)
- def turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
+ def turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
"""Turn on the fan."""
self.wink.set_state(True, speed)
diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py
index 6938926a19b..b9bc54b5c79 100644
--- a/homeassistant/components/fan/xiaomi_miio.py
+++ b/homeassistant/components/fan/xiaomi_miio.py
@@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA,
- SUPPORT_SET_SPEED, DOMAIN)
+ SUPPORT_SET_SPEED, DOMAIN, )
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN,
ATTR_ENTITY_ID, )
from homeassistant.exceptions import PlatformNotReady
@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.3.6']
+REQUIREMENTS = ['python-miio==0.3.7']
ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity'
@@ -167,6 +167,7 @@ class XiaomiAirPurifier(FanEntity):
ATTR_AVERAGE_AIR_QUALITY_INDEX: None,
ATTR_PURIFY_VOLUME: None,
}
+ self._skip_update = False
@property
def supported_features(self):
@@ -214,27 +215,39 @@ class XiaomiAirPurifier(FanEntity):
return False
@asyncio.coroutine
- def async_turn_on(self: ToggleEntity, speed: str=None, **kwargs) -> None:
+ def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None:
"""Turn the fan on."""
if speed:
# If operation mode was set the device must not be turned on.
- yield from self.async_set_speed(speed)
- return
+ result = yield from self.async_set_speed(speed)
+ else:
+ result = yield from self._try_command(
+ "Turning the air purifier on failed.", self._air_purifier.on)
- yield from self._try_command(
- "Turning the air purifier on failed.", self._air_purifier.on)
+ if result:
+ self._state = True
+ self._skip_update = True
@asyncio.coroutine
def async_turn_off(self: ToggleEntity, **kwargs) -> None:
"""Turn the fan off."""
- yield from self._try_command(
+ result = yield from self._try_command(
"Turning the air purifier off failed.", self._air_purifier.off)
+ if result:
+ self._state = False
+ self._skip_update = True
+
@asyncio.coroutine
def async_update(self):
"""Fetch state from the device."""
from miio import DeviceException
+ # On state change the device doesn't provide the new state immediately.
+ if self._skip_update:
+ self._skip_update = False
+ return
+
try:
state = yield from self.hass.async_add_job(
self._air_purifier.status)
@@ -262,6 +275,7 @@ class XiaomiAirPurifier(FanEntity):
ATTR_LED_BRIGHTNESS] = state.led_brightness.value
except DeviceException as ex:
+ self._state = None
_LOGGER.error("Got exception while fetching the state: %s", ex)
@property
@@ -283,12 +297,12 @@ class XiaomiAirPurifier(FanEntity):
@asyncio.coroutine
def async_set_speed(self: ToggleEntity, speed: str) -> None:
"""Set the speed of the fan."""
- _LOGGER.debug("Setting the operation mode to: " + speed)
+ _LOGGER.debug("Setting the operation mode to: %s", speed)
from miio.airpurifier import OperationMode
yield from self._try_command(
"Setting operation mode of the air purifier failed.",
- self._air_purifier.set_mode, OperationMode[speed])
+ self._air_purifier.set_mode, OperationMode[speed.title()])
@asyncio.coroutine
def async_set_buzzer_on(self):
@@ -333,7 +347,7 @@ class XiaomiAirPurifier(FanEntity):
self._air_purifier.set_child_lock, False)
@asyncio.coroutine
- def async_set_led_brightness(self, brightness: int=2):
+ def async_set_led_brightness(self, brightness: int = 2):
"""Set the led brightness."""
from miio.airpurifier import LedBrightness
@@ -342,7 +356,7 @@ class XiaomiAirPurifier(FanEntity):
self._air_purifier.set_led_brightness, LedBrightness(brightness))
@asyncio.coroutine
- def async_set_favorite_level(self, level: int=1):
+ def async_set_favorite_level(self, level: int = 1):
"""Set the favorite level."""
yield from self._try_command(
"Setting the favorite level of the air purifier failed.",
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index eedd33478a7..2a9a7a8a38a 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -17,13 +17,13 @@ import jinja2
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.http.auth import is_trusted_ip
+from homeassistant.components.http.const import KEY_AUTHENTICATED
from homeassistant.config import find_config_file, load_yaml_config_file
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.loader import bind_hass
-REQUIREMENTS = ['home-assistant-frontend==20180209.0', 'user-agents==1.1.0']
+REQUIREMENTS = ['home-assistant-frontend==20180221.1', 'user-agents==1.1.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@@ -490,7 +490,7 @@ class IndexView(HomeAssistantView):
panel_url = hass.data[DATA_PANELS][panel].webcomponent_url_es5
no_auth = '1'
- if hass.config.api.api_password and not is_trusted_ip(request):
+ if hass.config.api.api_password and not request[KEY_AUTHENTICATED]:
# do not try to auto connect on load
no_auth = '0'
diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py
index aac258b4e93..20dee082a08 100644
--- a/homeassistant/components/google_assistant/__init__.py
+++ b/homeassistant/components/google_assistant/__init__.py
@@ -27,7 +27,7 @@ from .const import (
CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS,
DEFAULT_EXPOSED_DOMAINS, CONF_AGENT_USER_ID, CONF_API_KEY,
SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG,
- CONF_EXPOSE, CONF_ALIASES
+ CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT
)
from .auth import GoogleAssistantAuthView
from .http import async_register_http
@@ -43,7 +43,8 @@ ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_TYPE): vol.In(MAPPING_COMPONENT),
vol.Optional(CONF_EXPOSE): cv.boolean,
- vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string])
+ vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_ROOM_HINT): cv.string
})
CONFIG_SCHEMA = vol.Schema(
diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py
index 0483f424ca3..1f1ae4682b4 100644
--- a/homeassistant/components/google_assistant/const.py
+++ b/homeassistant/components/google_assistant/const.py
@@ -13,6 +13,7 @@ CONF_CLIENT_ID = 'client_id'
CONF_ALIASES = 'aliases'
CONF_AGENT_USER_ID = 'agent_user_id'
CONF_API_KEY = 'api_key'
+CONF_ROOM_HINT = 'room'
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index b718c009160..f638b847bcb 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -33,7 +33,8 @@ from .const import (
TRAIT_ONOFF, TRAIT_BRIGHTNESS, TRAIT_COLOR_TEMP,
TRAIT_RGB_COLOR, TRAIT_SCENE, TRAIT_TEMPERATURE_SETTING,
TYPE_LIGHT, TYPE_SCENE, TYPE_SWITCH, TYPE_THERMOSTAT,
- CONF_ALIASES, CLIMATE_SUPPORTED_MODES, CLIMATE_MODE_HEATCOOL
+ CONF_ALIASES, CONF_ROOM_HINT, CLIMATE_SUPPORTED_MODES,
+ CLIMATE_MODE_HEATCOOL
)
HANDLERS = Registry()
@@ -124,6 +125,11 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
if aliases:
device['name']['nicknames'] = aliases
+ # add room hint if annotated
+ room = entity_config.get(CONF_ROOM_HINT)
+ if room:
+ device['roomHint'] = room
+
# add trait if entity supports feature
if class_data[2]:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
@@ -237,7 +243,10 @@ def query_response_sensor(
def query_response_climate(
entity: Entity, config: Config, units: UnitSystem) -> dict:
"""Convert a climate entity to a QUERY response."""
- mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
+ mode = entity.attributes.get(climate.ATTR_OPERATION_MODE)
+ if mode is None:
+ mode = entity.state
+ mode = mode.lower()
if mode not in CLIMATE_SUPPORTED_MODES:
mode = 'heat'
attrs = entity.attributes
diff --git a/homeassistant/components/hassio.py b/homeassistant/components/hassio.py
deleted file mode 100644
index 510b08e766f..00000000000
--- a/homeassistant/components/hassio.py
+++ /dev/null
@@ -1,451 +0,0 @@
-"""
-Exposes regular REST commands as services.
-
-For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/hassio/
-"""
-import asyncio
-from datetime import timedelta
-import logging
-import os
-import re
-
-import aiohttp
-from aiohttp import web
-from aiohttp.hdrs import CONTENT_TYPE
-from aiohttp.web_exceptions import HTTPBadGateway
-import async_timeout
-import voluptuous as vol
-
-from homeassistant.components import SERVICE_CHECK_CONFIG
-from homeassistant.components.http import (
- CONF_API_PASSWORD, CONF_SERVER_HOST, CONF_SERVER_PORT,
- CONF_SSL_CERTIFICATE, KEY_AUTHENTICATED, HomeAssistantView)
-from homeassistant.const import (
- CONF_TIME_ZONE, CONTENT_TYPE_TEXT_PLAIN, SERVER_PORT,
- SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP)
-from homeassistant.core import DOMAIN as HASS_DOMAIN
-from homeassistant.core import callback
-import homeassistant.helpers.config_validation as cv
-from homeassistant.loader import bind_hass
-from homeassistant.util.dt import utcnow
-
-_LOGGER = logging.getLogger(__name__)
-
-DOMAIN = 'hassio'
-DEPENDENCIES = ['http']
-
-X_HASSIO = 'X-HASSIO-KEY'
-
-DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version'
-HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
-
-SERVICE_ADDON_START = 'addon_start'
-SERVICE_ADDON_STOP = 'addon_stop'
-SERVICE_ADDON_RESTART = 'addon_restart'
-SERVICE_ADDON_STDIN = 'addon_stdin'
-SERVICE_HOST_SHUTDOWN = 'host_shutdown'
-SERVICE_HOST_REBOOT = 'host_reboot'
-SERVICE_SNAPSHOT_FULL = 'snapshot_full'
-SERVICE_SNAPSHOT_PARTIAL = 'snapshot_partial'
-SERVICE_RESTORE_FULL = 'restore_full'
-SERVICE_RESTORE_PARTIAL = 'restore_partial'
-
-ATTR_ADDON = 'addon'
-ATTR_INPUT = 'input'
-ATTR_SNAPSHOT = 'snapshot'
-ATTR_ADDONS = 'addons'
-ATTR_FOLDERS = 'folders'
-ATTR_HOMEASSISTANT = 'homeassistant'
-ATTR_NAME = 'name'
-
-NO_TIMEOUT = {
- re.compile(r'^homeassistant/update$'),
- re.compile(r'^host/update$'),
- re.compile(r'^supervisor/update$'),
- re.compile(r'^addons/[^/]*/update$'),
- re.compile(r'^addons/[^/]*/install$'),
- re.compile(r'^addons/[^/]*/rebuild$'),
- re.compile(r'^snapshots/.*/full$'),
- re.compile(r'^snapshots/.*/partial$'),
-}
-
-NO_AUTH = {
- re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'),
- re.compile(r'^addons/[^/]*/logo$')
-}
-
-SCHEMA_NO_DATA = vol.Schema({})
-
-SCHEMA_ADDON = vol.Schema({
- vol.Required(ATTR_ADDON): cv.slug,
-})
-
-SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend({
- vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)
-})
-
-SCHEMA_SNAPSHOT_FULL = vol.Schema({
- vol.Optional(ATTR_NAME): cv.string,
-})
-
-SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({
- vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
-})
-
-SCHEMA_RESTORE_FULL = vol.Schema({
- vol.Required(ATTR_SNAPSHOT): cv.slug,
-})
-
-SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend({
- vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
- vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
-})
-
-MAP_SERVICE_API = {
- SERVICE_ADDON_START: ('/addons/{addon}/start', SCHEMA_ADDON, 60, False),
- SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON, 60, False),
- SERVICE_ADDON_RESTART:
- ('/addons/{addon}/restart', SCHEMA_ADDON, 60, False),
- SERVICE_ADDON_STDIN:
- ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN, 60, False),
- SERVICE_HOST_SHUTDOWN: ('/host/shutdown', SCHEMA_NO_DATA, 60, False),
- SERVICE_HOST_REBOOT: ('/host/reboot', SCHEMA_NO_DATA, 60, False),
- SERVICE_SNAPSHOT_FULL:
- ('/snapshots/new/full', SCHEMA_SNAPSHOT_FULL, 300, True),
- SERVICE_SNAPSHOT_PARTIAL:
- ('/snapshots/new/partial', SCHEMA_SNAPSHOT_PARTIAL, 300, True),
- SERVICE_RESTORE_FULL:
- ('/snapshots/{snapshot}/restore/full', SCHEMA_RESTORE_FULL, 300, True),
- SERVICE_RESTORE_PARTIAL:
- ('/snapshots/{snapshot}/restore/partial', SCHEMA_RESTORE_PARTIAL, 300,
- True),
-}
-
-
-@callback
-@bind_hass
-def get_homeassistant_version(hass):
- """Return latest available Home Assistant version.
-
- Async friendly.
- """
- return hass.data.get(DATA_HOMEASSISTANT_VERSION)
-
-
-@callback
-@bind_hass
-def is_hassio(hass):
- """Return true if hass.io is loaded.
-
- Async friendly.
- """
- return DOMAIN in hass.config.components
-
-
-@bind_hass
-@asyncio.coroutine
-def async_check_config(hass):
- """Check configuration over Hass.io API."""
- result = yield from hass.data[DOMAIN].send_command(
- '/homeassistant/check', timeout=300)
-
- if not result:
- return "Hass.io config check API error"
- elif result['result'] == "error":
- return result['message']
- return None
-
-
-@asyncio.coroutine
-def async_setup(hass, config):
- """Set up the Hass.io component."""
- try:
- host = os.environ['HASSIO']
- except KeyError:
- _LOGGER.error("No Hass.io supervisor detect")
- return False
-
- websession = hass.helpers.aiohttp_client.async_get_clientsession()
- hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host)
-
- if not (yield from hassio.is_connected()):
- _LOGGER.error("Not connected with Hass.io")
- return False
-
- hass.http.register_view(HassIOView(hassio))
-
- if 'frontend' in hass.config.components:
- yield from hass.components.frontend.async_register_built_in_panel(
- 'hassio', 'Hass.io', 'mdi:access-point-network')
-
- if 'http' in config:
- yield from hassio.update_hass_api(config['http'])
-
- if 'homeassistant' in config:
- yield from hassio.update_hass_timezone(config['homeassistant'])
-
- @asyncio.coroutine
- def async_service_handler(service):
- """Handle service calls for Hass.io."""
- api_command = MAP_SERVICE_API[service.service][0]
- data = service.data.copy()
- addon = data.pop(ATTR_ADDON, None)
- snapshot = data.pop(ATTR_SNAPSHOT, None)
- payload = None
-
- # Pass data to hass.io API
- if service.service == SERVICE_ADDON_STDIN:
- payload = data[ATTR_INPUT]
- elif MAP_SERVICE_API[service.service][3]:
- payload = data
-
- # Call API
- ret = yield from hassio.send_command(
- api_command.format(addon=addon, snapshot=snapshot),
- payload=payload, timeout=MAP_SERVICE_API[service.service][2]
- )
-
- if not ret or ret['result'] != "ok":
- _LOGGER.error("Error on Hass.io API: %s", ret['message'])
-
- for service, settings in MAP_SERVICE_API.items():
- hass.services.async_register(
- DOMAIN, service, async_service_handler, schema=settings[1])
-
- @asyncio.coroutine
- def update_homeassistant_version(now):
- """Update last available Home Assistant version."""
- data = yield from hassio.get_homeassistant_info()
- if data:
- hass.data[DATA_HOMEASSISTANT_VERSION] = \
- data['data']['last_version']
-
- hass.helpers.event.async_track_point_in_utc_time(
- update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL)
-
- # Fetch last version
- yield from update_homeassistant_version(None)
-
- @asyncio.coroutine
- def async_handle_core_service(call):
- """Service handler for handling core services."""
- if call.service == SERVICE_HOMEASSISTANT_STOP:
- yield from hassio.send_command('/homeassistant/stop')
- return
-
- error = yield from async_check_config(hass)
- if error:
- _LOGGER.error(error)
- hass.components.persistent_notification.async_create(
- "Config error. See dev-info panel for details.",
- "Config validating", "{0}.check_config".format(HASS_DOMAIN))
- return
-
- if call.service == SERVICE_HOMEASSISTANT_RESTART:
- yield from hassio.send_command('/homeassistant/restart')
-
- # Mock core services
- for service in (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
- SERVICE_CHECK_CONFIG):
- hass.services.async_register(
- HASS_DOMAIN, service, async_handle_core_service)
-
- return True
-
-
-def _api_bool(funct):
- """Return a boolean."""
- @asyncio.coroutine
- def _wrapper(*argv, **kwargs):
- """Wrap function."""
- data = yield from funct(*argv, **kwargs)
- return data and data['result'] == "ok"
-
- return _wrapper
-
-
-class HassIO(object):
- """Small API wrapper for Hass.io."""
-
- def __init__(self, loop, websession, ip):
- """Initialize Hass.io API."""
- self.loop = loop
- self.websession = websession
- self._ip = ip
-
- @_api_bool
- def is_connected(self):
- """Return true if it connected to Hass.io supervisor.
-
- This method return a coroutine.
- """
- return self.send_command("/supervisor/ping", method="get")
-
- def get_homeassistant_info(self):
- """Return data for Home Assistant.
-
- This method return a coroutine.
- """
- return self.send_command("/homeassistant/info", method="get")
-
- @_api_bool
- def update_hass_api(self, http_config):
- """Update Home Assistant API data on Hass.io.
-
- This method return a coroutine.
- """
- port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT
- options = {
- 'ssl': CONF_SSL_CERTIFICATE in http_config,
- 'port': port,
- 'password': http_config.get(CONF_API_PASSWORD),
- 'watchdog': True,
- }
-
- if CONF_SERVER_HOST in http_config:
- options['watchdog'] = False
- _LOGGER.warning("Don't use 'server_host' options with Hass.io")
-
- return self.send_command("/homeassistant/options", payload=options)
-
- @_api_bool
- def update_hass_timezone(self, core_config):
- """Update Home-Assistant timezone data on Hass.io.
-
- This method return a coroutine.
- """
- return self.send_command("/supervisor/options", payload={
- 'timezone': core_config.get(CONF_TIME_ZONE)
- })
-
- @asyncio.coroutine
- def send_command(self, command, method="post", payload=None, timeout=10):
- """Send API command to Hass.io.
-
- This method is a coroutine.
- """
- try:
- with async_timeout.timeout(timeout, loop=self.loop):
- request = yield from self.websession.request(
- method, "http://{}{}".format(self._ip, command),
- json=payload, headers={
- X_HASSIO: os.environ.get('HASSIO_TOKEN', "")
- })
-
- if request.status not in (200, 400):
- _LOGGER.error(
- "%s return code %d.", command, request.status)
- return None
-
- answer = yield from request.json()
- return answer
-
- except asyncio.TimeoutError:
- _LOGGER.error("Timeout on %s request", command)
-
- except aiohttp.ClientError as err:
- _LOGGER.error("Client error on %s request %s", command, err)
-
- return None
-
- @asyncio.coroutine
- def command_proxy(self, path, request):
- """Return a client request with proxy origin for Hass.io supervisor.
-
- This method is a coroutine.
- """
- read_timeout = _get_timeout(path)
-
- try:
- data = None
- headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN', "")}
- with async_timeout.timeout(10, loop=self.loop):
- data = yield from request.read()
- if data:
- headers[CONTENT_TYPE] = request.content_type
- else:
- data = None
-
- method = getattr(self.websession, request.method.lower())
- client = yield from method(
- "http://{}/{}".format(self._ip, path), data=data,
- headers=headers, timeout=read_timeout
- )
-
- return client
-
- except aiohttp.ClientError as err:
- _LOGGER.error("Client error on api %s request %s", path, err)
-
- except asyncio.TimeoutError:
- _LOGGER.error("Client timeout error on API request %s", path)
-
- raise HTTPBadGateway()
-
-
-class HassIOView(HomeAssistantView):
- """Hass.io view to handle base part."""
-
- name = "api:hassio"
- url = "/api/hassio/{path:.+}"
- requires_auth = False
-
- def __init__(self, hassio):
- """Initialize a Hass.io base view."""
- self.hassio = hassio
-
- @asyncio.coroutine
- def _handle(self, request, path):
- """Route data to Hass.io."""
- if _need_auth(path) and not request[KEY_AUTHENTICATED]:
- return web.Response(status=401)
-
- client = yield from self.hassio.command_proxy(path, request)
-
- data = yield from client.read()
- if path.endswith('/logs'):
- return _create_response_log(client, data)
- return _create_response(client, data)
-
- get = _handle
- post = _handle
-
-
-def _create_response(client, data):
- """Convert a response from client request."""
- return web.Response(
- body=data,
- status=client.status,
- content_type=client.content_type,
- )
-
-
-def _create_response_log(client, data):
- """Convert a response from client request."""
- # Remove color codes
- log = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", data.decode())
-
- return web.Response(
- text=log,
- status=client.status,
- content_type=CONTENT_TYPE_TEXT_PLAIN,
- )
-
-
-def _get_timeout(path):
- """Return timeout for a URL path."""
- for re_path in NO_TIMEOUT:
- if re_path.match(path):
- return 0
- return 300
-
-
-def _need_auth(path):
- """Return if a path need authentication."""
- for re_path in NO_AUTH:
- if re_path.match(path):
- return False
- return True
diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py
new file mode 100644
index 00000000000..540659273b3
--- /dev/null
+++ b/homeassistant/components/hassio/__init__.py
@@ -0,0 +1,232 @@
+"""
+Exposes regular REST commands as services.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/hassio/
+"""
+import asyncio
+from datetime import timedelta
+import logging
+import os
+
+import voluptuous as vol
+
+from homeassistant.components import SERVICE_CHECK_CONFIG
+from homeassistant.const import (
+ SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP)
+from homeassistant.core import DOMAIN as HASS_DOMAIN
+from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
+from homeassistant.loader import bind_hass
+from homeassistant.util.dt import utcnow
+from .handler import HassIO
+from .http import HassIOView
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'hassio'
+DEPENDENCIES = ['http']
+
+DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version'
+HASSIO_UPDATE_INTERVAL = timedelta(minutes=55)
+
+SERVICE_ADDON_START = 'addon_start'
+SERVICE_ADDON_STOP = 'addon_stop'
+SERVICE_ADDON_RESTART = 'addon_restart'
+SERVICE_ADDON_STDIN = 'addon_stdin'
+SERVICE_HOST_SHUTDOWN = 'host_shutdown'
+SERVICE_HOST_REBOOT = 'host_reboot'
+SERVICE_SNAPSHOT_FULL = 'snapshot_full'
+SERVICE_SNAPSHOT_PARTIAL = 'snapshot_partial'
+SERVICE_RESTORE_FULL = 'restore_full'
+SERVICE_RESTORE_PARTIAL = 'restore_partial'
+
+ATTR_ADDON = 'addon'
+ATTR_INPUT = 'input'
+ATTR_SNAPSHOT = 'snapshot'
+ATTR_ADDONS = 'addons'
+ATTR_FOLDERS = 'folders'
+ATTR_HOMEASSISTANT = 'homeassistant'
+ATTR_NAME = 'name'
+ATTR_PASSWORD = 'password'
+
+SCHEMA_NO_DATA = vol.Schema({})
+
+SCHEMA_ADDON = vol.Schema({
+ vol.Required(ATTR_ADDON): cv.slug,
+})
+
+SCHEMA_ADDON_STDIN = SCHEMA_ADDON.extend({
+ vol.Required(ATTR_INPUT): vol.Any(dict, cv.string)
+})
+
+SCHEMA_SNAPSHOT_FULL = vol.Schema({
+ vol.Optional(ATTR_NAME): cv.string,
+ vol.Optional(ATTR_PASSWORD): cv.string,
+})
+
+SCHEMA_SNAPSHOT_PARTIAL = SCHEMA_SNAPSHOT_FULL.extend({
+ vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
+})
+
+SCHEMA_RESTORE_FULL = vol.Schema({
+ vol.Required(ATTR_SNAPSHOT): cv.slug,
+ vol.Optional(ATTR_PASSWORD): cv.string,
+})
+
+SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend({
+ vol.Optional(ATTR_HOMEASSISTANT): cv.boolean,
+ vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.string]),
+})
+
+MAP_SERVICE_API = {
+ SERVICE_ADDON_START: ('/addons/{addon}/start', SCHEMA_ADDON, 60, False),
+ SERVICE_ADDON_STOP: ('/addons/{addon}/stop', SCHEMA_ADDON, 60, False),
+ SERVICE_ADDON_RESTART:
+ ('/addons/{addon}/restart', SCHEMA_ADDON, 60, False),
+ SERVICE_ADDON_STDIN:
+ ('/addons/{addon}/stdin', SCHEMA_ADDON_STDIN, 60, False),
+ SERVICE_HOST_SHUTDOWN: ('/host/shutdown', SCHEMA_NO_DATA, 60, False),
+ SERVICE_HOST_REBOOT: ('/host/reboot', SCHEMA_NO_DATA, 60, False),
+ SERVICE_SNAPSHOT_FULL:
+ ('/snapshots/new/full', SCHEMA_SNAPSHOT_FULL, 300, True),
+ SERVICE_SNAPSHOT_PARTIAL:
+ ('/snapshots/new/partial', SCHEMA_SNAPSHOT_PARTIAL, 300, True),
+ SERVICE_RESTORE_FULL:
+ ('/snapshots/{snapshot}/restore/full', SCHEMA_RESTORE_FULL, 300, True),
+ SERVICE_RESTORE_PARTIAL:
+ ('/snapshots/{snapshot}/restore/partial', SCHEMA_RESTORE_PARTIAL, 300,
+ True),
+}
+
+
+@callback
+@bind_hass
+def get_homeassistant_version(hass):
+ """Return latest available Home Assistant version.
+
+ Async friendly.
+ """
+ return hass.data.get(DATA_HOMEASSISTANT_VERSION)
+
+
+@callback
+@bind_hass
+def is_hassio(hass):
+ """Return true if hass.io is loaded.
+
+ Async friendly.
+ """
+ return DOMAIN in hass.config.components
+
+
+@bind_hass
+@asyncio.coroutine
+def async_check_config(hass):
+ """Check configuration over Hass.io API."""
+ hassio = hass.data[DOMAIN]
+ result = yield from hassio.check_homeassistant_config()
+
+ if not result:
+ return "Hass.io config check API error"
+ elif result['result'] == "error":
+ return result['message']
+ return None
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Set up the Hass.io component."""
+ try:
+ host = os.environ['HASSIO']
+ except KeyError:
+ _LOGGER.error("No Hass.io supervisor detect")
+ return False
+
+ websession = hass.helpers.aiohttp_client.async_get_clientsession()
+ hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host)
+
+ if not (yield from hassio.is_connected()):
+ _LOGGER.error("Not connected with Hass.io")
+ return False
+
+ hass.http.register_view(HassIOView(host, websession))
+
+ if 'frontend' in hass.config.components:
+ yield from hass.components.frontend.async_register_built_in_panel(
+ 'hassio', 'Hass.io', 'mdi:access-point-network')
+
+ if 'http' in config:
+ yield from hassio.update_hass_api(config['http'])
+
+ if 'homeassistant' in config:
+ yield from hassio.update_hass_timezone(config['homeassistant'])
+
+ @asyncio.coroutine
+ def async_service_handler(service):
+ """Handle service calls for Hass.io."""
+ api_command = MAP_SERVICE_API[service.service][0]
+ data = service.data.copy()
+ addon = data.pop(ATTR_ADDON, None)
+ snapshot = data.pop(ATTR_SNAPSHOT, None)
+ payload = None
+
+ # Pass data to hass.io API
+ if service.service == SERVICE_ADDON_STDIN:
+ payload = data[ATTR_INPUT]
+ elif MAP_SERVICE_API[service.service][3]:
+ payload = data
+
+ # Call API
+ ret = yield from hassio.send_command(
+ api_command.format(addon=addon, snapshot=snapshot),
+ payload=payload, timeout=MAP_SERVICE_API[service.service][2]
+ )
+
+ if not ret or ret['result'] != "ok":
+ _LOGGER.error("Error on Hass.io API: %s", ret['message'])
+
+ for service, settings in MAP_SERVICE_API.items():
+ hass.services.async_register(
+ DOMAIN, service, async_service_handler, schema=settings[1])
+
+ @asyncio.coroutine
+ def update_homeassistant_version(now):
+ """Update last available Home Assistant version."""
+ data = yield from hassio.get_homeassistant_info()
+ if data:
+ hass.data[DATA_HOMEASSISTANT_VERSION] = data['last_version']
+
+ hass.helpers.event.async_track_point_in_utc_time(
+ update_homeassistant_version, utcnow() + HASSIO_UPDATE_INTERVAL)
+
+ # Fetch last version
+ yield from update_homeassistant_version(None)
+
+ @asyncio.coroutine
+ def async_handle_core_service(call):
+ """Service handler for handling core services."""
+ if call.service == SERVICE_HOMEASSISTANT_STOP:
+ yield from hassio.stop_homeassistant()
+ return
+
+ error = yield from async_check_config(hass)
+ if error:
+ _LOGGER.error(error)
+ hass.components.persistent_notification.async_create(
+ "Config error. See dev-info panel for details.",
+ "Config validating", "{0}.check_config".format(HASS_DOMAIN))
+ return
+
+ if call.service == SERVICE_HOMEASSISTANT_RESTART:
+ yield from hassio.restart_homeassistant()
+
+ # Mock core services
+ for service in (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
+ SERVICE_CHECK_CONFIG):
+ hass.services.async_register(
+ HASS_DOMAIN, service, async_handle_core_service)
+
+ return True
diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py
new file mode 100644
index 00000000000..a954aaccbd4
--- /dev/null
+++ b/homeassistant/components/hassio/handler.py
@@ -0,0 +1,154 @@
+"""
+Exposes regular REST commands as services.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/hassio/
+"""
+import asyncio
+import logging
+import os
+
+import aiohttp
+import async_timeout
+
+from homeassistant.components.http import (
+ CONF_API_PASSWORD, CONF_SERVER_HOST, CONF_SERVER_PORT,
+ CONF_SSL_CERTIFICATE)
+from homeassistant.const import CONF_TIME_ZONE, SERVER_PORT
+
+_LOGGER = logging.getLogger(__name__)
+
+X_HASSIO = 'X-HASSIO-KEY'
+
+
+def _api_bool(funct):
+ """Return a boolean."""
+ @asyncio.coroutine
+ def _wrapper(*argv, **kwargs):
+ """Wrap function."""
+ data = yield from funct(*argv, **kwargs)
+ return data and data['result'] == "ok"
+
+ return _wrapper
+
+
+def _api_data(funct):
+ """Return a api data."""
+ @asyncio.coroutine
+ def _wrapper(*argv, **kwargs):
+ """Wrap function."""
+ data = yield from funct(*argv, **kwargs)
+ if data and data['result'] == "ok":
+ return data['data']
+ return None
+
+ return _wrapper
+
+
+class HassIO(object):
+ """Small API wrapper for Hass.io."""
+
+ def __init__(self, loop, websession, ip):
+ """Initialize Hass.io API."""
+ self.loop = loop
+ self.websession = websession
+ self._ip = ip
+
+ @_api_bool
+ def is_connected(self):
+ """Return true if it connected to Hass.io supervisor.
+
+ This method return a coroutine.
+ """
+ return self.send_command("/supervisor/ping", method="get")
+
+ @_api_data
+ def get_homeassistant_info(self):
+ """Return data for Home Assistant.
+
+ This method return a coroutine.
+ """
+ return self.send_command("/homeassistant/info", method="get")
+
+ @_api_bool
+ def restart_homeassistant(self):
+ """Restart Home-Assistant container.
+
+ This method return a coroutine.
+ """
+ return self.send_command("/homeassistant/restart")
+
+ @_api_bool
+ def stop_homeassistant(self):
+ """Stop Home-Assistant container.
+
+ This method return a coroutine.
+ """
+ return self.send_command("/homeassistant/stop")
+
+ def check_homeassistant_config(self):
+ """Check Home-Assistant config with Hass.io API.
+
+ This method return a coroutine.
+ """
+ return self.send_command("/homeassistant/check", timeout=300)
+
+ @_api_bool
+ def update_hass_api(self, http_config):
+ """Update Home Assistant API data on Hass.io.
+
+ This method return a coroutine.
+ """
+ port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT
+ options = {
+ 'ssl': CONF_SSL_CERTIFICATE in http_config,
+ 'port': port,
+ 'password': http_config.get(CONF_API_PASSWORD),
+ 'watchdog': True,
+ }
+
+ if CONF_SERVER_HOST in http_config:
+ options['watchdog'] = False
+ _LOGGER.warning("Don't use 'server_host' options with Hass.io")
+
+ return self.send_command("/homeassistant/options", payload=options)
+
+ @_api_bool
+ def update_hass_timezone(self, core_config):
+ """Update Home-Assistant timezone data on Hass.io.
+
+ This method return a coroutine.
+ """
+ return self.send_command("/supervisor/options", payload={
+ 'timezone': core_config.get(CONF_TIME_ZONE)
+ })
+
+ @asyncio.coroutine
+ def send_command(self, command, method="post", payload=None, timeout=10):
+ """Send API command to Hass.io.
+
+ This method is a coroutine.
+ """
+ try:
+ with async_timeout.timeout(timeout, loop=self.loop):
+ request = yield from self.websession.request(
+ method, "http://{}{}".format(self._ip, command),
+ json=payload, headers={
+ X_HASSIO: os.environ.get('HASSIO_TOKEN', "")
+ })
+
+ if request.status not in (200, 400):
+ _LOGGER.error(
+ "%s return code %d.", command, request.status)
+ return None
+
+ answer = yield from request.json()
+ return answer
+
+ except asyncio.TimeoutError:
+ _LOGGER.error("Timeout on %s request", command)
+
+ except aiohttp.ClientError as err:
+ _LOGGER.error("Client error on %s request %s", command, err)
+
+ return None
diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py
new file mode 100644
index 00000000000..9dd6427ec38
--- /dev/null
+++ b/homeassistant/components/hassio/http.py
@@ -0,0 +1,142 @@
+"""
+Exposes regular REST commands as services.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/hassio/
+"""
+import asyncio
+import logging
+import os
+import re
+
+import async_timeout
+import aiohttp
+from aiohttp import web
+from aiohttp.hdrs import CONTENT_TYPE
+from aiohttp.web_exceptions import HTTPBadGateway
+
+from homeassistant.const import CONTENT_TYPE_TEXT_PLAIN
+from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView
+
+_LOGGER = logging.getLogger(__name__)
+
+X_HASSIO = 'X-HASSIO-KEY'
+
+NO_TIMEOUT = {
+ re.compile(r'^homeassistant/update$'),
+ re.compile(r'^host/update$'),
+ re.compile(r'^supervisor/update$'),
+ re.compile(r'^addons/[^/]*/update$'),
+ re.compile(r'^addons/[^/]*/install$'),
+ re.compile(r'^addons/[^/]*/rebuild$'),
+ re.compile(r'^snapshots/.*/full$'),
+ re.compile(r'^snapshots/.*/partial$'),
+ re.compile(r'^snapshots/[^/]*/upload$'),
+ re.compile(r'^snapshots/[^/]*/download$'),
+}
+
+NO_AUTH = {
+ re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'),
+ re.compile(r'^addons/[^/]*/logo$')
+}
+
+
+class HassIOView(HomeAssistantView):
+ """Hass.io view to handle base part."""
+
+ name = "api:hassio"
+ url = "/api/hassio/{path:.+}"
+ requires_auth = False
+
+ def __init__(self, host, websession):
+ """Initialize a Hass.io base view."""
+ self._host = host
+ self._websession = websession
+
+ @asyncio.coroutine
+ def _handle(self, request, path):
+ """Route data to Hass.io."""
+ if _need_auth(path) and not request[KEY_AUTHENTICATED]:
+ return web.Response(status=401)
+
+ client = yield from self._command_proxy(path, request)
+
+ data = yield from client.read()
+ if path.endswith('/logs'):
+ return _create_response_log(client, data)
+ return _create_response(client, data)
+
+ get = _handle
+ post = _handle
+
+ @asyncio.coroutine
+ def _command_proxy(self, path, request):
+ """Return a client request with proxy origin for Hass.io supervisor.
+
+ This method is a coroutine.
+ """
+ read_timeout = _get_timeout(path)
+ hass = request.app['hass']
+
+ try:
+ data = None
+ headers = {X_HASSIO: os.environ.get('HASSIO_TOKEN', "")}
+ with async_timeout.timeout(10, loop=hass.loop):
+ data = yield from request.read()
+ if data:
+ headers[CONTENT_TYPE] = request.content_type
+ else:
+ data = None
+
+ method = getattr(self._websession, request.method.lower())
+ client = yield from method(
+ "http://{}/{}".format(self._host, path), data=data,
+ headers=headers, timeout=read_timeout
+ )
+
+ return client
+
+ except aiohttp.ClientError as err:
+ _LOGGER.error("Client error on api %s request %s", path, err)
+
+ except asyncio.TimeoutError:
+ _LOGGER.error("Client timeout error on API request %s", path)
+
+ raise HTTPBadGateway()
+
+
+def _create_response(client, data):
+ """Convert a response from client request."""
+ return web.Response(
+ body=data,
+ status=client.status,
+ content_type=client.content_type,
+ )
+
+
+def _create_response_log(client, data):
+ """Convert a response from client request."""
+ # Remove color codes
+ log = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", data.decode())
+
+ return web.Response(
+ text=log,
+ status=client.status,
+ content_type=CONTENT_TYPE_TEXT_PLAIN,
+ )
+
+
+def _get_timeout(path):
+ """Return timeout for a URL path."""
+ for re_path in NO_TIMEOUT:
+ if re_path.match(path):
+ return 0
+ return 300
+
+
+def _need_auth(path):
+ """Return if a path need authentication."""
+ for re_path in NO_AUTH:
+ if re_path.match(path):
+ return False
+ return True
diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py
index 8f58f5f7e17..dd14bbf6811 100644
--- a/homeassistant/components/history.py
+++ b/homeassistant/components/history.py
@@ -241,12 +241,12 @@ def async_setup(hass, config):
filters = Filters()
exclude = config[DOMAIN].get(CONF_EXCLUDE)
if exclude:
- filters.excluded_entities = exclude[CONF_ENTITIES]
- filters.excluded_domains = exclude[CONF_DOMAINS]
+ filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
+ filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
include = config[DOMAIN].get(CONF_INCLUDE)
if include:
- filters.included_entities = include[CONF_ENTITIES]
- filters.included_domains = include[CONF_DOMAINS]
+ filters.included_entities = include.get(CONF_ENTITIES, [])
+ filters.included_domains = include.get(CONF_DOMAINS, [])
use_include_order = config[DOMAIN].get(CONF_ORDER)
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
@@ -303,8 +303,10 @@ class HistoryPeriodView(HomeAssistantView):
entity_ids = entity_ids.lower().split(',')
include_start_time_state = 'skip_initial_state' not in request.query
- result = yield from request.app['hass'].async_add_job(
- get_significant_states, request.app['hass'], start_time, end_time,
+ hass = request.app['hass']
+
+ result = yield from hass.async_add_job(
+ get_significant_states, hass, start_time, end_time,
entity_ids, self.filters, include_start_time_state)
result = result.values()
if _LOGGER.isEnabledFor(logging.DEBUG):
@@ -327,7 +329,8 @@ class HistoryPeriodView(HomeAssistantView):
sorted_result.extend(result)
result = sorted_result
- return self.json(result)
+ response = yield from hass.async_add_job(self.json, result)
+ return response
class Filters(object):
diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py
new file mode 100644
index 00000000000..021c682466e
--- /dev/null
+++ b/homeassistant/components/homekit/__init__.py
@@ -0,0 +1,133 @@
+"""Support for Apple Homekit.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/homekit/
+"""
+import asyncio
+import logging
+import re
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT,
+ TEMP_CELSIUS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
+from homeassistant.util import get_local_ip
+from homeassistant.util.decorator import Registry
+
+TYPES = Registry()
+_LOGGER = logging.getLogger(__name__)
+
+_RE_VALID_PINCODE = re.compile(r"^(\d{3}-\d{2}-\d{3})$")
+
+DOMAIN = 'homekit'
+REQUIREMENTS = ['HAP-python==1.1.5']
+
+BRIDGE_NAME = 'Home Assistant'
+CONF_PIN_CODE = 'pincode'
+
+HOMEKIT_FILE = '.homekit.state'
+
+
+def valid_pin(value):
+ """Validate pincode value."""
+ match = _RE_VALID_PINCODE.findall(value.strip())
+ if match == []:
+ raise vol.Invalid("Pin must be in the format: '123-45-678'")
+ return match[0]
+
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All({
+ vol.Optional(CONF_PORT, default=51826): vol.Coerce(int),
+ vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin,
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Setup the homekit component."""
+ _LOGGER.debug("Begin setup homekit")
+
+ conf = config[DOMAIN]
+ port = conf.get(CONF_PORT)
+ pin = str.encode(conf.get(CONF_PIN_CODE))
+
+ homekit = Homekit(hass, port)
+ homekit.setup_bridge(pin)
+
+ hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, homekit.start_driver)
+ return True
+
+
+def import_types():
+ """Import all types from files in the homekit dir."""
+ _LOGGER.debug("Import type files.")
+ # pylint: disable=unused-variable
+ from .covers import Window # noqa F401
+ # pylint: disable=unused-variable
+ from .sensors import TemperatureSensor # noqa F401
+
+
+def get_accessory(hass, state):
+ """Take state and return an accessory object if supported."""
+ if state.domain == 'sensor':
+ if state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS:
+ _LOGGER.debug("Add \"%s\" as \"%s\"",
+ state.entity_id, 'TemperatureSensor')
+ return TYPES['TemperatureSensor'](hass, state.entity_id,
+ state.name)
+
+ elif state.domain == 'cover':
+ # Only add covers that support set_cover_position
+ if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4:
+ _LOGGER.debug("Add \"%s\" as \"%s\"",
+ state.entity_id, 'Window')
+ return TYPES['Window'](hass, state.entity_id, state.name)
+
+ return None
+
+
+class Homekit():
+ """Class to handle all actions between homekit and Home Assistant."""
+
+ def __init__(self, hass, port):
+ """Initialize a homekit object."""
+ self._hass = hass
+ self._port = port
+ self.bridge = None
+ self.driver = None
+
+ def setup_bridge(self, pin):
+ """Setup the bridge component to track all accessories."""
+ from .accessories import HomeBridge
+ self.bridge = HomeBridge(BRIDGE_NAME, pincode=pin)
+ self.bridge.set_accessory_info('homekit.bridge')
+
+ def start_driver(self, event):
+ """Start the accessory driver."""
+ from pyhap.accessory_driver import AccessoryDriver
+ self._hass.bus.listen_once(
+ EVENT_HOMEASSISTANT_STOP, self.stop_driver)
+
+ import_types()
+ _LOGGER.debug("Start adding accessories.")
+ for state in self._hass.states.all():
+ acc = get_accessory(self._hass, state)
+ if acc is not None:
+ self.bridge.add_accessory(acc)
+
+ ip_address = get_local_ip()
+ path = self._hass.config.path(HOMEKIT_FILE)
+ self.driver = AccessoryDriver(self.bridge, self._port,
+ ip_address, path)
+ _LOGGER.debug("Driver started")
+ self.driver.start()
+
+ def stop_driver(self, event):
+ """Stop the accessory driver."""
+ _LOGGER.debug("Driver stop")
+ if self.driver is not None:
+ self.driver.stop()
diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py
new file mode 100644
index 00000000000..e1a25a2c976
--- /dev/null
+++ b/homeassistant/components/homekit/accessories.py
@@ -0,0 +1,55 @@
+"""Extend the basic Accessory and Bridge functions."""
+from pyhap.accessory import Accessory, Bridge, Category
+
+from .const import (
+ SERVICES_ACCESSORY_INFO, MANUFACTURER,
+ CHAR_MODEL, CHAR_MANUFACTURER, CHAR_SERIAL_NUMBER)
+
+
+class HomeAccessory(Accessory):
+ """Class to extend the Accessory class."""
+
+ ALL_CATEGORIES = Category
+
+ def __init__(self, display_name):
+ """Initialize a Accessory object."""
+ super().__init__(display_name)
+
+ def set_category(self, category):
+ """Set the category of the accessory."""
+ self.category = category
+
+ def add_preload_service(self, service):
+ """Define the services to be available for the accessory."""
+ from pyhap.loader import get_serv_loader
+ self.add_service(get_serv_loader().get(service))
+
+ def set_accessory_info(self, model, manufacturer=MANUFACTURER,
+ serial_number='0000'):
+ """Set the default accessory information."""
+ service_info = self.get_service(SERVICES_ACCESSORY_INFO)
+ service_info.get_characteristic(CHAR_MODEL) \
+ .set_value(model)
+ service_info.get_characteristic(CHAR_MANUFACTURER) \
+ .set_value(manufacturer)
+ service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
+ .set_value(serial_number)
+
+
+class HomeBridge(Bridge):
+ """Class to extend the Bridge class."""
+
+ def __init__(self, display_name, pincode):
+ """Initialize a Bridge object."""
+ super().__init__(display_name, pincode=pincode)
+
+ def set_accessory_info(self, model, manufacturer=MANUFACTURER,
+ serial_number='0000'):
+ """Set the default accessory information."""
+ service_info = self.get_service(SERVICES_ACCESSORY_INFO)
+ service_info.get_characteristic(CHAR_MODEL) \
+ .set_value(model)
+ service_info.get_characteristic(CHAR_MANUFACTURER) \
+ .set_value(manufacturer)
+ service_info.get_characteristic(CHAR_SERIAL_NUMBER) \
+ .set_value(serial_number)
diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py
new file mode 100644
index 00000000000..6c58b7fe45f
--- /dev/null
+++ b/homeassistant/components/homekit/const.py
@@ -0,0 +1,18 @@
+"""Constants used be the homekit component."""
+MANUFACTURER = 'HomeAssistant'
+
+# Service: AccessoryInfomation
+SERVICES_ACCESSORY_INFO = 'AccessoryInformation'
+CHAR_MODEL = 'Model'
+CHAR_MANUFACTURER = 'Manufacturer'
+CHAR_SERIAL_NUMBER = 'SerialNumber'
+
+# Service: TemperatureSensor
+SERVICES_TEMPERATURE_SENSOR = 'TemperatureSensor'
+CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature'
+
+# Service: WindowCovering
+SERVICES_WINDOW_COVERING = 'WindowCovering'
+CHAR_CURRENT_POSITION = 'CurrentPosition'
+CHAR_TARGET_POSITION = 'TargetPosition'
+CHAR_POSITION_STATE = 'PositionState'
diff --git a/homeassistant/components/homekit/covers.py b/homeassistant/components/homekit/covers.py
new file mode 100644
index 00000000000..1068b1e0e3f
--- /dev/null
+++ b/homeassistant/components/homekit/covers.py
@@ -0,0 +1,84 @@
+"""Class to hold all cover accessories."""
+import logging
+
+from homeassistant.components.cover import ATTR_CURRENT_POSITION
+from homeassistant.helpers.event import async_track_state_change
+
+from . import TYPES
+from .accessories import HomeAccessory
+from .const import (
+ SERVICES_WINDOW_COVERING, CHAR_CURRENT_POSITION,
+ CHAR_TARGET_POSITION, CHAR_POSITION_STATE)
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@TYPES.register('Window')
+class Window(HomeAccessory):
+ """Generate a Window accessory for a cover entity.
+
+ The cover entity must support: set_cover_position.
+ """
+
+ def __init__(self, hass, entity_id, display_name):
+ """Initialize a Window accessory object."""
+ super().__init__(display_name)
+ self.set_category(self.ALL_CATEGORIES.WINDOW)
+ self.set_accessory_info(entity_id)
+ self.add_preload_service(SERVICES_WINDOW_COVERING)
+
+ self._hass = hass
+ self._entity_id = entity_id
+
+ self.current_position = None
+ self.homekit_target = None
+
+ self.service_cover = self.get_service(SERVICES_WINDOW_COVERING)
+ self.char_current_position = self.service_cover. \
+ get_characteristic(CHAR_CURRENT_POSITION)
+ self.char_target_position = self.service_cover. \
+ get_characteristic(CHAR_TARGET_POSITION)
+ self.char_position_state = self.service_cover. \
+ get_characteristic(CHAR_POSITION_STATE)
+
+ self.char_target_position.setter_callback = self.move_cover
+
+ def run(self):
+ """Method called be object after driver is started."""
+ state = self._hass.states.get(self._entity_id)
+ self.update_cover_position(new_state=state)
+
+ async_track_state_change(
+ self._hass, self._entity_id, self.update_cover_position)
+
+ def move_cover(self, value):
+ """Move cover to value if call came from homekit."""
+ if value != self.current_position:
+ _LOGGER.debug("%s: Set position to %d", self._entity_id, value)
+ self.homekit_target = value
+ if value > self.current_position:
+ self.char_position_state.set_value(1)
+ elif value < self.current_position:
+ self.char_position_state.set_value(0)
+ self._hass.services.call(
+ 'cover', 'set_cover_position',
+ {'entity_id': self._entity_id, 'position': value})
+
+ def update_cover_position(self, entity_id=None, old_state=None,
+ new_state=None):
+ """Update cover position after state changed."""
+ if new_state is None:
+ return
+
+ current_position = new_state.attributes[ATTR_CURRENT_POSITION]
+ if current_position is None:
+ return
+ self.current_position = int(current_position)
+ self.char_current_position.set_value(self.current_position)
+
+ if self.homekit_target is None or \
+ abs(self.current_position - self.homekit_target) < 6:
+ self.char_target_position.set_value(self.current_position)
+ self.char_position_state.set_value(2)
+ self.homekit_target = None
diff --git a/homeassistant/components/homekit/sensors.py b/homeassistant/components/homekit/sensors.py
new file mode 100644
index 00000000000..db9ba2d628a
--- /dev/null
+++ b/homeassistant/components/homekit/sensors.py
@@ -0,0 +1,53 @@
+"""Class to hold all sensor accessories."""
+import logging
+
+from homeassistant.const import STATE_UNKNOWN
+from homeassistant.helpers.event import async_track_state_change
+
+from . import TYPES
+from .accessories import HomeAccessory
+from .const import (
+ SERVICES_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE)
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@TYPES.register('TemperatureSensor')
+class TemperatureSensor(HomeAccessory):
+ """Generate a TemperatureSensor accessory for a temperature sensor.
+
+ Sensor entity must return either temperature in °C or STATE_UNKNOWN.
+ """
+
+ def __init__(self, hass, entity_id, display_name):
+ """Initialize a TemperatureSensor accessory object."""
+ super().__init__(display_name)
+ self.set_category(self.ALL_CATEGORIES.SENSOR)
+ self.set_accessory_info(entity_id)
+ self.add_preload_service(SERVICES_TEMPERATURE_SENSOR)
+
+ self._hass = hass
+ self._entity_id = entity_id
+
+ self.service_temp = self.get_service(SERVICES_TEMPERATURE_SENSOR)
+ self.char_temp = self.service_temp. \
+ get_characteristic(CHAR_CURRENT_TEMPERATURE)
+
+ def run(self):
+ """Method called be object after driver is started."""
+ state = self._hass.states.get(self._entity_id)
+ self.update_temperature(new_state=state)
+
+ async_track_state_change(
+ self._hass, self._entity_id, self.update_temperature)
+
+ def update_temperature(self, entity_id=None, old_state=None,
+ new_state=None):
+ """Update temperature after state changed."""
+ if new_state is None:
+ return
+
+ temperature = new_state.state
+ if temperature != STATE_UNKNOWN:
+ self.char_temp.set_value(float(temperature))
diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py
index 9c08984a23e..38ce712b9b0 100644
--- a/homeassistant/components/homematic/__init__.py
+++ b/homeassistant/components/homematic/__init__.py
@@ -180,7 +180,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
}},
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
- vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
+ vol.Optional(CONF_LOCAL_PORT): cv.port,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -310,7 +310,7 @@ def setup(hass, config):
bound_system_callback = partial(_system_callback_handler, hass, config)
hass.data[DATA_HOMEMATIC] = homematic = HMConnection(
local=config[DOMAIN].get(CONF_LOCAL_IP),
- localport=config[DOMAIN].get(CONF_LOCAL_PORT),
+ localport=config[DOMAIN].get(CONF_LOCAL_PORT, DEFAULT_LOCAL_PORT),
remotes=remotes,
systemcallback=bound_system_callback,
interface_id='homeassistant'
diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py
index 33f97395945..450d802e408 100644
--- a/homeassistant/components/http/__init__.py
+++ b/homeassistant/components/http/__init__.py
@@ -5,7 +5,6 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/http/
"""
import asyncio
-from functools import wraps
from ipaddress import ip_network
import json
import logging
@@ -13,35 +12,28 @@ import os
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)
+ SERVER_PORT, CONTENT_TYPE_JSON,
+ EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,)
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.util.logging import HideSensitiveDataFilter
-from .auth import auth_middleware
-from .ban import ban_middleware
-from .const import (
- KEY_BANS_ENABLED, KEY_AUTHENTICATED, KEY_LOGIN_THRESHOLD,
- KEY_TRUSTED_NETWORKS, KEY_USE_X_FORWARDED_FOR)
+from .auth import setup_auth
+from .ban import setup_bans
+from .cors import setup_cors
+from .real_ip import setup_real_ip
+from .const import KEY_AUTHENTICATED, KEY_REAL_IP
from .static import (
CachingFileResponse, CachingStaticResource, staticresource_middleware)
-from .util import get_real_ip
REQUIREMENTS = ['aiohttp_cors==0.6.0']
-ALLOWED_CORS_HEADERS = [
- ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE,
- HTTP_HEADER_HA_AUTH]
-
DOMAIN = 'http'
CONF_API_PASSWORD = 'api_password'
@@ -81,22 +73,23 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_SERVER_HOST = '0.0.0.0'
DEFAULT_DEVELOPMENT = '0'
-DEFAULT_LOGIN_ATTEMPT_THRESHOLD = -1
+NO_LOGIN_ATTEMPT_THRESHOLD = -1
HTTP_SCHEMA = vol.Schema({
- vol.Optional(CONF_API_PASSWORD, default=None): cv.string,
+ vol.Optional(CONF_API_PASSWORD): cv.string,
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
vol.Optional(CONF_BASE_URL): cv.string,
- vol.Optional(CONF_SSL_CERTIFICATE, default=None): cv.isfile,
- vol.Optional(CONF_SSL_KEY, default=None): cv.isfile,
+ vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
+ vol.Optional(CONF_SSL_KEY): cv.isfile,
vol.Optional(CONF_CORS_ORIGINS, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_USE_X_FORWARDED_FOR, default=False): cv.boolean,
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
vol.All(cv.ensure_list, [ip_network]),
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
- default=DEFAULT_LOGIN_ATTEMPT_THRESHOLD): cv.positive_int,
+ default=NO_LOGIN_ATTEMPT_THRESHOLD):
+ vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD),
vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean
})
@@ -113,11 +106,11 @@ def async_setup(hass, config):
if conf is None:
conf = HTTP_SCHEMA({})
- api_password = conf[CONF_API_PASSWORD]
+ api_password = conf.get(CONF_API_PASSWORD)
server_host = conf[CONF_SERVER_HOST]
server_port = conf[CONF_SERVER_PORT]
- ssl_certificate = conf[CONF_SSL_CERTIFICATE]
- ssl_key = conf[CONF_SSL_KEY]
+ ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
+ ssl_key = conf.get(CONF_SSL_KEY)
cors_origins = conf[CONF_CORS_ORIGINS]
use_x_forwarded_for = conf[CONF_USE_X_FORWARDED_FOR]
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
@@ -128,7 +121,7 @@ def async_setup(hass, config):
logging.getLogger('aiohttp.access').addFilter(
HideSensitiveDataFilter(api_password))
- server = HomeAssistantWSGI(
+ server = HomeAssistantHTTP(
hass,
server_host=server_host,
server_port=server_port,
@@ -174,25 +167,29 @@ def async_setup(hass, config):
return True
-class HomeAssistantWSGI(object):
- """WSGI server for Home Assistant."""
+class HomeAssistantHTTP(object):
+ """HTTP server for Home Assistant."""
def __init__(self, hass, api_password, ssl_certificate,
ssl_key, server_host, server_port, cors_origins,
use_x_forwarded_for, trusted_networks,
login_threshold, is_ban_enabled):
- """Initialize the WSGI Home Assistant server."""
- middlewares = [auth_middleware, staticresource_middleware]
+ """Initialize the HTTP Home Assistant server."""
+ app = self.app = web.Application(
+ middlewares=[staticresource_middleware])
+
+ # This order matters
+ setup_real_ip(app, use_x_forwarded_for)
if is_ban_enabled:
- middlewares.insert(0, ban_middleware)
+ setup_bans(hass, app, login_threshold)
- self.app = web.Application(middlewares=middlewares)
- self.app['hass'] = hass
- self.app[KEY_USE_X_FORWARDED_FOR] = use_x_forwarded_for
- self.app[KEY_TRUSTED_NETWORKS] = trusted_networks
- self.app[KEY_BANS_ENABLED] = is_ban_enabled
- self.app[KEY_LOGIN_THRESHOLD] = login_threshold
+ setup_auth(app, trusted_networks, api_password)
+
+ if cors_origins:
+ setup_cors(app, cors_origins)
+
+ app['hass'] = hass
self.hass = hass
self.api_password = api_password
@@ -200,21 +197,10 @@ class HomeAssistantWSGI(object):
self.ssl_key = ssl_key
self.server_host = server_host
self.server_port = server_port
+ self.is_ban_enabled = is_ban_enabled
self._handler = None
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,
- allow_methods='*',
- ) for host in cors_origins
- })
- else:
- self.cors = None
-
def register_view(self, view):
"""Register a view with the WSGI server.
@@ -293,15 +279,7 @@ class HomeAssistantWSGI(object):
@asyncio.coroutine
def start(self):
"""Start the WSGI server."""
- cors_added = set()
- if self.cors is not None:
- for route in list(self.app.router.routes()):
- if hasattr(route, 'resource'):
- route = route.resource
- if route in cors_added:
- continue
- self.cors.add(route)
- cors_added.add(route)
+ yield from self.app.startup()
if self.ssl_certificate:
try:
@@ -362,9 +340,11 @@ class HomeAssistantView(object):
"""Return a JSON response."""
msg = json.dumps(
result, sort_keys=True, cls=rem.JSONEncoder).encode('UTF-8')
- return web.Response(
+ response = web.Response(
body=msg, content_type=CONTENT_TYPE_JSON, status=status_code,
headers=headers)
+ response.enable_compression()
+ return response
def json_message(self, message, status_code=200, message_code=None,
headers=None):
@@ -415,14 +395,13 @@ def request_handler_factory(view, handler):
if not request.app['hass'].is_running:
return web.Response(status=503)
- remote_addr = get_real_ip(request)
authenticated = request.get(KEY_AUTHENTICATED, False)
if view.requires_auth and not authenticated:
raise HTTPUnauthorized()
_LOGGER.info('Serving %s to %s (auth: %s)',
- request.path, remote_addr, authenticated)
+ request.path, request.get(KEY_REAL_IP), authenticated)
result = handler(request, **request.match_info)
@@ -449,41 +428,3 @@ def request_handler_factory(view, handler):
return web.Response(body=result, status=status_code)
return handle
-
-
-class RequestDataValidator:
- """Decorator that will validate the incoming data.
-
- Takes in a voluptuous schema and adds 'post_data' as
- keyword argument to the function call.
-
- Will return a 400 if no JSON provided or doesn't match schema.
- """
-
- def __init__(self, schema):
- """Initialize the decorator."""
- self._schema = schema
-
- def __call__(self, method):
- """Decorate a function."""
- @asyncio.coroutine
- @wraps(method)
- def wrapper(view, request, *args, **kwargs):
- """Wrap a request handler with data validation."""
- try:
- data = yield from request.json()
- except ValueError:
- _LOGGER.error('Invalid JSON received.')
- return view.json_message('Invalid JSON.', 400)
-
- try:
- kwargs['data'] = self._schema(data)
- except vol.Invalid as err:
- _LOGGER.error('Data does not match schema: %s', err)
- return view.json_message(
- 'Message format incorrect: {}'.format(err), 400)
-
- result = yield from method(view, request, *args, **kwargs)
- return result
-
- return wrapper
diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py
index a6a412b6ba2..3128489437a 100644
--- a/homeassistant/components/http/auth.py
+++ b/homeassistant/components/http/auth.py
@@ -7,55 +7,66 @@ import logging
from aiohttp import hdrs
from aiohttp.web import middleware
+from homeassistant.core import callback
from homeassistant.const import HTTP_HEADER_HA_AUTH
-from .util import get_real_ip
-from .const import KEY_TRUSTED_NETWORKS, KEY_AUTHENTICATED
+from .const import KEY_AUTHENTICATED, KEY_REAL_IP
DATA_API_PASSWORD = 'api_password'
_LOGGER = logging.getLogger(__name__)
-@middleware
-@asyncio.coroutine
-def auth_middleware(request, handler):
- """Authenticate as middleware."""
- # If no password set, just always set authenticated=True
- if request.app['hass'].http.api_password is None:
- request[KEY_AUTHENTICATED] = True
+@callback
+def setup_auth(app, trusted_networks, api_password):
+ """Create auth middleware for the app."""
+ @middleware
+ @asyncio.coroutine
+ def auth_middleware(request, handler):
+ """Authenticate as middleware."""
+ # If no password set, just always set authenticated=True
+ if api_password is None:
+ request[KEY_AUTHENTICATED] = True
+ return (yield from handler(request))
+
+ # Check authentication
+ authenticated = False
+
+ if (HTTP_HEADER_HA_AUTH in request.headers and
+ hmac.compare_digest(
+ api_password, request.headers[HTTP_HEADER_HA_AUTH])):
+ # A valid auth header has been set
+ authenticated = True
+
+ elif (DATA_API_PASSWORD in request.query and
+ hmac.compare_digest(api_password,
+ request.query[DATA_API_PASSWORD])):
+ authenticated = True
+
+ elif (hdrs.AUTHORIZATION in request.headers and
+ validate_authorization_header(api_password, request)):
+ authenticated = True
+
+ elif _is_trusted_ip(request, trusted_networks):
+ authenticated = True
+
+ request[KEY_AUTHENTICATED] = authenticated
return (yield from handler(request))
- # Check authentication
- authenticated = False
+ @asyncio.coroutine
+ def auth_startup(app):
+ """Initialize auth middleware when app starts up."""
+ app.middlewares.append(auth_middleware)
- 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 (yield from handler(request))
+ app.on_startup.append(auth_startup)
-def is_trusted_ip(request):
+def _is_trusted_ip(request, trusted_networks):
"""Test if request is from a trusted ip."""
- ip_addr = get_real_ip(request)
+ ip_addr = request[KEY_REAL_IP]
- return ip_addr and any(
+ return any(
ip_addr in trusted_network for trusted_network
- in request.app[KEY_TRUSTED_NETWORKS])
+ in trusted_networks)
def validate_password(request, api_password):
@@ -64,7 +75,7 @@ def validate_password(request, api_password):
api_password, request.app['hass'].http.api_password)
-def validate_authorization_header(request):
+def validate_authorization_header(api_password, request):
"""Test an authorization header if valid password."""
if hdrs.AUTHORIZATION not in request.headers:
return False
@@ -80,4 +91,4 @@ def validate_authorization_header(request):
if username != 'homeassistant':
return False
- return validate_password(request, password)
+ return hmac.compare_digest(api_password, password)
diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py
index f636ad80c36..4c797b05b19 100644
--- a/homeassistant/components/http/ban.py
+++ b/homeassistant/components/http/ban.py
@@ -10,18 +10,20 @@ from aiohttp.web import middleware
from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized
import voluptuous as vol
+from homeassistant.core import callback
from homeassistant.components import persistent_notification
from homeassistant.config import load_yaml_config_file
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.util.yaml import dump
-from .const import (
- KEY_BANS_ENABLED, KEY_BANNED_IPS, KEY_LOGIN_THRESHOLD,
- KEY_FAILED_LOGIN_ATTEMPTS)
-from .util import get_real_ip
+from .const import KEY_REAL_IP
_LOGGER = logging.getLogger(__name__)
+KEY_BANNED_IPS = 'ha_banned_ips'
+KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
+KEY_LOGIN_THRESHOLD = 'ha_login_threshold'
+
NOTIFICATION_ID_BAN = 'ip-ban'
NOTIFICATION_ID_LOGIN = 'http-login'
@@ -33,21 +35,31 @@ SCHEMA_IP_BAN_ENTRY = vol.Schema({
})
+@callback
+def setup_bans(hass, app, login_threshold):
+ """Create IP Ban middleware for the app."""
+ @asyncio.coroutine
+ def ban_startup(app):
+ """Initialize bans when app starts up."""
+ app.middlewares.append(ban_middleware)
+ app[KEY_BANNED_IPS] = yield from hass.async_add_job(
+ load_ip_bans_config, hass.config.path(IP_BANS_FILE))
+ app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
+ app[KEY_LOGIN_THRESHOLD] = login_threshold
+
+ app.on_startup.append(ban_startup)
+
+
@middleware
@asyncio.coroutine
def ban_middleware(request, handler):
"""IP Ban middleware."""
- if not request.app[KEY_BANS_ENABLED]:
+ if KEY_BANNED_IPS not in request.app:
+ _LOGGER.error('IP Ban middleware loaded but banned IPs not loaded')
return (yield from handler(request))
- 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))
-
# Verify if IP is not banned
- ip_address_ = get_real_ip(request)
-
+ ip_address_ = request[KEY_REAL_IP]
is_banned = any(ip_ban.ip_address == ip_address_
for ip_ban in request.app[KEY_BANNED_IPS])
@@ -64,7 +76,7 @@ def ban_middleware(request, handler):
@asyncio.coroutine
def process_wrong_login(request):
"""Process a wrong login attempt."""
- remote_addr = get_real_ip(request)
+ remote_addr = request[KEY_REAL_IP]
msg = ('Login attempt or request with invalid authentication '
'from {}'.format(remote_addr))
@@ -73,13 +85,11 @@ def process_wrong_login(request):
request.app['hass'], msg, 'Login attempt failed',
NOTIFICATION_ID_LOGIN)
- if (not request.app[KEY_BANS_ENABLED] or
+ # Check if ban middleware is loaded
+ if (KEY_BANNED_IPS not in request.app or
request.app[KEY_LOGIN_THRESHOLD] < 1):
return
- if KEY_FAILED_LOGIN_ATTEMPTS not in request.app:
- request.app[KEY_FAILED_LOGIN_ATTEMPTS] = defaultdict(int)
-
request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1
if (request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >
@@ -103,7 +113,7 @@ def process_wrong_login(request):
class IpBan(object):
"""Represents banned IP address."""
- def __init__(self, ip_ban: str, banned_at: datetime=None) -> None:
+ def __init__(self, ip_ban: str, banned_at: datetime = None) -> None:
"""Initialize IP Ban object."""
self.ip_address = ip_address(ip_ban)
self.banned_at = banned_at or datetime.utcnow()
diff --git a/homeassistant/components/http/const.py b/homeassistant/components/http/const.py
index 4250dd32514..e5494e945c4 100644
--- a/homeassistant/components/http/const.py
+++ b/homeassistant/components/http/const.py
@@ -1,11 +1,3 @@
"""HTTP specific constants."""
KEY_AUTHENTICATED = 'ha_authenticated'
-KEY_USE_X_FORWARDED_FOR = 'ha_use_x_forwarded_for'
-KEY_TRUSTED_NETWORKS = 'ha_trusted_networks'
KEY_REAL_IP = 'ha_real_ip'
-KEY_BANS_ENABLED = 'ha_bans_enabled'
-KEY_BANNED_IPS = 'ha_banned_ips'
-KEY_FAILED_LOGIN_ATTEMPTS = 'ha_failed_login_attempts'
-KEY_LOGIN_THRESHOLD = 'ha_login_threshold'
-
-HTTP_HEADER_X_FORWARDED_FOR = 'X-Forwarded-For'
diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py
new file mode 100644
index 00000000000..2eb92732d1e
--- /dev/null
+++ b/homeassistant/components/http/cors.py
@@ -0,0 +1,43 @@
+"""Provide cors support for the HTTP component."""
+import asyncio
+
+from aiohttp.hdrs import ACCEPT, ORIGIN, CONTENT_TYPE
+
+from homeassistant.const import (
+ HTTP_HEADER_X_REQUESTED_WITH, HTTP_HEADER_HA_AUTH)
+
+
+from homeassistant.core import callback
+
+
+ALLOWED_CORS_HEADERS = [
+ ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE,
+ HTTP_HEADER_HA_AUTH]
+
+
+@callback
+def setup_cors(app, origins):
+ """Setup cors."""
+ import aiohttp_cors
+
+ cors = aiohttp_cors.setup(app, defaults={
+ host: aiohttp_cors.ResourceOptions(
+ allow_headers=ALLOWED_CORS_HEADERS,
+ allow_methods='*',
+ ) for host in origins
+ })
+
+ @asyncio.coroutine
+ def cors_startup(app):
+ """Initialize cors when app starts up."""
+ cors_added = set()
+
+ for route in list(app.router.routes()):
+ if hasattr(route, 'resource'):
+ route = route.resource
+ if route in cors_added:
+ continue
+ cors.add(route)
+ cors_added.add(route)
+
+ app.on_startup.append(cors_startup)
diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py
new file mode 100644
index 00000000000..528c0a598e3
--- /dev/null
+++ b/homeassistant/components/http/data_validator.py
@@ -0,0 +1,51 @@
+"""Decorator for view methods to help with data validation."""
+import asyncio
+from functools import wraps
+import logging
+
+import voluptuous as vol
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class RequestDataValidator:
+ """Decorator that will validate the incoming data.
+
+ Takes in a voluptuous schema and adds 'post_data' as
+ keyword argument to the function call.
+
+ Will return a 400 if no JSON provided or doesn't match schema.
+ """
+
+ def __init__(self, schema, allow_empty=False):
+ """Initialize the decorator."""
+ self._schema = schema
+ self._allow_empty = allow_empty
+
+ def __call__(self, method):
+ """Decorate a function."""
+ @asyncio.coroutine
+ @wraps(method)
+ def wrapper(view, request, *args, **kwargs):
+ """Wrap a request handler with data validation."""
+ data = None
+ try:
+ data = yield from request.json()
+ except ValueError:
+ if not self._allow_empty or \
+ (yield from request.content.read()) != b'':
+ _LOGGER.error('Invalid JSON received.')
+ return view.json_message('Invalid JSON.', 400)
+ data = {}
+
+ try:
+ kwargs['data'] = self._schema(data)
+ except vol.Invalid as err:
+ _LOGGER.error('Data does not match schema: %s', err)
+ return view.json_message(
+ 'Message format incorrect: {}'.format(err), 400)
+
+ result = yield from method(view, request, *args, **kwargs)
+ return result
+
+ return wrapper
diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py
new file mode 100644
index 00000000000..1e50f33f69e
--- /dev/null
+++ b/homeassistant/components/http/real_ip.py
@@ -0,0 +1,35 @@
+"""Middleware to fetch real IP."""
+import asyncio
+from ipaddress import ip_address
+
+from aiohttp.web import middleware
+from aiohttp.hdrs import X_FORWARDED_FOR
+
+from homeassistant.core import callback
+
+from .const import KEY_REAL_IP
+
+
+@callback
+def setup_real_ip(app, use_x_forwarded_for):
+ """Create IP Ban middleware for the app."""
+ @middleware
+ @asyncio.coroutine
+ def real_ip_middleware(request, handler):
+ """Real IP middleware."""
+ if (use_x_forwarded_for and
+ X_FORWARDED_FOR in request.headers):
+ request[KEY_REAL_IP] = ip_address(
+ request.headers.get(X_FORWARDED_FOR).split(',')[0])
+ else:
+ request[KEY_REAL_IP] = \
+ ip_address(request.transport.get_extra_info('peername')[0])
+
+ return (yield from handler(request))
+
+ @asyncio.coroutine
+ def app_startup(app):
+ """Initialize bans when app starts up."""
+ app.middlewares.append(real_ip_middleware)
+
+ app.on_startup.append(app_startup)
diff --git a/homeassistant/components/http/util.py b/homeassistant/components/http/util.py
deleted file mode 100644
index 1a5a3d98a22..00000000000
--- a/homeassistant/components/http/util.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""HTTP utilities."""
-from ipaddress import ip_address
-
-from .const import (
- KEY_REAL_IP, KEY_USE_X_FORWARDED_FOR, HTTP_HEADER_X_FORWARDED_FOR)
-
-
-def get_real_ip(request):
- """Get IP address of client."""
- if KEY_REAL_IP in request:
- return request[KEY_REAL_IP]
-
- if (request.app[KEY_USE_X_FORWARDED_FOR] and
- HTTP_HEADER_X_FORWARDED_FOR in request.headers):
- request[KEY_REAL_IP] = ip_address(
- request.headers.get(HTTP_HEADER_X_FORWARDED_FOR).split(',')[0])
- else:
- peername = request.transport.get_extra_info('peername')
-
- if peername:
- request[KEY_REAL_IP] = ip_address(peername[0])
- else:
- request[KEY_REAL_IP] = None
-
- return request[KEY_REAL_IP]
diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py
index f3cd9d79046..04be7dd5ab0 100644
--- a/homeassistant/components/ihc/__init__.py
+++ b/homeassistant/components/ihc/__init__.py
@@ -46,7 +46,7 @@ AUTO_SETUP_SCHEMA = vol.Schema({
vol.All({
vol.Required(CONF_XPATH): cv.string,
vol.Required(CONF_NODE): cv.string,
- vol.Optional(CONF_TYPE, default=None): cv.string,
+ vol.Optional(CONF_TYPE): cv.string,
vol.Optional(CONF_INVERTING, default=False): cv.boolean,
})
]),
diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py
index 999dda42015..59f4d95f0a1 100644
--- a/homeassistant/components/ihc/ihcdevice.py
+++ b/homeassistant/components/ihc/ihcdevice.py
@@ -14,7 +14,7 @@ class IHCDevice(Entity):
"""
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
- product: Element=None) -> None:
+ product: Element = None) -> None:
"""Initialize IHC attributes."""
self.ihc_controller = ihc_controller
self._name = name
diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py
index 0abc449afba..df58e2e9dc4 100644
--- a/homeassistant/components/image_processing/opencv.py
+++ b/homeassistant/components/image_processing/opencv.py
@@ -42,7 +42,7 @@ DEFAULT_TIMEOUT = 10
SCAN_INTERVAL = timedelta(seconds=2)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_CLASSIFIER, default=None): {
+ vol.Optional(CONF_CLASSIFIER): {
cv.string: vol.Any(
cv.isfile,
vol.Schema({
@@ -60,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def _create_processor_from_config(hass, camera_entity, config):
"""Create an OpenCV processor from configuration."""
- classifier_config = config[CONF_CLASSIFIER]
+ classifier_config = config.get(CONF_CLASSIFIER)
name = '{} {}'.format(
config[CONF_NAME], split_entity_id(camera_entity)[1].replace('_', ' '))
@@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return
entities = []
- if config[CONF_CLASSIFIER] is None:
+ if CONF_CLASSIFIER not in config:
dest_path = hass.config.path(DEFAULT_CLASSIFIER_PATH)
_get_default_classifier(dest_path)
config[CONF_CLASSIFIER] = {
diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py
index 1ef8a4bb847..b49739bcec3 100644
--- a/homeassistant/components/image_processing/seven_segments.py
+++ b/homeassistant/components/image_processing/seven_segments.py
@@ -33,7 +33,7 @@ DEFAULT_BINARY = 'ssocr'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_EXTRA_ARGUMENTS, default=''): cv.string,
- vol.Optional(CONF_DIGITS, default=-1): cv.positive_int,
+ vol.Optional(CONF_DIGITS): cv.positive_int,
vol.Optional(CONF_HEIGHT, default=0): cv.positive_int,
vol.Optional(CONF_SSOCR_BIN, default=DEFAULT_BINARY): cv.string,
vol.Optional(CONF_THRESHOLD, default=0): cv.positive_int,
@@ -73,7 +73,7 @@ class ImageProcessingSsocr(ImageProcessingEntity):
self.filepath = os.path.join(self.hass.config.config_dir, 'ocr.png')
crop = ['crop', str(config[CONF_X_POS]), str(config[CONF_Y_POS]),
str(config[CONF_WIDTH]), str(config[CONF_HEIGHT])]
- digits = ['-d', str(config[CONF_DIGITS])]
+ digits = ['-d', str(config.get(CONF_DIGITS, -1))]
rotate = ['rotate', str(config[CONF_ROTATE])]
threshold = ['-t', str(config[CONF_THRESHOLD])]
extra_arguments = config[CONF_EXTRA_ARGUMENTS].split(' ')
diff --git a/homeassistant/components/input_datetime.py b/homeassistant/components/input_datetime.py
index fecc31f14ae..a77b67792f5 100644
--- a/homeassistant/components/input_datetime.py
+++ b/homeassistant/components/input_datetime.py
@@ -43,8 +43,8 @@ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: vol.All({
vol.Optional(CONF_NAME): cv.string,
- vol.Required(CONF_HAS_DATE): cv.boolean,
- vol.Required(CONF_HAS_TIME): cv.boolean,
+ vol.Optional(CONF_HAS_DATE, default=False): cv.boolean,
+ vol.Optional(CONF_HAS_TIME, default=False): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL): cv.string,
}, cv.has_at_least_one_key_value((CONF_HAS_DATE, True),
diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py
index 94b70e47cba..4e2e8e02c7a 100644
--- a/homeassistant/components/insteon_plm.py
+++ b/homeassistant/components/insteon_plm.py
@@ -26,8 +26,7 @@ CONF_OVERRIDE = 'device_override'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PORT): cv.string,
- vol.Optional(CONF_OVERRIDE, default=[]): vol.All(
- cv.ensure_list_csv, vol.Length(min=1))
+ vol.Optional(CONF_OVERRIDE, default=[]): cv.ensure_list_csv,
})
}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py
index df93bf51250..48a9499d1a9 100644
--- a/homeassistant/components/isy994.py
+++ b/homeassistant/components/isy994.py
@@ -83,9 +83,9 @@ NODE_FILTERS = {
},
'fan': {
'uom': [],
- 'states': ['off', 'low', 'medium', 'high'],
+ 'states': ['off', 'low', 'med', 'high'],
'node_def_id': ['FanLincMotor'],
- 'insteon_type': []
+ 'insteon_type': ['1.46.']
},
'cover': {
'uom': ['97'],
@@ -135,7 +135,7 @@ WeatherNode = namedtuple('WeatherNode', ('status', 'name', 'uom'))
def _check_for_node_def(hass: HomeAssistant, node,
- single_domain: str=None) -> bool:
+ single_domain: str = None) -> bool:
"""Check if the node matches the node_def_id for any domains.
This is only present on the 5.0 ISY firmware, and is the most reliable
@@ -157,7 +157,7 @@ def _check_for_node_def(hass: HomeAssistant, node,
def _check_for_insteon_type(hass: HomeAssistant, node,
- single_domain: str=None) -> bool:
+ single_domain: str = None) -> bool:
"""Check if the node matches the Insteon type for any domains.
This is for (presumably) every version of the ISY firmware, but only
@@ -173,6 +173,14 @@ def _check_for_insteon_type(hass: HomeAssistant, node,
for domain in domains:
if any([device_type.startswith(t) for t in
set(NODE_FILTERS[domain]['insteon_type'])]):
+
+ # Hacky special-case just for FanLinc, which has a light module
+ # as one of its nodes. Note that this special-case is not necessary
+ # on ISY 5.x firmware as it uses the superior NodeDefs method
+ if domain == 'fan' and int(node.nid[-1]) == 1:
+ hass.data[ISY994_NODES]['light'].append(node)
+ return True
+
hass.data[ISY994_NODES][domain].append(node)
return True
@@ -180,7 +188,8 @@ def _check_for_insteon_type(hass: HomeAssistant, node,
def _check_for_uom_id(hass: HomeAssistant, node,
- single_domain: str=None, uom_list: list=None) -> bool:
+ single_domain: str = None,
+ uom_list: list = None) -> bool:
"""Check if a node's uom matches any of the domains uom filter.
This is used for versions of the ISY firmware that report uoms as a single
@@ -207,8 +216,8 @@ def _check_for_uom_id(hass: HomeAssistant, node,
def _check_for_states_in_uom(hass: HomeAssistant, node,
- single_domain: str=None,
- states_list: list=None) -> bool:
+ single_domain: str = None,
+ states_list: list = None) -> bool:
"""Check if a list of uoms matches two possible filters.
This is for versions of the ISY firmware that report uoms as a list of all
@@ -302,24 +311,25 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None:
pass
else:
for dtype, _, node_id in folder.children:
- if dtype == KEY_FOLDER:
- entity_folder = folder[node_id]
- try:
- status = entity_folder[KEY_STATUS]
- assert status.dtype == 'program', 'Not a program'
- if domain != 'binary_sensor':
- actions = entity_folder[KEY_ACTIONS]
- assert actions.dtype == 'program', 'Not a program'
- else:
- actions = None
- except (AttributeError, KeyError, AssertionError):
- _LOGGER.warning("Program entity '%s' not loaded due "
- "to invalid folder structure.",
- entity_folder.name)
- continue
+ if dtype != KEY_FOLDER:
+ continue
+ entity_folder = folder[node_id]
+ try:
+ status = entity_folder[KEY_STATUS]
+ assert status.dtype == 'program', 'Not a program'
+ if domain != 'binary_sensor':
+ actions = entity_folder[KEY_ACTIONS]
+ assert actions.dtype == 'program', 'Not a program'
+ else:
+ actions = None
+ except (AttributeError, KeyError, AssertionError):
+ _LOGGER.warning("Program entity '%s' not loaded due "
+ "to invalid folder structure.",
+ entity_folder.name)
+ continue
- entity = (entity_folder.name, status, actions)
- hass.data[ISY994_PROGRAMS][domain].append(entity)
+ entity = (entity_folder.name, status, actions)
+ hass.data[ISY994_PROGRAMS][domain].append(entity)
def _categorize_weather(hass: HomeAssistant, climate) -> None:
@@ -464,8 +474,7 @@ class ISYDevice(Entity):
"""Return the state of the ISY device."""
if self.is_unknown():
return None
- else:
- return super().state
+ return super().state
@property
def device_state_attributes(self) -> Dict:
diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py
index eb5ae9a4590..a90a5246759 100644
--- a/homeassistant/components/knx.py
+++ b/homeassistant/components/knx.py
@@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
-REQUIREMENTS = ['xknx==0.7.18']
+REQUIREMENTS = ['xknx==0.8.3']
DOMAIN = "knx"
DATA_KNX = "data_knx"
@@ -120,7 +120,6 @@ class KNXModule(object):
self.hass = hass
self.config = config
self.connected = False
- self.initialized = True
self.init_xknx()
self.register_callbacks()
@@ -216,7 +215,7 @@ class KNXModule(object):
@asyncio.coroutine
def service_send_to_knx_bus(self, call):
"""Service for sending an arbitrary KNX message to the KNX bus."""
- from xknx.knx import Telegram, Address, DPTBinary, DPTArray
+ from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray
attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS)
@@ -226,7 +225,7 @@ class KNXModule(object):
return DPTBinary(attr_payload)
return DPTArray(attr_payload)
payload = calculate_payload(attr_payload)
- address = Address(attr_address)
+ address = GroupAddress(attr_address)
telegram = Telegram()
telegram.payload = payload
diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py
index 5344c3dce6d..b4b9f4e7775 100644
--- a/homeassistant/components/light/avion.py
+++ b/homeassistant/components/light/avion.py
@@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up an Avion switch."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member
import avion
lights = []
@@ -70,7 +70,7 @@ class AvionLight(Light):
def __init__(self, device):
"""Initialize the light."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member
import avion
self._name = device['name']
@@ -117,7 +117,7 @@ class AvionLight(Light):
def set_state(self, brightness):
"""Set the state of this lamp to the provided brightness."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member
import avion
# Bluetooth LE is unreliable, and the connection may drop at any
diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py
index e331fba32c2..db3171cf4cf 100644
--- a/homeassistant/components/light/blinkt.py
+++ b/homeassistant/components/light/blinkt.py
@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Blinkt Light platform."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member
import blinkt
# ensure that the lights are off when exiting
diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py
index 529917c36e2..0eef5a868b4 100644
--- a/homeassistant/components/light/deconz.py
+++ b/homeassistant/components/light/deconz.py
@@ -6,7 +6,8 @@ https://home-assistant.io/components/light.deconz/
"""
import asyncio
-from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
+from homeassistant.components.deconz import (
+ DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
@@ -17,8 +18,6 @@ from homeassistant.util.color import color_RGB_to_xy
DEPENDENCIES = ['deconz']
-ATTR_LIGHT_GROUP = 'LightGroup'
-
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@@ -26,8 +25,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if discovery_info is None:
return
- lights = hass.data[DECONZ_DATA].lights
- groups = hass.data[DECONZ_DATA].groups
+ lights = hass.data[DATA_DECONZ].lights
+ groups = hass.data[DATA_DECONZ].groups
entities = []
for light in lights.values():
@@ -64,6 +63,7 @@ class DeconzLight(Light):
def async_added_to_hass(self):
"""Subscribe to lights events."""
self._light.register_async_callback(self.async_update_callback)
+ self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._light.deconz_id
@callback
def async_update_callback(self, reason):
diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py
index 03441dd8ea6..c7478b435ee 100644
--- a/homeassistant/components/light/decora.py
+++ b/homeassistant/components/light/decora.py
@@ -37,7 +37,7 @@ def retry(method):
@wraps(method)
def wrapper_retry(device, *args, **kwargs):
"""Try send command and retry on error."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member
import decora
import bluepy
@@ -75,7 +75,7 @@ class DecoraLight(Light):
def __init__(self, device):
"""Initialize the light."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member
import decora
self._name = device['name']
diff --git a/homeassistant/components/light/decora_wifi.py b/homeassistant/components/light/decora_wifi.py
index 971ad21e84b..111d39f2019 100644
--- a/homeassistant/components/light/decora_wifi.py
+++ b/homeassistant/components/light/decora_wifi.py
@@ -36,7 +36,7 @@ NOTIFICATION_TITLE = 'myLeviton Decora Setup'
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Decora WiFi platform."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member, no-name-in-module
from decora_wifi import DecoraWiFiSession
from decora_wifi.models.person import Person
from decora_wifi.models.residential_account import ResidentialAccount
@@ -93,8 +93,7 @@ class DecoraWifiLight(Light):
"""Return supported features."""
if self._switch.canSetLevel:
return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
- else:
- return 0
+ return 0
@property
def name(self):
diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py
index 075b98117f8..2a239c9ae10 100644
--- a/homeassistant/components/light/flux_led.py
+++ b/homeassistant/components/light/flux_led.py
@@ -84,7 +84,7 @@ DEVICE_SCHEMA = vol.Schema({
vol.Optional(CONF_NAME): cv.string,
vol.Optional(ATTR_MODE, default=MODE_RGBW):
vol.All(cv.string, vol.In([MODE_RGBW, MODE_RGB])),
- vol.Optional(CONF_PROTOCOL, default=None):
+ vol.Optional(CONF_PROTOCOL):
vol.All(cv.string, vol.In(['ledenet'])),
})
@@ -104,7 +104,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device = {}
device['name'] = device_config[CONF_NAME]
device['ipaddr'] = ipaddr
- device[CONF_PROTOCOL] = device_config[CONF_PROTOCOL]
+ device[CONF_PROTOCOL] = device_config.get(CONF_PROTOCOL)
device[ATTR_MODE] = device_config[ATTR_MODE]
light = FluxLight(device)
lights.append(light)
diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py
index 5ba162a20d2..e57bdf2c046 100644
--- a/homeassistant/components/light/hive.py
+++ b/homeassistant/components/light/hive.py
@@ -116,7 +116,7 @@ class HiveDeviceLight(Light):
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Instruct the light to turn off."""
self.session.light.turn_off(self.node_id)
for entity in self.session.entities:
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index 07ba069d831..ffca48743e9 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -91,7 +91,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info is None or 'bridge_id' not in discovery_info:
return
- if config is not None and len(config) > 0:
+ if config is not None and config:
# Legacy configuration, will be removed in 0.60
config_str = yaml.dump([config])
# Indent so it renders in a fixed-width font
diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py
index ba78546cf77..e39b5dbf540 100644
--- a/homeassistant/components/light/iglo.py
+++ b/homeassistant/components/light/iglo.py
@@ -10,13 +10,14 @@ import math
import voluptuous as vol
from homeassistant.components.light import (
- ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, PLATFORM_SCHEMA,
- SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light)
+ ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR,
+ SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_EFFECT,
+ PLATFORM_SCHEMA, Light)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
-REQUIREMENTS = ['iglo==1.1.3']
+REQUIREMENTS = ['iglo==1.2.5']
_LOGGER = logging.getLogger(__name__)
@@ -46,10 +47,6 @@ class IGloLamp(Light):
from iglo import Lamp
self._name = name
self._lamp = Lamp(0, host, port)
- self._on = True
- self._brightness = 255
- self._rgb = (0, 0, 0)
- self._color_temp = 0
@property
def name(self):
@@ -59,12 +56,13 @@ class IGloLamp(Light):
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
- return int((self._brightness / 200.0) * 255)
+ return int((self._lamp.state['brightness'] / 200.0) * 255)
@property
def color_temp(self):
"""Return the color temperature."""
- return color_util.color_temperature_kelvin_to_mired(self._color_temp)
+ return color_util.color_temperature_kelvin_to_mired(
+ self._lamp.state['white'])
@property
def min_mireds(self):
@@ -81,21 +79,32 @@ class IGloLamp(Light):
@property
def rgb_color(self):
"""Return the RGB value."""
- return self._rgb
+ return self._lamp.state['rgb']
+
+ @property
+ def effect(self):
+ """Return the current effect."""
+ return self._lamp.state['effect']
+
+ @property
+ def effect_list(self):
+ """Return the list of supported effects."""
+ return self._lamp.effect_list
@property
def supported_features(self):
"""Flag supported features."""
- return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR
+ return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
+ SUPPORT_RGB_COLOR | SUPPORT_EFFECT)
@property
def is_on(self):
"""Return true if light is on."""
- return self._on
+ return self._lamp.state['on']
def turn_on(self, **kwargs):
"""Turn the light on."""
- if not self._on:
+ if not self.is_on:
self._lamp.switch(True)
if ATTR_BRIGHTNESS in kwargs:
brightness = int((kwargs[ATTR_BRIGHTNESS] / 255.0) * 200.0)
@@ -113,14 +122,11 @@ class IGloLamp(Light):
self._lamp.white(kelvin)
return
+ if ATTR_EFFECT in kwargs:
+ effect = kwargs[ATTR_EFFECT]
+ self._lamp.effect(effect)
+ return
+
def turn_off(self, **kwargs):
"""Turn the light off."""
self._lamp.switch(False)
-
- def update(self):
- """Update light status."""
- state = self._lamp.state()
- self._on = state['on']
- self._brightness = state['brightness']
- self._rgb = state['rgb']
- self._color_temp = state['white']
diff --git a/homeassistant/components/light/ihc.py b/homeassistant/components/light/ihc.py
index ead0f153562..c9ceda8651a 100644
--- a/homeassistant/components/light/ihc.py
+++ b/homeassistant/components/light/ihc.py
@@ -64,7 +64,7 @@ class IhcLight(IHCDevice, Light):
"""
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
- dimmable=False, product: Element=None) -> None:
+ dimmable=False, product: Element = None) -> None:
"""Initialize the light."""
super().__init__(ihc_controller, name, ihc_id, info, product)
self._brightness = 0
diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py
index cee8155c322..d2ed865892e 100644
--- a/homeassistant/components/light/isy994.py
+++ b/homeassistant/components/light/isy994.py
@@ -29,10 +29,6 @@ def setup_platform(hass, config: ConfigType,
class ISYLightDevice(ISYDevice, Light):
"""Representation of an ISY994 light device."""
- def __init__(self, node: object) -> None:
- """Initialize the ISY994 light device."""
- super().__init__(node)
-
@property
def is_on(self) -> bool:
"""Get whether the ISY994 light is on."""
@@ -48,6 +44,7 @@ class ISYLightDevice(ISYDevice, Light):
if not self._node.off():
_LOGGER.debug("Unable to turn off light")
+ # pylint: disable=arguments-differ
def turn_on(self, brightness=None, **kwargs) -> None:
"""Send the turn on command to the ISY994 light device."""
if not self._node.on(val=brightness):
diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py
index 8c9e78ab2b0..020184b8501 100644
--- a/homeassistant/components/light/knx.py
+++ b/homeassistant/components/light/knx.py
@@ -10,7 +10,8 @@ import voluptuous as vol
from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX
from homeassistant.components.light import (
- ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
+ ATTR_BRIGHTNESS, ATTR_RGB_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS,
+ SUPPORT_RGB_COLOR, Light)
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
@@ -19,6 +20,8 @@ CONF_ADDRESS = 'address'
CONF_STATE_ADDRESS = 'state_address'
CONF_BRIGHTNESS_ADDRESS = 'brightness_address'
CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address'
+CONF_COLOR_ADDRESS = 'color_address'
+CONF_COLOR_STATE_ADDRESS = 'color_state_address'
DEFAULT_NAME = 'KNX Light'
DEPENDENCIES = ['knx']
@@ -29,16 +32,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_STATE_ADDRESS): cv.string,
vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string,
vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
+ vol.Optional(CONF_COLOR_ADDRESS): cv.string,
+ vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string,
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up lights for KNX platform."""
- if DATA_KNX not in hass.data \
- or not hass.data[DATA_KNX].initialized:
- return
-
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
else:
@@ -66,7 +67,9 @@ def async_add_devices_config(hass, config, async_add_devices):
group_address_switch_state=config.get(CONF_STATE_ADDRESS),
group_address_brightness=config.get(CONF_BRIGHTNESS_ADDRESS),
group_address_brightness_state=config.get(
- CONF_BRIGHTNESS_STATE_ADDRESS))
+ CONF_BRIGHTNESS_STATE_ADDRESS),
+ group_address_color=config.get(CONF_COLOR_ADDRESS),
+ group_address_color_state=config.get(CONF_COLOR_STATE_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(light)
async_add_devices([KNXLight(hass, light)])
@@ -120,6 +123,8 @@ class KNXLight(Light):
@property
def rgb_color(self):
"""Return the RBG color value."""
+ if self.device.supports_color:
+ return self.device.current_color()
return None
@property
@@ -153,6 +158,8 @@ class KNXLight(Light):
flags = 0
if self.device.supports_dimming:
flags |= SUPPORT_BRIGHTNESS
+ if self.device.supports_color:
+ flags |= SUPPORT_RGB_COLOR
return flags
@asyncio.coroutine
@@ -160,6 +167,8 @@ class KNXLight(Light):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs and self.device.supports_dimming:
yield from self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
+ elif ATTR_RGB_COLOR in kwargs:
+ yield from self.device.set_color(kwargs[ATTR_RGB_COLOR])
else:
yield from self.device.set_on()
diff --git a/homeassistant/components/light/lifx_legacy.py b/homeassistant/components/light/lifx_legacy.py
index cc48f4cf4c1..cf3dba848a8 100644
--- a/homeassistant/components/light/lifx_legacy.py
+++ b/homeassistant/components/light/lifx_legacy.py
@@ -41,8 +41,8 @@ SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR |
SUPPORT_TRANSITION)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_SERVER, default=None): cv.string,
- vol.Optional(CONF_BROADCAST, default=None): cv.string,
+ vol.Optional(CONF_SERVER): cv.string,
+ vol.Optional(CONF_BROADCAST): cv.string,
})
diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py
index aad2abdd183..0606d097d49 100644
--- a/homeassistant/components/light/limitlessled.py
+++ b/homeassistant/components/light/limitlessled.py
@@ -4,19 +4,22 @@ Support for LimitlessLED bulbs.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.limitlessled/
"""
+import asyncio
import logging
import voluptuous as vol
-from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE)
+from homeassistant.const import (
+ CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE, STATE_ON)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.restore_state import async_get_last_state
-REQUIREMENTS = ['limitlessled==1.0.8']
+REQUIREMENTS = ['limitlessled==1.1.0']
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +35,9 @@ DEFAULT_TRANSITION = 0
DEFAULT_VERSION = 6
DEFAULT_FADE = False
-LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led']
+LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led', 'dimmer']
+
+EFFECT_NIGHT = 'night'
RGB_BOUNDARY = 40
@@ -40,6 +45,7 @@ WHITE = [255, 255, 255]
SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP |
SUPPORT_TRANSITION)
+SUPPORT_LIMITLESSLED_DIMMER = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION)
SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT |
SUPPORT_FLASH | SUPPORT_RGB_COLOR |
SUPPORT_TRANSITION)
@@ -115,7 +121,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_conf.get(CONF_NUMBER),
group_conf.get(CONF_NAME),
group_conf.get(CONF_TYPE, DEFAULT_LED_TYPE))
- lights.append(LimitlessLEDGroup.factory(group, {
+ lights.append(LimitlessLEDGroup(group, {
'fade': group_conf[CONF_FADE]
}))
add_devices(lights)
@@ -138,9 +144,6 @@ def state(new_state):
if self.repeating:
self.repeating = False
self.group.stop()
- # Not on and should be? Turn on.
- if not self.is_on and new_state is True:
- pipeline.on()
# Set transition time.
if ATTR_TRANSITION in kwargs:
transition_time = int(kwargs[ATTR_TRANSITION])
@@ -159,30 +162,51 @@ class LimitlessLEDGroup(Light):
def __init__(self, group, config):
"""Initialize a group."""
+ from limitlessled.group.rgbw import RgbwGroup
+ from limitlessled.group.white import WhiteGroup
+ from limitlessled.group.dimmer import DimmerGroup
+ from limitlessled.group.rgbww import RgbwwGroup
+ if isinstance(group, WhiteGroup):
+ self._supported = SUPPORT_LIMITLESSLED_WHITE
+ self._effect_list = [EFFECT_NIGHT]
+ elif isinstance(group, DimmerGroup):
+ self._supported = SUPPORT_LIMITLESSLED_DIMMER
+ self._effect_list = []
+ elif isinstance(group, RgbwGroup):
+ self._supported = SUPPORT_LIMITLESSLED_RGB
+ self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE]
+ elif isinstance(group, RgbwwGroup):
+ self._supported = SUPPORT_LIMITLESSLED_RGBWW
+ self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE]
+
self.group = group
+ self.config = config
self.repeating = False
self._is_on = False
self._brightness = None
- self.config = config
+ self._temperature = None
+ self._color = None
- @staticmethod
- def factory(group, config):
- """Produce LimitlessLEDGroup objects."""
- from limitlessled.group.rgbw import RgbwGroup
- from limitlessled.group.white import WhiteGroup
- from limitlessled.group.rgbww import RgbwwGroup
- if isinstance(group, WhiteGroup):
- return LimitlessLEDWhiteGroup(group, config)
- elif isinstance(group, RgbwGroup):
- return LimitlessLEDRGBWGroup(group, config)
- elif isinstance(group, RgbwwGroup):
- return LimitlessLEDRGBWWGroup(group, config)
+ @asyncio.coroutine
+ def async_added_to_hass(self):
+ """Called when entity is about to be added to hass."""
+ last_state = yield from async_get_last_state(self.hass, self.entity_id)
+ if last_state:
+ self._is_on = (last_state.state == STATE_ON)
+ self._brightness = last_state.attributes.get('brightness')
+ self._temperature = last_state.attributes.get('color_temp')
+ self._color = last_state.attributes.get('rgb_color')
@property
def should_poll(self):
"""No polling needed."""
return False
+ @property
+ def assumed_state(self):
+ """Return True because unable to access real state of the entity."""
+ return True
+
@property
def name(self):
"""Return the name of the group."""
@@ -198,215 +222,103 @@ class LimitlessLEDGroup(Light):
"""Return the brightness property."""
return self._brightness
+ @property
+ def color_temp(self):
+ """Return the temperature property."""
+ return self._temperature
+
+ @property
+ def rgb_color(self):
+ """Return the color property."""
+ return self._color
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return self._supported
+
+ @property
+ def effect_list(self):
+ """Return the list of supported effects for this light."""
+ return self._effect_list
+
+ # pylint: disable=arguments-differ
@state(False)
def turn_off(self, transition_time, pipeline, **kwargs):
"""Turn off a group."""
- if self.is_on:
- if self.config[CONF_FADE]:
- pipeline.transition(transition_time, brightness=0.0)
- pipeline.off()
-
-
-class LimitlessLEDWhiteGroup(LimitlessLEDGroup):
- """Representation of a LimitlessLED White group."""
-
- def __init__(self, group, config):
- """Initialize White group."""
- super().__init__(group, config)
- # Initialize group with known values.
- self.group.on = True
- self.group.temperature = 1.0
- self.group.brightness = 0.0
- self._brightness = _to_hass_brightness(1.0)
- self._temperature = _to_hass_temperature(self.group.temperature)
- self.group.on = False
-
- @property
- def color_temp(self):
- """Return the temperature property."""
- return self._temperature
-
- @property
- def supported_features(self):
- """Flag supported features."""
- return SUPPORT_LIMITLESSLED_WHITE
+ if self.config[CONF_FADE]:
+ pipeline.transition(transition_time, brightness=0.0)
+ pipeline.off()
+ # pylint: disable=arguments-differ
@state(True)
def turn_on(self, transition_time, pipeline, **kwargs):
"""Turn on (or adjust property of) a group."""
- # Check arguments.
+ # The night effect does not need a turned on light
+ if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT:
+ if EFFECT_NIGHT in self._effect_list:
+ pipeline.night_light()
+ return
+
+ pipeline.on()
+
+ # Set up transition.
+ args = {}
+ if self.config[CONF_FADE] and not self.is_on and self._brightness:
+ args['brightness'] = self.limitlessled_brightness()
+
if ATTR_BRIGHTNESS in kwargs:
self._brightness = kwargs[ATTR_BRIGHTNESS]
+ args['brightness'] = self.limitlessled_brightness()
+
+ if ATTR_RGB_COLOR in kwargs and self._supported & SUPPORT_RGB_COLOR:
+ self._color = kwargs[ATTR_RGB_COLOR]
+ # White is a special case.
+ if min(self._color) > 256 - RGB_BOUNDARY:
+ pipeline.white()
+ self._color = WHITE
+ else:
+ args['color'] = self.limitlessled_color()
+
if ATTR_COLOR_TEMP in kwargs:
- self._temperature = kwargs[ATTR_COLOR_TEMP]
- # Set up transition.
- pipeline.transition(
- transition_time,
- brightness=_from_hass_brightness(self._brightness),
- temperature=_from_hass_temperature(self._temperature)
- )
-
-
-class LimitlessLEDRGBWGroup(LimitlessLEDGroup):
- """Representation of a LimitlessLED RGBW group."""
-
- def __init__(self, group, config):
- """Initialize RGBW group."""
- super().__init__(group, config)
- # Initialize group with known values.
- self.group.on = True
- self.group.white()
- self._color = WHITE
- self.group.brightness = 0.0
- self._brightness = _to_hass_brightness(1.0)
- self.group.on = False
-
- @property
- def rgb_color(self):
- """Return the color property."""
- return self._color
-
- @property
- def supported_features(self):
- """Flag supported features."""
- return SUPPORT_LIMITLESSLED_RGB
-
- @state(True)
- def turn_on(self, transition_time, pipeline, **kwargs):
- """Turn on (or adjust property of) a group."""
- from limitlessled.presets import COLORLOOP
- # Check arguments.
- if ATTR_BRIGHTNESS in kwargs:
- self._brightness = kwargs[ATTR_BRIGHTNESS]
- if ATTR_RGB_COLOR in kwargs:
- self._color = kwargs[ATTR_RGB_COLOR]
- # White is a special case.
- if min(self._color) > 256 - RGB_BOUNDARY:
- pipeline.white()
+ if self._supported & SUPPORT_RGB_COLOR:
+ pipeline.white()
self._color = WHITE
- # Set up transition.
- pipeline.transition(
- transition_time,
- brightness=_from_hass_brightness(self._brightness),
- color=_from_hass_color(self._color)
- )
+ if self._supported & SUPPORT_COLOR_TEMP:
+ self._temperature = kwargs[ATTR_COLOR_TEMP]
+ args['temperature'] = self.limitlessled_temperature()
+
+ if args:
+ pipeline.transition(transition_time, **args)
+
# Flash.
- if ATTR_FLASH in kwargs:
+ if ATTR_FLASH in kwargs and self._supported & SUPPORT_FLASH:
duration = 0
if kwargs[ATTR_FLASH] == FLASH_LONG:
duration = 1
pipeline.flash(duration=duration)
+
# Add effects.
- if ATTR_EFFECT in kwargs:
+ if ATTR_EFFECT in kwargs and self._effect_list:
if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP:
+ from limitlessled.presets import COLORLOOP
self.repeating = True
pipeline.append(COLORLOOP)
if kwargs[ATTR_EFFECT] == EFFECT_WHITE:
pipeline.white()
self._color = WHITE
+ def limitlessled_temperature(self):
+ """Convert Home Assistant color temperature units to percentage."""
+ width = self.max_mireds - self.min_mireds
+ temperature = 1 - (self._temperature - self.min_mireds) / width
+ return max(0, min(1, temperature))
-class LimitlessLEDRGBWWGroup(LimitlessLEDGroup):
- """Representation of a LimitlessLED RGBWW group."""
+ def limitlessled_brightness(self):
+ """Convert Home Assistant brightness units to percentage."""
+ return self._brightness / 255
- def __init__(self, group, config):
- """Initialize RGBWW group."""
- super().__init__(group, config)
- # Initialize group with known values.
- self.group.on = True
- self.group.white()
- self.group.temperature = 0.0
- self._color = WHITE
- self.group.brightness = 0.0
- self._brightness = _to_hass_brightness(1.0)
- self._temperature = _to_hass_temperature(self.group.temperature)
- self.group.on = False
-
- @property
- def rgb_color(self):
- """Return the color property."""
- return self._color
-
- @property
- def color_temp(self):
- """Return the temperature property."""
- return self._temperature
-
- @property
- def supported_features(self):
- """Flag supported features."""
- return SUPPORT_LIMITLESSLED_RGBWW
-
- @state(True)
- def turn_on(self, transition_time, pipeline, **kwargs):
- """Turn on (or adjust property of) a group."""
- from limitlessled.presets import COLORLOOP
- # Check arguments.
- if ATTR_BRIGHTNESS in kwargs:
- self._brightness = kwargs[ATTR_BRIGHTNESS]
- if ATTR_RGB_COLOR in kwargs:
- self._color = kwargs[ATTR_RGB_COLOR]
- elif ATTR_COLOR_TEMP in kwargs:
- self._temperature = kwargs[ATTR_COLOR_TEMP]
- # White is a special case.
- if min(self._color) > 256 - RGB_BOUNDARY:
- pipeline.white()
- self._color = WHITE
- # Set up transition.
- if self._color == WHITE:
- pipeline.transition(
- transition_time,
- brightness=_from_hass_brightness(self._brightness),
- temperature=_from_hass_temperature(self._temperature)
- )
- else:
- pipeline.transition(
- transition_time,
- brightness=_from_hass_brightness(self._brightness),
- color=_from_hass_color(self._color)
- )
- # Flash.
- if ATTR_FLASH in kwargs:
- duration = 0
- if kwargs[ATTR_FLASH] == FLASH_LONG:
- duration = 1
- pipeline.flash(duration=duration)
- # Add effects.
- if ATTR_EFFECT in kwargs:
- if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP:
- self.repeating = True
- pipeline.append(COLORLOOP)
- if kwargs[ATTR_EFFECT] == EFFECT_WHITE:
- pipeline.white()
- self._color = WHITE
-
-
-def _from_hass_temperature(temperature):
- """Convert Home Assistant color temperature units to percentage."""
- return 1 - (temperature - 154) / 346
-
-
-def _to_hass_temperature(temperature):
- """Convert percentage to Home Assistant color temperature units."""
- return 500 - int(temperature * 346)
-
-
-def _from_hass_brightness(brightness):
- """Convert Home Assistant brightness units to percentage."""
- return brightness / 255
-
-
-def _to_hass_brightness(brightness):
- """Convert percentage to Home Assistant brightness units."""
- return int(brightness * 255)
-
-
-def _from_hass_color(color):
- """Convert Home Assistant RGB list to Color tuple."""
- from limitlessled import Color
- return Color(*tuple(color))
-
-
-def _to_hass_color(color):
- """Convert from Color tuple to Home Assistant RGB list."""
- return list([int(c) for c in color])
+ def limitlessled_color(self):
+ """Convert Home Assistant RGB list to Color tuple."""
+ from limitlessled import Color
+ return Color(*tuple(self._color))
diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py
index 9a48b13ed3b..a37553017e7 100644
--- a/homeassistant/components/light/mysensors.py
+++ b/homeassistant/components/light/mysensors.py
@@ -130,7 +130,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light):
self._white = white
self._values[self.value_type] = hex_color
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the device off."""
value_type = self.gateway.const.SetReq.V_LIGHT
self.gateway.set_child_value(
diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py
index cfd050f54f2..38cac649a1a 100644
--- a/homeassistant/components/light/template.py
+++ b/homeassistant/components/light/template.py
@@ -35,11 +35,11 @@ CONF_LEVEL_TEMPLATE = 'level_template'
LIGHT_SCHEMA = vol.Schema({
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
- vol.Optional(CONF_VALUE_TEMPLATE, default=None): cv.template,
- vol.Optional(CONF_ICON_TEMPLATE, default=None): cv.template,
- vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE, default=None): cv.template,
- vol.Optional(CONF_LEVEL_ACTION, default=None): cv.SCRIPT_SCHEMA,
- vol.Optional(CONF_LEVEL_TEMPLATE, default=None): cv.template,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+ vol.Optional(CONF_ICON_TEMPLATE): cv.template,
+ vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template,
+ vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA,
+ vol.Optional(CONF_LEVEL_TEMPLATE): cv.template,
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
@@ -56,14 +56,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
for device, device_config in config[CONF_LIGHTS].items():
friendly_name = device_config.get(CONF_FRIENDLY_NAME, device)
- state_template = device_config[CONF_VALUE_TEMPLATE]
+ state_template = device_config.get(CONF_VALUE_TEMPLATE)
icon_template = device_config.get(CONF_ICON_TEMPLATE)
entity_picture_template = device_config.get(
CONF_ENTITY_PICTURE_TEMPLATE)
on_action = device_config[CONF_ON_ACTION]
off_action = device_config[CONF_OFF_ACTION]
level_action = device_config.get(CONF_LEVEL_ACTION)
- level_template = device_config[CONF_LEVEL_TEMPLATE]
+ level_template = device_config.get(CONF_LEVEL_TEMPLATE)
template_entity_ids = set()
diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py
index 6aee02ee914..f87d624b83a 100644
--- a/homeassistant/components/light/tplink.py
+++ b/homeassistant/components/light/tplink.py
@@ -118,7 +118,7 @@ class TPLinkSmartBulb(Light):
rgb = kwargs.get(ATTR_RGB_COLOR)
self.smartbulb.hsv = rgb_to_hsv(rgb)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the light off."""
self.smartbulb.state = self.smartbulb.BULB_STATE_OFF
diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py
index 02605d24faf..e329fa04837 100644
--- a/homeassistant/components/light/wink.py
+++ b/homeassistant/components/light/wink.py
@@ -118,6 +118,6 @@ class WinkLight(WinkDevice, Light):
self.wink.set_state(True, **state_kwargs)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
self.wink.set_state(False)
diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py
index f2d327575b1..eaf41691903 100644
--- a/homeassistant/components/light/xiaomi_miio.py
+++ b/homeassistant/components/light/xiaomi_miio.py
@@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.3.6']
+REQUIREMENTS = ['python-miio==0.3.7']
# The light does not accept cct values < 1
CCT_MIN = 1
@@ -242,7 +242,7 @@ class XiaomiPhilipsGenericLight(Light):
_LOGGER.error("Got exception while fetching the state: %s", ex)
@asyncio.coroutine
- def async_set_scene(self, scene: int=1):
+ def async_set_scene(self, scene: int = 1):
"""Set the fixed scene."""
yield from self._try_command(
"Setting a fixed scene failed.",
@@ -260,10 +260,6 @@ class XiaomiPhilipsGenericLight(Light):
class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
"""Representation of a Xiaomi Philips Light Ball."""
- def __init__(self, name, light, device_info):
- """Initialize the light device."""
- super().__init__(name, light, device_info)
-
@property
def color_temp(self):
"""Return the color temperature."""
@@ -293,6 +289,28 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
color_temp, self.max_mireds,
self.min_mireds, CCT_MIN, CCT_MAX)
+ if ATTR_BRIGHTNESS in kwargs:
+ brightness = kwargs[ATTR_BRIGHTNESS]
+ percent_brightness = ceil(100 * brightness / 255.0)
+
+ if ATTR_BRIGHTNESS in kwargs and ATTR_COLOR_TEMP in kwargs:
+ _LOGGER.debug(
+ "Setting brightness and color temperature: "
+ "%s %s%%, %s mireds, %s%% cct",
+ brightness, percent_brightness,
+ color_temp, percent_color_temp)
+
+ result = yield from self._try_command(
+ "Setting brightness and color temperature failed: "
+ "%s bri, %s cct",
+ self._light.set_brightness_and_color_temperature,
+ percent_brightness, percent_color_temp)
+
+ if result:
+ self._color_temp = color_temp
+ self._brightness = brightness
+
+ elif ATTR_COLOR_TEMP in kwargs:
_LOGGER.debug(
"Setting color temperature: "
"%s mireds, %s%% cct",
@@ -305,7 +323,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
if result:
self._color_temp = color_temp
- if ATTR_BRIGHTNESS in kwargs:
+ elif ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
percent_brightness = ceil(100 * brightness / 255.0)
@@ -320,8 +338,9 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
if result:
self._brightness = brightness
- self._state = yield from self._try_command(
- "Turning the light on failed.", self._light.on)
+ else:
+ self._state = yield from self._try_command(
+ "Turning the light on failed.", self._light.on)
@asyncio.coroutine
def async_update(self):
@@ -345,10 +364,6 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light):
"""Representation of a Xiaomi Philips Ceiling Lamp."""
- def __init__(self, name, light, device_info):
- """Initialize the light device."""
- super().__init__(name, light, device_info)
-
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
@@ -363,6 +378,4 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light):
class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight, Light):
"""Representation of a Xiaomi Philips Eyecare Lamp 2."""
- def __init__(self, name, light, device_info):
- """Initialize the light device."""
- super().__init__(name, light, device_info)
+ pass
diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py
index ea4df658ef6..0cd49ab6c9a 100644
--- a/homeassistant/components/lirc.py
+++ b/homeassistant/components/lirc.py
@@ -4,7 +4,7 @@ LIRC interface to receive signals from an infrared remote control.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/lirc/
"""
-# pylint: disable=import-error
+# pylint: disable=import-error,no-member
import threading
import time
import logging
diff --git a/homeassistant/components/lock/august.py b/homeassistant/components/lock/august.py
new file mode 100644
index 00000000000..9ca63cb493b
--- /dev/null
+++ b/homeassistant/components/lock/august.py
@@ -0,0 +1,82 @@
+"""
+Support for August lock.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/lock.august/
+"""
+from datetime import timedelta
+
+from homeassistant.components.august import DATA_AUGUST
+from homeassistant.components.lock import LockDevice
+from homeassistant.const import ATTR_BATTERY_LEVEL
+
+DEPENDENCIES = ['august']
+
+SCAN_INTERVAL = timedelta(seconds=5)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up August locks."""
+ data = hass.data[DATA_AUGUST]
+ devices = []
+
+ for lock in data.locks:
+ devices.append(AugustLock(data, lock))
+
+ add_devices(devices, True)
+
+
+class AugustLock(LockDevice):
+ """Representation of an August lock."""
+
+ def __init__(self, data, lock):
+ """Initialize the lock."""
+ self._data = data
+ self._lock = lock
+ self._lock_status = None
+ self._lock_detail = None
+ self._changed_by = None
+
+ def lock(self, **kwargs):
+ """Lock the device."""
+ self._data.lock(self._lock.device_id)
+
+ def unlock(self, **kwargs):
+ """Unlock the device."""
+ self._data.unlock(self._lock.device_id)
+
+ def update(self):
+ """Get the latest state of the sensor."""
+ self._lock_status = self._data.get_lock_status(self._lock.device_id)
+ self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
+
+ from august.activity import ActivityType
+ activity = self._data.get_latest_device_activity(
+ self._lock.device_id,
+ ActivityType.LOCK_OPERATION)
+
+ if activity is not None:
+ self._changed_by = activity.operated_by
+
+ @property
+ def name(self):
+ """Return the name of this device."""
+ return self._lock.device_name
+
+ @property
+ def is_locked(self):
+ """Return true if device is on."""
+ from august.lock import LockStatus
+ return self._lock_status is LockStatus.LOCKED
+
+ @property
+ def changed_by(self):
+ """Last change triggered by."""
+ return self._changed_by
+
+ @property
+ def device_state_attributes(self):
+ """Return the device specific state attributes."""
+ return {
+ ATTR_BATTERY_LEVEL: self._lock_detail.battery_level,
+ }
diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py
index 33e2a0bea25..50371fdc9ae 100644
--- a/homeassistant/components/lock/isy994.py
+++ b/homeassistant/components/lock/isy994.py
@@ -53,8 +53,7 @@ class ISYLockDevice(ISYDevice, LockDevice):
"""Get the state of the lock."""
if self.is_unknown():
return None
- else:
- return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
+ return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
def lock(self, **kwargs) -> None:
"""Send the lock command to the ISY994 device."""
diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py
index c0560722966..8f39d440cae 100644
--- a/homeassistant/components/lock/zwave.py
+++ b/homeassistant/components/lock/zwave.py
@@ -49,6 +49,7 @@ LOCK_NOTIFICATION = {
LOCK_ALARM_TYPE = {
'9': 'Deadbolt Jammed',
+ '16': 'Unlocked by Bluetooth ',
'18': 'Locked with Keypad by user ',
'19': 'Unlocked with Keypad by user ',
'21': 'Manually Locked ',
@@ -60,6 +61,7 @@ LOCK_ALARM_TYPE = {
'112': 'Master code changed or User added: ',
'113': 'Duplicate Pin-code: ',
'130': 'RF module, power restored',
+ '144': 'Unlocked by NFC Tag or Card by user ',
'161': 'Tamper Alarm: ',
'167': 'Low Battery',
'168': 'Critical Battery Level',
@@ -98,7 +100,8 @@ ALARM_TYPE_STD = [
'19',
'33',
'112',
- '113'
+ '113',
+ '144'
]
SET_USERCODE_SCHEMA = vol.Schema({
diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py
index 9e1e2e54ad9..1fc6d1587fc 100644
--- a/homeassistant/components/logbook.py
+++ b/homeassistant/components/logbook.py
@@ -47,6 +47,11 @@ CONFIG_SCHEMA = vol.Schema({
}),
}, extra=vol.ALLOW_EXTRA)
+ALL_EVENT_TYPES = [
+ EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY,
+ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
+]
+
GROUP_BY_MINUTES = 15
CONTINUOUS_DOMAINS = ['proximity', 'sensor']
@@ -136,7 +141,8 @@ class LogbookView(HomeAssistantView):
events = yield from hass.async_add_job(
_get_events, hass, self.config, start_day, end_day)
- return self.json(events)
+ response = yield from hass.async_add_job(self.json, events)
+ return response
class Entry(object):
@@ -169,6 +175,8 @@ def humanify(events):
- if 2+ sensor updates in GROUP_BY_MINUTES, show last
- if home assistant stop and start happen in same minute call it restarted
"""
+ domain_prefixes = tuple('{}.'.format(dom) for dom in CONTINUOUS_DOMAINS)
+
# Group events in batches of GROUP_BY_MINUTES
for _, g_events in groupby(
events,
@@ -188,11 +196,7 @@ def humanify(events):
if event.event_type == EVENT_STATE_CHANGED:
entity_id = event.data.get('entity_id')
- if entity_id is None:
- continue
-
- if entity_id.startswith(tuple('{}.'.format(
- domain) for domain in CONTINUOUS_DOMAINS)):
+ if entity_id.startswith(domain_prefixes):
last_sensor_event[entity_id] = event
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
@@ -213,14 +217,6 @@ def humanify(events):
to_state = State.from_dict(event.data.get('new_state'))
- # If last_changed != last_updated only attributes have changed
- # we do not report on that yet. Also filter auto groups.
- if not to_state or \
- to_state.last_changed != to_state.last_updated or \
- to_state.domain == 'group' and \
- to_state.attributes.get('auto', False):
- continue
-
domain = to_state.domain
# Skip all but the last sensor state
@@ -275,21 +271,24 @@ def humanify(events):
def _get_events(hass, config, start_day, end_day):
"""Get events for a period of time."""
- from homeassistant.components.recorder.models import Events
+ from homeassistant.components.recorder.models import Events, States
from homeassistant.components.recorder.util import (
execute, session_scope)
with session_scope(hass=hass) as session:
- query = session.query(Events).order_by(
- Events.time_fired).filter(
- (Events.time_fired > start_day) &
- (Events.time_fired < end_day))
+ query = session.query(Events).order_by(Events.time_fired) \
+ .outerjoin(States, (Events.event_id == States.event_id)) \
+ .filter(Events.event_type.in_(ALL_EVENT_TYPES)) \
+ .filter((Events.time_fired > start_day)
+ & (Events.time_fired < end_day)) \
+ .filter((States.last_updated == States.last_changed)
+ | (States.last_updated.is_(None)))
events = execute(query)
return humanify(_exclude_events(events, config))
def _exclude_events(events, config):
- """Get lists of excluded entities and platforms."""
+ """Get list of filtered events."""
excluded_entities = []
excluded_domains = []
included_entities = []
@@ -308,23 +307,41 @@ def _exclude_events(events, config):
domain, entity_id = None, None
if event.event_type == EVENT_STATE_CHANGED:
- to_state = State.from_dict(event.data.get('new_state'))
+ entity_id = event.data.get('entity_id')
+
+ if entity_id is None:
+ continue
+
# Do not report on new entities
if event.data.get('old_state') is None:
continue
+ new_state = event.data.get('new_state')
+
# Do not report on entity removal
- if not to_state:
+ if not new_state:
+ continue
+
+ attributes = new_state.get('attributes', {})
+
+ # If last_changed != last_updated only attributes have changed
+ # we do not report on that yet.
+ last_changed = new_state.get('last_changed')
+ last_updated = new_state.get('last_updated')
+ if last_changed != last_updated:
+ continue
+
+ domain = split_entity_id(entity_id)[0]
+
+ # Also filter auto groups.
+ if domain == 'group' and attributes.get('auto', False):
continue
# exclude entities which are customized hidden
- hidden = to_state.attributes.get(ATTR_HIDDEN, False)
+ hidden = attributes.get(ATTR_HIDDEN, False)
if hidden:
continue
- domain = to_state.domain
- entity_id = to_state.entity_id
-
elif event.event_type == EVENT_LOGBOOK_ENTRY:
domain = event.data.get(ATTR_DOMAIN)
entity_id = event.data.get(ATTR_ENTITY_ID)
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index f712007ccec..265784be74d 100644
--- a/homeassistant/components/media_extractor.py
+++ b/homeassistant/components/media_extractor.py
@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA)
from homeassistant.helpers import config_validation as cv
-REQUIREMENTS = ['youtube_dl==2018.01.21']
+REQUIREMENTS = ['youtube_dl==2018.02.11']
_LOGGER = logging.getLogger(__name__)
@@ -85,7 +85,7 @@ class MediaExtractor(object):
else:
entities = self.get_entities()
- if len(entities) == 0:
+ if not entities:
self.call_media_player_service(stream_selector, None)
for entity_id in entities:
@@ -108,7 +108,7 @@ class MediaExtractor(object):
_LOGGER.warning(
"Playlists are not supported, looking for the first video")
entries = list(all_media['entries'])
- if len(entries) > 0:
+ if entries:
selected_media = entries[0]
else:
_LOGGER.error("Playlist is empty")
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 06e89548785..37536bf5586 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -31,7 +31,6 @@ 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
_LOGGER = logging.getLogger(__name__)
_RND = SystemRandom()
@@ -878,12 +877,6 @@ class MediaPlayerDevice(Entity):
return state_attr
- def preload_media_image_url(self, url):
- """Preload and cache a media image for future use."""
- run_coroutine_threadsafe(
- _async_fetch_image(self.hass, url), self.hass.loop
- ).result()
-
@asyncio.coroutine
def _async_fetch_image(hass, url):
diff --git a/homeassistant/components/media_player/aquostv.py b/homeassistant/components/media_player/aquostv.py
index ae6d9e04643..6933286f0fe 100644
--- a/homeassistant/components/media_player/aquostv.py
+++ b/homeassistant/components/media_player/aquostv.py
@@ -201,9 +201,9 @@ class SharpAquosTVDevice(MediaPlayerDevice):
self._remote.volume(int(self._volume * 60) - 2)
@_retry
- def set_volume_level(self, level):
+ def set_volume_level(self, volume):
"""Set Volume media player."""
- self._remote.volume(int(level * 60))
+ self._remote.volume(int(volume * 60))
@_retry
def mute_volume(self, mute):
diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py
index 848c6abe91f..d308b94e64c 100644
--- a/homeassistant/components/media_player/bluesound.py
+++ b/homeassistant/components/media_player/bluesound.py
@@ -16,14 +16,16 @@ import async_timeout
import voluptuous as vol
from homeassistant.components.media_player import (
- MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA, SUPPORT_CLEAR_PLAYLIST,
- SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA,
- SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP,
+ ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
+ SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY,
+ SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
+ SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP,
MediaPlayerDevice)
from homeassistant.const import (
- CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START,
- EVENT_HOMEASSISTANT_STOP, STATE_IDLE, STATE_PAUSED, STATE_PLAYING)
+ ATTR_ENTITY_ID, CONF_HOST, CONF_HOSTS, CONF_NAME, CONF_PORT,
+ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_IDLE,
+ STATE_OFF, STATE_PAUSED, STATE_PLAYING)
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@@ -35,10 +37,14 @@ REQUIREMENTS = ['xmltodict==0.11.0']
_LOGGER = logging.getLogger(__name__)
-STATE_OFFLINE = 'offline'
-ATTR_MODEL = 'model'
-ATTR_MODEL_NAME = 'model_name'
-ATTR_BRAND = 'brand'
+STATE_GROUPED = 'grouped'
+
+ATTR_MASTER = 'master'
+
+SERVICE_JOIN = 'bluesound_join'
+SERVICE_UNJOIN = 'bluesound_unjoin'
+SERVICE_SET_TIMER = 'bluesound_set_sleep_timer'
+SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer'
DATA_BLUESOUND = 'bluesound'
DEFAULT_PORT = 11000
@@ -58,6 +64,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}])
})
+BS_SCHEMA = vol.Schema({
+ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
+})
+
+BS_JOIN_SCHEMA = BS_SCHEMA.extend({
+ vol.Required(ATTR_MASTER): cv.entity_id,
+})
+
+SERVICE_TO_METHOD = {
+ SERVICE_JOIN: {
+ 'method': 'async_join',
+ 'schema': BS_JOIN_SCHEMA},
+ SERVICE_UNJOIN: {
+ 'method': 'async_unjoin',
+ 'schema': BS_SCHEMA},
+ SERVICE_SET_TIMER: {
+ 'method': 'async_increase_timer',
+ 'schema': BS_SCHEMA},
+ SERVICE_CLEAR_TIMER: {
+ 'method': 'async_clear_timer',
+ 'schema': BS_SCHEMA}
+}
+
def _add_player(hass, async_add_devices, host, port=None, name=None):
"""Add Bluesound players."""
@@ -120,6 +149,30 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hass, async_add_devices, host.get(CONF_HOST),
host.get(CONF_PORT), host.get(CONF_NAME))
+ @asyncio.coroutine
+ def async_service_handler(service):
+ """Map services to method of Bluesound devices."""
+ method = SERVICE_TO_METHOD.get(service.service)
+ if not method:
+ return
+
+ params = {key: value for key, value in service.data.items()
+ if key != ATTR_ENTITY_ID}
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+ if entity_ids:
+ target_players = [player for player in hass.data[DATA_BLUESOUND]
+ if player.entity_id in entity_ids]
+ else:
+ target_players = hass.data[DATA_BLUESOUND]
+
+ for player in target_players:
+ yield from getattr(player, method['method'])(**params)
+
+ for service in SERVICE_TO_METHOD:
+ schema = SERVICE_TO_METHOD[service]['schema']
+ hass.services.async_register(
+ DOMAIN, service, async_service_handler, schema=schema)
+
class BluesoundPlayer(MediaPlayerDevice):
"""Representation of a Bluesound Player."""
@@ -128,13 +181,10 @@ class BluesoundPlayer(MediaPlayerDevice):
"""Initialize the media player."""
self.host = host
self._hass = hass
- self._port = port
+ self.port = port
self._polling_session = async_get_clientsession(hass)
self._polling_task = None # The actual polling task.
self._name = name
- self._brand = None
- self._model = None
- self._model_name = None
self._icon = None
self._capture_items = []
self._services_items = []
@@ -145,9 +195,13 @@ class BluesoundPlayer(MediaPlayerDevice):
self._is_online = False
self._retry_remove = None
self._lastvol = None
+ self._master = None
+ self._is_master = False
+ self._group_name = None
+
self._init_callback = init_callback
- if self._port is None:
- self._port = DEFAULT_PORT
+ if self.port is None:
+ self.port = DEFAULT_PORT
@staticmethod
def _try_get_index(string, search_string):
@@ -158,7 +212,7 @@ class BluesoundPlayer(MediaPlayerDevice):
return -1
@asyncio.coroutine
- def _internal_update_sync_status(
+ def force_update_sync_status(
self, on_updated_cb=None, raise_timeout=False):
"""Update the internal status."""
resp = None
@@ -174,14 +228,27 @@ class BluesoundPlayer(MediaPlayerDevice):
if not self._name:
self._name = self._sync_status.get('@name', self.host)
- if not self._brand:
- self._brand = self._sync_status.get('@brand', self.host)
- if not self._model:
- self._model = self._sync_status.get('@model', self.host)
if not self._icon:
self._icon = self._sync_status.get('@icon', self.host)
- if not self._model_name:
- self._model_name = self._sync_status.get('@modelName', self.host)
+
+ master = self._sync_status.get('master', None)
+ if master is not None:
+ self._is_master = False
+ master_host = master.get('#text')
+ master_device = [device for device in
+ self._hass.data[DATA_BLUESOUND]
+ if device.host == master_host]
+
+ if master_device and master_host != self.host:
+ self._master = master_device[0]
+ else:
+ self._master = None
+ _LOGGER.error("Master not found %s", master_host)
+ else:
+ if self._master is not None:
+ self._master = None
+ slaves = self._sync_status.get('slave', None)
+ self._is_master = slaves is not None
if on_updated_cb:
on_updated_cb()
@@ -223,7 +290,7 @@ class BluesoundPlayer(MediaPlayerDevice):
self._retry_remove()
self._retry_remove = None
- yield from self._internal_update_sync_status(
+ yield from self.force_update_sync_status(
self._init_callback, True)
except (asyncio.TimeoutError, ClientError):
_LOGGER.info("Node %s is offline, retrying later", self.host)
@@ -256,7 +323,7 @@ class BluesoundPlayer(MediaPlayerDevice):
if method[0] == '/':
method = method[1:]
- url = "http://{}:{}/{}".format(self.host, self._port, method)
+ url = "http://{}:{}/{}".format(self.host, self.port, method)
_LOGGER.debug("Calling URL: %s", url)
response = None
@@ -297,42 +364,71 @@ class BluesoundPlayer(MediaPlayerDevice):
etag = self._status.get('@etag', '')
if etag != '':
- url = 'Status?etag={}&timeout=60.0'.format(etag)
- url = "http://{}:{}/{}".format(self.host, self._port, url)
+ url = 'Status?etag={}&timeout=120.0'.format(etag)
+ url = "http://{}:{}/{}".format(self.host, self.port, url)
_LOGGER.debug("Calling URL: %s", url)
try:
- with async_timeout.timeout(65, loop=self._hass.loop):
+ with async_timeout.timeout(125, loop=self._hass.loop):
response = yield from self._polling_session.get(
url,
headers={CONNECTION: KEEP_ALIVE})
if response.status != 200:
- _LOGGER.error("Error %s on %s", response.status, url)
+ _LOGGER.error("Error %s on %s. Trying one more time.",
+ response.status, url)
+ else:
+ result = yield from response.text()
+ self._is_online = True
+ self._last_status_update = dt_util.utcnow()
+ self._status = xmltodict.parse(result)['status'].copy()
- result = yield from response.text()
- self._is_online = True
- self._last_status_update = dt_util.utcnow()
- self._status = xmltodict.parse(result)['status'].copy()
- self.schedule_update_ha_state()
+ group_name = self._status.get('groupName', None)
+ if group_name != self._group_name:
+ _LOGGER.debug('Group name change detected on device: %s',
+ self.host)
+ self._group_name = group_name
+ # the sleep is needed to make sure that the
+ # devices is synced
+ yield from asyncio.sleep(1, loop=self._hass.loop)
+ yield from self.async_trigger_sync_on_all()
+ elif self.is_grouped:
+ # when player is grouped we need to fetch volume from
+ # sync_status. We will force an update if the player is
+ # grouped this isn't a foolproof solution. A better
+ # solution would be to fetch sync_status more often when
+ # the device is playing. This would solve alot of
+ # problems. This change will be done when the
+ # communication is moved to a separate library
+ yield from self.force_update_sync_status()
+
+ self.async_schedule_update_ha_state()
except (asyncio.TimeoutError, ClientError):
self._is_online = False
self._last_status_update = None
self._status = None
- self.schedule_update_ha_state()
+ self.async_schedule_update_ha_state()
_LOGGER.info("Client connection error, marking %s as offline",
self._name)
raise
+ @asyncio.coroutine
+ def async_trigger_sync_on_all(self):
+ """Trigger sync status update on all devices."""
+ _LOGGER.debug("Trigger sync status on all devices")
+
+ for player in self._hass.data[DATA_BLUESOUND]:
+ yield from player.force_update_sync_status()
+
@asyncio.coroutine
@Throttle(SYNC_STATUS_INTERVAL)
def async_update_sync_status(self, on_updated_cb=None,
raise_timeout=False):
"""Update sync status."""
- yield from self._internal_update_sync_status(
+ yield from self.force_update_sync_status(
on_updated_cb, raise_timeout=False)
@asyncio.coroutine
@@ -433,20 +529,23 @@ class BluesoundPlayer(MediaPlayerDevice):
def state(self):
"""Return the state of the device."""
if self._status is None:
- return STATE_OFFLINE
+ return STATE_OFF
+
+ if self.is_grouped and not self.is_master:
+ return STATE_GROUPED
status = self._status.get('state', None)
if status == 'pause' or status == 'stop':
return STATE_PAUSED
elif status == 'stream' or status == 'play':
return STATE_PLAYING
- else:
- return STATE_IDLE
+ return STATE_IDLE
@property
def media_title(self):
"""Title of current playing media."""
- if self._status is None:
+ if (self._status is None or
+ (self.is_grouped and not self.is_master)):
return None
return self._status.get('title1', None)
@@ -457,6 +556,9 @@ class BluesoundPlayer(MediaPlayerDevice):
if self._status is None:
return None
+ if self.is_grouped and not self.is_master:
+ return self._group_name
+
artist = self._status.get('artist', None)
if not artist:
artist = self._status.get('title2', None)
@@ -465,7 +567,8 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def media_album_name(self):
"""Artist of current playing media (Music track only)."""
- if self._status is None:
+ if (self._status is None or
+ (self.is_grouped and not self.is_master)):
return None
album = self._status.get('album', None)
@@ -476,21 +579,23 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def media_image_url(self):
"""Image url of current playing media."""
- if self._status is None:
+ if (self._status is None or
+ (self.is_grouped and not self.is_master)):
return None
url = self._status.get('image', None)
if not url:
return
if url[0] == '/':
- url = "http://{}:{}{}".format(self.host, self._port, url)
+ url = "http://{}:{}{}".format(self.host, self.port, url)
return url
@property
def media_position(self):
"""Position of current playing media in seconds."""
- if self._status is None:
+ if (self._status is None or
+ (self.is_grouped and not self.is_master)):
return None
mediastate = self.state
@@ -511,7 +616,8 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
- if self._status is None:
+ if (self._status is None or
+ (self.is_grouped and not self.is_master)):
return None
duration = self._status.get('totlen', None)
@@ -527,10 +633,10 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
- if self._status is None:
- return None
-
volume = self._status.get('volume', None)
+ if self.is_grouped:
+ volume = self._sync_status.get('@volume', None)
+
if volume is not None:
return int(volume) / 100
return None
@@ -538,9 +644,6 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def is_volume_muted(self):
"""Boolean if volume is currently muted."""
- if not self._status:
- return None
-
volume = self.volume_level
if not volume:
return None
@@ -559,7 +662,8 @@ class BluesoundPlayer(MediaPlayerDevice):
@property
def source_list(self):
"""List of available input sources."""
- if self._status is None:
+ if (self._status is None or
+ (self.is_grouped and not self.is_master)):
return None
sources = []
@@ -582,7 +686,8 @@ class BluesoundPlayer(MediaPlayerDevice):
"""Name of the current input source."""
from urllib import parse
- if self._status is None:
+ if (self._status is None or
+ (self.is_grouped and not self.is_master)):
return None
current_service = self._status.get('service', '')
@@ -595,7 +700,7 @@ class BluesoundPlayer(MediaPlayerDevice):
# 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:
+ if items:
return items[0]['title']
# This could be a bit difficult to detect. Bluetooth could be named
@@ -606,11 +711,11 @@ class BluesoundPlayer(MediaPlayerDevice):
if title == 'bluetooth' or stream_url == 'Capture:hw:2,0/44100/16/2':
items = [x for x in self._capture_items
if x['url'] == "Capture%3Abluez%3Abluetooth"]
- if len(items) > 0:
+ if items:
return items[0]['title']
items = [x for x in self._capture_items if x['url'] == stream_url]
- if len(items) > 0:
+ if items:
return items[0]['title']
if stream_url[:8] == 'Capture:':
@@ -631,12 +736,12 @@ class BluesoundPlayer(MediaPlayerDevice):
items = [x for x in self._capture_items
if x['name'] == current_service]
- if len(items) > 0:
+ if items:
return items[0]['title']
items = [x for x in self._services_items
if x['name'] == current_service]
- if len(items) > 0:
+ if items:
return items[0]['title']
if self._status.get('streamUrl', '') != '':
@@ -650,12 +755,17 @@ class BluesoundPlayer(MediaPlayerDevice):
if self._status is None:
return None
+ if self.is_grouped and not self.is_master:
+ return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | \
+ SUPPORT_VOLUME_MUTE
+
supported = SUPPORT_CLEAR_PLAYLIST
if self._status.get('indexing', '0') == '0':
supported = supported | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | \
- SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE
+ SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_SELECT_SOURCE | \
+ SUPPORT_SHUFFLE_SET
current_vol = self.volume_level
if current_vol is not None and current_vol >= 0:
@@ -668,17 +778,87 @@ class BluesoundPlayer(MediaPlayerDevice):
return supported
@property
- def device_state_attributes(self):
- """Return the state attributes."""
- return {
- ATTR_MODEL: self._model,
- ATTR_MODEL_NAME: self._model_name,
- ATTR_BRAND: self._brand,
- }
+ def is_master(self):
+ """Return true if player is a coordinator."""
+ return self._is_master
+
+ @property
+ def is_grouped(self):
+ """Return true if player is a coordinator."""
+ return self._master is not None or self._is_master
+
+ @property
+ def shuffle(self):
+ """Return true if shuffle is active."""
+ return True if self._status.get('shuffle', '0') == '1' else False
+
+ @asyncio.coroutine
+ def async_join(self, master):
+ """Join the player to a group."""
+ master_device = [device for device in self.hass.data[DATA_BLUESOUND]
+ if device.entity_id == master]
+
+ if master_device:
+ _LOGGER.debug("Trying to join player: %s to master: %s",
+ self.host, master_device[0].host)
+
+ yield from master_device[0].async_add_slave(self)
+ else:
+ _LOGGER.error("Master not found %s", master_device)
+
+ @asyncio.coroutine
+ def async_unjoin(self):
+ """Unjoin the player from a group."""
+ if self._master is None:
+ return
+
+ _LOGGER.debug("Trying to unjoin player: %s", self.host)
+ yield from self._master.async_remove_slave(self)
+
+ @asyncio.coroutine
+ def async_add_slave(self, slave_device):
+ """Add slave to master."""
+ return self.send_bluesound_command('/AddSlave?slave={}&port={}'
+ .format(slave_device.host,
+ slave_device.port))
+
+ @asyncio.coroutine
+ def async_remove_slave(self, slave_device):
+ """Remove slave to master."""
+ return self.send_bluesound_command('/RemoveSlave?slave={}&port={}'
+ .format(slave_device.host,
+ slave_device.port))
+
+ @asyncio.coroutine
+ def async_increase_timer(self):
+ """Increase sleep time on player."""
+ sleep_time = yield from self.send_bluesound_command('/Sleep')
+ if sleep_time is None:
+ _LOGGER.error('Error while increasing sleep time on player: %s',
+ self.host)
+ return 0
+
+ return int(sleep_time.get('sleep', '0'))
+
+ @asyncio.coroutine
+ def async_clear_timer(self):
+ """Clear sleep timer on player."""
+ sleep = 1
+ while sleep > 0:
+ sleep = yield from self.async_increase_timer()
+
+ @asyncio.coroutine
+ def async_set_shuffle(self, shuffle):
+ """Enable or disable shuffle mode."""
+ return self.send_bluesound_command('/Shuffle?state={}'
+ .format('1' if shuffle else '0'))
@asyncio.coroutine
def async_select_source(self, source):
"""Select input source."""
+ if self.is_grouped and not self.is_master:
+ return
+
items = [x for x in self._preset_items if x['title'] == source]
if len(items) < 1:
@@ -701,11 +881,17 @@ class BluesoundPlayer(MediaPlayerDevice):
@asyncio.coroutine
def async_clear_playlist(self):
"""Clear players playlist."""
+ if self.is_grouped and not self.is_master:
+ return
+
return self.send_bluesound_command('Clear')
@asyncio.coroutine
def async_media_next_track(self):
"""Send media_next command to media player."""
+ if self.is_grouped and not self.is_master:
+ return
+
cmd = 'Skip'
if self._status and 'actions' in self._status:
for action in self._status['actions']['action']:
@@ -718,6 +904,9 @@ class BluesoundPlayer(MediaPlayerDevice):
@asyncio.coroutine
def async_media_previous_track(self):
"""Send media_previous command to media player."""
+ if self.is_grouped and not self.is_master:
+ return
+
cmd = 'Back'
if self._status and 'actions' in self._status:
for action in self._status['actions']['action']:
@@ -730,23 +919,52 @@ class BluesoundPlayer(MediaPlayerDevice):
@asyncio.coroutine
def async_media_play(self):
"""Send media_play command to media player."""
+ if self.is_grouped and not self.is_master:
+ return
+
return self.send_bluesound_command('Play')
@asyncio.coroutine
def async_media_pause(self):
"""Send media_pause command to media player."""
+ if self.is_grouped and not self.is_master:
+ return
+
return self.send_bluesound_command('Pause')
@asyncio.coroutine
def async_media_stop(self):
"""Send stop command."""
+ if self.is_grouped and not self.is_master:
+ return
+
return self.send_bluesound_command('Pause')
@asyncio.coroutine
def async_media_seek(self, position):
"""Send media_seek command to media player."""
+ if self.is_grouped and not self.is_master:
+ return
+
return self.send_bluesound_command('Play?seek=' + str(float(position)))
+ @asyncio.coroutine
+ def async_play_media(self, media_type, media_id, **kwargs):
+ """
+ Send the play_media command to the media player.
+
+ If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
+ """
+ if self.is_grouped and not self.is_master:
+ return
+
+ url = 'Play?url={}'.format(media_id)
+
+ if kwargs.get(ATTR_MEDIA_ENQUEUE):
+ return self.send_bluesound_command(url)
+
+ return self.send_bluesound_command(url)
+
@asyncio.coroutine
def async_volume_up(self):
"""Volume up the media player."""
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index 928062cb2dc..40e09ea328c 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -5,10 +5,16 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.cast/
"""
# pylint: disable=import-error
+import asyncio
import logging
+import threading
import voluptuous as vol
+from homeassistant.helpers.typing import HomeAssistantType, ConfigType
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import (dispatcher_send,
+ async_dispatcher_connect)
from homeassistant.components.media_player import (
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK,
@@ -16,11 +22,11 @@ from homeassistant.components.media_player import (
SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
- STATE_UNKNOWN)
+ STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['pychromecast==1.0.3']
+REQUIREMENTS = ['pychromecast==2.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -33,7 +39,13 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY
-KNOWN_HOSTS_KEY = 'cast_known_hosts'
+INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running'
+# UUID -> CastDevice mapping; cast devices without UUID are not stored
+ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices'
+# Stores every discovered (host, port, uuid)
+KNOWN_CHROMECASTS_KEY = 'cast_all_chromecasts'
+
+SIGNAL_CAST_DISCOVERED = 'cast_discovered'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
@@ -41,67 +53,145 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-# pylint: disable=unused-argument
-def setup_platform(hass, config, add_devices, discovery_info=None):
+def _setup_internal_discovery(hass: HomeAssistantType) -> None:
+ """Set up the pychromecast internal discovery."""
+ hass.data.setdefault(INTERNAL_DISCOVERY_RUNNING_KEY, threading.Lock())
+ if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False):
+ # Internal discovery is already running
+ return
+
+ import pychromecast
+
+ def internal_callback(name):
+ """Called when zeroconf has discovered a new chromecast."""
+ mdns = listener.services[name]
+ ip_address, port, uuid, _, _ = mdns
+ key = (ip_address, port, uuid)
+
+ if key in hass.data[KNOWN_CHROMECASTS_KEY]:
+ _LOGGER.debug("Discovered previous chromecast %s", mdns)
+ return
+
+ _LOGGER.debug("Discovered new chromecast %s", mdns)
+ try:
+ # pylint: disable=protected-access
+ chromecast = pychromecast._get_chromecast_from_host(
+ mdns, blocking=True)
+ except pychromecast.ChromecastConnectionError:
+ _LOGGER.debug("Can't set up cast with mDNS info %s. "
+ "Assuming it's not a Chromecast", mdns)
+ return
+ hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast
+ dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, chromecast)
+
+ _LOGGER.debug("Starting internal pychromecast discovery.")
+ listener, browser = pychromecast.start_discovery(internal_callback)
+
+ def stop_discovery(event):
+ """Stop discovery of new chromecasts."""
+ pychromecast.stop_discovery(browser)
+ hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release()
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery)
+
+
+@callback
+def _async_create_cast_device(hass, chromecast):
+ """Create a CastDevice Entity from the chromecast object.
+
+ Returns None if the cast device has already been added. Additionally,
+ automatically updates existing chromecast entities.
+ """
+ if chromecast.uuid is None:
+ # Found a cast without UUID, we don't store it because we won't be able
+ # to update it anyway.
+ return CastDevice(chromecast)
+
+ # Found a cast with UUID
+ added_casts = hass.data[ADDED_CAST_DEVICES_KEY]
+ old_cast_device = added_casts.get(chromecast.uuid)
+ if old_cast_device is None:
+ # -> New cast device
+ cast_device = CastDevice(chromecast)
+ added_casts[chromecast.uuid] = cast_device
+ return cast_device
+
+ old_key = (old_cast_device.cast.host,
+ old_cast_device.cast.port,
+ old_cast_device.cast.uuid)
+ new_key = (chromecast.host, chromecast.port, chromecast.uuid)
+
+ if old_key == new_key:
+ # Re-discovered with same data, ignore
+ return None
+
+ # -> Cast device changed host
+ # Remove old pychromecast.Chromecast from global list, because it isn't
+ # valid anymore
+ old_cast_device.async_set_chromecast(chromecast)
+ return None
+
+
+@asyncio.coroutine
+def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
+ async_add_devices, discovery_info=None):
"""Set up the cast platform."""
import pychromecast
# Import CEC IGNORE attributes
pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, [])
+ hass.data.setdefault(ADDED_CAST_DEVICES_KEY, {})
+ hass.data.setdefault(KNOWN_CHROMECASTS_KEY, {})
- known_hosts = hass.data.get(KNOWN_HOSTS_KEY)
- if known_hosts is None:
- known_hosts = hass.data[KNOWN_HOSTS_KEY] = []
-
+ # None -> use discovery; (host, port) -> manually specify chromecast.
+ want_host = None
if discovery_info:
- host = (discovery_info.get('host'), discovery_info.get('port'))
-
- if host in known_hosts:
- return
-
- hosts = [host]
-
+ want_host = (discovery_info.get('host'), discovery_info.get('port'))
elif CONF_HOST in config:
- host = (config.get(CONF_HOST), DEFAULT_PORT)
+ want_host = (config.get(CONF_HOST), DEFAULT_PORT)
- if host in known_hosts:
- return
+ enable_discovery = False
+ if want_host is None:
+ # We were explicitly told to enable pychromecast discovery.
+ enable_discovery = True
+ elif want_host[1] != DEFAULT_PORT:
+ # We're trying to add a group, so we have to use pychromecast's
+ # discovery to get the correct friendly name.
+ enable_discovery = True
- hosts = [host]
+ if enable_discovery:
+ @callback
+ def async_cast_discovered(chromecast):
+ """Callback for when a new chromecast is discovered."""
+ if want_host is not None and \
+ (chromecast.host, chromecast.port) != want_host:
+ return # for groups, only add requested device
+ cast_device = _async_create_cast_device(hass, chromecast)
+ if cast_device is not None:
+ async_add_devices([cast_device])
+
+ async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED,
+ async_cast_discovered)
+ # Re-play the callback for all past chromecasts, store the objects in
+ # a list to avoid concurrent modification resulting in exception.
+ for chromecast in list(hass.data[KNOWN_CHROMECASTS_KEY].values()):
+ async_cast_discovered(chromecast)
+
+ hass.async_add_job(_setup_internal_discovery, hass)
else:
- hosts = [tuple(dev[:2]) for dev in pychromecast.discover_chromecasts()
- if tuple(dev[:2]) not in known_hosts]
-
- casts = []
-
- # get_chromecasts() returns Chromecast objects with the correct friendly
- # name for grouped devices
- all_chromecasts = pychromecast.get_chromecasts()
-
- for host in hosts:
- (_, port) = host
- found = [device for device in all_chromecasts
- if (device.host, device.port) == host]
- if found:
- try:
- casts.append(CastDevice(found[0]))
- known_hosts.append(host)
- except pychromecast.ChromecastConnectionError:
- pass
-
- # do not add groups using pychromecast.Chromecast as it leads to names
- # collision since pychromecast.Chromecast will get device name instead
- # of group name
- elif port == DEFAULT_PORT:
- try:
- # add the device anyway, get_chromecasts couldn't find it
- casts.append(CastDevice(pychromecast.Chromecast(*host)))
- known_hosts.append(host)
- except pychromecast.ChromecastConnectionError:
- pass
-
- add_devices(casts)
+ # Manually add a "normal" Chromecast, we can do that without discovery.
+ try:
+ chromecast = yield from hass.async_add_job(
+ pychromecast.Chromecast, *want_host)
+ except pychromecast.ChromecastConnectionError:
+ _LOGGER.warning("Can't set up chromecast on %s", want_host[0])
+ raise
+ key = (chromecast.host, chromecast.port, chromecast.uuid)
+ cast_device = _async_create_cast_device(hass, chromecast)
+ if cast_device is not None:
+ hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast
+ async_add_devices([cast_device])
class CastDevice(MediaPlayerDevice):
@@ -109,16 +199,13 @@ class CastDevice(MediaPlayerDevice):
def __init__(self, chromecast):
"""Initialize the Cast device."""
- self.cast = chromecast
-
- self.cast.socket_client.receiver_controller.register_status_listener(
- self)
- self.cast.socket_client.media_controller.register_status_listener(self)
-
- self.cast_status = self.cast.status
- self.media_status = self.cast.media_controller.status
+ self.cast = None # type: pychromecast.Chromecast
+ self.cast_status = None
+ self.media_status = None
self.media_status_received = None
+ self.async_set_chromecast(chromecast)
+
@property
def should_poll(self):
"""No polling needed."""
@@ -325,3 +412,39 @@ class CastDevice(MediaPlayerDevice):
self.media_status = status
self.media_status_received = dt_util.utcnow()
self.schedule_update_ha_state()
+
+ @property
+ def unique_id(self) -> str:
+ """Return an unique ID."""
+ if self.cast.uuid is not None:
+ return str(self.cast.uuid)
+ return None
+
+ @callback
+ def async_set_chromecast(self, chromecast):
+ """Set the internal Chromecast object and disconnect the previous."""
+ self._async_disconnect()
+
+ self.cast = chromecast
+
+ self.cast.socket_client.receiver_controller.register_status_listener(
+ self)
+ self.cast.socket_client.media_controller.register_status_listener(self)
+
+ self.cast_status = self.cast.status
+ self.media_status = self.cast.media_controller.status
+
+ @asyncio.coroutine
+ def async_will_remove_from_hass(self):
+ """Disconnect Chromecast object when removed."""
+ self._async_disconnect()
+
+ @callback
+ def _async_disconnect(self):
+ """Disconnect Chromecast object if it is set."""
+ if self.cast is None:
+ return
+ _LOGGER.debug("Disconnecting existing chromecast object")
+ old_key = (self.cast.host, self.cast.port, self.cast.uuid)
+ self.hass.data[KNOWN_CHROMECASTS_KEY].pop(old_key)
+ self.cast.disconnect(blocking=False)
diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py
index 057a23579ca..6847b87e54f 100644
--- a/homeassistant/components/media_player/clementine.py
+++ b/homeassistant/components/media_player/clementine.py
@@ -37,7 +37,7 @@ SUPPORT_CLEMENTINE = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_ACCESS_TOKEN, default=None): cv.positive_int,
+ vol.Optional(CONF_ACCESS_TOKEN): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py
index 0a03af0e1bf..5bc16d11d64 100644
--- a/homeassistant/components/media_player/denonavr.py
+++ b/homeassistant/components/media_player/denonavr.py
@@ -20,7 +20,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['denonavr==0.5.5']
+REQUIREMENTS = ['denonavr==0.6.0']
_LOGGER = logging.getLogger(__name__)
@@ -43,12 +43,12 @@ SUPPORT_MEDIA_MODES = SUPPORT_PLAY_MEDIA | \
DENON_ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE): vol.In(CONF_VALID_ZONES, CONF_INVALID_ZONES_ERR),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST): cv.string,
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
cv.boolean,
vol.Optional(CONF_ZONES):
@@ -80,7 +80,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if zones is not None:
add_zones = {}
for entry in zones:
- add_zones[entry[CONF_ZONE]] = entry[CONF_NAME]
+ add_zones[entry[CONF_ZONE]] = entry.get(CONF_NAME)
else:
add_zones = None
diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py
index a3fe62c5a42..e363ab12f92 100644
--- a/homeassistant/components/media_player/emby.py
+++ b/homeassistant/components/media_player/emby.py
@@ -45,7 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Required(CONF_API_KEY): cv.string,
- vol.Optional(CONF_PORT, default=None): cv.port,
+ vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean,
})
diff --git a/homeassistant/components/media_player/frontier_silicon.py b/homeassistant/components/media_player/frontier_silicon.py
index f46d0657604..6d95ea675fb 100644
--- a/homeassistant/components/media_player/frontier_silicon.py
+++ b/homeassistant/components/media_player/frontier_silicon.py
@@ -4,6 +4,7 @@ Support for Frontier Silicon Devices (Medion, Hama, Auna,...).
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.frontier_silicon/
"""
+import asyncio
import logging
import voluptuous as vol
@@ -19,7 +20,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, CONF_PASSWORD)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['fsapi==0.0.7']
+REQUIREMENTS = ['afsapi==0.0.3']
_LOGGER = logging.getLogger(__name__)
@@ -41,14 +42,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# 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 Frontier Silicon platform."""
import requests
if discovery_info is not None:
- add_devices(
- [FSAPIDevice(discovery_info['ssdp_description'],
- DEFAULT_PASSWORD)],
+ async_add_devices(
+ [AFSAPIDevice(discovery_info['ssdp_description'],
+ DEFAULT_PASSWORD)],
update_before_add=True)
return True
@@ -57,8 +59,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
password = config.get(CONF_PASSWORD)
try:
- add_devices(
- [FSAPIDevice(DEVICE_URL.format(host, port), password)],
+ async_add_devices(
+ [AFSAPIDevice(DEVICE_URL.format(host, port), password)],
update_before_add=True)
_LOGGER.debug("FSAPI device %s:%s -> %s", host, port, password)
return True
@@ -69,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return False
-class FSAPIDevice(MediaPlayerDevice):
+class AFSAPIDevice(MediaPlayerDevice):
"""Representation of a Frontier Silicon device on the network."""
def __init__(self, device_url, password):
@@ -97,9 +99,9 @@ class FSAPIDevice(MediaPlayerDevice):
connected to the device in between the updates and invalidated the
existing session (i.e UNDOK).
"""
- from fsapi import FSAPI
+ from afsapi import AFSAPI
- return FSAPI(self._device_url, self._password)
+ return AFSAPI(self._device_url, self._password)
@property
def should_poll(self):
@@ -157,17 +159,18 @@ class FSAPIDevice(MediaPlayerDevice):
"""Image url of current playing media."""
return self._media_image_url
- def update(self):
+ @asyncio.coroutine
+ def async_update(self):
"""Get the latest date and update device state."""
fs_device = self.fs_device
if not self._name:
- self._name = fs_device.friendly_name
+ self._name = yield from fs_device.get_friendly_name()
if not self._source_list:
- self._source_list = fs_device.mode_list
+ self._source_list = yield from fs_device.get_mode_list()
- status = fs_device.play_status
+ status = yield from fs_device.get_play_status()
self._state = {
'playing': STATE_PLAYING,
'paused': STATE_PAUSED,
@@ -176,54 +179,70 @@ class FSAPIDevice(MediaPlayerDevice):
None: STATE_OFF,
}.get(status, STATE_UNKNOWN)
- info_name = fs_device.play_info_name
- info_text = fs_device.play_info_text
+ if self._state != STATE_OFF:
+ info_name = yield from fs_device.get_play_name()
+ info_text = yield from fs_device.get_play_text()
- self._title = ' - '.join(filter(None, [info_name, info_text]))
- self._artist = fs_device.play_info_artist
- self._album_name = fs_device.play_info_album
+ self._title = ' - '.join(filter(None, [info_name, info_text]))
+ self._artist = yield from fs_device.get_play_artist()
+ self._album_name = yield from fs_device.get_play_album()
- self._source = fs_device.mode
- self._mute = fs_device.mute
- self._media_image_url = fs_device.play_info_graphics
+ self._source = yield from fs_device.get_mode()
+ self._mute = yield from fs_device.get_mute()
+ self._media_image_url = yield from fs_device.get_play_graphic()
+ else:
+ self._title = None
+ self._artist = None
+ self._album_name = None
+
+ self._source = None
+ self._mute = None
+ self._media_image_url = None
# Management actions
-
# power control
- def turn_on(self):
+ @asyncio.coroutine
+ def async_turn_on(self):
"""Turn on the device."""
- self.fs_device.power = True
+ yield from self.fs_device.set_power(True)
- def turn_off(self):
+ @asyncio.coroutine
+ def async_turn_off(self):
"""Turn off the device."""
- self.fs_device.power = False
+ yield from self.fs_device.set_power(False)
- def media_play(self):
+ @asyncio.coroutine
+ def async_media_play(self):
"""Send play command."""
- self.fs_device.play()
+ yield from self.fs_device.play()
- def media_pause(self):
+ @asyncio.coroutine
+ def async_media_pause(self):
"""Send pause command."""
- self.fs_device.pause()
+ yield from self.fs_device.pause()
- def media_play_pause(self):
+ @asyncio.coroutine
+ def async_media_play_pause(self):
"""Send play/pause command."""
if 'playing' in self._state:
- self.fs_device.pause()
+ yield from self.fs_device.pause()
else:
- self.fs_device.play()
+ yield from self.fs_device.play()
- def media_stop(self):
+ @asyncio.coroutine
+ def async_media_stop(self):
"""Send play/pause command."""
- self.fs_device.pause()
+ yield from self.fs_device.pause()
- def media_previous_track(self):
+ @asyncio.coroutine
+ def async_media_previous_track(self):
"""Send previous track command (results in rewind)."""
- self.fs_device.prev()
+ yield from self.fs_device.rewind()
- def media_next_track(self):
+ @asyncio.coroutine
+ def async_media_next_track(self):
"""Send next track command (results in fast-forward)."""
- self.fs_device.next()
+ yield from self.fs_device.forward()
# mute
@property
@@ -231,23 +250,30 @@ class FSAPIDevice(MediaPlayerDevice):
"""Boolean if volume is currently muted."""
return self._mute
- def mute_volume(self, mute):
+ @asyncio.coroutine
+ def async_mute_volume(self, mute):
"""Send mute command."""
- self.fs_device.mute = mute
+ yield from self.fs_device.set_mute(mute)
# volume
- def volume_up(self):
+ @asyncio.coroutine
+ def async_volume_up(self):
"""Send volume up command."""
- self.fs_device.volume += 1
+ volume = yield from self.fs_device.get_volume()
+ yield from self.fs_device.set_volume(volume+1)
- def volume_down(self):
+ @asyncio.coroutine
+ def async_volume_down(self):
"""Send volume down command."""
- self.fs_device.volume -= 1
+ volume = yield from self.fs_device.get_volume()
+ yield from self.fs_device.set_volume(volume-1)
- def set_volume_level(self, volume):
+ @asyncio.coroutine
+ def async_set_volume_level(self, volume):
"""Set volume command."""
- self.fs_device.volume = volume
+ yield from self.fs_device.set_volume(volume)
- def select_source(self, source):
+ @asyncio.coroutine
+ def async_select_source(self, source):
"""Select input source."""
- self.fs_device.mode = source
+ yield from self.fs_device.set_mode(source)
diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py
index e1fffefed18..f5b4cbd4854 100644
--- a/homeassistant/components/media_player/hdmi_cec.py
+++ b/homeassistant/components/media_player/hdmi_cec.py
@@ -87,7 +87,7 @@ class CecPlayerDevice(CecDevice, MediaPlayerDevice):
self.send_keypress(KEY_STOP)
self._state = STATE_IDLE
- def play_media(self, media_type, media_id):
+ def play_media(self, media_type, media_id, **kwargs):
"""Not supported."""
raise NotImplementedError()
diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py
index 2c428c6b833..d14bf0fadaf 100644
--- a/homeassistant/components/media_player/kodi.py
+++ b/homeassistant/components/media_player/kodi.py
@@ -86,7 +86,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port,
vol.Optional(CONF_PROXY_SSL, default=DEFAULT_PROXY_SSL): cv.boolean,
- vol.Optional(CONF_TURN_ON_ACTION, default=None): cv.SCRIPT_SCHEMA,
+ vol.Optional(CONF_TURN_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_OFF_ACTION):
vol.Any(cv.SCRIPT_SCHEMA, vol.In(DEPRECATED_TURN_OFF_ACTIONS)),
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
diff --git a/homeassistant/components/media_player/lg_netcast.py b/homeassistant/components/media_player/lg_netcast.py
index e657e1ce80d..edbd6546cca 100644
--- a/homeassistant/components/media_player/lg_netcast.py
+++ b/homeassistant/components/media_player/lg_netcast.py
@@ -38,7 +38,7 @@ SUPPORT_LGTV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_ACCESS_TOKEN, default=None):
+ vol.Optional(CONF_ACCESS_TOKEN):
vol.All(cv.string, vol.Length(max=6)),
})
diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py
index 4307b68e709..81a18ab93c5 100644
--- a/homeassistant/components/media_player/mpd.py
+++ b/homeassistant/components/media_player/mpd.py
@@ -182,8 +182,7 @@ class MpdDevice(MediaPlayerDevice):
if name is None and title is None:
if file_name is None:
return "None"
- else:
- return os.path.basename(file_name)
+ return os.path.basename(file_name)
elif name is None:
return title
elif title is None:
diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py
index 21a897f4d35..39e5f81b71d 100644
--- a/homeassistant/components/media_player/panasonic_viera.py
+++ b/homeassistant/components/media_player/panasonic_viera.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN, CONF_PORT)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['panasonic_viera==0.3',
+REQUIREMENTS = ['panasonic_viera==0.3.1',
'wakeonlan==1.0.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml
index 3e5ee57cb2f..7ac250b1d30 100644
--- a/homeassistant/components/media_player/services.yaml
+++ b/homeassistant/components/media_player/services.yaml
@@ -323,7 +323,6 @@ squeezebox_call_method:
yamaha_enable_output:
description: Enable or disable an output port
-
fields:
entity_id:
description: Name(s) of entites to enable/disable port on.
@@ -334,3 +333,34 @@ yamaha_enable_output:
enabled:
description: Boolean indicating if port should be enabled or not.
example: true
+
+bluesound_join:
+ description: Group player together.
+ fields:
+ master:
+ description: Entity ID of the player that should become the master of the group.
+ example: 'media_player.bluesound_livingroom'
+ entity_id:
+ description: Name(s) of entities that will coordinate the grouping. Platform dependent.
+ example: 'media_player.bluesound_livingroom'
+
+bluesound_unjoin:
+ description: Unjoin the player from a group.
+ fields:
+ entity_id:
+ description: Name(s) of entities that will be unjoined from their group. Platform dependent.
+ example: 'media_player.bluesound_livingroom'
+
+bluesound_set_sleep_timer:
+ description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0"
+ fields:
+ entity_id:
+ description: Name(s) of entities that will have a timer set.
+ example: 'media_player.bluesound_livingroom'
+
+bluesound_clear_sleep_timer:
+ description: Clear a Bluesound timer.
+ fields:
+ entity_id:
+ description: Name(s) of entities that will have the timer cleared.
+ example: 'media_player.bluesound_livingroom'
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index d4a7fd3adb5..d9236ae9a54 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -10,6 +10,7 @@ import functools as ft
import logging
import socket
import urllib
+import threading
import voluptuous as vol
@@ -25,23 +26,17 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
-REQUIREMENTS = ['SoCo==0.13']
+REQUIREMENTS = ['SoCo==0.14']
_LOGGER = logging.getLogger(__name__)
-# The soco library is excessively chatty when it comes to logging and
-# causes a LOT of spam in the logs due to making a http connection to each
-# speaker every 10 seconds. Quiet it down a bit to just actual problems.
-_SOCO_LOGGER = logging.getLogger('soco')
-_SOCO_LOGGER.setLevel(logging.ERROR)
+# Quiet down soco logging to just actual problems.
+logging.getLogger('soco').setLevel(logging.WARNING)
_SOCO_SERVICES_LOGGER = logging.getLogger('soco.services')
-_REQUESTS_LOGGER = logging.getLogger('requests')
-_REQUESTS_LOGGER.setLevel(logging.ERROR)
-SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
- SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
+SUPPORT_SONOS = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
- SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET
+ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_STOP
SERVICE_JOIN = 'sonos_join'
SERVICE_UNJOIN = 'sonos_unjoin'
@@ -54,8 +49,8 @@ SERVICE_SET_OPTION = 'sonos_set_option'
DATA_SONOS = 'sonos'
-SUPPORT_SOURCE_LINEIN = 'Line-in'
-SUPPORT_SOURCE_TV = 'TV'
+SOURCE_LINEIN = 'Line-in'
+SOURCE_TV = 'TV'
CONF_ADVERTISE_ADDR = 'advertise_addr'
CONF_INTERFACE_ADDR = 'interface_addr'
@@ -112,12 +107,21 @@ SONOS_SET_OPTION_SCHEMA = SONOS_SCHEMA.extend({
})
+class SonosData:
+ """Storage class for platform global data."""
+
+ def __init__(self):
+ """Initialize the data."""
+ self.devices = []
+ self.topology_lock = threading.Lock()
+
+
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sonos platform."""
import soco
if DATA_SONOS not in hass.data:
- hass.data[DATA_SONOS] = []
+ hass.data[DATA_SONOS] = SonosData()
advertise_addr = config.get(CONF_ADVERTISE_ADDR, None)
if advertise_addr:
@@ -127,14 +131,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
player = soco.SoCo(discovery_info.get('host'))
# If device already exists by config
- if player.uid in [x.unique_id for x in hass.data[DATA_SONOS]]:
+ if player.uid in [x.unique_id for x in hass.data[DATA_SONOS].devices]:
return
if player.is_visible:
device = SonosDevice(player)
- add_devices([device], True)
- hass.data[DATA_SONOS].append(device)
- if len(hass.data[DATA_SONOS]) > 1:
+ hass.data[DATA_SONOS].devices.append(device)
+ add_devices([device])
+ if len(hass.data[DATA_SONOS].devices) > 1:
return
else:
players = None
@@ -159,14 +163,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.warning("No Sonos speakers found")
return
- # Add coordinators first so they can be queried by slaves
- coordinators = [SonosDevice(p) for p in players if p.is_coordinator]
- slaves = [SonosDevice(p) for p in players if not p.is_coordinator]
- hass.data[DATA_SONOS] = coordinators + slaves
- if coordinators:
- add_devices(coordinators, True)
- if slaves:
- add_devices(slaves, True)
+ hass.data[DATA_SONOS].devices = [SonosDevice(p) for p in players]
+ add_devices(hass.data[DATA_SONOS].devices)
_LOGGER.debug("Added %s Sonos speakers", len(players))
def service_handle(service):
@@ -174,16 +172,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
entity_ids = service.data.get('entity_id')
if entity_ids:
- devices = [device for device in hass.data[DATA_SONOS]
+ devices = [device for device in hass.data[DATA_SONOS].devices
if device.entity_id in entity_ids]
else:
- devices = hass.data[DATA_SONOS]
+ devices = hass.data[DATA_SONOS].devices
+
+ if service.service == SERVICE_JOIN:
+ master = [device for device in hass.data[DATA_SONOS].devices
+ if device.entity_id == service.data[ATTR_MASTER]]
+ if master:
+ master[0].join(devices)
+ return
for device in devices:
- if service.service == SERVICE_JOIN:
- if device.entity_id != service.data[ATTR_MASTER]:
- device.join(service.data[ATTR_MASTER])
- elif service.service == SERVICE_UNJOIN:
+ if service.service == SERVICE_UNJOIN:
device.unjoin()
elif service.service == SERVICE_SNAPSHOT:
device.snapshot(service.data[ATTR_WITH_GROUP])
@@ -233,35 +235,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
schema=SONOS_SET_OPTION_SCHEMA)
-def _parse_timespan(timespan):
- """Parse a time-span into number of seconds."""
- if timespan in ('', 'NOT_IMPLEMENTED', None):
- return None
-
- return sum(60 ** x[0] * int(x[1]) for x in enumerate(
- reversed(timespan.split(':'))))
-
-
-class _ProcessSonosEventQueue(object):
+class _ProcessSonosEventQueue:
"""Queue like object for dispatching sonos events."""
- def __init__(self, sonos_device):
+ def __init__(self, handler):
"""Initialize Sonos event queue."""
- self._sonos_device = sonos_device
+ self._handler = handler
def put(self, item, block=True, timeout=None):
- """Queue up event for processing."""
- # Instead of putting events on a queue, dispatch them to the event
- # processing method.
- self._sonos_device.process_sonos_event(item)
+ """Process event."""
+ self._handler(item)
-def _get_entity_from_soco(hass, soco):
- """Return SonosDevice from SoCo."""
- for device in hass.data[DATA_SONOS]:
- if soco == device.soco:
- return device
- raise ValueError("No entity for SoCo device")
+def _get_entity_from_soco_uid(hass, uid):
+ """Return SonosDevice from SoCo uid."""
+ for entity in hass.data[DATA_SONOS].devices:
+ if uid == entity.soco.uid:
+ return entity
+ return None
def soco_error(errorcodes=None):
@@ -280,7 +271,7 @@ def soco_error(errorcodes=None):
try:
return funct(*args, **kwargs)
except SoCoUPnPException as err:
- if err.error_code in errorcodes:
+ if errorcodes and err.error_code in errorcodes:
pass
else:
_LOGGER.error("Error on %s with %s", funct.__name__, err)
@@ -305,21 +296,37 @@ def soco_coordinator(funct):
return wrapper
+def _timespan_secs(timespan):
+ """Parse a time-span into number of seconds."""
+ if timespan in ('', 'NOT_IMPLEMENTED', None):
+ return None
+
+ return sum(60 ** x[0] * int(x[1]) for x in enumerate(
+ reversed(timespan.split(':'))))
+
+
+def _is_radio_uri(uri):
+ """Return whether the URI is a radio stream."""
+ return uri.startswith('x-rincon-mp3radio:') or \
+ uri.startswith('x-sonosapi-stream:')
+
+
class SonosDevice(MediaPlayerDevice):
"""Representation of a Sonos device."""
def __init__(self, player):
"""Initialize the Sonos device."""
- self.volume_increment = 5
+ self._volume_increment = 5
self._unique_id = player.uid
self._player = player
+ self._model = None
self._player_volume = None
self._player_volume_muted = None
- self._speaker_info = None
+ self._play_mode = None
self._name = None
- self._status = None
self._coordinator = None
- self._media_content_id = None
+ self._status = None
+ self._extra_features = 0
self._media_duration = None
self._media_position = None
self._media_position_updated_at = None
@@ -327,37 +334,21 @@ class SonosDevice(MediaPlayerDevice):
self._media_artist = None
self._media_album_name = None
self._media_title = None
- self._media_radio_show = None
- self._available = True
- self._support_previous_track = False
- self._support_next_track = False
- self._support_play = False
- self._support_shuffle_set = True
- self._support_stop = False
- self._support_pause = False
self._night_sound = None
self._speech_enhance = None
- self._current_track_uri = None
- self._current_track_is_radio_stream = False
- self._queue = None
- self._last_avtransport_event = None
- self._is_playing_line_in = None
- self._is_playing_tv = None
- self._favorite_sources = None
self._source_name = None
+ self._available = True
+ self._favorites = None
self._soco_snapshot = None
self._snapshot_group = None
+ self._set_basic_information()
+
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe sonos events."""
self.hass.async_add_job(self._subscribe_to_player_events)
- @property
- def should_poll(self):
- """Return the polling state."""
- return True
-
@property
def unique_id(self):
"""Return an unique ID."""
@@ -369,10 +360,9 @@ class SonosDevice(MediaPlayerDevice):
return self._name
@property
+ @soco_coordinator
def state(self):
"""Return the state of the device."""
- if self._coordinator:
- return self._coordinator.state
if self._status in ('PAUSED_PLAYBACK', 'STOPPED'):
return STATE_PAUSED
if self._status in ('PLAYING', 'TRANSITIONING'):
@@ -401,260 +391,285 @@ class SonosDevice(MediaPlayerDevice):
"""Return True if entity is available."""
return self._available
- def _is_available(self):
+ def _check_available(self):
+ """Check that we can still connect to the player."""
try:
sock = socket.create_connection(
- address=(self._player.ip_address, 1443), timeout=3)
+ address=(self.soco.ip_address, 1443), timeout=3)
sock.close()
return True
except socket.error:
return False
- # pylint: disable=invalid-name
+ def _set_basic_information(self):
+ """Set initial device information."""
+ speaker_info = self.soco.get_speaker_info(True)
+ self._name = speaker_info['zone_name']
+ self._model = speaker_info['model_name']
+ self._player_volume = self.soco.volume
+ self._player_volume_muted = self.soco.mute
+ self._play_mode = self.soco.play_mode
+ self._night_sound = self.soco.night_mode
+ self._speech_enhance = self.soco.dialog_mode
+ self._favorites = self.soco.music_library.get_sonos_favorites()
+
def _subscribe_to_player_events(self):
- if self._queue is None:
- self._queue = _ProcessSonosEventQueue(self)
- self._player.avTransport.subscribe(
- auto_renew=True,
- event_queue=self._queue)
- self._player.renderingControl.subscribe(
- auto_renew=True,
- event_queue=self._queue)
+ """Add event subscriptions."""
+ player = self.soco
+
+ queue = _ProcessSonosEventQueue(self.process_avtransport_event)
+ player.avTransport.subscribe(auto_renew=True, event_queue=queue)
+
+ queue = _ProcessSonosEventQueue(self.process_rendering_event)
+ player.renderingControl.subscribe(auto_renew=True, event_queue=queue)
+
+ queue = _ProcessSonosEventQueue(self.process_zonegrouptopology_event)
+ player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue)
def update(self):
"""Retrieve latest state."""
- if self._speaker_info is None:
- self._speaker_info = self._player.get_speaker_info(True)
- self._name = self._speaker_info['zone_name'].replace(
- ' (R)', '').replace(' (L)', '')
- self._favorite_sources = \
- self._player.get_sonos_favorites()['favorites']
-
- if self._last_avtransport_event:
- self._available = True
- else:
- self._available = self._is_available()
-
- if not self._available:
- self._player_volume = None
- self._player_volume_muted = None
- self._status = 'OFF'
- self._coordinator = None
- self._media_content_id = None
- self._media_duration = None
- self._media_position = None
- self._media_position_updated_at = None
- self._media_image_url = None
- self._media_artist = None
- self._media_album_name = None
- self._media_title = None
- self._media_radio_show = None
- self._current_track_uri = None
- self._current_track_is_radio_stream = False
- self._support_previous_track = False
- self._support_next_track = False
- self._support_play = False
- self._support_shuffle_set = False
- self._support_stop = False
- self._support_pause = False
- self._night_sound = None
- self._speech_enhance = None
- self._is_playing_tv = False
- self._is_playing_line_in = False
- self._source_name = None
- self._last_avtransport_event = None
- return
-
- # set group coordinator
- if self._player.is_coordinator:
- self._coordinator = None
- else:
- try:
- self._coordinator = _get_entity_from_soco(
- self.hass, self._player.group.coordinator)
-
- # protect for loop
- if not self._coordinator.is_coordinator:
- # pylint: disable=protected-access
- self._coordinator._coordinator = None
- except ValueError:
+ available = self._check_available()
+ if self._available != available:
+ self._available = available
+ if available:
+ self._set_basic_information()
+ self._subscribe_to_player_events()
+ else:
+ self._player_volume = None
+ self._player_volume_muted = None
+ self._status = 'OFF'
self._coordinator = None
+ self._media_duration = None
+ self._media_position = None
+ self._media_position_updated_at = None
+ self._media_image_url = None
+ self._media_artist = None
+ self._media_album_name = None
+ self._media_title = None
+ self._extra_features = 0
+ self._source_name = None
- track_info = None
- if self._last_avtransport_event:
- variables = self._last_avtransport_event.variables
- current_track_metadata = variables.get(
- 'current_track_meta_data', {}
- )
+ def process_avtransport_event(self, event):
+ """Process a track change event coming from a coordinator."""
+ variables = event.variables
- self._status = variables.get('transport_state')
-
- if current_track_metadata:
- # no need to ask speaker for information we already have
- current_track_metadata = current_track_metadata.__dict__
-
- track_info = {
- 'uri': variables.get('current_track_uri'),
- 'artist': current_track_metadata.get('creator'),
- 'album': current_track_metadata.get('album'),
- 'title': current_track_metadata.get('title'),
- 'playlist_position': variables.get('current_track'),
- 'duration': variables.get('current_track_duration')
- }
- else:
- self._player_volume = self._player.volume
- self._player_volume_muted = self._player.mute
- transport_info = self._player.get_current_transport_info()
- self._status = transport_info.get('current_transport_state')
-
- if not track_info:
- track_info = self._player.get_current_track_info()
-
- if self._coordinator:
- self._last_avtransport_event = None
+ # Ignore transitions, we should get the target state soon
+ new_status = variables.get('transport_state')
+ if new_status == 'TRANSITIONING':
return
- is_playing_tv = self._player.is_playing_tv
- is_playing_line_in = self._player.is_playing_line_in
-
- media_info = self._player.avTransport.GetMediaInfo(
- [('InstanceID', 0)]
- )
-
- current_media_uri = media_info['CurrentURI']
- media_artist = track_info.get('artist')
- media_album_name = track_info.get('album')
- media_title = track_info.get('title')
- media_image_url = track_info.get('album_art', None)
-
- media_position = None
- media_position_updated_at = None
- source_name = None
-
- night_sound = self._player.night_mode
- speech_enhance = self._player.dialog_mode
-
- is_radio_stream = \
- current_media_uri.startswith('x-sonosapi-stream:') or \
- current_media_uri.startswith('x-rincon-mp3radio:')
-
- if is_playing_tv or is_playing_line_in:
- # playing from line-in/tv.
-
- support_previous_track = False
- support_next_track = False
- support_play = False
- support_stop = True
- support_pause = False
- support_shuffle_set = False
-
- if is_playing_tv:
- media_artist = SUPPORT_SOURCE_TV
- else:
- media_artist = SUPPORT_SOURCE_LINEIN
-
- source_name = media_artist
-
- media_album_name = None
- media_title = None
- media_image_url = None
-
- elif is_radio_stream:
- media_image_url = self._format_media_image_url(
- media_image_url,
- current_media_uri
- )
- support_previous_track = False
- support_next_track = False
- support_play = True
- support_stop = True
- support_pause = False
- support_shuffle_set = False
-
- source_name = 'Radio'
- # Check if currently playing radio station is in favorites
- favc = [fav for fav in self._favorite_sources
- if fav['uri'] == current_media_uri]
- if len(favc) == 1:
- src = favc.pop()
- source_name = src['title']
-
- # for radio streams we set the radio station name as the
- # title.
- if media_artist and media_title:
- # artist and album name are in the data, concatenate
- # that do display as artist.
- # "Information" field in the sonos pc app
-
- media_artist = '{artist} - {title}'.format(
- artist=media_artist,
- title=media_title
- )
- else:
- # "On Now" field in the sonos pc app
- media_artist = self._media_radio_show
-
- current_uri_metadata = media_info["CurrentURIMetaData"]
- if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None):
-
- # currently soco does not have an API for this
- import soco
- current_uri_metadata = soco.xml.XML.fromstring(
- soco.utils.really_utf8(current_uri_metadata))
-
- md_title = current_uri_metadata.findtext(
- './/{http://purl.org/dc/elements/1.1/}title')
-
- if md_title not in ('', 'NOT_IMPLEMENTED', None):
- media_title = md_title
-
- if media_artist and media_title:
- # some radio stations put their name into the artist
- # name, e.g.:
- # media_title = "Station"
- # media_artist = "Station - Artist - Title"
- # detect this case and trim from the front of
- # media_artist for cosmetics
- str_to_trim = '{title} - '.format(
- title=media_title
- )
- chars = min(len(media_artist), len(str_to_trim))
-
- if media_artist[:chars].upper() == str_to_trim[:chars].upper():
- media_artist = media_artist[chars:]
+ self._play_mode = variables.get('current_play_mode', self._play_mode)
+ if self.soco.is_playing_tv:
+ self._refresh_linein(SOURCE_TV)
+ elif self.soco.is_playing_line_in:
+ self._refresh_linein(SOURCE_LINEIN)
else:
- # not a radio stream
- media_image_url = self._format_media_image_url(
- media_image_url,
- track_info['uri']
- )
- support_previous_track = True
- support_next_track = True
- support_play = True
- support_stop = True
- support_pause = True
- support_shuffle_set = True
+ track_info = self.soco.get_current_track_info()
- position_info = self._player.avTransport.GetPositionInfo(
- [('InstanceID', 0),
- ('Channel', 'Master')]
- )
- rel_time = _parse_timespan(
- position_info.get("RelTime")
+ media_info = self.soco.avTransport.GetMediaInfo(
+ [('InstanceID', 0)]
)
- # player no longer reports position?
- update_media_position = rel_time is None and \
- self._media_position is not None
+ if _is_radio_uri(track_info['uri']):
+ self._refresh_radio(variables, media_info, track_info)
+ else:
+ self._refresh_music(variables, media_info, track_info)
- # player started reporting position?
- update_media_position |= rel_time is not None and \
- self._media_position is None
+ if new_status:
+ self._status = new_status
- # position changed?
+ self.schedule_update_ha_state()
+
+ # Also update slaves
+ for entity in self.hass.data[DATA_SONOS].devices:
+ coordinator = entity.coordinator
+ if coordinator and coordinator.unique_id == self.unique_id:
+ entity.schedule_update_ha_state()
+
+ def process_rendering_event(self, event):
+ """Process a volume change event coming from a player."""
+ variables = event.variables
+
+ if 'volume' in variables:
+ self._player_volume = int(variables['volume']['Master'])
+
+ if 'mute' in variables:
+ self._player_volume_muted = (variables['mute']['Master'] == '1')
+
+ if 'night_mode' in variables:
+ self._night_sound = (variables['night_mode'] == '1')
+
+ if 'dialog_level' in variables:
+ self._speech_enhance = (variables['dialog_level'] == '1')
+
+ self.schedule_update_ha_state()
+
+ def process_zonegrouptopology_event(self, event):
+ """Process a zone group topology event coming from a player."""
+ if not hasattr(event, 'zone_player_uui_ds_in_group'):
+ return
+
+ with self.hass.data[DATA_SONOS].topology_lock:
+ group = event.zone_player_uui_ds_in_group
+ if group:
+ # New group information is pushed
+ coordinator_uid, *slave_uids = group.split(',')
+ else:
+ # Use SoCo cache for existing topology
+ coordinator_uid = self.soco.group.coordinator.uid
+ slave_uids = [p.uid for p in self.soco.group.members
+ if p.uid != coordinator_uid]
+
+ if self.unique_id == coordinator_uid:
+ self._coordinator = None
+ self.schedule_update_ha_state()
+
+ for slave_uid in slave_uids:
+ slave = _get_entity_from_soco_uid(self.hass, slave_uid)
+ if slave:
+ # pylint: disable=protected-access
+ slave._coordinator = self
+ slave.schedule_update_ha_state()
+
+ def _radio_artwork(self, url):
+ """Return the private URL with artwork for a radio stream."""
+ if url not in ('', 'NOT_IMPLEMENTED', None):
+ if url.find('tts_proxy') > 0:
+ # If the content is a tts don't try to fetch an image from it.
+ return None
+ url = 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
+ host=self.soco.ip_address,
+ port=1400,
+ uri=urllib.parse.quote(url, safe='')
+ )
+ return url
+
+ def _refresh_linein(self, source):
+ """Update state when playing from line-in/tv."""
+ self._extra_features = 0
+
+ self._media_duration = None
+ self._media_position = None
+ self._media_position_updated_at = None
+
+ self._media_image_url = None
+
+ self._media_artist = source
+ self._media_album_name = None
+ self._media_title = None
+
+ self._source_name = source
+
+ def _refresh_radio(self, variables, media_info, track_info):
+ """Update state when streaming radio."""
+ self._extra_features = 0
+
+ self._media_duration = None
+ self._media_position = None
+ self._media_position_updated_at = None
+
+ self._media_image_url = self._radio_artwork(media_info['CurrentURI'])
+
+ self._media_artist = track_info.get('artist')
+ self._media_album_name = None
+ self._media_title = track_info.get('title')
+
+ if self._media_artist and self._media_title:
+ # artist and album name are in the data, concatenate
+ # that do display as artist.
+ # "Information" field in the sonos pc app
+ self._media_artist = '{artist} - {title}'.format(
+ artist=self._media_artist,
+ title=self._media_title
+ )
+ else:
+ # "On Now" field in the sonos pc app
+ current_track_metadata = variables.get(
+ 'current_track_meta_data'
+ )
+ if current_track_metadata:
+ self._media_artist = \
+ current_track_metadata.radio_show.split(',')[0]
+
+ # For radio streams we set the radio station name as the title.
+ current_uri_metadata = media_info["CurrentURIMetaData"]
+ if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None):
+ # currently soco does not have an API for this
+ import soco
+ current_uri_metadata = soco.xml.XML.fromstring(
+ soco.utils.really_utf8(current_uri_metadata))
+
+ md_title = current_uri_metadata.findtext(
+ './/{http://purl.org/dc/elements/1.1/}title')
+
+ if md_title not in ('', 'NOT_IMPLEMENTED', None):
+ self._media_title = md_title
+
+ if self._media_artist and self._media_title:
+ # some radio stations put their name into the artist
+ # name, e.g.:
+ # media_title = "Station"
+ # media_artist = "Station - Artist - Title"
+ # detect this case and trim from the front of
+ # media_artist for cosmetics
+ trim = '{title} - '.format(title=self._media_title)
+ chars = min(len(self._media_artist), len(trim))
+
+ if self._media_artist[:chars].upper() == trim[:chars].upper():
+ self._media_artist = self._media_artist[chars:]
+
+ # Check if currently playing radio station is in favorites
+ self._source_name = None
+ for fav in self._favorites:
+ if fav.reference.get_uri() == media_info['CurrentURI']:
+ self._source_name = fav.title
+
+ def _refresh_music(self, variables, media_info, track_info):
+ """Update state when playing music tracks."""
+ self._extra_features = SUPPORT_PAUSE | SUPPORT_SHUFFLE_SET |\
+ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
+
+ playlist_position = track_info.get('playlist_position')
+ if playlist_position in ('', 'NOT_IMPLEMENTED', None):
+ playlist_position = None
+ else:
+ playlist_position = int(playlist_position)
+
+ playlist_size = media_info.get('NrTracks')
+ if playlist_size in ('', 'NOT_IMPLEMENTED', None):
+ playlist_size = None
+ else:
+ playlist_size = int(playlist_size)
+
+ if playlist_position is not None and playlist_size is not None:
+ if playlist_position <= 1:
+ self._extra_features &= ~SUPPORT_PREVIOUS_TRACK
+
+ if playlist_position == playlist_size:
+ self._extra_features &= ~SUPPORT_NEXT_TRACK
+
+ self._media_duration = _timespan_secs(track_info.get('duration'))
+
+ position_info = self.soco.avTransport.GetPositionInfo(
+ [('InstanceID', 0),
+ ('Channel', 'Master')]
+ )
+ rel_time = _timespan_secs(position_info.get("RelTime"))
+
+ # player no longer reports position?
+ update_media_position = rel_time is None and \
+ self._media_position is not None
+
+ # player started reporting position?
+ update_media_position |= rel_time is not None and \
+ self._media_position is None
+
+ if self._status != variables.get('transport_state'):
+ update_media_position = True
+ else:
+ # position jumped?
if rel_time is not None and self._media_position is not None:
-
time_diff = utcnow() - self._media_position_updated_at
time_diff = time_diff.total_seconds()
@@ -663,115 +678,22 @@ class SonosDevice(MediaPlayerDevice):
update_media_position = \
abs(calculated_position - rel_time) > 1.5
- if update_media_position and self.state == STATE_PLAYING:
- media_position = rel_time
- media_position_updated_at = utcnow()
- else:
- # don't update media_position (don't want unneeded
- # state transitions)
- media_position = self._media_position
- media_position_updated_at = self._media_position_updated_at
+ if update_media_position:
+ self._media_position = rel_time
+ self._media_position_updated_at = utcnow()
- playlist_position = track_info.get('playlist_position')
- if playlist_position in ('', 'NOT_IMPLEMENTED', None):
- playlist_position = None
- else:
- playlist_position = int(playlist_position)
+ self._media_image_url = track_info.get('album_art')
- playlist_size = media_info.get('NrTracks')
- if playlist_size in ('', 'NOT_IMPLEMENTED', None):
- playlist_size = None
- else:
- playlist_size = int(playlist_size)
+ self._media_artist = track_info.get('artist')
+ self._media_album_name = track_info.get('album')
+ self._media_title = track_info.get('title')
- if playlist_position is not None and playlist_size is not None:
-
- if playlist_position <= 1:
- support_previous_track = False
-
- if playlist_position == playlist_size:
- support_next_track = False
-
- self._media_content_id = track_info.get('title')
- self._media_duration = _parse_timespan(
- track_info.get('duration')
- )
- self._media_position = media_position
- self._media_position_updated_at = media_position_updated_at
- self._media_image_url = media_image_url
- self._media_artist = media_artist
- self._media_album_name = media_album_name
- self._media_title = media_title
- self._current_track_uri = track_info['uri']
- self._current_track_is_radio_stream = is_radio_stream
- self._support_previous_track = support_previous_track
- self._support_next_track = support_next_track
- self._support_play = support_play
- self._support_shuffle_set = support_shuffle_set
- self._support_stop = support_stop
- self._support_pause = support_pause
- self._night_sound = night_sound
- self._speech_enhance = speech_enhance
- self._is_playing_tv = is_playing_tv
- self._is_playing_line_in = is_playing_line_in
- self._source_name = source_name
- self._last_avtransport_event = None
-
- def _format_media_image_url(self, url, fallback_uri):
- if url in ('', 'NOT_IMPLEMENTED', None):
- if fallback_uri in ('', 'NOT_IMPLEMENTED', None):
- return None
- if fallback_uri.find('tts_proxy') > 0:
- # If the content is a tts don't try to fetch an image from it.
- return None
- return 'http://{host}:{port}/getaa?s=1&u={uri}'.format(
- host=self._player.ip_address,
- port=1400,
- uri=urllib.parse.quote(fallback_uri)
- )
- return url
-
- def process_sonos_event(self, event):
- """Process a service event coming from the speaker."""
- next_track_image_url = None
- if event.service == self._player.avTransport:
- self._last_avtransport_event = event
-
- self._media_radio_show = None
- if self._current_track_is_radio_stream:
- current_track_metadata = event.variables.get(
- 'current_track_meta_data'
- )
- if current_track_metadata:
- self._media_radio_show = \
- current_track_metadata.radio_show.split(',')[0]
-
- next_track_uri = event.variables.get('next_track_uri')
- if next_track_uri:
- next_track_image_url = self._format_media_image_url(
- None,
- next_track_uri
- )
-
- elif event.service == self._player.renderingControl:
- if 'volume' in event.variables:
- self._player_volume = int(
- event.variables['volume'].get('Master')
- )
-
- if 'mute' in event.variables:
- self._player_volume_muted = \
- event.variables['mute'].get('Master') == '1'
-
- self.schedule_update_ha_state(True)
-
- if next_track_image_url:
- self.preload_media_image_url(next_track_image_url)
+ self._source_name = None
@property
def volume_level(self):
"""Volume level of the media player (0..1)."""
- return self._player_volume / 100.0
+ return self._player_volume / 100
@property
def is_volume_muted(self):
@@ -779,17 +701,10 @@ class SonosDevice(MediaPlayerDevice):
return self._player_volume_muted
@property
+ @soco_coordinator
def shuffle(self):
"""Shuffling state."""
- return True if self._player.play_mode == 'SHUFFLE' else False
-
- @property
- def media_content_id(self):
- """Content ID of current playing media."""
- if self._coordinator:
- return self._coordinator.media_content_id
-
- return self._media_content_id
+ return 'SHUFFLE' in self._play_mode
@property
def media_content_type(self):
@@ -797,260 +712,170 @@ class SonosDevice(MediaPlayerDevice):
return MEDIA_TYPE_MUSIC
@property
+ @soco_coordinator
def media_duration(self):
"""Duration of current playing media in seconds."""
- if self._coordinator:
- return self._coordinator.media_duration
-
return self._media_duration
@property
+ @soco_coordinator
def media_position(self):
"""Position of current playing media in seconds."""
- if self._coordinator:
- return self._coordinator.media_position
-
return self._media_position
@property
+ @soco_coordinator
def media_position_updated_at(self):
- """When was the position of the current playing media valid.
-
- Returns value from homeassistant.util.dt.utcnow().
- """
- if self._coordinator:
- return self._coordinator.media_position_updated_at
-
+ """When was the position of the current playing media valid."""
return self._media_position_updated_at
@property
+ @soco_coordinator
def media_image_url(self):
"""Image url of current playing media."""
- if self._coordinator:
- return self._coordinator.media_image_url
-
- return self._media_image_url
+ return self._media_image_url or None
@property
+ @soco_coordinator
def media_artist(self):
"""Artist of current playing media, music track only."""
- if self._coordinator:
- return self._coordinator.media_artist
-
return self._media_artist
@property
+ @soco_coordinator
def media_album_name(self):
"""Album name of current playing media, music track only."""
- if self._coordinator:
- return self._coordinator.media_album_name
-
return self._media_album_name
@property
+ @soco_coordinator
def media_title(self):
"""Title of current playing media."""
- if self._coordinator:
- return self._coordinator.media_title
-
return self._media_title
@property
- def night_sound(self):
- """Get status of Night Sound."""
- return self._night_sound
-
- @property
- def speech_enhance(self):
- """Get status of Speech Enhancement."""
- return self._speech_enhance
+ @soco_coordinator
+ def source(self):
+ """Name of the current input source."""
+ return self._source_name
@property
+ @soco_coordinator
def supported_features(self):
"""Flag media player features that are supported."""
- if self._coordinator:
- return self._coordinator.supported_features
-
- supported = SUPPORT_SONOS
-
- if not self._support_previous_track:
- supported = supported ^ SUPPORT_PREVIOUS_TRACK
-
- if not self._support_next_track:
- supported = supported ^ SUPPORT_NEXT_TRACK
-
- if not self._support_play:
- supported = supported ^ SUPPORT_PLAY
- if not self._support_shuffle_set:
- supported = supported ^ SUPPORT_SHUFFLE_SET
- if not self._support_stop:
- supported = supported ^ SUPPORT_STOP
-
- if not self._support_pause:
- supported = supported ^ SUPPORT_PAUSE
-
- return supported
+ return SUPPORT_SONOS | self._extra_features
@soco_error()
def volume_up(self):
"""Volume up media player."""
- self._player.volume += self.volume_increment
+ self._player.volume += self._volume_increment
@soco_error()
def volume_down(self):
"""Volume down media player."""
- self._player.volume -= self.volume_increment
+ self._player.volume -= self._volume_increment
@soco_error()
def set_volume_level(self, volume):
"""Set volume level, range 0..1."""
- self._player.volume = str(int(volume * 100))
+ self.soco.volume = str(int(volume * 100))
@soco_error()
+ @soco_coordinator
def set_shuffle(self, shuffle):
"""Enable/Disable shuffle mode."""
- self._player.play_mode = 'SHUFFLE' if shuffle else 'NORMAL'
+ self.soco.play_mode = 'SHUFFLE_NOREPEAT' if shuffle else 'NORMAL'
@soco_error()
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
- self._player.mute = mute
+ self.soco.mute = mute
@soco_error()
@soco_coordinator
def select_source(self, source):
"""Select input source."""
- if source == SUPPORT_SOURCE_LINEIN:
- self._source_name = SUPPORT_SOURCE_LINEIN
- self._player.switch_to_line_in()
- elif source == SUPPORT_SOURCE_TV:
- self._source_name = SUPPORT_SOURCE_TV
- self._player.switch_to_tv()
+ if source == SOURCE_LINEIN:
+ self.soco.switch_to_line_in()
+ elif source == SOURCE_TV:
+ self.soco.switch_to_tv()
else:
- fav = [fav for fav in self._favorite_sources
- if fav['title'] == source]
+ fav = [fav for fav in self._favorites
+ if fav.title == source]
if len(fav) == 1:
src = fav.pop()
- self._source_name = src['title']
-
- if ('object.container.playlistContainer' in src['meta'] or
- 'object.container.album.musicAlbum' in src['meta']):
- self._replace_queue_with_playlist(src)
- self._player.play_from_queue(0)
+ uri = src.reference.get_uri()
+ if _is_radio_uri(uri):
+ self.soco.play_uri(uri, title=source)
else:
- self._player.play_uri(src['uri'], src['meta'],
- src['title'])
-
- def _replace_queue_with_playlist(self, src):
- """Replace queue with playlist represented by src.
-
- Playlists can't be played directly with the self._player.play_uri
- API as they are actually composed of multiple URLs. Until soco has
- support for playing a playlist, we'll need to parse the playlist item
- and replace the current queue in order to play it.
- """
- import soco
- import xml.etree.ElementTree as ET
-
- root = ET.fromstring(src['meta'])
- namespaces = {'item':
- 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/',
- 'desc': 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/'}
- desc = root.find('item:item', namespaces).find('desc:desc',
- namespaces).text
-
- res = [soco.data_structures.DidlResource(uri=src['uri'],
- protocol_info="DUMMY")]
- didl = soco.data_structures.DidlItem(title="DUMMY",
- parent_id="DUMMY",
- item_id=src['uri'],
- desc=desc,
- resources=res)
-
- self._player.stop()
- self._player.clear_queue()
- self._player.add_to_queue(didl)
+ self.soco.clear_queue()
+ self.soco.add_to_queue(src.reference)
+ self.soco.play_from_queue(0)
@property
+ @soco_coordinator
def source_list(self):
"""List of available input sources."""
- if self._coordinator:
- return self._coordinator.source_list
+ sources = [fav.title for fav in self._favorites]
- model_name = self._speaker_info['model_name']
- sources = []
+ if 'PLAY:5' in self._model or 'CONNECT' in self._model:
+ sources += [SOURCE_LINEIN]
+ elif 'PLAYBAR' in self._model:
+ sources += [SOURCE_LINEIN, SOURCE_TV]
- if self._favorite_sources:
- for fav in self._favorite_sources:
- sources.append(fav['title'])
-
- if 'PLAY:5' in model_name:
- sources += [SUPPORT_SOURCE_LINEIN]
- elif 'PLAYBAR' in model_name:
- sources += [SUPPORT_SOURCE_LINEIN, SUPPORT_SOURCE_TV]
return sources
- @property
- def source(self):
- """Name of the current input source."""
- if self._coordinator:
- return self._coordinator.source
-
- return self._source_name
+ @soco_error()
+ def turn_on(self):
+ """Turn the media player on."""
+ self.media_play()
@soco_error()
def turn_off(self):
"""Turn off media player."""
- if self._support_stop:
- self.media_stop()
+ self.media_stop()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_play(self):
"""Send play command."""
- self._player.play()
+ self.soco.play()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_stop(self):
"""Send stop command."""
- self._player.stop()
+ self.soco.stop()
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_pause(self):
"""Send pause command."""
- self._player.pause()
+ self.soco.pause()
@soco_error()
@soco_coordinator
def media_next_track(self):
"""Send next track command."""
- self._player.next()
+ self.soco.next()
@soco_error()
@soco_coordinator
def media_previous_track(self):
"""Send next track command."""
- self._player.previous()
+ self.soco.previous()
@soco_error()
@soco_coordinator
def media_seek(self, position):
"""Send seek command."""
- self._player.seek(str(datetime.timedelta(seconds=int(position))))
+ self.soco.seek(str(datetime.timedelta(seconds=int(position))))
@soco_error()
@soco_coordinator
def clear_playlist(self):
"""Clear players playlist."""
- self._player.clear_queue()
-
- @soco_error()
- def turn_on(self):
- """Turn the media player on."""
- if self.support_play:
- self.media_play()
+ self.soco.clear_queue()
@soco_error()
@soco_coordinator
@@ -1063,45 +888,38 @@ class SonosDevice(MediaPlayerDevice):
if kwargs.get(ATTR_MEDIA_ENQUEUE):
from soco.exceptions import SoCoUPnPException
try:
- self._player.add_uri_to_queue(media_id)
+ self.soco.add_uri_to_queue(media_id)
except SoCoUPnPException:
_LOGGER.error('Error parsing media uri "%s", '
"please check it's a valid media resource "
'supported by Sonos', media_id)
else:
- self._player.play_uri(media_id)
+ self.soco.play_uri(media_id)
@soco_error()
- def join(self, master):
- """Join the player to a group."""
- coord = [device for device in self.hass.data[DATA_SONOS]
- if device.entity_id == master]
+ def join(self, slaves):
+ """Form a group with other players."""
+ if self._coordinator:
+ self.soco.unjoin()
- if coord and master != self.entity_id:
- coord = coord[0]
- if coord.soco.group.coordinator != coord.soco:
- coord.soco.unjoin()
- self._player.join(coord.soco)
- self._coordinator = coord
- else:
- _LOGGER.error("Master not found %s", master)
+ for slave in slaves:
+ slave.soco.join(self.soco)
@soco_error()
def unjoin(self):
"""Unjoin the player from a group."""
- self._player.unjoin()
- self._coordinator = None
+ self.soco.unjoin()
@soco_error()
def snapshot(self, with_group=True):
"""Snapshot the player."""
from soco.snapshot import Snapshot
- self._soco_snapshot = Snapshot(self._player)
+ self._soco_snapshot = Snapshot(self.soco)
self._soco_snapshot.snapshot()
if with_group:
- self._snapshot_group = self._player.group
+ self._snapshot_group = self.soco.group
if self._coordinator:
self._coordinator.snapshot(False)
else:
@@ -1121,12 +939,12 @@ class SonosDevice(MediaPlayerDevice):
# restore groups
if with_group and self._snapshot_group:
old = self._snapshot_group
- actual = self._player.group
+ actual = self.soco.group
##
# Master have not change, update group
if old.coordinator == actual.coordinator:
- if self._player is not old.coordinator:
+ if self.soco is not old.coordinator:
# restore state of the groups
self._coordinator.restore(False)
remove = actual.members - old.members
@@ -1144,13 +962,14 @@ class SonosDevice(MediaPlayerDevice):
##
# old is already master, rejoin
if old.coordinator.group.coordinator == old.coordinator:
- self._player.join(old.coordinator)
+ self.soco.join(old.coordinator)
return
##
# restore old master, update group
old.coordinator.unjoin()
- coordinator = _get_entity_from_soco(self.hass, old.coordinator)
+ coordinator = _get_entity_from_soco_uid(
+ self.hass, old.coordinator.uid)
coordinator.restore(False)
for s_dev in list(old.members):
@@ -1161,45 +980,45 @@ class SonosDevice(MediaPlayerDevice):
@soco_coordinator
def set_sleep_timer(self, sleep_time):
"""Set the timer on the player."""
- self._player.set_sleep_timer(sleep_time)
+ self.soco.set_sleep_timer(sleep_time)
@soco_error()
@soco_coordinator
def clear_sleep_timer(self):
"""Clear the timer on the player."""
- self._player.set_sleep_timer(None)
+ self.soco.set_sleep_timer(None)
@soco_error()
@soco_coordinator
def update_alarm(self, **data):
"""Set the alarm clock on the player."""
from soco import alarms
- a = None
- for alarm in alarms.get_alarms(self.soco):
+ alarm = None
+ for one_alarm in alarms.get_alarms(self.soco):
# pylint: disable=protected-access
- if alarm._alarm_id == str(data[ATTR_ALARM_ID]):
- a = alarm
- if a is None:
+ if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]):
+ alarm = one_alarm
+ if alarm is None:
_LOGGER.warning("did not find alarm with id %s",
data[ATTR_ALARM_ID])
return
if ATTR_TIME in data:
- a.start_time = data[ATTR_TIME]
+ alarm.start_time = data[ATTR_TIME]
if ATTR_VOLUME in data:
- a.volume = int(data[ATTR_VOLUME] * 100)
+ alarm.volume = int(data[ATTR_VOLUME] * 100)
if ATTR_ENABLED in data:
- a.enabled = data[ATTR_ENABLED]
+ alarm.enabled = data[ATTR_ENABLED]
if ATTR_INCLUDE_LINKED_ZONES in data:
- a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
- a.save()
+ alarm.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
+ alarm.save()
@soco_error()
def update_option(self, **data):
"""Modify playback options."""
- if ATTR_NIGHT_SOUND in data and self.night_sound is not None:
+ if ATTR_NIGHT_SOUND in data and self._night_sound is not None:
self.soco.night_mode = data[ATTR_NIGHT_SOUND]
- if ATTR_SPEECH_ENHANCE in data and self.speech_enhance is not None:
+ if ATTR_SPEECH_ENHANCE in data and self._speech_enhance is not None:
self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE]
@property
@@ -1207,10 +1026,10 @@ class SonosDevice(MediaPlayerDevice):
"""Return device specific state attributes."""
attributes = {ATTR_IS_COORDINATOR: self.is_coordinator}
- if self.night_sound is not None:
- attributes[ATTR_NIGHT_SOUND] = self.night_sound
+ if self._night_sound is not None:
+ attributes[ATTR_NIGHT_SOUND] = self._night_sound
- if self.speech_enhance is not None:
- attributes[ATTR_SPEECH_ENHANCE] = self.speech_enhance
+ if self._speech_enhance is not None:
+ attributes[ATTR_SPEECH_ENHANCE] = self._speech_enhance
return attributes
diff --git a/homeassistant/components/media_player/soundtouch.py b/homeassistant/components/media_player/soundtouch.py
index e4c3fa623c9..9c4a0e9fa17 100644
--- a/homeassistant/components/media_player/soundtouch.py
+++ b/homeassistant/components/media_player/soundtouch.py
@@ -296,7 +296,7 @@ class SoundTouchDevice(MediaPlayerDevice):
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
- _LOGGER.debug("Starting media with media_id: " + str(media_id))
+ _LOGGER.debug("Starting media with media_id: %s", media_id)
if re.match(r'http://', str(media_id)):
# URL
_LOGGER.debug("Playing URL %s", str(media_id))
@@ -307,11 +307,10 @@ class SoundTouchDevice(MediaPlayerDevice):
preset = next([preset for preset in presets if
preset.preset_id == str(media_id)].__iter__(), None)
if preset is not None:
- _LOGGER.debug("Playing preset: " + preset.name)
+ _LOGGER.debug("Playing preset: %s", preset.name)
self._device.select_preset(preset)
else:
- _LOGGER.warning(
- "Unable to find preset with id " + str(media_id))
+ _LOGGER.warning("Unable to find preset with id %s", media_id)
def create_zone(self, slaves):
"""
@@ -323,8 +322,8 @@ class SoundTouchDevice(MediaPlayerDevice):
if not slaves:
_LOGGER.warning("Unable to create zone without slaves")
else:
- _LOGGER.info(
- "Creating zone with master " + str(self.device.config.name))
+ _LOGGER.info("Creating zone with master %s",
+ self.device.config.name)
self.device.create_zone([slave.device for slave in slaves])
def remove_zone_slave(self, slaves):
@@ -341,8 +340,8 @@ class SoundTouchDevice(MediaPlayerDevice):
if not slaves:
_LOGGER.warning("Unable to find slaves to remove")
else:
- _LOGGER.info("Removing slaves from zone with master " +
- str(self.device.config.name))
+ _LOGGER.info("Removing slaves from zone with master %s",
+ self.device.config.name)
self.device.remove_zone_slave([slave.device for slave in slaves])
def add_zone_slave(self, slaves):
@@ -357,7 +356,6 @@ class SoundTouchDevice(MediaPlayerDevice):
if not slaves:
_LOGGER.warning("Unable to find slaves to add")
else:
- _LOGGER.info(
- "Adding slaves to zone with master " + str(
- self.device.config.name))
+ _LOGGER.info("Adding slaves to zone with master %s",
+ self.device.config.name)
self.device.add_zone_slave([slave.device for slave in slaves])
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index 3ccd3c7dbe9..acd1ffad6eb 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -270,8 +270,7 @@ class LgWebOSDevice(MediaPlayerDevice):
"""Title of current playing media."""
if (self._channel is not None) and ('channelName' in self._channel):
return self._channel['channelName']
- else:
- return None
+ return None
@property
def media_image_url(self):
diff --git a/homeassistant/components/media_player/xiaomi_tv.py b/homeassistant/components/media_player/xiaomi_tv.py
new file mode 100644
index 00000000000..be40bf7d010
--- /dev/null
+++ b/homeassistant/components/media_player/xiaomi_tv.py
@@ -0,0 +1,112 @@
+"""
+Add support for the Xiaomi TVs.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/xiaomi_tv/
+"""
+
+import logging
+import voluptuous as vol
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON)
+from homeassistant.components.media_player import (
+ SUPPORT_TURN_ON, SUPPORT_TURN_OFF, MediaPlayerDevice, PLATFORM_SCHEMA,
+ SUPPORT_VOLUME_STEP)
+
+REQUIREMENTS = ['pymitv==1.0.0']
+
+DEFAULT_NAME = "Xiaomi TV"
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_XIAOMI_TV = SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | \
+ SUPPORT_TURN_OFF
+
+# No host is needed for configuration, however it can be set.
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Xiaomi TV platform."""
+ from pymitv import Discover
+
+ # If a hostname is set. Discovery is skipped.
+ host = config.get(CONF_HOST)
+ name = config.get(CONF_NAME)
+
+ if host is not None:
+ # Check if there's a valid TV at the IP address.
+ if not Discover().checkIp(host):
+ _LOGGER.error(
+ "Could not find Xiaomi TV with specified IP: %s", host
+ )
+ else:
+ # Register TV with Home Assistant.
+ add_devices([XiaomiTV(host, name)])
+ else:
+ # Otherwise, discover TVs on network.
+ add_devices(XiaomiTV(tv, DEFAULT_NAME) for tv in Discover().scan())
+
+
+class XiaomiTV(MediaPlayerDevice):
+ """Represent the Xiaomi TV for Home Assistant."""
+
+ def __init__(self, ip, name):
+ """Receive IP address and name to construct class."""
+ # Import pymitv library.
+ from pymitv import TV
+
+ # Initialize the Xiaomi TV.
+ self._tv = TV(ip)
+ # Default name value, only to be overridden by user.
+ self._name = name
+ self._state = STATE_OFF
+
+ @property
+ def name(self):
+ """Return the display name of this TV."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return _state variable, containing the appropriate constant."""
+ return self._state
+
+ @property
+ def assumed_state(self):
+ """Indicate that state is assumed."""
+ return True
+
+ @property
+ def supported_features(self):
+ """Flag media player features that are supported."""
+ return SUPPORT_XIAOMI_TV
+
+ def turn_off(self):
+ """
+ Instruct the TV to turn sleep.
+
+ This is done instead of turning off,
+ because the TV won't accept any input when turned off. Thus, the user
+ would be unable to turn the TV back on, unless it's done manually.
+ """
+ self._tv.sleep()
+
+ self._state = STATE_OFF
+
+ def turn_on(self):
+ """Wake the TV back up from sleep."""
+ self._tv.wake()
+
+ self._state = STATE_ON
+
+ def volume_up(self):
+ """Increase volume by one."""
+ self._tv.volume_up()
+
+ def volume_down(self):
+ """Decrease volume by one."""
+ self._tv.volume_down()
diff --git a/homeassistant/components/melissa.py b/homeassistant/components/melissa.py
index ae82b96222e..f5a757dbcf3 100644
--- a/homeassistant/components/melissa.py
+++ b/homeassistant/components/melissa.py
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import load_platform
-REQUIREMENTS = ["py-melissa-climate==1.0.1"]
+REQUIREMENTS = ["py-melissa-climate==1.0.6"]
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py
index cdf59b92606..0485d82a274 100644
--- a/homeassistant/components/mqtt/__init__.py
+++ b/homeassistant/components/mqtt/__init__.py
@@ -5,6 +5,10 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mqtt/
"""
import asyncio
+from collections import namedtuple
+from itertools import groupby
+from typing import Optional
+from operator import attrgetter
import logging
import os
import socket
@@ -15,13 +19,12 @@ import requests.certs
import voluptuous as vol
+from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.core import callback
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass
-from homeassistant.helpers import template, config_validation as cv
-from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect, dispatcher_send)
+from homeassistant.helpers import template, ConfigType, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util.async import (
run_coroutine_threadsafe, run_callback_threadsafe)
@@ -39,7 +42,6 @@ DOMAIN = 'mqtt'
DATA_MQTT = 'mqtt'
SERVICE_PUBLISH = 'publish'
-SIGNAL_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received'
CONF_EMBEDDED = 'embedded'
CONF_BROKER = 'broker'
@@ -173,7 +175,6 @@ MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
-
# Service call validation schema
MQTT_PUBLISH_SCHEMA = vol.Schema({
vol.Required(ATTR_TOPIC): valid_publish_topic,
@@ -221,32 +222,13 @@ def publish_template(hass, topic, payload_template, qos=None, retain=None):
@bind_hass
def async_subscribe(hass, topic, msg_callback, qos=DEFAULT_QOS,
encoding='utf-8'):
- """Subscribe to an MQTT topic."""
- @callback
- def async_mqtt_topic_subscriber(dp_topic, dp_payload, dp_qos):
- """Match subscribed MQTT topic."""
- if not _match_topic(topic, dp_topic):
- return
+ """Subscribe to an MQTT topic.
- if encoding is not None:
- try:
- payload = dp_payload.decode(encoding)
- _LOGGER.debug("Received message on %s: %s", dp_topic, payload)
- except (AttributeError, UnicodeDecodeError):
- _LOGGER.error("Illegal payload encoding %s from "
- "MQTT topic: %s, Payload: %s",
- encoding, dp_topic, dp_payload)
- return
- else:
- _LOGGER.debug("Received binary message on %s", dp_topic)
- payload = dp_payload
-
- hass.async_run_job(msg_callback, dp_topic, payload, dp_qos)
-
- async_remove = async_dispatcher_connect(
- hass, SIGNAL_MQTT_MESSAGE_RECEIVED, async_mqtt_topic_subscriber)
-
- yield from hass.data[DATA_MQTT].async_subscribe(topic, qos)
+ Call the return value to unsubscribe.
+ """
+ async_remove = \
+ yield from hass.data[DATA_MQTT].async_subscribe(topic, msg_callback,
+ qos, encoding)
return async_remove
@@ -308,7 +290,7 @@ def _async_setup_discovery(hass, config):
@asyncio.coroutine
-def async_setup(hass, config):
+def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Start the MQTT protocol service."""
conf = config.get(DOMAIN)
@@ -351,8 +333,8 @@ def async_setup(hass, config):
return False
# For cloudmqtt.com, secured connection, auto fill in certificate
- if certificate is None and 19999 < port < 30000 and \
- broker.endswith('.cloudmqtt.com'):
+ if (certificate is None and 19999 < port < 30000 and
+ broker.endswith('.cloudmqtt.com')):
certificate = os.path.join(os.path.dirname(__file__),
'addtrustexternalcaroot.crt')
@@ -360,8 +342,12 @@ def async_setup(hass, config):
if certificate == 'auto':
certificate = requests.certs.where()
- will_message = conf.get(CONF_WILL_MESSAGE)
- birth_message = conf.get(CONF_BIRTH_MESSAGE)
+ will_message = None
+ if conf.get(CONF_WILL_MESSAGE) is not None:
+ will_message = Message(**conf.get(CONF_WILL_MESSAGE))
+ birth_message = None
+ if conf.get(CONF_BIRTH_MESSAGE) is not None:
+ birth_message = Message(**conf.get(CONF_BIRTH_MESSAGE))
# Be able to override versions other than TLSv1.0 under Python3.6
conf_tls_version = conf.get(CONF_TLS_VERSION)
@@ -414,8 +400,8 @@ def async_setup(hass, config):
template.Template(payload_template, hass).async_render()
except template.jinja2.TemplateError as exc:
_LOGGER.error(
- "Unable to publish to '%s': rendering payload template of "
- "'%s' failed because %s",
+ "Unable to publish to %s: rendering payload template of "
+ "%s failed because %s",
msg_topic, payload_template, exc)
return
@@ -432,13 +418,21 @@ def async_setup(hass, config):
return True
+Subscription = namedtuple('Subscription',
+ ['topic', 'callback', 'qos', 'encoding'])
+Subscription.__new__.__defaults__ = (0, 'utf-8')
+
+Message = namedtuple('Message', ['topic', 'payload', 'qos', 'retain'])
+Message.__new__.__defaults__ = (0, False)
+
+
class MQTT(object):
"""Home Assistant MQTT client."""
def __init__(self, hass, broker, port, client_id, keepalive, username,
password, certificate, client_key, client_cert,
- tls_insecure, protocol, will_message, birth_message,
- tls_version):
+ tls_insecure, protocol, will_message: Optional[Message],
+ birth_message: Optional[Message], tls_version):
"""Initialize Home Assistant MQTT client."""
import paho.mqtt.client as mqtt
@@ -446,9 +440,7 @@ class MQTT(object):
self.broker = broker
self.port = port
self.keepalive = keepalive
- self.wanted_topics = {}
- self.subscribed_topics = {}
- self.progress = {}
+ self.subscriptions = []
self.birth_message = birth_message
self._mqttc = None
self._paho_lock = asyncio.Lock(loop=hass.loop)
@@ -474,17 +466,12 @@ class MQTT(object):
if tls_insecure is not None:
self._mqttc.tls_insecure_set(tls_insecure)
- self._mqttc.on_subscribe = self._mqtt_on_subscribe
- self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe
self._mqttc.on_connect = self._mqtt_on_connect
self._mqttc.on_disconnect = self._mqtt_on_disconnect
self._mqttc.on_message = self._mqtt_on_message
if will_message:
- self._mqttc.will_set(will_message.get(ATTR_TOPIC),
- will_message.get(ATTR_PAYLOAD),
- will_message.get(ATTR_QOS),
- will_message.get(ATTR_RETAIN))
+ self._mqttc.will_set(*will_message)
@asyncio.coroutine
def async_publish(self, topic, payload, qos, retain):
@@ -526,36 +513,53 @@ class MQTT(object):
return self.hass.async_add_job(stop)
@asyncio.coroutine
- def async_subscribe(self, topic, qos):
- """Subscribe to a topic.
+ def async_subscribe(self, topic, msg_callback, qos, encoding):
+ """Set up a subscription to a topic with the provided qos.
This method is a coroutine.
"""
if not isinstance(topic, str):
- raise HomeAssistantError("topic need to be a string!")
+ raise HomeAssistantError("topic needs to be a string!")
- with (yield from self._paho_lock):
- if topic in self.subscribed_topics:
+ subscription = Subscription(topic, msg_callback, qos, encoding)
+ self.subscriptions.append(subscription)
+
+ yield from self._async_perform_subscription(topic, qos)
+
+ @callback
+ def async_remove():
+ """Remove subscription."""
+ if subscription not in self.subscriptions:
+ raise HomeAssistantError("Can't remove subscription twice")
+ self.subscriptions.remove(subscription)
+
+ if any(other.topic == topic for other in self.subscriptions):
+ # Other subscriptions on topic remaining - don't unsubscribe.
return
- self.wanted_topics[topic] = qos
- result, mid = yield from self.hass.async_add_job(
- self._mqttc.subscribe, topic, qos)
+ self.hass.async_add_job(self._async_unsubscribe(topic))
- _raise_on_error(result)
- self.progress[mid] = topic
+ return async_remove
@asyncio.coroutine
- def async_unsubscribe(self, topic):
- """Unsubscribe from topic.
+ def _async_unsubscribe(self, topic):
+ """Unsubscribe from a topic.
This method is a coroutine.
"""
- self.wanted_topics.pop(topic, None)
- result, mid = yield from self.hass.async_add_job(
- self._mqttc.unsubscribe, topic)
+ with (yield from self._paho_lock):
+ result, _ = yield from self.hass.async_add_job(
+ self._mqttc.unsubscribe, topic)
+ _raise_on_error(result)
- _raise_on_error(result)
- self.progress[mid] = topic
+ @asyncio.coroutine
+ def _async_perform_subscription(self, topic, qos):
+ """Perform a paho-mqtt subscription."""
+ _LOGGER.debug("Subscribing to %s", topic)
+
+ with (yield from self._paho_lock):
+ result, _ = yield from self.hass.async_add_job(
+ self._mqttc.subscribe, topic, qos)
+ _raise_on_error(result)
def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code):
"""On connect callback.
@@ -571,50 +575,50 @@ class MQTT(object):
self._mqttc.disconnect()
return
- self.progress = {}
- self.subscribed_topics = {}
- for topic, qos in self.wanted_topics.items():
- self.hass.add_job(self.async_subscribe, topic, qos)
+ # Group subscriptions to only re-subscribe once for each topic.
+ keyfunc = attrgetter('topic')
+ for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc),
+ keyfunc):
+ # Re-subscribe with the highest requested qos
+ max_qos = max(subscription.qos for subscription in subs)
+ self.hass.add_job(self._async_perform_subscription, topic, max_qos)
if self.birth_message:
- self.hass.add_job(self.async_publish(
- self.birth_message.get(ATTR_TOPIC),
- self.birth_message.get(ATTR_PAYLOAD),
- self.birth_message.get(ATTR_QOS),
- self.birth_message.get(ATTR_RETAIN)))
-
- def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos):
- """Subscribe successful callback."""
- topic = self.progress.pop(mid, None)
- if topic is None:
- return
- self.subscribed_topics[topic] = granted_qos[0]
+ self.hass.add_job(self.async_publish(*self.birth_message))
def _mqtt_on_message(self, _mqttc, _userdata, msg):
"""Message received callback."""
- dispatcher_send(
- self.hass, SIGNAL_MQTT_MESSAGE_RECEIVED, msg.topic, msg.payload,
- msg.qos
- )
+ self.hass.async_add_job(self._mqtt_handle_message, msg)
- def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos):
- """Unsubscribe successful callback."""
- topic = self.progress.pop(mid, None)
- if topic is None:
- return
- self.subscribed_topics.pop(topic, None)
+ @callback
+ def _mqtt_handle_message(self, msg):
+ _LOGGER.debug("Received message on %s: %s", msg.topic, msg.payload)
+
+ for subscription in self.subscriptions:
+ if not _match_topic(subscription.topic, msg.topic):
+ continue
+
+ payload = msg.payload
+ if subscription.encoding is not None:
+ try:
+ payload = msg.payload.decode(subscription.encoding)
+ except (AttributeError, UnicodeDecodeError):
+ _LOGGER.warning("Can't decode payload %s on %s "
+ "with encoding %s",
+ msg.payload, msg.topic,
+ subscription.encoding)
+ continue
+
+ self.hass.async_run_job(subscription.callback,
+ msg.topic, payload, msg.qos)
def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code):
"""Disconnected callback."""
- self.progress = {}
- self.subscribed_topics = {}
-
# When disconnected because of calling disconnect()
if result_code == 0:
return
tries = 0
- wait_time = 0
while True:
try:
@@ -693,7 +697,7 @@ class MqttAvailability(Entity):
if self._availability_topic is not None:
yield from async_subscribe(
self.hass, self._availability_topic,
- availability_message_received, self. _availability_qos)
+ availability_message_received, self._availability_qos)
@property
def available(self) -> bool:
diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py
index 40a752807ed..6f6cb312f2b 100644
--- a/homeassistant/components/mqtt_eventstream.py
+++ b/homeassistant/components/mqtt_eventstream.py
@@ -26,6 +26,7 @@ DEPENDENCIES = ['mqtt']
CONF_PUBLISH_TOPIC = 'publish_topic'
CONF_SUBSCRIBE_TOPIC = 'subscribe_topic'
CONF_PUBLISH_EVENTSTREAM_RECEIVED = 'publish_eventstream_received'
+CONF_IGNORE_EVENT = 'ignore_event'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -33,6 +34,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_SUBSCRIBE_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_PUBLISH_EVENTSTREAM_RECEIVED, default=False):
cv.boolean,
+ vol.Optional(CONF_IGNORE_EVENT, default=[]): cv.ensure_list
}),
}, extra=vol.ALLOW_EXTRA)
@@ -44,6 +46,7 @@ def async_setup(hass, config):
conf = config.get(DOMAIN, {})
pub_topic = conf.get(CONF_PUBLISH_TOPIC)
sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC)
+ ignore_event = conf.get(CONF_IGNORE_EVENT)
@callback
def _event_publisher(event):
@@ -53,6 +56,10 @@ def async_setup(hass, config):
if event.event_type == EVENT_TIME_CHANGED:
return
+ # User-defined events to ignore
+ if event.event_type in ignore_event:
+ return
+
# Filter out the events that were triggered by publishing
# to the MQTT topic, or you will end up in an infinite loop.
if event.event_type == EVENT_CALL_SERVICE:
diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py
index e7a727bc5e2..dcbd1ce1317 100644
--- a/homeassistant/components/notify/apns.py
+++ b/homeassistant/components/notify/apns.py
@@ -39,7 +39,7 @@ PLATFORM_SCHEMA = vol.Schema({
REGISTER_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_PUSH_ID): cv.string,
- vol.Optional(ATTR_NAME, default=None): cv.string,
+ vol.Optional(ATTR_NAME): cv.string,
})
diff --git a/homeassistant/components/notify/html5.py b/homeassistant/components/notify/html5.py
index f2611cf65d3..45439dbfbfe 100644
--- a/homeassistant/components/notify/html5.py
+++ b/homeassistant/components/notify/html5.py
@@ -136,7 +136,7 @@ def _load_config(filename):
class JSONBytesDecoder(json.JSONEncoder):
"""JSONEncoder to decode bytes objects to unicode."""
- # pylint: disable=method-hidden
+ # pylint: disable=method-hidden, arguments-differ
def default(self, obj):
"""Decode object if it's a bytes object, else defer to base class."""
if isinstance(obj, bytes):
@@ -255,12 +255,12 @@ class HTML5PushCallbackView(HomeAssistantView):
# 2a. If decode is successful, return the payload.
# 2b. If decode is unsuccessful, return a 401.
- target_check = jwt.decode(token, options={'verify_signature': False})
+ target_check = jwt.decode(token, verify=False)
if target_check[ATTR_TARGET] in self.registrations:
possible_target = self.registrations[target_check[ATTR_TARGET]]
key = possible_target[ATTR_SUBSCRIPTION][ATTR_KEYS][ATTR_AUTH]
try:
- return jwt.decode(token, key)
+ return jwt.decode(token, key, algorithms=["ES256", "HS256"])
except jwt.exceptions.DecodeError:
pass
diff --git a/homeassistant/components/notify/knx.py b/homeassistant/components/notify/knx.py
index d14d8dcf8ad..e6bb400d421 100644
--- a/homeassistant/components/notify/knx.py
+++ b/homeassistant/components/notify/knx.py
@@ -27,10 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_get_service(hass, config, discovery_info=None):
"""Get the KNX notification service."""
- if DATA_KNX not in hass.data \
- or not hass.data[DATA_KNX].initialized:
- return False
-
return async_get_service_discovery(hass, discovery_info) \
if discovery_info is not None else \
async_get_service_config(hass, config)
@@ -44,7 +40,7 @@ def async_get_service_discovery(hass, discovery_info):
device = hass.data[DATA_KNX].xknx.devices[device_name]
notification_devices.append(device)
return \
- KNXNotificationService(hass, notification_devices) \
+ KNXNotificationService(notification_devices) \
if notification_devices else \
None
@@ -58,15 +54,14 @@ def async_get_service_config(hass, config):
name=config.get(CONF_NAME),
group_address=config.get(CONF_ADDRESS))
hass.data[DATA_KNX].xknx.devices.add(notification)
- return KNXNotificationService(hass, [notification, ])
+ return KNXNotificationService([notification, ])
class KNXNotificationService(BaseNotificationService):
"""Implement demo notification service."""
- def __init__(self, hass, devices):
+ def __init__(self, devices):
"""Initialize the service."""
- self.hass = hass
self.devices = devices
@property
diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py
index 2f967dcdda4..f4c9c391408 100644
--- a/homeassistant/components/notify/lametric.py
+++ b/homeassistant/components/notify/lametric.py
@@ -93,6 +93,11 @@ class LaMetricNotificationService(BaseNotificationService):
devices = lmn.get_devices()
for dev in devices:
if targets is None or dev["name"] in targets:
- lmn.set_device(dev)
- lmn.send_notification(model, lifetime=self._lifetime)
- _LOGGER.debug("Sent notification to LaMetric %s", dev["name"])
+ try:
+ lmn.set_device(dev)
+ lmn.send_notification(model, lifetime=self._lifetime)
+ _LOGGER.debug("Sent notification to LaMetric %s",
+ dev["name"])
+ except OSError:
+ _LOGGER.warning("Cannot connect to LaMetric %s",
+ dev["name"])
diff --git a/homeassistant/components/notify/llamalab_automate.py b/homeassistant/components/notify/llamalab_automate.py
index 606c0fafc8b..0ddcb450bcf 100644
--- a/homeassistant/components/notify/llamalab_automate.py
+++ b/homeassistant/components/notify/llamalab_automate.py
@@ -56,4 +56,4 @@ class AutomateNotificationService(BaseNotificationService):
response = requests.post(_RESOURCE, json=data)
if response.status_code != 200:
- _LOGGER.error("Error sending message: " + str(response))
+ _LOGGER.error("Error sending message: %s", response)
diff --git a/homeassistant/components/notify/rest.py b/homeassistant/components/notify/rest.py
index 19339a2c7ec..73618c19502 100644
--- a/homeassistant/components/notify/rest.py
+++ b/homeassistant/components/notify/rest.py
@@ -22,8 +22,6 @@ CONF_TARGET_PARAMETER_NAME = 'target_param_name'
CONF_TITLE_PARAMETER_NAME = 'title_param_name'
DEFAULT_MESSAGE_PARAM_NAME = 'message'
DEFAULT_METHOD = 'GET'
-DEFAULT_TARGET_PARAM_NAME = None
-DEFAULT_TITLE_PARAM_NAME = None
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_RESOURCE): cv.url,
@@ -32,14 +30,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_METHOD, default=DEFAULT_METHOD):
vol.In(['POST', 'GET', 'POST_JSON']),
vol.Optional(CONF_NAME): cv.string,
- vol.Optional(CONF_TARGET_PARAMETER_NAME,
- default=DEFAULT_TARGET_PARAM_NAME): cv.string,
- vol.Optional(CONF_TITLE_PARAMETER_NAME,
- default=DEFAULT_TITLE_PARAM_NAME): cv.string,
- vol.Optional(CONF_DATA,
- default=None): dict,
- vol.Optional(CONF_DATA_TEMPLATE,
- default=None): {cv.match_all: cv.template_complex}
+ vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string,
+ vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string,
+ vol.Optional(CONF_DATA): dict,
+ vol.Optional(CONF_DATA_TEMPLATE): {cv.match_all: cv.template_complex}
})
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py
index 6ddf00cf7d4..4574437bac9 100644
--- a/homeassistant/components/panel_iframe.py
+++ b/homeassistant/components/panel_iframe.py
@@ -23,13 +23,14 @@ CONF_RELATIVE_URL_REGEX = r'\A/'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: {
+ # pylint: disable=no-value-for-parameter
vol.Optional(CONF_TITLE): cv.string,
vol.Optional(CONF_ICON): cv.icon,
vol.Required(CONF_URL): vol.Any(
vol.Match(
CONF_RELATIVE_URL_REGEX,
msg=CONF_RELATIVE_URL_ERROR_MSG),
- cv.url),
+ vol.Url()),
}})}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py
index 24b8c682d02..048851e97f5 100644
--- a/homeassistant/components/plant.py
+++ b/homeassistant/components/plant.py
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta
from collections import deque
import voluptuous as vol
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.const import (
STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, TEMP_CELSIUS, ATTR_TEMPERATURE,
CONF_SENSORS, ATTR_UNIT_OF_MEASUREMENT)
@@ -198,8 +199,8 @@ class Plant(Entity):
self._brightness_history.add_measurement(self._brightness,
new_state.last_updated)
else:
- raise _LOGGER.error("Unknown reading from sensor %s: %s",
- entity_id, value)
+ raise HomeAssistantError(
+ "Unknown reading from sensor {}: {}".format(entity_id, value))
if ATTR_UNIT_OF_MEASUREMENT in new_state.attributes:
self._unit_of_measurement[reading] = \
new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py
index b49b280791a..dedc39ef3a2 100644
--- a/homeassistant/components/python_script.py
+++ b/homeassistant/components/python_script.py
@@ -208,5 +208,4 @@ class TimeWrapper:
"""Wrap to return callable method if callable."""
return attribute(*args, **kw)
return wrapper
- else:
- return attribute
+ return attribute
diff --git a/homeassistant/components/raspihats.py b/homeassistant/components/raspihats.py
index e3c1ab8ff88..3bc45eab34e 100644
--- a/homeassistant/components/raspihats.py
+++ b/homeassistant/components/raspihats.py
@@ -4,6 +4,7 @@ Support for controlling raspihats boards.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/raspihats/
"""
+# pylint: disable=import-error,no-name-in-module
import logging
import threading
import time
@@ -143,7 +144,6 @@ class I2CHatsManager(threading.Thread):
def run(self):
"""Keep alive for I2C-HATs."""
- # pylint: disable=import-error
from raspihats.i2c_hats import ResponseException
_LOGGER.info(log_message(self, "starting"))
@@ -206,7 +206,6 @@ class I2CHatsManager(threading.Thread):
def read_di(self, address, channel):
"""Read a value from a I2C-HAT digital input."""
- # pylint: disable=import-error
from raspihats.i2c_hats import ResponseException
with self._lock:
@@ -219,7 +218,6 @@ class I2CHatsManager(threading.Thread):
def write_dq(self, address, channel, value):
"""Write a value to a I2C-HAT digital output."""
- # pylint: disable=import-error
from raspihats.i2c_hats import ResponseException
with self._lock:
@@ -231,7 +229,6 @@ class I2CHatsManager(threading.Thread):
def read_dq(self, address, channel):
"""Read a value from a I2C-HAT digital output."""
- # pylint: disable=import-error
from raspihats.i2c_hats import ResponseException
with self._lock:
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index b2628f954fc..bffe29ec59b 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -43,10 +43,12 @@ DOMAIN = 'recorder'
SERVICE_PURGE = 'purge'
ATTR_KEEP_DAYS = 'keep_days'
+ATTR_REPACK = 'repack'
SERVICE_PURGE_SCHEMA = vol.Schema({
- vol.Required(ATTR_KEEP_DAYS):
- vol.All(vol.Coerce(int), vol.Range(min=0))
+ vol.Optional(ATTR_KEEP_DAYS):
+ vol.All(vol.Coerce(int), vol.Range(min=0)),
+ vol.Optional(ATTR_REPACK, default=False): cv.boolean
})
DEFAULT_URL = 'sqlite:///{hass_config_path}'
@@ -61,22 +63,22 @@ CONNECT_RETRY_WAIT = 3
FILTER_SCHEMA = vol.Schema({
vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({
- vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
- vol.Optional(CONF_DOMAINS, default=[]):
+ vol.Optional(CONF_ENTITIES): cv.entity_ids,
+ vol.Optional(CONF_DOMAINS):
vol.All(cv.ensure_list, [cv.string]),
- vol.Optional(CONF_EVENT_TYPES, default=[]):
+ vol.Optional(CONF_EVENT_TYPES):
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.Optional(CONF_ENTITIES): cv.entity_ids,
+ vol.Optional(CONF_DOMAINS):
vol.All(cv.ensure_list, [cv.string])
})
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: FILTER_SCHEMA.extend({
- vol.Optional(CONF_PURGE_KEEP_DAYS):
+ vol.Optional(CONF_PURGE_KEEP_DAYS, default=10):
vol.All(vol.Coerce(int), vol.Range(min=1)),
vol.Optional(CONF_PURGE_INTERVAL, default=1):
vol.All(vol.Coerce(int), vol.Range(min=0)),
@@ -85,16 +87,17 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
+@asyncio.coroutine
def wait_connection_ready(hass):
"""
Wait till the connection is ready.
Returns a coroutine object.
"""
- return hass.data[DATA_INSTANCE].async_db_ready
+ return (yield from hass.data[DATA_INSTANCE].async_db_ready)
-def run_information(hass, point_in_time: Optional[datetime]=None):
+def run_information(hass, point_in_time: Optional[datetime] = None):
"""Return information about current run.
There is also the run that covers point_in_time.
@@ -122,12 +125,6 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
keep_days = conf.get(CONF_PURGE_KEEP_DAYS)
purge_interval = conf.get(CONF_PURGE_INTERVAL)
- if keep_days is None and purge_interval != 0:
- _LOGGER.warning(
- "From version 0.64.0 the 'recorder' component will by default "
- "purge data older than 10 days. To keep data longer you must "
- "configure 'purge_keep_days' or 'purge_interval'.")
-
db_url = conf.get(CONF_DB_URL, None)
if not db_url:
db_url = DEFAULT_URL.format(
@@ -144,7 +141,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@asyncio.coroutine
def async_handle_purge_service(service):
"""Handle calls to the purge service."""
- instance.do_adhoc_purge(service.data[ATTR_KEEP_DAYS])
+ instance.do_adhoc_purge(**service.data)
hass.services.async_register(
DOMAIN, SERVICE_PURGE, async_handle_purge_service,
@@ -153,7 +150,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return (yield from instance.async_db_ready)
-PurgeTask = namedtuple('PurgeTask', ['keep_days'])
+PurgeTask = namedtuple('PurgeTask', ['keep_days', 'repack'])
class Recorder(threading.Thread):
@@ -188,10 +185,12 @@ class Recorder(threading.Thread):
"""Initialize the recorder."""
self.hass.bus.async_listen(MATCH_ALL, self.event_listener)
- def do_adhoc_purge(self, keep_days):
+ def do_adhoc_purge(self, **kwargs):
"""Trigger an adhoc purge retaining keep_days worth of data."""
- if keep_days is not None:
- self.queue.put(PurgeTask(keep_days))
+ keep_days = kwargs.get(ATTR_KEEP_DAYS, self.keep_days)
+ repack = kwargs.get(ATTR_REPACK)
+
+ self.queue.put(PurgeTask(keep_days, repack))
def run(self):
"""Start processing events to save."""
@@ -257,27 +256,6 @@ class Recorder(threading.Thread):
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START, notify_hass_started)
- if self.keep_days and self.purge_interval:
- @callback
- def async_purge(now):
- """Trigger the purge and schedule the next run."""
- self.queue.put(PurgeTask(self.keep_days))
- self.hass.helpers.event.async_track_point_in_time(
- async_purge, now + timedelta(days=self.purge_interval))
-
- earliest = dt_util.utcnow() + timedelta(minutes=30)
- run = latest = dt_util.utcnow() + \
- timedelta(days=self.purge_interval)
- with session_scope(session=self.get_session()) as session:
- event = session.query(Events).first()
- if event is not None:
- session.expunge(event)
- run = dt_util.as_utc(event.time_fired) + \
- timedelta(days=self.keep_days+self.purge_interval)
- run = min(latest, max(run, earliest))
- self.hass.helpers.event.async_track_point_in_time(
- async_purge, run)
-
self.hass.add_job(register)
result = hass_started.result()
@@ -285,6 +263,29 @@ class Recorder(threading.Thread):
if result is shutdown_task:
return
+ # Start periodic purge
+ if self.keep_days and self.purge_interval:
+ @callback
+ def async_purge(now):
+ """Trigger the purge and schedule the next run."""
+ self.queue.put(
+ PurgeTask(self.keep_days, repack=not self.did_vacuum))
+ self.hass.helpers.event.async_track_point_in_time(
+ async_purge, now + timedelta(days=self.purge_interval))
+
+ earliest = dt_util.utcnow() + timedelta(minutes=30)
+ run = latest = dt_util.utcnow() + \
+ timedelta(days=self.purge_interval)
+ with session_scope(session=self.get_session()) as session:
+ event = session.query(Events).first()
+ if event is not None:
+ session.expunge(event)
+ run = dt_util.as_utc(event.time_fired) + timedelta(
+ days=self.keep_days+self.purge_interval)
+ run = min(latest, max(run, earliest))
+
+ self.hass.helpers.event.track_point_in_time(async_purge, run)
+
while True:
event = self.queue.get()
@@ -294,7 +295,7 @@ class Recorder(threading.Thread):
self.queue.task_done()
return
elif isinstance(event, PurgeTask):
- purge.purge_old_data(self, event.keep_days)
+ purge.purge_old_data(self, event.keep_days, event.repack)
self.queue.task_done()
continue
elif event.event_type == EVENT_TIME_CHANGED:
@@ -319,6 +320,7 @@ class Recorder(threading.Thread):
with session_scope(session=self.get_session()) as session:
dbevent = Events.from_event(event)
session.add(dbevent)
+ session.flush()
if event.event_type == EVENT_STATE_CHANGED:
dbstate = States.from_event(event)
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index 3fffb521d5a..d2afb6076e3 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -9,54 +9,61 @@ from .util import session_scope
_LOGGER = logging.getLogger(__name__)
-def purge_old_data(instance, purge_days):
+def purge_old_data(instance, purge_days, repack):
"""Purge events and states older than purge_days ago."""
from .models import States, Events
from sqlalchemy import func
purge_before = dt_util.utcnow() - timedelta(days=purge_days)
+ _LOGGER.debug("Purging events before %s", purge_before)
with session_scope(session=instance.get_session()) as session:
+ delete_states = session.query(States) \
+ .filter((States.last_updated < purge_before))
+
# For each entity, the most recent state is protected from deletion
# s.t. we can properly restore state even if the entity has not been
# updated in a long time
protected_states = session.query(func.max(States.state_id)) \
.group_by(States.entity_id).all()
- protected_state_ids = tuple((state[0] for state in protected_states))
+ protected_state_ids = tuple(state[0] for state in protected_states)
- deleted_rows = session.query(States) \
- .filter((States.last_updated < purge_before)) \
- .filter(~States.state_id.in_(
- protected_state_ids)) \
- .delete(synchronize_session=False)
+ if protected_state_ids:
+ delete_states = delete_states \
+ .filter(~States.state_id.in_(protected_state_ids))
+
+ deleted_rows = delete_states.delete(synchronize_session=False)
_LOGGER.debug("Deleted %s states", deleted_rows)
+ delete_events = session.query(Events) \
+ .filter((Events.time_fired < purge_before))
+
# We also need to protect the events belonging to the protected states.
# Otherwise, if the SQL server has "ON DELETE CASCADE" as default, it
# will delete the protected state when deleting its associated
# event. Also, we would be producing NULLed foreign keys otherwise.
- protected_events = session.query(States.event_id) \
- .filter(States.state_id.in_(protected_state_ids)) \
- .filter(States.event_id.isnot(None)) \
- .all()
+ if protected_state_ids:
+ protected_events = session.query(States.event_id) \
+ .filter(States.state_id.in_(protected_state_ids)) \
+ .filter(States.event_id.isnot(None)) \
+ .all()
- protected_event_ids = tuple((state[0] for state in protected_events))
+ protected_event_ids = tuple(state[0] for state in protected_events)
- deleted_rows = session.query(Events) \
- .filter((Events.time_fired < purge_before)) \
- .filter(~Events.event_id.in_(
- protected_event_ids
- )) \
- .delete(synchronize_session=False)
+ if protected_event_ids:
+ delete_events = delete_events \
+ .filter(~Events.event_id.in_(protected_event_ids))
+
+ deleted_rows = delete_events.delete(synchronize_session=False)
_LOGGER.debug("Deleted %s events", deleted_rows)
# Execute sqlite vacuum command to free up space on disk
_LOGGER.debug("DB engine driver: %s", instance.engine.driver)
- if instance.engine.driver == 'pysqlite' and not instance.did_vacuum:
+ if repack and instance.engine.driver == 'pysqlite':
from sqlalchemy import exc
- _LOGGER.info("Vacuuming SQLite to free space")
+ _LOGGER.debug("Vacuuming SQLite to free space")
try:
instance.engine.execute("VACUUM")
instance.did_vacuum = True
diff --git a/homeassistant/components/recorder/services.yaml b/homeassistant/components/recorder/services.yaml
index a2a8c9eab8d..512807c9f69 100644
--- a/homeassistant/components/recorder/services.yaml
+++ b/homeassistant/components/recorder/services.yaml
@@ -6,3 +6,6 @@ purge:
keep_days:
description: Number of history days to keep in database after purge. Value >= 0.
example: 2
+ repack:
+ description: Attempt to save disk space by rewriting the entire database file.
+ example: true
diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py
index 39f09ea66a2..25a1a684d3c 100644
--- a/homeassistant/components/remote/harmony.py
+++ b/homeassistant/components/remote/harmony.py
@@ -31,7 +31,7 @@ CONF_DEVICE_CACHE = 'harmony_device_cache'
SERVICE_SYNC = 'harmony_sync'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(ATTR_ACTIVITY, default=None): cv.string,
+ vol.Required(ATTR_ACTIVITY): cv.string,
vol.Required(CONF_NAME): cv.string,
vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS):
vol.Coerce(float),
@@ -207,6 +207,7 @@ class HarmonyRemote(remote.RemoteDevice):
"""Start the PowerOff activity."""
self._client.power_off()
+ # pylint: disable=arguments-differ
def send_command(self, commands, **kwargs):
"""Send a list of commands to one device."""
device = kwargs.get(ATTR_DEVICE)
diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py
index aa9f0c95a7c..a44934d0a39 100644
--- a/homeassistant/components/remote/xiaomi_miio.py
+++ b/homeassistant/components/remote/xiaomi_miio.py
@@ -21,7 +21,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
-REQUIREMENTS = ['python-miio==0.3.6']
+REQUIREMENTS = ['python-miio==0.3.7']
_LOGGER = logging.getLogger(__name__)
@@ -210,8 +210,7 @@ class XiaomiMiioRemote(RemoteDevice):
"""Hide remote by default."""
if self._is_hidden:
return {'hidden': 'true'}
- else:
- return
+ return
# pylint: disable=R0201
@asyncio.coroutine
diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py
index d97d4f38f02..439f938beb3 100644
--- a/homeassistant/components/rflink.py
+++ b/homeassistant/components/rflink.py
@@ -8,9 +8,10 @@ import asyncio
from collections import defaultdict
import functools as ft
import logging
-
import async_timeout
+import voluptuous as vol
+
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_COMMAND, CONF_HOST, CONF_PORT,
EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
@@ -19,7 +20,6 @@ from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import get_deprecated
from homeassistant.helpers.entity import Entity
-import voluptuous as vol
REQUIREMENTS = ['rflink==0.0.34']
@@ -74,7 +74,7 @@ DEVICE_DEFAULTS_SCHEMA = vol.Schema({
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_PORT): vol.Any(cv.port, cv.string),
- vol.Optional(CONF_HOST, default=None): cv.string,
+ vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_WAIT_FOR_ACK, default=True): cv.boolean,
vol.Optional(CONF_RECONNECT_INTERVAL,
default=DEFAULT_RECONNECT_INTERVAL): int,
@@ -175,7 +175,7 @@ def async_setup(hass, config):
hass.data[DATA_DEVICE_REGISTER][event_type], event)
# When connecting to tcp host instead of serial port (optional)
- host = config[DOMAIN][CONF_HOST]
+ host = config[DOMAIN].get(CONF_HOST)
# TCP port when host configured, otherwise serial port
port = config[DOMAIN][CONF_PORT]
diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py
index de8a0c00d80..e7301836d7e 100644
--- a/homeassistant/components/rfxtrx.py
+++ b/homeassistant/components/rfxtrx.py
@@ -188,8 +188,8 @@ def find_possible_pt2262_device(device_id):
for dev_id, device in RFX_DEVICES.items():
if hasattr(device, 'is_lighting4') and len(dev_id) == len(device_id):
size = None
- for i in range(0, len(dev_id)):
- if dev_id[i] != device_id[i]:
+ for i, (char1, char2) in enumerate(zip(dev_id, device_id)):
+ if char1 != char2:
break
size = i
diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py
index 62bd07d2c27..6e70ddb244d 100644
--- a/homeassistant/components/ring.py
+++ b/homeassistant/components/ring.py
@@ -5,13 +5,13 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/ring/
"""
import logging
-import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
-
-from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
-
from requests.exceptions import HTTPError, ConnectTimeout
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
+
REQUIREMENTS = ['ring_doorbell==0.1.8']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py
index 067db1f93a3..db81d84c2b7 100644
--- a/homeassistant/components/scene/deconz.py
+++ b/homeassistant/components/scene/deconz.py
@@ -6,7 +6,8 @@ https://home-assistant.io/components/scene.deconz/
"""
import asyncio
-from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
+from homeassistant.components.deconz import (
+ DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
from homeassistant.components.scene import Scene
DEPENDENCIES = ['deconz']
@@ -18,7 +19,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if discovery_info is None:
return
- scenes = hass.data[DECONZ_DATA].scenes
+ scenes = hass.data[DATA_DECONZ].scenes
entities = []
for scene in scenes.values():
@@ -34,7 +35,12 @@ class DeconzScene(Scene):
self._scene = scene
@asyncio.coroutine
- def async_activate(self, **kwargs):
+ def async_added_to_hass(self):
+ """Subscribe to sensors events."""
+ self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._scene.deconz_id
+
+ @asyncio.coroutine
+ def async_activate(self):
"""Activate the scene."""
yield from self._scene.async_set_state({})
diff --git a/homeassistant/components/scene/litejet.py b/homeassistant/components/scene/litejet.py
index 432ce060774..37fb58d8dc7 100644
--- a/homeassistant/components/scene/litejet.py
+++ b/homeassistant/components/scene/litejet.py
@@ -42,11 +42,6 @@ class LiteJetScene(Scene):
"""Return the name of the scene."""
return self._name
- @property
- def should_poll(self):
- """Return that polling is not necessary."""
- return False
-
@property
def device_state_attributes(self):
"""Return the device-specific state attributes."""
@@ -54,6 +49,6 @@ class LiteJetScene(Scene):
ATTR_NUMBER: self._index
}
- def activate(self, **kwargs):
+ def activate(self):
"""Activate the scene."""
self._lj.activate_scene(self._index)
diff --git a/homeassistant/components/scene/lutron_caseta.py b/homeassistant/components/scene/lutron_caseta.py
index 53df0da7617..0d9024d194e 100644
--- a/homeassistant/components/scene/lutron_caseta.py
+++ b/homeassistant/components/scene/lutron_caseta.py
@@ -42,17 +42,7 @@ class LutronCasetaScene(Scene):
"""Return the name of the scene."""
return self._scene_name
- @property
- def should_poll(self):
- """Return that polling is not necessary."""
- return False
-
- @property
- def is_on(self):
- """There is no way of detecting if a scene is active (yet)."""
- return False
-
@asyncio.coroutine
- def async_activate(self, **kwargs):
+ def async_activate(self):
"""Activate the scene."""
self._bridge.activate_scene(self._scene_id)
diff --git a/homeassistant/components/scene/tahoma.py b/homeassistant/components/scene/tahoma.py
new file mode 100644
index 00000000000..39206623901
--- /dev/null
+++ b/homeassistant/components/scene/tahoma.py
@@ -0,0 +1,48 @@
+"""
+Support for Tahoma scenes.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/scene.tahoma/
+"""
+import logging
+
+from homeassistant.components.scene import Scene
+from homeassistant.components.tahoma import (
+ DOMAIN as TAHOMA_DOMAIN)
+
+DEPENDENCIES = ['tahoma']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Tahoma scenes."""
+ controller = hass.data[TAHOMA_DOMAIN]['controller']
+ scenes = []
+ for scene in hass.data[TAHOMA_DOMAIN]['scenes']:
+ scenes.append(TahomaScene(scene, controller))
+ add_devices(scenes, True)
+
+
+class TahomaScene(Scene):
+ """Representation of a Tahoma scene entity."""
+
+ def __init__(self, tahoma_scene, controller):
+ """Initialize the scene."""
+ self.tahoma_scene = tahoma_scene
+ self.controller = controller
+ self._name = self.tahoma_scene.name
+
+ def activate(self):
+ """Activate the scene."""
+ self.controller.launch_action_group(self.tahoma_scene.oid)
+
+ @property
+ def name(self):
+ """Return the name of the scene."""
+ return self._name
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the scene."""
+ return {'tahoma_scene_oid': self.tahoma_scene.oid}
diff --git a/homeassistant/components/scene/velux.py b/homeassistant/components/scene/velux.py
index 9da7a662117..86d71153a2b 100644
--- a/homeassistant/components/scene/velux.py
+++ b/homeassistant/components/scene/velux.py
@@ -4,6 +4,8 @@ Support for VELUX scenes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.velux/
"""
+import asyncio
+
from homeassistant.components.scene import Scene
from homeassistant.components.velux import _LOGGER, DATA_VELUX
@@ -11,26 +13,22 @@ from homeassistant.components.velux import _LOGGER, DATA_VELUX
DEPENDENCIES = ['velux']
-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 scenes for velux platform."""
- if DATA_VELUX not in hass.data \
- or not hass.data[DATA_VELUX].initialized:
- return False
-
entities = []
for scene in hass.data[DATA_VELUX].pyvlx.scenes:
- entities.append(VeluxScene(hass, scene))
- add_devices(entities)
- return True
+ entities.append(VeluxScene(scene))
+ async_add_devices(entities)
class VeluxScene(Scene):
"""Representation of a velux scene."""
- def __init__(self, hass, scene):
+ def __init__(self, scene):
"""Init velux scene."""
_LOGGER.info("Adding VELUX scene: %s", scene)
- self.hass = hass
self.scene = scene
@property
@@ -38,16 +36,7 @@ class VeluxScene(Scene):
"""Return the name of the scene."""
return self.scene.name
- @property
- def should_poll(self):
- """Return that polling is not necessary."""
- return False
-
- @property
- def is_on(self):
- """There is no way of detecting if a scene is active (yet)."""
- return False
-
- def activate(self, **kwargs):
+ @asyncio.coroutine
+ def async_activate(self):
"""Activate the scene."""
- self.hass.async_add_job(self.scene.run())
+ yield from self.scene.run()
diff --git a/homeassistant/components/scene/vera.py b/homeassistant/components/scene/vera.py
index 3dbb68d214f..4f580356fbb 100644
--- a/homeassistant/components/scene/vera.py
+++ b/homeassistant/components/scene/vera.py
@@ -40,7 +40,7 @@ class VeraScene(Scene):
"""Update the scene status."""
self.vera_scene.refresh()
- def activate(self, **kwargs):
+ def activate(self):
"""Activate the scene."""
self.vera_scene.activate()
@@ -53,8 +53,3 @@ class VeraScene(Scene):
def device_state_attributes(self):
"""Return the state attributes of the scene."""
return {'vera_scene_id': self.vera_scene.vera_scene_id}
-
- @property
- def should_poll(self):
- """Return that polling is not necessary."""
- return False
diff --git a/homeassistant/components/scene/wink.py b/homeassistant/components/scene/wink.py
index 2d4a6d0621c..5bd053bdd39 100644
--- a/homeassistant/components/scene/wink.py
+++ b/homeassistant/components/scene/wink.py
@@ -38,11 +38,6 @@ class WinkScene(WinkDevice, Scene):
"""Call when entity is added to hass."""
self.hass.data[DOMAIN]['entities']['scene'].append(self)
- @property
- def is_on(self):
- """Python-wink will always return False."""
- return self.wink.state()
-
- def activate(self, **kwargs):
+ def activate(self):
"""Activate the scene."""
self.wink.activate()
diff --git a/homeassistant/components/sensor/airvisual.py b/homeassistant/components/sensor/airvisual.py
index 5ea24dab823..d67415fc65e 100644
--- a/homeassistant/components/sensor/airvisual.py
+++ b/homeassistant/components/sensor/airvisual.py
@@ -19,7 +19,6 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
REQUIREMENTS = ['pyairvisual==1.0.0']
-
_LOGGER = getLogger(__name__)
ATTR_CITY = 'city'
@@ -27,7 +26,6 @@ ATTR_COUNTRY = 'country'
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
ATTR_POLLUTANT_UNIT = 'pollutant_unit'
ATTR_REGION = 'region'
-ATTR_TIMESTAMP = 'timestamp'
CONF_CITY = 'city'
CONF_COUNTRY = 'country'
@@ -39,6 +37,12 @@ VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
+SENSOR_TYPES = [
+ ('AirPollutionLevelSensor', 'Air Pollution Level', 'mdi:scale'),
+ ('AirQualityIndexSensor', 'Air Quality Index', 'mdi:format-list-numbers'),
+ ('MainPollutantSensor', 'Main Pollutant', 'mdi:chemical-weapon'),
+]
+
POLLUTANT_LEVEL_MAPPING = [
{'label': 'Good', 'minimum': 0, 'maximum': 50},
{'label': 'Moderate', 'minimum': 51, 'maximum': 100},
@@ -58,11 +62,6 @@ POLLUTANT_MAPPING = {
}
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
-SENSOR_TYPES = [
- ('AirPollutionLevelSensor', 'Air Pollution Level', 'mdi:scale'),
- ('AirQualityIndexSensor', 'Air Quality Index', 'mdi:format-list-numbers'),
- ('MainPollutantSensor', 'Main Pollutant', 'mdi:chemical-weapon'),
-]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
@@ -80,7 +79,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Configure the platform and add the sensors."""
- import pyairvisual as pav
+ from pyairvisual import Client
+
+ classes = {
+ 'AirPollutionLevelSensor': AirPollutionLevelSensor,
+ 'AirQualityIndexSensor': AirQualityIndexSensor,
+ 'MainPollutantSensor': MainPollutantSensor
+ }
api_key = config.get(CONF_API_KEY)
monitored_locales = config.get(CONF_MONITORED_CONDITIONS)
@@ -95,60 +100,63 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if city and state and country:
_LOGGER.debug(
"Using city, state, and country: %s, %s, %s", city, state, country)
+ location_id = ','.join((city, state, country))
data = AirVisualData(
- pav.Client(api_key), city=city, state=state, country=country,
+ Client(api_key), city=city, state=state, country=country,
show_on_map=show_on_map)
else:
_LOGGER.debug(
"Using latitude and longitude: %s, %s", latitude, longitude)
+ location_id = ','.join((str(latitude), str(longitude)))
data = AirVisualData(
- pav.Client(api_key), latitude=latitude, longitude=longitude,
+ Client(api_key), latitude=latitude, longitude=longitude,
radius=radius, show_on_map=show_on_map)
data.update()
+
sensors = []
for locale in monitored_locales:
for sensor_class, name, icon in SENSOR_TYPES:
- sensors.append(globals()[sensor_class](data, name, icon, locale))
+ sensors.append(classes[sensor_class](
+ data,
+ name,
+ icon,
+ locale,
+ location_id
+ ))
add_devices(sensors, True)
-def merge_two_dicts(dict1, dict2):
- """Merge two dicts into a new dict as a shallow copy."""
- final = dict1.copy()
- final.update(dict2)
- return final
-
-
class AirVisualBaseSensor(Entity):
"""Define a base class for all of our sensors."""
- def __init__(self, data, name, icon, locale):
+ def __init__(self, data, name, icon, locale, entity_id):
"""Initialize the sensor."""
self.data = data
+ self._attrs = {}
self._icon = icon
self._locale = locale
self._name = name
self._state = None
+ self._entity_id = entity_id
self._unit = None
@property
def device_state_attributes(self):
"""Return the device state attributes."""
- attrs = merge_two_dicts({
+ self._attrs.update({
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
- 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
+ self._attrs[ATTR_LATITUDE] = self.data.latitude
+ self._attrs[ATTR_LONGITUDE] = self.data.longitude
else:
- attrs['lati'] = self.data.latitude
- attrs['long'] = self.data.longitude
+ self._attrs['lati'] = self.data.latitude
+ self._attrs['long'] = self.data.longitude
- return attrs
+ return self._attrs
@property
def icon(self):
@@ -169,6 +177,11 @@ class AirVisualBaseSensor(Entity):
class AirPollutionLevelSensor(AirVisualBaseSensor):
"""Define a sensor to measure air pollution level."""
+ @property
+ def unique_id(self):
+ """Return a unique, HASS-friendly identifier for this entity."""
+ return '{0}_pollution_level'.format(self._entity_id)
+
def update(self):
"""Update the status of the sensor."""
self.data.update()
@@ -189,6 +202,11 @@ class AirPollutionLevelSensor(AirVisualBaseSensor):
class AirQualityIndexSensor(AirVisualBaseSensor):
"""Define a sensor to measure AQI."""
+ @property
+ def unique_id(self):
+ """Return a unique, HASS-friendly identifier for this entity."""
+ return '{0}_aqi'.format(self._entity_id)
+
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
@@ -205,19 +223,16 @@ class AirQualityIndexSensor(AirVisualBaseSensor):
class MainPollutantSensor(AirVisualBaseSensor):
"""Define a sensor to the main pollutant of an area."""
- def __init__(self, data, name, icon, locale):
+ def __init__(self, data, name, icon, locale, entity_id):
"""Initialize the sensor."""
- super().__init__(data, name, icon, locale)
+ super().__init__(data, name, icon, locale, entity_id)
self._symbol = None
self._unit = None
@property
- def device_state_attributes(self):
- """Return the device state attributes."""
- return merge_two_dicts(super().device_state_attributes, {
- ATTR_POLLUTANT_SYMBOL: self._symbol,
- ATTR_POLLUTANT_UNIT: self._unit
- })
+ def unique_id(self):
+ """Return a unique, HASS-friendly identifier for this entity."""
+ return '{0}_main_pollutant'.format(self._entity_id)
def update(self):
"""Update the status of the sensor."""
@@ -229,6 +244,11 @@ class MainPollutantSensor(AirVisualBaseSensor):
self._unit = pollution_info.get('unit')
self._symbol = symbol
+ self._attrs.update({
+ ATTR_POLLUTANT_SYMBOL: self._symbol,
+ ATTR_POLLUTANT_UNIT: self._unit
+ })
+
class AirVisualData(object):
"""Define an object to hold sensor data."""
@@ -252,7 +272,7 @@ class AirVisualData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update with new AirVisual data."""
- import pyairvisual.exceptions as exceptions
+ from pyairvisual.exceptions import HTTPError
try:
if self.city and self.state and self.country:
@@ -272,7 +292,7 @@ class AirVisualData(object):
ATTR_REGION: resp.get('state'),
ATTR_COUNTRY: resp.get('country')
}
- except exceptions.HTTPError as exc_info:
+ except HTTPError as exc_info:
_LOGGER.error("Unable to retrieve data on this location: %s",
self.__dict__)
_LOGGER.debug(exc_info)
diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py
index 6b224492ffb..81c84a7f918 100644
--- a/homeassistant/components/sensor/alpha_vantage.py
+++ b/homeassistant/components/sensor/alpha_vantage.py
@@ -15,7 +15,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['alpha_vantage==1.8.0']
+REQUIREMENTS = ['alpha_vantage==1.9.0']
_LOGGER = logging.getLogger(__name__)
@@ -31,25 +31,13 @@ CONF_SYMBOL = 'symbol'
CONF_SYMBOLS = 'symbols'
CONF_TO = 'to'
-DEFAULT_SYMBOL = {
- CONF_CURRENCY: 'USD',
- CONF_NAME: 'Google',
- CONF_SYMBOL: 'GOOGL',
-}
-
-DEFAULT_CURRENCY = {
- CONF_FROM: 'BTC',
- CONF_NAME: 'Bitcon',
- CONF_TO: 'USD',
-}
-
ICONS = {
'BTC': 'mdi:currency-btc',
'EUR': 'mdi:currency-eur',
'GBP': 'mdi:currency-gbp',
'INR': 'mdi:currency-inr',
'RUB': 'mdi:currency-rub',
- 'TRY': 'mdi: currency-try',
+ 'TRY': 'mdi:currency-try',
'USD': 'mdi:currency-usd',
}
@@ -69,9 +57,9 @@ CURRENCY_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
- vol.Optional(CONF_FOREIGN_EXCHANGE, default=[DEFAULT_CURRENCY]):
+ vol.Optional(CONF_FOREIGN_EXCHANGE):
vol.All(cv.ensure_list, [CURRENCY_SCHEMA]),
- vol.Optional(CONF_SYMBOLS, default=[DEFAULT_SYMBOL]):
+ vol.Optional(CONF_SYMBOLS):
vol.All(cv.ensure_list, [SYMBOL_SCHEMA]),
})
@@ -83,6 +71,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
api_key = config.get(CONF_API_KEY)
symbols = config.get(CONF_SYMBOLS)
+ conversions = config.get(CONF_FOREIGN_EXCHANGE)
+
+ if not symbols and not conversions:
+ msg = 'Warning: No symbols or currencies configured.'
+ hass.components.persistent_notification.create(
+ msg, 'Sensor alpha_vantage')
+ _LOGGER.warning(msg)
+ return
timeseries = TimeSeries(key=api_key)
@@ -98,12 +94,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev.append(AlphaVantageSensor(timeseries, symbol))
forex = ForeignExchange(key=api_key)
- for conversion in config.get(CONF_FOREIGN_EXCHANGE):
+ for conversion in conversions:
from_cur = conversion.get(CONF_FROM)
to_cur = conversion.get(CONF_TO)
try:
- _LOGGER.debug("Configuring forex %s - %s",
- from_cur, to_cur)
+ _LOGGER.debug("Configuring forex %s - %s", from_cur, to_cur)
forex.get_currency_exchange_rate(
from_currency=from_cur, to_currency=to_cur)
except ValueError as error:
@@ -218,10 +213,8 @@ class AlphaVantageForeignExchange(Entity):
def update(self):
"""Get the latest data and updates the states."""
_LOGGER.debug("Requesting new data for forex %s - %s",
- self._from_currency,
- self._to_currency)
+ self._from_currency, self._to_currency)
self.values, _ = self._foreign_exchange.get_currency_exchange_rate(
from_currency=self._from_currency, to_currency=self._to_currency)
_LOGGER.debug("Received new data for forex %s - %s",
- self._from_currency,
- self._to_currency)
+ self._from_currency, self._to_currency)
diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py
index 470d7749ea2..2dbda26ac32 100644
--- a/homeassistant/components/sensor/bme680.py
+++ b/homeassistant/components/sensor/bme680.py
@@ -116,7 +116,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return
-# pylint: disable=import-error
+# pylint: disable=import-error, no-member
def _setup_bme680(config):
"""Set up and configure the BME680 sensor."""
from smbus import SMBus
diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py
new file mode 100644
index 00000000000..26bfd19e6fc
--- /dev/null
+++ b/homeassistant/components/sensor/bmw_connected_drive.py
@@ -0,0 +1,99 @@
+"""
+Reads vehicle status from BMW connected drive portal.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.bmw_connected_drive/
+"""
+import logging
+import asyncio
+
+from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
+from homeassistant.helpers.entity import Entity
+
+DEPENDENCIES = ['bmw_connected_drive']
+
+_LOGGER = logging.getLogger(__name__)
+
+LENGTH_ATTRIBUTES = [
+ 'remaining_range_fuel',
+ 'mileage',
+ ]
+
+VALID_ATTRIBUTES = LENGTH_ATTRIBUTES + [
+ 'remaining_fuel',
+]
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the BMW sensors."""
+ accounts = hass.data[BMW_DOMAIN]
+ _LOGGER.debug('Found BMW accounts: %s',
+ ', '.join([a.name for a in accounts]))
+ devices = []
+ for account in accounts:
+ for vehicle in account.account.vehicles:
+ for sensor in VALID_ATTRIBUTES:
+ device = BMWConnectedDriveSensor(account, vehicle, sensor)
+ devices.append(device)
+ add_devices(devices)
+
+
+class BMWConnectedDriveSensor(Entity):
+ """Representation of a BMW vehicle sensor."""
+
+ def __init__(self, account, vehicle, attribute: str):
+ """Constructor."""
+ self._vehicle = vehicle
+ self._account = account
+ self._attribute = attribute
+ self._state = None
+ self._unit_of_measurement = None
+ self._name = '{} {}'.format(self._vehicle.modelName, self._attribute)
+
+ @property
+ def should_poll(self) -> bool:
+ """Data update is triggered from BMWConnectedDriveEntity."""
+ return False
+
+ @property
+ def name(self) -> str:
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor.
+
+ The return type of this call depends on the attribute that
+ is configured.
+ """
+ return self._state
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Get the unit of measurement."""
+ return self._unit_of_measurement
+
+ def update(self) -> None:
+ """Read new state data from the library."""
+ _LOGGER.debug('Updating %s', self.entity_id)
+ vehicle_state = self._vehicle.state
+ self._state = getattr(vehicle_state, self._attribute)
+
+ if self._attribute in LENGTH_ATTRIBUTES:
+ self._unit_of_measurement = vehicle_state.unit_of_length
+ elif self._attribute == 'remaining_fuel':
+ self._unit_of_measurement = vehicle_state.unit_of_volume
+ else:
+ self._unit_of_measurement = None
+
+ self.schedule_update_ha_state()
+
+ @asyncio.coroutine
+ def async_added_to_hass(self):
+ """Add callback after being added to hass.
+
+ Show latest data after startup.
+ """
+ self._account.add_update_listener(self.update)
+ yield from self.hass.async_add_job(self.update)
diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py
index 545bef12d83..272d5d1e0b8 100644
--- a/homeassistant/components/sensor/bom.py
+++ b/homeassistant/components/sensor/bom.py
@@ -88,7 +88,7 @@ def validate_station(station):
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Inclusive(CONF_ZONE_ID, 'Deprecated partial station ID'): cv.string,
vol.Inclusive(CONF_WMO_ID, 'Deprecated partial station ID'): cv.string,
- vol.Optional(CONF_NAME, default=None): cv.string,
+ vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_STATION): validate_station,
vol.Required(CONF_MONITORED_CONDITIONS, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py
index 1b5cfc4b491..5d74f038eaa 100644
--- a/homeassistant/components/sensor/buienradar.py
+++ b/homeassistant/components/sensor/buienradar.py
@@ -23,7 +23,7 @@ from homeassistant.helpers.event import (
async_track_point_in_utc_time)
from homeassistant.util import dt as dt_util
-REQUIREMENTS = ['buienradar==0.9']
+REQUIREMENTS = ['buienradar==0.91']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/cups.py b/homeassistant/components/sensor/cups.py
index f4d826c250d..7c1d9fc3d49 100644
--- a/homeassistant/components/sensor/cups.py
+++ b/homeassistant/components/sensor/cups.py
@@ -128,7 +128,7 @@ class CupsSensor(Entity):
self._printer = self.data.printers.get(self._name)
-# pylint: disable=import-error
+# pylint: disable=import-error, no-name-in-module
class CupsData(object):
"""Get the latest data from CUPS and update the state."""
diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py
index 0b2f6495b45..e045043e09c 100644
--- a/homeassistant/components/sensor/daikin.py
+++ b/homeassistant/components/sensor/daikin.py
@@ -23,8 +23,8 @@ from homeassistant.util.unit_system import UnitSystem
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
- vol.Optional(CONF_NAME, default=None): cv.string,
- vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES.keys()):
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py
index b3adaa412ff..b60df1c6ac9 100644
--- a/homeassistant/components/sensor/deconz.py
+++ b/homeassistant/components/sensor/deconz.py
@@ -6,7 +6,8 @@ https://home-assistant.io/components/sensor.deconz/
"""
import asyncio
-from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
+from homeassistant.components.deconz import (
+ DOMAIN as DATA_DECONZ, DATA_DECONZ_ID)
from homeassistant.const import ATTR_BATTERY_LEVEL, CONF_EVENT, CONF_ID
from homeassistant.core import EventOrigin, callback
from homeassistant.helpers.entity import Entity
@@ -25,7 +26,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return
from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE
- sensors = hass.data[DECONZ_DATA].sensors
+ sensors = hass.data[DATA_DECONZ].sensors
entities = []
for key in sorted(sensors.keys(), key=int):
@@ -51,6 +52,7 @@ class DeconzSensor(Entity):
def async_added_to_hass(self):
"""Subscribe to sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
+ self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._sensor.deconz_id
@callback
def async_update_callback(self, reason):
@@ -127,6 +129,7 @@ class DeconzBattery(Entity):
def async_added_to_hass(self):
"""Subscribe to sensors events."""
self._device.register_async_callback(self.async_update_callback)
+ self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._device.deconz_id
@callback
def async_update_callback(self, reason):
diff --git a/homeassistant/components/sensor/dovado.py b/homeassistant/components/sensor/dovado.py
index eba6596efc4..ee2292d4122 100644
--- a/homeassistant/components/sensor/dovado.py
+++ b/homeassistant/components/sensor/dovado.py
@@ -79,7 +79,7 @@ class Dovado:
def send_sms(service):
"""Send SMS through the router."""
- number = service.data.get('number'),
+ number = service.data.get('number')
message = service.data.get('message')
_LOGGER.debug("message for %s: %s", number, message)
self._dovado.send_sms(number, message)
diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py
index 32c888bad3b..cea29d437ae 100644
--- a/homeassistant/components/sensor/dsmr.py
+++ b/homeassistant/components/sensor/dsmr.py
@@ -9,13 +9,14 @@ from datetime import timedelta
from functools import partial
import logging
+import voluptuous as vol
+
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN)
from homeassistant.core import CoreState
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-import voluptuous as vol
_LOGGER = logging.getLogger(__name__)
@@ -38,7 +39,7 @@ RECONNECT_INTERVAL = 5
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string,
- vol.Optional(CONF_HOST, default=None): cv.string,
+ vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All(
cv.string, vol.In(['5', '4', '2.2'])),
vol.Optional(CONF_RECONNECT_INTERVAL, default=30): int,
@@ -95,7 +96,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
# Creates an asyncio.Protocol factory for reading DSMR telegrams from
# serial and calls update_entities_telegram to update entities on arrival
- if config[CONF_HOST]:
+ if CONF_HOST in config:
reader_factory = partial(
create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT],
config[CONF_DSMR_VERSION], update_entities_telegram,
diff --git a/homeassistant/components/sensor/dwd_weather_warnings.py b/homeassistant/components/sensor/dwd_weather_warnings.py
index 0eeaa9424e8..9105e30eb42 100644
--- a/homeassistant/components/sensor/dwd_weather_warnings.py
+++ b/homeassistant/components/sensor/dwd_weather_warnings.py
@@ -47,8 +47,9 @@ MONITORED_CONDITIONS = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_REGION_NAME): 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)]),
+ vol.Optional(CONF_MONITORED_CONDITIONS,
+ default=list(MONITORED_CONDITIONS)):
+ vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]),
})
@@ -137,11 +138,11 @@ class DwdWeatherWarningsSensor(Entity):
data['warning_{}_name'.format(i)] = event['event']
data['warning_{}_level'.format(i)] = event['level']
data['warning_{}_type'.format(i)] = event['type']
- if len(event['headline']) > 0:
+ if event['headline']:
data['warning_{}_headline'.format(i)] = event['headline']
- if len(event['description']) > 0:
+ if event['description']:
data['warning_{}_description'.format(i)] = event['description']
- if len(event['instruction']) > 0:
+ if event['instruction']:
data['warning_{}_instruction'.format(i)] = event['instruction']
if event['start'] is not None:
diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py
index ce5e2a81939..b11dae8e168 100644
--- a/homeassistant/components/sensor/envirophat.py
+++ b/homeassistant/components/sensor/envirophat.py
@@ -45,7 +45,7 @@ SENSOR_TYPES = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_TYPES):
+ vol.Required(CONF_DISPLAY_OPTIONS, default=list(SENSOR_TYPES)):
[vol.In(SENSOR_TYPES)],
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_USE_LEDS, default=False): cv.boolean
diff --git a/homeassistant/components/sensor/fail2ban.py b/homeassistant/components/sensor/fail2ban.py
index a343a59c314..87c301d34f5 100644
--- a/homeassistant/components/sensor/fail2ban.py
+++ b/homeassistant/components/sensor/fail2ban.py
@@ -33,9 +33,8 @@ STATE_CURRENT_BANS = 'current_bans'
STATE_ALL_BANS = 'total_bans'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(CONF_JAILS, default=[]):
- vol.All(cv.ensure_list, vol.Length(min=1)),
- vol.Optional(CONF_FILE_PATH, default=DEFAULT_LOG): cv.isfile,
+ vol.Required(CONF_JAILS): vol.All(cv.ensure_list, vol.Length(min=1)),
+ vol.Optional(CONF_FILE_PATH): cv.isfile,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
@@ -46,7 +45,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
name = config.get(CONF_NAME)
jails = config.get(CONF_JAILS)
scan_interval = config.get(CONF_SCAN_INTERVAL)
- log_file = config.get(CONF_FILE_PATH)
+ log_file = config.get(CONF_FILE_PATH, DEFAULT_LOG)
device_list = []
log_parser = BanLogParser(scan_interval, log_file)
diff --git a/homeassistant/components/sensor/fastdotcom.py b/homeassistant/components/sensor/fastdotcom.py
index 02dd32c20af..9143ccaf23f 100644
--- a/homeassistant/components/sensor/fastdotcom.py
+++ b/homeassistant/components/sensor/fastdotcom.py
@@ -26,6 +26,8 @@ CONF_HOUR = 'hour'
CONF_DAY = 'day'
CONF_MANUAL = 'manual'
+ICON = 'mdi:speedometer'
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SECOND, default=[0]):
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]),
@@ -94,6 +96,11 @@ class SpeedtestSensor(Entity):
return
self._state = state.state
+ @property
+ def icon(self):
+ """Return icon."""
+ return ICON
+
class SpeedtestData(object):
"""Get the latest data from fast.com."""
diff --git a/homeassistant/components/sensor/fedex.py b/homeassistant/components/sensor/fedex.py
index 7991a94eb05..0c42ef28496 100644
--- a/homeassistant/components/sensor/fedex.py
+++ b/homeassistant/components/sensor/fedex.py
@@ -19,7 +19,7 @@ from homeassistant.util import Throttle
from homeassistant.util.dt import now, parse_date
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['fedexdeliverymanager==1.0.4']
+REQUIREMENTS = ['fedexdeliverymanager==1.0.5']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/filesize.py b/homeassistant/components/sensor/filesize.py
new file mode 100644
index 00000000000..a5a65f9bb5e
--- /dev/null
+++ b/homeassistant/components/sensor/filesize.py
@@ -0,0 +1,92 @@
+"""
+Sensor for monitoring the size of a file.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.filesize/
+"""
+import datetime
+import logging
+import os
+
+import voluptuous as vol
+
+from homeassistant.helpers.entity import Entity
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+
+_LOGGER = logging.getLogger(__name__)
+
+
+CONF_FILE_PATHS = 'file_paths'
+ICON = 'mdi:file'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_FILE_PATHS):
+ vol.All(cv.ensure_list, [cv.isfile]),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the file size sensor."""
+ sensors = []
+ for path in config.get(CONF_FILE_PATHS):
+ if not hass.config.is_allowed_path(path):
+ _LOGGER.error(
+ "Filepath %s is not valid or allowed", path)
+ continue
+ else:
+ sensors.append(Filesize(path))
+
+ if sensors:
+ add_devices(sensors, True)
+
+
+class Filesize(Entity):
+ """Encapsulates file size information."""
+
+ def __init__(self, path):
+ """Initialize the data object."""
+ self._path = path # Need to check its a valid path
+ self._size = None
+ self._last_updated = None
+ self._name = path.split("/")[-1]
+ self._unit_of_measurement = 'MB'
+
+ def update(self):
+ """Update the sensor."""
+ statinfo = os.stat(self._path)
+ self._size = statinfo.st_size
+ last_updated = datetime.datetime.fromtimestamp(statinfo.st_mtime)
+ self._last_updated = last_updated.isoformat()
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the size of the file in MB."""
+ decimals = 2
+ state_mb = round(self._size/1e6, decimals)
+ return state_mb
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return ICON
+
+ @property
+ def device_state_attributes(self):
+ """Return other details about the sensor state."""
+ attr = {
+ 'path': self._path,
+ 'last_updated': self._last_updated,
+ 'bytes': self._size,
+ }
+ return attr
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return self._unit_of_measurement
diff --git a/homeassistant/components/sensor/folder.py b/homeassistant/components/sensor/folder.py
new file mode 100644
index 00000000000..bd3957a36ca
--- /dev/null
+++ b/homeassistant/components/sensor/folder.py
@@ -0,0 +1,108 @@
+"""
+Sensor for monitoring the contents of a folder.
+
+For more details about this platform, refer to the documentation at
+https://home-assistant.io/components/sensor.folder/
+"""
+from datetime import timedelta
+import glob
+import logging
+import os
+
+import voluptuous as vol
+
+from homeassistant.helpers.entity import Entity
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_FOLDER_PATHS = 'folder'
+CONF_FILTER = 'filter'
+DEFAULT_FILTER = '*'
+
+SCAN_INTERVAL = timedelta(seconds=1)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_FOLDER_PATHS): cv.isdir,
+ vol.Optional(CONF_FILTER, default=DEFAULT_FILTER): cv.string,
+})
+
+
+def get_files_list(folder_path, filter_term):
+ """Return the list of files, applying filter."""
+ query = folder_path + filter_term
+ files_list = glob.glob(query)
+ return files_list
+
+
+def get_size(files_list):
+ """Return the sum of the size in bytes of files in the list."""
+ size_list = [os.stat(f).st_size for f in files_list]
+ return sum(size_list)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the folder sensor."""
+ path = config.get(CONF_FOLDER_PATHS)
+
+ if not hass.config.is_allowed_path(path):
+ _LOGGER.error("folder %s is not valid or allowed", path)
+ else:
+ folder = Folder(path, config.get(CONF_FILTER))
+ add_devices([folder], True)
+
+
+class Folder(Entity):
+ """Representation of a folder."""
+
+ ICON = 'mdi:folder'
+
+ def __init__(self, folder_path, filter_term):
+ """Initialize the data object."""
+ folder_path = os.path.join(folder_path, '') # If no trailing / add it
+ self._folder_path = folder_path # Need to check its a valid path
+ self._filter_term = filter_term
+ self._number_of_files = None
+ self._size = None
+ self._name = os.path.split(os.path.split(folder_path)[0])[1]
+ self._unit_of_measurement = 'MB'
+
+ def update(self):
+ """Update the sensor."""
+ files_list = get_files_list(self._folder_path, self._filter_term)
+ self._number_of_files = len(files_list)
+ self._size = get_size(files_list)
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ decimals = 2
+ size_mb = round(self._size/1e6, decimals)
+ return size_mb
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return self.ICON
+
+ @property
+ def device_state_attributes(self):
+ """Return other details about the sensor state."""
+ attr = {
+ 'path': self._folder_path,
+ 'filter': self._filter_term,
+ 'number_of_files': self._number_of_files,
+ 'bytes': self._size,
+ }
+ return attr
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return self._unit_of_measurement
diff --git a/homeassistant/components/sensor/fritzbox_netmonitor.py b/homeassistant/components/sensor/fritzbox_netmonitor.py
index c7486b56c25..f4f774cad1e 100644
--- a/homeassistant/components/sensor/fritzbox_netmonitor.py
+++ b/homeassistant/components/sensor/fritzbox_netmonitor.py
@@ -6,6 +6,7 @@ https://home-assistant.io/components/sensor.fritzbox_netmonitor/
"""
import logging
from datetime import timedelta
+from requests.exceptions import RequestException
import voluptuous as vol
@@ -15,8 +16,6 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
-from requests.exceptions import RequestException
-
REQUIREMENTS = ['fritzconnection==0.6.5']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/google_wifi.py b/homeassistant/components/sensor/google_wifi.py
index d377c03d710..c070a3e990f 100644
--- a/homeassistant/components/sensor/google_wifi.py
+++ b/homeassistant/components/sensor/google_wifi.py
@@ -69,8 +69,9 @@ MONITORED_CONDITIONS = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
- vol.Optional(CONF_MONITORED_CONDITIONS, default=MONITORED_CONDITIONS):
- vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]),
+ vol.Optional(CONF_MONITORED_CONDITIONS,
+ default=list(MONITORED_CONDITIONS)):
+ vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
diff --git a/homeassistant/components/sensor/gtfs.py b/homeassistant/components/sensor/gtfs.py
index 9aa9f14663c..616144d2bc6 100644
--- a/homeassistant/components/sensor/gtfs.py
+++ b/homeassistant/components/sensor/gtfs.py
@@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DESTINATION): cv.string,
vol.Required(CONF_DATA): cv.string,
vol.Optional(CONF_NAME): cv.string,
- vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)):
- cv.time_period_dict,
+ vol.Optional(CONF_OFFSET, default=0): cv.time_period,
})
diff --git a/homeassistant/components/sensor/history_stats.py b/homeassistant/components/sensor/history_stats.py
index 175bdafd4a9..de7b7ebaf9e 100644
--- a/homeassistant/components/sensor/history_stats.py
+++ b/homeassistant/components/sensor/history_stats.py
@@ -49,13 +49,7 @@ ATTR_VALUE = 'value'
def exactly_two_period_keys(conf):
"""Ensure exactly 2 of CONF_PERIOD_KEYS are provided."""
- provided = 0
-
- for param in CONF_PERIOD_KEYS:
- if param in conf and conf[param] is not None:
- provided += 1
-
- if provided != 2:
+ if sum(param in conf for param in CONF_PERIOD_KEYS) != 2:
raise vol.Invalid('You must provide exactly 2 of the following:'
' start, end, duration')
return conf
@@ -64,9 +58,9 @@ def exactly_two_period_keys(conf):
PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE): cv.string,
- vol.Optional(CONF_START, default=None): cv.template,
- vol.Optional(CONF_END, default=None): cv.template,
- vol.Optional(CONF_DURATION, default=None): cv.time_period,
+ vol.Optional(CONF_START): cv.template,
+ vol.Optional(CONF_END): cv.template,
+ vol.Optional(CONF_DURATION): cv.time_period,
vol.Optional(CONF_TYPE, default=CONF_TYPE_TIME): vol.In(CONF_TYPE_KEYS),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}), exactly_two_period_keys)
diff --git a/homeassistant/components/sensor/hp_ilo.py b/homeassistant/components/sensor/hp_ilo.py
index 016d68b3b0e..922ed04a8d9 100644
--- a/homeassistant/components/sensor/hp_ilo.py
+++ b/homeassistant/components/sensor/hp_ilo.py
@@ -51,8 +51,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_SENSOR_TYPE):
vol.All(cv.string, vol.In(SENSOR_TYPES)),
- vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=None): cv.string,
- vol.Optional(CONF_VALUE_TEMPLATE, default=None): cv.template
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template
})]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
@@ -85,8 +85,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensor_name='{} {}'.format(
config.get(CONF_NAME), monitored_variable[CONF_NAME]),
sensor_type=monitored_variable[CONF_SENSOR_TYPE],
- sensor_value_template=monitored_variable[CONF_VALUE_TEMPLATE],
- unit_of_measurement=monitored_variable[CONF_UNIT_OF_MEASUREMENT])
+ sensor_value_template=monitored_variable.get(CONF_VALUE_TEMPLATE),
+ unit_of_measurement=monitored_variable.get(
+ CONF_UNIT_OF_MEASUREMENT))
devices.append(new_device)
add_devices(devices, True)
@@ -172,4 +173,4 @@ class HpIloData(object):
password=self._password, port=self._port)
except (hpilo.IloError, hpilo.IloCommunicationError,
hpilo.IloLoginFailed) as error:
- raise ValueError("Unable to init HP ILO, %s", error)
+ raise ValueError("Unable to init HP ILO, {}".format(error))
diff --git a/homeassistant/components/sensor/ihc.py b/homeassistant/components/sensor/ihc.py
index b6440a407a4..b30a242c17c 100644
--- a/homeassistant/components/sensor/ihc.py
+++ b/homeassistant/components/sensor/ihc.py
@@ -62,7 +62,7 @@ class IHCSensor(IHCDevice, Entity):
"""Implementation of the IHC sensor."""
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
- unit, product: Element=None) -> None:
+ unit, product: Element = None) -> None:
"""Initialize the IHC sensor."""
super().__init__(ihc_controller, name, ihc_id, info, product)
self._state = None
diff --git a/homeassistant/components/sensor/irish_rail_transport.py b/homeassistant/components/sensor/irish_rail_transport.py
index 0c34a5f6ce8..603d82359de 100644
--- a/homeassistant/components/sensor/irish_rail_transport.py
+++ b/homeassistant/components/sensor/irish_rail_transport.py
@@ -42,9 +42,9 @@ TIME_STR_FORMAT = '%H:%M'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STATION): cv.string,
- vol.Optional(CONF_DIRECTION, default=None): cv.string,
- vol.Optional(CONF_DESTINATION, default=None): cv.string,
- vol.Optional(CONF_STOPS_AT, default=None): cv.string,
+ vol.Optional(CONF_DIRECTION): cv.string,
+ vol.Optional(CONF_DESTINATION): cv.string,
+ vol.Optional(CONF_STOPS_AT): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
@@ -92,7 +92,7 @@ class IrishRailTransportSensor(Entity):
@property
def device_state_attributes(self):
"""Return the state attributes."""
- if len(self._times) > 0:
+ if self._times:
next_up = "None"
if len(self._times) > 1:
next_up = self._times[1][ATTR_ORIGIN] + " to "
@@ -126,7 +126,7 @@ class IrishRailTransportSensor(Entity):
"""Get the latest data and update the states."""
self.data.update()
self._times = self.data.info
- if len(self._times) > 0:
+ if self._times:
self._state = self._times[0][ATTR_DUE_IN]
else:
self._state = None
@@ -164,7 +164,7 @@ class IrishRailTransportData(object):
ATTR_TRAIN_TYPE: train.get('type')}
self.info.append(train_data)
- if not self.info or len(self.info) == 0:
+ if not self.info or not self.info:
self.info = self._empty_train_data()
def _empty_train_data(self):
diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py
index 39c9d8a3b9d..c34a4a8fca7 100644
--- a/homeassistant/components/sensor/isy994.py
+++ b/homeassistant/components/sensor/isy994.py
@@ -254,10 +254,6 @@ def setup_platform(hass, config: ConfigType,
class ISYSensorDevice(ISYDevice):
"""Representation of an ISY994 sensor device."""
- def __init__(self, node) -> None:
- """Initialize the ISY994 sensor device."""
- super().__init__(node)
-
@property
def raw_unit_of_measurement(self) -> str:
"""Get the raw unit of measurement for the ISY994 sensor device."""
@@ -313,10 +309,6 @@ class ISYSensorDevice(ISYDevice):
class ISYWeatherDevice(ISYDevice):
"""Representation of an ISY994 weather device."""
- def __init__(self, node) -> None:
- """Initialize the ISY994 weather device."""
- super().__init__(node)
-
@property
def raw_units(self) -> str:
"""Return the raw unit of measurement."""
diff --git a/homeassistant/components/sensor/knx.py b/homeassistant/components/sensor/knx.py
index 70afa6fe1e1..bdceb729e89 100644
--- a/homeassistant/components/sensor/knx.py
+++ b/homeassistant/components/sensor/knx.py
@@ -31,9 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up sensor(s) for KNX platform."""
- if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
- return
-
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
else:
diff --git a/homeassistant/components/sensor/lacrosse.py b/homeassistant/components/sensor/lacrosse.py
index b402fc5c70f..3e0a5af283f 100644
--- a/homeassistant/components/sensor/lacrosse.py
+++ b/homeassistant/components/sensor/lacrosse.py
@@ -133,10 +133,6 @@ class LaCrosseSensor(Entity):
"""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."""
diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py
index 89647d258b4..3d28c44d606 100644
--- a/homeassistant/components/sensor/linux_battery.py
+++ b/homeassistant/components/sensor/linux_battery.py
@@ -119,24 +119,23 @@ class LinuxBatterySensor(Entity):
ATTR_HEALTH: self._battery_stat.health,
ATTR_STATUS: self._battery_stat.status,
}
- else:
- return {
- ATTR_NAME: self._battery_stat.name,
- ATTR_PATH: self._battery_stat.path,
- ATTR_ALARM: self._battery_stat.alarm,
- ATTR_CAPACITY_LEVEL: self._battery_stat.capacity_level,
- ATTR_CYCLE_COUNT: self._battery_stat.cycle_count,
- ATTR_ENERGY_FULL: self._battery_stat.energy_full,
- ATTR_ENERGY_FULL_DESIGN: self._battery_stat.energy_full_design,
- ATTR_ENERGY_NOW: self._battery_stat.energy_now,
- ATTR_MANUFACTURER: self._battery_stat.manufacturer,
- ATTR_MODEL_NAME: self._battery_stat.model_name,
- ATTR_POWER_NOW: self._battery_stat.power_now,
- ATTR_SERIAL_NUMBER: self._battery_stat.serial_number,
- ATTR_STATUS: self._battery_stat.status,
- ATTR_VOLTAGE_MIN_DESIGN: self._battery_stat.voltage_min_design,
- ATTR_VOLTAGE_NOW: self._battery_stat.voltage_now,
- }
+ return {
+ ATTR_NAME: self._battery_stat.name,
+ ATTR_PATH: self._battery_stat.path,
+ ATTR_ALARM: self._battery_stat.alarm,
+ ATTR_CAPACITY_LEVEL: self._battery_stat.capacity_level,
+ ATTR_CYCLE_COUNT: self._battery_stat.cycle_count,
+ ATTR_ENERGY_FULL: self._battery_stat.energy_full,
+ ATTR_ENERGY_FULL_DESIGN: self._battery_stat.energy_full_design,
+ ATTR_ENERGY_NOW: self._battery_stat.energy_now,
+ ATTR_MANUFACTURER: self._battery_stat.manufacturer,
+ ATTR_MODEL_NAME: self._battery_stat.model_name,
+ ATTR_POWER_NOW: self._battery_stat.power_now,
+ ATTR_SERIAL_NUMBER: self._battery_stat.serial_number,
+ ATTR_STATUS: self._battery_stat.status,
+ ATTR_VOLTAGE_MIN_DESIGN: self._battery_stat.voltage_min_design,
+ ATTR_VOLTAGE_NOW: self._battery_stat.voltage_now,
+ }
def update(self):
"""Get the latest data and updates the states."""
diff --git a/homeassistant/components/sensor/loopenergy.py b/homeassistant/components/sensor/loopenergy.py
index a2d6b0c3a0c..5be24b1532c 100644
--- a/homeassistant/components/sensor/loopenergy.py
+++ b/homeassistant/components/sensor/loopenergy.py
@@ -51,10 +51,8 @@ GAS_SCHEMA = vol.Schema({
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(CONF_ELEC): vol.All(
- dict, ELEC_SCHEMA),
- vol.Optional(CONF_GAS, default={}): vol.All(
- dict, GAS_SCHEMA)
+ vol.Required(CONF_ELEC): ELEC_SCHEMA,
+ vol.Optional(CONF_GAS): GAS_SCHEMA
})
@@ -63,7 +61,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
import pyloopenergy
elec_config = config.get(CONF_ELEC)
- gas_config = config.get(CONF_GAS)
+ gas_config = config.get(CONF_GAS, {})
# pylint: disable=too-many-function-args
controller = pyloopenergy.LoopEnergy(
diff --git a/homeassistant/components/sensor/lyft.py b/homeassistant/components/sensor/lyft.py
index 0efc4063dc2..c2f6412049c 100644
--- a/homeassistant/components/sensor/lyft.py
+++ b/homeassistant/components/sensor/lyft.py
@@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_START_LONGITUDE): cv.longitude,
vol.Optional(CONF_END_LATITUDE): cv.latitude,
vol.Optional(CONF_END_LONGITUDE): cv.longitude,
- vol.Optional(CONF_PRODUCT_IDS, default=None):
+ vol.Optional(CONF_PRODUCT_IDS):
vol.All(cv.ensure_list, [cv.string]),
})
diff --git a/homeassistant/components/sensor/melissa.py b/homeassistant/components/sensor/melissa.py
index 58313428861..f67722b0198 100644
--- a/homeassistant/components/sensor/melissa.py
+++ b/homeassistant/components/sensor/melissa.py
@@ -22,8 +22,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = api.fetch_devices().values()
for device in devices:
- sensors.append(MelissaTemperatureSensor(device, api))
- sensors.append(MelissaHumiditySensor(device, api))
+ if device['type'] == 'melissa':
+ sensors.append(MelissaTemperatureSensor(device, api))
+ sensors.append(MelissaHumiditySensor(device, api))
add_devices(sensors)
diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py
index ec68588f241..37976151190 100644
--- a/homeassistant/components/sensor/miflora.py
+++ b/homeassistant/components/sensor/miflora.py
@@ -46,7 +46,7 @@ SENSOR_TYPES = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MAC): cv.string,
- vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES):
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int,
diff --git a/homeassistant/components/sensor/mold_indicator.py b/homeassistant/components/sensor/mold_indicator.py
index b47367cafc8..057718400c4 100644
--- a/homeassistant/components/sensor/mold_indicator.py
+++ b/homeassistant/components/sensor/mold_indicator.py
@@ -174,7 +174,7 @@ class MoldIndicator(Entity):
self._dewpoint = \
MAGNUS_K3 * (alpha + math.log(self._indoor_hum / 100.0)) / \
(beta - math.log(self._indoor_hum / 100.0))
- _LOGGER.debug("Dewpoint: %f " + TEMP_CELSIUS, self._dewpoint)
+ _LOGGER.debug("Dewpoint: %f %s", self._dewpoint, TEMP_CELSIUS)
def _calc_moldindicator(self):
"""Calculate the humidity at the (cold) calibration point."""
@@ -192,8 +192,8 @@ class MoldIndicator(Entity):
self._outdoor_temp + (self._indoor_temp - self._outdoor_temp) / \
self._calib_factor
- _LOGGER.debug("Estimated Critical Temperature: %f " +
- TEMP_CELSIUS, self._crit_temp)
+ _LOGGER.debug("Estimated Critical Temperature: %f %s",
+ self._crit_temp, TEMP_CELSIUS)
# Then calculate the humidity at this point
alpha = MAGNUS_K2 * self._crit_temp / (MAGNUS_K3 + self._crit_temp)
diff --git a/homeassistant/components/sensor/nederlandse_spoorwegen.py b/homeassistant/components/sensor/nederlandse_spoorwegen.py
index ec534047ccc..431a44c56e3 100644
--- a/homeassistant/components/sensor/nederlandse_spoorwegen.py
+++ b/homeassistant/components/sensor/nederlandse_spoorwegen.py
@@ -71,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
NSDepartureSensor(
nsapi, departure.get(CONF_NAME), departure.get(CONF_FROM),
departure.get(CONF_TO), departure.get(CONF_VIA)))
- if len(sensors):
+ if sensors:
add_devices(sensors, True)
diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py
index a9fb3ae7a6f..e0d5b7250e9 100644
--- a/homeassistant/components/sensor/nut.py
+++ b/homeassistant/components/sensor/nut.py
@@ -126,9 +126,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
- vol.Optional(CONF_ALIAS, default=None): cv.string,
- vol.Optional(CONF_USERNAME, default=None): cv.string,
- vol.Optional(CONF_PASSWORD, default=None): cv.string,
+ vol.Optional(CONF_ALIAS): cv.string,
+ vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_PASSWORD): cv.string,
vol.Required(CONF_RESOURCES, default=[]):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py
index 71b72b0a671..8a800e8616c 100644
--- a/homeassistant/components/sensor/octoprint.py
+++ b/homeassistant/components/sensor/octoprint.py
@@ -32,7 +32,7 @@ SENSOR_TYPES = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_TYPES):
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py
index 49280efe718..96db4430d32 100644
--- a/homeassistant/components/sensor/openweathermap.py
+++ b/homeassistant/components/sensor/openweathermap.py
@@ -47,7 +47,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_FORECAST, default=False): cv.boolean,
- vol.Optional(CONF_LANGUAGE, default=None): cv.string,
+ vol.Optional(CONF_LANGUAGE): cv.string,
})
diff --git a/homeassistant/components/sensor/pi_hole.py b/homeassistant/components/sensor/pi_hole.py
index 0b2f43195a6..027c12569a6 100644
--- a/homeassistant/components/sensor/pi_hole.py
+++ b/homeassistant/components/sensor/pi_hole.py
@@ -59,8 +59,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
- vol.Optional(CONF_MONITORED_CONDITIONS, default=MONITORED_CONDITIONS):
- vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]),
+ vol.Optional(CONF_MONITORED_CONDITIONS,
+ default=list(MONITORED_CONDITIONS)):
+ vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]),
})
diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py
index 5b5385f14ef..596887998ec 100644
--- a/homeassistant/components/sensor/pilight.py
+++ b/homeassistant/components/sensor/pilight.py
@@ -26,7 +26,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_VARIABLE): cv.string,
vol.Required(CONF_PAYLOAD): vol.Schema(dict),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=None): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
})
diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py
index 3998af7e32f..0771e7cbd2e 100644
--- a/homeassistant/components/sensor/pollen.py
+++ b/homeassistant/components/sensor/pollen.py
@@ -16,7 +16,7 @@ from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS
)
from homeassistant.helpers.entity import Entity
-from homeassistant.util import Throttle
+from homeassistant.util import Throttle, slugify
REQUIREMENTS = ['pypollencom==1.1.1']
_LOGGER = logging.getLogger(__name__)
@@ -125,6 +125,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
'allergy_index_data': AllergyIndexData(client),
'disease_average_data': DiseaseData(client)
}
+ classes = {
+ 'AllergyAverageSensor': AllergyAverageSensor,
+ 'AllergyIndexSensor': AllergyIndexSensor
+ }
for data in datas.values():
data.update()
@@ -132,11 +136,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors = []
for condition in config[CONF_MONITORED_CONDITIONS]:
name, sensor_class, data_key, params, icon = CONDITIONS[condition]
- sensors.append(globals()[sensor_class](
+ sensors.append(classes[sensor_class](
datas[data_key],
params,
name,
- icon
+ icon,
+ config[CONF_ZIP_CODE]
))
add_devices(sensors, True)
@@ -154,7 +159,7 @@ def calculate_trend(list_of_nums):
class BaseSensor(Entity):
"""Define a base class for all of our sensors."""
- def __init__(self, data, data_params, name, icon):
+ def __init__(self, data, data_params, name, icon, unique_id):
"""Initialize the sensor."""
self._attrs = {}
self._icon = icon
@@ -162,6 +167,7 @@ class BaseSensor(Entity):
self._data_params = data_params
self._state = None
self._unit = None
+ self._unique_id = unique_id
self.data = data
@property
@@ -185,6 +191,11 @@ class BaseSensor(Entity):
"""Return the state."""
return self._state
+ @property
+ def unique_id(self):
+ """Return a unique, HASS-friendly identifier for this entity."""
+ return '{0}_{1}'.format(self._unique_id, slugify(self._name))
+
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py
index 3caebad2007..09c9938f1c1 100644
--- a/homeassistant/components/sensor/qnap.py
+++ b/homeassistant/components/sensor/qnap.py
@@ -7,6 +7,8 @@ https://home-assistant.io/components/sensor.qnap/
import logging
from datetime import timedelta
+import voluptuous as vol
+
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
from homeassistant.const import (
@@ -15,8 +17,6 @@ from homeassistant.const import (
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
-import voluptuous as vol
-
REQUIREMENTS = ['qnapstats==0.2.4']
_LOGGER = logging.getLogger(__name__)
@@ -97,9 +97,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(_MONITORED_CONDITIONS)]),
- vol.Optional(CONF_NICS, default=None): cv.ensure_list,
- vol.Optional(CONF_DRIVES, default=None): cv.ensure_list,
- vol.Optional(CONF_VOLUMES, default=None): cv.ensure_list,
+ vol.Optional(CONF_NICS): cv.ensure_list,
+ vol.Optional(CONF_DRIVES): cv.ensure_list,
+ vol.Optional(CONF_VOLUMES): cv.ensure_list,
})
@@ -133,33 +133,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
api, variable, _MEMORY_MON_COND[variable]))
# Network sensors
- nics = config[CONF_NICS]
- if nics is None:
- nics = api.data["system_stats"]["nics"].keys()
-
- for nic in nics:
+ for nic in config.get(CONF_NICS, api.data["system_stats"]["nics"]):
sensors += [QNAPNetworkSensor(api, variable,
_NETWORK_MON_COND[variable], nic)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in _NETWORK_MON_COND]
# Drive sensors
- drives = config[CONF_DRIVES]
- if drives is None:
- drives = api.data["smart_drive_health"].keys()
-
- for drive in drives:
+ for drive in config.get(CONF_DRIVES, api.data["smart_drive_health"]):
sensors += [QNAPDriveSensor(api, variable,
_DRIVE_MON_COND[variable], drive)
for variable in config[CONF_MONITORED_CONDITIONS]
if variable in _DRIVE_MON_COND]
# Volume sensors
- volumes = config[CONF_VOLUMES]
- if volumes is None:
- volumes = api.data["volumes"].keys()
-
- for volume in volumes:
+ for volume in config.get(CONF_VOLUMES, api.data["volumes"]):
sensors += [QNAPVolumeSensor(api, variable,
_VOLUME_MON_COND[variable], volume)
for variable in config[CONF_MONITORED_CONDITIONS]
diff --git a/homeassistant/components/sensor/rflink.py b/homeassistant/components/sensor/rflink.py
index 0d5fc283e32..80d77033bbb 100644
--- a/homeassistant/components/sensor/rflink.py
+++ b/homeassistant/components/sensor/rflink.py
@@ -35,7 +35,7 @@ PLATFORM_SCHEMA = vol.Schema({
cv.string: {
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_SENSOR_TYPE): cv.string,
- vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=None): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_ALIASES, default=[]):
vol.All(cv.ensure_list, [cv.string]),
# deprecated config options
@@ -61,7 +61,7 @@ def devices_from_config(domain_config, hass=None):
"""Parse configuration and add Rflink sensor devices."""
devices = []
for device_id, config in domain_config[CONF_DEVICES].items():
- if not config[ATTR_UNIT_OF_MEASUREMENT]:
+ if ATTR_UNIT_OF_MEASUREMENT not in config:
config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type(
config[CONF_SENSOR_TYPE])
remove_deprecated(config)
diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py
index 1696e8e3770..4a555905d50 100644
--- a/homeassistant/components/sensor/rfxtrx.py
+++ b/homeassistant/components/sensor/rfxtrx.py
@@ -71,14 +71,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if device_id in rfxtrx.RFX_DEVICES:
sensors = rfxtrx.RFX_DEVICES[device_id]
- for key in sensors:
- sensor = sensors[key]
+ for data_type in sensors:
+ # Some multi-sensor devices send individual messages for each
+ # of their sensors. Update only if event contains the
+ # right data_type for the sensor.
+ if data_type not in event.values:
+ continue
+ sensor = sensors[data_type]
sensor.event = event
# Fire event
- if sensors[key].should_fire_event:
+ if sensor.should_fire_event:
sensor.hass.bus.fire(
"signal_received", {
- ATTR_ENTITY_ID: sensors[key].entity_id,
+ ATTR_ENTITY_ID: sensor.entity_id,
}
)
return
diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py
index 9ce2da09451..632e1ed5c1d 100644
--- a/homeassistant/components/sensor/sabnzbd.py
+++ b/homeassistant/components/sensor/sabnzbd.py
@@ -4,6 +4,7 @@ Support for monitoring an SABnzbd NZB client.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.sabnzbd/
"""
+import asyncio
import logging
from datetime import timedelta
@@ -18,13 +19,10 @@ from homeassistant.util import Throttle
from homeassistant.util.json import load_json, save_json
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['https://github.com/jamespcole/home-assistant-nzb-clients/'
- 'archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip'
- '#python-sabnzbd==0.1']
+REQUIREMENTS = ['pysabnzbd==0.0.3']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
-_THROTTLED_REFRESH = None
CONFIG_FILE = 'sabnzbd.conf'
DEFAULT_NAME = 'SABnzbd'
@@ -54,38 +52,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-def _check_sabnzbd(sab_api, base_url, api_key):
+@asyncio.coroutine
+def async_check_sabnzbd(sab_api, base_url, api_key):
"""Check if we can reach SABnzbd."""
from pysabnzbd import SabnzbdApiException
sab_api = sab_api(base_url, api_key)
try:
- sab_api.check_available()
+ yield from sab_api.check_available()
except SabnzbdApiException:
_LOGGER.error("Connection to SABnzbd API failed")
return False
return True
-def setup_sabnzbd(base_url, apikey, name, hass, config, add_devices, sab_api):
+def setup_sabnzbd(base_url, apikey, name, config,
+ async_add_devices, sab_api):
"""Set up polling from SABnzbd and sensors."""
sab_api = sab_api(base_url, apikey)
- # Add minimal info to the front end
- monitored = config.get(CONF_MONITORED_VARIABLES, ['current_status'])
-
- # pylint: disable=global-statement
- global _THROTTLED_REFRESH
- _THROTTLED_REFRESH = Throttle(
- MIN_TIME_BETWEEN_UPDATES)(sab_api.refresh_queue)
-
- devices = []
- for variable in monitored:
- devices.append(SabnzbdSensor(variable, sab_api, name))
-
- add_devices(devices)
+ monitored = config.get(CONF_MONITORED_VARIABLES)
+ async_add_devices([SabnzbdSensor(variable, sab_api, name)
+ for variable in monitored])
-def request_configuration(host, name, hass, config, add_devices, sab_api):
+@asyncio.coroutine
+@Throttle(MIN_TIME_BETWEEN_UPDATES)
+def async_update_queue(sab_api):
+ """
+ Throttled function to update SABnzbd queue.
+
+ This ensures that the queue info only gets updated once for all sensors
+ """
+ yield from sab_api.refresh_queue()
+
+
+def request_configuration(host, name, hass, config, async_add_devices,
+ sab_api):
"""Request configuration steps from the user."""
configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
@@ -95,12 +97,13 @@ def request_configuration(host, name, hass, config, add_devices, sab_api):
return
- def sabnzbd_configuration_callback(data):
+ @asyncio.coroutine
+ def async_configuration_callback(data):
"""Handle configuration changes."""
api_key = data.get('api_key')
- if _check_sabnzbd(sab_api, host, api_key):
- setup_sabnzbd(host, api_key, name,
- hass, config, add_devices, sab_api)
+ if (yield from async_check_sabnzbd(sab_api, host, api_key)):
+ setup_sabnzbd(host, api_key, name, config,
+ async_add_devices, sab_api)
def success():
"""Set up was successful."""
@@ -108,23 +111,21 @@ def request_configuration(host, name, hass, config, add_devices, sab_api):
conf[host] = {'api_key': api_key}
save_json(hass.config.path(CONFIG_FILE), conf)
req_config = _CONFIGURING.pop(host)
- hass.async_add_job(configurator.request_done, req_config)
+ configurator.async_request_done(req_config)
hass.async_add_job(success)
- _CONFIGURING[host] = configurator.request_config(
+ _CONFIGURING[host] = configurator.async_request_config(
DEFAULT_NAME,
- sabnzbd_configuration_callback,
- description=('Enter the API Key'),
+ async_configuration_callback,
+ description='Enter the API Key',
submit_caption='Confirm',
- fields=[{
- 'id': 'api_key',
- 'name': 'API Key',
- 'type': ''}]
+ fields=[{'id': 'api_key', 'name': 'API Key', 'type': ''}]
)
-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 SABnzbd platform."""
from pysabnzbd import SabnzbdApi
@@ -139,31 +140,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME, DEFAULT_NAME)
use_ssl = config.get(CONF_SSL)
+ api_key = config.get(CONF_API_KEY)
+
uri_scheme = 'https://' if use_ssl else 'http://'
base_url = "{}{}:{}/".format(uri_scheme, host, port)
- api_key = config.get(CONF_API_KEY)
if not api_key:
conf = load_json(hass.config.path(CONFIG_FILE))
if conf.get(base_url, {}).get('api_key'):
api_key = conf[base_url]['api_key']
- if not _check_sabnzbd(SabnzbdApi, base_url, api_key):
+ if not (yield from async_check_sabnzbd(SabnzbdApi, base_url, api_key)):
request_configuration(base_url, name, hass, config,
- add_devices, SabnzbdApi)
+ async_add_devices, SabnzbdApi)
return
- setup_sabnzbd(base_url, api_key, name, hass,
- config, add_devices, SabnzbdApi)
+ setup_sabnzbd(base_url, api_key, name, config,
+ async_add_devices, SabnzbdApi)
class SabnzbdSensor(Entity):
"""Representation of an SABnzbd sensor."""
- def __init__(self, sensor_type, sabnzb_client, client_name):
+ def __init__(self, sensor_type, sabnzbd_api, client_name):
"""Initialize the sensor."""
self._name = SENSOR_TYPES[sensor_type][0]
- self.sabnzb_client = sabnzb_client
+ self.sabnzbd_api = sabnzbd_api
self.type = sensor_type
self.client_name = client_name
self._state = None
@@ -184,35 +186,35 @@ class SabnzbdSensor(Entity):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
- # pylint: disable=no-self-use
- def refresh_sabnzbd_data(self):
+ @asyncio.coroutine
+ def async_refresh_sabnzbd_data(self):
"""Call the throttled SABnzbd refresh method."""
- if _THROTTLED_REFRESH is not None:
- from pysabnzbd import SabnzbdApiException
- try:
- _THROTTLED_REFRESH()
- except SabnzbdApiException:
- _LOGGER.exception("Connection to SABnzbd API failed")
+ from pysabnzbd import SabnzbdApiException
+ try:
+ yield from async_update_queue(self.sabnzbd_api)
+ except SabnzbdApiException:
+ _LOGGER.exception("Connection to SABnzbd API failed")
- def update(self):
+ @asyncio.coroutine
+ def async_update(self):
"""Get the latest data and updates the states."""
- self.refresh_sabnzbd_data()
+ yield from self.async_refresh_sabnzbd_data()
- if self.sabnzb_client.queue:
+ if self.sabnzbd_api.queue:
if self.type == 'current_status':
- self._state = self.sabnzb_client.queue.get('status')
+ self._state = self.sabnzbd_api.queue.get('status')
elif self.type == 'speed':
- mb_spd = float(self.sabnzb_client.queue.get('kbpersec')) / 1024
+ mb_spd = float(self.sabnzbd_api.queue.get('kbpersec')) / 1024
self._state = round(mb_spd, 1)
elif self.type == 'queue_size':
- self._state = self.sabnzb_client.queue.get('mb')
+ self._state = self.sabnzbd_api.queue.get('mb')
elif self.type == 'queue_remaining':
- self._state = self.sabnzb_client.queue.get('mbleft')
+ self._state = self.sabnzbd_api.queue.get('mbleft')
elif self.type == 'disk_size':
- self._state = self.sabnzb_client.queue.get('diskspacetotal1')
+ self._state = self.sabnzbd_api.queue.get('diskspacetotal1')
elif self.type == 'disk_free':
- self._state = self.sabnzb_client.queue.get('diskspace1')
+ self._state = self.sabnzbd_api.queue.get('diskspace1')
elif self.type == 'queue_count':
- self._state = self.sabnzb_client.queue.get('noofslots_total')
+ self._state = self.sabnzbd_api.queue.get('noofslots_total')
else:
self._state = 'Unknown'
diff --git a/homeassistant/components/sensor/sensehat.py b/homeassistant/components/sensor/sensehat.py
index db6d931d1b2..a50f4cdfd2c 100644
--- a/homeassistant/components/sensor/sensehat.py
+++ b/homeassistant/components/sensor/sensehat.py
@@ -32,7 +32,7 @@ SENSOR_TYPES = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Required(CONF_DISPLAY_OPTIONS, default=SENSOR_TYPES):
+ vol.Required(CONF_DISPLAY_OPTIONS, default=list(SENSOR_TYPES)):
[vol.In(SENSOR_TYPES)],
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_IS_HAT_ATTACHED, default=True): cv.boolean
diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py
index 3c14625202e..eabc33312b2 100644
--- a/homeassistant/components/sensor/skybeacon.py
+++ b/homeassistant/components/sensor/skybeacon.py
@@ -140,7 +140,7 @@ class Monitor(threading.Thread):
def run(self):
"""Thread that keeps connection alive."""
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-name-in-module, no-member
import pygatt
from pygatt.backends import Characteristic
from pygatt.exceptions import (
diff --git a/homeassistant/components/sensor/sma.py b/homeassistant/components/sensor/sma.py
index 2f3a29efbc0..3451789424b 100644
--- a/homeassistant/components/sensor/sma.py
+++ b/homeassistant/components/sensor/sma.py
@@ -12,13 +12,14 @@ import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL)
+ EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PASSWORD, CONF_SCAN_INTERVAL,
+ CONF_SSL)
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pysma==0.1.3']
+REQUIREMENTS = ['pysma==0.2']
_LOGGER = logging.getLogger(__name__)
@@ -49,6 +50,7 @@ def _check_sensor_schema(conf):
PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): str,
+ vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_GROUP, default=GROUPS[0]): vol.In(GROUPS),
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: cv.ensure_list}),
@@ -97,8 +99,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
session = async_get_clientsession(hass)
grp = {GROUP_INSTALLER: pysma.GROUP_INSTALLER,
GROUP_USER: pysma.GROUP_USER}[config[CONF_GROUP]]
- sma = pysma.SMA(session, config[CONF_HOST], config[CONF_PASSWORD],
- group=grp)
+
+ url = "http{}://{}".format(
+ "s" if config[CONF_SSL] else "", config[CONF_HOST])
+
+ sma = pysma.SMA(session, url, config[CONF_PASSWORD], group=grp)
# Ensure we logout on shutdown
@asyncio.coroutine
diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py
new file mode 100644
index 00000000000..51595d19b1a
--- /dev/null
+++ b/homeassistant/components/sensor/smappee.py
@@ -0,0 +1,162 @@
+"""
+Support for monitoring a Smappee energy sensor.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.smappee/
+"""
+import logging
+from datetime import timedelta
+
+from homeassistant.components.smappee import DATA_SMAPPEE
+from homeassistant.helpers.entity import Entity
+
+DEPENDENCIES = ['smappee']
+
+_LOGGER = logging.getLogger(__name__)
+
+SENSOR_PREFIX = 'Smappee'
+SENSOR_TYPES = {
+ 'solar':
+ ['Solar', 'mdi:white-balance-sunny', 'local', 'W', 'solar'],
+ 'active_power':
+ ['Active Power', 'mdi:power-plug', 'local', 'W', 'active_power'],
+ 'current':
+ ['Current', 'mdi:gauge', 'local', 'Amps', 'current'],
+ 'voltage':
+ ['Voltage', 'mdi:gauge', 'local', 'V', 'voltage'],
+ 'active_cosfi':
+ ['Power Factor', 'mdi:gauge', 'local', '%', 'active_cosfi'],
+ 'alwayson_today':
+ ['Always On Today', 'mdi:gauge', 'remote', 'kW', 'alwaysOn'],
+ 'solar_today':
+ ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kW', 'solar'],
+ 'power_today':
+ ['Power Today', 'mdi:power-plug', 'remote', 'kW', 'consumption']
+}
+
+SCAN_INTERVAL = timedelta(seconds=30)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Smappee sensor."""
+ smappee = hass.data[DATA_SMAPPEE]
+
+ dev = []
+ if smappee.is_remote_active:
+ for sensor in SENSOR_TYPES:
+ if 'remote' in SENSOR_TYPES[sensor]:
+ for location_id in smappee.locations.keys():
+ dev.append(SmappeeSensor(smappee, location_id, sensor))
+
+ if smappee.is_local_active:
+ for sensor in SENSOR_TYPES:
+ if 'local' in SENSOR_TYPES[sensor]:
+ if smappee.is_remote_active:
+ for location_id in smappee.locations.keys():
+ dev.append(SmappeeSensor(smappee, location_id, sensor))
+ else:
+ dev.append(SmappeeSensor(smappee, None, sensor))
+ add_devices(dev, True)
+
+
+class SmappeeSensor(Entity):
+ """Implementation of a Smappee sensor."""
+
+ def __init__(self, smappee, location_id, sensor):
+ """Initialize the sensor."""
+ self._smappee = smappee
+ self._location_id = location_id
+ self._sensor = sensor
+ self.data = None
+ self._state = None
+ self._name = SENSOR_TYPES[self._sensor][0]
+ self._icon = SENSOR_TYPES[self._sensor][1]
+ self._unit_of_measurement = SENSOR_TYPES[self._sensor][3]
+ self._smappe_name = SENSOR_TYPES[self._sensor][4]
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ if self._location_id:
+ location_name = self._smappee.locations[self._location_id]
+ else:
+ location_name = 'Local'
+
+ return "{} {} {}".format(SENSOR_PREFIX,
+ location_name,
+ self._name)
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return self._icon
+
+ @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
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the device."""
+ attr = {}
+ if self._location_id:
+ attr['Location Id'] = self._location_id
+ attr['Location Name'] = self._smappee.locations[self._location_id]
+ return attr
+
+ def update(self):
+ """Get the latest data from Smappee and update the state."""
+ self._smappee.update()
+
+ if self._sensor in ['alwayson_today', 'solar_today', 'power_today']:
+ data = self._smappee.consumption[self._location_id]
+ if data:
+ consumption = data.get('consumptions')[-1]
+ _LOGGER.debug("%s %s", self._sensor, consumption)
+ value = consumption.get(self._smappe_name)
+ self._state = round(value / 1000, 2)
+ elif self._sensor == 'active_cosfi':
+ cosfi = self._smappee.active_cosfi()
+ _LOGGER.debug("%s %s", self._sensor, cosfi)
+ if cosfi:
+ self._state = round(cosfi, 2)
+ elif self._sensor == 'current':
+ current = self._smappee.active_current()
+ _LOGGER.debug("%s %s", self._sensor, current)
+ if current:
+ self._state = round(current, 2)
+ elif self._sensor == 'voltage':
+ voltage = self._smappee.active_voltage()
+ _LOGGER.debug("%s %s", self._sensor, voltage)
+ if voltage:
+ self._state = round(voltage, 3)
+ elif self._sensor == 'active_power':
+ data = self._smappee.instantaneous
+ _LOGGER.debug("%s %s", self._sensor, data)
+ if data:
+ value1 = [float(i['value']) for i in data
+ if i['key'].endswith('phase0ActivePower')]
+ value2 = [float(i['value']) for i in data
+ if i['key'].endswith('phase1ActivePower')]
+ value3 = [float(i['value']) for i in data
+ if i['key'].endswith('phase2ActivePower')]
+ active_power = sum(value1 + value2 + value3) / 1000
+ self._state = round(active_power, 2)
+ elif self._sensor == 'solar':
+ data = self._smappee.instantaneous
+ _LOGGER.debug("%s %s", self._sensor, data)
+ if data:
+ value1 = [float(i['value']) for i in data
+ if i['key'].endswith('phase3ActivePower')]
+ value2 = [float(i['value']) for i in data
+ if i['key'].endswith('phase4ActivePower')]
+ value3 = [float(i['value']) for i in data
+ if i['key'].endswith('phase5ActivePower')]
+ power = sum(value1 + value2 + value3) / 1000
+ self._state = round(power, 2)
diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py
index c7ba61ef504..5b03be036d5 100644
--- a/homeassistant/components/sensor/speedtest.py
+++ b/homeassistant/components/sensor/speedtest.py
@@ -6,27 +6,30 @@ https://home-assistant.io/components/sensor.speedtest/
"""
import asyncio
import logging
-import re
-import sys
-from subprocess import check_output, CalledProcessError
import voluptuous as vol
-import homeassistant.util.dt as dt_util
+from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv
-from homeassistant.components.sensor import (DOMAIN, PLATFORM_SCHEMA)
-from homeassistant.const import CONF_MONITORED_CONDITIONS
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_time_change
from homeassistant.helpers.restore_state import async_get_last_state
+import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['speedtest-cli==1.0.7']
+REQUIREMENTS = ['speedtest-cli==2.0.0']
_LOGGER = logging.getLogger(__name__)
-_SPEEDTEST_REGEX = re.compile(r'Ping:\s(\d+\.\d+)\sms[\r\n]+'
- r'Download:\s(\d+\.\d+)\sMbit/s[\r\n]+'
- r'Upload:\s(\d+\.\d+)\sMbit/s[\r\n]+')
+ATTR_BYTES_RECEIVED = 'bytes_received'
+ATTR_BYTES_SENT = 'bytes_sent'
+ATTR_SERVER_COUNTRY = 'server_country'
+ATTR_SERVER_HOST = 'server_host'
+ATTR_SERVER_ID = 'server_id'
+ATTR_SERVER_LATENCY = 'latency'
+ATTR_SERVER_NAME = 'server_name'
+
+CONF_ATTRIBUTION = "Data retrieved from Speedtest by Ookla"
CONF_SECOND = 'second'
CONF_MINUTE = 'minute'
CONF_HOUR = 'hour'
@@ -45,28 +48,26 @@ SENSOR_TYPES = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]),
- vol.Optional(CONF_SERVER_ID): cv.positive_int,
- vol.Optional(CONF_SECOND, default=[0]):
- vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]),
- vol.Optional(CONF_MINUTE, default=[0]):
- vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]),
- vol.Optional(CONF_HOUR):
- vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
vol.Optional(CONF_DAY):
vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(1, 31))]),
+ vol.Optional(CONF_HOUR):
+ vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 23))]),
vol.Optional(CONF_MANUAL, default=False): cv.boolean,
+ vol.Optional(CONF_MINUTE, default=[0]):
+ vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]),
+ vol.Optional(CONF_SECOND, default=[0]):
+ vol.All(cv.ensure_list, [vol.All(vol.Coerce(int), vol.Range(0, 59))]),
+ vol.Optional(CONF_SERVER_ID): cv.positive_int,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Speedtest sensor."""
data = SpeedtestData(hass, config)
+
dev = []
for sensor in config[CONF_MONITORED_CONDITIONS]:
- if sensor not in SENSOR_TYPES:
- _LOGGER.error("Sensor type: %s does not exist", sensor)
- else:
- dev.append(SpeedtestSensor(data, sensor))
+ dev.append(SpeedtestSensor(data, sensor))
add_devices(dev)
@@ -88,6 +89,7 @@ class SpeedtestSensor(Entity):
self.speedtest_client = speedtest_data
self.type = sensor_type
self._state = None
+ self._data = None
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
@property
@@ -110,18 +112,32 @@ class SpeedtestSensor(Entity):
"""Return icon."""
return ICON
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._data is not None:
+ return {
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
+ ATTR_BYTES_RECEIVED: self._data['bytes_received'],
+ ATTR_BYTES_SENT: self._data['bytes_sent'],
+ ATTR_SERVER_COUNTRY: self._data['server']['country'],
+ ATTR_SERVER_ID: self._data['server']['id'],
+ ATTR_SERVER_LATENCY: self._data['server']['latency'],
+ ATTR_SERVER_NAME: self._data['server']['name'],
+ }
+
def update(self):
"""Get the latest data and update the states."""
- data = self.speedtest_client.data
- if data is None:
+ self._data = self.speedtest_client.data
+ if self._data is None:
return
if self.type == 'ping':
- self._state = data['ping']
+ self._state = self._data['ping']
elif self.type == 'download':
- self._state = data['download']
+ self._state = round(self._data['download'] / 10**6, 2)
elif self.type == 'upload':
- self._state = data['upload']
+ self._state = round(self._data['upload'] / 10**6, 2)
@asyncio.coroutine
def async_added_to_hass(self):
@@ -148,20 +164,14 @@ class SpeedtestData(object):
def update(self, now):
"""Get the latest data from speedtest.net."""
import speedtest
+ _LOGGER.debug("Executing speedtest...")
- _LOGGER.info("Executing speedtest...")
- try:
- args = [sys.executable, speedtest.__file__, '--simple']
- if self._server_id:
- args = args + ['--server', str(self._server_id)]
+ servers = [] if self._server_id is None else [self._server_id]
- re_output = _SPEEDTEST_REGEX.split(
- check_output(args).decode('utf-8'))
- except CalledProcessError as process_error:
- _LOGGER.error("Error executing speedtest: %s", process_error)
- return
- self.data = {
- 'ping': round(float(re_output[1]), 2),
- 'download': round(float(re_output[2]), 2),
- 'upload': round(float(re_output[3]), 2),
- }
+ speed = speedtest.Speedtest()
+ speed.get_servers(servers)
+ speed.get_best_server()
+ speed.download()
+ speed.upload()
+
+ self.data = speed.results.dict()
diff --git a/homeassistant/components/sensor/spotcrime.py b/homeassistant/components/sensor/spotcrime.py
new file mode 100644
index 00000000000..169bcc5f867
--- /dev/null
+++ b/homeassistant/components/sensor/spotcrime.py
@@ -0,0 +1,123 @@
+"""
+Sensor for Spot Crime.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.spotcrime/
+"""
+
+from datetime import timedelta
+from collections import defaultdict
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_INCLUDE, CONF_EXCLUDE, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE,
+ ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_RADIUS)
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import slugify
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['spotcrime==1.0.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_DAYS = 'days'
+DEFAULT_DAYS = 1
+NAME = 'spotcrime'
+
+EVENT_INCIDENT = '{}_incident'.format(NAME)
+
+SCAN_INTERVAL = timedelta(minutes=30)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_RADIUS): vol.Coerce(float),
+ vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude,
+ vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude,
+ vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.positive_int,
+ vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string])
+})
+
+
+# pylint: disable=unused-argument
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Crime Reports platform."""
+ latitude = config.get(CONF_LATITUDE, hass.config.latitude)
+ longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
+ name = config[CONF_NAME]
+ radius = config[CONF_RADIUS]
+ days = config.get(CONF_DAYS)
+ include = config.get(CONF_INCLUDE)
+ exclude = config.get(CONF_EXCLUDE)
+
+ add_devices([SpotCrimeSensor(
+ name, latitude, longitude, radius, include,
+ exclude, days)], True)
+
+
+class SpotCrimeSensor(Entity):
+ """Representation of a Spot Crime Sensor."""
+
+ def __init__(self, name, latitude, longitude, radius,
+ include, exclude, days):
+ """Initialize the Spot Crime sensor."""
+ import spotcrime
+ self._name = name
+ self._include = include
+ self._exclude = exclude
+ self.days = days
+ self._spotcrime = spotcrime.SpotCrime(
+ (latitude, longitude), radius, None, None, self.days)
+ self._attributes = None
+ self._state = None
+ self._previous_incidents = set()
+ self._attributes = {
+ ATTR_ATTRIBUTION: spotcrime.ATTRIBUTION
+ }
+
+ @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 device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
+
+ def _incident_event(self, incident):
+ data = {
+ 'type': incident.get('type'),
+ 'timestamp': incident.get('timestamp'),
+ 'address': incident.get('location')
+ }
+ if incident.get('coordinates'):
+ data.update({
+ ATTR_LATITUDE: incident.get('lat'),
+ ATTR_LONGITUDE: incident.get('lon')
+ })
+ self.hass.bus.fire(EVENT_INCIDENT, data)
+
+ def update(self):
+ """Update device state."""
+ incident_counts = defaultdict(int)
+ incidents = self._spotcrime.get_incidents()
+ if len(incidents) < len(self._previous_incidents):
+ self._previous_incidents = set()
+ for incident in incidents:
+ incident_type = slugify(incident.get('type'))
+ incident_counts[incident_type] += 1
+ if (self._previous_incidents and incident.get('id')
+ not in self._previous_incidents):
+ self._incident_event(incident)
+ self._previous_incidents.add(incident.get('id'))
+ self._attributes.update(incident_counts)
+ self._state = len(incidents)
diff --git a/homeassistant/components/sensor/startca.py b/homeassistant/components/sensor/startca.py
new file mode 100644
index 00000000000..a5908812b6c
--- /dev/null
+++ b/homeassistant/components/sensor/startca.py
@@ -0,0 +1,186 @@
+"""
+Support for Start.ca Bandwidth Monitor.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.startca/
+"""
+from datetime import timedelta
+from xml.parsers.expat import ExpatError
+import logging
+import asyncio
+import async_timeout
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME)
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['xmltodict==0.11.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = 'Start.ca'
+CONF_TOTAL_BANDWIDTH = 'total_bandwidth'
+
+GIGABYTES = 'GB' # type: str
+PERCENT = '%' # type: str
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1)
+REQUEST_TIMEOUT = 5 # seconds
+
+SENSOR_TYPES = {
+ 'usage': ['Usage Ratio', PERCENT, 'mdi:percent'],
+ 'usage_gb': ['Usage', GIGABYTES, 'mdi:download'],
+ 'limit': ['Data limit', GIGABYTES, 'mdi:download'],
+ 'used_download': ['Used Download', GIGABYTES, 'mdi:download'],
+ 'used_upload': ['Used Upload', GIGABYTES, 'mdi:upload'],
+ 'used_total': ['Used Total', GIGABYTES, 'mdi:download'],
+ 'grace_download': ['Grace Download', GIGABYTES, 'mdi:download'],
+ 'grace_upload': ['Grace Upload', GIGABYTES, 'mdi:upload'],
+ 'grace_total': ['Grace Total', GIGABYTES, 'mdi:download'],
+ 'total_download': ['Total Download', GIGABYTES, 'mdi:download'],
+ 'total_upload': ['Total Upload', GIGABYTES, 'mdi:download'],
+ 'used_remaining': ['Remaining', GIGABYTES, 'mdi:download']
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_MONITORED_VARIABLES):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_TOTAL_BANDWIDTH): cv.positive_int,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+ """Set up the sensor platform."""
+ websession = async_get_clientsession(hass)
+ apikey = config.get(CONF_API_KEY)
+ bandwidthcap = config.get(CONF_TOTAL_BANDWIDTH)
+
+ ts_data = StartcaData(hass.loop, websession, apikey, bandwidthcap)
+ ret = yield from ts_data.async_update()
+ if ret is False:
+ _LOGGER.error("Invalid Start.ca API key: %s", apikey)
+ return
+
+ name = config.get(CONF_NAME)
+ sensors = []
+ for variable in config[CONF_MONITORED_VARIABLES]:
+ sensors.append(StartcaSensor(ts_data, variable, name))
+ async_add_devices(sensors, True)
+
+
+class StartcaSensor(Entity):
+ """Representation of Start.ca Bandwidth sensor."""
+
+ def __init__(self, startcadata, sensor_type, name):
+ """Initialize the sensor."""
+ self.client_name = name
+ self.type = sensor_type
+ self._name = SENSOR_TYPES[sensor_type][0]
+ self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
+ self._icon = SENSOR_TYPES[sensor_type][2]
+ self.startcadata = startcadata
+ self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return '{} {}'.format(self.client_name, 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
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend, if any."""
+ return self._icon
+
+ @asyncio.coroutine
+ def async_update(self):
+ """Get the latest data from Start.ca and update the state."""
+ yield from self.startcadata.async_update()
+ if self.type in self.startcadata.data:
+ self._state = round(self.startcadata.data[self.type], 2)
+
+
+class StartcaData(object):
+ """Get data from Start.ca API."""
+
+ def __init__(self, loop, websession, api_key, bandwidth_cap):
+ """Initialize the data object."""
+ self.loop = loop
+ self.websession = websession
+ self.api_key = api_key
+ self.bandwidth_cap = bandwidth_cap
+ # Set unlimited users to infinite, otherwise the cap.
+ self.data = {"limit": self.bandwidth_cap} if self.bandwidth_cap > 0 \
+ else {"limit": float('inf')}
+
+ @staticmethod
+ def bytes_to_gb(value):
+ """Convert from bytes to GB.
+
+ :param value: The value in bytes to convert to GB.
+ :return: Converted GB value
+ """
+ return float(value) * 10 ** -9
+
+ @asyncio.coroutine
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def async_update(self):
+ """Get the Start.ca bandwidth data from the web service."""
+ import xmltodict
+ _LOGGER.debug("Updating Start.ca usage data")
+ url = 'https://www.start.ca/support/usage/api?key=' + \
+ self.api_key
+ with async_timeout.timeout(REQUEST_TIMEOUT, loop=self.loop):
+ req = yield from self.websession.get(url)
+ if req.status != 200:
+ _LOGGER.error("Request failed with status: %u", req.status)
+ return False
+
+ data = yield from req.text()
+ try:
+ xml_data = xmltodict.parse(data)
+ except ExpatError:
+ return False
+
+ used_dl = self.bytes_to_gb(xml_data['usage']['used']['download'])
+ used_ul = self.bytes_to_gb(xml_data['usage']['used']['upload'])
+ grace_dl = self.bytes_to_gb(xml_data['usage']['grace']['download'])
+ grace_ul = self.bytes_to_gb(xml_data['usage']['grace']['upload'])
+ total_dl = self.bytes_to_gb(xml_data['usage']['total']['download'])
+ total_ul = self.bytes_to_gb(xml_data['usage']['total']['upload'])
+
+ limit = self.data['limit']
+ if self.bandwidth_cap > 0:
+ self.data['usage'] = 100*used_dl/self.bandwidth_cap
+ else:
+ self.data['usage'] = 0
+ self.data['usage_gb'] = used_dl
+ self.data['used_download'] = used_dl
+ self.data['used_upload'] = used_ul
+ self.data['used_total'] = used_dl + used_ul
+ self.data['grace_download'] = grace_dl
+ self.data['grace_upload'] = grace_ul
+ self.data['grace_total'] = grace_dl + grace_ul
+ self.data['total_download'] = total_dl
+ self.data['total_upload'] = total_ul
+ self.data['used_remaining'] = limit - used_dl
+
+ return True
diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py
index b26fd5cc804..7b2ae537d4b 100644
--- a/homeassistant/components/sensor/statistics.py
+++ b/homeassistant/components/sensor/statistics.py
@@ -173,7 +173,7 @@ class StatisticsSensor(Entity):
"""Remove states which are older than self._max_age."""
now = dt_util.utcnow()
- while (len(self.ages) > 0) and (now - self.ages[0]) > self._max_age:
+ while self.ages and (now - self.ages[0]) > self._max_age:
self.ages.popleft()
self.states.popleft()
diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py
index f5a41c7b8ce..a0198169b6d 100644
--- a/homeassistant/components/sensor/synologydsm.py
+++ b/homeassistant/components/sensor/synologydsm.py
@@ -78,8 +78,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(_MONITORED_CONDITIONS)]),
- vol.Optional(CONF_DISKS, default=None): cv.ensure_list,
- vol.Optional(CONF_VOLUMES, default=None): cv.ensure_list,
+ vol.Optional(CONF_DISKS): cv.ensure_list,
+ vol.Optional(CONF_VOLUMES): cv.ensure_list,
})
@@ -106,22 +106,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if variable in _UTILISATION_MON_COND]
# Handle all volumes
- volumes = config['volumes']
- if volumes is None:
- volumes = api.storage.volumes
-
- for volume in volumes:
+ for volume in config.get(CONF_VOLUMES, api.storage.volumes):
sensors += [SynoNasStorageSensor(
api, variable, _STORAGE_VOL_MON_COND[variable], volume)
for variable in monitored_conditions
if variable in _STORAGE_VOL_MON_COND]
# Handle all disks
- disks = config['disks']
- if disks is None:
- disks = api.storage.disks
-
- for disk in disks:
+ for disk in config.get(CONF_DISKS, api.storage.disks):
sensors += [SynoNasStorageSensor(
api, variable, _STORAGE_DSK_MON_COND[variable], disk)
for variable in monitored_conditions
diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py
index ea8595e3991..3aed9d5a21b 100644
--- a/homeassistant/components/sensor/systemmonitor.py
+++ b/homeassistant/components/sensor/systemmonitor.py
@@ -48,7 +48,7 @@ SENSOR_TYPES = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_RESOURCES, default=['disk_use']):
+ vol.Optional(CONF_RESOURCES, default={CONF_TYPE: 'disk_use'}):
vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(CONF_ARG): cv.string,
diff --git a/homeassistant/components/sensor/teksavvy.py b/homeassistant/components/sensor/teksavvy.py
index cb78caae095..33e5c0cf4ce 100644
--- a/homeassistant/components/sensor/teksavvy.py
+++ b/homeassistant/components/sensor/teksavvy.py
@@ -6,9 +6,11 @@ https://home-assistant.io/components/sensor.teksavvy/
"""
from datetime import timedelta
import logging
-
import asyncio
import async_timeout
+
+import voluptuous as vol
+
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME)
@@ -16,7 +18,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
-import voluptuous as vol
_LOGGER = logging.getLogger(__name__)
@@ -142,18 +143,17 @@ class TekSavvyData(object):
if req.status != 200:
_LOGGER.error("Request failed with status: %u", req.status)
return False
- else:
- data = yield from req.json()
- for (api, ha_name) in API_HA_MAP:
- self.data[ha_name] = float(data["value"][0][api])
- on_peak_download = self.data["onpeak_download"]
- on_peak_upload = self.data["onpeak_upload"]
- off_peak_download = self.data["offpeak_download"]
- off_peak_upload = self.data["offpeak_upload"]
- limit = self.data["limit"]
- self.data["usage"] = 100*on_peak_download/self.bandwidth_cap
- self.data["usage_gb"] = on_peak_download
- self.data["onpeak_total"] = on_peak_download + on_peak_upload
- self.data["offpeak_total"] = off_peak_download + off_peak_upload
- self.data["onpeak_remaining"] = limit - on_peak_download
- return True
+ data = yield from req.json()
+ for (api, ha_name) in API_HA_MAP:
+ self.data[ha_name] = float(data["value"][0][api])
+ on_peak_download = self.data["onpeak_download"]
+ on_peak_upload = self.data["onpeak_upload"]
+ off_peak_download = self.data["offpeak_download"]
+ off_peak_upload = self.data["offpeak_upload"]
+ limit = self.data["limit"]
+ self.data["usage"] = 100*on_peak_download/self.bandwidth_cap
+ self.data["usage_gb"] = on_peak_download
+ self.data["onpeak_total"] = on_peak_download + on_peak_upload
+ self.data["offpeak_total"] = off_peak_download + off_peak_upload
+ self.data["onpeak_remaining"] = limit - on_peak_download
+ return True
diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py
index b347439e08d..582bc3a0150 100644
--- a/homeassistant/components/sensor/template.py
+++ b/homeassistant/components/sensor/template.py
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, ATTR_ENTITY_ID,
- CONF_SENSORS, EVENT_HOMEASSISTANT_START)
+ CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity, async_generate_entity_id
@@ -26,6 +26,7 @@ SENSOR_SCHEMA = vol.Schema({
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template,
+ vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template,
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
@@ -50,6 +51,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
entity_ids = (device_config.get(ATTR_ENTITY_ID) or
state_template.extract_entities())
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
+ friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE)
unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT)
state_template.hass = hass
@@ -60,11 +62,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if entity_picture_template is not None:
entity_picture_template.hass = hass
+ if friendly_name_template is not None:
+ friendly_name_template.hass = hass
+
sensors.append(
SensorTemplate(
hass,
device,
friendly_name,
+ friendly_name_template,
unit_of_measurement,
state_template,
icon_template,
@@ -82,7 +88,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class SensorTemplate(Entity):
"""Representation of a Template Sensor."""
- def __init__(self, hass, device_id, friendly_name,
+ def __init__(self, hass, device_id, friendly_name, friendly_name_template,
unit_of_measurement, state_template, icon_template,
entity_picture_template, entity_ids):
"""Initialize the sensor."""
@@ -90,6 +96,7 @@ class SensorTemplate(Entity):
self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id,
hass=hass)
self._name = friendly_name
+ self._friendly_name_template = friendly_name_template
self._unit_of_measurement = unit_of_measurement
self._template = state_template
self._state = None
@@ -165,7 +172,8 @@ class SensorTemplate(Entity):
for property_name, template in (
('_icon', self._icon_template),
- ('_entity_picture', self._entity_picture_template)):
+ ('_entity_picture', self._entity_picture_template),
+ ('_name', self._friendly_name_template)):
if template is None:
continue
diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py
index dd09b9f7891..519ff05cbd8 100644
--- a/homeassistant/components/sensor/tibber.py
+++ b/homeassistant/components/sensor/tibber.py
@@ -42,7 +42,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
yield from home.update_info()
dev.append(TibberSensor(home))
- async_add_devices(dev)
+ async_add_devices(dev, True)
class TibberSensor(Entity):
diff --git a/homeassistant/components/sensor/viaggiatreno.py b/homeassistant/components/sensor/viaggiatreno.py
index a7f4b070f2d..43ba80d2630 100644
--- a/homeassistant/components/sensor/viaggiatreno.py
+++ b/homeassistant/components/sensor/viaggiatreno.py
@@ -76,9 +76,8 @@ def async_http_request(hass, uri):
req = yield from session.get(uri)
if req.status != 200:
return {'error': req.status}
- else:
- json_response = yield from req.json()
- return json_response
+ 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:
diff --git a/homeassistant/components/sensor/volvooncall.py b/homeassistant/components/sensor/volvooncall.py
index 32b228ca1f9..343bcdf2033 100644
--- a/homeassistant/components/sensor/volvooncall.py
+++ b/homeassistant/components/sensor/volvooncall.py
@@ -42,12 +42,10 @@ class VolvoSensor(VolvoEntity):
val /= 10 # L/1000km -> L/100km
if 'mil' in self.unit_of_measurement:
return round(val, 2)
- else:
- return round(val, 1)
+ return round(val, 1)
elif self._attribute == 'distance_to_empty':
return int(floor(val))
- else:
- return int(round(val))
+ return int(round(val))
@property
def unit_of_measurement(self):
@@ -56,8 +54,7 @@ class VolvoSensor(VolvoEntity):
if self._state.config[CONF_SCANDINAVIAN_MILES] and 'km' in unit:
if self._attribute == 'average_fuel_consumption':
return 'L/mil'
- else:
- return unit.replace('km', 'mil')
+ return unit.replace('km', 'mil')
return unit
@property
diff --git a/homeassistant/components/sensor/vultr.py b/homeassistant/components/sensor/vultr.py
index 012c6eb7398..291639c81d6 100644
--- a/homeassistant/components/sensor/vultr.py
+++ b/homeassistant/components/sensor/vultr.py
@@ -30,8 +30,9 @@ MONITORED_CONDITIONS = {
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)])
+ vol.Optional(CONF_MONITORED_CONDITIONS,
+ default=list(MONITORED_CONDITIONS)):
+ vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)])
})
diff --git a/homeassistant/components/sensor/worldtidesinfo.py b/homeassistant/components/sensor/worldtidesinfo.py
index f23d244cf3a..8884d790eed 100644
--- a/homeassistant/components/sensor/worldtidesinfo.py
+++ b/homeassistant/components/sensor/worldtidesinfo.py
@@ -90,10 +90,8 @@ class WorldTidesInfoSensor(Entity):
tidetime = time.strftime('%I:%M %p', time.localtime(
self.data['extremes'][0]['dt']))
return "Low tide at %s" % (tidetime)
- else:
- return STATE_UNKNOWN
- else:
return STATE_UNKNOWN
+ return STATE_UNKNOWN
def update(self):
"""Get the latest data from WorldTidesInfo API."""
diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py
index aa5d431a7b0..edcc1c92bf9 100644
--- a/homeassistant/components/sensor/wunderground.py
+++ b/homeassistant/components/sensor/wunderground.py
@@ -4,21 +4,24 @@ Support for WUnderground weather service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.wunderground/
"""
+import asyncio
from datetime import timedelta
import logging
-
import re
-import requests
+
+import aiohttp
+import async_timeout
import voluptuous as vol
-from homeassistant.helpers.typing import HomeAssistantType
+from homeassistant.helpers.typing import HomeAssistantType, ConfigType
from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE,
TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS,
- LENGTH_MILES, LENGTH_FEET, STATE_UNKNOWN, ATTR_ATTRIBUTION)
+ LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION)
from homeassistant.exceptions import PlatformNotReady
-from homeassistant.helpers.entity import Entity, generate_entity_id
+from homeassistant.helpers.entity import Entity, async_generate_entity_id
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
@@ -627,7 +630,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-def setup_platform(hass, config, add_devices, discovery_info=None):
+@asyncio.coroutine
+def async_setup_platform(hass: HomeAssistantType, config: ConfigType,
+ async_add_devices, discovery_info=None):
"""Set up the WUnderground sensor."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
@@ -639,13 +644,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for variable in config[CONF_MONITORED_CONDITIONS]:
sensors.append(WUndergroundSensor(hass, rest, variable))
- rest.update()
+ yield from rest.async_update()
if not rest.data:
raise PlatformNotReady
- add_devices(sensors)
-
- return True
+ async_add_devices(sensors, True)
class WUndergroundSensor(Entity):
@@ -663,7 +666,7 @@ class WUndergroundSensor(Entity):
self._entity_picture = None
self._unit_of_measurement = self._cfg_expand("unit_of_measurement")
self.rest.request_feature(SENSOR_TYPES[condition].feature)
- self.entity_id = generate_entity_id(
+ self.entity_id = async_generate_entity_id(
ENTITY_ID_FORMAT, "pws_" + condition, hass=hass)
def _cfg_expand(self, what, default=None):
@@ -727,15 +730,16 @@ class WUndergroundSensor(Entity):
"""Return the units of measurement."""
return self._unit_of_measurement
- def update(self):
+ @asyncio.coroutine
+ def async_update(self):
"""Update current conditions."""
- self.rest.update()
+ yield from self.rest.async_update()
if not self.rest.data:
# no data, return
return
- self._state = self._cfg_expand("value", STATE_UNKNOWN)
+ self._state = self._cfg_expand("value")
self._update_attrs()
self._icon = self._cfg_expand("icon", super().icon)
url = self._cfg_expand("entity_picture")
@@ -757,6 +761,7 @@ class WUndergroundData(object):
self._longitude = longitude
self._features = set()
self.data = None
+ self._session = async_get_clientsession(self._hass)
def request_feature(self, feature):
"""Register feature to be fetched from WU API."""
@@ -764,7 +769,7 @@ class WUndergroundData(object):
def _build_url(self, baseurl=_RESOURCE):
url = baseurl.format(
- self._api_key, "/".join(self._features), self._lang)
+ self._api_key, '/'.join(sorted(self._features)), self._lang)
if self._pws_id:
url = url + 'pws:{}'.format(self._pws_id)
else:
@@ -772,20 +777,18 @@ class WUndergroundData(object):
return url + '.json'
+ @asyncio.coroutine
@Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
+ def async_update(self):
"""Get the latest data from WUnderground."""
try:
- result = requests.get(self._build_url(), timeout=10).json()
+ with async_timeout.timeout(10, loop=self._hass.loop):
+ response = yield from self._session.get(self._build_url())
+ result = yield from response.json()
if "error" in result['response']:
- raise ValueError(result['response']["error"]
- ["description"])
- else:
- self.data = result
- return True
+ raise ValueError(result['response']["error"]["description"])
+ self.data = result
except ValueError as err:
_LOGGER.error("Check WUnderground API %s", err.args)
- self.data = None
- except requests.RequestException as err:
+ except (asyncio.TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Error fetching WUnderground data: %s", repr(err))
- self.data = None
diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py
index 3f9579c1f13..88c23771bd4 100644
--- a/homeassistant/components/sensor/yr.py
+++ b/homeassistant/components/sensor/yr.py
@@ -222,7 +222,7 @@ class YrData(object):
# Update all devices
tasks = []
- if len(ordered_entries) > 0:
+ if ordered_entries:
for dev in self.devices:
new_state = None
@@ -254,5 +254,5 @@ class YrData(object):
dev._state = new_state
tasks.append(dev.async_update_ha_state())
- if len(tasks) > 0:
+ if tasks:
yield from asyncio.wait(tasks, loop=self.hass.loop)
diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py
index e066e38fb1e..df18e086ddd 100644
--- a/homeassistant/components/sensor/yweather.py
+++ b/homeassistant/components/sensor/yweather.py
@@ -42,7 +42,7 @@ SENSOR_TYPES = {
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_WOEID, default=None): cv.string,
+ vol.Optional(CONF_WOEID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_FORECAST, default=0):
vol.All(vol.Coerce(int), vol.Range(min=0, max=5)),
diff --git a/homeassistant/components/sensor/zabbix.py b/homeassistant/components/sensor/zabbix.py
index a47d466c07e..baeed391557 100644
--- a/homeassistant/components/sensor/zabbix.py
+++ b/homeassistant/components/sensor/zabbix.py
@@ -25,8 +25,8 @@ _CONF_INDIVIDUAL = 'individual'
_ZABBIX_ID_LIST_SCHEMA = vol.Schema([int])
_ZABBIX_TRIGGER_SCHEMA = vol.Schema({
vol.Optional(_CONF_HOSTIDS, default=[]): _ZABBIX_ID_LIST_SCHEMA,
- vol.Optional(_CONF_INDIVIDUAL, default=False): cv.boolean(True),
- vol.Optional(CONF_NAME, default=None): cv.string,
+ vol.Optional(_CONF_INDIVIDUAL, default=False): cv.boolean,
+ vol.Optional(CONF_NAME): cv.string,
})
# SCAN_INTERVAL = 30
diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py
index a1820f7d7dd..36cdca2e638 100644
--- a/homeassistant/components/sensor/zha.py
+++ b/homeassistant/components/sensor/zha.py
@@ -31,19 +31,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@asyncio.coroutine
def make_sensor(discovery_info):
"""Create ZHA sensors factory."""
- from zigpy.zcl.clusters.measurement import TemperatureMeasurement
+ from zigpy.zcl.clusters.measurement import (
+ RelativeHumidity, TemperatureMeasurement
+ )
in_clusters = discovery_info['in_clusters']
- if TemperatureMeasurement.cluster_id in in_clusters:
+ if RelativeHumidity.cluster_id in in_clusters:
+ sensor = RelativeHumiditySensor(**discovery_info)
+ elif TemperatureMeasurement.cluster_id in in_clusters:
sensor = TemperatureSensor(**discovery_info)
else:
sensor = Sensor(**discovery_info)
- attr = sensor.value_attribute
if discovery_info['new_join']:
cluster = list(in_clusters.values())[0]
yield from cluster.bind()
yield from cluster.configure_reporting(
- attr, 300, 600, sensor.min_reportable_change,
+ sensor.value_attribute, 300, 600, sensor.min_reportable_change,
)
return sensor
@@ -89,3 +92,22 @@ class TemperatureSensor(Sensor):
celsius = round(float(self._state) / 100, 1)
return convert_temperature(
celsius, TEMP_CELSIUS, self.unit_of_measurement)
+
+
+class RelativeHumiditySensor(Sensor):
+ """ZHA relative humidity sensor."""
+
+ min_reportable_change = 50 # 0.5%
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity."""
+ return '%'
+
+ @property
+ def state(self):
+ """Return the state of the entity."""
+ if self._state == 'unknown':
+ return 'unknown'
+
+ return round(float(self._state) / 100, 1)
diff --git a/homeassistant/components/sensor/zigbee.py b/homeassistant/components/sensor/zigbee.py
index a1d549cb382..37cc6fabe2e 100644
--- a/homeassistant/components/sensor/zigbee.py
+++ b/homeassistant/components/sensor/zigbee.py
@@ -70,7 +70,7 @@ class ZigBeeTemperatureSensor(Entity):
"""Return the unit of measurement the value is expressed in."""
return TEMP_CELSIUS
- def update(self, *args):
+ def update(self):
"""Get the latest data."""
try:
self._temp = zigbee.DEVICE.get_temperature(self._config.address)
diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py
index 31259325c04..2452188a889 100644
--- a/homeassistant/components/shopping_list.py
+++ b/homeassistant/components/shopping_list.py
@@ -1,8 +1,6 @@
"""Component to manage a shopping list."""
import asyncio
-import json
import logging
-import os
import uuid
import voluptuous as vol
@@ -10,9 +8,11 @@ import voluptuous as vol
from homeassistant.const import HTTP_NOT_FOUND, HTTP_BAD_REQUEST
from homeassistant.core import callback
from homeassistant.components import http
+from homeassistant.components.http.data_validator import (
+ RequestDataValidator)
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
-
+from homeassistant.util.json import load_json, save_json
DOMAIN = 'shopping_list'
DEPENDENCIES = ['http']
@@ -99,18 +99,13 @@ class ShoppingData:
"""Load items."""
def load():
"""Load the items synchronously."""
- path = self.hass.config.path(PERSISTENCE)
- if not os.path.isfile(path):
- return []
- with open(path) as file:
- return json.loads(file.read())
+ return load_json(self.hass.config.path(PERSISTENCE), default=[])
self.items = yield from self.hass.async_add_job(load)
def save(self):
"""Save the items."""
- with open(self.hass.config.path(PERSISTENCE), 'wt') as file:
- file.write(json.dumps(self.items, sort_keys=True, indent=4))
+ save_json(self.hass.config.path(PERSISTENCE), self.items)
class AddItemIntent(intent.IntentHandler):
@@ -199,7 +194,7 @@ class CreateShoppingListItemView(http.HomeAssistantView):
url = '/api/shopping_list/item'
name = "api:shopping_list:item"
- @http.RequestDataValidator(vol.Schema({
+ @RequestDataValidator(vol.Schema({
vol.Required('name'): str,
}))
@asyncio.coroutine
diff --git a/homeassistant/components/sleepiq.py b/homeassistant/components/sleepiq.py
index baf6d154c66..3b74b79b36b 100644
--- a/homeassistant/components/sleepiq.py
+++ b/homeassistant/components/sleepiq.py
@@ -6,6 +6,7 @@ https://home-assistant.io/components/sleepiq/
"""
import logging
from datetime import timedelta
+from requests.exceptions import HTTPError
import voluptuous as vol
@@ -14,7 +15,6 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.util import Throttle
-from requests.exceptions import HTTPError
DOMAIN = 'sleepiq'
diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py
new file mode 100644
index 00000000000..0111e0437fb
--- /dev/null
+++ b/homeassistant/components/smappee.py
@@ -0,0 +1,337 @@
+"""
+Support for Smappee energy monitor.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/smappee/
+"""
+import logging
+from datetime import datetime, timedelta
+import re
+import voluptuous as vol
+from requests.exceptions import RequestException
+from homeassistant.const import (
+ CONF_USERNAME, CONF_PASSWORD, CONF_HOST
+)
+from homeassistant.util import Throttle
+from homeassistant.helpers.discovery import load_platform
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['smappy==0.2.15']
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = 'Smappee'
+DEFAULT_HOST_PASSWORD = 'admin'
+
+CONF_CLIENT_ID = 'client_id'
+CONF_CLIENT_SECRET = 'client_secret'
+CONF_HOST_PASSWORD = 'host_password'
+
+DOMAIN = 'smappee'
+DATA_SMAPPEE = 'SMAPPEE'
+
+_SENSOR_REGEX = re.compile(
+ r'(?P([A-Za-z]+))\=' +
+ r'(?P([0-9\.]+))')
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Inclusive(CONF_CLIENT_ID, 'Server credentials'): cv.string,
+ vol.Inclusive(CONF_CLIENT_SECRET, 'Server credentials'): cv.string,
+ vol.Inclusive(CONF_USERNAME, 'Server credentials'): cv.string,
+ vol.Inclusive(CONF_PASSWORD, 'Server credentials'): cv.string,
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Optional(CONF_HOST_PASSWORD, default=DEFAULT_HOST_PASSWORD):
+ cv.string
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
+
+
+def setup(hass, config):
+ """Set up the Smapee component."""
+ client_id = config.get(DOMAIN).get(CONF_CLIENT_ID)
+ client_secret = config.get(DOMAIN).get(CONF_CLIENT_SECRET)
+ username = config.get(DOMAIN).get(CONF_USERNAME)
+ password = config.get(DOMAIN).get(CONF_PASSWORD)
+ host = config.get(DOMAIN).get(CONF_HOST)
+ host_password = config.get(DOMAIN).get(CONF_HOST_PASSWORD)
+
+ smappee = Smappee(client_id, client_secret, username,
+ password, host, host_password)
+
+ if not smappee.is_local_active and not smappee.is_remote_active:
+ _LOGGER.error("Neither Smappee server or local component enabled.")
+ return False
+
+ hass.data[DATA_SMAPPEE] = smappee
+ load_platform(hass, 'switch', DOMAIN)
+ load_platform(hass, 'sensor', DOMAIN)
+ return True
+
+
+class Smappee(object):
+ """Stores data retrieved from Smappee sensor."""
+
+ def __init__(self, client_id, client_secret, username,
+ password, host, host_password):
+ """Initialize the data."""
+ import smappy
+
+ self._remote_active = False
+ self._local_active = False
+ if client_id is not None:
+ try:
+ self._smappy = smappy.Smappee(client_id, client_secret)
+ self._smappy.authenticate(username, password)
+ self._remote_active = True
+ except RequestException as error:
+ self._smappy = None
+ _LOGGER.exception(
+ "Smappee server authentication failed (%s)",
+ error)
+ else:
+ _LOGGER.warning("Smappee server component init skipped.")
+
+ if host is not None:
+ try:
+ self._localsmappy = smappy.LocalSmappee(host)
+ self._localsmappy.logon(host_password)
+ self._local_active = True
+ except RequestException as error:
+ self._localsmappy = None
+ _LOGGER.exception(
+ "Local Smappee device authentication failed (%s)",
+ error)
+ else:
+ _LOGGER.warning("Smappee local component init skipped.")
+
+ self.locations = {}
+ self.info = {}
+ self.consumption = {}
+ self.instantaneous = {}
+
+ if self._remote_active or self._local_active:
+ self.update()
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Update data from Smappee API."""
+ if self.is_remote_active:
+ service_locations = self._smappy.get_service_locations() \
+ .get('serviceLocations')
+ for location in service_locations:
+ location_id = location.get('serviceLocationId')
+ if location_id is not None:
+ self.locations[location_id] = location.get('name')
+ self.info[location_id] = self._smappy \
+ .get_service_location_info(location_id)
+ _LOGGER.debug("Remote info %s %s",
+ self.locations, self.info)
+
+ self.consumption[location_id] = self.get_consumption(
+ location_id, aggregation=3, delta=1440)
+ _LOGGER.debug("Remote consumption %s %s",
+ self.locations,
+ self.consumption[location_id])
+
+ if self.is_local_active:
+ self.local_devices = self.get_switches()
+ _LOGGER.debug("Local switches %s", self.local_devices)
+
+ self.instantaneous = self.load_instantaneous()
+ _LOGGER.debug("Local values %s", self.instantaneous)
+
+ @property
+ def is_remote_active(self):
+ """Return true if Smappe server is configured and working."""
+ return self._remote_active
+
+ @property
+ def is_local_active(self):
+ """Return true if Smappe local device is configured and working."""
+ return self._local_active
+
+ def get_switches(self):
+ """Get switches from local Smappee."""
+ if not self.is_local_active:
+ return
+
+ try:
+ return self._localsmappy.load_command_control_config()
+ except RequestException as error:
+ _LOGGER.error(
+ "Error getting switches from local Smappee. (%s)",
+ error)
+
+ def get_consumption(self, location_id, aggregation, delta):
+ """Update data from Smappee."""
+ # Start & End accept epoch (in milliseconds),
+ # datetime and pandas timestamps
+ # Aggregation:
+ # 1 = 5 min values (only available for the last 14 days),
+ # 2 = hourly values,
+ # 3 = daily values,
+ # 4 = monthly values,
+ # 5 = quarterly values
+ if not self.is_remote_active:
+ return
+
+ end = datetime.utcnow()
+ start = end - timedelta(minutes=delta)
+ try:
+ return self._smappy.get_consumption(location_id,
+ start,
+ end,
+ aggregation)
+ except RequestException as error:
+ _LOGGER.error(
+ "Error getting comsumption from Smappee cloud. (%s)",
+ error)
+
+ def get_sensor_consumption(self, location_id, sensor_id):
+ """Update data from Smappee."""
+ # Start & End accept epoch (in milliseconds),
+ # datetime and pandas timestamps
+ # Aggregation:
+ # 1 = 5 min values (only available for the last 14 days),
+ # 2 = hourly values,
+ # 3 = daily values,
+ # 4 = monthly values,
+ # 5 = quarterly values
+ if not self.is_remote_active:
+ return
+
+ start = datetime.utcnow() - timedelta(minutes=30)
+ end = datetime.utcnow()
+ try:
+ return self._smappy.get_sensor_consumption(location_id,
+ sensor_id,
+ start,
+ end, 1)
+ except RequestException as error:
+ _LOGGER.error(
+ "Error getting comsumption from Smappee cloud. (%s)",
+ error)
+
+ def actuator_on(self, location_id, actuator_id,
+ is_remote_switch, duration=None):
+ """Turn on actuator."""
+ # Duration = 300,900,1800,3600
+ # or any other value for an undetermined period of time.
+ #
+ # The comport plugs have a tendency to ignore the on/off signal.
+ # And because you can't read the status of a plug, it's more
+ # reliable to execute the command twice.
+ try:
+ if is_remote_switch:
+ self._smappy.actuator_on(location_id, actuator_id, duration)
+ self._smappy.actuator_on(location_id, actuator_id, duration)
+ else:
+ self._localsmappy.on_command_control(actuator_id)
+ self._localsmappy.on_command_control(actuator_id)
+ except RequestException as error:
+ _LOGGER.error(
+ "Error turning actuator on. (%s)",
+ error)
+ return False
+
+ return True
+
+ def actuator_off(self, location_id, actuator_id,
+ is_remote_switch, duration=None):
+ """Turn off actuator."""
+ # Duration = 300,900,1800,3600
+ # or any other value for an undetermined period of time.
+ #
+ # The comport plugs have a tendency to ignore the on/off signal.
+ # And because you can't read the status of a plug, it's more
+ # reliable to execute the command twice.
+ try:
+ if is_remote_switch:
+ self._smappy.actuator_off(location_id, actuator_id, duration)
+ self._smappy.actuator_off(location_id, actuator_id, duration)
+ else:
+ self._localsmappy.off_command_control(actuator_id)
+ self._localsmappy.off_command_control(actuator_id)
+ except RequestException as error:
+ _LOGGER.error(
+ "Error turning actuator on. (%s)",
+ error)
+ return False
+
+ return True
+
+ def active_power(self):
+ """Get sum of all instantanious active power values from local hub."""
+ if not self.is_local_active:
+ return
+
+ try:
+ return self._localsmappy.active_power()
+ except RequestException as error:
+ _LOGGER.error(
+ "Error getting data from Local Smappee unit. (%s)",
+ error)
+
+ def active_cosfi(self):
+ """Get the average of all instantaneous cosfi values."""
+ if not self.is_local_active:
+ return
+
+ try:
+ return self._localsmappy.active_cosfi()
+ except RequestException as error:
+ _LOGGER.error(
+ "Error getting data from Local Smappee unit. (%s)",
+ error)
+
+ def instantaneous_values(self):
+ """ReportInstantaneousValues."""
+ if not self.is_local_active:
+ return
+
+ report_instantaneous_values = \
+ self._localsmappy.report_instantaneous_values()
+
+ report_result = \
+ report_instantaneous_values['report'].split('
')
+ properties = {}
+ for lines in report_result:
+ lines_result = lines.split(',')
+ for prop in lines_result:
+ match = _SENSOR_REGEX.search(prop)
+ if match:
+ properties[match.group('key')] = \
+ match.group('value')
+ _LOGGER.debug(properties)
+ return properties
+
+ def active_current(self):
+ """Get current active Amps."""
+ if not self.is_local_active:
+ return
+
+ properties = self.instantaneous_values()
+ return float(properties['current'])
+
+ def active_voltage(self):
+ """Get current active Voltage."""
+ if not self.is_local_active:
+ return
+
+ properties = self.instantaneous_values()
+ return float(properties['voltage'])
+
+ def load_instantaneous(self):
+ """LoadInstantaneous."""
+ if not self.is_local_active:
+ return
+
+ try:
+ return self._localsmappy.load_instantaneous()
+ except RequestException as error:
+ _LOGGER.error(
+ "Error getting data from Local Smappee unit. (%s)",
+ error)
diff --git a/homeassistant/components/statsd.py b/homeassistant/components/statsd.py
index 3613f53c098..6b528733601 100644
--- a/homeassistant/components/statsd.py
+++ b/homeassistant/components/statsd.py
@@ -35,7 +35,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string,
vol.Optional(CONF_RATE, default=DEFAULT_RATE):
vol.All(vol.Coerce(int), vol.Range(min=1)),
- vol.Optional(CONF_VALUE_MAP, default=None): dict,
+ vol.Optional(CONF_VALUE_MAP): dict,
}),
}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py
index 8fd70ec7ed8..527456d6d19 100644
--- a/homeassistant/components/switch/acer_projector.py
+++ b/homeassistant/components/switch/acer_projector.py
@@ -155,13 +155,13 @@ class AcerSwitch(SwitchDevice):
awns = self._write_read_format(msg)
self._attributes[key] = awns
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the projector on."""
msg = CMD_DICT[STATE_ON]
self._write_read(msg)
self._state = STATE_ON
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the projector off."""
msg = CMD_DICT[STATE_OFF]
self._write_read(msg)
diff --git a/homeassistant/components/switch/anel_pwrctrl.py b/homeassistant/components/switch/anel_pwrctrl.py
index bfa6e2af976..9144222e5c7 100644
--- a/homeassistant/components/switch/anel_pwrctrl.py
+++ b/homeassistant/components/switch/anel_pwrctrl.py
@@ -101,11 +101,11 @@ class PwrCtrlSwitch(SwitchDevice):
"""Trigger update for all switches on the parent device."""
self._parent_device.update()
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the switch on."""
self._port.on()
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
self._port.off()
diff --git a/homeassistant/components/switch/arduino.py b/homeassistant/components/switch/arduino.py
index 3aa61feffc8..1547f4f1dee 100644
--- a/homeassistant/components/switch/arduino.py
+++ b/homeassistant/components/switch/arduino.py
@@ -83,12 +83,12 @@ class ArduinoSwitch(SwitchDevice):
"""Return true if pin is high/on."""
return self._state
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the pin to high/on."""
self._state = True
self.turn_on_handler(self._pin)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the pin to low/off."""
self._state = False
self.turn_off_handler(self._pin)
diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py
index e79b7c3f34c..91ecc9c7111 100644
--- a/homeassistant/components/switch/broadlink.py
+++ b/homeassistant/components/switch/broadlink.py
@@ -45,8 +45,8 @@ MP1_TYPES = ['mp1']
SWITCH_TYPES = RM_TYPES + SP1_TYPES + SP2_TYPES + MP1_TYPES
SWITCH_SCHEMA = vol.Schema({
- vol.Optional(CONF_COMMAND_OFF, default=None): cv.string,
- vol.Optional(CONF_COMMAND_ON, default=None): cv.string,
+ vol.Optional(CONF_COMMAND_OFF): cv.string,
+ vol.Optional(CONF_COMMAND_ON): cv.string,
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
})
diff --git a/homeassistant/components/switch/dlink.py b/homeassistant/components/switch/dlink.py
index f6ed6dac018..5d727e72138 100644
--- a/homeassistant/components/switch/dlink.py
+++ b/homeassistant/components/switch/dlink.py
@@ -117,7 +117,7 @@ class SmartPlugSwitch(SwitchDevice):
"""Turn the switch on."""
self.data.smartplug.state = 'ON'
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
self.data.smartplug.state = 'OFF'
diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py
index c5973c3ee04..d4b02749c1b 100644
--- a/homeassistant/components/switch/edimax.py
+++ b/homeassistant/components/switch/edimax.py
@@ -77,7 +77,7 @@ class SmartPlugSwitch(SwitchDevice):
"""Turn the switch on."""
self.smartplug.state = 'ON'
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
self.smartplug.state = 'OFF'
diff --git a/homeassistant/components/switch/fritzdect.py b/homeassistant/components/switch/fritzdect.py
index 8ddfca05fb6..58ad745a2d2 100644
--- a/homeassistant/components/switch/fritzdect.py
+++ b/homeassistant/components/switch/fritzdect.py
@@ -130,7 +130,7 @@ class FritzDectSwitch(SwitchDevice):
_LOGGER.error("Fritz!Box query failed, triggering relogin")
self.data.is_online = False
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
if not self.data.is_online:
_LOGGER.error("turn_off: Not online skipping request")
diff --git a/homeassistant/components/switch/gc100.py b/homeassistant/components/switch/gc100.py
index ed50c3f63f6..f4175926aa0 100644
--- a/homeassistant/components/switch/gc100.py
+++ b/homeassistant/components/switch/gc100.py
@@ -56,11 +56,11 @@ class GC100Switch(ToggleEntity):
"""Return the state of the entity."""
return self._state
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the device on."""
self._gc100.write_switch(self._port_addr, 1, self.set_state)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the device off."""
self._gc100.write_switch(self._port_addr, 0, self.set_state)
diff --git a/homeassistant/components/switch/hdmi_cec.py b/homeassistant/components/switch/hdmi_cec.py
index 65a7a762c0f..e81c09894ab 100644
--- a/homeassistant/components/switch/hdmi_cec.py
+++ b/homeassistant/components/switch/hdmi_cec.py
@@ -47,7 +47,7 @@ class CecSwitchDevice(CecDevice, SwitchDevice):
self._device.turn_off()
self._state = STATE_ON
- def toggle(self):
+ def toggle(self, **kwargs):
"""Toggle the entity."""
self._device.toggle()
if self._state == STATE_ON:
diff --git a/homeassistant/components/switch/ihc.py b/homeassistant/components/switch/ihc.py
index eab88035c73..499a4ca53a7 100644
--- a/homeassistant/components/switch/ihc.py
+++ b/homeassistant/components/switch/ihc.py
@@ -53,7 +53,7 @@ class IHCSwitch(IHCDevice, SwitchDevice):
"""IHC Switch."""
def __init__(self, ihc_controller, name: str, ihc_id: int,
- info: bool, product: Element=None) -> None:
+ info: bool, product: Element = None) -> None:
"""Initialize the IHC switch."""
super().__init__(ihc_controller, name, ihc_id, product)
self._state = False
diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py
index f0fd397710e..efdda6ed40c 100644
--- a/homeassistant/components/switch/isy994.py
+++ b/homeassistant/components/switch/isy994.py
@@ -33,10 +33,6 @@ def setup_platform(hass, config: ConfigType,
class ISYSwitchDevice(ISYDevice, SwitchDevice):
"""Representation of an ISY994 switch device."""
- def __init__(self, node) -> None:
- """Initialize the ISY994 switch device."""
- super().__init__(node)
-
@property
def is_on(self) -> bool:
"""Get whether the ISY994 device is in the on state."""
diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py
index 01c08767ca0..86a9adf0495 100644
--- a/homeassistant/components/switch/knx.py
+++ b/homeassistant/components/switch/knx.py
@@ -30,9 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up switch(es) for KNX platform."""
- if DATA_KNX not in hass.data or not hass.data[DATA_KNX].initialized:
- return
-
if discovery_info is not None:
async_add_devices_discovery(hass, discovery_info, async_add_devices)
else:
diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py
index 211ff54d5a4..ca70c212774 100644
--- a/homeassistant/components/switch/modbus.py
+++ b/homeassistant/components/switch/modbus.py
@@ -37,12 +37,12 @@ REGISTERS_SCHEMA = vol.Schema({
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):
+ vol.Optional(CONF_VERIFY_REGISTER):
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,
+ vol.Optional(CONF_STATE_ON): cv.positive_int,
+ vol.Optional(CONF_STATE_OFF): cv.positive_int,
})
COILS_SCHEMA = vol.Schema({
diff --git a/homeassistant/components/switch/netio.py b/homeassistant/components/switch/netio.py
index 2a72703c5df..365bbaa3679 100644
--- a/homeassistant/components/switch/netio.py
+++ b/homeassistant/components/switch/netio.py
@@ -141,11 +141,11 @@ class NetioSwitch(SwitchDevice):
"""Return true if entity is available."""
return not hasattr(self, 'telnet')
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn switch on."""
self._set(True)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn switch off."""
self._set(False)
diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py
index 1ce599366a1..57fa4b00c98 100644
--- a/homeassistant/components/switch/pilight.py
+++ b/homeassistant/components/switch/pilight.py
@@ -188,10 +188,10 @@ class PilightSwitch(SwitchDevice):
self._state = turn_on
self.schedule_update_ha_state()
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the switch on by calling pilight.send service with on code."""
self.set_state(turn_on=True)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch on by calling pilight.send service with off code."""
self.set_state(turn_on=False)
diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py
index d8d424be361..dc661c3e5bf 100644
--- a/homeassistant/components/switch/rachio.py
+++ b/homeassistant/components/switch/rachio.py
@@ -216,7 +216,7 @@ class RachioZone(SwitchDevice):
_LOGGER.debug("Updated %s", str(self))
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Start the zone."""
# Stop other zones first
self.turn_off()
@@ -224,7 +224,7 @@ class RachioZone(SwitchDevice):
_LOGGER.info("Watering %s for %d s", self.name, self._manual_run_secs)
self.rachio.zone.start(self.zone_id, self._manual_run_secs)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Stop all zones."""
_LOGGER.info("Stopping watering of all zones")
self.rachio.device.stopWater(self._device.device_id)
diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py
index a18d6544acc..8a5c4347cf7 100644
--- a/homeassistant/components/switch/raincloud.py
+++ b/homeassistant/components/switch/raincloud.py
@@ -59,7 +59,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchDevice):
"""Return true if device is on."""
return self._state
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the device on."""
if self._sensor_type == 'manual_watering':
self.data.watering_time = self._default_watering_timer
@@ -67,7 +67,7 @@ class RainCloudSwitch(RainCloudEntity, SwitchDevice):
self.data.auto_watering = True
self._state = True
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the device off."""
if self._sensor_type == 'manual_watering':
self.data.watering_time = 'off'
diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py
index 3147ded96bd..99d41bdd9c3 100644
--- a/homeassistant/components/switch/rainmachine.py
+++ b/homeassistant/components/switch/rainmachine.py
@@ -32,22 +32,16 @@ PLATFORM_SCHEMA = vol.Schema(
vol.All(
cv.has_at_least_one_key(CONF_IP_ADDRESS, CONF_EMAIL),
{
- vol.Required(CONF_PLATFORM):
- cv.string,
- vol.Optional(CONF_SCAN_INTERVAL):
- cv.time_period,
- vol.Exclusive(CONF_IP_ADDRESS, 'auth'):
- cv.string,
+ vol.Required(CONF_PLATFORM): cv.string,
+ vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
+ vol.Exclusive(CONF_IP_ADDRESS, 'auth'): cv.string,
vol.Exclusive(CONF_EMAIL, 'auth'):
- vol.Email(), # pylint: disable=no-value-for-parameter
- vol.Required(CONF_PASSWORD):
- cv.string,
- vol.Optional(CONF_PORT, default=DEFAULT_PORT):
- cv.port,
- vol.Optional(CONF_SSL, default=DEFAULT_SSL):
- cv.boolean,
+ vol.Email(), # pylint: disable=no-value-for-parameter
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+ vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS):
- cv.positive_int
+ cv.positive_int
}),
extra=vol.ALLOW_EXTRA)
@@ -56,27 +50,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set this component up under its platform."""
import regenmaschine as rm
- ip_address = config.get(CONF_IP_ADDRESS)
- _LOGGER.debug('IP address: %s', ip_address)
+ _LOGGER.debug('Config data: %s', config)
- email_address = config.get(CONF_EMAIL)
- _LOGGER.debug('Email address: %s', email_address)
-
- password = config.get(CONF_PASSWORD)
- _LOGGER.debug('Password: %s', password)
-
- zone_run_time = config.get(CONF_ZONE_RUN_TIME)
- _LOGGER.debug('Zone run time: %s', zone_run_time)
+ ip_address = config.get(CONF_IP_ADDRESS, None)
+ email_address = config.get(CONF_EMAIL, None)
+ password = config[CONF_PASSWORD]
+ zone_run_time = config[CONF_ZONE_RUN_TIME]
try:
if ip_address:
- port = config.get(CONF_PORT)
- _LOGGER.debug('Port: %s', port)
-
- ssl = config.get(CONF_SSL)
- _LOGGER.debug('SSL: %s', ssl)
-
_LOGGER.debug('Configuring local API')
+
+ port = config[CONF_PORT]
+ ssl = config[CONF_SSL]
auth = rm.Authenticator.create_local(
ip_address, password, port=port, https=ssl)
elif email_address:
@@ -85,32 +71,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.debug('Querying against: %s', auth.url)
- _LOGGER.debug('Instantiating RainMachine client')
client = rm.Client(auth)
-
- rainmachine_device_name = client.provision.device_name().get('name')
+ device_name = client.provision.device_name()['name']
+ device_mac = client.provision.wifi()['macAddress']
entities = []
- for program in client.programs.all().get('programs'):
+ for program in client.programs.all().get('programs', {}):
if not program.get('active'):
continue
_LOGGER.debug('Adding program: %s', program)
entities.append(
- RainMachineProgram(
- client, program, device_name=rainmachine_device_name))
+ RainMachineProgram(client, device_name, device_mac, program))
- for zone in client.zones.all().get('zones'):
+ for zone in client.zones.all().get('zones', {}):
if not zone.get('active'):
continue
_LOGGER.debug('Adding zone: %s', zone)
entities.append(
- RainMachineZone(
- client,
- zone,
- zone_run_time,
- device_name=rainmachine_device_name, ))
+ RainMachineZone(client, device_name, device_mac, zone,
+ zone_run_time))
add_devices(entities)
except rm.exceptions.HTTPError as exc_info:
@@ -149,16 +130,17 @@ def aware_throttle(api_type):
class RainMachineEntity(SwitchDevice):
"""A class to represent a generic RainMachine entity."""
- def __init__(self, client, entity_json, **kwargs):
+ def __init__(self, client, device_name, device_mac, entity_json):
"""Initialize a generic RainMachine entity."""
self._api_type = 'remote' if client.auth.using_remote_api else 'local'
self._client = client
- self._device_name = kwargs.get('device_name')
self._entity_json = entity_json
+ self.device_mac = device_mac
+ self.device_name = device_name
self._attrs = {
ATTR_ATTRIBUTION: '© RainMachine',
- ATTR_DEVICE_CLASS: self._device_name
+ ATTR_DEVICE_CLASS: self.device_name
}
@property
@@ -173,15 +155,10 @@ class RainMachineEntity(SwitchDevice):
return self._entity_json.get('active')
@property
- def rainmachine_id(self) -> int:
+ def rainmachine_entity_id(self) -> int:
"""Return the RainMachine ID for this entity."""
return self._entity_json.get('uid')
- @property
- def unique_id(self) -> str:
- """Return a unique, HASS-friendly identifier for this entity."""
- return self.rainmachine_id
-
@aware_throttle('local')
def _local_update(self) -> None:
"""Call an update with scan times appropriate for the local API."""
@@ -217,17 +194,22 @@ class RainMachineProgram(RainMachineEntity):
"""Return the name of the program."""
return 'Program: {}'.format(self._entity_json.get('name'))
+ @property
+ def unique_id(self) -> str:
+ """Return a unique, HASS-friendly identifier for this entity."""
+ return '{0}_program_{1}'.format(
+ self.device_mac.replace(':', ''), self.rainmachine_entity_id)
+
def turn_off(self, **kwargs) -> None:
"""Turn the program off."""
import regenmaschine.exceptions as exceptions
try:
- self._client.programs.stop(self.rainmachine_id)
+ self._client.programs.stop(self.rainmachine_entity_id)
except exceptions.BrokenAPICall:
_LOGGER.error('programs.stop currently broken in remote API')
except exceptions.HTTPError as exc_info:
- _LOGGER.error('Unable to turn off program "%s"',
- self.rainmachine_id)
+ _LOGGER.error('Unable to turn off program "%s"', self.unique_id)
_LOGGER.debug(exc_info)
def turn_on(self, **kwargs) -> None:
@@ -235,12 +217,11 @@ class RainMachineProgram(RainMachineEntity):
import regenmaschine.exceptions as exceptions
try:
- self._client.programs.start(self.rainmachine_id)
+ self._client.programs.start(self.rainmachine_entity_id)
except exceptions.BrokenAPICall:
_LOGGER.error('programs.start currently broken in remote API')
except exceptions.HTTPError as exc_info:
- _LOGGER.error('Unable to turn on program "%s"',
- self.rainmachine_id)
+ _LOGGER.error('Unable to turn on program "%s"', self.unique_id)
_LOGGER.debug(exc_info)
def _update(self) -> None:
@@ -248,25 +229,25 @@ class RainMachineProgram(RainMachineEntity):
import regenmaschine.exceptions as exceptions
try:
- self._entity_json = self._client.programs.get(self.rainmachine_id)
+ self._entity_json = self._client.programs.get(
+ self.rainmachine_entity_id)
except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to update info for program "%s"',
- self.rainmachine_id)
+ self.unique_id)
_LOGGER.debug(exc_info)
class RainMachineZone(RainMachineEntity):
"""A RainMachine zone."""
- def __init__(self, client, zone_json, zone_run_time, **kwargs):
+ def __init__(self, client, device_name, device_mac, zone_json,
+ zone_run_time):
"""Initialize a RainMachine zone."""
- super().__init__(client, zone_json, **kwargs)
+ super().__init__(client, device_name, device_mac, zone_json)
self._run_time = zone_run_time
self._attrs.update({
- ATTR_CYCLES:
- self._entity_json.get('noOfCycles'),
- ATTR_TOTAL_DURATION:
- self._entity_json.get('userDuration')
+ ATTR_CYCLES: self._entity_json.get('noOfCycles'),
+ ATTR_TOTAL_DURATION: self._entity_json.get('userDuration')
})
@property
@@ -279,14 +260,20 @@ class RainMachineZone(RainMachineEntity):
"""Return the name of the zone."""
return 'Zone: {}'.format(self._entity_json.get('name'))
+ @property
+ def unique_id(self) -> str:
+ """Return a unique, HASS-friendly identifier for this entity."""
+ return '{0}_zone_{1}'.format(
+ self.device_mac.replace(':', ''), self.rainmachine_entity_id)
+
def turn_off(self, **kwargs) -> None:
"""Turn the zone off."""
import regenmaschine.exceptions as exceptions
try:
- self._client.zones.stop(self.rainmachine_id)
+ self._client.zones.stop(self.rainmachine_entity_id)
except exceptions.HTTPError as exc_info:
- _LOGGER.error('Unable to turn off zone "%s"', self.rainmachine_id)
+ _LOGGER.error('Unable to turn off zone "%s"', self.unique_id)
_LOGGER.debug(exc_info)
def turn_on(self, **kwargs) -> None:
@@ -294,9 +281,10 @@ class RainMachineZone(RainMachineEntity):
import regenmaschine.exceptions as exceptions
try:
- self._client.zones.start(self.rainmachine_id, self._run_time)
+ self._client.zones.start(self.rainmachine_entity_id,
+ self._run_time)
except exceptions.HTTPError as exc_info:
- _LOGGER.error('Unable to turn on zone "%s"', self.rainmachine_id)
+ _LOGGER.error('Unable to turn on zone "%s"', self.unique_id)
_LOGGER.debug(exc_info)
def _update(self) -> None:
@@ -304,8 +292,9 @@ class RainMachineZone(RainMachineEntity):
import regenmaschine.exceptions as exceptions
try:
- self._entity_json = self._client.zones.get(self.rainmachine_id)
+ self._entity_json = self._client.zones.get(
+ self.rainmachine_entity_id)
except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to update info for zone "%s"',
- self.rainmachine_id)
+ self.unique_id)
_LOGGER.debug(exc_info)
diff --git a/homeassistant/components/switch/raspihats.py b/homeassistant/components/switch/raspihats.py
index 183ee6edb77..7be3a6f0baa 100644
--- a/homeassistant/components/switch/raspihats.py
+++ b/homeassistant/components/switch/raspihats.py
@@ -25,7 +25,7 @@ _CHANNELS_SCHEMA = vol.Schema([{
vol.Required(CONF_INDEX): cv.positive_int,
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_INVERT_LOGIC, default=False): cv.boolean,
- vol.Optional(CONF_INITIAL_STATE, default=None): cv.boolean,
+ vol.Optional(CONF_INITIAL_STATE): cv.boolean,
}])
_I2C_HATS_SCHEMA = vol.Schema([{
@@ -56,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
board, address, channel_config[CONF_INDEX],
channel_config[CONF_NAME],
channel_config[CONF_INVERT_LOGIC],
- channel_config[CONF_INITIAL_STATE]
+ channel_config.get(CONF_INITIAL_STATE)
)
)
except I2CHatsException as ex:
@@ -121,7 +121,7 @@ class I2CHatSwitch(ToggleEntity):
_LOGGER.error(self._log_message("Is ON check failed, " + str(ex)))
return False
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the device on."""
try:
state = True if self._invert_logic is False else False
@@ -130,7 +130,7 @@ class I2CHatSwitch(ToggleEntity):
except I2CHatsException as ex:
_LOGGER.error(self._log_message("Turn ON failed, " + str(ex)))
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the device off."""
try:
state = False if self._invert_logic is False else True
diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py
index c0f75509425..b68cc038e89 100644
--- a/homeassistant/components/switch/rest.py
+++ b/homeassistant/components/switch/rest.py
@@ -17,7 +17,6 @@ from homeassistant.const import (
CONF_PASSWORD)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
-from homeassistant.helpers.template import Template
_LOGGER = logging.getLogger(__name__)
@@ -26,8 +25,8 @@ CONF_BODY_ON = 'body_on'
CONF_IS_ON_TEMPLATE = 'is_on_template'
DEFAULT_METHOD = 'post'
-DEFAULT_BODY_OFF = Template('OFF')
-DEFAULT_BODY_ON = Template('ON')
+DEFAULT_BODY_OFF = 'OFF'
+DEFAULT_BODY_ON = 'ON'
DEFAULT_NAME = 'REST Switch'
DEFAULT_TIMEOUT = 10
diff --git a/homeassistant/components/switch/rpi_pfio.py b/homeassistant/components/switch/rpi_pfio.py
index a493a8e9589..c10f417ba49 100644
--- a/homeassistant/components/switch/rpi_pfio.py
+++ b/homeassistant/components/switch/rpi_pfio.py
@@ -26,7 +26,7 @@ CONF_PORTS = 'ports'
DEFAULT_INVERT_LOGIC = False
PORT_SCHEMA = vol.Schema({
- vol.Optional(ATTR_NAME, default=None): cv.string,
+ vol.Optional(ATTR_NAME): cv.string,
vol.Optional(ATTR_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
})
@@ -42,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
switches = []
ports = config.get(CONF_PORTS)
for port, port_entity in ports.items():
- name = port_entity[ATTR_NAME]
+ name = port_entity.get(ATTR_NAME)
invert_logic = port_entity[ATTR_INVERT_LOGIC]
switches.append(RPiPFIOSwitch(port, name, invert_logic))
@@ -75,13 +75,13 @@ class RPiPFIOSwitch(ToggleEntity):
"""Return true if device is on."""
return self._state
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the device on."""
rpi_pfio.write_output(self._port, 0 if self._invert_logic else 1)
self._state = True
self.schedule_update_ha_state()
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the device off."""
rpi_pfio.write_output(self._port, 1 if self._invert_logic else 0)
self._state = False
diff --git a/homeassistant/components/switch/rpi_rf.py b/homeassistant/components/switch/rpi_rf.py
index 94a61314d1d..40200f05806 100644
--- a/homeassistant/components/switch/rpi_rf.py
+++ b/homeassistant/components/switch/rpi_rf.py
@@ -44,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
-# pylint: disable=unused-argument, import-error
+# pylint: disable=unused-argument, import-error, no-member
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return switches controlled by a generic RF device via GPIO."""
import rpi_rf
@@ -117,13 +117,13 @@ class RPiRFSwitch(SwitchDevice):
self._rfdevice.tx_code(code, protocol, pulselength)
return True
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the switch on."""
if self._send_code(self._code_on, self._protocol, self._pulselength):
self._state = True
self.schedule_update_ha_state()
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
if self._send_code(self._code_off, self._protocol, self._pulselength):
self._state = False
diff --git a/homeassistant/components/switch/smappee.py b/homeassistant/components/switch/smappee.py
new file mode 100644
index 00000000000..fd8f141500b
--- /dev/null
+++ b/homeassistant/components/switch/smappee.py
@@ -0,0 +1,92 @@
+"""
+Support for interacting with Smappee Comport Plugs.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/switch.smappee/
+"""
+import logging
+
+from homeassistant.components.smappee import DATA_SMAPPEE
+from homeassistant.components.switch import (SwitchDevice)
+
+DEPENDENCIES = ['smappee']
+
+_LOGGER = logging.getLogger(__name__)
+
+ICON = 'mdi:power-plug'
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Smappee Comfort Plugs."""
+ smappee = hass.data[DATA_SMAPPEE]
+
+ dev = []
+ if smappee.is_remote_active:
+ for location_id in smappee.locations.keys():
+ for items in smappee.info[location_id].get('actuators'):
+ if items.get('name') != '':
+ _LOGGER.debug("Remote actuator %s", items)
+ dev.append(SmappeeSwitch(smappee,
+ items.get('name'),
+ location_id,
+ items.get('id')))
+ elif smappee.is_local_active:
+ for items in smappee.local_devices:
+ _LOGGER.debug("Local actuator %s", items)
+ dev.append(SmappeeSwitch(smappee,
+ items.get('value'),
+ None,
+ items.get('key')))
+ add_devices(dev)
+
+
+class SmappeeSwitch(SwitchDevice):
+ """Representation of a Smappee Comport Plug."""
+
+ def __init__(self, smappee, name, location_id, switch_id):
+ """Initialize a new Smappee Comfort Plug."""
+ self._name = name
+ self._state = False
+ self._smappee = smappee
+ self._location_id = location_id
+ self._switch_id = switch_id
+ self._remoteswitch = True
+ if location_id is None:
+ self._remoteswitch = False
+
+ @property
+ def name(self):
+ """Return the name of the switch."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return true if switch is on."""
+ return self._state
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return ICON
+
+ def turn_on(self, **kwargs):
+ """Turn on Comport Plug."""
+ if self._smappee.actuator_on(self._location_id, self._switch_id,
+ self._remoteswitch):
+ self._state = True
+
+ def turn_off(self, **kwargs):
+ """Turn off Comport Plug."""
+ if self._smappee.actuator_off(self._location_id, self._switch_id,
+ self._remoteswitch):
+ self._state = False
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the device."""
+ attr = {}
+ if self._remoteswitch:
+ attr['Location Id'] = self._location_id
+ attr['Location Name'] = self._smappee.locations[self._location_id]
+ attr['Switch Id'] = self._switch_id
+ return attr
diff --git a/homeassistant/components/switch/snmp.py b/homeassistant/components/switch/snmp.py
index 8aa9744b3da..b0c192cdafa 100644
--- a/homeassistant/components/switch/snmp.py
+++ b/homeassistant/components/switch/snmp.py
@@ -95,13 +95,13 @@ class SnmpSwitch(SwitchDevice):
self._payload_on = payload_on
self._payload_off = payload_off
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn on the switch."""
from pyasn1.type.univ import (Integer)
self._set(Integer(self._command_payload_on))
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn off the switch."""
from pyasn1.type.univ import (Integer)
diff --git a/homeassistant/components/switch/toon.py b/homeassistant/components/switch/toon.py
index 09dc45c6587..94086d819e2 100644
--- a/homeassistant/components/switch/toon.py
+++ b/homeassistant/components/switch/toon.py
@@ -64,7 +64,7 @@ class EnecoSmartPlug(SwitchDevice):
"""Turn the switch on."""
return self.smartplug.turn_on()
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
return self.smartplug.turn_off()
diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py
index 14faa98fb59..1eca5284f76 100644
--- a/homeassistant/components/switch/tplink.py
+++ b/homeassistant/components/switch/tplink.py
@@ -75,7 +75,7 @@ class SmartPlugSwitch(SwitchDevice):
"""Turn the switch on."""
self.smartplug.turn_on()
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
self.smartplug.turn_off()
diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py
index 710580c2ec6..810946a5058 100644
--- a/homeassistant/components/switch/verisure.py
+++ b/homeassistant/components/switch/verisure.py
@@ -60,13 +60,13 @@ class VerisureSmartplug(SwitchDevice):
"$.smartPlugs[?(@.deviceLabel == '%s')]",
self._device_label) is not None
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Set smartplug status on."""
hub.session.set_smartplug_state(self._device_label, True)
self._state = True
self._change_timestamp = time()
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Set smartplug status off."""
hub.session.set_smartplug_state(self._device_label, False)
self._state = False
diff --git a/homeassistant/components/switch/vultr.py b/homeassistant/components/switch/vultr.py
index a044fca2972..fe3d67470d7 100644
--- a/homeassistant/components/switch/vultr.py
+++ b/homeassistant/components/switch/vultr.py
@@ -90,12 +90,12 @@ class VultrSwitch(SwitchDevice):
ATTR_VCPUS: self.data.get('vcpu_count'),
}
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Boot-up the subscription."""
if self.data['power_status'] != 'running':
self._vultr.start(self.subscription)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Halt the subscription."""
if self.data['power_status'] == 'running':
self._vultr.halt(self.subscription)
diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py
index ecaff14e2e2..80102621f7d 100644
--- a/homeassistant/components/switch/wake_on_lan.py
+++ b/homeassistant/components/switch/wake_on_lan.py
@@ -78,7 +78,7 @@ class WOLSwitch(SwitchDevice):
"""Return the name of the switch."""
return self._name
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the device on."""
if self._broadcast_address:
self._wol.send_magic_packet(
@@ -86,7 +86,7 @@ class WOLSwitch(SwitchDevice):
else:
self._wol.send_magic_packet(self._mac_address)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the device off if an off action is present."""
if self._off_script is not None:
self._off_script.run()
diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py
index 5a43de9425c..6a244615065 100644
--- a/homeassistant/components/switch/wink.py
+++ b/homeassistant/components/switch/wink.py
@@ -54,7 +54,7 @@ class WinkToggleDevice(WinkDevice, ToggleEntity):
"""Turn the device on."""
self.wink.set_state(True)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the device off."""
self.wink.set_state(False)
diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py
index 578036a1677..1688b6b89e1 100644
--- a/homeassistant/components/switch/xiaomi_aqara.py
+++ b/homeassistant/components/switch/xiaomi_aqara.py
@@ -101,7 +101,7 @@ class XiaomiGenericSwitch(XiaomiDevice, SwitchDevice):
self._state = True
self.schedule_update_ha_state()
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the switch off."""
if self._write_to_hub(self._sid, **{self._data_key: 'off'}):
self._state = False
diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py
index ad71b3944cf..7defc3d3b2b 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.6']
+REQUIREMENTS = ['python-miio==0.3.7']
ATTR_POWER = 'power'
ATTR_TEMPERATURE = 'temperature'
diff --git a/homeassistant/components/switch/zoneminder.py b/homeassistant/components/switch/zoneminder.py
index 5dffd99c324..adf3bf2d9bd 100644
--- a/homeassistant/components/switch/zoneminder.py
+++ b/homeassistant/components/switch/zoneminder.py
@@ -75,14 +75,14 @@ class ZMSwitchMonitors(SwitchDevice):
"""Return True if entity is on."""
return self._state
- def turn_on(self):
+ def turn_on(self, **kwargs):
"""Turn the entity on."""
zoneminder.change_state(
'api/monitors/%i.json' % self._monitor_id,
{'Monitor[Function]': self._on_state}
)
- def turn_off(self):
+ def turn_off(self, **kwargs):
"""Turn the entity off."""
zoneminder.change_state(
'api/monitors/%i.json' % self._monitor_id,
diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py
index 28a54f40d56..b288a704d74 100644
--- a/homeassistant/components/tahoma.py
+++ b/homeassistant/components/tahoma.py
@@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['tahoma-api==0.0.11']
+REQUIREMENTS = ['tahoma-api==0.0.12']
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
TAHOMA_COMPONENTS = [
- 'sensor', 'cover'
+ 'scene', 'sensor', 'cover'
]
TAHOMA_TYPES = {
@@ -57,19 +57,21 @@ def setup(hass, config):
try:
api = TahomaApi(username, password)
except RequestException:
- _LOGGER.exception("Error communicating with Tahoma API")
+ _LOGGER.exception("Error when trying to log in to the Tahoma API")
return False
try:
api.get_setup()
devices = api.get_devices()
+ scenes = api.get_action_groups()
except RequestException:
- _LOGGER.exception("Cannot fetch information from Tahoma API")
+ _LOGGER.exception("Error when getting devices from the Tahoma API")
return False
hass.data[DOMAIN] = {
'controller': api,
- 'devices': defaultdict(list)
+ 'devices': defaultdict(list),
+ 'scenes': []
}
for device in devices:
@@ -82,6 +84,9 @@ def setup(hass, config):
continue
hass.data[DOMAIN]['devices'][device_type].append(_device)
+ for scene in scenes:
+ hass.data[DOMAIN]['scenes'].append(scene)
+
for component in TAHOMA_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, config)
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index 170e1517a6d..d4ac115d9c6 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -96,6 +96,7 @@ BASE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_DISABLE_WEB_PREV): cv.boolean,
vol.Optional(ATTR_KEYBOARD): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_KEYBOARD_INLINE): cv.ensure_list,
+ vol.Optional(CONF_TIMEOUT): vol.Coerce(float),
}, extra=vol.ALLOW_EXTRA)
SERVICE_SCHEMA_SEND_MESSAGE = BASE_SERVICE_SCHEMA.extend({
diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py
index 0ce11441843..bec239ba1dd 100644
--- a/homeassistant/components/telegram_bot/polling.py
+++ b/homeassistant/components/telegram_bot/polling.py
@@ -93,7 +93,7 @@ class TelegramPoll(BaseTelegramBotEntity):
_json = yield from resp.json()
return _json
else:
- raise WrongHttpStatus('wrong status %s', resp.status)
+ raise WrongHttpStatus('wrong status {}'.format(resp.status))
finally:
if resp is not None:
yield from resp.release()
diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py
index 055f68884a6..5c293459447 100644
--- a/homeassistant/components/telegram_bot/webhooks.py
+++ b/homeassistant/components/telegram_bot/webhooks.py
@@ -12,7 +12,7 @@ import logging
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
-from homeassistant.components.http.util import get_real_ip
+from homeassistant.components.http.const import KEY_REAL_IP
from homeassistant.components.telegram_bot import (
CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity, PLATFORM_SCHEMA)
from homeassistant.const import (
@@ -110,7 +110,7 @@ class BotPushReceiver(HomeAssistantView, BaseTelegramBotEntity):
@asyncio.coroutine
def post(self, request):
"""Accept the POST from telegram."""
- real_ip = get_real_ip(request)
+ real_ip = request[KEY_REAL_IP]
if not any(real_ip in net for net in self.trusted_networks):
_LOGGER.warning("Access denied from %s", real_ip)
return self.json_message('Access denied', HTTP_UNAUTHORIZED)
diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py
index 28bf65bc4c5..dfb4b1e5fa9 100644
--- a/homeassistant/components/tellduslive.py
+++ b/homeassistant/components/tellduslive.py
@@ -7,6 +7,8 @@ https://home-assistant.io/components/tellduslive/
from datetime import datetime, timedelta
import logging
+import voluptuous as vol
+
from homeassistant.const import (
ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME,
CONF_TOKEN, CONF_HOST,
@@ -18,7 +20,6 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.util.dt import utcnow
from homeassistant.util.json import load_json, save_json
-import voluptuous as vol
APPLICATION_NAME = 'Home Assistant'
@@ -352,8 +353,7 @@ class TelldusLiveEntity(Entity):
return None
elif self.device.battery == BATTERY_OK:
return 100
- else:
- return self.device.battery # Percentage
+ return self.device.battery # Percentage
@property
def _last_updated(self):
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index 532b4529eca..17aa66ea825 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -411,7 +411,7 @@ class SpeechManager(object):
if key not in self.mem_cache:
if key not in self.file_cache:
- raise HomeAssistantError("%s not in cache!", key)
+ raise HomeAssistantError("{} not in cache!".format(key))
yield from self.async_file_to_mem(key)
content, _ = mimetypes.guess_type(filename)
diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py
index 87990495cf4..960d8f3780e 100644
--- a/homeassistant/components/upnp.py
+++ b/homeassistant/components/upnp.py
@@ -51,7 +51,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
-# pylint: disable=import-error, no-member, broad-except
+# pylint: disable=import-error, no-member, broad-except, c-extension-no-member
def setup(hass, config):
"""Register a port mapping for Home Assistant via UPnP."""
config = config[DOMAIN]
diff --git a/homeassistant/components/usps.py b/homeassistant/components/usps.py
index 58f858b0975..364562f1119 100644
--- a/homeassistant/components/usps.py
+++ b/homeassistant/components/usps.py
@@ -15,7 +15,7 @@ from homeassistant.helpers import (config_validation as cv, discovery)
from homeassistant.util import Throttle
from homeassistant.util.dt import now
-REQUIREMENTS = ['myusps==1.2.2']
+REQUIREMENTS = ['myusps==1.3.2']
_LOGGER = logging.getLogger(__name__)
@@ -24,6 +24,7 @@ DATA_USPS = 'data_usps'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
COOKIE = 'usps_cookies.pickle'
CACHE = 'usps_cache'
+CONF_DRIVER = 'driver'
USPS_TYPE = ['sensor', 'camera']
@@ -32,6 +33,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DOMAIN): cv.string,
+ vol.Optional(CONF_DRIVER): cv.string
}),
}, extra=vol.ALLOW_EXTRA)
@@ -42,13 +44,15 @@ def setup(hass, config):
username = conf.get(CONF_USERNAME)
password = conf.get(CONF_PASSWORD)
name = conf.get(CONF_NAME)
+ driver = conf.get(CONF_DRIVER)
import myusps
try:
cookie = hass.config.path(COOKIE)
cache = hass.config.path(CACHE)
session = myusps.get_session(username, password,
- cookie_path=cookie, cache_path=cache)
+ cookie_path=cookie, cache_path=cache,
+ driver=driver)
except myusps.USPSError:
_LOGGER.exception('Could not connect to My USPS')
return False
diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py
index 500b98420fc..6485f0025e2 100644
--- a/homeassistant/components/vacuum/roomba.py
+++ b/homeassistant/components/vacuum/roomba.py
@@ -242,7 +242,7 @@ class RoombaVacuum(VacuumDevice):
self.vacuum.set_preference, 'vacHigh', str(high_perf))
@asyncio.coroutine
- def async_send_command(self, command, params, **kwargs):
+ def async_send_command(self, command, params=None, **kwargs):
"""Send raw command."""
_LOGGER.debug("async_send_command %s (%s), %s",
command, params, kwargs)
diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py
index bcfb3b6738e..55f166c4004 100644
--- a/homeassistant/components/vacuum/xiaomi_miio.py
+++ b/homeassistant/components/vacuum/xiaomi_miio.py
@@ -19,7 +19,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.6']
+REQUIREMENTS = ['python-miio==0.3.7']
_LOGGER = logging.getLogger(__name__)
@@ -341,9 +341,9 @@ class MiroboVacuum(VacuumDevice):
@asyncio.coroutine
def async_remote_control_move(self,
- rotation: int=0,
- velocity: float=0.3,
- duration: int=1500):
+ rotation: int = 0,
+ velocity: float = 0.3,
+ duration: int = 1500):
"""Move vacuum with remote control mode."""
yield from self._try_command(
"Unable to move with remote control the vacuum: %s",
@@ -352,9 +352,9 @@ class MiroboVacuum(VacuumDevice):
@asyncio.coroutine
def async_remote_control_move_step(self,
- rotation: int=0,
- velocity: float=0.2,
- duration: int=1500):
+ rotation: int = 0,
+ velocity: float = 0.2,
+ duration: int = 1500):
"""Move vacuum one step with remote control mode."""
yield from self._try_command(
"Unable to remote control the vacuum: %s",
diff --git a/homeassistant/components/velux.py b/homeassistant/components/velux.py
index b0c902aa83e..ad541ee9cfe 100644
--- a/homeassistant/components/velux.py
+++ b/homeassistant/components/velux.py
@@ -52,16 +52,13 @@ class VeluxModule:
def __init__(self, hass, config):
"""Initialize for velux component."""
from pyvlx import PyVLX
- self.initialized = False
host = config[DOMAIN].get(CONF_HOST)
password = config[DOMAIN].get(CONF_PASSWORD)
self.pyvlx = PyVLX(
host=host,
password=password)
- self.hass = hass
@asyncio.coroutine
def async_start(self):
"""Start velux component."""
yield from self.pyvlx.load_scenes()
- self.initialized = True
diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py
index b15c4ddabfd..a7c10462e0d 100644
--- a/homeassistant/components/vera.py
+++ b/homeassistant/components/vera.py
@@ -19,7 +19,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE)
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pyvera==0.2.39']
+REQUIREMENTS = ['pyvera==0.2.41']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py
index 3e36d0a3028..6557be2fb1b 100644
--- a/homeassistant/components/volvooncall.py
+++ b/homeassistant/components/volvooncall.py
@@ -4,10 +4,11 @@ Support for Volvo On Call.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/volvooncall/
"""
-
from datetime import timedelta
import logging
+import voluptuous as vol
+
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
CONF_NAME, CONF_RESOURCES)
from homeassistant.helpers import discovery
@@ -16,7 +17,6 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import track_point_in_utc_time
from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.util.dt import utcnow
-import voluptuous as vol
DOMAIN = 'volvooncall'
@@ -143,8 +143,7 @@ class VolvoData:
return vehicle.registration_number
elif vehicle.vin:
return vehicle.vin
- else:
- return ''
+ return ''
class VolvoEntity(Entity):
diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py
index f37914b3b0f..a49a1664eec 100644
--- a/homeassistant/components/weather/buienradar.py
+++ b/homeassistant/components/weather/buienradar.py
@@ -6,6 +6,9 @@ https://home-assistant.io/components/weather.buienradar/
"""
import logging
import asyncio
+
+import voluptuous as vol
+
from homeassistant.components.weather import (
WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME)
from homeassistant.const import \
@@ -14,9 +17,8 @@ from homeassistant.helpers import config_validation as cv
# Reuse data and API logic from the sensor implementation
from homeassistant.components.sensor.buienradar import (
BrData)
-import voluptuous as vol
-REQUIREMENTS = ['buienradar==0.9']
+REQUIREMENTS = ['buienradar==0.91']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py
index bbf9f1ae590..f9610e469b2 100644
--- a/homeassistant/components/weather/yweather.py
+++ b/homeassistant/components/weather/yweather.py
@@ -50,7 +50,7 @@ CONDITION_CLASSES = {
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
- vol.Optional(CONF_WOEID, default=None): cv.string,
+ vol.Optional(CONF_WOEID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
diff --git a/homeassistant/components/weblink.py b/homeassistant/components/weblink.py
index a20b0fc9b0c..cd87bd838fa 100644
--- a/homeassistant/components/weblink.py
+++ b/homeassistant/components/weblink.py
@@ -22,9 +22,10 @@ CONF_RELATIVE_URL_REGEX = r'\A/'
DOMAIN = 'weblink'
ENTITIES_SCHEMA = vol.Schema({
+ # pylint: disable=no-value-for-parameter
vol.Required(CONF_URL): vol.Any(
vol.Match(CONF_RELATIVE_URL_REGEX, msg=CONF_RELATIVE_URL_ERROR_MSG),
- cv.url),
+ vol.Url()),
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON): cv.icon,
})
diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py
index 030d1bee579..b79812a8dce 100644
--- a/homeassistant/components/websocket_api.py
+++ b/homeassistant/components/websocket_api.py
@@ -81,7 +81,7 @@ CALL_SERVICE_MESSAGE_SCHEMA = vol.Schema({
vol.Required('type'): TYPE_CALL_SERVICE,
vol.Required('domain'): str,
vol.Required('service'): str,
- vol.Optional('service_data', default=None): dict
+ vol.Optional('service_data'): dict
})
GET_STATES_MESSAGE_SCHEMA = vol.Schema({
@@ -451,7 +451,7 @@ class ActiveConnection:
def call_service_helper(msg):
"""Call a service and fire complete message."""
yield from self.hass.services.async_call(
- msg['domain'], msg['service'], msg['service_data'], True)
+ msg['domain'], msg['service'], msg.get('service_data'), True)
self.send_message_outside(result_message(msg['id']))
self.hass.async_add_job(call_service_helper(msg))
diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py
index 0dcca28e228..e5942f97139 100644
--- a/homeassistant/components/xiaomi_aqara.py
+++ b/homeassistant/components/xiaomi_aqara.py
@@ -21,8 +21,9 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util.dt import utcnow
+from homeassistant.util import slugify
-REQUIREMENTS = ['PyXiaomiGateway==0.8.0']
+REQUIREMENTS = ['PyXiaomiGateway==0.8.1']
_LOGGER = logging.getLogger(__name__)
@@ -68,7 +69,7 @@ SERVICE_SCHEMA_REMOVE_DEVICE = vol.Schema({
GATEWAY_CONFIG = vol.Schema({
vol.Optional(CONF_MAC, default=None): vol.Any(GW_MAC, None),
- vol.Optional(CONF_KEY, default=None):
+ vol.Optional(CONF_KEY):
vol.All(cv.string, vol.Length(min=16, max=16)),
vol.Optional(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=9898): cv.port,
@@ -90,11 +91,9 @@ def _fix_conf_defaults(config):
return config
-DEFAULT_GATEWAY_CONFIG = [{CONF_MAC: None, CONF_KEY: None}]
-
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
- vol.Optional(CONF_GATEWAYS, default=DEFAULT_GATEWAY_CONFIG):
+ vol.Optional(CONF_GATEWAYS, default={}):
vol.All(cv.ensure_list, [GATEWAY_CONFIG], [_fix_conf_defaults]),
vol.Optional(CONF_INTERFACE, default='any'): cv.string,
vol.Optional(CONF_DISCOVERY_RETRY, default=3): cv.positive_int
@@ -205,12 +204,13 @@ def setup(hass, config):
class XiaomiDevice(Entity):
"""Representation a base Xiaomi device."""
- def __init__(self, device, name, xiaomi_hub):
+ def __init__(self, device, device_type, xiaomi_hub):
"""Initialize the Xiaomi device."""
self._state = None
self._is_available = True
self._sid = device['sid']
- self._name = '{}_{}'.format(name, self._sid)
+ self._name = '{}_{}'.format(device_type, self._sid)
+ self._type = device_type
self._write_to_hub = xiaomi_hub.write_to_hub
self._get_from_hub = xiaomi_hub.get_from_hub
self._device_state_attributes = {}
@@ -219,6 +219,14 @@ class XiaomiDevice(Entity):
self.parse_data(device['data'], device['raw_data'])
self.parse_voltage(device['data'])
+ if hasattr(self, '_data_key') \
+ and self._data_key: # pylint: disable=no-member
+ self._unique_id = slugify("{}-{}".format(
+ self._data_key, # pylint: disable=no-member
+ self._sid))
+ else:
+ self._unique_id = slugify("{}-{}".format(self._type, self._sid))
+
def _add_push_data_job(self, *args):
self.hass.add_job(self.push_data, *args)
@@ -232,6 +240,11 @@ class XiaomiDevice(Entity):
"""Return the name of the device."""
return self._name
+ @property
+ def unique_id(self) -> str:
+ """Return an unique ID."""
+ return self._unique_id
+
@property
def available(self):
"""Return True if entity is available."""
diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py
index 3729ce8a153..bb29cb28b0f 100644
--- a/homeassistant/components/zha/__init__.py
+++ b/homeassistant/components/zha/__init__.py
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import asyncio
+import collections
import enum
import logging
@@ -44,8 +45,7 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
- vol.Optional(CONF_RADIO_TYPE, default=RadioType.ezsp):
- cv.enum(RadioType),
+ vol.Optional(CONF_RADIO_TYPE, default='ezsp'): cv.enum(RadioType),
CONF_USB_PATH: cv.string,
vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int,
CONF_DATABASE: cv.string,
@@ -55,13 +55,18 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
ATTR_DURATION = 'duration'
+ATTR_IEEE = 'ieee_address'
SERVICE_PERMIT = 'permit'
+SERVICE_REMOVE = 'remove'
SERVICE_SCHEMAS = {
SERVICE_PERMIT: vol.Schema({
vol.Optional(ATTR_DURATION, default=60):
vol.All(vol.Coerce(int), vol.Range(1, 254)),
}),
+ SERVICE_REMOVE: vol.Schema({
+ vol.Required(ATTR_IEEE): cv.string,
+ }),
}
@@ -116,6 +121,18 @@ def async_setup(hass, config):
hass.services.async_register(DOMAIN, SERVICE_PERMIT, permit,
schema=SERVICE_SCHEMAS[SERVICE_PERMIT])
+ @asyncio.coroutine
+ def remove(service):
+ """Remove a node from the network."""
+ from bellows.types import EmberEUI64, uint8_t
+ ieee = service.data.get(ATTR_IEEE)
+ ieee = EmberEUI64([uint8_t(p, base=16) for p in ieee.split(':')])
+ _LOGGER.info("Removing node %s", ieee)
+ yield from APPLICATION_CONTROLLER.remove(ieee)
+
+ hass.services.async_register(DOMAIN, SERVICE_REMOVE, remove,
+ schema=SERVICE_SCHEMAS[SERVICE_REMOVE])
+
return True
@@ -126,6 +143,7 @@ class ApplicationListener:
"""Initialize the listener."""
self._hass = hass
self._config = config
+ self._device_registry = collections.defaultdict(list)
hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {})
def device_joined(self, device):
@@ -147,7 +165,8 @@ class ApplicationListener:
def device_removed(self, device):
"""Handle device being removed from the network."""
- pass
+ for device_entity in self._device_registry[device.ieee]:
+ self._hass.async_add_job(device_entity.async_remove())
@asyncio.coroutine
def async_device_initialized(self, device, join):
@@ -164,7 +183,7 @@ class ApplicationListener:
component = None
profile_clusters = ([], [])
- device_key = '%s-%s' % (str(device.ieee), endpoint_id)
+ device_key = "{}-{}".format(device.ieee, endpoint_id)
node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(
device_key, {})
@@ -189,10 +208,12 @@ class ApplicationListener:
for c in profile_clusters[1]
if c in endpoint.out_clusters]
discovery_info = {
+ 'application_listener': self,
'endpoint': endpoint,
'in_clusters': {c.cluster_id: c for c in in_clusters},
'out_clusters': {c.cluster_id: c for c in out_clusters},
'new_join': join,
+ 'unique_id': device_key,
}
discovery_info.update(discovered_info)
self._hass.data[DISCOVERY_KEY][device_key] = discovery_info
@@ -213,14 +234,17 @@ class ApplicationListener:
continue
component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type]
+ cluster_key = "{}-{}".format(device_key, cluster_id)
discovery_info = {
+ 'application_listener': self,
'endpoint': endpoint,
'in_clusters': {cluster.cluster_id: cluster},
'out_clusters': {},
'new_join': join,
+ 'unique_id': cluster_key,
+ 'entity_suffix': '_{}'.format(cluster_id),
}
discovery_info.update(discovered_info)
- cluster_key = '%s-%s' % (device_key, cluster_id)
self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info
yield from discovery.async_load_platform(
@@ -231,6 +255,10 @@ class ApplicationListener:
self._config,
)
+ def register_entity(self, ieee, entity_obj):
+ """Record the creation of a hass entity associated with ieee."""
+ self._device_registry[ieee].append(entity_obj)
+
class Entity(entity.Entity):
"""A base class for ZHA entities."""
@@ -238,30 +266,32 @@ class Entity(entity.Entity):
_domain = None # Must be overridden by subclasses
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
- model, **kwargs):
+ model, application_listener, unique_id, **kwargs):
"""Init ZHA entity."""
self._device_state_attributes = {}
- ieeetail = ''.join([
- '%02x' % (o, ) for o in endpoint.device.ieee[-4:]
- ])
+ ieee = endpoint.device.ieee
+ ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer and model is not None:
- self.entity_id = '%s.%s_%s_%s_%s' % (
+ self.entity_id = "{}.{}_{}_{}_{}{}".format(
self._domain,
slugify(manufacturer),
slugify(model),
ieeetail,
endpoint.endpoint_id,
+ kwargs.get('entity_suffix', ''),
)
- self._device_state_attributes['friendly_name'] = '%s %s' % (
+ self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
else:
- self.entity_id = "%s.zha_%s_%s" % (
+ self.entity_id = "{}.zha_{}_{}{}".format(
self._domain,
ieeetail,
endpoint.endpoint_id,
+ kwargs.get('entity_suffix', ''),
)
+
for cluster in in_clusters.values():
cluster.add_listener(self)
for cluster in out_clusters.values():
@@ -270,6 +300,19 @@ class Entity(entity.Entity):
self._in_clusters = in_clusters
self._out_clusters = out_clusters
self._state = ha_const.STATE_UNKNOWN
+ self._unique_id = unique_id
+
+ application_listener.register_entity(ieee, self)
+
+ @property
+ def unique_id(self) -> str:
+ """Return an unique ID."""
+ return self._unique_id
+
+ @property
+ def device_state_attributes(self):
+ """Return device specific state attributes."""
+ return self._device_state_attributes
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
@@ -279,11 +322,6 @@ class Entity(entity.Entity):
"""Handle a ZDO command received on this cluster."""
pass
- @property
- def device_state_attributes(self):
- """Return device specific state attributes."""
- return self._device_state_attributes
-
@asyncio.coroutine
def _discover_endpoint_info(endpoint):
@@ -335,8 +373,7 @@ def get_discovery_info(hass, discovery_info):
discovery_key = discovery_info.get('discovery_key', None)
all_discovery_info = hass.data.get(DISCOVERY_KEY, {})
- discovery_info = all_discovery_info.get(discovery_key, None)
- return discovery_info
+ return all_discovery_info.get(discovery_key, None)
@asyncio.coroutine
diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py
index a8d4671ebf7..deaa1257396 100644
--- a/homeassistant/components/zha/const.py
+++ b/homeassistant/components/zha/const.py
@@ -15,15 +15,11 @@ def populate_data():
from zigpy.profiles import PROFILES, zha, zll
DEVICE_CLASS[zha.PROFILE_ID] = {
- zha.DeviceType.ON_OFF_SWITCH: 'switch',
zha.DeviceType.SMART_PLUG: 'switch',
zha.DeviceType.ON_OFF_LIGHT: 'light',
zha.DeviceType.DIMMABLE_LIGHT: 'light',
zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light',
- zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'light',
- zha.DeviceType.DIMMER_SWITCH: 'light',
- zha.DeviceType.COLOR_DIMMER_SWITCH: 'light',
}
DEVICE_CLASS[zll.PROFILE_ID] = {
zll.DeviceType.ON_OFF_LIGHT: 'light',
@@ -37,6 +33,7 @@ def populate_data():
SINGLE_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'switch',
+ zcl.clusters.measurement.RelativeHumidity: 'sensor',
zcl.clusters.measurement.TemperatureMeasurement: 'sensor',
zcl.clusters.security.IasZone: 'binary_sensor',
})
diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml
index a9ad0e7a1ca..4b1122b8167 100644
--- a/homeassistant/components/zha/services.yaml
+++ b/homeassistant/components/zha/services.yaml
@@ -6,3 +6,10 @@ permit:
duration:
description: Time to permit joins, in seconds
example: 60
+
+remove:
+ description: Remove a node from the ZigBee network.
+ fields:
+ ieee_address:
+ description: IEEE address of the node to remove
+ example: "00:0d:6f:00:05:7d:2d:34"
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 5e82ef1baa0..6507e2a74f6 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -95,10 +95,6 @@ conversation:
# Enables support for tracking state changes over time
history:
-# Tracked history is kept for 10 days
-recorder:
- purge_keep_days: 10
-
# View all events in a logbook
logbook:
@@ -166,7 +162,7 @@ def get_default_config_dir() -> str:
return os.path.join(data_dir, CONFIG_DIR_NAME)
-def ensure_config_exists(config_dir: str, detect_location: bool=True) -> str:
+def ensure_config_exists(config_dir: str, detect_location: bool = True) -> str:
"""Ensure a configuration file exists in given configuration directory.
Creating a default one if needed.
@@ -677,7 +673,7 @@ def async_check_ha_config_file(hass):
@callback
-def async_notify_setup_error(hass, component, link=False):
+def async_notify_setup_error(hass, component, display_link=False):
"""Print a persistent notification.
This method must be run in the event loop.
@@ -689,7 +685,7 @@ def async_notify_setup_error(hass, component, link=False):
if errors is None:
errors = hass.data[DATA_PERSISTENT_ERRORS] = {}
- errors[component] = errors.get(component) or link
+ errors[component] = errors.get(component) or display_link
message = 'The following components and platforms could not be set up:\n\n'
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
new file mode 100644
index 00000000000..7b5d23d284f
--- /dev/null
+++ b/homeassistant/config_entries.py
@@ -0,0 +1,516 @@
+"""The Config Manager is responsible for managing configuration for components.
+
+The Config Manager allows for creating config entries to be consumed by
+components. Each entry is created via a Config Flow Handler, as defined by each
+component.
+
+During startup, Home Assistant will setup the entries during the normal setup
+of a component. It will first call the normal setup and then call the method
+`async_setup_entry(hass, entry)` for each entry. The same method is called when
+Home Assistant is running while a config entry is created.
+
+## Config Flows
+
+A component needs to define a Config Handler to allow the user to create config
+entries for that component. A config flow will manage the creation of entries
+from user input, discovery or other sources (like hassio).
+
+When a config flow is started for a domain, the handler will be instantiated
+and receives a unique id. The instance of this handler will be reused for every
+interaction of the user with this flow. This makes it possible to store
+instance variables on the handler.
+
+Before instantiating the handler, Home Assistant will make sure to load all
+dependencies and install the requirements of the component.
+
+At a minimum, each config flow will have to define a version number and the
+'init' step.
+
+ @config_entries.HANDLERS.register(DOMAIN)
+ class ExampleConfigFlow(config_entries.ConfigFlowHandler):
+
+ VERSION = 1
+
+ async def async_step_init(self, user_input=None):
+ …
+
+The 'init' step is the first step of a flow and is called when a user
+starts a new flow. Each step has three different possible results: "Show Form",
+"Abort" and "Create Entry".
+
+### Show Form
+
+This will show a form to the user to fill in. You define the current step,
+a title, a description and the schema of the data that needs to be returned.
+
+ async def async_step_init(self, user_input=None):
+ # Use OrderedDict to guarantee order of the form shown to the user
+ data_schema = OrderedDict()
+ data_schema[vol.Required('username')] = str
+ data_schema[vol.Required('password')] = str
+
+ return self.async_show_form(
+ step_id='init',
+ title='Account Info',
+ data_schema=vol.Schema(data_schema)
+ )
+
+After the user has filled in the form, the step method will be called again and
+the user input is passed in. If the validation of the user input fails , you
+can return a dictionary with errors. Each key in the dictionary refers to a
+field name that contains the error. Use the key 'base' if you want to show a
+generic error.
+
+ async def async_step_init(self, user_input=None):
+ errors = None
+ if user_input is not None:
+ # Validate user input
+ if valid:
+ return self.create_entry(…)
+
+ errors['base'] = 'Unable to reach authentication server.'
+
+ return self.async_show_form(…)
+
+If the user input passes validation, you can again return one of the three
+return values. If you want to navigate the user to the next step, return the
+return value of that step:
+
+ return (await self.async_step_account())
+
+### Abort
+
+When the result is "Abort", a message will be shown to the user and the
+configuration flow is finished.
+
+ return self.async_abort(
+ reason='This device is not supported by Home Assistant.'
+ )
+
+### Create Entry
+
+When the result is "Create Entry", an entry will be created and stored in Home
+Assistant, a success message is shown to the user and the flow is finished.
+
+## Initializing a config flow from an external source
+
+You might want to initialize a config flow programmatically. For example, if
+we discover a device on the network that requires user interaction to finish
+setup. To do so, pass a source parameter and optional user input to the init
+step:
+
+ await hass.config_entries.flow.async_init(
+ 'hue', source='discovery', data=discovery_info)
+
+The config flow handler will need to add a step to support the source. The step
+should follow the same return values as a normal step.
+
+ async def async_step_discovery(info):
+
+If the result of the step is to show a form, the user will be able to continue
+the flow from the config panel.
+"""
+import asyncio
+import logging
+import os
+import uuid
+
+from .core import callback
+from .exceptions import HomeAssistantError
+from .setup import async_setup_component, async_process_deps_reqs
+from .util.json import load_json, save_json
+from .util.decorator import Registry
+
+
+_LOGGER = logging.getLogger(__name__)
+HANDLERS = Registry()
+# Components that have config flows. In future we will auto-generate this list.
+FLOWS = [
+ 'config_entry_example'
+]
+
+SOURCE_USER = 'user'
+SOURCE_DISCOVERY = 'discovery'
+
+PATH_CONFIG = '.config_entries.json'
+
+SAVE_DELAY = 1
+
+RESULT_TYPE_FORM = 'form'
+RESULT_TYPE_CREATE_ENTRY = 'create_entry'
+RESULT_TYPE_ABORT = 'abort'
+
+ENTRY_STATE_LOADED = 'loaded'
+ENTRY_STATE_SETUP_ERROR = 'setup_error'
+ENTRY_STATE_NOT_LOADED = 'not_loaded'
+ENTRY_STATE_FAILED_UNLOAD = 'failed_unload'
+
+
+class ConfigEntry:
+ """Hold a configuration entry."""
+
+ __slots__ = ('entry_id', 'version', 'domain', 'title', 'data', 'source',
+ 'state')
+
+ def __init__(self, version, domain, title, data, source, entry_id=None,
+ state=ENTRY_STATE_NOT_LOADED):
+ """Initialize a config entry."""
+ # Unique id of the config entry
+ self.entry_id = entry_id or uuid.uuid4().hex
+
+ # Version of the configuration.
+ self.version = version
+
+ # Domain the configuration belongs to
+ self.domain = domain
+
+ # Title of the configuration
+ self.title = title
+
+ # Config data
+ self.data = data
+
+ # Source of the configuration (user, discovery, cloud)
+ self.source = source
+
+ # State of the entry (LOADED, NOT_LOADED)
+ self.state = state
+
+ @asyncio.coroutine
+ def async_setup(self, hass, *, component=None):
+ """Set up an entry."""
+ if component is None:
+ component = getattr(hass.components, self.domain)
+
+ try:
+ result = yield from component.async_setup_entry(hass, self)
+
+ if not isinstance(result, bool):
+ _LOGGER.error('%s.async_config_entry did not return boolean',
+ self.domain)
+ result = False
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception('Error setting up entry %s for %s',
+ self.title, self.domain)
+ result = False
+
+ if result:
+ self.state = ENTRY_STATE_LOADED
+ else:
+ self.state = ENTRY_STATE_SETUP_ERROR
+
+ @asyncio.coroutine
+ def async_unload(self, hass):
+ """Unload an entry.
+
+ Returns if unload is possible and was successful.
+ """
+ component = getattr(hass.components, self.domain)
+
+ supports_unload = hasattr(component, 'async_unload_entry')
+
+ if not supports_unload:
+ return False
+
+ try:
+ result = yield from component.async_unload_entry(hass, self)
+
+ if not isinstance(result, bool):
+ _LOGGER.error('%s.async_unload_entry did not return boolean',
+ self.domain)
+ result = False
+
+ return result
+ except Exception: # pylint: disable=broad-except
+ _LOGGER.exception('Error unloading entry %s for %s',
+ self.title, self.domain)
+ self.state = ENTRY_STATE_FAILED_UNLOAD
+ return False
+
+ def as_dict(self):
+ """Return dictionary version of this entry."""
+ return {
+ 'entry_id': self.entry_id,
+ 'version': self.version,
+ 'domain': self.domain,
+ 'title': self.title,
+ 'data': self.data,
+ 'source': self.source,
+ }
+
+
+class ConfigError(HomeAssistantError):
+ """Error while configuring an account."""
+
+
+class UnknownEntry(ConfigError):
+ """Unknown entry specified."""
+
+
+class UnknownHandler(ConfigError):
+ """Unknown handler specified."""
+
+
+class UnknownFlow(ConfigError):
+ """Uknown flow specified."""
+
+
+class UnknownStep(ConfigError):
+ """Unknown step specified."""
+
+
+class ConfigEntries:
+ """Manage the configuration entries.
+
+ An instance of this object is available via `hass.config_entries`.
+ """
+
+ def __init__(self, hass, hass_config):
+ """Initialize the entry manager."""
+ self.hass = hass
+ self.flow = FlowManager(hass, hass_config, self._async_add_entry)
+ self._hass_config = hass_config
+ self._entries = None
+ self._sched_save = None
+
+ @callback
+ def async_domains(self):
+ """Return domains for which we have entries."""
+ seen = set()
+ result = []
+
+ for entry in self._entries:
+ if entry.domain not in seen:
+ seen.add(entry.domain)
+ result.append(entry.domain)
+
+ return result
+
+ @callback
+ def async_entries(self, domain=None):
+ """Return all entries or entries for a specific domain."""
+ if domain is None:
+ return list(self._entries)
+ return [entry for entry in self._entries if entry.domain == domain]
+
+ @asyncio.coroutine
+ def async_remove(self, entry_id):
+ """Remove an entry."""
+ found = None
+ for index, entry in enumerate(self._entries):
+ if entry.entry_id == entry_id:
+ found = index
+ break
+
+ if found is None:
+ raise UnknownEntry
+
+ entry = self._entries.pop(found)
+ self._async_schedule_save()
+
+ unloaded = yield from entry.async_unload(self.hass)
+
+ return {
+ 'require_restart': not unloaded
+ }
+
+ @asyncio.coroutine
+ def async_load(self):
+ """Load the config."""
+ path = self.hass.config.path(PATH_CONFIG)
+ if not os.path.isfile(path):
+ self._entries = []
+ return
+
+ entries = yield from self.hass.async_add_job(load_json, path)
+ self._entries = [ConfigEntry(**entry) for entry in entries]
+
+ @asyncio.coroutine
+ def _async_add_entry(self, entry):
+ """Add an entry."""
+ self._entries.append(entry)
+ self._async_schedule_save()
+
+ # Setup entry
+ if entry.domain in self.hass.config.components:
+ # Component already set up, just need to call setup_entry
+ yield from entry.async_setup(self.hass)
+ else:
+ # Setting up component will also load the entries
+ yield from async_setup_component(
+ self.hass, entry.domain, self._hass_config)
+
+ @callback
+ def _async_schedule_save(self):
+ """Schedule saving the entity registry."""
+ if self._sched_save is not None:
+ self._sched_save.cancel()
+
+ self._sched_save = self.hass.loop.call_later(
+ SAVE_DELAY, self.hass.async_add_job, self._async_save
+ )
+
+ @asyncio.coroutine
+ def _async_save(self):
+ """Save the entity registry to a file."""
+ self._sched_save = None
+ data = [entry.as_dict() for entry in self._entries]
+
+ yield from self.hass.async_add_job(
+ save_json, self.hass.config.path(PATH_CONFIG), data)
+
+
+class FlowManager:
+ """Manage all the config flows that are in progress."""
+
+ def __init__(self, hass, hass_config, async_add_entry):
+ """Initialize the flow manager."""
+ self.hass = hass
+ self._hass_config = hass_config
+ self._progress = {}
+ self._async_add_entry = async_add_entry
+
+ @callback
+ def async_progress(self):
+ """Return the flows in progress."""
+ return [{
+ 'flow_id': flow.flow_id,
+ 'domain': flow.domain,
+ 'source': flow.source,
+ } for flow in self._progress.values()]
+
+ @asyncio.coroutine
+ def async_init(self, domain, *, source=SOURCE_USER, data=None):
+ """Start a configuration flow."""
+ handler = HANDLERS.get(domain)
+
+ if handler is None:
+ # This will load the component and thus register the handler
+ component = getattr(self.hass.components, domain)
+ handler = HANDLERS.get(domain)
+
+ if handler is None:
+ raise self.hass.helpers.UnknownHandler
+
+ # Make sure requirements and dependencies of component are resolved
+ yield from async_process_deps_reqs(
+ self.hass, self._hass_config, domain, component)
+
+ flow_id = uuid.uuid4().hex
+ flow = self._progress[flow_id] = handler()
+ flow.hass = self.hass
+ flow.domain = domain
+ flow.flow_id = flow_id
+ flow.source = source
+
+ if source == SOURCE_USER:
+ step = 'init'
+ else:
+ step = source
+
+ return (yield from self._async_handle_step(flow, step, data))
+
+ @asyncio.coroutine
+ def async_configure(self, flow_id, user_input=None):
+ """Start or continue a configuration flow."""
+ flow = self._progress.get(flow_id)
+
+ if flow is None:
+ raise UnknownFlow
+
+ step_id, data_schema = flow.cur_step
+
+ if data_schema is not None and user_input is not None:
+ user_input = data_schema(user_input)
+
+ return (yield from self._async_handle_step(
+ flow, step_id, user_input))
+
+ @callback
+ def async_abort(self, flow_id):
+ """Abort a flow."""
+ if self._progress.pop(flow_id, None) is None:
+ raise UnknownFlow
+
+ @asyncio.coroutine
+ def _async_handle_step(self, flow, step_id, user_input):
+ """Handle a step of a flow."""
+ method = "async_step_{}".format(step_id)
+
+ if not hasattr(flow, method):
+ self._progress.pop(flow.flow_id)
+ raise UnknownStep("Handler {} doesn't support step {}".format(
+ flow.__class__.__name__, step_id))
+
+ result = yield from getattr(flow, method)(user_input)
+
+ if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY,
+ RESULT_TYPE_ABORT):
+ raise ValueError(
+ 'Handler returned incorrect type: {}'.format(result['type']))
+
+ if result['type'] == RESULT_TYPE_FORM:
+ flow.cur_step = (result.pop('step_id'), result['data_schema'])
+ return result
+
+ # Abort and Success results both finish the flow
+ self._progress.pop(flow.flow_id)
+
+ if result['type'] == RESULT_TYPE_ABORT:
+ return result
+
+ entry = ConfigEntry(
+ version=flow.VERSION,
+ domain=flow.domain,
+ title=result['title'],
+ data=result.pop('data'),
+ source=flow.source
+ )
+ yield from self._async_add_entry(entry)
+ return result
+
+
+class ConfigFlowHandler:
+ """Handle the configuration flow of a component."""
+
+ # Set by flow manager
+ flow_id = None
+ hass = None
+ source = SOURCE_USER
+ cur_step = None
+
+ # Set by dev
+ # VERSION
+
+ @callback
+ def async_show_form(self, *, title, step_id, description=None,
+ data_schema=None, errors=None):
+ """Return the definition of a form to gather user input."""
+ return {
+ 'type': RESULT_TYPE_FORM,
+ 'flow_id': self.flow_id,
+ 'title': title,
+ 'step_id': step_id,
+ 'description': description,
+ 'data_schema': data_schema,
+ 'errors': errors,
+ }
+
+ @callback
+ def async_create_entry(self, *, title, data):
+ """Finish config flow and create a config entry."""
+ return {
+ 'type': RESULT_TYPE_CREATE_ENTRY,
+ 'flow_id': self.flow_id,
+ 'title': title,
+ 'data': data,
+ }
+
+ @callback
+ def async_abort(self, *, reason):
+ """Abort the config flow."""
+ return {
+ 'type': RESULT_TYPE_ABORT,
+ 'flow_id': self.flow_id,
+ 'reason': reason
+ }
diff --git a/homeassistant/const.py b/homeassistant/const.py
index eb90a19c778..10c29d19107 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,8 +1,8 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 63
-PATCH_VERSION = '3'
+MINOR_VERSION = 64
+PATCH_VERSION = '0.dev0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2)
@@ -76,6 +76,7 @@ CONF_FILENAME = 'filename'
CONF_FOR = 'for'
CONF_FORCE_UPDATE = 'force_update'
CONF_FRIENDLY_NAME = 'friendly_name'
+CONF_FRIENDLY_NAME_TEMPLATE = 'friendly_name_template'
CONF_HEADERS = 'headers'
CONF_HOST = 'host'
CONF_HOSTS = 'hosts'
diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py
index 87b84a80815..bad6bfe83c3 100644
--- a/homeassistant/helpers/condition.py
+++ b/homeassistant/helpers/condition.py
@@ -13,7 +13,7 @@ from homeassistant.const import (
CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION,
WEEKDAYS, CONF_STATE, CONF_ZONE, CONF_BEFORE,
CONF_AFTER, CONF_WEEKDAY, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET,
- CONF_BELOW, CONF_ABOVE)
+ CONF_BELOW, CONF_ABOVE, STATE_UNAVAILABLE, STATE_UNKNOWN)
from homeassistant.exceptions import TemplateError, HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.sun import get_astral_event_date
@@ -47,7 +47,7 @@ def _threaded_factory(async_factory):
return factory
-def async_from_config(config: ConfigType, config_validation: bool=True):
+def async_from_config(config: ConfigType, config_validation: bool = True):
"""Turn a condition configuration into a method.
Should be run on the event loop.
@@ -70,7 +70,7 @@ def async_from_config(config: ConfigType, config_validation: bool=True):
from_config = _threaded_factory(async_from_config)
-def async_and_from_config(config: ConfigType, config_validation: bool=True):
+def async_and_from_config(config: ConfigType, config_validation: bool = True):
"""Create multi condition matcher using 'AND'."""
if config_validation:
config = cv.AND_CONDITION_SCHEMA(config)
@@ -101,7 +101,7 @@ def async_and_from_config(config: ConfigType, config_validation: bool=True):
and_from_config = _threaded_factory(async_and_from_config)
-def async_or_from_config(config: ConfigType, config_validation: bool=True):
+def async_or_from_config(config: ConfigType, config_validation: bool = True):
"""Create multi condition matcher using 'OR'."""
if config_validation:
config = cv.OR_CONDITION_SCHEMA(config)
@@ -160,6 +160,9 @@ def async_numeric_state(hass: HomeAssistant, entity, below=None, above=None,
_LOGGER.error("Template error: %s", ex)
return False
+ if value in (STATE_UNAVAILABLE, STATE_UNKNOWN):
+ return False
+
try:
value = float(value)
except ValueError:
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index 6b882d2fdad..04719e89187 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -22,8 +22,8 @@ SLOW_UPDATE_WARNING = 10
def generate_entity_id(entity_id_format: str, name: Optional[str],
- current_ids: Optional[List[str]]=None,
- hass: Optional[HomeAssistant]=None) -> str:
+ current_ids: Optional[List[str]] = None,
+ hass: Optional[HomeAssistant] = None) -> str:
"""Generate a unique entity ID based on given entity IDs or used IDs."""
if current_ids is None:
if hass is None:
@@ -42,8 +42,8 @@ def generate_entity_id(entity_id_format: str, name: Optional[str],
@callback
def async_generate_entity_id(entity_id_format: str, name: Optional[str],
- current_ids: Optional[List[str]]=None,
- hass: Optional[HomeAssistant]=None) -> str:
+ current_ids: Optional[List[str]] = None,
+ hass: Optional[HomeAssistant] = None) -> str:
"""Generate a unique entity ID based on given entity IDs or used IDs."""
if current_ids is None:
if hass is None:
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 6cf58212c8e..e17e178bcfb 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -216,6 +216,14 @@ class EntityPlatform(object):
entry = registry.async_get_or_create(
self.domain, self.platform_name, entity.unique_id,
suggested_object_id=suggested_object_id)
+
+ if entry.disabled:
+ self.logger.info(
+ "Not adding entity %s because it's disabled",
+ entry.name or entity.name or
+ '"{} {}"'.format(self.platform_name, entity.unique_id))
+ return
+
entity.entity_id = entry.entity_id
entity.registry_name = entry.name
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
index d33ca93f290..89719b0b823 100644
--- a/homeassistant/helpers/entity_registry.py
+++ b/homeassistant/helpers/entity_registry.py
@@ -26,6 +26,9 @@ PATH_REGISTRY = 'entity_registry.yaml'
SAVE_DELAY = 10
_LOGGER = logging.getLogger(__name__)
+DISABLED_HASS = 'hass'
+DISABLED_USER = 'user'
+
@attr.s(slots=True, frozen=True)
class RegistryEntry:
@@ -35,12 +38,20 @@ class RegistryEntry:
unique_id = attr.ib(type=str)
platform = attr.ib(type=str)
name = attr.ib(type=str, default=None)
+ disabled_by = attr.ib(
+ type=str, default=None,
+ validator=attr.validators.in_((DISABLED_HASS, DISABLED_USER, None)))
domain = attr.ib(type=str, default=None, init=False, repr=False)
def __attrs_post_init__(self):
"""Computed properties."""
object.__setattr__(self, "domain", split_entity_id(self.entity_id)[0])
+ @property
+ def disabled(self):
+ """Return if entry is disabled."""
+ return self.disabled_by is not None
+
class EntityRegistry:
"""Class to hold a registry of entities."""
@@ -116,7 +127,8 @@ class EntityRegistry:
entity_id=entity_id,
unique_id=info['unique_id'],
platform=info['platform'],
- name=info.get('name')
+ name=info.get('name'),
+ disabled_by=info.get('disabled_by')
)
self.entities = entities
diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py
index f78c70e57d3..c9554488aa7 100644
--- a/homeassistant/helpers/entityfilter.py
+++ b/homeassistant/helpers/entityfilter.py
@@ -74,8 +74,7 @@ def generate_filter(include_domains, include_entities,
domain = split_entity_id(entity_id)[0]
if domain in include_d:
return entity_id not in exclude_e
- else:
- return entity_id in include_e
+ return entity_id in include_e
return entity_filter_4a
@@ -88,8 +87,7 @@ def generate_filter(include_domains, include_entities,
domain = split_entity_id(entity_id)[0]
if domain in exclude_d:
return entity_id in include_e
- else:
- return entity_id not in exclude_e
+ return entity_id not in exclude_e
return entity_filter_4b
diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py
index e4c78fcbed2..e3fb983f691 100644
--- a/homeassistant/helpers/icon.py
+++ b/homeassistant/helpers/icon.py
@@ -2,8 +2,8 @@
from typing import Optional
-def icon_for_battery_level(battery_level: Optional[int]=None,
- charging: bool=False) -> str:
+def icon_for_battery_level(battery_level: Optional[int] = None,
+ charging: bool = False) -> str:
"""Return a battery icon valid identifier."""
icon = 'mdi:battery'
if battery_level is None:
diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py
index 0cf9d83863f..bf2773d32b8 100644
--- a/homeassistant/helpers/intent.py
+++ b/homeassistant/helpers/intent.py
@@ -1,20 +1,27 @@
"""Module to coordinate user intentions."""
import asyncio
import logging
+import re
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
+from homeassistant.helpers import config_validation as cv
from homeassistant.loader import bind_hass
+from homeassistant.const import ATTR_ENTITY_ID
-
-DATA_KEY = 'intent'
_LOGGER = logging.getLogger(__name__)
+INTENT_TURN_OFF = 'HassTurnOff'
+INTENT_TURN_ON = 'HassTurnOn'
+INTENT_TOGGLE = 'HassToggle'
+
SLOT_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
+DATA_KEY = 'intent'
+
SPEECH_TYPE_PLAIN = 'plain'
SPEECH_TYPE_SSML = 'ssml'
@@ -87,7 +94,7 @@ class IntentHandler:
intent_type = None
slot_schema = None
_slot_schema = None
- platforms = None
+ platforms = []
@callback
def async_can_handle(self, intent_obj):
@@ -117,6 +124,67 @@ class IntentHandler:
return '<{} - {}>'.format(self.__class__.__name__, self.intent_type)
+def fuzzymatch(name, entities):
+ """Fuzzy matching function."""
+ matches = []
+ pattern = '.*?'.join(name)
+ regex = re.compile(pattern, re.IGNORECASE)
+ for entity_id, entity_name in entities.items():
+ match = regex.search(entity_name)
+ if match:
+ matches.append((len(match.group()), match.start(), entity_id))
+ return [x for _, _, x in sorted(matches)]
+
+
+class ServiceIntentHandler(IntentHandler):
+ """Service Intent handler registration.
+
+ Service specific intent handler that calls a service by name/entity_id.
+ """
+
+ slot_schema = {
+ 'name': cv.string,
+ }
+
+ def __init__(self, intent_type, domain, service, speech):
+ """Create Service Intent Handler."""
+ self.intent_type = intent_type
+ self.domain = domain
+ self.service = service
+ self.speech = speech
+
+ @asyncio.coroutine
+ def async_handle(self, intent_obj):
+ """Handle the hass intent."""
+ hass = intent_obj.hass
+ slots = self.async_validate_slots(intent_obj.slots)
+ response = intent_obj.create_response()
+
+ name = slots['name']['value']
+ entities = {state.entity_id: state.name for state
+ in hass.states.async_all()}
+
+ matches = fuzzymatch(name, entities)
+ entity_id = matches[0] if matches else None
+ _LOGGER.debug("%s matched entity: %s", name, entity_id)
+
+ response = intent_obj.create_response()
+ if not entity_id:
+ response.async_set_speech(
+ "Could not find entity id matching {}.".format(name))
+ _LOGGER.error("Could not find entity id matching %s", name)
+ return response
+
+ yield from hass.services.async_call(
+ self.domain, self.service, {
+ ATTR_ENTITY_ID: entity_id
+ })
+
+ response.async_set_speech(
+ self.speech.format(name))
+ return response
+
+
class Intent:
"""Hold the intent."""
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index 1ef9aa15674..7a989267572 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -33,7 +33,7 @@ CONF_WAIT_TEMPLATE = 'wait_template'
def call_from_config(hass: HomeAssistant, config: ConfigType,
- variables: Optional[Sequence]=None) -> None:
+ variables: Optional[Sequence] = None) -> None:
"""Call a script based on a config entry."""
Script(hass, cv.SCRIPT_SCHEMA(config)).run(variables)
@@ -41,7 +41,7 @@ def call_from_config(hass: HomeAssistant, config: ConfigType,
class Script():
"""Representation of a script."""
- def __init__(self, hass: HomeAssistant, sequence, name: str=None,
+ def __init__(self, hass: HomeAssistant, sequence, name: str = None,
change_listener=None) -> None:
"""Initialize the script."""
self.hass = hass
@@ -69,7 +69,7 @@ class Script():
self.async_run(variables), self.hass.loop).result()
@asyncio.coroutine
- def async_run(self, variables: Optional[Sequence]=None) -> None:
+ def async_run(self, variables: Optional[Sequence] = None) -> None:
"""Run script.
This method is a coroutine.
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index b381e1c2b0e..6fab1c6c844 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -271,8 +271,7 @@ class TemplateState(State):
"""Return an attribute of the state."""
if name in TemplateState.__dict__:
return object.__getattribute__(self, name)
- else:
- return getattr(object.__getattribute__(self, '_state'), name)
+ return getattr(object.__getattribute__(self, '_state'), name)
def __repr__(self):
"""Representation of Template State."""
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 7d182aebfa3..2eb42b94389 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -3,7 +3,7 @@ pyyaml>=3.11,<4
pytz>=2017.02
pip>=8.0.3
jinja2>=2.10
-voluptuous==0.10.5
+voluptuous==0.11.1
typing>=3,<4
aiohttp==2.3.10
yarl==1.1.0
@@ -13,5 +13,5 @@ astral==1.5
certifi>=2017.4.17
attrs==17.4.0
-# Breaks Python 3.6 and is not needed for our supported Pythons
+# Breaks Python 3.6 and is not needed for our supported Python versions
enum34==1000000000.0.0
diff --git a/homeassistant/remote.py b/homeassistant/remote.py
index 7d032303548..566f37a621a 100644
--- a/homeassistant/remote.py
+++ b/homeassistant/remote.py
@@ -45,8 +45,9 @@ class APIStatus(enum.Enum):
class API(object):
"""Object to pass around Home Assistant API location and credentials."""
- def __init__(self, host: str, api_password: Optional[str]=None,
- port: Optional[int]=SERVER_PORT, use_ssl: bool=False) -> None:
+ def __init__(self, host: str, api_password: Optional[str] = None,
+ port: Optional[int] = SERVER_PORT,
+ use_ssl: bool = False) -> None:
"""Init the API."""
self.host = host
self.port = port
@@ -68,7 +69,7 @@ class API(object):
if api_password is not None:
self._headers[HTTP_HEADER_HA_AUTH] = api_password
- def validate_api(self, force_validate: bool=False) -> bool:
+ def validate_api(self, force_validate: bool = False) -> bool:
"""Test if we can communicate with the API."""
if self.status is None or force_validate:
self.status = validate_api(self)
diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py
index 470040b8295..834334b8a90 100644
--- a/homeassistant/scripts/benchmark/__init__.py
+++ b/homeassistant/scripts/benchmark/__init__.py
@@ -144,3 +144,53 @@ def async_million_state_changed_helper(hass):
yield from event.wait()
return timer() - start
+
+
+@benchmark
+@asyncio.coroutine
+def logbook_filtering_state(hass):
+ """Filter state changes."""
+ return _logbook_filtering(hass, 1, 1)
+
+
+@benchmark
+@asyncio.coroutine
+def logbook_filtering_attributes(hass):
+ """Filter attribute changes."""
+ return _logbook_filtering(hass, 1, 2)
+
+
+@benchmark
+@asyncio.coroutine
+def _logbook_filtering(hass, last_changed, last_updated):
+ from homeassistant.components import logbook
+
+ entity_id = 'test.entity'
+
+ old_state = {
+ 'entity_id': entity_id,
+ 'state': 'off'
+ }
+
+ new_state = {
+ 'entity_id': entity_id,
+ 'state': 'on',
+ 'last_updated': last_updated,
+ 'last_changed': last_changed
+ }
+
+ event = core.Event(EVENT_STATE_CHANGED, {
+ 'entity_id': entity_id,
+ 'old_state': old_state,
+ 'new_state': new_state
+ })
+
+ events = [event] * 10**5
+
+ start = timer()
+
+ # pylint: disable=protected-access
+ events = logbook._exclude_events(events, {})
+ list(logbook.humanify(events))
+
+ return timer() - start
diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py
index 5cfcf628ec5..ec55b1d70c5 100644
--- a/homeassistant/scripts/check_config.py
+++ b/homeassistant/scripts/check_config.py
@@ -1,4 +1,5 @@
"""Script to ensure a configuration file exists."""
+import asyncio
import argparse
import logging
import os
@@ -30,15 +31,14 @@ MOCKS = {
config_util._log_pkg_error),
'logger_exception': ("homeassistant.setup._LOGGER.error",
setup._LOGGER.error),
+ 'logger_exception_bootstrap': ("homeassistant.bootstrap._LOGGER.error",
+ bootstrap._LOGGER.error),
}
SILENCE = (
+ 'homeassistant.bootstrap.async_enable_logging',
'homeassistant.bootstrap.clear_secret_cache',
'homeassistant.bootstrap.async_register_signal_handling',
- 'homeassistant.core._LOGGER.info',
- 'homeassistant.loader._LOGGER.info',
- 'homeassistant.bootstrap._LOGGER.info',
- 'homeassistant.bootstrap._LOGGER.warning',
- 'homeassistant.util.yaml._LOGGER.debug',
+ 'homeassistant.config.process_ha_config_upgrade',
)
PATCHES = {}
@@ -46,6 +46,12 @@ C_HEAD = 'bold'
ERROR_STR = 'General Errors'
+@asyncio.coroutine
+def mock_coro(*args):
+ """Coroutine that returns None."""
+ return None
+
+
def color(the_color, *args, reset=None):
"""Color helper."""
from colorlog.escape_codes import escape_codes, parse_colors
@@ -153,6 +159,11 @@ def run(script_args: List) -> int:
def check(config_path):
"""Perform a check by mocking hass load functions."""
+ logging.getLogger('homeassistant.core').setLevel(logging.WARNING)
+ logging.getLogger('homeassistant.loader').setLevel(logging.WARNING)
+ logging.getLogger('homeassistant.setup').setLevel(logging.WARNING)
+ logging.getLogger('homeassistant.bootstrap').setLevel(logging.ERROR)
+ logging.getLogger('homeassistant.util.yaml').setLevel(logging.INFO)
res = {
'yaml_files': OrderedDict(), # yaml_files loaded
'secrets': OrderedDict(), # secret cache and secrets loaded
@@ -170,11 +181,12 @@ def check(config_path):
# pylint: disable=unused-variable
def mock_get(comp_name):
"""Mock hass.loader.get_component to replace setup & setup_platform."""
- def mock_setup(*kwargs):
+ @asyncio.coroutine
+ def mock_async_setup(*args):
"""Mock setup, only record the component name & config."""
assert comp_name not in res['components'], \
"Components should contain a list of platforms"
- res['components'][comp_name] = kwargs[1].get(comp_name)
+ res['components'][comp_name] = args[1].get(comp_name)
return True
module = MOCKS['get'][1](comp_name)
@@ -187,15 +199,15 @@ def check(config_path):
# Test if platform/component and overwrite setup
if '.' in comp_name:
- module.setup_platform = mock_setup
+ module.async_setup_platform = mock_async_setup
- if hasattr(module, 'async_setup_platform'):
- del module.async_setup_platform
+ if hasattr(module, 'setup_platform'):
+ del module.setup_platform
else:
- module.setup = mock_setup
+ module.async_setup = mock_async_setup
- if hasattr(module, 'async_setup'):
- del module.async_setup
+ if hasattr(module, 'setup'):
+ del module.setup
return module
@@ -229,9 +241,14 @@ def check(config_path):
res['except'].setdefault(ERROR_STR, []).append(msg % params)
MOCKS['logger_exception'][1](msg, *params)
+ def mock_logger_exception_bootstrap(msg, *params):
+ """Log logger.exceptions."""
+ res['except'].setdefault(ERROR_STR, []).append(msg % params)
+ MOCKS['logger_exception_bootstrap'][1](msg, *params)
+
# Patches to skip functions
for sil in SILENCE:
- PATCHES[sil] = patch(sil)
+ PATCHES[sil] = patch(sil, return_value=mock_coro())
# Patches with local mock functions
for key, val in MOCKS.items():
diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py
index 12516e55c7d..84ba20619d8 100644
--- a/homeassistant/scripts/credstash.py
+++ b/homeassistant/scripts/credstash.py
@@ -24,7 +24,7 @@ def run(args):
'value', help="The value to save when putting a secret",
nargs='?', default=None)
- # pylint: disable=import-error
+ # pylint: disable=import-error, no-member
import credstash
import botocore
diff --git a/homeassistant/scripts/db_migrator.py b/homeassistant/scripts/db_migrator.py
index bf4dddc94fe..419f1138bf0 100644
--- a/homeassistant/scripts/db_migrator.py
+++ b/homeassistant/scripts/db_migrator.py
@@ -23,8 +23,9 @@ def ts_to_dt(timestamp: Optional[float]) -> Optional[datetime]:
# Based on code at
# http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
-def print_progress(iteration: int, total: int, prefix: str='', suffix: str='',
- decimals: int=2, bar_length: int=68) -> None:
+def print_progress(iteration: int, total: int, prefix: str = '',
+ suffix: str = '', decimals: int = 2,
+ bar_length: int = 68) -> None:
"""Print progress bar.
Call in a loop to create terminal progress bar
diff --git a/homeassistant/scripts/influxdb_import.py b/homeassistant/scripts/influxdb_import.py
index e91aeb8a0d7..421e84d503a 100644
--- a/homeassistant/scripts/influxdb_import.py
+++ b/homeassistant/scripts/influxdb_import.py
@@ -257,8 +257,9 @@ def run(script_args: List) -> int:
# Based on code at
# http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
-def print_progress(iteration: int, total: int, prefix: str='', suffix: str='',
- decimals: int=2, bar_length: int=68) -> None:
+def print_progress(iteration: int, total: int, prefix: str = '',
+ suffix: str = '', decimals: int = 2,
+ bar_length: int = 68) -> None:
"""Print progress bar.
Call in a loop to create terminal progress bar
diff --git a/homeassistant/scripts/influxdb_migrator.py b/homeassistant/scripts/influxdb_migrator.py
index f41240bad74..a4c0df74b09 100644
--- a/homeassistant/scripts/influxdb_migrator.py
+++ b/homeassistant/scripts/influxdb_migrator.py
@@ -8,8 +8,9 @@ from typing import List
# Based on code at
# http://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
-def print_progress(iteration: int, total: int, prefix: str='', suffix: str='',
- decimals: int=2, bar_length: int=68) -> None:
+def print_progress(iteration: int, total: int, prefix: str = '',
+ suffix: str = '', decimals: int = 2,
+ bar_length: int = 68) -> None:
"""Print progress bar.
Call in a loop to create terminal progress bar
diff --git a/homeassistant/setup.py b/homeassistant/setup.py
index 5cff2cbc6f5..5a8681e82fd 100644
--- a/homeassistant/setup.py
+++ b/homeassistant/setup.py
@@ -24,7 +24,7 @@ SLOW_SETUP_WARNING = 10
def setup_component(hass: core.HomeAssistant, domain: str,
- config: Optional[Dict]=None) -> bool:
+ config: Optional[Dict] = None) -> bool:
"""Set up a component and all its dependencies."""
return run_coroutine_threadsafe(
async_setup_component(hass, domain, config), loop=hass.loop).result()
@@ -32,7 +32,7 @@ def setup_component(hass: core.HomeAssistant, domain: str,
@asyncio.coroutine
def async_setup_component(hass: core.HomeAssistant, domain: str,
- config: Optional[Dict]=None) -> bool:
+ config: Optional[Dict] = None) -> bool:
"""Set up a component and all its dependencies.
This method is a coroutine.
@@ -123,7 +123,7 @@ def _async_setup_component(hass: core.HomeAssistant,
return False
try:
- yield from _process_deps_reqs(hass, config, domain, component)
+ yield from async_process_deps_reqs(hass, config, domain, component)
except HomeAssistantError as err:
log_error(str(err))
return False
@@ -165,6 +165,9 @@ def _async_setup_component(hass: core.HomeAssistant,
loader.set_component(domain, None)
return False
+ for entry in hass.config_entries.async_entries(domain):
+ yield from entry.async_setup(hass, component=component)
+
hass.config.components.add(component.DOMAIN)
# Cleanup
@@ -206,7 +209,8 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
return platform
try:
- yield from _process_deps_reqs(hass, config, platform_path, platform)
+ yield from async_process_deps_reqs(
+ hass, config, platform_path, platform)
except HomeAssistantError as err:
log_error(str(err))
return None
@@ -215,7 +219,7 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
@asyncio.coroutine
-def _process_deps_reqs(hass, config, name, module):
+def async_process_deps_reqs(hass, config, name, module):
"""Process all dependencies and requirements for a module.
Module is a Python module of either a component or platform.
diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py
index c4fea2846c5..75721a37466 100644
--- a/homeassistant/util/__init__.py
+++ b/homeassistant/util/__init__.py
@@ -61,7 +61,7 @@ def repr_helper(inp: Any) -> str:
def convert(value: T, to_type: Callable[[T], U],
- default: Optional[U]=None) -> Optional[U]:
+ default: Optional[U] = None) -> Optional[U]:
"""Convert value to to_type, returns default if fails."""
try:
return default if value is None else to_type(value)
@@ -164,6 +164,7 @@ class OrderedSet(MutableSet):
"""Check if key is in set."""
return key in self.map
+ # pylint: disable=arguments-differ
def add(self, key):
"""Add an element to the end of the set."""
if key not in self.map:
@@ -180,6 +181,7 @@ class OrderedSet(MutableSet):
curr = begin[1]
curr[2] = begin[1] = self.map[key] = [key, curr, begin]
+ # pylint: disable=arguments-differ
def discard(self, key):
"""Discard an element from the set."""
if key in self.map:
diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py
index 9c7fa0d70e7..089e1e733ed 100644
--- a/homeassistant/util/color.py
+++ b/homeassistant/util/color.py
@@ -392,8 +392,8 @@ def color_temperature_to_rgb(color_temperature_kelvin):
return (red, green, blue)
-def _bound(color_component: float, minimum: float=0,
- maximum: float=255) -> float:
+def _bound(color_component: float, minimum: float = 0,
+ maximum: float = 255) -> float:
"""
Bound the given color component value between the given min and max values.
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index c3400bac9be..7b5b996a3a3 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -51,7 +51,7 @@ def utcnow() -> dt.datetime:
return dt.datetime.now(UTC)
-def now(time_zone: dt.tzinfo=None) -> dt.datetime:
+def now(time_zone: dt.tzinfo = None) -> dt.datetime:
"""Get now in specified time zone."""
return dt.datetime.now(time_zone or DEFAULT_TIME_ZONE)
diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py
index 810463260fd..7a326c34f15 100644
--- a/homeassistant/util/json.py
+++ b/homeassistant/util/json.py
@@ -8,8 +8,11 @@ from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
+_UNDEFINED = object()
-def load_json(filename: str) -> Union[List, Dict]:
+
+def load_json(filename: str, default: Union[List, Dict] = _UNDEFINED) \
+ -> Union[List, Dict]:
"""Load JSON data from a file and return as dict or list.
Defaults to returning empty dict if file is not found.
@@ -26,7 +29,7 @@ def load_json(filename: str) -> Union[List, Dict]:
except OSError as error:
_LOGGER.exception('JSON file reading failed: %s', filename)
raise HomeAssistantError(error)
- return {} # (also evaluates to False)
+ return {} if default is _UNDEFINED else default
def save_json(filename: str, config: Union[List, Dict]):
diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py
index 35b266cb104..0cd0b14d3ab 100644
--- a/homeassistant/util/location.py
+++ b/homeassistant/util/location.py
@@ -84,7 +84,7 @@ def elevation(latitude, longitude):
# License: https://github.com/maurycyp/vincenty/blob/master/LICENSE
# pylint: disable=invalid-name, unused-variable, invalid-sequence-index
def vincenty(point1: Tuple[float, float], point2: Tuple[float, float],
- miles: bool=False) -> Optional[float]:
+ miles: bool = False) -> Optional[float]:
"""
Vincenty formula (inverse method) to calculate the distance.
diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py
index a82a50f4e02..e8149a85262 100644
--- a/homeassistant/util/package.py
+++ b/homeassistant/util/package.py
@@ -17,9 +17,9 @@ _LOGGER = logging.getLogger(__name__)
INSTALL_LOCK = threading.Lock()
-def install_package(package: str, upgrade: bool=True,
- target: Optional[str]=None,
- constraints: Optional[str]=None) -> bool:
+def install_package(package: str, upgrade: bool = True,
+ target: Optional[str] = None,
+ constraints: Optional[str] = None) -> bool:
"""Install a package on PyPi. Accepts pip compatible package strings.
Return boolean if install successful.
diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py
index d0d5199e0f4..8ac8d096b99 100644
--- a/homeassistant/util/yaml.py
+++ b/homeassistant/util/yaml.py
@@ -13,7 +13,7 @@ except ImportError:
keyring = None
try:
- import credstash # pylint: disable=import-error
+ import credstash # pylint: disable=import-error, no-member
except ImportError:
credstash = None
@@ -276,6 +276,7 @@ def _secret_yaml(loader: SafeLineLoader,
global credstash # pylint: disable=invalid-name
if credstash:
+ # pylint: disable=no-member
try:
pwd = credstash.getSecret(node.value, table=_SECRET_NAMESPACE)
if pwd:
diff --git a/pylintrc b/pylintrc
index 1ed8d2af336..85a44782af1 100644
--- a/pylintrc
+++ b/pylintrc
@@ -13,6 +13,7 @@ reports=no
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
+# inconsistent-return-statements - doesn't handle raise
generated-members=botocore.errorfactory
@@ -23,6 +24,7 @@ disable=
cyclic-import,
duplicate-code,
global-statement,
+ inconsistent-return-statements,
locally-disabled,
not-context-manager,
redefined-variable-type,
diff --git a/requirements_all.txt b/requirements_all.txt
index 19e6f5cb6e6..7deac085466 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -4,7 +4,7 @@ pyyaml>=3.11,<4
pytz>=2017.02
pip>=8.0.3
jinja2>=2.10
-voluptuous==0.10.5
+voluptuous==0.11.1
typing>=3,<4
aiohttp==2.3.10
yarl==1.1.0
@@ -23,6 +23,9 @@ attrs==17.4.0
# homeassistant.components.doorbird
DoorBirdPy==0.1.2
+# homeassistant.components.homekit
+HAP-python==1.1.5
+
# homeassistant.components.isy994
PyISY==1.1.0
@@ -36,7 +39,7 @@ PyMVGLive==1.1.4
PyMata==2.14
# homeassistant.components.xiaomi_aqara
-PyXiaomiGateway==0.8.0
+PyXiaomiGateway==0.8.1
# homeassistant.components.rpi_gpio
# RPi.GPIO==0.6.1
@@ -45,7 +48,7 @@ PyXiaomiGateway==0.8.0
RtmAPI==0.7.0
# homeassistant.components.media_player.sonos
-SoCo==0.13
+SoCo==0.14
# homeassistant.components.sensor.travisci
TravisPy==0.3.5
@@ -59,8 +62,11 @@ YesssSMS==0.1.1b3
# homeassistant.components.abode
abodepy==0.12.2
+# homeassistant.components.media_player.frontier_silicon
+afsapi==0.0.3
+
# homeassistant.components.device_tracker.automatic
-aioautomatic==0.6.4
+aioautomatic==0.6.5
# homeassistant.components.sensor.dnsip
aiodns==1.1.1
@@ -85,7 +91,7 @@ aiopvapi==1.5.4
alarmdecoder==1.13.2
# homeassistant.components.sensor.alpha_vantage
-alpha_vantage==1.8.0
+alpha_vantage==1.9.0
# homeassistant.components.amcrest
amcrest==1.2.1
@@ -129,6 +135,9 @@ beautifulsoup4==4.6.0
# homeassistant.components.zha
bellows==0.5.0
+# homeassistant.components.bmw_connected_drive
+bimmer_connected==0.3.0
+
# homeassistant.components.blink
blinkpy==0.6.0
@@ -162,7 +171,7 @@ broadlink==0.5
# homeassistant.components.sensor.buienradar
# homeassistant.components.weather.buienradar
-buienradar==0.9
+buienradar==0.91
# homeassistant.components.calendar.caldav
caldav==0.5.0
@@ -210,7 +219,7 @@ defusedxml==0.5.0
deluge-client==1.0.5
# homeassistant.components.media_player.denonavr
-denonavr==0.5.5
+denonavr==0.6.0
# homeassistant.components.media_player.directv
directpy==0.2
@@ -266,7 +275,7 @@ evohomeclient==0.2.5
fastdotcom==0.0.3
# homeassistant.components.sensor.fedex
-fedexdeliverymanager==1.0.4
+fedexdeliverymanager==1.0.5
# homeassistant.components.feedreader
# homeassistant.components.sensor.geo_rss_events
@@ -292,12 +301,6 @@ freesms==0.1.2
# homeassistant.components.switch.fritzdect
fritzhome==1.0.4
-# homeassistant.components.media_player.frontier_silicon
-fsapi==0.0.7
-
-# homeassistant.components.conversation
-fuzzywuzzy==0.16.0
-
# homeassistant.components.tts.google
gTTS-token==1.1.1
@@ -353,7 +356,7 @@ hipnotify==1.0.8
holidays==0.9.3
# homeassistant.components.frontend
-home-assistant-frontend==20180209.0
+home-assistant-frontend==20180221.1
# homeassistant.components.camera.onvif
http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a
@@ -376,9 +379,6 @@ https://github.com/jabesq/netatmo-api-python/archive/v0.9.2.1.zip#lnetatmo==0.9.
# homeassistant.components.neato
https://github.com/jabesq/pybotvac/archive/v0.0.5.zip#pybotvac==0.0.5
-# homeassistant.components.sensor.sabnzbd
-https://github.com/jamespcole/home-assistant-nzb-clients/archive/616cad59154092599278661af17e2a9f2cf5e2a9.zip#python-sabnzbd==0.1
-
# homeassistant.components.switch.anel_pwrctrl
https://github.com/mweinelt/anel-pwrctrl/archive/ed26e8830e28a2bfa4260a9002db23ce3e7e63d7.zip#anel_pwrctrl==0.0.1
@@ -400,7 +400,7 @@ https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0
# i2csense==0.0.4
# homeassistant.components.light.iglo
-iglo==1.1.3
+iglo==1.2.5
# homeassistant.components.ihc
ihcsdk==2.1.1
@@ -454,7 +454,7 @@ liffylights==0.9.4
lightify==1.0.6.1
# homeassistant.components.light.limitlessled
-limitlessled==1.0.8
+limitlessled==1.1.0
# homeassistant.components.linode
linode-api==4.1.4b2
@@ -507,7 +507,7 @@ mychevy==0.1.1
mycroftapi==2.0
# homeassistant.components.usps
-myusps==1.2.2
+myusps==1.3.2
# homeassistant.components.media_player.nad
# homeassistant.components.media_player.nadtcp
@@ -555,7 +555,7 @@ orvibo==1.1.1
paho-mqtt==1.3.1
# homeassistant.components.media_player.panasonic_viera
-panasonic_viera==0.3
+panasonic_viera==0.3.1
# homeassistant.components.media_player.dunehd
pdunehd==1.3
@@ -618,6 +618,9 @@ pushetta==1.0.15
# homeassistant.components.light.rpi_gpio_pwm
pwmled==1.2.1
+# homeassistant.components.august
+py-august==0.3.0
+
# homeassistant.components.canary
py-canary==0.4.0
@@ -625,7 +628,7 @@ py-canary==0.4.0
py-cpuinfo==3.3.0
# homeassistant.components.melissa
-py-melissa-climate==1.0.1
+py-melissa-climate==1.0.6
# homeassistant.components.camera.synology
py-synology==0.1.5
@@ -675,7 +678,7 @@ pybbox==0.0.5-alpha
# pybluez==0.22
# homeassistant.components.media_player.cast
-pychromecast==1.0.3
+pychromecast==2.0.0
# homeassistant.components.media_player.cmus
pycmus==0.1.0
@@ -694,7 +697,7 @@ pycsspeechtts==1.0.2
pydaikin==0.4
# homeassistant.components.deconz
-pydeconz==27
+pydeconz==30
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -793,6 +796,9 @@ pymailgunner==1.4
# homeassistant.components.media_player.mediaroom
pymediaroom==0.5
+# homeassistant.components.media_player.xiaomi_tv
+pymitv==1.0.0
+
# homeassistant.components.mochad
pymochad==0.2.0
@@ -849,6 +855,9 @@ pyqwikswitch==0.4
# homeassistant.components.rainbird
pyrainbird==0.1.3
+# homeassistant.components.sensor.sabnzbd
+pysabnzbd==0.0.3
+
# homeassistant.components.climate.sensibo
pysensibo==1.0.2
@@ -865,7 +874,7 @@ pysesame==0.1.0
pysher==0.2.0
# homeassistant.components.sensor.sma
-pysma==0.1.3
+pysma==0.2
# homeassistant.components.device_tracker.snmp
# homeassistant.components.sensor.snmp
@@ -892,7 +901,7 @@ python-digitalocean==1.13.2
python-ecobee-api==0.0.15
# homeassistant.components.climate.eq3btsmart
-# python-eq3bt==0.1.8
+# python-eq3bt==0.1.9
# homeassistant.components.sensor.etherscan
python-etherscan-api==0.0.3
@@ -922,7 +931,7 @@ python-juicenet==0.0.5
# homeassistant.components.remote.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
-python-miio==0.3.6
+python-miio==0.3.7
# homeassistant.components.media_player.mpd
python-mpd2==0.5.5
@@ -1001,7 +1010,7 @@ pyunifi==2.13
# pyuserinput==0.1.11
# homeassistant.components.vera
-pyvera==0.2.39
+pyvera==0.2.41
# homeassistant.components.media_player.vizio
pyvizio==0.0.2
@@ -1109,6 +1118,9 @@ sleekxmpp==1.3.2
# homeassistant.components.sleepiq
sleepyq==0.6
+# homeassistant.components.smappee
+smappy==0.2.15
+
# homeassistant.components.raspihats
# homeassistant.components.sensor.bh1750
# homeassistant.components.sensor.bme280
@@ -1124,7 +1136,10 @@ snapcast==2.0.8
somecomfort==0.5.0
# homeassistant.components.sensor.speedtest
-speedtest-cli==1.0.7
+speedtest-cli==2.0.0
+
+# homeassistant.components.sensor.spotcrime
+spotcrime==1.0.2
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
@@ -1141,7 +1156,7 @@ steamodd==4.21
suds-py3==1.3.3.0
# homeassistant.components.tahoma
-tahoma-api==0.0.11
+tahoma-api==0.0.12
# homeassistant.components.sensor.tank_utility
tank_utility==1.4.0
@@ -1201,6 +1216,9 @@ uvcclient==0.10.1
# homeassistant.components.climate.venstar
venstarcolortouch==0.6
+# homeassistant.components.config.config_entries
+voluptuous-serialize==1
+
# homeassistant.components.volvooncall
volvooncall==0.4.0
@@ -1242,9 +1260,10 @@ xbee-helper==0.0.7
xboxapi==0.1.1
# homeassistant.components.knx
-xknx==0.7.18
+xknx==0.8.3
# homeassistant.components.media_player.bluesound
+# homeassistant.components.sensor.startca
# homeassistant.components.sensor.swiss_hydrological_data
# homeassistant.components.sensor.ted5000
# homeassistant.components.sensor.yr
@@ -1264,7 +1283,7 @@ yeelight==0.3.3
yeelightsunflower==0.0.8
# homeassistant.components.media_extractor
-youtube_dl==2018.01.21
+youtube_dl==2018.02.11
# homeassistant.components.light.zengge
zengge==0.2
diff --git a/requirements_docs.txt b/requirements_docs.txt
index c5c48e0bc73..60946fd00a8 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -1,3 +1,3 @@
-Sphinx==1.6.7
-sphinx-autodoc-typehints==1.2.4
+Sphinx==1.7.0
+sphinx-autodoc-typehints==1.2.5
sphinx-autodoc-annotation==1.0.post1
diff --git a/requirements_test.txt b/requirements_test.txt
index cddf11a34b8..d56a7085c74 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -2,7 +2,7 @@
# make new things fail. Manually update these pins when pulling in a
# new version
flake8==3.5
-pylint==1.6.5
+pylint==1.8.2
mypy==0.560
pydocstyle==1.1.1
coveralls==1.2.0
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 1ae1b9f2e14..1e443e3ad00 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -3,7 +3,7 @@
# make new things fail. Manually update these pins when pulling in a
# new version
flake8==3.5
-pylint==1.6.5
+pylint==1.8.2
mypy==0.560
pydocstyle==1.1.1
coveralls==1.2.0
@@ -18,14 +18,17 @@ flake8-docstrings==1.0.3
asynctest>=0.11.1
+# homeassistant.components.homekit
+HAP-python==1.1.5
+
# homeassistant.components.notify.html5
PyJWT==1.5.3
# homeassistant.components.media_player.sonos
-SoCo==0.13
+SoCo==0.14
# homeassistant.components.device_tracker.automatic
-aioautomatic==0.6.4
+aioautomatic==0.6.5
# homeassistant.components.emulated_hue
# homeassistant.components.http
@@ -56,9 +59,6 @@ evohomeclient==0.2.5
# homeassistant.components.sensor.geo_rss_events
feedparser==5.2.1
-# homeassistant.components.conversation
-fuzzywuzzy==0.16.0
-
# homeassistant.components.tts.google
gTTS-token==1.1.1
@@ -75,7 +75,7 @@ hbmqtt==0.9.1
holidays==0.9.3
# homeassistant.components.frontend
-home-assistant-frontend==20180209.0
+home-assistant-frontend==20180221.1
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@@ -178,6 +178,9 @@ statsd==3.2.1
# homeassistant.components.camera.uvc
uvcclient==0.10.1
+# homeassistant.components.config.config_entries
+voluptuous-serialize==1
+
# homeassistant.components.vultr
vultr==0.1.2
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index dcd201667dd..460c998f556 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -46,8 +46,8 @@ TEST_REQUIREMENTS = (
'ephem',
'evohomeclient',
'feedparser',
- 'fuzzywuzzy',
'gTTS-token',
+ 'HAP-python',
'ha-ffmpeg',
'haversine',
'hbmqtt',
@@ -83,6 +83,7 @@ TEST_REQUIREMENTS = (
'sqlalchemy',
'statsd',
'uvcclient',
+ 'voluptuous-serialize',
'warrant',
'yahoo-finance',
'pythonwhois',
@@ -92,6 +93,9 @@ TEST_REQUIREMENTS = (
IGNORE_PACKAGES = (
'homeassistant.components.recorder.models',
+ 'homeassistant.components.homekit.accessories',
+ 'homeassistant.components.homekit.covers',
+ 'homeassistant.components.homekit.sensors'
)
IGNORE_PIN = ('colorlog>2.1,<3', 'keyring>=9.3,<10.0', 'urllib3')
@@ -107,7 +111,7 @@ URL_PIN = ('https://home-assistant.io/developers/code_review_platform/'
CONSTRAINT_PATH = os.path.join(os.path.dirname(__file__),
'../homeassistant/package_constraints.txt')
CONSTRAINT_BASE = """
-# Breaks Python 3.6 and is not needed for our supported Pythons
+# Breaks Python 3.6 and is not needed for our supported Python versions
enum34==1000000000.0.0
"""
diff --git a/script/lint b/script/lint
index ab7561b9a5b..b16b92a45b4 100755
--- a/script/lint
+++ b/script/lint
@@ -4,11 +4,15 @@
cd "$(dirname "$0")/.."
if [ "$1" = "--changed" ]; then
- export files="`git diff upstream/dev --name-only | grep -e '\.py$'`"
+ export files="`git diff upstream/dev... --name-only | grep -e '\.py$'`"
echo "================================================="
- echo "FILES CHANGED (git diff upstream/dev --name-only)"
+ echo "FILES CHANGED (git diff upstream/dev... --name-only)"
echo "================================================="
- echo $files
+ if $files >/dev/null; then
+ echo "No python file changed"
+ exit
+ fi
+ printf "%s\n" $files
echo "================"
echo "LINT with flake8"
echo "================"
diff --git a/setup.py b/setup.py
index 0a454f9eb4d..bca49d33647 100755
--- a/setup.py
+++ b/setup.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""Home Assistant setup script."""
-import os
-from setuptools import setup, find_packages
import sys
+from setuptools import setup, find_packages
+
import homeassistant.const as hass_const
@@ -41,8 +41,6 @@ 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, hass_const.__version__)
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
@@ -53,7 +51,7 @@ REQUIRES = [
'pytz>=2017.02',
'pip>=8.0.3',
'jinja2>=2.10',
- 'voluptuous==0.10.5',
+ 'voluptuous==0.11.1',
'typing>=3,<4',
'aiohttp==2.3.10', # If updated, check if yarl also needs an update!
'yarl==1.1.0',
diff --git a/tests/common.py b/tests/common.py
index 511d59dbdfe..6fee7b1bec0 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -9,13 +9,11 @@ import logging
import threading
from contextlib import contextmanager
-from aiohttp import web
-
-from homeassistant import core as ha, loader
+from homeassistant import core as ha, loader, config_entries
from homeassistant.setup import setup_component, async_setup_component
from homeassistant.config import async_process_component_config
from homeassistant.helpers import (
- intent, dispatcher, entity, restore_state, entity_registry,
+ intent, entity, restore_state, entity_registry,
entity_platform)
from homeassistant.util.unit_system import METRIC_SYSTEM
import homeassistant.util.dt as date_util
@@ -25,9 +23,6 @@ from homeassistant.const import (
EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE,
ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE)
from homeassistant.components import mqtt, recorder
-from homeassistant.components.http.auth import auth_middleware
-from homeassistant.components.http.const import (
- KEY_USE_X_FORWARDED_FOR, KEY_BANS_ENABLED, KEY_TRUSTED_NETWORKS)
from homeassistant.util.async import (
run_callback_threadsafe, run_coroutine_threadsafe)
@@ -114,6 +109,9 @@ def get_test_home_assistant():
def async_test_home_assistant(loop):
"""Return a Home Assistant object pointing at test config dir."""
hass = ha.HomeAssistant(loop)
+ hass.config_entries = config_entries.ConfigEntries(hass, {})
+ hass.config_entries._entries = []
+ hass.config.async_load = Mock()
INSTANCES.append(hass)
orig_async_add_job = hass.async_add_job
@@ -214,13 +212,12 @@ def async_mock_intent(hass, intent_typ):
@ha.callback
-def async_fire_mqtt_message(hass, topic, payload, qos=0):
+def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False):
"""Fire the MQTT message."""
if isinstance(payload, str):
payload = payload.encode('utf-8')
- dispatcher.async_dispatcher_send(
- hass, mqtt.SIGNAL_MQTT_MESSAGE_RECEIVED, topic,
- payload, qos)
+ msg = mqtt.Message(topic, payload, qos, retain)
+ hass.async_run_job(hass.data['mqtt']._mqtt_on_message, None, None, msg)
fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message)
@@ -246,7 +243,7 @@ def fire_service_discovered(hass, service, info):
def load_fixture(filename):
"""Load a fixture."""
path = os.path.join(os.path.dirname(__file__), 'fixtures', filename)
- with open(path) as fptr:
+ with open(path, encoding='utf-8') as fptr:
return fptr.read()
@@ -263,46 +260,26 @@ def mock_state_change_event(hass, new_state, old_state=None):
hass.bus.fire(EVENT_STATE_CHANGED, event_data)
-def mock_http_component(hass, api_password=None):
- """Mock the HTTP component."""
- hass.http = MagicMock(api_password=api_password)
- mock_component(hass, 'http')
- hass.http.views = {}
-
- def mock_register_view(view):
- """Store registered view."""
- if isinstance(view, type):
- # Instantiate the view, if needed
- view = view()
-
- hass.http.views[view.name] = view
-
- hass.http.register_view = mock_register_view
-
-
-def mock_http_component_app(hass, api_password=None):
- """Create an aiohttp.web.Application instance for testing."""
- if 'http' not in hass.config.components:
- mock_http_component(hass, api_password)
- app = web.Application(middlewares=[auth_middleware])
- app['hass'] = hass
- app[KEY_USE_X_FORWARDED_FOR] = False
- app[KEY_BANS_ENABLED] = False
- app[KEY_TRUSTED_NETWORKS] = []
- return app
-
-
@asyncio.coroutine
-def async_mock_mqtt_component(hass):
+def async_mock_mqtt_component(hass, config=None):
"""Mock the MQTT component."""
- with patch('homeassistant.components.mqtt.MQTT') as mock_mqtt:
- mock_mqtt().async_connect.return_value = mock_coro(True)
- yield from async_setup_component(hass, mqtt.DOMAIN, {
- mqtt.DOMAIN: {
- mqtt.CONF_BROKER: 'mock-broker',
- }
+ if config is None:
+ config = {mqtt.CONF_BROKER: 'mock-broker'}
+
+ with patch('paho.mqtt.client.Client') as mock_client:
+ mock_client().connect.return_value = 0
+ mock_client().subscribe.return_value = (0, 0)
+ mock_client().publish.return_value = (0, 0)
+
+ result = yield from async_setup_component(hass, mqtt.DOMAIN, {
+ mqtt.DOMAIN: config
})
- return mock_mqtt
+ assert result
+
+ hass.data['mqtt'] = MagicMock(spec_set=hass.data['mqtt'],
+ wraps=hass.data['mqtt'])
+
+ return hass.data['mqtt']
mock_mqtt_component = threadsafe_coroutine_factory(async_mock_mqtt_component)
@@ -331,12 +308,12 @@ class MockModule(object):
# pylint: disable=invalid-name
def __init__(self, domain=None, dependencies=None, setup=None,
requirements=None, config_schema=None, platform_schema=None,
- async_setup=None):
+ async_setup=None, async_setup_entry=None,
+ async_unload_entry=None):
"""Initialize the mock module."""
self.DOMAIN = domain
self.DEPENDENCIES = dependencies or []
self.REQUIREMENTS = requirements or []
- self._setup = setup
if config_schema is not None:
self.CONFIG_SCHEMA = config_schema
@@ -344,18 +321,21 @@ class MockModule(object):
if platform_schema is not None:
self.PLATFORM_SCHEMA = platform_schema
+ if setup is not None:
+ # We run this in executor, wrap it in function
+ self.setup = lambda *args: setup(*args)
+
if async_setup is not None:
self.async_setup = async_setup
- def setup(self, hass, config):
- """Set up the component.
+ if setup is None and async_setup is None:
+ self.async_setup = mock_coro_func(True)
- We always define this mock because MagicMock setups will be seen by the
- executor as a coroutine, raising an exception.
- """
- if self._setup is not None:
- return self._setup(hass, config)
- return True
+ if async_setup_entry is not None:
+ self.async_setup_entry = async_setup_entry
+
+ if async_unload_entry is not None:
+ self.async_unload_entry = async_unload_entry
class MockPlatform(object):
@@ -366,18 +346,19 @@ class MockPlatform(object):
platform_schema=None, async_setup_platform=None):
"""Initialize the platform."""
self.DEPENDENCIES = dependencies or []
- self._setup_platform = setup_platform
if platform_schema is not None:
self.PLATFORM_SCHEMA = platform_schema
+ if setup_platform is not None:
+ # We run this in executor, wrap it in function
+ self.setup_platform = lambda *args: setup_platform(*args)
+
if async_setup_platform is not None:
self.async_setup_platform = async_setup_platform
- def setup_platform(self, hass, config, add_devices, discovery_info=None):
- """Set up the platform."""
- if self._setup_platform is not None:
- self._setup_platform(hass, config, add_devices, discovery_info)
+ if setup_platform is None and async_setup_platform is None:
+ self.async_setup_platform = mock_coro_func()
class MockToggleDevice(entity.ToggleEntity):
@@ -431,6 +412,35 @@ class MockToggleDevice(entity.ToggleEntity):
return None
+class MockConfigEntry(config_entries.ConfigEntry):
+ """Helper for creating config entries that adds some defaults."""
+
+ def __init__(self, *, domain='test', data=None, version=0, entry_id=None,
+ source=config_entries.SOURCE_USER, title='Mock Title',
+ state=None):
+ """Initialize a mock config entry."""
+ kwargs = {
+ 'entry_id': entry_id or 'mock-id',
+ 'domain': domain,
+ 'data': data or {},
+ 'version': version,
+ 'title': title
+ }
+ if source is not None:
+ kwargs['source'] = source
+ if state is not None:
+ kwargs['state'] = state
+ super().__init__(**kwargs)
+
+ def add_to_hass(self, hass):
+ """Test helper to add entry to hass."""
+ hass.config_entries._entries.append(self)
+
+ def add_to_manager(self, manager):
+ """Test helper to add entry to entry manager."""
+ manager._entries.append(self)
+
+
def patch_yaml_files(files_dict, endswith=True):
"""Patch load_yaml with a dictionary of yaml files."""
# match using endswith, start search with longest string
diff --git a/tests/components/alarm_control_panel/test_manual_mqtt.py b/tests/components/alarm_control_panel/test_manual_mqtt.py
index 83254d9104f..719352c5419 100644
--- a/tests/components/alarm_control_panel/test_manual_mqtt.py
+++ b/tests/components/alarm_control_panel/test_manual_mqtt.py
@@ -1395,53 +1395,60 @@ class TestAlarmControlPanelManualMqtt(unittest.TestCase):
# Component should send disarmed alarm state on startup
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_DISARMED, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_DISARMED, 0, True)
+ self.mock_publish.async_publish.reset_mock()
# Arm in home mode
alarm_control_panel.alarm_arm_home(self.hass)
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_PENDING, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_PENDING, 0, True)
+ self.mock_publish.async_publish.reset_mock()
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_ARMED_HOME, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_ARMED_HOME, 0, True)
+ self.mock_publish.async_publish.reset_mock()
# Arm in away mode
alarm_control_panel.alarm_arm_away(self.hass)
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_PENDING, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_PENDING, 0, True)
+ self.mock_publish.async_publish.reset_mock()
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_ARMED_AWAY, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_ARMED_AWAY, 0, True)
+ self.mock_publish.async_publish.reset_mock()
# Arm in night mode
alarm_control_panel.alarm_arm_night(self.hass)
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_PENDING, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_PENDING, 0, True)
+ self.mock_publish.async_publish.reset_mock()
# Fast-forward a little bit
future = dt_util.utcnow() + timedelta(seconds=1)
with patch(('homeassistant.components.alarm_control_panel.manual_mqtt.'
'dt_util.utcnow'), return_value=future):
fire_time_changed(self.hass, future)
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_ARMED_NIGHT, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_ARMED_NIGHT, 0, True)
+ self.mock_publish.async_publish.reset_mock()
# Disarm
alarm_control_panel.alarm_disarm(self.hass)
self.hass.block_till_done()
- self.assertEqual(('alarm/state', STATE_ALARM_DISARMED, 0, True),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/state', STATE_ALARM_DISARMED, 0, True)
diff --git a/tests/components/alarm_control_panel/test_mqtt.py b/tests/components/alarm_control_panel/test_mqtt.py
index 5a93a55254d..dee9b3959ca 100644
--- a/tests/components/alarm_control_panel/test_mqtt.py
+++ b/tests/components/alarm_control_panel/test_mqtt.py
@@ -106,8 +106,8 @@ class TestAlarmControlPanelMQTT(unittest.TestCase):
alarm_control_panel.alarm_arm_home(self.hass)
self.hass.block_till_done()
- self.assertEqual(('alarm/command', 'ARM_HOME', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/command', 'ARM_HOME', 0, False)
def test_arm_home_not_publishes_mqtt_with_invalid_code(self):
"""Test not publishing of MQTT messages with invalid code."""
@@ -139,8 +139,8 @@ class TestAlarmControlPanelMQTT(unittest.TestCase):
alarm_control_panel.alarm_arm_away(self.hass)
self.hass.block_till_done()
- self.assertEqual(('alarm/command', 'ARM_AWAY', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/command', 'ARM_AWAY', 0, False)
def test_arm_away_not_publishes_mqtt_with_invalid_code(self):
"""Test not publishing of MQTT messages with invalid code."""
@@ -172,8 +172,8 @@ class TestAlarmControlPanelMQTT(unittest.TestCase):
alarm_control_panel.alarm_disarm(self.hass)
self.hass.block_till_done()
- self.assertEqual(('alarm/command', 'DISARM', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'alarm/command', 'DISARM', 0, False)
def test_disarm_not_publishes_mqtt_with_invalid_code(self):
"""Test not publishing of MQTT messages with invalid code."""
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 9654c667c5f..8de4d0d9aff 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -515,17 +515,15 @@ def test_media_player(hass):
call, _ = yield from assert_request_calls_service(
'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test',
- 'media_player.volume_set',
+ 'media_player.volume_up',
hass,
payload={'volumeSteps': 20})
- assert call.data['volume_level'] == 0.95
call, _ = yield from assert_request_calls_service(
'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test',
- 'media_player.volume_set',
+ 'media_player.volume_down',
hass,
payload={'volumeSteps': -20})
- assert call.data['volume_level'] == 0.55
@asyncio.coroutine
@@ -571,15 +569,11 @@ def test_group(hass):
appliance = yield from discovery_test(device, hass)
assert appliance['endpointId'] == 'group#test'
- assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
+ assert appliance['displayCategories'][0] == "OTHER"
assert appliance['friendlyName'] == "Test group"
+ assert_endpoint_capabilities(appliance, 'Alexa.PowerController')
- (capability,) = assert_endpoint_capabilities(
- appliance,
- 'Alexa.SceneController')
- assert capability['supportsDeactivation']
-
- yield from assert_scene_controller_works(
+ yield from assert_power_controller_works(
'group#test',
'homeassistant.turn_on',
'homeassistant.turn_off',
diff --git a/tests/components/binary_sensor/test_command_line.py b/tests/components/binary_sensor/test_command_line.py
index f35e6f08452..d01b62e4c12 100644
--- a/tests/components/binary_sensor/test_command_line.py
+++ b/tests/components/binary_sensor/test_command_line.py
@@ -3,7 +3,6 @@ import unittest
from homeassistant.const import (STATE_ON, STATE_OFF)
from homeassistant.components.binary_sensor import command_line
-from homeassistant import setup
from homeassistant.helpers import template
from tests.common import get_test_home_assistant
@@ -42,16 +41,6 @@ class TestCommandSensorBinarySensor(unittest.TestCase):
self.assertEqual('Test', entity.name)
self.assertEqual(STATE_ON, entity.state)
- def test_setup_bad_config(self):
- """Test the setup with a bad configuration."""
- config = {'name': 'test',
- 'platform': 'not_command_line',
- }
-
- self.assertFalse(setup.setup_component(self.hass, 'test', {
- 'command_line': config,
- }))
-
def test_template(self):
"""Test setting the state with a template."""
data = command_line.CommandSensorData(self.hass, 'echo 10')
diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py
index 7234d40c410..e44e5cfc1f0 100644
--- a/tests/components/calendar/test_caldav.py
+++ b/tests/components/calendar/test_caldav.py
@@ -64,7 +64,49 @@ LOCATION:Hamburg
DESCRIPTION:What a beautiful day
END:VEVENT
END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:4
+DTSTAMP:20171125T000000Z
+DTSTART:20171127
+SUMMARY:This is an event without dtend or duration
+LOCATION:Hamburg
+DESCRIPTION:What an endless day
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:5
+DTSTAMP:20171125T000000Z
+DTSTART:20171127
+DURATION:PT1H
+SUMMARY:This is an event with duration
+LOCATION:Hamburg
+DESCRIPTION:What a day
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:6
+DTSTAMP:20171125T000000Z
+DTSTART:20171127T100000Z
+DURATION:PT1H
+SUMMARY:This is an event with duration
+LOCATION:Hamburg
+DESCRIPTION:What a day
+END:VEVENT
+END:VCALENDAR
"""
+
]
diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py
index ad7ee5f5bcb..40b4fb2d8e2 100644
--- a/tests/components/camera/test_uvc.py
+++ b/tests/components/camera/test_uvc.py
@@ -9,7 +9,7 @@ from uvcclient import nvr
from homeassistant.setup import setup_component
from homeassistant.components.camera import uvc
-from tests.common import get_test_home_assistant, mock_http_component
+from tests.common import get_test_home_assistant
class TestUVCSetup(unittest.TestCase):
@@ -18,7 +18,6 @@ class TestUVCSetup(unittest.TestCase):
def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
- mock_http_component(self.hass)
def tearDown(self):
"""Stop everything that was started."""
diff --git a/tests/components/climate/test_melissa.py b/tests/components/climate/test_melissa.py
index f8a044c2f4b..5022c556b7d 100644
--- a/tests/components/climate/test_melissa.py
+++ b/tests/components/climate/test_melissa.py
@@ -122,7 +122,7 @@ class TestMelissa(unittest.TestCase):
def test_operation_list(self):
"""Test the operation list."""
self.assertEqual(
- [STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT],
+ [STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT],
self.thermostat.operation_list
)
@@ -202,7 +202,7 @@ class TestMelissa(unittest.TestCase):
self.thermostat._cur_settings = None
self.assertFalse(self.thermostat.send({
'fan': self.api.FAN_LOW}))
- self.assertNotEquals(SPEED_LOW, self.thermostat.current_fan_mode)
+ self.assertNotEqual(SPEED_LOW, self.thermostat.current_fan_mode)
self.assertIsNone(self.thermostat._cur_settings)
@mock.patch('homeassistant.components.climate.melissa._LOGGER.warning')
@@ -226,7 +226,6 @@ class TestMelissa(unittest.TestCase):
def test_melissa_op_to_hass(self):
"""Test for translate melissa operations to hass."""
- self.assertEqual(STATE_AUTO, self.thermostat.melissa_op_to_hass(0))
self.assertEqual(STATE_FAN_ONLY, self.thermostat.melissa_op_to_hass(1))
self.assertEqual(STATE_HEAT, self.thermostat.melissa_op_to_hass(2))
self.assertEqual(STATE_COOL, self.thermostat.melissa_op_to_hass(3))
@@ -245,7 +244,6 @@ class TestMelissa(unittest.TestCase):
@mock.patch('homeassistant.components.climate.melissa._LOGGER.warning')
def test_hass_mode_to_melissa(self, mocked_warning):
"""Test for hass operations to melssa."""
- self.assertEqual(0, self.thermostat.hass_mode_to_melissa(STATE_AUTO))
self.assertEqual(
1, self.thermostat.hass_mode_to_melissa(STATE_FAN_ONLY))
self.assertEqual(2, self.thermostat.hass_mode_to_melissa(STATE_HEAT))
diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py
index c4fa2b304df..663393503ac 100644
--- a/tests/components/climate/test_mqtt.py
+++ b/tests/components/climate/test_mqtt.py
@@ -104,8 +104,8 @@ class TestMQTTClimate(unittest.TestCase):
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("cool", state.attributes.get('operation_mode'))
self.assertEqual("cool", state.state)
- self.assertEqual(('mode-topic', 'cool', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'mode-topic', 'cool', 0, False)
def test_set_operation_pessimistic(self):
"""Test setting operation mode in pessimistic mode."""
@@ -178,8 +178,8 @@ class TestMQTTClimate(unittest.TestCase):
self.assertEqual("low", state.attributes.get('fan_mode'))
climate.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('fan-mode-topic', 'high', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'fan-mode-topic', 'high', 0, False)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('high', state.attributes.get('fan_mode'))
@@ -226,8 +226,8 @@ class TestMQTTClimate(unittest.TestCase):
self.assertEqual("off", state.attributes.get('swing_mode'))
climate.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('swing-mode-topic', 'on', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'swing-mode-topic', 'on', 0, False)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual("on", state.attributes.get('swing_mode'))
@@ -239,15 +239,16 @@ class TestMQTTClimate(unittest.TestCase):
self.assertEqual(21, state.attributes.get('temperature'))
climate.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('mode-topic', 'heat', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'mode-topic', 'heat', 0, False)
+ self.mock_publish.async_publish.reset_mock()
climate.set_temperature(self.hass, temperature=47,
entity_id=ENTITY_CLIMATE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual(47, state.attributes.get('temperature'))
- self.assertEqual(('temperature-topic', 47, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'temperature-topic', 47, 0, False)
def test_set_target_temperature_pessimistic(self):
"""Test setting the target temperature."""
@@ -328,15 +329,16 @@ class TestMQTTClimate(unittest.TestCase):
self.assertEqual('off', state.attributes.get('away_mode'))
climate.set_away_mode(self.hass, True, ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('away-mode-topic', 'AN', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'away-mode-topic', 'AN', 0, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('away_mode'))
climate.set_away_mode(self.hass, False, ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('away-mode-topic', 'AUS', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'away-mode-topic', 'AUS', 0, False)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('off', state.attributes.get('away_mode'))
@@ -372,15 +374,16 @@ class TestMQTTClimate(unittest.TestCase):
self.assertEqual(None, state.attributes.get('hold_mode'))
climate.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('hold-topic', 'on', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'hold-topic', 'on', 0, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('hold_mode'))
climate.set_hold_mode(self.hass, 'off', ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('hold-topic', 'off', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'hold-topic', 'off', 0, False)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('off', state.attributes.get('hold_mode'))
@@ -421,15 +424,16 @@ class TestMQTTClimate(unittest.TestCase):
self.assertEqual('off', state.attributes.get('aux_heat'))
climate.set_aux_heat(self.hass, True, ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('aux-topic', 'ON', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'aux-topic', 'ON', 0, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('on', state.attributes.get('aux_heat'))
climate.set_aux_heat(self.hass, False, ENTITY_CLIMATE)
self.hass.block_till_done()
- self.assertEqual(('aux-topic', 'OFF', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'aux-topic', 'OFF', 0, False)
state = self.hass.states.get(ENTITY_CLIMATE)
self.assertEqual('off', state.attributes.get('aux_heat'))
diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py
index 53340ecede1..3eec350b2cb 100644
--- a/tests/components/cloud/test_iot.py
+++ b/tests/components/cloud/test_iot.py
@@ -17,7 +17,8 @@ def mock_client():
client = MagicMock()
type(client).closed = PropertyMock(side_effect=[False, True])
- with patch('asyncio.sleep'), \
+ # Trigger cancelled error to avoid reconnect.
+ with patch('asyncio.sleep', side_effect=asyncio.CancelledError), \
patch('homeassistant.components.cloud.iot'
'.async_get_clientsession') as session:
session().ws_connect.return_value = mock_coro(client)
@@ -160,10 +161,10 @@ def test_cloud_getting_disconnected_by_server(mock_client, caplog, mock_cloud):
type=WSMsgType.CLOSING,
))
- yield from conn.connect()
+ with patch('asyncio.sleep', side_effect=[None, asyncio.CancelledError]):
+ yield from conn.connect()
- assert 'Connection closed: Connection cancelled.' in caplog.text
- assert 'connect' in str(mock_cloud.hass.async_add_job.mock_calls[-1][1][0])
+ assert 'Connection closed' in caplog.text
@asyncio.coroutine
@@ -177,7 +178,6 @@ def test_cloud_receiving_bytes(mock_client, caplog, mock_cloud):
yield from conn.connect()
assert 'Connection closed: Received non-Text message' in caplog.text
- assert 'connect' in str(mock_cloud.hass.async_add_job.mock_calls[-1][1][0])
@asyncio.coroutine
@@ -192,19 +192,17 @@ def test_cloud_sending_invalid_json(mock_client, caplog, mock_cloud):
yield from conn.connect()
assert 'Connection closed: Received invalid JSON.' in caplog.text
- 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, mock_cloud):
- """Test cloud sending invalid JSON."""
+ """Test cloud unable to check token."""
conn = iot.CloudIoT(mock_cloud)
- mock_client.receive.side_effect = auth_api.CloudError
+ mock_cloud.hass.async_add_job.side_effect = auth_api.CloudError("BLA")
yield from conn.connect()
- assert 'Unable to connect: Unable to refresh token.' in caplog.text
- assert 'connect' in str(mock_cloud.hass.async_add_job.mock_calls[-1][1][0])
+ assert 'Unable to connect: BLA' in caplog.text
@asyncio.coroutine
diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py
new file mode 100644
index 00000000000..1551ba74319
--- /dev/null
+++ b/tests/components/config/test_config_entries.py
@@ -0,0 +1,317 @@
+"""Test config entries API."""
+
+import asyncio
+from collections import OrderedDict
+from unittest.mock import patch
+
+import pytest
+import voluptuous as vol
+
+from homeassistant import config_entries as core_ce
+from homeassistant.config_entries import ConfigFlowHandler, HANDLERS
+from homeassistant.setup import async_setup_component
+from homeassistant.components.config import config_entries
+from homeassistant.loader import set_component
+
+from tests.common import MockConfigEntry, MockModule, mock_coro_func
+
+
+@pytest.fixture
+def client(hass, test_client):
+ """Fixture that can interact with the config manager API."""
+ hass.loop.run_until_complete(async_setup_component(hass, 'http', {}))
+ hass.loop.run_until_complete(config_entries.async_setup(hass))
+ yield hass.loop.run_until_complete(test_client(hass.http.app))
+
+
+@asyncio.coroutine
+def test_get_entries(hass, client):
+ """Test get entries."""
+ MockConfigEntry(
+ domain='comp',
+ title='Test 1',
+ source='bla'
+ ).add_to_hass(hass)
+ MockConfigEntry(
+ domain='comp2',
+ title='Test 2',
+ source='bla2',
+ state=core_ce.ENTRY_STATE_LOADED,
+ ).add_to_hass(hass)
+ resp = yield from client.get('/api/config/config_entries/entry')
+ assert resp.status == 200
+ data = yield from resp.json()
+ for entry in data:
+ entry.pop('entry_id')
+ assert data == [
+ {
+ 'domain': 'comp',
+ 'title': 'Test 1',
+ 'source': 'bla',
+ 'state': 'not_loaded'
+ },
+ {
+ 'domain': 'comp2',
+ 'title': 'Test 2',
+ 'source': 'bla2',
+ 'state': 'loaded',
+ },
+ ]
+
+
+@asyncio.coroutine
+def test_remove_entry(hass, client):
+ """Test removing an entry via the API."""
+ entry = MockConfigEntry(domain='demo')
+ entry.add_to_hass(hass)
+ resp = yield from client.delete(
+ '/api/config/config_entries/entry/{}'.format(entry.entry_id))
+ assert resp.status == 200
+ data = yield from resp.json()
+ assert data == {
+ 'require_restart': True
+ }
+ assert len(hass.config_entries.async_entries()) == 0
+
+
+@asyncio.coroutine
+def test_available_flows(hass, client):
+ """Test querying the available flows."""
+ with patch.object(core_ce, 'FLOWS', ['hello', 'world']):
+ resp = yield from client.get(
+ '/api/config/config_entries/flow_handlers')
+ assert resp.status == 200
+ data = yield from resp.json()
+ assert data == ['hello', 'world']
+
+
+############################
+# FLOW MANAGER API TESTS #
+############################
+
+
+@asyncio.coroutine
+def test_initialize_flow(hass, client):
+ """Test we can initialize a flow."""
+ class TestFlow(ConfigFlowHandler):
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ schema = OrderedDict()
+ schema[vol.Required('username')] = str
+ schema[vol.Required('password')] = str
+
+ return self.async_show_form(
+ title='test-title',
+ step_id='init',
+ description='test-description',
+ data_schema=schema,
+ errors={
+ 'username': 'Should be unique.'
+ }
+ )
+
+ with patch.dict(HANDLERS, {'test': TestFlow}):
+ resp = yield from client.post('/api/config/config_entries/flow',
+ json={'domain': 'test'})
+
+ assert resp.status == 200
+ data = yield from resp.json()
+
+ data.pop('flow_id')
+
+ assert data == {
+ 'type': 'form',
+ 'title': 'test-title',
+ 'description': 'test-description',
+ 'data_schema': [
+ {
+ 'name': 'username',
+ 'required': True,
+ 'type': 'string'
+ },
+ {
+ 'name': 'password',
+ 'required': True,
+ 'type': 'string'
+ }
+ ],
+ 'errors': {
+ 'username': 'Should be unique.'
+ }
+ }
+
+
+@asyncio.coroutine
+def test_abort(hass, client):
+ """Test a flow that aborts."""
+ class TestFlow(ConfigFlowHandler):
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_abort(reason='bla')
+
+ with patch.dict(HANDLERS, {'test': TestFlow}):
+ resp = yield from client.post('/api/config/config_entries/flow',
+ json={'domain': 'test'})
+
+ assert resp.status == 200
+ data = yield from resp.json()
+ data.pop('flow_id')
+ assert data == {
+ 'reason': 'bla',
+ 'type': 'abort'
+ }
+
+
+@asyncio.coroutine
+def test_create_account(hass, client):
+ """Test a flow that creates an account."""
+ set_component(
+ 'test', MockModule('test', async_setup_entry=mock_coro_func(True)))
+
+ class TestFlow(ConfigFlowHandler):
+ VERSION = 1
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_create_entry(
+ title='Test Entry',
+ data={'secret': 'account_token'}
+ )
+
+ with patch.dict(HANDLERS, {'test': TestFlow}):
+ resp = yield from client.post('/api/config/config_entries/flow',
+ json={'domain': 'test'})
+
+ assert resp.status == 200
+ data = yield from resp.json()
+ data.pop('flow_id')
+ assert data == {
+ 'title': 'Test Entry',
+ 'type': 'create_entry'
+ }
+
+
+@asyncio.coroutine
+def test_two_step_flow(hass, client):
+ """Test we can finish a two step flow."""
+ set_component(
+ 'test', MockModule('test', async_setup_entry=mock_coro_func(True)))
+
+ class TestFlow(ConfigFlowHandler):
+ VERSION = 1
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_show_form(
+ title='test-title',
+ step_id='account',
+ data_schema=vol.Schema({
+ 'user_title': str
+ }))
+
+ @asyncio.coroutine
+ def async_step_account(self, user_input=None):
+ return self.async_create_entry(
+ title=user_input['user_title'],
+ data={'secret': 'account_token'}
+ )
+
+ with patch.dict(HANDLERS, {'test': TestFlow}):
+ resp = yield from client.post('/api/config/config_entries/flow',
+ json={'domain': 'test'})
+ assert resp.status == 200
+ data = yield from resp.json()
+ flow_id = data.pop('flow_id')
+ assert data == {
+ 'type': 'form',
+ 'title': 'test-title',
+ 'description': None,
+ 'data_schema': [
+ {
+ 'name': 'user_title',
+ 'type': 'string'
+ }
+ ],
+ 'errors': None
+ }
+
+ with patch.dict(HANDLERS, {'test': TestFlow}):
+ resp = yield from client.post(
+ '/api/config/config_entries/flow/{}'.format(flow_id),
+ json={'user_title': 'user-title'})
+ assert resp.status == 200
+ data = yield from resp.json()
+ data.pop('flow_id')
+ assert data == {
+ 'type': 'create_entry',
+ 'title': 'user-title',
+ }
+
+
+@asyncio.coroutine
+def test_get_progress_index(hass, client):
+ """Test querying for the flows that are in progress."""
+ class TestFlow(ConfigFlowHandler):
+ VERSION = 5
+
+ @asyncio.coroutine
+ def async_step_hassio(self, info):
+ return (yield from self.async_step_account())
+
+ @asyncio.coroutine
+ def async_step_account(self, user_input=None):
+ return self.async_show_form(
+ step_id='account',
+ title='Finish setup'
+ )
+
+ with patch.dict(HANDLERS, {'test': TestFlow}):
+ form = yield from hass.config_entries.flow.async_init(
+ 'test', source='hassio')
+
+ resp = yield from client.get('/api/config/config_entries/flow')
+ assert resp.status == 200
+ data = yield from resp.json()
+ assert data == [
+ {
+ 'flow_id': form['flow_id'],
+ 'domain': 'test',
+ 'source': 'hassio'
+ }
+ ]
+
+
+@asyncio.coroutine
+def test_get_progress_flow(hass, client):
+ """Test we can query the API for same result as we get from init a flow."""
+ class TestFlow(ConfigFlowHandler):
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ schema = OrderedDict()
+ schema[vol.Required('username')] = str
+ schema[vol.Required('password')] = str
+
+ return self.async_show_form(
+ title='test-title',
+ step_id='init',
+ description='test-description',
+ data_schema=schema,
+ errors={
+ 'username': 'Should be unique.'
+ }
+ )
+
+ with patch.dict(HANDLERS, {'test': TestFlow}):
+ resp = yield from client.post('/api/config/config_entries/flow',
+ json={'domain': 'test'})
+
+ assert resp.status == 200
+ data = yield from resp.json()
+
+ resp2 = yield from client.get(
+ '/api/config/config_entries/flow/{}'.format(data['flow_id']))
+
+ assert resp2.status == 200
+ data2 = yield from resp2.json()
+
+ assert data == data2
diff --git a/tests/components/config/test_hassbian.py b/tests/components/config/test_hassbian.py
index 659e5ad2448..9038ccc6aa4 100644
--- a/tests/components/config/test_hassbian.py
+++ b/tests/components/config/test_hassbian.py
@@ -14,7 +14,7 @@ def test_setup_check_env_prevents_load(hass, loop):
with patch.dict(os.environ, clear=True), \
patch.object(config, 'SECTIONS', ['hassbian']), \
patch('homeassistant.components.http.'
- 'HomeAssistantWSGI.register_view') as reg_view:
+ 'HomeAssistantHTTP.register_view') as reg_view:
loop.run_until_complete(async_setup_component(hass, 'config', {}))
assert 'config' in hass.config.components
assert reg_view.called is False
@@ -25,7 +25,7 @@ def test_setup_check_env_works(hass, loop):
with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}), \
patch.object(config, 'SECTIONS', ['hassbian']), \
patch('homeassistant.components.http.'
- 'HomeAssistantWSGI.register_view') as reg_view:
+ 'HomeAssistantHTTP.register_view') as reg_view:
loop.run_until_complete(async_setup_component(hass, 'config', {}))
assert 'config' in hass.config.components
assert len(reg_view.mock_calls) == 2
diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py
index 6f69f886419..2d5d814ac8a 100644
--- a/tests/components/config/test_init.py
+++ b/tests/components/config/test_init.py
@@ -2,19 +2,11 @@
import asyncio
from unittest.mock import patch
-import pytest
-
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.setup import async_setup_component, ATTR_COMPONENT
from homeassistant.components import config
-from tests.common import mock_http_component, mock_coro, mock_component
-
-
-@pytest.fixture(autouse=True)
-def stub_http(hass):
- """Stub the HTTP component."""
- mock_http_component(hass)
+from tests.common import mock_coro, mock_component
@asyncio.coroutine
diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py
index 81800d709e3..c98385a3c32 100644
--- a/tests/components/config/test_zwave.py
+++ b/tests/components/config/test_zwave.py
@@ -3,28 +3,30 @@ import asyncio
import json
from unittest.mock import MagicMock, patch
+import pytest
+
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
from homeassistant.components.zwave import DATA_NETWORK, const
-from homeassistant.components.config.zwave import (
- ZWaveNodeValueView, ZWaveNodeGroupView, ZWaveNodeConfigView,
- ZWaveUserCodeView, ZWaveConfigWriteView)
-from tests.common import mock_http_component_app
from tests.mock.zwave import MockNode, MockValue, MockEntityValues
VIEW_NAME = 'api:config:zwave:device_config'
-@asyncio.coroutine
-def test_get_device_config(hass, test_client):
- """Test getting device config."""
+@pytest.fixture
+def client(loop, hass, test_client):
+ """Client to communicate with Z-Wave config views."""
with patch.object(config, 'SECTIONS', ['zwave']):
- yield from async_setup_component(hass, 'config', {})
+ loop.run_until_complete(async_setup_component(hass, 'config', {}))
- client = yield from test_client(hass.http.app)
+ return loop.run_until_complete(test_client(hass.http.app))
+
+@asyncio.coroutine
+def test_get_device_config(client):
+ """Test getting device config."""
def mock_read(path):
"""Mock reading data."""
return {
@@ -47,13 +49,8 @@ def test_get_device_config(hass, test_client):
@asyncio.coroutine
-def test_update_device_config(hass, test_client):
+def test_update_device_config(client):
"""Test updating device config."""
- with patch.object(config, 'SECTIONS', ['zwave']):
- yield from async_setup_component(hass, 'config', {})
-
- client = yield from test_client(hass.http.app)
-
orig_data = {
'hello.beer': {
'ignored': True,
@@ -90,13 +87,8 @@ def test_update_device_config(hass, test_client):
@asyncio.coroutine
-def test_update_device_config_invalid_key(hass, test_client):
+def test_update_device_config_invalid_key(client):
"""Test updating device config."""
- with patch.object(config, 'SECTIONS', ['zwave']):
- yield from async_setup_component(hass, 'config', {})
-
- client = yield from test_client(hass.http.app)
-
resp = yield from client.post(
'/api/config/zwave/device_config/invalid_entity', data=json.dumps({
'polling_intensity': 2
@@ -106,13 +98,8 @@ def test_update_device_config_invalid_key(hass, test_client):
@asyncio.coroutine
-def test_update_device_config_invalid_data(hass, test_client):
+def test_update_device_config_invalid_data(client):
"""Test updating device config."""
- with patch.object(config, 'SECTIONS', ['zwave']):
- yield from async_setup_component(hass, 'config', {})
-
- client = yield from test_client(hass.http.app)
-
resp = yield from client.post(
'/api/config/zwave/device_config/hello.beer', data=json.dumps({
'invalid_option': 2
@@ -122,13 +109,8 @@ def test_update_device_config_invalid_data(hass, test_client):
@asyncio.coroutine
-def test_update_device_config_invalid_json(hass, test_client):
+def test_update_device_config_invalid_json(client):
"""Test updating device config."""
- with patch.object(config, 'SECTIONS', ['zwave']):
- yield from async_setup_component(hass, 'config', {})
-
- client = yield from test_client(hass.http.app)
-
resp = yield from client.post(
'/api/config/zwave/device_config/hello.beer', data='not json')
@@ -136,11 +118,8 @@ def test_update_device_config_invalid_json(hass, test_client):
@asyncio.coroutine
-def test_get_values(hass, test_client):
+def test_get_values(hass, client):
"""Test getting values on node."""
- app = mock_http_component_app(hass)
- ZWaveNodeValueView().register(app.router)
-
node = MockNode(node_id=1)
value = MockValue(value_id=123456, node=node, label='Test Label',
instance=1, index=2, poll_intensity=4)
@@ -150,8 +129,6 @@ def test_get_values(hass, test_client):
values2 = MockEntityValues(primary=value2)
hass.data[const.DATA_ENTITY_VALUES] = [values, values2]
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/values/1')
assert resp.status == 200
@@ -168,11 +145,8 @@ def test_get_values(hass, test_client):
@asyncio.coroutine
-def test_get_groups(hass, test_client):
+def test_get_groups(hass, client):
"""Test getting groupdata on node."""
- app = mock_http_component_app(hass)
- ZWaveNodeGroupView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
node = MockNode(node_id=2)
node.groups.associations = 'assoc'
@@ -182,8 +156,6 @@ def test_get_groups(hass, test_client):
node.groups = {1: node.groups}
network.nodes = {2: node}
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/groups/2')
assert resp.status == 200
@@ -200,18 +172,13 @@ def test_get_groups(hass, test_client):
@asyncio.coroutine
-def test_get_groups_nogroups(hass, test_client):
+def test_get_groups_nogroups(hass, client):
"""Test getting groupdata on node with no groups."""
- app = mock_http_component_app(hass)
- ZWaveNodeGroupView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
node = MockNode(node_id=2)
network.nodes = {2: node}
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/groups/2')
assert resp.status == 200
@@ -221,16 +188,11 @@ def test_get_groups_nogroups(hass, test_client):
@asyncio.coroutine
-def test_get_groups_nonode(hass, test_client):
+def test_get_groups_nonode(hass, client):
"""Test getting groupdata on nonexisting node."""
- app = mock_http_component_app(hass)
- ZWaveNodeGroupView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
network.nodes = {1: 1, 5: 5}
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/groups/2')
assert resp.status == 404
@@ -240,11 +202,8 @@ def test_get_groups_nonode(hass, test_client):
@asyncio.coroutine
-def test_get_config(hass, test_client):
+def test_get_config(hass, client):
"""Test getting config on node."""
- app = mock_http_component_app(hass)
- ZWaveNodeConfigView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
node = MockNode(node_id=2)
value = MockValue(
@@ -261,8 +220,6 @@ def test_get_config(hass, test_client):
network.nodes = {2: node}
node.get_values.return_value = node.values
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/config/2')
assert resp.status == 200
@@ -278,19 +235,14 @@ def test_get_config(hass, test_client):
@asyncio.coroutine
-def test_get_config_noconfig_node(hass, test_client):
+def test_get_config_noconfig_node(hass, client):
"""Test getting config on node without config."""
- app = mock_http_component_app(hass)
- ZWaveNodeConfigView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
node = MockNode(node_id=2)
network.nodes = {2: node}
node.get_values.return_value = node.values
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/config/2')
assert resp.status == 200
@@ -300,16 +252,11 @@ def test_get_config_noconfig_node(hass, test_client):
@asyncio.coroutine
-def test_get_config_nonode(hass, test_client):
+def test_get_config_nonode(hass, client):
"""Test getting config on nonexisting node."""
- app = mock_http_component_app(hass)
- ZWaveNodeConfigView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
network.nodes = {1: 1, 5: 5}
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/config/2')
assert resp.status == 404
@@ -319,16 +266,11 @@ def test_get_config_nonode(hass, test_client):
@asyncio.coroutine
-def test_get_usercodes_nonode(hass, test_client):
+def test_get_usercodes_nonode(hass, client):
"""Test getting usercodes on nonexisting node."""
- app = mock_http_component_app(hass)
- ZWaveUserCodeView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
network.nodes = {1: 1, 5: 5}
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/usercodes/2')
assert resp.status == 404
@@ -338,11 +280,8 @@ def test_get_usercodes_nonode(hass, test_client):
@asyncio.coroutine
-def test_get_usercodes(hass, test_client):
+def test_get_usercodes(hass, client):
"""Test getting usercodes on node."""
- app = mock_http_component_app(hass)
- ZWaveUserCodeView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
node = MockNode(node_id=18,
command_classes=[const.COMMAND_CLASS_USER_CODE])
@@ -356,8 +295,6 @@ def test_get_usercodes(hass, test_client):
network.nodes = {18: node}
node.get_values.return_value = node.values
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/usercodes/18')
assert resp.status == 200
@@ -369,19 +306,14 @@ def test_get_usercodes(hass, test_client):
@asyncio.coroutine
-def test_get_usercode_nousercode_node(hass, test_client):
+def test_get_usercode_nousercode_node(hass, client):
"""Test getting usercodes on node without usercodes."""
- app = mock_http_component_app(hass)
- ZWaveUserCodeView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
node = MockNode(node_id=18)
network.nodes = {18: node}
node.get_values.return_value = node.values
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/usercodes/18')
assert resp.status == 200
@@ -391,11 +323,8 @@ def test_get_usercode_nousercode_node(hass, test_client):
@asyncio.coroutine
-def test_get_usercodes_no_genreuser(hass, test_client):
+def test_get_usercodes_no_genreuser(hass, client):
"""Test getting usercodes on node missing genre user."""
- app = mock_http_component_app(hass)
- ZWaveUserCodeView().register(app.router)
-
network = hass.data[DATA_NETWORK] = MagicMock()
node = MockNode(node_id=18,
command_classes=[const.COMMAND_CLASS_USER_CODE])
@@ -409,8 +338,6 @@ def test_get_usercodes_no_genreuser(hass, test_client):
network.nodes = {18: node}
node.get_values.return_value = node.values
- client = yield from test_client(app)
-
resp = yield from client.get('/api/zwave/usercodes/18')
assert resp.status == 200
@@ -420,13 +347,8 @@ def test_get_usercodes_no_genreuser(hass, test_client):
@asyncio.coroutine
-def test_save_config_no_network(hass, test_client):
+def test_save_config_no_network(hass, 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
@@ -435,15 +357,10 @@ def test_save_config_no_network(hass, test_client):
@asyncio.coroutine
-def test_save_config(hass, test_client):
+def test_save_config(hass, 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
diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py
index 0b49e21674e..23a7b32fc28 100644
--- a/tests/components/cover/test_mqtt.py
+++ b/tests/components/cover/test_mqtt.py
@@ -116,16 +116,17 @@ class TestCoverMQTT(unittest.TestCase):
cover.open_cover(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'OPEN', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'OPEN', 0, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('cover.test')
self.assertEqual(STATE_OPEN, state.state)
cover.close_cover(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'CLOSE', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'CLOSE', 0, False)
state = self.hass.states.get('cover.test')
self.assertEqual(STATE_CLOSED, state.state)
@@ -147,8 +148,8 @@ class TestCoverMQTT(unittest.TestCase):
cover.open_cover(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'OPEN', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'OPEN', 2, False)
state = self.hass.states.get('cover.test')
self.assertEqual(STATE_UNKNOWN, state.state)
@@ -170,8 +171,8 @@ class TestCoverMQTT(unittest.TestCase):
cover.close_cover(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'CLOSE', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'CLOSE', 2, False)
state = self.hass.states.get('cover.test')
self.assertEqual(STATE_UNKNOWN, state.state)
@@ -193,8 +194,8 @@ class TestCoverMQTT(unittest.TestCase):
cover.stop_cover(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'STOP', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'STOP', 2, False)
state = self.hass.states.get('cover.test')
self.assertEqual(STATE_UNKNOWN, state.state)
@@ -295,8 +296,8 @@ class TestCoverMQTT(unittest.TestCase):
cover.set_cover_position(self.hass, 100, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('position-topic', '38', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'position-topic', '38', 0, False)
def test_set_position_untemplated(self):
"""Test setting cover position via template."""
@@ -316,8 +317,8 @@ class TestCoverMQTT(unittest.TestCase):
cover.set_cover_position(self.hass, 62, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('position-topic', 62, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'position-topic', 62, 0, False)
def test_no_command_topic(self):
"""Test with no command topic."""
@@ -401,14 +402,15 @@ class TestCoverMQTT(unittest.TestCase):
cover.open_cover_tilt(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('tilt-command-topic', 100, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'tilt-command-topic', 100, 0, False)
+ self.mock_publish.async_publish.reset_mock()
cover.close_cover_tilt(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('tilt-command-topic', 0, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'tilt-command-topic', 0, 0, False)
def test_tilt_given_value(self):
"""Test tilting to a given value."""
@@ -432,14 +434,15 @@ class TestCoverMQTT(unittest.TestCase):
cover.open_cover_tilt(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('tilt-command-topic', 400, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'tilt-command-topic', 400, 0, False)
+ self.mock_publish.async_publish.reset_mock()
cover.close_cover_tilt(self.hass, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('tilt-command-topic', 125, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'tilt-command-topic', 125, 0, False)
def test_tilt_via_topic(self):
"""Test tilt by updating status via MQTT."""
@@ -538,8 +541,8 @@ class TestCoverMQTT(unittest.TestCase):
cover.set_cover_tilt_position(self.hass, 50, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('tilt-command-topic', 50, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'tilt-command-topic', 50, 0, False)
def test_tilt_position_altered_range(self):
"""Test tilt via method invocation with altered range."""
@@ -565,8 +568,8 @@ class TestCoverMQTT(unittest.TestCase):
cover.set_cover_tilt_position(self.hass, 50, 'cover.test')
self.hass.block_till_done()
- self.assertEqual(('tilt-command-topic', 25, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'tilt-command-topic', 25, 0, False)
def test_find_percentage_in_range_defaults(self):
"""Test find percentage in range with default range."""
diff --git a/tests/components/cover/test_zwave.py b/tests/components/cover/test_zwave.py
index 4b9be9f5e00..b870075d39f 100644
--- a/tests/components/cover/test_zwave.py
+++ b/tests/components/cover/test_zwave.py
@@ -118,7 +118,7 @@ def test_roller_commands(hass, mock_openzwave):
device = zwave.get_device(hass=hass, node=node, values=values,
node_config={})
- device.set_cover_position(25)
+ device.set_cover_position(position=25)
assert node.set_dimmer.called
value_id, brightness = node.set_dimmer.mock_calls[0][1]
assert value_id == value.value_id
diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py
index f8d3fdf128b..48ddf1d3692 100644
--- a/tests/components/device_tracker/test_asuswrt.py
+++ b/tests/components/device_tracker/test_asuswrt.py
@@ -52,8 +52,8 @@ WL_DEVICES = {
ARP_DATA = [
'? (123.123.123.125) at 01:02:03:04:06:08 [ether] on eth0\r',
- '? (123.123.123.126) at 08:09:10:11:12:14 [ether] on br0\r'
- '? (123.123.123.127) at on br0\r'
+ '? (123.123.123.126) at 08:09:10:11:12:14 [ether] on br0\r',
+ '? (123.123.123.127) at on br0\r',
]
ARP_DEVICES = {
@@ -65,8 +65,10 @@ ARP_DEVICES = {
NEIGH_DATA = [
'123.123.123.125 dev eth0 lladdr 01:02:03:04:06:08 REACHABLE\r',
- '123.123.123.126 dev br0 lladdr 08:09:10:11:12:14 STALE\r'
- '123.123.123.127 dev br0 FAILED\r'
+ '123.123.123.126 dev br0 lladdr 08:09:10:11:12:14 REACHABLE\r',
+ '123.123.123.127 dev br0 FAILED\r',
+ '123.123.123.128 dev br0 lladdr 08:09:15:15:15:15 DELAY\r',
+ 'fe80::feff:a6ff:feff:12ff dev br0 lladdr fc:ff:a6:ff:12:ff STALE\r',
]
NEIGH_DEVICES = {
@@ -473,7 +475,7 @@ class TestSshConnection(unittest.TestCase):
def setUp(self):
"""Setup test env."""
self.connection = SshConnection(
- 'fake', 'fake', 'fake', 'fake', 'fake', 'fake')
+ 'fake', 'fake', 'fake', 'fake', 'fake')
self.connection._connected = True
def test_run_command_exception_eof(self):
@@ -513,7 +515,7 @@ class TestTelnetConnection(unittest.TestCase):
def setUp(self):
"""Setup test env."""
self.connection = TelnetConnection(
- 'fake', 'fake', 'fake', 'fake', 'fake')
+ 'fake', 'fake', 'fake', 'fake')
self.connection._connected = True
def test_run_command_exception_eof(self):
diff --git a/tests/components/device_tracker/test_automatic.py b/tests/components/device_tracker/test_automatic.py
index d40c1518ffa..d90b5c0dd62 100644
--- a/tests/components/device_tracker/test_automatic.py
+++ b/tests/components/device_tracker/test_automatic.py
@@ -5,11 +5,10 @@ import logging
from unittest.mock import patch, MagicMock
import aioautomatic
+from homeassistant.setup import async_setup_component
from homeassistant.components.device_tracker.automatic import (
async_setup_scanner)
-from tests.common import mock_http_component
-
_LOGGER = logging.getLogger(__name__)
@@ -23,8 +22,7 @@ def test_invalid_credentials(
mock_open, mock_isfile, mock_makedirs, mock_json_dump, mock_json_load,
mock_create_session, hass):
"""Test with invalid credentials."""
- mock_http_component(hass)
-
+ hass.loop.run_until_complete(async_setup_component(hass, 'http', {}))
mock_json_load.return_value = {'refresh_token': 'bad_token'}
@asyncio.coroutine
@@ -59,8 +57,7 @@ def test_valid_credentials(
mock_open, mock_isfile, mock_makedirs, mock_json_dump, mock_json_load,
mock_ws_connect, mock_create_session, hass):
"""Test with valid credentials."""
- mock_http_component(hass)
-
+ hass.loop.run_until_complete(async_setup_component(hass, 'http', {}))
mock_json_load.return_value = {'refresh_token': 'good_token'}
session = MagicMock()
diff --git a/tests/components/device_tracker/test_unifi.py b/tests/components/device_tracker/test_unifi.py
index 083315b4c71..ccc58d728ed 100644
--- a/tests/components/device_tracker/test_unifi.py
+++ b/tests/components/device_tracker/test_unifi.py
@@ -53,7 +53,8 @@ def test_config_valid_verify_ssl(hass, mock_scanner, mock_ctrl):
assert mock_scanner.call_count == 1
assert mock_scanner.call_args == mock.call(mock_ctrl.return_value,
- DEFAULT_DETECTION_TIME)
+ DEFAULT_DETECTION_TIME,
+ None)
def test_config_minimal(hass, mock_scanner, mock_ctrl):
@@ -74,7 +75,8 @@ def test_config_minimal(hass, mock_scanner, mock_ctrl):
assert mock_scanner.call_count == 1
assert mock_scanner.call_args == mock.call(mock_ctrl.return_value,
- DEFAULT_DETECTION_TIME)
+ DEFAULT_DETECTION_TIME,
+ None)
def test_config_full(hass, mock_scanner, mock_ctrl):
@@ -100,7 +102,8 @@ def test_config_full(hass, mock_scanner, mock_ctrl):
assert mock_scanner.call_count == 1
assert mock_scanner.call_args == mock.call(mock_ctrl.return_value,
- DEFAULT_DETECTION_TIME)
+ DEFAULT_DETECTION_TIME,
+ None)
def test_config_error():
@@ -148,11 +151,13 @@ def test_scanner_update():
"""Test the scanner update."""
ctrl = mock.MagicMock()
fake_clients = [
- {'mac': '123', 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
- {'mac': '234', 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ {'mac': '123', 'essid': 'barnet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ {'mac': '234', 'essid': 'barnet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
]
ctrl.get_clients.return_value = fake_clients
- unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME)
+ unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None)
assert ctrl.get_clients.call_count == 1
assert ctrl.get_clients.call_args == mock.call()
@@ -162,36 +167,61 @@ def test_scanner_update_error():
ctrl = mock.MagicMock()
ctrl.get_clients.side_effect = APIError(
'/', 500, 'foo', {}, None)
- unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME)
+ unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None)
def test_scan_devices():
"""Test the scanning for devices."""
ctrl = mock.MagicMock()
fake_clients = [
- {'mac': '123', 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
- {'mac': '234', 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ {'mac': '123', 'essid': 'barnet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ {'mac': '234', 'essid': 'barnet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
]
ctrl.get_clients.return_value = fake_clients
- scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME)
+ scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None)
assert set(scanner.scan_devices()) == set(['123', '234'])
+def test_scan_devices_filtered():
+ """Test the scanning for devices based on SSID."""
+ ctrl = mock.MagicMock()
+ fake_clients = [
+ {'mac': '123', 'essid': 'foonet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ {'mac': '234', 'essid': 'foonet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ {'mac': '567', 'essid': 'notnet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ {'mac': '890', 'essid': 'barnet',
+ 'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
+ ]
+
+ ssid_filter = ['foonet', 'barnet']
+ ctrl.get_clients.return_value = fake_clients
+ scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, ssid_filter)
+ assert set(scanner.scan_devices()) == set(['123', '234', '890'])
+
+
def test_get_device_name():
"""Test the getting of device names."""
ctrl = mock.MagicMock()
fake_clients = [
{'mac': '123',
'hostname': 'foobar',
+ 'essid': 'barnet',
'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
{'mac': '234',
'name': 'Nice Name',
+ 'essid': 'barnet',
'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
{'mac': '456',
+ 'essid': 'barnet',
'last_seen': '1504786810'},
]
ctrl.get_clients.return_value = fake_clients
- scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME)
+ scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None)
assert scanner.get_device_name('123') == 'foobar'
assert scanner.get_device_name('234') == 'Nice Name'
assert scanner.get_device_name('456') is None
diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py
new file mode 100644
index 00000000000..6fcd9d2229f
--- /dev/null
+++ b/tests/components/hassio/__init__.py
@@ -0,0 +1,4 @@
+"""Tests for Hassio component."""
+
+API_PASSWORD = 'pass1234'
+HASSIO_TOKEN = '123456'
diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py
new file mode 100644
index 00000000000..56d6cbe666e
--- /dev/null
+++ b/tests/components/hassio/conftest.py
@@ -0,0 +1,50 @@
+"""Fixtures for Hass.io."""
+import os
+from unittest.mock import patch, Mock
+
+import pytest
+
+from homeassistant.setup import async_setup_component
+from homeassistant.components.hassio.handler import HassIO
+
+from tests.common import mock_coro
+from . import API_PASSWORD, HASSIO_TOKEN
+
+
+@pytest.fixture
+def hassio_env():
+ """Fixture to inject hassio env."""
+ with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \
+ patch('homeassistant.components.hassio.HassIO.is_connected',
+ Mock(return_value=mock_coro(
+ {"result": "ok", "data": {}}))), \
+ patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}), \
+ patch('homeassistant.components.hassio.HassIO.'
+ 'get_homeassistant_info',
+ Mock(return_value=mock_coro(None))):
+ yield
+
+
+@pytest.fixture
+def hassio_client(hassio_env, hass, test_client):
+ """Create mock hassio http client."""
+ with patch('homeassistant.components.hassio.HassIO.update_hass_api',
+ Mock(return_value=mock_coro({"result": "ok"}))), \
+ patch('homeassistant.components.hassio.HassIO.'
+ 'get_homeassistant_info',
+ Mock(return_value=mock_coro(None))):
+ hass.loop.run_until_complete(async_setup_component(hass, 'hassio', {
+ 'http': {
+ 'api_password': API_PASSWORD
+ }
+ }))
+ yield hass.loop.run_until_complete(test_client(hass.http.app))
+
+
+@pytest.fixture
+def hassio_handler(hass, aioclient_mock):
+ """Create mock hassio handler."""
+ websession = hass.helpers.aiohttp_client.async_get_clientsession()
+
+ with patch.dict(os.environ, {'HASSIO_TOKEN': HASSIO_TOKEN}):
+ yield HassIO(hass.loop, websession, "127.0.0.1")
diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py
new file mode 100644
index 00000000000..78745489a78
--- /dev/null
+++ b/tests/components/hassio/test_handler.py
@@ -0,0 +1,90 @@
+"""The tests for the hassio component."""
+import asyncio
+
+import aiohttp
+
+
+@asyncio.coroutine
+def test_api_ping(hassio_handler, aioclient_mock):
+ """Test setup with API ping."""
+ aioclient_mock.get(
+ "http://127.0.0.1/supervisor/ping", json={'result': 'ok'})
+
+ assert (yield from hassio_handler.is_connected())
+ assert aioclient_mock.call_count == 1
+
+
+@asyncio.coroutine
+def test_api_ping_error(hassio_handler, aioclient_mock):
+ """Test setup with API ping error."""
+ aioclient_mock.get(
+ "http://127.0.0.1/supervisor/ping", json={'result': 'error'})
+
+ assert not (yield from hassio_handler.is_connected())
+ assert aioclient_mock.call_count == 1
+
+
+@asyncio.coroutine
+def test_api_ping_exeption(hassio_handler, aioclient_mock):
+ """Test setup with API ping exception."""
+ aioclient_mock.get(
+ "http://127.0.0.1/supervisor/ping", exc=aiohttp.ClientError())
+
+ assert not (yield from hassio_handler.is_connected())
+ assert aioclient_mock.call_count == 1
+
+
+@asyncio.coroutine
+def test_api_homeassistant_info(hassio_handler, aioclient_mock):
+ """Test setup with API homeassistant info."""
+ aioclient_mock.get(
+ "http://127.0.0.1/homeassistant/info", json={
+ 'result': 'ok', 'data': {'last_version': '10.0'}})
+
+ data = yield from hassio_handler.get_homeassistant_info()
+ assert aioclient_mock.call_count == 1
+ assert data['last_version'] == "10.0"
+
+
+@asyncio.coroutine
+def test_api_homeassistant_info_error(hassio_handler, aioclient_mock):
+ """Test setup with API homeassistant info error."""
+ aioclient_mock.get(
+ "http://127.0.0.1/homeassistant/info", json={
+ 'result': 'error', 'message': None})
+
+ data = yield from hassio_handler.get_homeassistant_info()
+ assert aioclient_mock.call_count == 1
+ assert data is None
+
+
+@asyncio.coroutine
+def test_api_homeassistant_stop(hassio_handler, aioclient_mock):
+ """Test setup with API HomeAssistant stop."""
+ aioclient_mock.post(
+ "http://127.0.0.1/homeassistant/stop", json={'result': 'ok'})
+
+ assert (yield from hassio_handler.stop_homeassistant())
+ assert aioclient_mock.call_count == 1
+
+
+@asyncio.coroutine
+def test_api_homeassistant_restart(hassio_handler, aioclient_mock):
+ """Test setup with API HomeAssistant restart."""
+ aioclient_mock.post(
+ "http://127.0.0.1/homeassistant/restart", json={'result': 'ok'})
+
+ assert (yield from hassio_handler.restart_homeassistant())
+ assert aioclient_mock.call_count == 1
+
+
+@asyncio.coroutine
+def test_api_homeassistant_config(hassio_handler, aioclient_mock):
+ """Test setup with API HomeAssistant restart."""
+ aioclient_mock.post(
+ "http://127.0.0.1/homeassistant/check", json={
+ 'result': 'ok', 'data': {'test': 'bla'}})
+
+ data = yield from hassio_handler.check_homeassistant_config()
+ assert data['data']['test'] == 'bla'
+ assert aioclient_mock.call_count == 1
diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py
new file mode 100644
index 00000000000..ed425ad8cca
--- /dev/null
+++ b/tests/components/hassio/test_http.py
@@ -0,0 +1,133 @@
+"""The tests for the hassio component."""
+import asyncio
+from unittest.mock import patch, Mock, MagicMock
+
+import pytest
+
+from homeassistant.const import HTTP_HEADER_HA_AUTH
+
+from tests.common import mock_coro
+from . import API_PASSWORD
+
+
+@asyncio.coroutine
+def test_forward_request(hassio_client):
+ """Test fetching normal path."""
+ response = MagicMock()
+ response.read.return_value = mock_coro('data')
+
+ with patch('homeassistant.components.hassio.HassIOView._command_proxy',
+ Mock(return_value=mock_coro(response))), \
+ patch('homeassistant.components.hassio.http'
+ '._create_response') as mresp:
+ mresp.return_value = 'response'
+ resp = yield from hassio_client.post('/api/hassio/beer', headers={
+ HTTP_HEADER_HA_AUTH: API_PASSWORD
+ })
+
+ # Check we got right response
+ assert resp.status == 200
+ body = yield from resp.text()
+ assert body == 'response'
+
+ # Check we forwarded command
+ assert len(mresp.mock_calls) == 1
+ assert mresp.mock_calls[0][1] == (response, 'data')
+
+
+@asyncio.coroutine
+def test_auth_required_forward_request(hassio_client):
+ """Test auth required for normal request."""
+ resp = yield from hassio_client.post('/api/hassio/beer')
+
+ # Check we got right response
+ assert resp.status == 401
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize(
+ 'build_type', [
+ 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html',
+ 'latest/hassio-app.html'
+ ])
+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')
+
+ with patch('homeassistant.components.hassio.HassIOView._command_proxy',
+ Mock(return_value=mock_coro(response))), \
+ patch('homeassistant.components.hassio.http.'
+ '_create_response') as mresp:
+ mresp.return_value = 'response'
+ resp = yield from hassio_client.get(
+ '/api/hassio/app-{}'.format(build_type))
+
+ # Check we got right response
+ assert resp.status == 200
+ body = yield from resp.text()
+ assert body == 'response'
+
+ # Check we forwarded command
+ assert len(mresp.mock_calls) == 1
+ assert mresp.mock_calls[0][1] == (response, 'data')
+
+
+@asyncio.coroutine
+def test_forward_request_no_auth_for_logo(hassio_client):
+ """Test no auth needed for ."""
+ response = MagicMock()
+ response.read.return_value = mock_coro('data')
+
+ with patch('homeassistant.components.hassio.HassIOView._command_proxy',
+ Mock(return_value=mock_coro(response))), \
+ patch('homeassistant.components.hassio.http.'
+ '_create_response') as mresp:
+ mresp.return_value = 'response'
+ resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo')
+
+ # Check we got right response
+ assert resp.status == 200
+ body = yield from resp.text()
+ assert body == 'response'
+
+ # Check we forwarded command
+ assert len(mresp.mock_calls) == 1
+ assert mresp.mock_calls[0][1] == (response, 'data')
+
+
+@asyncio.coroutine
+def test_forward_log_request(hassio_client):
+ """Test fetching normal log path."""
+ response = MagicMock()
+ response.read.return_value = mock_coro('data')
+
+ with patch('homeassistant.components.hassio.HassIOView._command_proxy',
+ Mock(return_value=mock_coro(response))), \
+ patch('homeassistant.components.hassio.http.'
+ '_create_response_log') as mresp:
+ mresp.return_value = 'response'
+ resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={
+ HTTP_HEADER_HA_AUTH: API_PASSWORD
+ })
+
+ # Check we got right response
+ assert resp.status == 200
+ body = yield from resp.text()
+ assert body == 'response'
+
+ # Check we forwarded command
+ assert len(mresp.mock_calls) == 1
+ assert mresp.mock_calls[0][1] == (response, 'data')
+
+
+@asyncio.coroutine
+def test_bad_gateway_when_cannot_find_supervisor(hassio_client):
+ """Test we get a bad gateway error if we can't find supervisor."""
+ with patch('homeassistant.components.hassio.http.async_timeout.timeout',
+ side_effect=asyncio.TimeoutError):
+ resp = yield from hassio_client.get(
+ '/api/hassio/addons/test/info', headers={
+ HTTP_HEADER_HA_AUTH: API_PASSWORD
+ })
+ assert resp.status == 502
diff --git a/tests/components/test_hassio.py b/tests/components/hassio/test_init.py
similarity index 67%
rename from tests/components/test_hassio.py
rename to tests/components/hassio/test_init.py
index 8fb017309de..e17419e7fd5 100644
--- a/tests/components/test_hassio.py
+++ b/tests/components/hassio/test_init.py
@@ -1,68 +1,13 @@
"""The tests for the hassio component."""
import asyncio
import os
-from unittest.mock import patch, Mock, MagicMock
+from unittest.mock import patch, Mock
-import pytest
-
-from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.setup import async_setup_component
from homeassistant.components.hassio import async_check_config
from tests.common import mock_coro
-API_PASSWORD = 'pass1234'
-
-
-@pytest.fixture
-def hassio_env():
- """Fixture to inject hassio env."""
- with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \
- patch('homeassistant.components.hassio.HassIO.is_connected',
- Mock(return_value=mock_coro(
- {"result": "ok", "data": {}}))), \
- patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}), \
- patch('homeassistant.components.hassio.HassIO.'
- 'get_homeassistant_info',
- Mock(return_value=mock_coro(None))):
- yield
-
-
-@pytest.fixture
-def hassio_client(hassio_env, hass, test_client):
- """Create mock hassio http client."""
- with patch('homeassistant.components.hassio.HassIO.update_hass_api',
- Mock(return_value=mock_coro({"result": "ok"}))), \
- patch('homeassistant.components.hassio.HassIO.'
- 'get_homeassistant_info',
- Mock(return_value=mock_coro(None))):
- hass.loop.run_until_complete(async_setup_component(hass, 'hassio', {
- 'http': {
- 'api_password': API_PASSWORD
- }
- }))
- yield hass.loop.run_until_complete(test_client(hass.http.app))
-
-
-@asyncio.coroutine
-def test_fail_setup_without_environ_var(hass):
- """Fail setup if no environ variable set."""
- with patch.dict(os.environ, {}, clear=True):
- result = yield from async_setup_component(hass, 'hassio', {})
- assert not result
-
-
-@asyncio.coroutine
-def test_fail_setup_cannot_connect(hass):
- """Fail setup if cannot connect."""
- with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \
- patch('homeassistant.components.hassio.HassIO.is_connected',
- Mock(return_value=mock_coro(None))):
- result = yield from async_setup_component(hass, 'hassio', {})
- assert not result
-
- assert not hass.components.hassio.is_hassio()
-
@asyncio.coroutine
def test_setup_api_ping(hass, aioclient_mock):
@@ -209,6 +154,26 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock):
assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456"
+@asyncio.coroutine
+def test_fail_setup_without_environ_var(hass):
+ """Fail setup if no environ variable set."""
+ with patch.dict(os.environ, {}, clear=True):
+ result = yield from async_setup_component(hass, 'hassio', {})
+ assert not result
+
+
+@asyncio.coroutine
+def test_fail_setup_cannot_connect(hass):
+ """Fail setup if cannot connect."""
+ with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \
+ patch('homeassistant.components.hassio.HassIO.is_connected',
+ Mock(return_value=mock_coro(None))):
+ result = yield from async_setup_component(hass, 'hassio', {})
+ assert not result
+
+ assert not hass.components.hassio.is_hassio()
+
+
@asyncio.coroutine
def test_service_register(hassio_env, hass):
"""Check if service will be setup."""
@@ -276,12 +241,13 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
yield from hass.services.async_call('hassio', 'snapshot_partial', {
'addons': ['test'],
'folders': ['ssl'],
+ 'password': "123456",
})
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 8
assert aioclient_mock.mock_calls[-1][2] == {
- 'addons': ['test'], 'folders': ['ssl']}
+ 'addons': ['test'], 'folders': ['ssl'], 'password': "123456"}
yield from hass.services.async_call('hassio', 'restore_full', {
'snapshot': 'test',
@@ -291,12 +257,15 @@ def test_service_calls(hassio_env, hass, aioclient_mock):
'homeassistant': False,
'addons': ['test'],
'folders': ['ssl'],
+ 'password': "123456",
})
yield from hass.async_block_till_done()
assert aioclient_mock.call_count == 10
assert aioclient_mock.mock_calls[-1][2] == {
- 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False}
+ 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False,
+ 'password': "123456"
+ }
@asyncio.coroutine
@@ -348,123 +317,3 @@ def test_check_config_fail(hassio_env, hass, aioclient_mock):
'result': 'error', 'message': "Error"})
assert (yield from async_check_config(hass)) == "Error"
-
-
-@asyncio.coroutine
-def test_forward_request(hassio_client):
- """Test fetching normal path."""
- response = MagicMock()
- response.read.return_value = mock_coro('data')
-
- with patch('homeassistant.components.hassio.HassIO.command_proxy',
- Mock(return_value=mock_coro(response))), \
- patch('homeassistant.components.hassio._create_response') as mresp:
- mresp.return_value = 'response'
- resp = yield from hassio_client.post('/api/hassio/beer', headers={
- HTTP_HEADER_HA_AUTH: API_PASSWORD
- })
-
- # Check we got right response
- assert resp.status == 200
- body = yield from resp.text()
- assert body == 'response'
-
- # Check we forwarded command
- assert len(mresp.mock_calls) == 1
- assert mresp.mock_calls[0][1] == (response, 'data')
-
-
-@asyncio.coroutine
-def test_auth_required_forward_request(hassio_client):
- """Test auth required for normal request."""
- resp = yield from hassio_client.post('/api/hassio/beer')
-
- # Check we got right response
- assert resp.status == 401
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize(
- 'build_type', [
- 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html',
- 'latest/hassio-app.html'
- ])
-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')
-
- with patch('homeassistant.components.hassio.HassIO.command_proxy',
- 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/app-{}'.format(build_type))
-
- # Check we got right response
- assert resp.status == 200
- body = yield from resp.text()
- assert body == 'response'
-
- # Check we forwarded command
- assert len(mresp.mock_calls) == 1
- assert mresp.mock_calls[0][1] == (response, 'data')
-
-
-@asyncio.coroutine
-def test_forward_request_no_auth_for_logo(hassio_client):
- """Test no auth needed for ."""
- response = MagicMock()
- response.read.return_value = mock_coro('data')
-
- with patch('homeassistant.components.hassio.HassIO.command_proxy',
- 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/addons/bl_b392/logo')
-
- # Check we got right response
- assert resp.status == 200
- body = yield from resp.text()
- assert body == 'response'
-
- # Check we forwarded command
- assert len(mresp.mock_calls) == 1
- assert mresp.mock_calls[0][1] == (response, 'data')
-
-
-@asyncio.coroutine
-def test_forward_log_request(hassio_client):
- """Test fetching normal log path."""
- response = MagicMock()
- response.read.return_value = mock_coro('data')
-
- with patch('homeassistant.components.hassio.HassIO.command_proxy',
- Mock(return_value=mock_coro(response))), \
- patch('homeassistant.components.hassio.'
- '_create_response_log') as mresp:
- mresp.return_value = 'response'
- resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={
- HTTP_HEADER_HA_AUTH: API_PASSWORD
- })
-
- # Check we got right response
- assert resp.status == 200
- body = yield from resp.text()
- assert body == 'response'
-
- # Check we forwarded command
- assert len(mresp.mock_calls) == 1
- assert mresp.mock_calls[0][1] == (response, 'data')
-
-
-@asyncio.coroutine
-def test_bad_gateway_when_cannot_find_supervisor(hassio_client):
- """Test we get a bad gateway error if we can't find supervisor."""
- with patch('homeassistant.components.hassio.async_timeout.timeout',
- side_effect=asyncio.TimeoutError):
- resp = yield from hassio_client.get(
- '/api/hassio/addons/test/info', headers={
- HTTP_HEADER_HA_AUTH: API_PASSWORD
- })
- assert resp.status == 502
diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py
new file mode 100644
index 00000000000..61a60cee2ac
--- /dev/null
+++ b/tests/components/homekit/__init__.py
@@ -0,0 +1 @@
+"""The tests for the homekit component."""
diff --git a/tests/components/homekit/test_covers.py b/tests/components/homekit/test_covers.py
new file mode 100644
index 00000000000..b6e8334346a
--- /dev/null
+++ b/tests/components/homekit/test_covers.py
@@ -0,0 +1,85 @@
+"""Test different accessory types: Covers."""
+import unittest
+
+from homeassistant.core import callback
+from homeassistant.components.cover import (
+ ATTR_POSITION, ATTR_CURRENT_POSITION)
+from homeassistant.components.homekit.covers import Window
+from homeassistant.const import (
+ STATE_UNKNOWN, STATE_OPEN,
+ ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE)
+
+from tests.common import get_test_home_assistant
+
+
+class TestHomekitSensors(unittest.TestCase):
+ """Test class for all accessory types regarding covers."""
+
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.events = []
+
+ @callback
+ def record_event(event):
+ """Track called event."""
+ self.events.append(event)
+
+ self.hass.bus.listen(EVENT_CALL_SERVICE, record_event)
+
+ def tearDown(self):
+ """Stop down everthing that was started."""
+ self.hass.stop()
+
+ def test_window_set_cover_position(self):
+ """Test if accessory and HA are updated accordingly."""
+ window_cover = 'cover.window'
+
+ acc = Window(self.hass, window_cover, 'Cover')
+ acc.run()
+
+ self.assertEqual(acc.char_current_position.value, 0)
+ self.assertEqual(acc.char_target_position.value, 0)
+ # Temporarily disabled due to bug in HAP-python==1.15 with py3.5
+ # self.assertEqual(acc.char_position_state.value, 0)
+
+ self.hass.states.set(window_cover, STATE_UNKNOWN,
+ {ATTR_CURRENT_POSITION: None})
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_position.value, 0)
+ self.assertEqual(acc.char_target_position.value, 0)
+ # Temporarily disabled due to bug in HAP-python==1.15 with py3.5
+ # self.assertEqual(acc.char_position_state.value, 0)
+
+ self.hass.states.set(window_cover, STATE_OPEN,
+ {ATTR_CURRENT_POSITION: 50})
+ self.hass.block_till_done()
+
+ self.assertEqual(acc.char_current_position.value, 50)
+ self.assertEqual(acc.char_target_position.value, 50)
+ self.assertEqual(acc.char_position_state.value, 2)
+
+ # Set from HomeKit
+ acc.char_target_position.set_value(25)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE], 'set_cover_position')
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE_DATA][ATTR_POSITION], 25)
+
+ self.assertEqual(acc.char_current_position.value, 50)
+ self.assertEqual(acc.char_target_position.value, 25)
+ self.assertEqual(acc.char_position_state.value, 0)
+
+ # Set from HomeKit
+ acc.char_target_position.set_value(75)
+ self.hass.block_till_done()
+ self.assertEqual(
+ self.events[0].data[ATTR_SERVICE], 'set_cover_position')
+ self.assertEqual(
+ self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75)
+
+ self.assertEqual(acc.char_current_position.value, 50)
+ self.assertEqual(acc.char_target_position.value, 75)
+ self.assertEqual(acc.char_position_state.value, 1)
diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py
new file mode 100644
index 00000000000..e20e87871b8
--- /dev/null
+++ b/tests/components/homekit/test_get_accessories.py
@@ -0,0 +1,46 @@
+"""Package to test the get_accessory method."""
+from unittest.mock import patch, MagicMock
+
+from homeassistant.core import State
+from homeassistant.components.homekit import (
+ TYPES, get_accessory, import_types)
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES,
+ TEMP_CELSIUS, STATE_UNKNOWN)
+
+
+def test_import_types():
+ """Test if all type files are imported correctly."""
+ try:
+ import_types()
+ assert True
+ # pylint: disable=broad-except
+ except Exception:
+ assert False
+
+
+def test_component_not_supported():
+ """Test with unsupported component."""
+ state = State('demo.unsupported', STATE_UNKNOWN)
+
+ assert True if get_accessory(None, state) is None else False
+
+
+def test_sensor_temperatur_celsius():
+ """Test temperature sensor with celsius as unit."""
+ mock_type = MagicMock()
+ with patch.dict(TYPES, {'TemperatureSensor': mock_type}):
+ state = State('sensor.temperatur', '23',
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ get_accessory(None, state)
+ assert len(mock_type.mock_calls) == 1
+
+
+def test_cover_set_position():
+ """Test cover with support for set_cover_position."""
+ mock_type = MagicMock()
+ with patch.dict(TYPES, {'Window': mock_type}):
+ state = State('cover.setposition', 'open',
+ {ATTR_SUPPORTED_FEATURES: 4})
+ get_accessory(None, state)
+ assert len(mock_type.mock_calls) == 1
diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py
new file mode 100644
index 00000000000..06cb8096140
--- /dev/null
+++ b/tests/components/homekit/test_homekit.py
@@ -0,0 +1,124 @@
+"""Tests for the homekit component."""
+
+import unittest
+from unittest.mock import patch
+
+import voluptuous as vol
+
+from homeassistant import setup
+from homeassistant.core import Event
+from homeassistant.components.homekit import (
+ CONF_PIN_CODE, BRIDGE_NAME, Homekit, valid_pin)
+from homeassistant.components.homekit.covers import Window
+from homeassistant.components.homekit.sensors import TemperatureSensor
+from homeassistant.const import (
+ CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
+
+from tests.common import get_test_home_assistant
+
+HOMEKIT_PATH = 'homeassistant.components.homekit'
+
+CONFIG_MIN = {'homekit': {}}
+CONFIG = {
+ 'homekit': {
+ CONF_PORT: 11111,
+ CONF_PIN_CODE: '987-65-432',
+ }
+}
+
+
+class TestHomekit(unittest.TestCase):
+ """Test the Multicover component."""
+
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop down everthing that was started."""
+ self.hass.stop()
+
+ @patch(HOMEKIT_PATH + '.Homekit.start_driver')
+ @patch(HOMEKIT_PATH + '.Homekit.setup_bridge')
+ @patch(HOMEKIT_PATH + '.Homekit.__init__')
+ def test_setup_min(self, mock_homekit, mock_setup_bridge,
+ mock_start_driver):
+ """Test async_setup with minimal config option."""
+ mock_homekit.return_value = None
+
+ self.assertTrue(setup.setup_component(
+ self.hass, 'homekit', CONFIG_MIN))
+
+ mock_homekit.assert_called_once_with(self.hass, 51826)
+ mock_setup_bridge.assert_called_with(b'123-45-678')
+ mock_start_driver.assert_not_called()
+
+ self.hass.start()
+ self.hass.block_till_done()
+ self.assertEqual(mock_start_driver.call_count, 1)
+
+ @patch(HOMEKIT_PATH + '.Homekit.start_driver')
+ @patch(HOMEKIT_PATH + '.Homekit.setup_bridge')
+ @patch(HOMEKIT_PATH + '.Homekit.__init__')
+ def test_setup_parameters(self, mock_homekit, mock_setup_bridge,
+ mock_start_driver):
+ """Test async_setup with full config option."""
+ mock_homekit.return_value = None
+
+ self.assertTrue(setup.setup_component(
+ self.hass, 'homekit', CONFIG))
+
+ mock_homekit.assert_called_once_with(self.hass, 11111)
+ mock_setup_bridge.assert_called_with(b'987-65-432')
+
+ def test_validate_pincode(self):
+ """Test async_setup with invalid config option."""
+ schema = vol.Schema(valid_pin)
+
+ for value in ('', '123-456-78', 'a23-45-678', '12345678'):
+ with self.assertRaises(vol.MultipleInvalid):
+ schema(value)
+
+ for value in ('123-45-678', '234-56-789'):
+ self.assertTrue(schema(value))
+
+ @patch('pyhap.accessory_driver.AccessoryDriver.persist')
+ @patch('pyhap.accessory_driver.AccessoryDriver.stop')
+ @patch('pyhap.accessory_driver.AccessoryDriver.start')
+ @patch(HOMEKIT_PATH + '.import_types')
+ @patch(HOMEKIT_PATH + '.get_accessory')
+ def test_homekit_pyhap_interaction(
+ self, mock_get_accessory, mock_import_types,
+ mock_driver_start, mock_driver_stop, mock_file_persist):
+ """Test the interaction between the homekit class and pyhap."""
+ acc1 = TemperatureSensor(self.hass, 'sensor.temp', 'Temperature')
+ acc2 = Window(self.hass, 'cover.hall_window', 'Cover')
+ mock_get_accessory.side_effect = [acc1, acc2]
+
+ homekit = Homekit(self.hass, 51826)
+ homekit.setup_bridge(b'123-45-678')
+
+ self.assertEqual(homekit.bridge.display_name, BRIDGE_NAME)
+
+ self.hass.states.set('demo.demo1', 'on')
+ self.hass.states.set('demo.demo2', 'off')
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ homekit.start_driver(Event(EVENT_HOMEASSISTANT_START))
+
+ self.assertEqual(mock_get_accessory.call_count, 2)
+ self.assertEqual(mock_import_types.call_count, 1)
+ self.assertEqual(mock_driver_start.call_count, 1)
+
+ accessories = homekit.bridge.accessories
+ self.assertEqual(accessories[2], acc1)
+ self.assertEqual(accessories[3], acc2)
+
+ mock_driver_stop.assert_not_called()
+
+ self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP)
+ self.hass.block_till_done()
+
+ self.assertEqual(mock_driver_stop.call_count, 1)
diff --git a/tests/components/homekit/test_sensors.py b/tests/components/homekit/test_sensors.py
new file mode 100644
index 00000000000..b7d3de4e90b
--- /dev/null
+++ b/tests/components/homekit/test_sensors.py
@@ -0,0 +1,37 @@
+"""Test different accessory types: Sensors."""
+import unittest
+
+from homeassistant.components.homekit.sensors import TemperatureSensor
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN)
+
+from tests.common import get_test_home_assistant
+
+
+class TestHomekitSensors(unittest.TestCase):
+ """Test class for all accessory types regarding sensors."""
+
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop down everthing that was started."""
+ self.hass.stop()
+
+ def test_temperature_celsius(self):
+ """Test if accessory is updated after state change."""
+ temperature_sensor = 'sensor.temperature'
+
+ acc = TemperatureSensor(self.hass, temperature_sensor, 'Temperature')
+ acc.run()
+
+ self.assertEqual(acc.char_temp.value, 0.0)
+
+ self.hass.states.set(temperature_sensor, STATE_UNKNOWN,
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+
+ self.hass.states.set(temperature_sensor, '20')
+ self.hass.block_till_done()
+ self.assertEqual(acc.char_temp.value, 20)
diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py
index 869e80fff75..ef9817a2f1b 100644
--- a/tests/components/http/__init__.py
+++ b/tests/components/http/__init__.py
@@ -1 +1,38 @@
"""Tests for the HTTP component."""
+import asyncio
+from ipaddress import ip_address
+
+from aiohttp import web
+
+from homeassistant.components.http.const import KEY_REAL_IP
+
+
+def mock_real_ip(app):
+ """Inject middleware to mock real IP.
+
+ Returns a function to set the real IP.
+ """
+ ip_to_mock = None
+
+ def set_ip_to_mock(value):
+ nonlocal ip_to_mock
+ ip_to_mock = value
+
+ @asyncio.coroutine
+ @web.middleware
+ def mock_real_ip(request, handler):
+ """Mock Real IP middleware."""
+ nonlocal ip_to_mock
+
+ request[KEY_REAL_IP] = ip_address(ip_to_mock)
+
+ return (yield from handler(request))
+
+ @asyncio.coroutine
+ def real_ip_startup(app):
+ """Startup of real ip."""
+ app.middlewares.insert(0, mock_real_ip)
+
+ app.on_startup.append(real_ip_startup)
+
+ return set_ip_to_mock
diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py
index ef9c63ad09e..c2687c05a8f 100644
--- a/tests/components/http/test_auth.py
+++ b/tests/components/http/test_auth.py
@@ -1,195 +1,156 @@
"""The tests for the Home Assistant HTTP component."""
# pylint: disable=protected-access
import asyncio
-from ipaddress import ip_address, ip_network
+from ipaddress import ip_network
from unittest.mock import patch
-import aiohttp
+from aiohttp import BasicAuth, web
+from aiohttp.web_exceptions import HTTPUnauthorized
import pytest
-from homeassistant import const
+from homeassistant.const import HTTP_HEADER_HA_AUTH
from homeassistant.setup import async_setup_component
-import homeassistant.components.http as http
-from homeassistant.components.http.const import (
- KEY_TRUSTED_NETWORKS, KEY_USE_X_FORWARDED_FOR, HTTP_HEADER_X_FORWARDED_FOR)
+from homeassistant.components.http.auth import setup_auth
+from homeassistant.components.http.real_ip import setup_real_ip
+from homeassistant.components.http.const import KEY_AUTHENTICATED
+
+from . import mock_real_ip
API_PASSWORD = 'test1234'
# Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases
-TRUSTED_NETWORKS = ['192.0.2.0/24', '2001:DB8:ABCD::/48', '100.64.0.1',
- 'FD01:DB8::1']
+TRUSTED_NETWORKS = [
+ ip_network('192.0.2.0/24'),
+ ip_network('2001:DB8:ABCD::/48'),
+ ip_network('100.64.0.1'),
+ ip_network('FD01:DB8::1'),
+]
TRUSTED_ADDRESSES = ['100.64.0.1', '192.0.2.100', 'FD01:DB8::1',
'2001:DB8:ABCD::1']
UNTRUSTED_ADDRESSES = ['198.51.100.1', '2001:DB8:FA1::1', '127.0.0.1', '::1']
-@pytest.fixture
-def mock_api_client(hass, test_client):
- """Start the Hass HTTP component."""
- hass.loop.run_until_complete(async_setup_component(hass, 'api', {
- 'http': {
- http.CONF_API_PASSWORD: API_PASSWORD,
- }
- }))
- return hass.loop.run_until_complete(test_client(hass.http.app))
+@asyncio.coroutine
+def mock_handler(request):
+ """Return if request was authenticated."""
+ if not request[KEY_AUTHENTICATED]:
+ raise HTTPUnauthorized
+ return web.Response(status=200)
@pytest.fixture
-def mock_trusted_networks(hass, mock_api_client):
- """Mock trusted networks."""
- hass.http.app[KEY_TRUSTED_NETWORKS] = [
- ip_network(trusted_network)
- for trusted_network in TRUSTED_NETWORKS]
+def app():
+ """Fixture to setup a web.Application."""
+ app = web.Application()
+ app.router.add_get('/', mock_handler)
+ setup_real_ip(app, False)
+ return app
@asyncio.coroutine
-def test_access_denied_without_password(mock_api_client):
+def test_auth_middleware_loaded_by_default(hass):
+ """Test accessing to server from banned IP when feature is off."""
+ with patch('homeassistant.components.http.setup_auth') as mock_setup:
+ yield from async_setup_component(hass, 'http', {
+ 'http': {}
+ })
+
+ assert len(mock_setup.mock_calls) == 1
+
+
+@asyncio.coroutine
+def test_access_without_password(app, test_client):
"""Test access without password."""
- resp = yield from mock_api_client.get(const.URL_API)
+ setup_auth(app, [], None)
+ client = yield from test_client(app)
+
+ resp = yield from client.get('/')
+ assert resp.status == 200
+
+
+@asyncio.coroutine
+def test_access_with_password_in_header(app, test_client):
+ """Test access with password in URL."""
+ setup_auth(app, [], API_PASSWORD)
+ client = yield from test_client(app)
+
+ req = yield from client.get(
+ '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD})
+ assert req.status == 200
+
+ req = yield from client.get(
+ '/', headers={HTTP_HEADER_HA_AUTH: 'wrong-pass'})
+ assert req.status == 401
+
+
+@asyncio.coroutine
+def test_access_with_password_in_query(app, test_client):
+ """Test access without password."""
+ setup_auth(app, [], API_PASSWORD)
+ client = yield from test_client(app)
+
+ resp = yield from client.get('/', params={
+ 'api_password': API_PASSWORD
+ })
+ assert resp.status == 200
+
+ resp = yield from client.get('/')
assert resp.status == 401
-
-@asyncio.coroutine
-def test_access_denied_with_wrong_password_in_header(mock_api_client):
- """Test access with wrong password."""
- resp = yield from mock_api_client.get(const.URL_API, headers={
- const.HTTP_HEADER_HA_AUTH: 'wrongpassword'
+ resp = yield from client.get('/', params={
+ 'api_password': 'wrong-password'
})
assert resp.status == 401
@asyncio.coroutine
-def test_access_denied_with_x_forwarded_for(hass, mock_api_client,
- mock_trusted_networks):
- """Test access denied through the X-Forwarded-For http header."""
- hass.http.use_x_forwarded_for = True
- for remote_addr in UNTRUSTED_ADDRESSES:
- resp = yield from mock_api_client.get(const.URL_API, headers={
- HTTP_HEADER_X_FORWARDED_FOR: remote_addr})
-
- assert resp.status == 401, \
- "{} shouldn't be trusted".format(remote_addr)
-
-
-@asyncio.coroutine
-def test_access_denied_with_untrusted_ip(mock_api_client,
- mock_trusted_networks):
- """Test access with an untrusted ip address."""
- for remote_addr in UNTRUSTED_ADDRESSES:
- with patch('homeassistant.components.http.'
- 'util.get_real_ip',
- return_value=ip_address(remote_addr)):
- resp = yield from mock_api_client.get(
- const.URL_API, params={'api_password': ''})
-
- assert resp.status == 401, \
- "{} shouldn't be trusted".format(remote_addr)
-
-
-@asyncio.coroutine
-def test_access_with_password_in_header(mock_api_client, caplog):
- """Test access with password in URL."""
- # Hide logging from requests package that we use to test logging
- req = yield from mock_api_client.get(
- const.URL_API, headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD})
-
- assert req.status == 200
-
- logs = caplog.text
-
- assert const.URL_API in logs
- assert API_PASSWORD not in logs
-
-
-@asyncio.coroutine
-def test_access_denied_with_wrong_password_in_url(mock_api_client):
- """Test access with wrong password."""
- resp = yield from mock_api_client.get(
- const.URL_API, params={'api_password': 'wrongpassword'})
-
- assert resp.status == 401
-
-
-@asyncio.coroutine
-def test_access_with_password_in_url(mock_api_client, caplog):
- """Test access with password in URL."""
- req = yield from mock_api_client.get(
- const.URL_API, params={'api_password': API_PASSWORD})
-
- assert req.status == 200
-
- logs = caplog.text
-
- assert const.URL_API in logs
- assert API_PASSWORD not in logs
-
-
-@asyncio.coroutine
-def test_access_granted_with_x_forwarded_for(hass, mock_api_client, caplog,
- mock_trusted_networks):
- """Test access denied through the X-Forwarded-For http header."""
- hass.http.app[KEY_USE_X_FORWARDED_FOR] = True
- for remote_addr in TRUSTED_ADDRESSES:
- resp = yield from mock_api_client.get(const.URL_API, headers={
- HTTP_HEADER_X_FORWARDED_FOR: remote_addr})
-
- assert resp.status == 200, \
- "{} should be trusted".format(remote_addr)
-
-
-@asyncio.coroutine
-def test_access_granted_with_trusted_ip(mock_api_client, caplog,
- mock_trusted_networks):
- """Test access with trusted addresses."""
- for remote_addr in TRUSTED_ADDRESSES:
- with patch('homeassistant.components.http.'
- 'auth.get_real_ip',
- return_value=ip_address(remote_addr)):
- resp = yield from mock_api_client.get(
- const.URL_API, params={'api_password': ''})
-
- assert resp.status == 200, \
- '{} should be trusted'.format(remote_addr)
-
-
-@asyncio.coroutine
-def test_basic_auth_works(mock_api_client, caplog):
+def test_basic_auth_works(app, test_client):
"""Test access with basic authentication."""
- req = yield from mock_api_client.get(
- const.URL_API,
- auth=aiohttp.BasicAuth('homeassistant', API_PASSWORD))
+ setup_auth(app, [], API_PASSWORD)
+ client = yield from test_client(app)
+ req = yield from client.get(
+ '/',
+ auth=BasicAuth('homeassistant', API_PASSWORD))
assert req.status == 200
- assert const.URL_API in caplog.text
-
-
-@asyncio.coroutine
-def test_basic_auth_username_homeassistant(mock_api_client, caplog):
- """Test access with basic auth requires username homeassistant."""
- req = yield from mock_api_client.get(
- const.URL_API,
- auth=aiohttp.BasicAuth('wrong_username', API_PASSWORD))
+ req = yield from client.get(
+ '/',
+ auth=BasicAuth('wrong_username', API_PASSWORD))
assert req.status == 401
-
-@asyncio.coroutine
-def test_basic_auth_wrong_password(mock_api_client, caplog):
- """Test access with basic auth not allowed with wrong password."""
- req = yield from mock_api_client.get(
- const.URL_API,
- auth=aiohttp.BasicAuth('homeassistant', 'wrong password'))
-
+ req = yield from client.get(
+ '/',
+ auth=BasicAuth('homeassistant', 'wrong password'))
assert req.status == 401
-
-@asyncio.coroutine
-def test_authorization_header_must_be_basic_type(mock_api_client, caplog):
- """Test only basic authorization is allowed for auth header."""
- req = yield from mock_api_client.get(
- const.URL_API,
+ req = yield from client.get(
+ '/',
headers={
'authorization': 'NotBasic abcdefg'
})
-
assert req.status == 401
+
+
+@asyncio.coroutine
+def test_access_with_trusted_ip(test_client):
+ """Test access with an untrusted ip address."""
+ app = web.Application()
+ app.router.add_get('/', mock_handler)
+
+ setup_auth(app, TRUSTED_NETWORKS, 'some-pass')
+
+ set_mock_ip = mock_real_ip(app)
+ client = yield from test_client(app)
+
+ for remote_addr in UNTRUSTED_ADDRESSES:
+ set_mock_ip(remote_addr)
+ resp = yield from client.get('/')
+ assert resp.status == 401, \
+ "{} shouldn't be trusted".format(remote_addr)
+
+ for remote_addr in TRUSTED_ADDRESSES:
+ set_mock_ip(remote_addr)
+ resp = yield from client.get('/')
+ assert resp.status == 200, \
+ "{} should be trusted".format(remote_addr)
diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py
index c9147367c10..bd6df4f4e73 100644
--- a/tests/components/http/test_ban.py
+++ b/tests/components/http/test_ban.py
@@ -1,91 +1,96 @@
"""The tests for the Home Assistant HTTP component."""
# pylint: disable=protected-access
import asyncio
-from ipaddress import ip_address
from unittest.mock import patch, mock_open
-import pytest
+from aiohttp import web
+from aiohttp.web_exceptions import HTTPUnauthorized
-from homeassistant import const
from homeassistant.setup import async_setup_component
import homeassistant.components.http as http
-from homeassistant.components.http.const import (
- KEY_BANS_ENABLED, KEY_LOGIN_THRESHOLD, KEY_BANNED_IPS)
-from homeassistant.components.http.ban import IpBan, IP_BANS_FILE
+from homeassistant.components.http.ban import (
+ IpBan, IP_BANS_FILE, setup_bans, KEY_BANNED_IPS)
+
+from . import mock_real_ip
-API_PASSWORD = 'test1234'
BANNED_IPS = ['200.201.202.203', '100.64.0.2']
-@pytest.fixture
-def mock_api_client(hass, test_client):
- """Start the Hass HTTP component."""
- hass.loop.run_until_complete(async_setup_component(hass, 'api', {
- 'http': {
- http.CONF_API_PASSWORD: API_PASSWORD,
- }
- }))
- hass.http.app[KEY_BANNED_IPS] = [IpBan(banned_ip) for banned_ip
- in BANNED_IPS]
- return hass.loop.run_until_complete(test_client(hass.http.app))
-
-
@asyncio.coroutine
-def test_access_from_banned_ip(hass, mock_api_client):
+def test_access_from_banned_ip(hass, test_client):
"""Test accessing to server from banned IP. Both trusted and not."""
- hass.http.app[KEY_BANS_ENABLED] = True
+ app = web.Application()
+ setup_bans(hass, app, 5)
+ set_real_ip = mock_real_ip(app)
+
+ with patch('homeassistant.components.http.ban.load_ip_bans_config',
+ return_value=[IpBan(banned_ip) for banned_ip
+ in BANNED_IPS]):
+ client = yield from test_client(app)
+
for remote_addr in BANNED_IPS:
- with patch('homeassistant.components.http.'
- 'ban.get_real_ip',
- return_value=ip_address(remote_addr)):
- resp = yield from mock_api_client.get(
- const.URL_API)
- assert resp.status == 403
+ set_real_ip(remote_addr)
+ resp = yield from client.get('/')
+ assert resp.status == 403
@asyncio.coroutine
-def test_access_from_banned_ip_when_ban_is_off(hass, mock_api_client):
+def test_ban_middleware_not_loaded_by_config(hass):
"""Test accessing to server from banned IP when feature is off."""
- hass.http.app[KEY_BANS_ENABLED] = False
- for remote_addr in BANNED_IPS:
- with patch('homeassistant.components.http.'
- 'ban.get_real_ip',
- return_value=ip_address(remote_addr)):
- resp = yield from mock_api_client.get(
- const.URL_API,
- headers={const.HTTP_HEADER_HA_AUTH: API_PASSWORD})
- assert resp.status == 200
+ with patch('homeassistant.components.http.setup_bans') as mock_setup:
+ yield from async_setup_component(hass, 'http', {
+ 'http': {
+ http.CONF_IP_BAN_ENABLED: False,
+ }
+ })
+
+ assert len(mock_setup.mock_calls) == 0
@asyncio.coroutine
-def test_ip_bans_file_creation(hass, mock_api_client):
+def test_ban_middleware_loaded_by_default(hass):
+ """Test accessing to server from banned IP when feature is off."""
+ with patch('homeassistant.components.http.setup_bans') as mock_setup:
+ yield from async_setup_component(hass, 'http', {
+ 'http': {}
+ })
+
+ assert len(mock_setup.mock_calls) == 1
+
+
+@asyncio.coroutine
+def test_ip_bans_file_creation(hass, test_client):
"""Testing if banned IP file created."""
- hass.http.app[KEY_BANS_ENABLED] = True
- hass.http.app[KEY_LOGIN_THRESHOLD] = 1
+ app = web.Application()
+ app['hass'] = hass
+
+ @asyncio.coroutine
+ def unauth_handler(request):
+ """Return a mock web response."""
+ raise HTTPUnauthorized
+
+ app.router.add_get('/', unauth_handler)
+ setup_bans(hass, app, 1)
+ mock_real_ip(app)("200.201.202.204")
+
+ with patch('homeassistant.components.http.ban.load_ip_bans_config',
+ return_value=[IpBan(banned_ip) for banned_ip
+ in BANNED_IPS]):
+ client = yield from test_client(app)
m = mock_open()
- @asyncio.coroutine
- def call_server():
- with patch('homeassistant.components.http.'
- 'ban.get_real_ip',
- return_value=ip_address("200.201.202.204")):
- resp = yield from mock_api_client.get(
- const.URL_API,
- headers={const.HTTP_HEADER_HA_AUTH: 'Wrong password'})
- return resp
-
with patch('homeassistant.components.http.ban.open', m, create=True):
- resp = yield from call_server()
+ resp = yield from client.get('/')
assert resp.status == 401
- assert len(hass.http.app[KEY_BANNED_IPS]) == len(BANNED_IPS)
+ assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS)
assert m.call_count == 0
- resp = yield from call_server()
+ resp = yield from client.get('/')
assert resp.status == 401
- assert len(hass.http.app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1
+ assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1
m.assert_called_once_with(hass.config.path(IP_BANS_FILE), 'a')
- resp = yield from call_server()
+ resp = yield from client.get('/')
assert resp.status == 403
assert m.call_count == 1
diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py
new file mode 100644
index 00000000000..22b70e1c0c5
--- /dev/null
+++ b/tests/components/http/test_cors.py
@@ -0,0 +1,104 @@
+"""Test cors for the HTTP component."""
+import asyncio
+from unittest.mock import patch
+
+from aiohttp import web
+from aiohttp.hdrs import (
+ ACCESS_CONTROL_ALLOW_ORIGIN,
+ ACCESS_CONTROL_ALLOW_HEADERS,
+ ACCESS_CONTROL_REQUEST_HEADERS,
+ ACCESS_CONTROL_REQUEST_METHOD,
+ ORIGIN
+)
+import pytest
+
+from homeassistant.const import HTTP_HEADER_HA_AUTH
+from homeassistant.setup import async_setup_component
+from homeassistant.components.http.cors import setup_cors
+
+
+TRUSTED_ORIGIN = 'https://home-assistant.io'
+
+
+@asyncio.coroutine
+def test_cors_middleware_not_loaded_by_default(hass):
+ """Test accessing to server from banned IP when feature is off."""
+ with patch('homeassistant.components.http.setup_cors') as mock_setup:
+ yield from async_setup_component(hass, 'http', {
+ 'http': {}
+ })
+
+ assert len(mock_setup.mock_calls) == 0
+
+
+@asyncio.coroutine
+def test_cors_middleware_loaded_from_config(hass):
+ """Test accessing to server from banned IP when feature is off."""
+ with patch('homeassistant.components.http.setup_cors') as mock_setup:
+ yield from async_setup_component(hass, 'http', {
+ 'http': {
+ 'cors_allowed_origins': ['http://home-assistant.io']
+ }
+ })
+
+ assert len(mock_setup.mock_calls) == 1
+
+
+@asyncio.coroutine
+def mock_handler(request):
+ """Return if request was authenticated."""
+ return web.Response(status=200)
+
+
+@pytest.fixture
+def client(loop, test_client):
+ """Fixture to setup a web.Application."""
+ app = web.Application()
+ app.router.add_get('/', mock_handler)
+ setup_cors(app, [TRUSTED_ORIGIN])
+ return loop.run_until_complete(test_client(app))
+
+
+@asyncio.coroutine
+def test_cors_requests(client):
+ """Test cross origin requests."""
+ req = yield from client.get('/', headers={
+ ORIGIN: TRUSTED_ORIGIN
+ })
+ assert req.status == 200
+ assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == \
+ TRUSTED_ORIGIN
+
+ # With password in URL
+ req = yield from client.get('/', params={
+ 'api_password': 'some-pass'
+ }, headers={
+ ORIGIN: TRUSTED_ORIGIN
+ })
+ assert req.status == 200
+ assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == \
+ TRUSTED_ORIGIN
+
+ # With password in headers
+ req = yield from client.get('/', headers={
+ HTTP_HEADER_HA_AUTH: 'some-pass',
+ ORIGIN: TRUSTED_ORIGIN
+ })
+ assert req.status == 200
+ assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == \
+ TRUSTED_ORIGIN
+
+
+@asyncio.coroutine
+def test_cors_preflight_allowed(client):
+ """Test cross origin resource sharing preflight (OPTIONS) request."""
+ req = yield from client.options('/', headers={
+ ORIGIN: TRUSTED_ORIGIN,
+ ACCESS_CONTROL_REQUEST_METHOD: 'GET',
+ ACCESS_CONTROL_REQUEST_HEADERS: 'x-ha-access'
+ })
+
+ assert req.status == 200
+ assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN
+ assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == \
+ HTTP_HEADER_HA_AUTH.upper()
diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py
new file mode 100644
index 00000000000..f00be4fc6f9
--- /dev/null
+++ b/tests/components/http/test_data_validator.py
@@ -0,0 +1,77 @@
+"""Test data validator decorator."""
+import asyncio
+from unittest.mock import Mock
+
+from aiohttp import web
+import voluptuous as vol
+
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.components.http.data_validator import RequestDataValidator
+
+
+@asyncio.coroutine
+def get_client(test_client, validator):
+ """Generate a client that hits a view decorated with validator."""
+ app = web.Application()
+ app['hass'] = Mock(is_running=True)
+
+ class TestView(HomeAssistantView):
+ url = '/'
+ name = 'test'
+ requires_auth = False
+
+ @asyncio.coroutine
+ @validator
+ def post(self, request, data):
+ """Test method."""
+ return b''
+
+ TestView().register(app.router)
+ client = yield from test_client(app)
+ return client
+
+
+@asyncio.coroutine
+def test_validator(test_client):
+ """Test the validator."""
+ client = yield from get_client(
+ test_client, RequestDataValidator(vol.Schema({
+ vol.Required('test'): str
+ })))
+
+ resp = yield from client.post('/', json={
+ 'test': 'bla'
+ })
+ assert resp.status == 200
+
+ resp = yield from client.post('/', json={
+ 'test': 100
+ })
+ assert resp.status == 400
+
+ resp = yield from client.post('/')
+ assert resp.status == 400
+
+
+@asyncio.coroutine
+def test_validator_allow_empty(test_client):
+ """Test the validator with empty data."""
+ client = yield from get_client(
+ test_client, RequestDataValidator(vol.Schema({
+ # Although we allow empty, our schema should still be able
+ # to validate an empty dict.
+ vol.Optional('test'): str
+ }), allow_empty=True))
+
+ resp = yield from client.post('/', json={
+ 'test': 'bla'
+ })
+ assert resp.status == 200
+
+ resp = yield from client.post('/', json={
+ 'test': 100
+ })
+ assert resp.status == 400
+
+ resp = yield from client.post('/')
+ assert resp.status == 200
diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py
index 4ff87efd137..ab06b48043e 100644
--- a/tests/components/http/test_init.py
+++ b/tests/components/http/test_init.py
@@ -1,124 +1,10 @@
"""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 tests.common import get_test_instance_port, get_test_home_assistant
+from homeassistant.setup import async_setup_component
-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,
- CONTENT_TYPE: const.CONTENT_TYPE_JSON,
-}
-CORS_ORIGINS = [HTTP_BASE_URL, HTTP_BASE]
-
-hass = None
-
-
-def _url(path=''):
- """Helper method to generate URLs."""
- return HTTP_BASE_URL + path
-
-
-# pylint: disable=invalid-name
-def setUpModule():
- """Initialize a Home Assistant server."""
- global hass
-
- hass = get_test_home_assistant()
-
- setup.setup_component(
- hass, http.DOMAIN, {
- http.DOMAIN: {
- http.CONF_API_PASSWORD: API_PASSWORD,
- http.CONF_SERVER_PORT: SERVER_PORT,
- http.CONF_CORS_ORIGINS: CORS_ORIGINS,
- }
- }
- )
-
- setup.setup_component(hass, 'api')
-
- # Registering static path as it caused CORS to blow up
- hass.http.register_static_path(
- '/custom_components', hass.config.path('custom_components'))
-
- hass.start()
-
-
-# pylint: disable=invalid-name
-def tearDownModule():
- """Stop the Home Assistant server."""
- hass.stop()
-
-
-class TestCors:
- """Test HTTP component."""
-
- def test_cors_allowed_with_password_in_url(self):
- """Test cross origin resource sharing with password in url."""
- req = requests.get(_url(const.URL_API),
- params={'api_password': API_PASSWORD},
- headers={ORIGIN: HTTP_BASE_URL})
-
- allow_origin = ACCESS_CONTROL_ALLOW_ORIGIN
-
- assert req.status_code == 200
- assert req.headers.get(allow_origin) == HTTP_BASE_URL
-
- def test_cors_allowed_with_password_in_header(self):
- """Test cross origin resource sharing with password in header."""
- headers = {
- const.HTTP_HEADER_HA_AUTH: API_PASSWORD,
- ORIGIN: HTTP_BASE_URL
- }
- req = requests.get(_url(const.URL_API), headers=headers)
-
- allow_origin = ACCESS_CONTROL_ALLOW_ORIGIN
-
- assert req.status_code == 200
- assert req.headers.get(allow_origin) == HTTP_BASE_URL
-
- def test_cors_denied_without_origin_header(self):
- """Test cross origin resource sharing with password in header."""
- headers = {
- const.HTTP_HEADER_HA_AUTH: API_PASSWORD
- }
- req = requests.get(_url(const.URL_API), headers=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
- assert allow_headers not in req.headers
-
- def test_cors_preflight_allowed(self):
- """Test cross origin resource sharing preflight (OPTIONS) request."""
- headers = {
- 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 = ACCESS_CONTROL_ALLOW_ORIGIN
- allow_headers = ACCESS_CONTROL_ALLOW_HEADERS
-
- assert req.status_code == 200
- assert req.headers.get(allow_origin) == HTTP_BASE_URL
- assert req.headers.get(allow_headers) == \
- const.HTTP_HEADER_HA_AUTH.upper()
-
class TestView(http.HomeAssistantView):
"""Test the HTTP views."""
@@ -133,12 +19,12 @@ class TestView(http.HomeAssistantView):
@asyncio.coroutine
-def test_registering_view_while_running(hass, test_client):
+def test_registering_view_while_running(hass, test_client, unused_port):
"""Test that we can register a view while the server is running."""
- yield from setup.async_setup_component(
+ yield from async_setup_component(
hass, http.DOMAIN, {
http.DOMAIN: {
- http.CONF_SERVER_PORT: get_test_instance_port(),
+ http.CONF_SERVER_PORT: unused_port(),
}
}
)
@@ -151,7 +37,7 @@ def test_registering_view_while_running(hass, test_client):
@asyncio.coroutine
def test_api_base_url_with_domain(hass):
"""Test setting API URL."""
- result = yield from setup.async_setup_component(hass, 'http', {
+ result = yield from async_setup_component(hass, 'http', {
'http': {
'base_url': 'example.com'
}
@@ -163,7 +49,7 @@ def test_api_base_url_with_domain(hass):
@asyncio.coroutine
def test_api_base_url_with_ip(hass):
"""Test setting api url."""
- result = yield from setup.async_setup_component(hass, 'http', {
+ result = yield from async_setup_component(hass, 'http', {
'http': {
'server_host': '1.1.1.1'
}
@@ -175,7 +61,7 @@ def test_api_base_url_with_ip(hass):
@asyncio.coroutine
def test_api_base_url_with_ip_port(hass):
"""Test setting api url."""
- result = yield from setup.async_setup_component(hass, 'http', {
+ result = yield from async_setup_component(hass, 'http', {
'http': {
'base_url': '1.1.1.1:8124'
}
@@ -187,9 +73,34 @@ def test_api_base_url_with_ip_port(hass):
@asyncio.coroutine
def test_api_no_base_url(hass):
"""Test setting api url."""
- result = yield from setup.async_setup_component(hass, 'http', {
+ result = yield from async_setup_component(hass, 'http', {
'http': {
}
})
assert result
assert hass.config.api.base_url == 'http://127.0.0.1:8123'
+
+
+@asyncio.coroutine
+def test_not_log_password(hass, unused_port, test_client, caplog):
+ """Test access with password doesn't get logged."""
+ result = yield from async_setup_component(hass, 'api', {
+ 'http': {
+ http.CONF_SERVER_PORT: unused_port(),
+ http.CONF_API_PASSWORD: 'some-pass'
+ }
+ })
+ assert result
+
+ client = yield from test_client(hass.http.app)
+
+ resp = yield from client.get('/api/', params={
+ 'api_password': 'some-pass'
+ })
+
+ assert resp.status == 200
+ logs = caplog.text
+
+ # Ensure we don't log API passwords
+ assert '/api/' in logs
+ assert 'some-pass' not in logs
diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py
new file mode 100644
index 00000000000..90201ab4c10
--- /dev/null
+++ b/tests/components/http/test_real_ip.py
@@ -0,0 +1,48 @@
+"""Test real IP middleware."""
+import asyncio
+
+from aiohttp import web
+from aiohttp.hdrs import X_FORWARDED_FOR
+
+from homeassistant.components.http.real_ip import setup_real_ip
+from homeassistant.components.http.const import KEY_REAL_IP
+
+
+@asyncio.coroutine
+def mock_handler(request):
+ """Handler that returns the real IP as text."""
+ return web.Response(text=str(request[KEY_REAL_IP]))
+
+
+@asyncio.coroutine
+def test_ignore_x_forwarded_for(test_client):
+ """Test that we get the IP from the transport."""
+ app = web.Application()
+ app.router.add_get('/', mock_handler)
+ setup_real_ip(app, False)
+
+ mock_api_client = yield from test_client(app)
+
+ resp = yield from mock_api_client.get('/', headers={
+ X_FORWARDED_FOR: '255.255.255.255'
+ })
+ assert resp.status == 200
+ text = yield from resp.text()
+ assert text != '255.255.255.255'
+
+
+@asyncio.coroutine
+def test_use_x_forwarded_for(test_client):
+ """Test that we get the IP from the transport."""
+ app = web.Application()
+ app.router.add_get('/', mock_handler)
+ setup_real_ip(app, True)
+
+ mock_api_client = yield from test_client(app)
+
+ resp = yield from mock_api_client.get('/', headers={
+ X_FORWARDED_FOR: '255.255.255.255'
+ })
+ assert resp.status == 200
+ text = yield from resp.text()
+ assert text == '255.255.255.255'
diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py
index 7ef33aad2d9..6c56564df69 100644
--- a/tests/components/light/test_mqtt.py
+++ b/tests/components/light/test_mqtt.py
@@ -492,16 +492,18 @@ class TestLightMQTT(unittest.TestCase):
light.turn_on(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light_rgb/set', 'on', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'on', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
light.turn_off(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light_rgb/set', 'off', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'off', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_OFF, state.state)
@@ -512,7 +514,7 @@ class TestLightMQTT(unittest.TestCase):
white_value=80)
self.hass.block_till_done()
- self.mock_publish().async_publish.assert_has_calls([
+ self.mock_publish.async_publish.assert_has_calls([
mock.call('test_light_rgb/set', 'on', 2, False),
mock.call('test_light_rgb/rgb/set', '75,75,75', 2, False),
mock.call('test_light_rgb/brightness/set', 50, 2, False),
@@ -550,7 +552,7 @@ class TestLightMQTT(unittest.TestCase):
light.turn_on(self.hass, 'light.test', rgb_color=[255, 128, 64])
self.hass.block_till_done()
- self.mock_publish().async_publish.assert_has_calls([
+ self.mock_publish.async_publish.assert_has_calls([
mock.call('test_light_rgb/set', 'on', 0, False),
mock.call('test_light_rgb/rgb/set', '#ff8040', 0, False),
], any_order=True)
@@ -701,16 +703,17 @@ class TestLightMQTT(unittest.TestCase):
# Should get the following MQTT messages.
# test_light/set: 'ON'
# test_light/bright: 50
- self.assertEqual(('test_light/set', 'ON', 0, False),
- self.mock_publish.mock_calls[-4][1])
- self.assertEqual(('test_light/bright', 50, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_has_calls([
+ mock.call('test_light/set', 'ON', 0, False),
+ mock.call('test_light/bright', 50, 0, False),
+ ], any_order=True)
+ self.mock_publish.async_publish.reset_mock()
light.turn_off(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light/set', 'OFF', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light/set', 'OFF', 0, False)
def test_on_command_last(self):
"""Test on command being sent after brightness."""
@@ -733,16 +736,17 @@ class TestLightMQTT(unittest.TestCase):
# Should get the following MQTT messages.
# test_light/bright: 50
# test_light/set: 'ON'
- self.assertEqual(('test_light/bright', 50, 0, False),
- self.mock_publish.mock_calls[-4][1])
- self.assertEqual(('test_light/set', 'ON', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_has_calls([
+ mock.call('test_light/bright', 50, 0, False),
+ mock.call('test_light/set', 'ON', 0, False),
+ ], any_order=True)
+ self.mock_publish.async_publish.reset_mock()
light.turn_off(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light/set', 'OFF', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light/set', 'OFF', 0, False)
def test_on_command_brightness(self):
"""Test on command being sent as only brightness."""
@@ -767,21 +771,24 @@ class TestLightMQTT(unittest.TestCase):
# Should get the following MQTT messages.
# test_light/bright: 255
- self.assertEqual(('test_light/bright', 255, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light/bright', 255, 0, False)
+ self.mock_publish.async_publish.reset_mock()
light.turn_off(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light/set', 'OFF', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light/set', 'OFF', 0, False)
+ self.mock_publish.async_publish.reset_mock()
# Turn on w/ brightness
light.turn_on(self.hass, 'light.test', brightness=50)
self.hass.block_till_done()
- self.assertEqual(('test_light/bright', 50, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light/bright', 50, 0, False)
+ self.mock_publish.async_publish.reset_mock()
light.turn_off(self.hass, 'light.test')
self.hass.block_till_done()
@@ -791,10 +798,10 @@ class TestLightMQTT(unittest.TestCase):
light.turn_on(self.hass, 'light.test', rgb_color=[75, 75, 75])
self.hass.block_till_done()
- self.assertEqual(('test_light/rgb', '75,75,75', 0, False),
- self.mock_publish.mock_calls[-4][1])
- self.assertEqual(('test_light/bright', 50, 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_has_calls([
+ mock.call('test_light/rgb', '75,75,75', 0, False),
+ mock.call('test_light/bright', 50, 0, False)
+ ], any_order=True)
def test_default_availability_payload(self):
"""Test availability by default payload with defined topic."""
diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py
index a06f8e7d093..ba306a81a34 100644
--- a/tests/components/light/test_mqtt_json.py
+++ b/tests/components/light/test_mqtt_json.py
@@ -293,16 +293,18 @@ class TestLightMQTTJSON(unittest.TestCase):
light.turn_on(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light_rgb/set', '{"state": "ON"}', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', '{"state": "ON"}', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
light.turn_off(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light_rgb/set', '{"state": "OFF"}', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', '{"state": "OFF"}', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_OFF, state.state)
@@ -312,11 +314,14 @@ class TestLightMQTTJSON(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
+ self.mock_publish.async_publish.mock_calls[0][1][0])
+ self.assertEqual(2,
+ self.mock_publish.async_publish.mock_calls[0][1][2])
+ self.assertEqual(False,
+ self.mock_publish.async_publish.mock_calls[0][1][3])
# Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ message_json = json.loads(
+ self.mock_publish.async_publish.mock_calls[0][1][1])
self.assertEqual(50, message_json["brightness"])
self.assertEqual(155, message_json["color_temp"])
self.assertEqual('colorloop', message_json["effect"])
@@ -353,23 +358,30 @@ class TestLightMQTTJSON(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
+ self.mock_publish.async_publish.mock_calls[0][1][0])
+ self.assertEqual(0,
+ self.mock_publish.async_publish.mock_calls[0][1][2])
+ self.assertEqual(False,
+ self.mock_publish.async_publish.mock_calls[0][1][3])
# Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ message_json = json.loads(
+ self.mock_publish.async_publish.mock_calls[0][1][1])
self.assertEqual(5, message_json["flash"])
self.assertEqual("ON", message_json["state"])
+ self.mock_publish.async_publish.reset_mock()
light.turn_on(self.hass, 'light.test', flash="long")
self.hass.block_till_done()
self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
+ self.mock_publish.async_publish.mock_calls[0][1][0])
+ self.assertEqual(0,
+ self.mock_publish.async_publish.mock_calls[0][1][2])
+ self.assertEqual(False,
+ self.mock_publish.async_publish.mock_calls[0][1][3])
# Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ message_json = json.loads(
+ self.mock_publish.async_publish.mock_calls[0][1][1])
self.assertEqual(15, message_json["flash"])
self.assertEqual("ON", message_json["state"])
@@ -393,11 +405,14 @@ class TestLightMQTTJSON(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
+ self.mock_publish.async_publish.mock_calls[0][1][0])
+ self.assertEqual(0,
+ self.mock_publish.async_publish.mock_calls[0][1][2])
+ self.assertEqual(False,
+ self.mock_publish.async_publish.mock_calls[0][1][3])
# Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ message_json = json.loads(
+ self.mock_publish.async_publish.mock_calls[0][1][1])
self.assertEqual(10, message_json["transition"])
self.assertEqual("ON", message_json["state"])
@@ -406,11 +421,14 @@ class TestLightMQTTJSON(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
+ self.mock_publish.async_publish.mock_calls[1][1][0])
+ self.assertEqual(0,
+ self.mock_publish.async_publish.mock_calls[1][1][2])
+ self.assertEqual(False,
+ self.mock_publish.async_publish.mock_calls[1][1][3])
# Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ message_json = json.loads(
+ self.mock_publish.async_publish.mock_calls[1][1][1])
self.assertEqual(10, message_json["transition"])
self.assertEqual("OFF", message_json["state"])
diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py
index 0df9d8136e1..5a01aa15fa2 100644
--- a/tests/components/light/test_mqtt_template.py
+++ b/tests/components/light/test_mqtt_template.py
@@ -232,8 +232,9 @@ class TestLightMQTTTemplate(unittest.TestCase):
light.turn_on(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light_rgb/set', 'on,,,,--', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'on,,,,--', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_ON, state.state)
@@ -241,8 +242,9 @@ class TestLightMQTTTemplate(unittest.TestCase):
light.turn_off(self.hass, 'light.test')
self.hass.block_till_done()
- self.assertEqual(('test_light_rgb/set', 'off', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'off', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('light.test')
self.assertEqual(STATE_OFF, state.state)
@@ -251,22 +253,16 @@ class TestLightMQTTTemplate(unittest.TestCase):
rgb_color=[75, 75, 75])
self.hass.block_till_done()
- self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
-
- # check the payload
- payload = self.mock_publish.mock_calls[-2][1][1]
- self.assertEqual('on,50,,,75-75-75', payload)
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'on,50,,,75-75-75', 2, False)
+ self.mock_publish.async_publish.reset_mock()
# turn on the light with color temp and white val
light.turn_on(self.hass, 'light.test', color_temp=200, white_value=139)
self.hass.block_till_done()
- payload = self.mock_publish.mock_calls[-2][1][1]
- self.assertEqual('on,,200,139,--', payload)
-
- self.assertEqual(2, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'on,,200,139,--', 2, False)
# check the state
state = self.hass.states.get('light.test')
@@ -298,27 +294,16 @@ class TestLightMQTTTemplate(unittest.TestCase):
light.turn_on(self.hass, 'light.test', flash='short')
self.hass.block_till_done()
- self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
-
- # check the payload
- payload = self.mock_publish.mock_calls[-2][1][1]
- self.assertEqual('on,short', payload)
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'on,short', 0, False)
+ self.mock_publish.async_publish.reset_mock()
# long flash
light.turn_on(self.hass, 'light.test', flash='long')
self.hass.block_till_done()
- self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
-
- # check the payload
- payload = self.mock_publish.mock_calls[-2][1][1]
- self.assertEqual('on,long', payload)
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'on,long', 0, False)
def test_transition(self):
"""Test for transition time being sent when included."""
@@ -340,27 +325,16 @@ class TestLightMQTTTemplate(unittest.TestCase):
light.turn_on(self.hass, 'light.test', transition=10)
self.hass.block_till_done()
- self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
-
- # check the payload
- payload = self.mock_publish.mock_calls[-2][1][1]
- self.assertEqual('on,10', payload)
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'on,10', 0, False)
+ self.mock_publish.async_publish.reset_mock()
# transition off
light.turn_off(self.hass, 'light.test', transition=4)
self.hass.block_till_done()
- self.assertEqual('test_light_rgb/set',
- self.mock_publish.mock_calls[-2][1][0])
- self.assertEqual(0, self.mock_publish.mock_calls[-2][1][2])
- self.assertEqual(False, self.mock_publish.mock_calls[-2][1][3])
-
- # check the payload
- payload = self.mock_publish.mock_calls[-2][1][1]
- self.assertEqual('off,4', payload)
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'test_light_rgb/set', 'off,4', 0, False)
def test_invalid_values(self): \
# pylint: disable=invalid-name
diff --git a/tests/components/lock/test_mqtt.py b/tests/components/lock/test_mqtt.py
index 0f4df75d1a2..f87b8f8b74b 100644
--- a/tests/components/lock/test_mqtt.py
+++ b/tests/components/lock/test_mqtt.py
@@ -70,16 +70,17 @@ class TestLockMQTT(unittest.TestCase):
lock.lock(self.hass, 'lock.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'LOCK', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'LOCK', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('lock.test')
self.assertEqual(STATE_LOCKED, state.state)
lock.unlock(self.hass, 'lock.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'UNLOCK', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'UNLOCK', 2, False)
state = self.hass.states.get('lock.test')
self.assertEqual(STATE_UNLOCKED, state.state)
diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py
index 0bcfc9b9a1a..6eeb9136b07 100644
--- a/tests/components/media_player/test_cast.py
+++ b/tests/components/media_player/test_cast.py
@@ -1,12 +1,15 @@
"""The tests for the Cast Media player platform."""
# pylint: disable=protected-access
-import unittest
-from unittest.mock import patch, MagicMock
+import asyncio
+from typing import Optional
+from unittest.mock import patch, MagicMock, Mock
+from uuid import UUID
import pytest
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.media_player import cast
-from tests.common import get_test_home_assistant
@pytest.fixture(autouse=True)
@@ -18,83 +21,221 @@ def cast_mock():
yield
-class FakeChromeCast(object):
- """A fake Chrome Cast."""
-
- def __init__(self, host, port):
- """Initialize the fake Chrome Cast."""
- self.host = host
- self.port = port
+# pylint: disable=invalid-name
+FakeUUID = UUID('57355bce-9364-4aa6-ac1e-eb849dccf9e2')
-class TestCastMediaPlayer(unittest.TestCase):
- """Test the media_player module."""
+def get_fake_chromecast(host='192.168.178.42', port=8009,
+ uuid: Optional[UUID] = FakeUUID):
+ """Generate a Fake Chromecast object with the specified arguments."""
+ return MagicMock(host=host, port=port, uuid=uuid)
- def setUp(self):
- """Setup things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- def tearDown(self):
- """Stop everything that was started."""
- self.hass.stop()
+@asyncio.coroutine
+def async_setup_cast(hass, config=None, discovery_info=None):
+ """Helper to setup the cast platform."""
+ if config is None:
+ config = {}
+ add_devices = Mock()
- @patch('homeassistant.components.media_player.cast.CastDevice')
- @patch('pychromecast.get_chromecasts')
- def test_filter_duplicates(self, mock_get_chromecasts, mock_device):
- """Test filtering of duplicates."""
- mock_get_chromecasts.return_value = [
- FakeChromeCast('some_host', cast.DEFAULT_PORT)
- ]
+ yield from cast.async_setup_platform(hass, config, add_devices,
+ discovery_info=discovery_info)
+ yield from hass.async_block_till_done()
- # Test chromecasts as if they were hardcoded in configuration.yaml
- cast.setup_platform(self.hass, {
- 'host': 'some_host'
- }, lambda _: _)
+ return add_devices
- assert mock_device.called
- mock_device.reset_mock()
- assert not mock_device.called
+@asyncio.coroutine
+def async_setup_cast_internal_discovery(hass, config=None,
+ discovery_info=None,
+ no_from_host_patch=False):
+ """Setup the cast platform and the discovery."""
+ listener = MagicMock(services={})
- # Test chromecasts as if they were automatically discovered
- cast.setup_platform(self.hass, {}, lambda _: _, {
- 'host': 'some_host',
- 'port': cast.DEFAULT_PORT,
- })
- assert not mock_device.called
+ with patch('pychromecast.start_discovery',
+ return_value=(listener, None)) as start_discovery:
+ add_devices = yield from async_setup_cast(hass, config, discovery_info)
+ yield from hass.async_block_till_done()
+ yield from hass.async_block_till_done()
- @patch('homeassistant.components.media_player.cast.CastDevice')
- @patch('pychromecast.get_chromecasts')
- @patch('pychromecast.Chromecast')
- def test_fallback_cast(self, mock_chromecast, mock_get_chromecasts,
- mock_device):
- """Test falling back to creating Chromecast when not discovered."""
- mock_get_chromecasts.return_value = [
- FakeChromeCast('some_host', cast.DEFAULT_PORT)
- ]
+ assert start_discovery.call_count == 1
- # Test chromecasts as if they were hardcoded in configuration.yaml
- cast.setup_platform(self.hass, {
- 'host': 'some_other_host'
- }, lambda _: _)
+ discovery_callback = start_discovery.call_args[0][0]
- assert mock_chromecast.called
- assert mock_device.called
+ def discover_chromecast(service_name, chromecast):
+ """Discover a chromecast device."""
+ listener.services[service_name] = (
+ chromecast.host, chromecast.port, chromecast.uuid, None, None)
+ if no_from_host_patch:
+ discovery_callback(service_name)
+ else:
+ with patch('pychromecast._get_chromecast_from_host',
+ return_value=chromecast):
+ discovery_callback(service_name)
- @patch('homeassistant.components.media_player.cast.CastDevice')
- @patch('pychromecast.get_chromecasts')
- @patch('pychromecast.Chromecast')
- def test_fallback_cast_group(self, mock_chromecast, mock_get_chromecasts,
- mock_device):
- """Test not creating Cast Group when not discovered."""
- mock_get_chromecasts.return_value = [
- FakeChromeCast('some_host', cast.DEFAULT_PORT)
- ]
+ return discover_chromecast, add_devices
- # Test chromecasts as if they were automatically discovered
- cast.setup_platform(self.hass, {}, lambda _: _, {
- 'host': 'some_other_host',
- 'port': 43546,
- })
- assert not mock_chromecast.called
- assert not mock_device.called
+
+@asyncio.coroutine
+def test_start_discovery_called_once(hass):
+ """Test pychromecast.start_discovery called exactly once."""
+ with patch('pychromecast.start_discovery',
+ return_value=(None, None)) as start_discovery:
+ yield from async_setup_cast(hass)
+
+ assert start_discovery.call_count == 1
+
+ yield from async_setup_cast(hass)
+ assert start_discovery.call_count == 1
+
+
+@asyncio.coroutine
+def test_stop_discovery_called_on_stop(hass):
+ """Test pychromecast.stop_discovery called on shutdown."""
+ with patch('pychromecast.start_discovery',
+ return_value=(None, 'the-browser')) as start_discovery:
+ yield from async_setup_cast(hass)
+
+ assert start_discovery.call_count == 1
+
+ with patch('pychromecast.stop_discovery') as stop_discovery:
+ hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
+ yield from hass.async_block_till_done()
+
+ stop_discovery.assert_called_once_with('the-browser')
+
+ with patch('pychromecast.start_discovery',
+ return_value=(None, 'the-browser')) as start_discovery:
+ yield from async_setup_cast(hass)
+
+ assert start_discovery.call_count == 1
+
+
+@asyncio.coroutine
+def test_internal_discovery_callback_only_generates_once(hass):
+ """Test _get_chromecast_from_host only called once per device."""
+ discover_cast, _ = yield from async_setup_cast_internal_discovery(
+ hass, no_from_host_patch=True)
+ chromecast = get_fake_chromecast()
+
+ with patch('pychromecast._get_chromecast_from_host',
+ return_value=chromecast) as gen_chromecast:
+ discover_cast('the-service', chromecast)
+ mdns = (chromecast.host, chromecast.port, chromecast.uuid, None, None)
+ gen_chromecast.assert_called_once_with(mdns, blocking=True)
+
+ discover_cast('the-service', chromecast)
+ gen_chromecast.reset_mock()
+ assert gen_chromecast.call_count == 0
+
+
+@asyncio.coroutine
+def test_internal_discovery_callback_calls_dispatcher(hass):
+ """Test internal discovery calls dispatcher."""
+ discover_cast, _ = yield from async_setup_cast_internal_discovery(hass)
+ chromecast = get_fake_chromecast()
+
+ with patch('pychromecast._get_chromecast_from_host',
+ return_value=chromecast):
+ signal = MagicMock()
+
+ async_dispatcher_connect(hass, 'cast_discovered', signal)
+ discover_cast('the-service', chromecast)
+ yield from hass.async_block_till_done()
+
+ signal.assert_called_once_with(chromecast)
+
+
+@asyncio.coroutine
+def test_internal_discovery_callback_with_connection_error(hass):
+ """Test internal discovery not calling dispatcher on ConnectionError."""
+ import pychromecast # imports mock pychromecast
+
+ pychromecast.ChromecastConnectionError = IOError
+
+ discover_cast, _ = yield from async_setup_cast_internal_discovery(
+ hass, no_from_host_patch=True)
+ chromecast = get_fake_chromecast()
+
+ with patch('pychromecast._get_chromecast_from_host',
+ side_effect=pychromecast.ChromecastConnectionError):
+ signal = MagicMock()
+
+ async_dispatcher_connect(hass, 'cast_discovered', signal)
+ discover_cast('the-service', chromecast)
+ yield from hass.async_block_till_done()
+
+ assert signal.call_count == 0
+
+
+def test_create_cast_device_without_uuid(hass):
+ """Test create a cast device without a UUID."""
+ chromecast = get_fake_chromecast(uuid=None)
+ cast_device = cast._async_create_cast_device(hass, chromecast)
+ assert cast_device is not None
+
+
+def test_create_cast_device_with_uuid(hass):
+ """Test create cast devices with UUID."""
+ added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = {}
+ chromecast = get_fake_chromecast()
+ cast_device = cast._async_create_cast_device(hass, chromecast)
+ assert cast_device is not None
+ assert chromecast.uuid in added_casts
+
+ with patch.object(cast_device, 'async_set_chromecast') as mock_set:
+ assert cast._async_create_cast_device(hass, chromecast) is None
+ assert mock_set.call_count == 0
+
+ chromecast = get_fake_chromecast(host='192.168.178.1')
+ assert cast._async_create_cast_device(hass, chromecast) is None
+ assert mock_set.call_count == 1
+ mock_set.assert_called_once_with(chromecast)
+
+
+@asyncio.coroutine
+def test_normal_chromecast_not_starting_discovery(hass):
+ """Test cast platform not starting discovery when not required."""
+ chromecast = get_fake_chromecast()
+
+ with patch('pychromecast.Chromecast', return_value=chromecast):
+ add_devices = yield from async_setup_cast(hass, {'host': 'host1'})
+ assert add_devices.call_count == 1
+
+ # Same entity twice
+ add_devices = yield from async_setup_cast(hass, {'host': 'host1'})
+ assert add_devices.call_count == 0
+
+ hass.data[cast.ADDED_CAST_DEVICES_KEY] = {}
+ add_devices = yield from async_setup_cast(
+ hass, discovery_info={'host': 'host1', 'port': 8009})
+ assert add_devices.call_count == 1
+
+ hass.data[cast.ADDED_CAST_DEVICES_KEY] = {}
+ add_devices = yield from async_setup_cast(
+ hass, discovery_info={'host': 'host1', 'port': 42})
+ assert add_devices.call_count == 0
+
+
+@asyncio.coroutine
+def test_replay_past_chromecasts(hass):
+ """Test cast platform re-playing past chromecasts when adding new one."""
+ cast_group1 = get_fake_chromecast(host='host1', port=42)
+ cast_group2 = get_fake_chromecast(host='host2', port=42, uuid=UUID(
+ '9462202c-e747-4af5-a66b-7dce0e1ebc09'))
+
+ discover_cast, add_dev1 = yield from async_setup_cast_internal_discovery(
+ hass, discovery_info={'host': 'host1', 'port': 42})
+ discover_cast('service2', cast_group2)
+ yield from hass.async_block_till_done()
+ assert add_dev1.call_count == 0
+
+ discover_cast('service1', cast_group1)
+ yield from hass.async_block_till_done()
+ yield from hass.async_block_till_done() # having jobs that add jobs
+ assert add_dev1.call_count == 1
+
+ add_dev2 = yield from async_setup_cast(
+ hass, discovery_info={'host': 'host2', 'port': 42})
+ yield from hass.async_block_till_done()
+ assert add_dev2.call_count == 1
diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py
index d3ebc67931f..f1a0f4a82fc 100644
--- a/tests/components/media_player/test_sonos.py
+++ b/tests/components/media_player/test_sonos.py
@@ -41,6 +41,14 @@ class AvTransportMock():
}
+class MusicLibraryMock():
+ """Mock class for the music_library property on soco.SoCo object."""
+
+ def get_sonos_favorites(self):
+ """Return favorites."""
+ return []
+
+
class SoCoMock():
"""Mock class for the soco.SoCo object."""
@@ -48,6 +56,12 @@ class SoCoMock():
"""Initialize soco object."""
self.ip_address = ip
self.is_visible = True
+ self.volume = 50
+ self.mute = False
+ self.play_mode = 'NORMAL'
+ self.night_mode = False
+ self.dialog_mode = False
+ self.music_library = MusicLibraryMock()
self.avTransport = AvTransportMock()
def get_sonos_favorites(self):
@@ -62,6 +76,7 @@ class SoCoMock():
'zone_icon': 'x-rincon-roomicon:kitchen',
'mac_address': 'B8:E9:37:BO:OC:BA',
'zone_name': 'Kitchen',
+ 'model_name': 'Sonos PLAY:1',
'hardware_version': '1.8.1.2-1'}
def get_current_transport_info(self):
@@ -145,8 +160,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
'host': '192.0.2.1'
})
- self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 1)
- self.assertEqual(self.hass.data[sonos.DATA_SONOS][0].name, 'Kitchen')
+ devices = self.hass.data[sonos.DATA_SONOS].devices
+ self.assertEqual(len(devices), 1)
+ self.assertEqual(devices[0].name, 'Kitchen')
@mock.patch('soco.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@@ -164,7 +180,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
- self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 1)
+ self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1)
self.assertEqual(discover_mock.call_count, 1)
@mock.patch('soco.SoCo', new=SoCoMock)
@@ -184,7 +200,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
- self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 1)
+ self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1)
self.assertEqual(discover_mock.call_count, 1)
self.assertEqual(soco.config.EVENT_ADVERTISE_IP, '192.0.1.1')
@@ -201,8 +217,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
- self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 1)
- self.assertEqual(self.hass.data[sonos.DATA_SONOS][0].name, 'Kitchen')
+ devices = self.hass.data[sonos.DATA_SONOS].devices
+ self.assertEqual(len(devices), 1)
+ self.assertEqual(devices[0].name, 'Kitchen')
@mock.patch('soco.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@@ -217,8 +234,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
- self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 2)
- self.assertEqual(self.hass.data[sonos.DATA_SONOS][0].name, 'Kitchen')
+ devices = self.hass.data[sonos.DATA_SONOS].devices
+ self.assertEqual(len(devices), 2)
+ self.assertEqual(devices[0].name, 'Kitchen')
@mock.patch('soco.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@@ -233,8 +251,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
assert setup_component(self.hass, DOMAIN, config)
- self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 2)
- self.assertEqual(self.hass.data[sonos.DATA_SONOS][0].name, 'Kitchen')
+ devices = self.hass.data[sonos.DATA_SONOS].devices
+ self.assertEqual(len(devices), 2)
+ self.assertEqual(devices[0].name, 'Kitchen')
@mock.patch('soco.SoCo', new=SoCoMock)
@mock.patch.object(soco, 'discover', new=socoDiscoverMock.discover)
@@ -242,58 +261,9 @@ class TestSonosMediaPlayer(unittest.TestCase):
def test_ensure_setup_sonos_discovery(self, *args):
"""Test a single device using the autodiscovery provided by Sonos."""
sonos.setup_platform(self.hass, {}, fake_add_device)
- self.assertEqual(len(self.hass.data[sonos.DATA_SONOS]), 1)
- self.assertEqual(self.hass.data[sonos.DATA_SONOS][0].name, 'Kitchen')
-
- @mock.patch('soco.SoCo', new=SoCoMock)
- @mock.patch('socket.create_connection', side_effect=socket.error())
- @mock.patch.object(SoCoMock, 'join')
- def test_sonos_group_players(self, join_mock, *args):
- """Ensuring soco methods called for sonos_group_players service."""
- sonos.setup_platform(self.hass, {}, fake_add_device, {
- 'host': '192.0.2.1'
- })
- device = self.hass.data[sonos.DATA_SONOS][-1]
- device.hass = self.hass
-
- device_master = mock.MagicMock()
- device_master.entity_id = "media_player.test"
- device_master.soco_device = mock.MagicMock()
- self.hass.data[sonos.DATA_SONOS].append(device_master)
-
- join_mock.return_value = True
- device.join("media_player.test")
- self.assertEqual(join_mock.call_count, 1)
-
- @mock.patch('soco.SoCo', new=SoCoMock)
- @mock.patch('socket.create_connection', side_effect=socket.error())
- @mock.patch.object(SoCoMock, 'unjoin')
- def test_sonos_unjoin(self, unjoinMock, *args):
- """Ensuring soco methods called for sonos_unjoin service."""
- sonos.setup_platform(self.hass, {}, fake_add_device, {
- 'host': '192.0.2.1'
- })
- device = self.hass.data[sonos.DATA_SONOS][-1]
- device.hass = self.hass
-
- unjoinMock.return_value = True
- device.unjoin()
- self.assertEqual(unjoinMock.call_count, 1)
- self.assertEqual(unjoinMock.call_args, mock.call())
-
- @mock.patch('soco.SoCo', new=SoCoMock)
- @mock.patch('socket.create_connection', side_effect=socket.error())
- def test_set_shuffle(self, shuffle_set_mock, *args):
- """Ensuring soco methods called for sonos_snapshot service."""
- sonos.setup_platform(self.hass, {}, fake_add_device, {
- 'host': '192.0.2.1'
- })
- device = self.hass.data[sonos.DATA_SONOS][-1]
- device.hass = self.hass
-
- device.set_shuffle(True)
- self.assertEqual(shuffle_set_mock.call_count, 1)
- self.assertEqual(device._player.play_mode, 'SHUFFLE')
+ devices = self.hass.data[sonos.DATA_SONOS].devices
+ self.assertEqual(len(devices), 1)
+ self.assertEqual(devices[0].name, 'Kitchen')
@mock.patch('soco.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@@ -303,7 +273,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, fake_add_device, {
'host': '192.0.2.1'
})
- device = self.hass.data[sonos.DATA_SONOS][-1]
+ device = self.hass.data[sonos.DATA_SONOS].devices[-1]
device.hass = self.hass
device.set_sleep_timer(30)
@@ -317,7 +287,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, mock.MagicMock(), {
'host': '192.0.2.1'
})
- device = self.hass.data[sonos.DATA_SONOS][-1]
+ device = self.hass.data[sonos.DATA_SONOS].devices[-1]
device.hass = self.hass
device.set_sleep_timer(None)
@@ -331,7 +301,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, fake_add_device, {
'host': '192.0.2.1'
})
- device = self.hass.data[sonos.DATA_SONOS][-1]
+ device = self.hass.data[sonos.DATA_SONOS].devices[-1]
device.hass = self.hass
alarm1 = alarms.Alarm(soco_mock)
alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False,
@@ -361,7 +331,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, fake_add_device, {
'host': '192.0.2.1'
})
- device = self.hass.data[sonos.DATA_SONOS][-1]
+ device = self.hass.data[sonos.DATA_SONOS].devices[-1]
device.hass = self.hass
snapshotMock.return_value = True
@@ -379,7 +349,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
sonos.setup_platform(self.hass, {}, fake_add_device, {
'host': '192.0.2.1'
})
- device = self.hass.data[sonos.DATA_SONOS][-1]
+ device = self.hass.data[sonos.DATA_SONOS].devices[-1]
device.hass = self.hass
restoreMock.return_value = True
@@ -389,21 +359,3 @@ class TestSonosMediaPlayer(unittest.TestCase):
device.restore()
self.assertEqual(restoreMock.call_count, 1)
self.assertEqual(restoreMock.call_args, mock.call(False))
-
- @mock.patch('soco.SoCo', new=SoCoMock)
- @mock.patch('socket.create_connection', side_effect=socket.error())
- def test_sonos_set_option(self, option_mock, *args):
- """Ensuring soco methods called for sonos_set_option service."""
- sonos.setup_platform(self.hass, {}, fake_add_device, {
- 'host': '192.0.2.1'
- })
- device = self.hass.data[sonos.DATA_SONOS][-1]
- device.hass = self.hass
-
- option_mock.return_value = True
- device._snapshot_coordinator = mock.MagicMock()
- device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17')
-
- device.update_option(night_sound=True, speech_enhance=True)
-
- self.assertEqual(option_mock.call_count, 1)
diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py
index d0704aac227..995f7e891f9 100644
--- a/tests/components/mqtt/test_discovery.py
+++ b/tests/components/mqtt/test_discovery.py
@@ -18,7 +18,7 @@ def test_subscribing_config_topic(hass, mqtt_mock):
assert mqtt_mock.async_subscribe.called
call_args = mqtt_mock.async_subscribe.mock_calls[0][1]
assert call_args[0] == discovery_topic + '/#'
- assert call_args[1] == 0
+ assert call_args[2] == 0
@asyncio.coroutine
diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py
index 55ff0e9ff05..24308bc9a7e 100644
--- a/tests/components/mqtt/test_init.py
+++ b/tests/components/mqtt/test_init.py
@@ -1,6 +1,5 @@
"""The tests for the MQTT component."""
import asyncio
-from collections import namedtuple, OrderedDict
import unittest
from unittest import mock
import socket
@@ -9,26 +8,27 @@ import ssl
import voluptuous as vol
from homeassistant.core import callback
-from homeassistant.setup import setup_component, async_setup_component
+from homeassistant.setup import async_setup_component
import homeassistant.components.mqtt as mqtt
-from homeassistant.const import (
- EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, EVENT_HOMEASSISTANT_STOP)
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.const import (EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE,
+ EVENT_HOMEASSISTANT_STOP)
-from tests.common import (
- get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, mock_coro)
+from tests.common import (get_test_home_assistant, mock_coro,
+ mock_mqtt_component,
+ threadsafe_coroutine_factory, fire_mqtt_message,
+ async_fire_mqtt_message)
@asyncio.coroutine
-def mock_mqtt_client(hass, config=None):
+def async_mock_mqtt_client(hass, config=None):
"""Mock the MQTT paho client."""
if config is None:
- config = {
- mqtt.CONF_BROKER: 'mock-broker'
- }
+ config = {mqtt.CONF_BROKER: 'mock-broker'}
with mock.patch('paho.mqtt.client.Client') as mock_client:
- mock_client().connect = lambda *args: 0
+ mock_client().connect.return_value = 0
+ mock_client().subscribe.return_value = (0, 0)
+ mock_client().publish.return_value = (0, 0)
result = yield from async_setup_component(hass, mqtt.DOMAIN, {
mqtt.DOMAIN: config
})
@@ -36,8 +36,11 @@ def mock_mqtt_client(hass, config=None):
return mock_client()
+mock_mqtt_client = threadsafe_coroutine_factory(async_mock_mqtt_client)
+
+
# pylint: disable=invalid-name
-class TestMQTT(unittest.TestCase):
+class TestMQTTComponent(unittest.TestCase):
"""Test the MQTT component."""
def setUp(self): # pylint: disable=invalid-name
@@ -55,12 +58,8 @@ class TestMQTT(unittest.TestCase):
"""Helper for recording calls."""
self.calls.append(args)
- def test_client_starts_on_home_assistant_mqtt_setup(self):
- """Test if client is connect after mqtt init on bootstrap."""
- assert self.hass.data['mqtt'].async_connect.called
-
def test_client_stops_on_home_assistant_start(self):
- """Test if client stops on HA launch."""
+ """Test if client stops on HA stop."""
self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP)
self.hass.block_till_done()
self.assertTrue(self.hass.data['mqtt'].async_disconnect.called)
@@ -131,6 +130,59 @@ class TestMQTT(unittest.TestCase):
self.hass.data['mqtt'].async_publish.call_args[0][2], 2)
self.assertFalse(self.hass.data['mqtt'].async_publish.call_args[0][3])
+ def test_invalid_mqtt_topics(self):
+ """Test invalid topics."""
+ self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'bad+topic')
+ self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad\0one')
+
+
+# pylint: disable=invalid-name
+class TestMQTTCallbacks(unittest.TestCase):
+ """Test the MQTT callbacks."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ mock_mqtt_client(self.hass)
+ self.calls = []
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @callback
+ def record_calls(self, *args):
+ """Helper for recording calls."""
+ self.calls.append(args)
+
+ def test_client_starts_on_home_assistant_mqtt_setup(self):
+ """Test if client is connected after mqtt init on bootstrap."""
+ self.assertEqual(self.hass.data['mqtt']._mqttc.connect.call_count, 1)
+
+ def test_receiving_non_utf8_message_gets_logged(self):
+ """Test receiving a non utf8 encoded message."""
+ mqtt.subscribe(self.hass, 'test-topic', self.record_calls)
+
+ with self.assertLogs(level='WARNING') as test_handle:
+ fire_mqtt_message(self.hass, 'test-topic', b'\x9a')
+
+ self.hass.block_till_done()
+ self.assertIn(
+ "WARNING:homeassistant.components.mqtt:Can't decode payload "
+ "b'\\x9a' on test-topic with encoding utf-8",
+ test_handle.output[0])
+
+ def test_all_subscriptions_run_when_decode_fails(self):
+ """Test all other subscriptions still run when decode fails for one."""
+ mqtt.subscribe(self.hass, 'test-topic', self.record_calls,
+ encoding='ascii')
+ mqtt.subscribe(self.hass, 'test-topic', self.record_calls)
+
+ fire_mqtt_message(self.hass, 'test-topic', '°C')
+
+ self.hass.block_till_done()
+ self.assertEqual(1, len(self.calls))
+
def test_subscribe_topic(self):
"""Test the subscription of a topic."""
unsub = mqtt.subscribe(self.hass, 'test-topic', self.record_calls)
@@ -296,82 +348,6 @@ class TestMQTT(unittest.TestCase):
self.assertEqual(topic, self.calls[0][0])
self.assertEqual(payload, self.calls[0][1])
- def test_subscribe_binary_topic(self):
- """Test the subscription to a binary topic."""
- mqtt.subscribe(self.hass, 'test-topic', self.record_calls,
- 0, None)
-
- fire_mqtt_message(self.hass, 'test-topic', 0x9a)
-
- self.hass.block_till_done()
- self.assertEqual(1, len(self.calls))
- self.assertEqual('test-topic', self.calls[0][0])
- self.assertEqual(0x9a, self.calls[0][1])
-
- def test_receiving_non_utf8_message_gets_logged(self):
- """Test receiving a non utf8 encoded message."""
- mqtt.subscribe(self.hass, 'test-topic', self.record_calls)
-
- with self.assertLogs(level='ERROR') as test_handle:
- fire_mqtt_message(self.hass, 'test-topic', 0x9a)
- self.hass.block_till_done()
- self.assertIn(
- "ERROR:homeassistant.components.mqtt:Illegal payload "
- "encoding utf-8 from MQTT "
- "topic: test-topic, Payload: 154",
- test_handle.output[0])
-
-
-class TestMQTTCallbacks(unittest.TestCase):
- """Test the MQTT callbacks."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Setup things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- with mock.patch('paho.mqtt.client.Client') as client:
- client().connect = lambda *args: 0
- assert setup_component(self.hass, mqtt.DOMAIN, {
- mqtt.DOMAIN: {
- mqtt.CONF_BROKER: 'mock-broker',
- }
- })
-
- def tearDown(self): # pylint: disable=invalid-name
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_receiving_mqtt_message_fires_hass_event(self):
- """Test if receiving triggers an event."""
- calls = []
-
- @callback
- def record(topic, payload, qos):
- """Helper to record calls."""
- data = {
- 'topic': topic,
- 'payload': payload,
- 'qos': qos,
- }
- calls.append(data)
-
- async_dispatcher_connect(
- self.hass, mqtt.SIGNAL_MQTT_MESSAGE_RECEIVED, record)
-
- MQTTMessage = namedtuple('MQTTMessage', ['topic', 'qos', 'payload'])
- message = MQTTMessage('test_topic', 1, 'Hello World!'.encode('utf-8'))
-
- self.hass.data['mqtt']._mqtt_on_message(
- None, {'hass': self.hass}, message)
- self.hass.block_till_done()
-
- self.assertEqual(1, len(calls))
- last_event = calls[0]
- self.assertEqual(bytearray('Hello World!', 'utf-8'),
- last_event['payload'])
- self.assertEqual(message.topic, last_event['topic'])
- self.assertEqual(message.qos, last_event['qos'])
-
def test_mqtt_failed_connection_results_in_disconnect(self):
"""Test if connection failure leads to disconnect."""
for result_code in range(1, 6):
@@ -388,16 +364,11 @@ 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'].subscribed_topics = {
- 'test/topic': 1,
- }
- self.hass.data['mqtt'].wanted_topics = {
- 'test/progress': 0,
- 'test/topic': 2,
- }
- self.hass.data['mqtt'].progress = {
- 1: 'test/progress'
- }
+ self.hass.data['mqtt'].subscriptions = [
+ mqtt.Subscription('test/progress', None, 0),
+ mqtt.Subscription('test/progress', None, 1),
+ mqtt.Subscription('test/topic', None, 2),
+ ]
self.hass.data['mqtt']._mqttc.reconnect.side_effect = [1, 1, 1, 0]
self.hass.data['mqtt']._mqtt_on_disconnect(None, None, 1)
self.assertTrue(self.hass.data['mqtt']._mqttc.reconnect.called)
@@ -406,15 +377,77 @@ class TestMQTTCallbacks(unittest.TestCase):
self.assertEqual([1, 2, 4],
[call[1][0] for call in mock_sleep.mock_calls])
- 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_retained_message_on_subscribe_received(self):
+ """Test every subscriber receives retained message on subscribe."""
+ def side_effect(*args):
+ async_fire_mqtt_message(self.hass, 'test/state', 'online')
+ return 0, 0
- def test_invalid_mqtt_topics(self):
- """Test invalid topics."""
- self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'bad+topic')
- self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad\0one')
+ self.hass.data['mqtt']._mqttc.subscribe.side_effect = side_effect
+
+ calls_a = mock.MagicMock()
+ mqtt.subscribe(self.hass, 'test/state', calls_a)
+ self.hass.block_till_done()
+ self.assertTrue(calls_a.called)
+
+ calls_b = mock.MagicMock()
+ mqtt.subscribe(self.hass, 'test/state', calls_b)
+ self.hass.block_till_done()
+ self.assertTrue(calls_b.called)
+
+ def test_not_calling_unsubscribe_with_active_subscribers(self):
+ """Test not calling unsubscribe() when other subscribers are active."""
+ unsub = mqtt.subscribe(self.hass, 'test/state', None)
+ mqtt.subscribe(self.hass, 'test/state', None)
+ self.hass.block_till_done()
+ self.assertTrue(self.hass.data['mqtt']._mqttc.subscribe.called)
+
+ unsub()
+ self.hass.block_till_done()
+ self.assertFalse(self.hass.data['mqtt']._mqttc.unsubscribe.called)
+
+ def test_restore_subscriptions_on_reconnect(self):
+ """Test subscriptions are restored on reconnect."""
+ mqtt.subscribe(self.hass, 'test/state', None)
+ self.hass.block_till_done()
+ self.assertEqual(self.hass.data['mqtt']._mqttc.subscribe.call_count, 1)
+
+ self.hass.data['mqtt']._mqtt_on_disconnect(None, None, 0)
+ self.hass.data['mqtt']._mqtt_on_connect(None, None, None, 0)
+ self.hass.block_till_done()
+ self.assertEqual(self.hass.data['mqtt']._mqttc.subscribe.call_count, 2)
+
+ def test_restore_all_active_subscriptions_on_reconnect(self):
+ """Test active subscriptions are restored correctly on reconnect."""
+ self.hass.data['mqtt']._mqttc.subscribe.side_effect = (
+ (0, 1), (0, 2), (0, 3), (0, 4)
+ )
+
+ unsub = mqtt.subscribe(self.hass, 'test/state', None, qos=2)
+ mqtt.subscribe(self.hass, 'test/state', None)
+ mqtt.subscribe(self.hass, 'test/state', None, qos=1)
+ self.hass.block_till_done()
+
+ expected = [
+ mock.call('test/state', 2),
+ mock.call('test/state', 0),
+ mock.call('test/state', 1)
+ ]
+ self.assertEqual(self.hass.data['mqtt']._mqttc.subscribe.mock_calls,
+ expected)
+
+ unsub()
+ self.hass.block_till_done()
+ self.assertEqual(self.hass.data['mqtt']._mqttc.unsubscribe.call_count,
+ 0)
+
+ self.hass.data['mqtt']._mqtt_on_disconnect(None, None, 0)
+ self.hass.data['mqtt']._mqtt_on_connect(None, None, None, 0)
+ self.hass.block_till_done()
+
+ expected.append(mock.call('test/state', 1))
+ self.assertEqual(self.hass.data['mqtt']._mqttc.subscribe.mock_calls,
+ expected)
@asyncio.coroutine
@@ -426,7 +459,7 @@ def test_setup_embedded_starts_with_no_config(hass):
return_value=mock_coro(
return_value=(True, client_config))
) as _start:
- yield from mock_mqtt_client(hass, {})
+ yield from async_mock_mqtt_client(hass, {})
assert _start.call_count == 1
@@ -440,7 +473,7 @@ def test_setup_embedded_with_embedded(hass):
return_value=(True, client_config))
) as _start:
_start.return_value = mock_coro(return_value=(True, client_config))
- yield from mock_mqtt_client(hass, {'embedded': None})
+ yield from async_mock_mqtt_client(hass, {'embedded': None})
assert _start.call_count == 1
@@ -544,13 +577,13 @@ def test_setup_with_tls_config_of_v1_under_python36_only_uses_v1(hass):
@asyncio.coroutine
def test_birth_message(hass):
"""Test sending birth message."""
- mqtt_client = yield from mock_mqtt_client(hass, {
+ mqtt_client = yield from async_mock_mqtt_client(hass, {
mqtt.CONF_BROKER: 'mock-broker',
mqtt.CONF_BIRTH_MESSAGE: {mqtt.ATTR_TOPIC: 'birth',
mqtt.ATTR_PAYLOAD: 'birth'}
})
calls = []
- mqtt_client.publish = lambda *args: calls.append(args)
+ mqtt_client.publish.side_effect = lambda *args: calls.append(args)
hass.data['mqtt']._mqtt_on_connect(None, None, 0, 0)
yield from hass.async_block_till_done()
assert calls[-1] == ('birth', 'birth', 0, False)
@@ -559,30 +592,26 @@ def test_birth_message(hass):
@asyncio.coroutine
def test_mqtt_subscribes_topics_on_connect(hass):
"""Test subscription to topic on connect."""
- mqtt_client = yield from mock_mqtt_client(hass)
+ mqtt_client = yield from async_mock_mqtt_client(hass)
- subscribed_topics = OrderedDict()
- subscribed_topics['topic/test'] = 1
- subscribed_topics['home/sensor'] = 2
-
- 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)
- mqtt_client.subscribe.side_effect = ((0, 2), (0, 3))
+ hass.data['mqtt'].subscriptions = [
+ mqtt.Subscription('topic/test', None),
+ mqtt.Subscription('home/sensor', None, 2),
+ mqtt.Subscription('still/pending', None),
+ mqtt.Subscription('still/pending', None, 1),
+ ]
hass.add_job = mock.MagicMock()
hass.data['mqtt']._mqtt_on_connect(None, None, 0, 0)
yield from hass.async_block_till_done()
- assert not mqtt_client.disconnect.called
+ assert mqtt_client.disconnect.call_count == 0
- 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 == {}
+ expected = {
+ 'topic/test': 0,
+ 'home/sensor': 2,
+ 'still/pending': 1
+ }
+ calls = {call[1][1]: call[1][2] for call in hass.add_job.mock_calls}
+ assert calls == expected
diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py
index 7ce9ec00797..9b4c0c69ac6 100644
--- a/tests/components/mqtt/test_server.py
+++ b/tests/components/mqtt/test_server.py
@@ -4,8 +4,7 @@ from unittest.mock import Mock, MagicMock, patch
from homeassistant.setup import setup_component
import homeassistant.components.mqtt as mqtt
-from tests.common import (
- get_test_home_assistant, mock_coro, mock_http_component)
+from tests.common import get_test_home_assistant, mock_coro
class TestMQTT:
@@ -14,7 +13,9 @@ class TestMQTT:
def setup_method(self, method):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
- mock_http_component(self.hass, 'super_secret')
+ setup_component(self.hass, 'http', {
+ 'api_password': 'super_secret'
+ })
def teardown_method(self, method):
"""Stop everything that was started."""
diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py
index 6fb2e6454de..d6c06f77d93 100644
--- a/tests/components/notify/test_html5.py
+++ b/tests/components/notify/test_html5.py
@@ -4,12 +4,10 @@ import json
from unittest.mock import patch, MagicMock, mock_open
from aiohttp.hdrs import AUTHORIZATION
+from homeassistant.setup import async_setup_component
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.util.json import save_json
from homeassistant.components.notify import html5
-from tests.common import mock_http_component_app
-
CONFIG_FILE = 'file.conf'
SUBSCRIPTION_1 = {
@@ -52,6 +50,23 @@ REGISTER_URL = '/api/notify.html5'
PUBLISH_URL = '/api/notify.html5/callback'
+@asyncio.coroutine
+def mock_client(hass, test_client, registrations=None):
+ """Create a test client for HTML5 views."""
+ if registrations is None:
+ registrations = {}
+
+ with patch('homeassistant.components.notify.html5._load_config',
+ return_value=registrations):
+ yield from async_setup_component(hass, 'notify', {
+ 'notify': {
+ 'platform': 'html5'
+ }
+ })
+
+ return (yield from test_client(hass.http.app))
+
+
class TestHtml5Notify(object):
"""Tests for HTML5 notify platform."""
@@ -89,8 +104,6 @@ class TestHtml5Notify(object):
service.send_message('Hello', target=['device', 'non_existing'],
data={'icon': 'beer.png'})
- print(mock_wp.mock_calls)
-
assert len(mock_wp.mock_calls) == 3
# WebPusher constructor
@@ -104,421 +117,224 @@ class TestHtml5Notify(object):
assert payload['body'] == 'Hello'
assert payload['icon'] == 'beer.png'
- @asyncio.coroutine
- def test_registering_new_device_view(self, loop, test_client):
- """Test that the HTML view works."""
- hass = MagicMock()
- expected = {
- 'unnamed device': SUBSCRIPTION_1,
- }
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {})
+@asyncio.coroutine
+def test_registering_new_device_view(hass, test_client):
+ """Test that the HTML view works."""
+ client = yield from mock_client(hass, test_client)
- assert service is not None
-
- assert len(hass.mock_calls) == 3
-
- view = hass.mock_calls[1][1][0]
- assert view.json_path == hass.config.path.return_value
- assert view.registrations == {}
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
+ with patch('homeassistant.components.notify.html5.save_json') as mock_save:
resp = yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_1))
- content = yield from resp.text()
- assert resp.status == 200, content
- assert view.registrations == expected
+ assert resp.status == 200
+ assert len(mock_save.mock_calls) == 1
+ assert mock_save.mock_calls[0][1][1] == {
+ 'unnamed device': SUBSCRIPTION_1,
+ }
- hass.async_add_job.assert_called_with(save_json, CONFIG_FILE, expected)
- @asyncio.coroutine
- def test_registering_new_device_expiration_view(self, loop, test_client):
- """Test that the HTML view works."""
- hass = MagicMock()
- expected = {
- 'unnamed device': SUBSCRIPTION_4,
- }
+@asyncio.coroutine
+def test_registering_new_device_expiration_view(hass, test_client):
+ """Test that the HTML view works."""
+ client = yield from mock_client(hass, test_client)
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {})
-
- assert service is not None
-
- # assert hass.called
- assert len(hass.mock_calls) == 3
-
- view = hass.mock_calls[1][1][0]
- assert view.json_path == hass.config.path.return_value
- assert view.registrations == {}
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
+ with patch('homeassistant.components.notify.html5.save_json') as mock_save:
resp = yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_4))
- content = yield from resp.text()
- assert resp.status == 200, content
- assert view.registrations == expected
+ assert resp.status == 200
+ assert mock_save.mock_calls[0][1][1] == {
+ 'unnamed device': SUBSCRIPTION_4,
+ }
- hass.async_add_job.assert_called_with(save_json, CONFIG_FILE, expected)
- @asyncio.coroutine
- def test_registering_new_device_fails_view(self, loop, test_client):
- """Test subs. are not altered when registering a new device fails."""
- hass = MagicMock()
- expected = {}
-
- hass.config.path.return_value = CONFIG_FILE
- html5.get_service(hass, {})
- view = hass.mock_calls[1][1][0]
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
-
- hass.async_add_job.side_effect = HomeAssistantError()
+@asyncio.coroutine
+def test_registering_new_device_fails_view(hass, test_client):
+ """Test subs. are not altered when registering a new device fails."""
+ registrations = {}
+ client = yield from mock_client(hass, test_client, registrations)
+ with patch('homeassistant.components.notify.html5.save_json',
+ side_effect=HomeAssistantError()):
resp = yield from client.post(REGISTER_URL,
- data=json.dumps(SUBSCRIPTION_1))
+ data=json.dumps(SUBSCRIPTION_4))
- content = yield from resp.text()
- assert resp.status == 500, content
- assert view.registrations == expected
+ assert resp.status == 500
+ assert registrations == {}
- @asyncio.coroutine
- def test_registering_existing_device_view(self, loop, test_client):
- """Test subscription is updated when registering existing device."""
- hass = MagicMock()
- expected = {
- 'unnamed device': SUBSCRIPTION_4,
- }
- hass.config.path.return_value = CONFIG_FILE
- html5.get_service(hass, {})
- view = hass.mock_calls[1][1][0]
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
+@asyncio.coroutine
+def test_registering_existing_device_view(hass, test_client):
+ """Test subscription is updated when registering existing device."""
+ registrations = {}
+ client = yield from mock_client(hass, test_client, registrations)
+ with patch('homeassistant.components.notify.html5.save_json') as mock_save:
yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_1))
resp = yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_4))
- content = yield from resp.text()
- assert resp.status == 200, content
- assert view.registrations == expected
+ assert resp.status == 200
+ assert mock_save.mock_calls[0][1][1] == {
+ 'unnamed device': SUBSCRIPTION_4,
+ }
+ assert registrations == {
+ 'unnamed device': SUBSCRIPTION_4,
+ }
- hass.async_add_job.assert_called_with(save_json, CONFIG_FILE, expected)
- @asyncio.coroutine
- def test_registering_existing_device_fails_view(self, loop, test_client):
- """Test sub. is not updated when registering existing device fails."""
- hass = MagicMock()
- expected = {
- 'unnamed device': SUBSCRIPTION_1,
- }
-
- hass.config.path.return_value = CONFIG_FILE
- html5.get_service(hass, {})
- view = hass.mock_calls[1][1][0]
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
+@asyncio.coroutine
+def test_registering_existing_device_fails_view(hass, test_client):
+ """Test sub. is not updated when registering existing device fails."""
+ registrations = {}
+ client = yield from mock_client(hass, test_client, registrations)
+ with patch('homeassistant.components.notify.html5.save_json') as mock_save:
yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_1))
-
- hass.async_add_job.side_effect = HomeAssistantError()
+ mock_save.side_effect = HomeAssistantError
resp = yield from client.post(REGISTER_URL,
data=json.dumps(SUBSCRIPTION_4))
- content = yield from resp.text()
- assert resp.status == 500, content
- assert view.registrations == expected
+ assert resp.status == 500
+ assert registrations == {
+ 'unnamed device': SUBSCRIPTION_1,
+ }
- @asyncio.coroutine
- def test_registering_new_device_validation(self, loop, test_client):
- """Test various errors when registering a new device."""
- hass = MagicMock()
- m = mock_open()
- with patch(
- 'homeassistant.util.json.open',
- m, create=True
- ):
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {})
+@asyncio.coroutine
+def test_registering_new_device_validation(hass, test_client):
+ """Test various errors when registering a new device."""
+ client = yield from mock_client(hass, test_client)
- assert service is not None
+ resp = yield from client.post(REGISTER_URL, data=json.dumps({
+ 'browser': 'invalid browser',
+ 'subscription': 'sub info',
+ }))
+ assert resp.status == 400
- # assert hass.called
- assert len(hass.mock_calls) == 3
+ resp = yield from client.post(REGISTER_URL, data=json.dumps({
+ 'browser': 'chrome',
+ }))
+ assert resp.status == 400
- view = hass.mock_calls[1][1][0]
+ with patch('homeassistant.components.notify.html5.save_json',
+ return_value=False):
+ resp = yield from client.post(REGISTER_URL, data=json.dumps({
+ 'browser': 'chrome',
+ 'subscription': 'sub info',
+ }))
+ assert resp.status == 400
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
- resp = yield from client.post(REGISTER_URL, data=json.dumps({
- 'browser': 'invalid browser',
- 'subscription': 'sub info',
- }))
- assert resp.status == 400
+@asyncio.coroutine
+def test_unregistering_device_view(hass, test_client):
+ """Test that the HTML unregister view works."""
+ registrations = {
+ 'some device': SUBSCRIPTION_1,
+ 'other device': SUBSCRIPTION_2,
+ }
+ client = yield from mock_client(hass, test_client, registrations)
- resp = yield from client.post(REGISTER_URL, data=json.dumps({
- 'browser': 'chrome',
- }))
- assert resp.status == 400
+ with patch('homeassistant.components.notify.html5.save_json') as mock_save:
+ resp = yield from client.delete(REGISTER_URL, data=json.dumps({
+ 'subscription': SUBSCRIPTION_1['subscription'],
+ }))
- 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({
- 'browser': 'chrome',
- 'subscription': 'sub info',
- }))
+ assert resp.status == 200
+ assert len(mock_save.mock_calls) == 1
+ assert registrations == {
+ 'other device': SUBSCRIPTION_2
+ }
- assert resp.status == 400
- @asyncio.coroutine
- def test_unregistering_device_view(self, loop, test_client):
- """Test that the HTML unregister view works."""
- hass = MagicMock()
+@asyncio.coroutine
+def test_unregister_device_view_handle_unknown_subscription(hass, test_client):
+ """Test that the HTML unregister view handles unknown subscriptions."""
+ registrations = {}
+ client = yield from mock_client(hass, test_client, registrations)
- config = {
- 'some device': SUBSCRIPTION_1,
- 'other device': SUBSCRIPTION_2,
- }
+ with patch('homeassistant.components.notify.html5.save_json') as mock_save:
+ resp = yield from client.delete(REGISTER_URL, data=json.dumps({
+ 'subscription': SUBSCRIPTION_3['subscription']
+ }))
- m = mock_open(read_data=json.dumps(config))
- with patch(
- 'homeassistant.util.json.open',
- m, create=True
- ):
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {})
+ assert resp.status == 200, resp.response
+ assert registrations == {}
+ assert len(mock_save.mock_calls) == 0
- assert service is not None
- # assert hass.called
- assert len(hass.mock_calls) == 3
+@asyncio.coroutine
+def test_unregistering_device_view_handles_save_error(hass, test_client):
+ """Test that the HTML unregister view handles save errors."""
+ registrations = {
+ 'some device': SUBSCRIPTION_1,
+ 'other device': SUBSCRIPTION_2,
+ }
+ client = yield from mock_client(hass, test_client, registrations)
- view = hass.mock_calls[1][1][0]
- assert view.json_path == hass.config.path.return_value
- assert view.registrations == config
+ with patch('homeassistant.components.notify.html5.save_json',
+ side_effect=HomeAssistantError()):
+ resp = yield from client.delete(REGISTER_URL, data=json.dumps({
+ 'subscription': SUBSCRIPTION_1['subscription'],
+ }))
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
+ assert resp.status == 500, resp.response
+ assert registrations == {
+ 'some device': SUBSCRIPTION_1,
+ 'other device': SUBSCRIPTION_2,
+ }
- resp = yield from client.delete(REGISTER_URL, data=json.dumps({
- 'subscription': SUBSCRIPTION_1['subscription'],
- }))
- config.pop('some device')
+@asyncio.coroutine
+def test_callback_view_no_jwt(hass, test_client):
+ """Test that the notification callback view works without JWT."""
+ client = yield from mock_client(hass, test_client)
+ resp = yield from client.post(PUBLISH_URL, data=json.dumps({
+ 'type': 'push',
+ 'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72'
+ }))
- assert resp.status == 200, resp.response
- assert view.registrations == config
+ assert resp.status == 401, resp.response
- hass.async_add_job.assert_called_with(save_json, CONFIG_FILE,
- config)
- @asyncio.coroutine
- def test_unregister_device_view_handle_unknown_subscription(
- self, loop, test_client):
- """Test that the HTML unregister view handles unknown subscriptions."""
- hass = MagicMock()
+@asyncio.coroutine
+def test_callback_view_with_jwt(hass, test_client):
+ """Test that the notification callback view works with JWT."""
+ registrations = {
+ 'device': SUBSCRIPTION_1
+ }
+ client = yield from mock_client(hass, test_client, registrations)
- config = {
- 'some device': SUBSCRIPTION_1,
- 'other device': SUBSCRIPTION_2,
- }
+ with patch('pywebpush.WebPusher') as mock_wp:
+ yield from hass.services.async_call('notify', 'notify', {
+ 'message': 'Hello',
+ 'target': ['device'],
+ 'data': {'icon': 'beer.png'}
+ }, blocking=True)
- m = mock_open(read_data=json.dumps(config))
- with patch(
- 'homeassistant.util.json.open',
- m, create=True
- ):
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {})
+ assert len(mock_wp.mock_calls) == 3
- assert service is not None
+ # WebPusher constructor
+ assert mock_wp.mock_calls[0][1][0] == \
+ SUBSCRIPTION_1['subscription']
+ # Third mock_call checks the status_code of the response.
+ assert mock_wp.mock_calls[2][0] == '().send().status_code.__eq__'
- # assert hass.called
- assert len(hass.mock_calls) == 3
+ # Call to send
+ push_payload = json.loads(mock_wp.mock_calls[1][1][0])
- view = hass.mock_calls[1][1][0]
- assert view.json_path == hass.config.path.return_value
- assert view.registrations == config
+ assert push_payload['body'] == 'Hello'
+ assert push_payload['icon'] == 'beer.png'
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
+ bearer_token = "Bearer {}".format(push_payload['data']['jwt'])
- resp = yield from client.delete(REGISTER_URL, data=json.dumps({
- 'subscription': SUBSCRIPTION_3['subscription']
- }))
+ resp = yield from client.post(PUBLISH_URL, json={
+ 'type': 'push',
+ }, headers={AUTHORIZATION: bearer_token})
- assert resp.status == 200, resp.response
- assert view.registrations == config
-
- hass.async_add_job.assert_not_called()
-
- @asyncio.coroutine
- def test_unregistering_device_view_handles_save_error(
- self, loop, test_client):
- """Test that the HTML unregister view handles save errors."""
- hass = MagicMock()
-
- config = {
- 'some device': SUBSCRIPTION_1,
- 'other device': SUBSCRIPTION_2,
- }
-
- m = mock_open(read_data=json.dumps(config))
- with patch(
- 'homeassistant.util.json.open',
- m, create=True
- ):
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {})
-
- assert service is not None
-
- # assert hass.called
- assert len(hass.mock_calls) == 3
-
- view = hass.mock_calls[1][1][0]
- assert view.json_path == hass.config.path.return_value
- assert view.registrations == config
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
-
- hass.async_add_job.side_effect = HomeAssistantError()
- resp = yield from client.delete(REGISTER_URL, data=json.dumps({
- 'subscription': SUBSCRIPTION_1['subscription'],
- }))
-
- assert resp.status == 500, resp.response
- assert view.registrations == config
-
- @asyncio.coroutine
- def test_callback_view_no_jwt(self, loop, test_client):
- """Test that the notification callback view works without JWT."""
- hass = MagicMock()
-
- m = mock_open()
- with patch(
- 'homeassistant.util.json.open',
- m, create=True
- ):
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {})
-
- assert service is not None
-
- # assert hass.called
- assert len(hass.mock_calls) == 3
-
- view = hass.mock_calls[2][1][0]
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
-
- resp = yield from client.post(PUBLISH_URL, data=json.dumps({
- 'type': 'push',
- 'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72'
- }))
-
- assert resp.status == 401, resp.response
-
- @asyncio.coroutine
- def test_callback_view_with_jwt(self, loop, test_client):
- """Test that the notification callback view works with JWT."""
- hass = MagicMock()
-
- data = {
- 'device': SUBSCRIPTION_1
- }
-
- m = mock_open(read_data=json.dumps(data))
- with patch(
- 'homeassistant.util.json.open',
- m, create=True
- ):
- hass.config.path.return_value = CONFIG_FILE
- service = html5.get_service(hass, {'gcm_sender_id': '100'})
-
- assert service is not None
-
- # assert hass.called
- assert len(hass.mock_calls) == 3
-
- with patch('pywebpush.WebPusher') as mock_wp:
- service.send_message(
- 'Hello', target=['device'], data={'icon': 'beer.png'})
-
- assert len(mock_wp.mock_calls) == 3
-
- # WebPusher constructor
- assert mock_wp.mock_calls[0][1][0] == \
- SUBSCRIPTION_1['subscription']
- # Third mock_call checks the status_code of the response.
- assert mock_wp.mock_calls[2][0] == '().send().status_code.__eq__'
-
- # Call to send
- push_payload = json.loads(mock_wp.mock_calls[1][1][0])
-
- assert push_payload['body'] == 'Hello'
- assert push_payload['icon'] == 'beer.png'
-
- view = hass.mock_calls[2][1][0]
- view.registrations = data
-
- bearer_token = "Bearer {}".format(push_payload['data']['jwt'])
-
- hass.loop = loop
- app = mock_http_component_app(hass)
- view.register(app.router)
- client = yield from test_client(app)
- hass.http.is_banned_ip.return_value = False
-
- resp = yield from client.post(PUBLISH_URL, data=json.dumps({
- 'type': 'push',
- }), headers={AUTHORIZATION: bearer_token})
-
- assert resp.status == 200
- body = yield from resp.json()
- assert body == {"event": "push", "status": "ok"}
+ assert resp.status == 200
+ body = yield from resp.json()
+ assert body == {"event": "push", "status": "ok"}
diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py
index 58b8dc1f839..191c0d6e733 100644
--- a/tests/components/recorder/test_init.py
+++ b/tests/components/recorder/test_init.py
@@ -42,6 +42,7 @@ class TestRecorder(unittest.TestCase):
with session_scope(hass=self.hass) as session:
db_states = list(session.query(States))
assert len(db_states) == 1
+ assert db_states[0].event_id > 0
state = db_states[0].to_native()
assert state == self.hass.states.get(entity_id)
diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py
index c429ee2fbbb..2ae039b6712 100644
--- a/tests/components/recorder/test_purge.py
+++ b/tests/components/recorder/test_purge.py
@@ -16,9 +16,8 @@ class TestRecorderPurge(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name
"""Setup things to be run when tests are started."""
- config = {'purge_keep_days': 4, 'purge_interval': 2}
self.hass = get_test_home_assistant()
- init_recorder_component(self.hass, config)
+ init_recorder_component(self.hass)
self.hass.start()
def tearDown(self): # pylint: disable=invalid-name
@@ -29,14 +28,18 @@ class TestRecorderPurge(unittest.TestCase):
"""Add multiple states to the db for testing."""
now = datetime.now()
five_days_ago = now - timedelta(days=5)
+ eleven_days_ago = now - timedelta(days=11)
attributes = {'test_attr': 5, 'test_attr_10': 'nice'}
self.hass.block_till_done()
self.hass.data[DATA_INSTANCE].block_till_done()
with recorder.session_scope(hass=self.hass) as session:
- for event_id in range(5):
- if event_id < 3:
+ for event_id in range(6):
+ if event_id < 2:
+ timestamp = eleven_days_ago
+ state = 'autopurgeme'
+ elif event_id < 4:
timestamp = five_days_ago
state = 'purgeme'
else:
@@ -65,9 +68,9 @@ class TestRecorderPurge(unittest.TestCase):
domain='sensor',
state='iamprotected',
attributes=json.dumps(attributes),
- last_changed=five_days_ago,
- last_updated=five_days_ago,
- created=five_days_ago,
+ last_changed=eleven_days_ago,
+ last_updated=eleven_days_ago,
+ created=eleven_days_ago,
event_id=protected_event_id
))
@@ -75,14 +78,18 @@ class TestRecorderPurge(unittest.TestCase):
"""Add a few events for testing."""
now = datetime.now()
five_days_ago = now - timedelta(days=5)
+ eleven_days_ago = now - timedelta(days=11)
event_data = {'test_attr': 5, 'test_attr_10': 'nice'}
self.hass.block_till_done()
self.hass.data[DATA_INSTANCE].block_till_done()
with recorder.session_scope(hass=self.hass) as session:
- for event_id in range(5):
+ for event_id in range(6):
if event_id < 2:
+ timestamp = eleven_days_ago
+ event_type = 'EVENT_TEST_AUTOPURGE'
+ elif event_id < 4:
timestamp = five_days_ago
event_type = 'EVENT_TEST_PURGE'
else:
@@ -102,8 +109,8 @@ class TestRecorderPurge(unittest.TestCase):
event_type='EVENT_TEST_FOR_PROTECTED',
event_data=json.dumps(event_data),
origin='LOCAL',
- created=five_days_ago,
- time_fired=five_days_ago,
+ created=eleven_days_ago,
+ time_fired=eleven_days_ago,
)
session.add(protected_event)
session.flush()
@@ -113,13 +120,13 @@ class TestRecorderPurge(unittest.TestCase):
def test_purge_old_states(self):
"""Test deleting old states."""
self._add_test_states()
- # make sure we start with 6 states
+ # make sure we start with 7 states
with session_scope(hass=self.hass) as session:
states = session.query(States)
- self.assertEqual(states.count(), 6)
+ self.assertEqual(states.count(), 7)
# run purge_old_data()
- purge_old_data(self.hass.data[DATA_INSTANCE], 4)
+ purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
# we should only have 3 states left after purging
self.assertEqual(states.count(), 3)
@@ -131,13 +138,13 @@ class TestRecorderPurge(unittest.TestCase):
with session_scope(hass=self.hass) as session:
events = session.query(Events).filter(
Events.event_type.like("EVENT_TEST%"))
- self.assertEqual(events.count(), 6)
+ self.assertEqual(events.count(), 7)
# run purge_old_data()
- purge_old_data(self.hass.data[DATA_INSTANCE], 4)
+ purge_old_data(self.hass.data[DATA_INSTANCE], 4, repack=False)
- # now we should only have 3 events left
- self.assertEqual(events.count(), 3)
+ # no state to protect, now we should only have 2 events left
+ self.assertEqual(events.count(), 2)
def test_purge_method(self):
"""Test purge method."""
@@ -148,24 +155,24 @@ class TestRecorderPurge(unittest.TestCase):
# make sure we start with 6 states
with session_scope(hass=self.hass) as session:
states = session.query(States)
- self.assertEqual(states.count(), 6)
+ self.assertEqual(states.count(), 7)
events = session.query(Events).filter(
Events.event_type.like("EVENT_TEST%"))
- self.assertEqual(events.count(), 6)
+ self.assertEqual(events.count(), 7)
self.hass.data[DATA_INSTANCE].block_till_done()
- # run purge method - no service data, should not work
+ # run purge method - no service data, use defaults
self.hass.services.call('recorder', 'purge')
self.hass.async_block_till_done()
# Small wait for recorder thread
self.hass.data[DATA_INSTANCE].block_till_done()
- # we should still have everything from before
- self.assertEqual(states.count(), 6)
- self.assertEqual(events.count(), 6)
+ # only purged old events
+ self.assertEqual(states.count(), 5)
+ self.assertEqual(events.count(), 5)
# run purge method - correct service data
self.hass.services.call('recorder', 'purge',
@@ -182,11 +189,20 @@ class TestRecorderPurge(unittest.TestCase):
self.assertTrue('iamprotected' in (
state.state for state in states))
- # now we should only have 4 events left
- self.assertEqual(events.count(), 4)
+ # now we should only have 3 events left
+ self.assertEqual(events.count(), 3)
# and the protected event is among them
self.assertTrue('EVENT_TEST_FOR_PROTECTED' in (
event.event_type for event in events.all()))
self.assertFalse('EVENT_TEST_PURGE' in (
event.event_type for event in events.all()))
+
+ # run purge method - correct service data, with repack
+ service_data['repack'] = True
+ self.assertFalse(self.hass.data[DATA_INSTANCE].did_vacuum)
+ self.hass.services.call('recorder', 'purge',
+ service_data=service_data)
+ self.hass.async_block_till_done()
+ self.hass.data[DATA_INSTANCE].block_till_done()
+ self.assertTrue(self.hass.data[DATA_INSTANCE].did_vacuum)
diff --git a/tests/components/sensor/test_command_line.py b/tests/components/sensor/test_command_line.py
index 6eb97b41e11..bc073a04c47 100644
--- a/tests/components/sensor/test_command_line.py
+++ b/tests/components/sensor/test_command_line.py
@@ -3,7 +3,6 @@ import unittest
from homeassistant.helpers.template import Template
from homeassistant.components.sensor import command_line
-from homeassistant import setup
from tests.common import get_test_home_assistant
@@ -40,16 +39,6 @@ class TestCommandSensorSensor(unittest.TestCase):
self.assertEqual('in', entity.unit_of_measurement)
self.assertEqual('5', entity.state)
- def test_setup_bad_config(self):
- """Test setup with a bad configuration."""
- config = {'name': 'test',
- 'platform': 'not_command_line',
- }
-
- self.assertFalse(setup.setup_component(self.hass, 'test', {
- 'command_line': config,
- }))
-
def test_template(self):
"""Test command sensor with template."""
data = command_line.CommandSensorData(self.hass, 'echo 50')
diff --git a/tests/components/sensor/test_filesize.py b/tests/components/sensor/test_filesize.py
new file mode 100644
index 00000000000..23ef1c6081b
--- /dev/null
+++ b/tests/components/sensor/test_filesize.py
@@ -0,0 +1,58 @@
+"""The tests for the filesize sensor."""
+import unittest
+import os
+
+from homeassistant.components.sensor.filesize import CONF_FILE_PATHS
+from homeassistant.setup import setup_component
+from tests.common import get_test_home_assistant
+
+
+TEST_DIR = os.path.join(os.path.dirname(__file__))
+TEST_FILE = os.path.join(TEST_DIR, 'mock_file_test_filesize.txt')
+
+
+def create_file(path):
+ """Create a test file."""
+ with open(path, 'w') as test_file:
+ test_file.write("test")
+
+
+class TestFileSensor(unittest.TestCase):
+ """Test the filesize sensor."""
+
+ def setup_method(self, method):
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.hass.config.whitelist_external_dirs = set((TEST_DIR))
+
+ def teardown_method(self, method):
+ """Stop everything that was started."""
+ if os.path.isfile(TEST_FILE):
+ os.remove(TEST_FILE)
+ self.hass.stop()
+
+ def test_invalid_path(self):
+ """Test that an invalid path is caught."""
+ config = {
+ 'sensor': {
+ 'platform': 'filesize',
+ CONF_FILE_PATHS: ['invalid_path']}
+ }
+ self.assertTrue(
+ setup_component(self.hass, 'sensor', config))
+ assert len(self.hass.states.entity_ids()) == 0
+
+ def test_valid_path(self):
+ """Test for a valid path."""
+ create_file(TEST_FILE)
+ config = {
+ 'sensor': {
+ 'platform': 'filesize',
+ CONF_FILE_PATHS: [TEST_FILE]}
+ }
+ self.assertTrue(
+ setup_component(self.hass, 'sensor', config))
+ assert len(self.hass.states.entity_ids()) == 1
+ state = self.hass.states.get('sensor.mock_file_test_filesizetxt')
+ assert state.state == '0.0'
+ assert state.attributes.get('bytes') == 4
diff --git a/tests/components/sensor/test_folder.py b/tests/components/sensor/test_folder.py
new file mode 100644
index 00000000000..85ae8a688e7
--- /dev/null
+++ b/tests/components/sensor/test_folder.py
@@ -0,0 +1,64 @@
+"""The tests for the folder sensor."""
+import unittest
+import os
+
+from homeassistant.components.sensor.folder import CONF_FOLDER_PATHS
+from homeassistant.setup import setup_component
+from tests.common import get_test_home_assistant
+
+
+CWD = os.path.join(os.path.dirname(__file__))
+TEST_FOLDER = 'test_folder'
+TEST_DIR = os.path.join(CWD, TEST_FOLDER)
+TEST_TXT = 'mock_test_folder.txt'
+TEST_FILE = os.path.join(TEST_DIR, TEST_TXT)
+
+
+def create_file(path):
+ """Create a test file."""
+ with open(path, 'w') as test_file:
+ test_file.write("test")
+
+
+class TestFolderSensor(unittest.TestCase):
+ """Test the filesize sensor."""
+
+ def setup_method(self, method):
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ if not os.path.isdir(TEST_DIR):
+ os.mkdir(TEST_DIR)
+ self.hass.config.whitelist_external_dirs = set((TEST_DIR))
+
+ def teardown_method(self, method):
+ """Stop everything that was started."""
+ if os.path.isfile(TEST_FILE):
+ os.remove(TEST_FILE)
+ os.rmdir(TEST_DIR)
+ self.hass.stop()
+
+ def test_invalid_path(self):
+ """Test that an invalid path is caught."""
+ config = {
+ 'sensor': {
+ 'platform': 'folder',
+ CONF_FOLDER_PATHS: 'invalid_path'}
+ }
+ self.assertTrue(
+ setup_component(self.hass, 'sensor', config))
+ assert len(self.hass.states.entity_ids()) == 0
+
+ def test_valid_path(self):
+ """Test for a valid path."""
+ create_file(TEST_FILE)
+ config = {
+ 'sensor': {
+ 'platform': 'folder',
+ CONF_FOLDER_PATHS: TEST_DIR}
+ }
+ self.assertTrue(
+ setup_component(self.hass, 'sensor', config))
+ assert len(self.hass.states.entity_ids()) == 1
+ state = self.hass.states.get('sensor.test_folder')
+ assert state.state == '0.0'
+ assert state.attributes.get('number_of_files') == 1
diff --git a/tests/components/sensor/test_startca.py b/tests/components/sensor/test_startca.py
new file mode 100644
index 00000000000..95da1c93a0c
--- /dev/null
+++ b/tests/components/sensor/test_startca.py
@@ -0,0 +1,215 @@
+"""Tests for the Start.ca sensor platform."""
+import asyncio
+from homeassistant.bootstrap import async_setup_component
+from homeassistant.components.sensor.startca import StartcaData
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+
+@asyncio.coroutine
+def test_capped_setup(hass, aioclient_mock):
+ """Test the default setup."""
+ config = {'platform': 'startca',
+ 'api_key': 'NOTAKEY',
+ 'total_bandwidth': 400,
+ 'monitored_variables': [
+ 'usage',
+ 'usage_gb',
+ 'limit',
+ 'used_download',
+ 'used_upload',
+ 'used_total',
+ 'grace_download',
+ 'grace_upload',
+ 'grace_total',
+ 'total_download',
+ 'total_upload',
+ 'used_remaining']}
+
+ result = ''\
+ ''\
+ '1.1'\
+ ' '\
+ '304946829777'\
+ '6480700153'\
+ ''\
+ ' '\
+ '304946829777'\
+ '6480700153'\
+ ''\
+ ' '\
+ '304946829777'\
+ '6480700153'\
+ ''\
+ ''
+ aioclient_mock.get('https://www.start.ca/support/usage/api?key='
+ 'NOTAKEY',
+ text=result)
+
+ yield from async_setup_component(hass, 'sensor', {'sensor': config})
+
+ state = hass.states.get('sensor.startca_usage_ratio')
+ assert state.attributes.get('unit_of_measurement') == '%'
+ assert state.state == '76.24'
+
+ state = hass.states.get('sensor.startca_usage')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '304.95'
+
+ state = hass.states.get('sensor.startca_data_limit')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '400'
+
+ state = hass.states.get('sensor.startca_used_download')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '304.95'
+
+ state = hass.states.get('sensor.startca_used_upload')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '6.48'
+
+ state = hass.states.get('sensor.startca_used_total')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '311.43'
+
+ state = hass.states.get('sensor.startca_grace_download')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '304.95'
+
+ state = hass.states.get('sensor.startca_grace_upload')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '6.48'
+
+ state = hass.states.get('sensor.startca_grace_total')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '311.43'
+
+ state = hass.states.get('sensor.startca_total_download')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '304.95'
+
+ state = hass.states.get('sensor.startca_total_upload')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '6.48'
+
+ state = hass.states.get('sensor.startca_remaining')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '95.05'
+
+
+@asyncio.coroutine
+def test_unlimited_setup(hass, aioclient_mock):
+ """Test the default setup."""
+ config = {'platform': 'startca',
+ 'api_key': 'NOTAKEY',
+ 'total_bandwidth': 0,
+ 'monitored_variables': [
+ 'usage',
+ 'usage_gb',
+ 'limit',
+ 'used_download',
+ 'used_upload',
+ 'used_total',
+ 'grace_download',
+ 'grace_upload',
+ 'grace_total',
+ 'total_download',
+ 'total_upload',
+ 'used_remaining']}
+
+ result = ''\
+ ''\
+ '1.1'\
+ ' '\
+ '304946829777'\
+ '6480700153'\
+ ''\
+ ' '\
+ '0'\
+ '0'\
+ ''\
+ ' '\
+ '304946829777'\
+ '6480700153'\
+ ''\
+ ''
+ aioclient_mock.get('https://www.start.ca/support/usage/api?key='
+ 'NOTAKEY',
+ text=result)
+
+ yield from async_setup_component(hass, 'sensor', {'sensor': config})
+
+ state = hass.states.get('sensor.startca_usage_ratio')
+ assert state.attributes.get('unit_of_measurement') == '%'
+ assert state.state == '0'
+
+ state = hass.states.get('sensor.startca_usage')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '0.0'
+
+ state = hass.states.get('sensor.startca_data_limit')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == 'inf'
+
+ state = hass.states.get('sensor.startca_used_download')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '0.0'
+
+ state = hass.states.get('sensor.startca_used_upload')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '0.0'
+
+ state = hass.states.get('sensor.startca_used_total')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '0.0'
+
+ state = hass.states.get('sensor.startca_grace_download')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '304.95'
+
+ state = hass.states.get('sensor.startca_grace_upload')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '6.48'
+
+ state = hass.states.get('sensor.startca_grace_total')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '311.43'
+
+ state = hass.states.get('sensor.startca_total_download')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '304.95'
+
+ state = hass.states.get('sensor.startca_total_upload')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == '6.48'
+
+ state = hass.states.get('sensor.startca_remaining')
+ assert state.attributes.get('unit_of_measurement') == 'GB'
+ assert state.state == 'inf'
+
+
+@asyncio.coroutine
+def test_bad_return_code(hass, aioclient_mock):
+ """Test handling a return code that isn't HTTP OK."""
+ aioclient_mock.get('https://www.start.ca/support/usage/api?key='
+ 'NOTAKEY',
+ status=404)
+
+ scd = StartcaData(hass.loop, async_get_clientsession(hass),
+ 'NOTAKEY', 400)
+
+ result = yield from scd.async_update()
+ assert result is False
+
+
+@asyncio.coroutine
+def test_bad_json_decode(hass, aioclient_mock):
+ """Test decoding invalid json result."""
+ aioclient_mock.get('https://www.start.ca/support/usage/api?key='
+ 'NOTAKEY',
+ text='this is not xml')
+
+ scd = StartcaData(hass.loop, async_get_clientsession(hass),
+ 'NOTAKEY', 400)
+
+ result = yield from scd.async_update()
+ assert result is False
diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py
index 3033b41b142..5e258bc9245 100644
--- a/tests/components/sensor/test_template.py
+++ b/tests/components/sensor/test_template.py
@@ -104,6 +104,33 @@ class TestTemplateSensor:
state = self.hass.states.get('sensor.test_template_sensor')
assert state.attributes['entity_picture'] == '/local/sensor.png'
+ def test_friendly_name_template(self):
+ """Test friendly_name template."""
+ with assert_setup_component(1):
+ assert setup_component(self.hass, 'sensor', {
+ 'sensor': {
+ 'platform': 'template',
+ 'sensors': {
+ 'test_template_sensor': {
+ 'value_template': "State",
+ 'friendly_name_template':
+ "It {{ states.sensor.test_state.state }}."
+ }
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('sensor.test_template_sensor')
+ assert state.attributes.get('friendly_name') == 'It .'
+
+ self.hass.states.set('sensor.test_state', 'Works')
+ self.hass.block_till_done()
+ state = self.hass.states.get('sensor.test_template_sensor')
+ assert state.attributes['friendly_name'] == 'It Works.'
+
def test_template_syntax_error(self):
"""Test templating syntax error."""
with assert_setup_component(0):
diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py
index c1508f49851..27047ba0ad0 100644
--- a/tests/components/sensor/test_wunderground.py
+++ b/tests/components/sensor/test_wunderground.py
@@ -1,13 +1,14 @@
"""The tests for the WUnderground platform."""
-import unittest
+import asyncio
+import aiohttp
+
+from pytest import raises
from homeassistant.components.sensor import wunderground
from homeassistant.const import TEMP_CELSIUS, LENGTH_INCHES, STATE_UNKNOWN
from homeassistant.exceptions import PlatformNotReady
-
-from requests.exceptions import ConnectionError
-
-from tests.common import get_test_home_assistant
+from homeassistant.setup import async_setup_component
+from tests.common import load_fixture, assert_setup_component
VALID_CONFIG_PWS = {
'platform': 'wunderground',
@@ -21,6 +22,7 @@ VALID_CONFIG_PWS = {
VALID_CONFIG = {
'platform': 'wunderground',
'api_key': 'foo',
+ 'lang': 'EN',
'monitored_conditions': [
'weather', 'feelslike_c', 'alerts', 'elevation', 'location',
'weather_1d_metric', 'precip_1d_in'
@@ -37,268 +39,107 @@ INVALID_CONFIG = {
]
}
-FEELS_LIKE = '40'
-WEATHER = 'Clear'
-HTTPS_ICON_URL = 'https://icons.wxug.com/i/c/k/clear.gif'
-ALERT_MESSAGE = 'This is a test alert message'
-ALERT_ICON = 'mdi:alert-circle-outline'
-FORECAST_TEXT = 'Mostly Cloudy. Fog overnight.'
-PRECIP_IN = 0.03
+URL = 'http://api.wunderground.com/api/foo/alerts/conditions/forecast/lang' \
+ ':EN/q/32.87336,-117.22743.json'
+PWS_URL = 'http://api.wunderground.com/api/foo/alerts/conditions/' \
+ 'lang:EN/q/pws:bar.json'
+INVALID_URL = 'http://api.wunderground.com/api/BOB/alerts/conditions/' \
+ 'lang:foo/q/pws:bar.json'
-def mocked_requests_get(*args, **kwargs):
- """Mock requests.get invocations."""
- class MockResponse:
- """Class to represent a mocked response."""
+@asyncio.coroutine
+def test_setup(hass, aioclient_mock):
+ """Test that the component is loaded."""
+ aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json'))
- def __init__(self, json_data, status_code):
- """Initialize the mock response class."""
- self.json_data = json_data
- self.status_code = status_code
-
- def json(self):
- """Return the json of the response."""
- return self.json_data
-
- if str(args[0]).startswith('http://api.wunderground.com/api/foo/'):
- return MockResponse({
- "response": {
- "version": "0.1",
- "termsofService":
- "http://www.wunderground.com/weather/api/d/terms.html",
- "features": {
- "conditions": 1,
- "alerts": 1,
- "forecast": 1,
- }
- }, "current_observation": {
- "image": {
- "url":
- 'http://icons.wxug.com/graphics/wu2/logo_130x80.png',
- "title": "Weather Underground",
- "link": "http://www.wunderground.com"
- },
- "feelslike_c": FEELS_LIKE,
- "weather": WEATHER,
- "icon_url": 'http://icons.wxug.com/i/c/k/clear.gif',
- "display_location": {
- "city": "Holly Springs",
- "country": "US",
- "full": "Holly Springs, NC"
- },
- "observation_location": {
- "elevation": "413 ft",
- "full": "Twin Lake, Holly Springs, North Carolina"
- },
- }, "alerts": [
- {
- "type": 'FLO',
- "description": "Areal Flood Warning",
- "date": "9:36 PM CDT on September 22, 2016",
- "expires": "10:00 AM CDT on September 23, 2016",
- "message": ALERT_MESSAGE,
- },
-
- ], "forecast": {
- "txt_forecast": {
- "date": "22:35 CEST",
- "forecastday": [
- {
- "period": 0,
- "icon_url":
- "http://icons.wxug.com/i/c/k/clear.gif",
- "title": "Tuesday",
- "fcttext": FORECAST_TEXT,
- "fcttext_metric": FORECAST_TEXT,
- "pop": "0"
- },
- ],
- }, "simpleforecast": {
- "forecastday": [
- {
- "date": {
- "pretty": "19:00 CEST 4. Duben 2017",
- },
- "period": 1,
- "high": {
- "fahrenheit": "56",
- "celsius": "13",
- },
- "low": {
- "fahrenheit": "43",
- "celsius": "6",
- },
- "conditions": "Možnost deště",
- "icon_url":
- "http://icons.wxug.com/i/c/k/chancerain.gif",
- "qpf_allday": {
- "in": PRECIP_IN,
- "mm": 1,
- },
- "maxwind": {
- "mph": 0,
- "kph": 0,
- "dir": "",
- "degrees": 0,
- },
- "avewind": {
- "mph": 0,
- "kph": 0,
- "dir": "severnÃ",
- "degrees": 0
- }
- },
- ],
- },
- },
- }, 200)
- else:
- return MockResponse({
- "response": {
- "version": "0.1",
- "termsofService":
- "http://www.wunderground.com/weather/api/d/terms.html",
- "features": {},
- "error": {
- "type": "keynotfound",
- "description": "this key does not exist"
- }
- }
- }, 200)
+ with assert_setup_component(1, 'sensor'):
+ yield from async_setup_component(hass, 'sensor',
+ {'sensor': VALID_CONFIG})
-def mocked_requests_get_invalid(*args, **kwargs):
- """Mock requests.get invocations invalid data."""
- class MockResponse:
- """Class to represent a mocked response."""
+@asyncio.coroutine
+def test_setup_pws(hass, aioclient_mock):
+ """Test that the component is loaded with PWS id."""
+ aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json'))
- def __init__(self, json_data, status_code):
- """Initialize the mock response class."""
- self.json_data = json_data
- self.status_code = status_code
-
- def json(self):
- """Return the json of the response."""
- return self.json_data
-
- return MockResponse({
- "response": {
- "version": "0.1",
- "termsofService":
- "http://www.wunderground.com/weather/api/d/terms.html",
- "features": {
- "conditions": 1,
- "alerts": 1,
- "forecast": 1,
- }
- }, "current_observation": {
- "image": {
- "url":
- 'http://icons.wxug.com/graphics/wu2/logo_130x80.png',
- "title": "Weather Underground",
- "link": "http://www.wunderground.com"
- },
- },
- }, 200)
+ with assert_setup_component(1, 'sensor'):
+ yield from async_setup_component(hass, 'sensor',
+ {'sensor': VALID_CONFIG_PWS})
-class TestWundergroundSetup(unittest.TestCase):
- """Test the WUnderground platform."""
+@asyncio.coroutine
+def test_setup_invalid(hass, aioclient_mock):
+ """Test that the component is not loaded with invalid config."""
+ aioclient_mock.get(INVALID_URL,
+ text=load_fixture('wunderground-error.json'))
- # pylint: disable=invalid-name
- DEVICES = []
+ with assert_setup_component(0, 'sensor'):
+ yield from async_setup_component(hass, 'sensor',
+ {'sensor': INVALID_CONFIG})
- def add_devices(self, devices):
- """Mock add devices."""
- for device in devices:
- self.DEVICES.append(device)
- def setUp(self):
- """Initialize values for this testcase class."""
- self.DEVICES = []
- self.hass = get_test_home_assistant()
- self.key = 'foo'
- self.config = VALID_CONFIG_PWS
- self.lat = 37.8267
- self.lon = -122.423
- self.hass.config.latitude = self.lat
- self.hass.config.longitude = self.lon
+@asyncio.coroutine
+def test_sensor(hass, aioclient_mock):
+ """Test the WUnderground sensor class and methods."""
+ aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json'))
- def tearDown(self): # pylint: disable=invalid-name
- """Stop everything that was started."""
- self.hass.stop()
+ yield from async_setup_component(hass, 'sensor', {'sensor': VALID_CONFIG})
- @unittest.mock.patch('requests.get', side_effect=mocked_requests_get)
- def test_setup(self, req_mock):
- """Test that the component is loaded if passed in PWS Id."""
- self.assertTrue(
- wunderground.setup_platform(self.hass, VALID_CONFIG_PWS,
- self.add_devices, None))
- self.assertTrue(
- wunderground.setup_platform(self.hass, VALID_CONFIG,
- self.add_devices, None))
+ state = hass.states.get('sensor.pws_weather')
+ assert state.state == 'Clear'
+ assert state.name == "Weather Summary"
+ assert 'unit_of_measurement' not in state.attributes
+ assert state.attributes['entity_picture'] == \
+ 'https://icons.wxug.com/i/c/k/clear.gif'
- with self.assertRaises(PlatformNotReady):
- wunderground.setup_platform(self.hass, INVALID_CONFIG,
- self.add_devices, None)
+ state = hass.states.get('sensor.pws_alerts')
+ assert state.state == '1'
+ assert state.name == 'Alerts'
+ assert state.attributes['Message'] == \
+ "This is a test alert message"
+ assert state.attributes['icon'] == 'mdi:alert-circle-outline'
+ assert 'entity_picture' not in state.attributes
- @unittest.mock.patch('requests.get', side_effect=mocked_requests_get)
- def test_sensor(self, req_mock):
- """Test the WUnderground sensor class and methods."""
- wunderground.setup_platform(self.hass, VALID_CONFIG, self.add_devices,
- None)
- for device in self.DEVICES:
- device.update()
- entity_id = device.entity_id
- friendly_name = device.name
- self.assertTrue(entity_id.startswith('sensor.pws_'))
- if entity_id == 'sensor.pws_weather':
- self.assertEqual(HTTPS_ICON_URL, device.entity_picture)
- self.assertEqual(WEATHER, device.state)
- self.assertIsNone(device.unit_of_measurement)
- self.assertEqual("Weather Summary", friendly_name)
- elif entity_id == 'sensor.pws_alerts':
- self.assertEqual(1, device.state)
- self.assertEqual(ALERT_MESSAGE,
- device.device_state_attributes['Message'])
- self.assertEqual(ALERT_ICON, device.icon)
- self.assertIsNone(device.entity_picture)
- self.assertEqual('Alerts', friendly_name)
- elif entity_id == 'sensor.pws_location':
- self.assertEqual('Holly Springs, NC', device.state)
- self.assertEqual('Location', friendly_name)
- elif entity_id == 'sensor.pws_elevation':
- self.assertEqual('413', device.state)
- self.assertEqual('Elevation', friendly_name)
- elif entity_id == 'sensor.pws_feelslike_c':
- self.assertIsNone(device.entity_picture)
- self.assertEqual(FEELS_LIKE, device.state)
- self.assertEqual(TEMP_CELSIUS, device.unit_of_measurement)
- self.assertEqual("Feels Like", friendly_name)
- elif entity_id == 'sensor.pws_weather_1d_metric':
- self.assertEqual(FORECAST_TEXT, device.state)
- self.assertEqual('Tuesday', friendly_name)
- else:
- self.assertEqual(entity_id, 'sensor.pws_precip_1d_in')
- self.assertEqual(PRECIP_IN, device.state)
- self.assertEqual(LENGTH_INCHES, device.unit_of_measurement)
- self.assertEqual('Precipitation Intensity Today',
- friendly_name)
+ state = hass.states.get('sensor.pws_location')
+ assert state.state == "Holly Springs, NC"
+ assert state.name == 'Location'
- @unittest.mock.patch('requests.get',
- side_effect=ConnectionError('test exception'))
- def test_connect_failed(self, req_mock):
- """Test the WUnderground connection error."""
- with self.assertRaises(PlatformNotReady):
- wunderground.setup_platform(self.hass, VALID_CONFIG,
- self.add_devices, None)
+ state = hass.states.get('sensor.pws_elevation')
+ assert state.state == '413'
+ assert state.name == 'Elevation'
- @unittest.mock.patch('requests.get',
- side_effect=mocked_requests_get_invalid)
- def test_invalid_data(self, req_mock):
- """Test the WUnderground invalid data."""
- wunderground.setup_platform(self.hass, VALID_CONFIG_PWS,
- self.add_devices, None)
- for device in self.DEVICES:
- device.update()
- self.assertEqual(STATE_UNKNOWN, device.state)
+ state = hass.states.get('sensor.pws_feelslike_c')
+ assert state.state == '40'
+ assert state.name == "Feels Like"
+ assert 'entity_picture' not in state.attributes
+ assert state.attributes['unit_of_measurement'] == TEMP_CELSIUS
+
+ state = hass.states.get('sensor.pws_weather_1d_metric')
+ assert state.state == "Mostly Cloudy. Fog overnight."
+ assert state.name == 'Tuesday'
+
+ state = hass.states.get('sensor.pws_precip_1d_in')
+ assert state.state == '0.03'
+ assert state.name == "Precipitation Intensity Today"
+ assert state.attributes['unit_of_measurement'] == LENGTH_INCHES
+
+
+@asyncio.coroutine
+def test_connect_failed(hass, aioclient_mock):
+ """Test the WUnderground connection error."""
+ aioclient_mock.get(URL, exc=aiohttp.ClientError())
+ with raises(PlatformNotReady):
+ yield from wunderground.async_setup_platform(hass, VALID_CONFIG,
+ lambda _: None)
+
+
+@asyncio.coroutine
+def test_invalid_data(hass, aioclient_mock):
+ """Test the WUnderground invalid data."""
+ aioclient_mock.get(URL, text=load_fixture('wunderground-invalid.json'))
+
+ yield from async_setup_component(hass, 'sensor', {'sensor': VALID_CONFIG})
+
+ for condition in VALID_CONFIG['monitored_conditions']:
+ state = hass.states.get('sensor.pws_' + condition)
+ assert state.state == STATE_UNKNOWN
diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py
index 661f570e698..f79d0706321 100644
--- a/tests/components/switch/test_mqtt.py
+++ b/tests/components/switch/test_mqtt.py
@@ -70,16 +70,17 @@ class TestSwitchMQTT(unittest.TestCase):
switch.turn_on(self.hass, 'switch.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'beer on', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'beer on', 2, False)
+ self.mock_publish.async_publish.reset_mock()
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_ON, state.state)
switch.turn_off(self.hass, 'switch.test')
self.hass.block_till_done()
- self.assertEqual(('command-topic', 'beer off', 2, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'command-topic', 'beer off', 2, False)
state = self.hass.states.get('switch.test')
self.assertEqual(STATE_OFF, state.state)
diff --git a/tests/components/test_config_entry_example.py b/tests/components/test_config_entry_example.py
new file mode 100644
index 00000000000..31084384c31
--- /dev/null
+++ b/tests/components/test_config_entry_example.py
@@ -0,0 +1,38 @@
+"""Test the config entry example component."""
+import asyncio
+
+from homeassistant import config_entries
+
+
+@asyncio.coroutine
+def test_flow_works(hass):
+ """Test that the config flow works."""
+ result = yield from hass.config_entries.flow.async_init(
+ 'config_entry_example')
+
+ assert result['type'] == config_entries.RESULT_TYPE_FORM
+
+ result = yield from hass.config_entries.flow.async_configure(
+ result['flow_id'], {
+ 'object_id': 'bla'
+ })
+
+ assert result['type'] == config_entries.RESULT_TYPE_FORM
+
+ result = yield from hass.config_entries.flow.async_configure(
+ result['flow_id'], {
+ 'name': 'Hello'
+ })
+
+ assert result['type'] == config_entries.RESULT_TYPE_CREATE_ENTRY
+ state = hass.states.get('config_entry_example.bla')
+ assert state is not None
+ assert state.name == 'Hello'
+ assert 'config_entry_example' in hass.config.components
+ assert len(hass.config_entries.async_entries()) == 1
+
+ # Test removing entry.
+ entry = hass.config_entries.async_entries()[0]
+ yield from hass.config_entries.async_remove(entry.entry_id)
+ state = hass.states.get('config_entry_example.bla')
+ assert state is None
diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py
index fab1e24d8e7..8d629321853 100644
--- a/tests/components/test_conversation.py
+++ b/tests/components/test_conversation.py
@@ -6,6 +6,7 @@ import pytest
from homeassistant.setup import async_setup_component
from homeassistant.components import conversation
+import homeassistant.components as component
from homeassistant.helpers import intent
from tests.common import async_mock_intent, async_mock_service
@@ -16,6 +17,9 @@ def test_calling_intent(hass):
"""Test calling an intent from a conversation."""
intents = async_mock_intent(hass, 'OrderBeer')
+ result = yield from component.async_setup(hass, {})
+ assert result
+
result = yield from async_setup_component(hass, 'conversation', {
'conversation': {
'intents': {
@@ -145,6 +149,9 @@ def test_http_processing_intent(hass, test_client):
@pytest.mark.parametrize('sentence', ('turn on kitchen', 'turn kitchen on'))
def test_turn_on_intent(hass, sentence):
"""Test calling the turn on intent."""
+ result = yield from component.async_setup(hass, {})
+ assert result
+
result = yield from async_setup_component(hass, 'conversation', {})
assert result
@@ -168,6 +175,9 @@ def test_turn_on_intent(hass, sentence):
@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off'))
def test_turn_off_intent(hass, sentence):
"""Test calling the turn on intent."""
+ result = yield from component.async_setup(hass, {})
+ assert result
+
result = yield from async_setup_component(hass, 'conversation', {})
assert result
@@ -187,9 +197,38 @@ def test_turn_off_intent(hass, sentence):
assert call.data == {'entity_id': 'light.kitchen'}
+@asyncio.coroutine
+@pytest.mark.parametrize('sentence', ('toggle kitchen', 'kitchen toggle'))
+def test_toggle_intent(hass, sentence):
+ """Test calling the turn on intent."""
+ result = yield from component.async_setup(hass, {})
+ assert result
+
+ result = yield from async_setup_component(hass, 'conversation', {})
+ assert result
+
+ hass.states.async_set('light.kitchen', 'on')
+ calls = async_mock_service(hass, 'homeassistant', 'toggle')
+
+ yield from hass.services.async_call(
+ 'conversation', 'process', {
+ conversation.ATTR_TEXT: sentence
+ })
+ yield from hass.async_block_till_done()
+
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'homeassistant'
+ assert call.service == 'toggle'
+ assert call.data == {'entity_id': 'light.kitchen'}
+
+
@asyncio.coroutine
def test_http_api(hass, test_client):
"""Test the HTTP conversation API."""
+ result = yield from component.async_setup(hass, {})
+ assert result
+
result = yield from async_setup_component(hass, 'conversation', {})
assert result
@@ -212,6 +251,9 @@ def test_http_api(hass, test_client):
@asyncio.coroutine
def test_http_api_wrong_data(hass, test_client):
"""Test the HTTP conversation API."""
+ result = yield from component.async_setup(hass, {})
+ assert result
+
result = yield from async_setup_component(hass, 'conversation', {})
assert result
diff --git a/tests/components/test_history.py b/tests/components/test_history.py
index 8484e2c536f..4a759e7e0ac 100644
--- a/tests/components/test_history.py
+++ b/tests/components/test_history.py
@@ -10,8 +10,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.components import history, recorder
from tests.common import (
- init_recorder_component, mock_http_component, mock_state_change_event,
- get_test_home_assistant)
+ init_recorder_component, mock_state_change_event, get_test_home_assistant)
class TestComponentHistory(unittest.TestCase):
@@ -38,7 +37,6 @@ class TestComponentHistory(unittest.TestCase):
def test_setup(self):
"""Test setup method of history."""
- mock_http_component(self.hass)
config = history.CONFIG_SCHEMA({
# ha.DOMAIN: {},
history.DOMAIN: {
@@ -403,12 +401,12 @@ class TestComponentHistory(unittest.TestCase):
filters = history.Filters()
exclude = config[history.DOMAIN].get(history.CONF_EXCLUDE)
if exclude:
- filters.excluded_entities = exclude[history.CONF_ENTITIES]
- filters.excluded_domains = exclude[history.CONF_DOMAINS]
+ filters.excluded_entities = exclude.get(history.CONF_ENTITIES, [])
+ filters.excluded_domains = exclude.get(history.CONF_DOMAINS, [])
include = config[history.DOMAIN].get(history.CONF_INCLUDE)
if include:
- filters.included_entities = include[history.CONF_ENTITIES]
- filters.included_domains = include[history.CONF_DOMAINS]
+ filters.included_entities = include.get(history.CONF_ENTITIES, [])
+ filters.included_domains = include.get(history.CONF_DOMAINS, [])
hist = history.get_significant_states(
self.hass, zero, four, filters=filters)
diff --git a/tests/components/test_init.py b/tests/components/test_init.py
index dde141b6495..fff3b74c831 100644
--- a/tests/components/test_init.py
+++ b/tests/components/test_init.py
@@ -11,6 +11,7 @@ from homeassistant import config
from homeassistant.const import (
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE)
import homeassistant.components as comps
+import homeassistant.helpers.intent as intent
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity
from homeassistant.util.async import run_coroutine_threadsafe
@@ -195,3 +196,96 @@ class TestComponentsCore(unittest.TestCase):
self.hass.block_till_done()
assert mock_check.called
assert not mock_stop.called
+
+
+@asyncio.coroutine
+def test_turn_on_intent(hass):
+ """Test HassTurnOn intent."""
+ result = yield from comps.async_setup(hass, {})
+ assert result
+
+ hass.states.async_set('light.test_light', 'off')
+ calls = async_mock_service(hass, 'light', SERVICE_TURN_ON)
+
+ response = yield from intent.async_handle(
+ hass, 'test', 'HassTurnOn', {'name': {'value': 'test light'}}
+ )
+ yield from hass.async_block_till_done()
+
+ assert response.speech['plain']['speech'] == 'Turned on test light'
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'light'
+ assert call.service == 'turn_on'
+ assert call.data == {'entity_id': ['light.test_light']}
+
+
+@asyncio.coroutine
+def test_turn_off_intent(hass):
+ """Test HassTurnOff intent."""
+ result = yield from comps.async_setup(hass, {})
+ assert result
+
+ hass.states.async_set('light.test_light', 'on')
+ calls = async_mock_service(hass, 'light', SERVICE_TURN_OFF)
+
+ response = yield from intent.async_handle(
+ hass, 'test', 'HassTurnOff', {'name': {'value': 'test light'}}
+ )
+ yield from hass.async_block_till_done()
+
+ assert response.speech['plain']['speech'] == 'Turned off test light'
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'light'
+ assert call.service == 'turn_off'
+ assert call.data == {'entity_id': ['light.test_light']}
+
+
+@asyncio.coroutine
+def test_toggle_intent(hass):
+ """Test HassToggle intent."""
+ result = yield from comps.async_setup(hass, {})
+ assert result
+
+ hass.states.async_set('light.test_light', 'off')
+ calls = async_mock_service(hass, 'light', SERVICE_TOGGLE)
+
+ response = yield from intent.async_handle(
+ hass, 'test', 'HassToggle', {'name': {'value': 'test light'}}
+ )
+ yield from hass.async_block_till_done()
+
+ assert response.speech['plain']['speech'] == 'Toggled test light'
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'light'
+ assert call.service == 'toggle'
+ assert call.data == {'entity_id': ['light.test_light']}
+
+
+@asyncio.coroutine
+def test_turn_on_multiple_intent(hass):
+ """Test HassTurnOn intent with multiple similar entities.
+
+ This tests that matching finds the proper entity among similar names.
+ """
+ result = yield from comps.async_setup(hass, {})
+ assert result
+
+ hass.states.async_set('light.test_light', 'off')
+ hass.states.async_set('light.test_lights_2', 'off')
+ hass.states.async_set('light.test_lighter', 'off')
+ calls = async_mock_service(hass, 'light', SERVICE_TURN_ON)
+
+ response = yield from intent.async_handle(
+ hass, 'test', 'HassTurnOn', {'name': {'value': 'test lights'}}
+ )
+ yield from hass.async_block_till_done()
+
+ assert response.speech['plain']['speech'] == 'Turned on test lights'
+ assert len(calls) == 1
+ call = calls[0]
+ assert call.domain == 'light'
+ assert call.service == 'turn_on'
+ assert call.data == {'entity_id': ['light.test_lights_2']}
diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py
index 6a79994586c..bd10416c7a2 100644
--- a/tests/components/test_logbook.py
+++ b/tests/components/test_logbook.py
@@ -14,7 +14,7 @@ from homeassistant.components import logbook
from homeassistant.setup import setup_component
from tests.common import (
- mock_http_component, init_recorder_component, get_test_home_assistant)
+ init_recorder_component, get_test_home_assistant)
_LOGGER = logging.getLogger(__name__)
@@ -29,10 +29,7 @@ class TestComponentLogbook(unittest.TestCase):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
init_recorder_component(self.hass) # Force an in memory DB
- mock_http_component(self.hass)
- self.hass.config.components |= set(['frontend', 'recorder', 'api'])
- assert setup_component(self.hass, logbook.DOMAIN,
- self.EMPTY_CONFIG)
+ assert setup_component(self.hass, logbook.DOMAIN, self.EMPTY_CONFIG)
self.hass.start()
def tearDown(self):
@@ -375,7 +372,8 @@ class TestComponentLogbook(unittest.TestCase):
eventB = self.create_state_changed_event(pointA, entity_id2, 20,
{'auto': True})
- entries = list(logbook.humanify((eventA, eventB)))
+ events = logbook._exclude_events((eventA, eventB), {})
+ entries = list(logbook.humanify(events))
self.assertEqual(1, len(entries))
self.assert_entry(entries[0], pointA, 'bla', domain='switch',
@@ -392,7 +390,8 @@ class TestComponentLogbook(unittest.TestCase):
eventB = self.create_state_changed_event(
pointA, entity_id2, 20, last_changed=pointA, last_updated=pointB)
- entries = list(logbook.humanify((eventA, eventB)))
+ events = logbook._exclude_events((eventA, eventB), {})
+ entries = list(logbook.humanify(events))
self.assertEqual(1, len(entries))
self.assert_entry(entries[0], pointA, 'bla', domain='switch',
diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py
index 91175024ea6..f4fc3e89ee0 100644
--- a/tests/components/test_mqtt_eventstream.py
+++ b/tests/components/test_mqtt_eventstream.py
@@ -30,13 +30,16 @@ class TestMqttEventStream(object):
"""Stop everything that was started."""
self.hass.stop()
- def add_eventstream(self, sub_topic=None, pub_topic=None):
+ def add_eventstream(self, sub_topic=None, pub_topic=None,
+ ignore_event=None):
"""Add a mqtt_eventstream component."""
config = {}
if sub_topic:
config['subscribe_topic'] = sub_topic
if pub_topic:
config['publish_topic'] = pub_topic
+ if ignore_event:
+ config['ignore_event'] = ignore_event
return setup_component(self.hass, eventstream.DOMAIN, {
eventstream.DOMAIN: config})
@@ -144,3 +147,57 @@ class TestMqttEventStream(object):
self.hass.block_till_done()
assert 1 == len(calls)
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ def test_ignored_event_doesnt_send_over_stream(self, mock_pub):
+ """"Test the ignoring of sending events if defined."""
+ assert self.add_eventstream(pub_topic='bar',
+ ignore_event=['state_changed'])
+ self.hass.block_till_done()
+
+ e_id = 'entity.test_id'
+ event = {}
+ event['event_type'] = EVENT_STATE_CHANGED
+ new_state = {
+ "state": "on",
+ "entity_id": e_id,
+ "attributes": {},
+ }
+ event['event_data'] = {"new_state": new_state, "entity_id": e_id}
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_eventstream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State(e_id, 'on'))
+ self.hass.block_till_done()
+
+ assert not mock_pub.called
+
+ @patch('homeassistant.components.mqtt.async_publish')
+ def test_wrong_ignored_event_sends_over_stream(self, mock_pub):
+ """"Test the ignoring of sending events if defined."""
+ assert self.add_eventstream(pub_topic='bar',
+ ignore_event=['statee_changed'])
+ self.hass.block_till_done()
+
+ e_id = 'entity.test_id'
+ event = {}
+ event['event_type'] = EVENT_STATE_CHANGED
+ new_state = {
+ "state": "on",
+ "entity_id": e_id,
+ "attributes": {},
+ }
+ event['event_data'] = {"new_state": new_state, "entity_id": e_id}
+
+ # Reset the mock because it will have already gotten calls for the
+ # mqtt_eventstream state change on initialization, etc.
+ mock_pub.reset_mock()
+
+ # Set a state of an entity
+ mock_state_change_event(self.hass, State(e_id, 'on'))
+ self.hass.block_till_done()
+
+ assert mock_pub.called
diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py
index ef702b96f4b..91a07511787 100644
--- a/tests/components/test_panel_iframe.py
+++ b/tests/components/test_panel_iframe.py
@@ -55,6 +55,11 @@ class TestPanelIframe(unittest.TestCase):
'title': 'Api',
'url': '/api',
},
+ 'ftp': {
+ 'icon': 'mdi:weather',
+ 'title': 'FTP',
+ 'url': 'ftp://some/ftp',
+ },
},
})
@@ -86,3 +91,12 @@ class TestPanelIframe(unittest.TestCase):
'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'api',
}
+
+ assert panels.get('ftp').to_response(self.hass, None) == {
+ 'component_name': 'iframe',
+ 'config': {'url': 'ftp://some/ftp'},
+ 'icon': 'mdi:weather',
+ 'title': 'FTP',
+ 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
+ 'url_path': 'ftp',
+ }
diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py
index 9f6573920ca..ccb88018c66 100644
--- a/tests/components/test_rflink.py
+++ b/tests/components/test_rflink.py
@@ -8,12 +8,10 @@ from homeassistant.components.rflink import (
CONF_RECONNECT_INTERVAL, SERVICE_SEND_COMMAND)
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_STOP_COVER)
-from tests.common import assert_setup_component
@asyncio.coroutine
-def mock_rflink(hass, config, domain, monkeypatch, failures=None,
- platform_count=1):
+def mock_rflink(hass, config, domain, monkeypatch, failures=None):
"""Create mock Rflink asyncio protocol, test component setup."""
transport, protocol = (Mock(), Mock())
@@ -47,9 +45,7 @@ def mock_rflink(hass, config, domain, monkeypatch, failures=None,
'rflink.protocol.create_rflink_connection',
mock_create)
- # verify instantiation of component with given config
- with assert_setup_component(platform_count, domain):
- yield from async_setup_component(hass, domain, config)
+ yield from async_setup_component(hass, domain, config)
# hook into mock config for injecting events
event_callback = mock_create.call_args_list[0][1]['event_callback']
@@ -164,7 +160,7 @@ def test_send_command(hass, monkeypatch):
# setup mocking rflink module
_, _, protocol, _ = yield from mock_rflink(
- hass, config, domain, monkeypatch, platform_count=5)
+ hass, config, domain, monkeypatch)
hass.async_add_job(
hass.services.async_call(domain, SERVICE_SEND_COMMAND,
@@ -188,7 +184,7 @@ def test_send_command_invalid_arguments(hass, monkeypatch):
# setup mocking rflink module
_, _, protocol, _ = yield from mock_rflink(
- hass, config, domain, monkeypatch, platform_count=5)
+ hass, config, domain, monkeypatch)
# one argument missing
hass.async_add_job(
diff --git a/tests/components/test_shopping_list.py b/tests/components/test_shopping_list.py
index 2e1a03c37d0..4203f7587ae 100644
--- a/tests/components/test_shopping_list.py
+++ b/tests/components/test_shopping_list.py
@@ -150,7 +150,6 @@ def test_api_update_fails(hass, test_client):
assert resp.status == 404
beer_id = hass.data['shopping_list'].items[0]['id']
- client = yield from test_client(hass.http.app)
resp = yield from client.post(
'/api/shopping_list/item/{}'.format(beer_id), json={
'name': 123,
diff --git a/tests/components/test_weblink.py b/tests/components/test_weblink.py
index 249e81d37af..f35398e034c 100644
--- a/tests/components/test_weblink.py
+++ b/tests/components/test_weblink.py
@@ -91,6 +91,19 @@ class TestComponentWeblink(unittest.TestCase):
}
}))
+ def test_good_config_ftp_link(self):
+ """Test if new entity is created."""
+ self.assertTrue(setup_component(self.hass, 'weblink', {
+ 'weblink': {
+ 'entities': [
+ {
+ weblink.CONF_NAME: 'My FTP URL',
+ weblink.CONF_URL: 'ftp://somehost/'
+ },
+ ],
+ }
+ }))
+
def test_entities_get_created(self):
"""Test if new entity is created."""
self.assertTrue(setup_component(self.hass, weblink.DOMAIN, {
diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py
index 8b6c7494214..f85030a6892 100644
--- a/tests/components/test_websocket_api.py
+++ b/tests/components/test_websocket_api.py
@@ -8,8 +8,9 @@ import pytest
from homeassistant.core import callback
from homeassistant.components import websocket_api as wapi, frontend
+from homeassistant.setup import async_setup_component
-from tests.common import mock_http_component_app, mock_coro
+from tests.common import mock_coro
API_PASSWORD = 'test1234'
@@ -17,10 +18,10 @@ API_PASSWORD = 'test1234'
@pytest.fixture
def websocket_client(loop, hass, test_client):
"""Websocket client fixture connected to websocket server."""
- websocket_app = mock_http_component_app(hass)
- wapi.WebsocketAPIView().register(websocket_app.router)
+ assert loop.run_until_complete(
+ async_setup_component(hass, 'websocket_api'))
- client = loop.run_until_complete(test_client(websocket_app))
+ client = loop.run_until_complete(test_client(hass.http.app))
ws = loop.run_until_complete(client.ws_connect(wapi.URL))
auth_ok = loop.run_until_complete(ws.receive_json())
@@ -35,10 +36,14 @@ def websocket_client(loop, hass, test_client):
@pytest.fixture
def no_auth_websocket_client(hass, loop, test_client):
"""Websocket connection that requires authentication."""
- websocket_app = mock_http_component_app(hass, API_PASSWORD)
- wapi.WebsocketAPIView().register(websocket_app.router)
+ assert loop.run_until_complete(
+ async_setup_component(hass, 'websocket_api', {
+ 'http': {
+ 'api_password': API_PASSWORD
+ }
+ }))
- client = loop.run_until_complete(test_client(websocket_app))
+ client = loop.run_until_complete(test_client(hass.http.app))
ws = loop.run_until_complete(client.ws_connect(wapi.URL))
auth_ok = loop.run_until_complete(ws.receive_json())
diff --git a/tests/components/vacuum/test_mqtt.py b/tests/components/vacuum/test_mqtt.py
index 8c3b5fa4eeb..ba2288e3fc6 100644
--- a/tests/components/vacuum/test_mqtt.py
+++ b/tests/components/vacuum/test_mqtt.py
@@ -71,52 +71,56 @@ class TestVacuumMQTT(unittest.TestCase):
vacuum.turn_on(self.hass, 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(('vacuum/command', 'turn_on', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/command', 'turn_on', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.turn_off(self.hass, 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(('vacuum/command', 'turn_off', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/command', 'turn_off', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.stop(self.hass, 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(('vacuum/command', 'stop', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/command', 'stop', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.clean_spot(self.hass, 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(('vacuum/command', 'clean_spot', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/command', 'clean_spot', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.locate(self.hass, 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(('vacuum/command', 'locate', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/command', 'locate', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.start_pause(self.hass, 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(('vacuum/command', 'start_pause', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/command', 'start_pause', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.return_to_base(self.hass, 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(('vacuum/command', 'return_to_base', 0, False),
- self.mock_publish.mock_calls[-2][1])
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/command', 'return_to_base', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.set_fan_speed(self.hass, 'high', 'vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(
- ('vacuum/set_fan_speed', 'high', 0, False),
- self.mock_publish.mock_calls[-2][1]
- )
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/set_fan_speed', 'high', 0, False)
+ self.mock_publish.async_publish.reset_mock()
vacuum.send_command(self.hass, '44 FE 93', entity_id='vacuum.mqtttest')
self.hass.block_till_done()
- self.assertEqual(
- ('vacuum/send_command', '44 FE 93', 0, False),
- self.mock_publish.mock_calls[-2][1]
- )
+ self.mock_publish.async_publish.assert_called_once_with(
+ 'vacuum/send_command', '44 FE 93', 0, False)
def test_status(self):
"""Test status updates from the vacuum."""
diff --git a/tests/conftest.py b/tests/conftest.py
index f1947a61ad0..989785e72d5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,11 +8,11 @@ from unittest.mock import patch, MagicMock
import pytest
import requests_mock as _requests_mock
-from homeassistant import util, setup
+from homeassistant import util
from homeassistant.util import location
-from homeassistant.components import mqtt
-from tests.common import async_test_home_assistant, mock_coro, INSTANCES
+from tests.common import async_test_home_assistant, INSTANCES, \
+ async_mock_mqtt_component
from tests.test_util.aiohttp import mock_aiohttp_client
from tests.mock.zwave import MockNetwork, MockOption
@@ -85,17 +85,9 @@ def aioclient_mock():
@pytest.fixture
def mqtt_mock(loop, hass):
"""Fixture to mock MQTT."""
- with patch('homeassistant.components.mqtt.MQTT') as mock_mqtt:
- mock_mqtt().async_connect.return_value = mock_coro(True)
- assert loop.run_until_complete(setup.async_setup_component(
- hass, mqtt.DOMAIN, {
- mqtt.DOMAIN: {
- mqtt.CONF_BROKER: 'mock-broker',
- }
- }))
- client = mock_mqtt()
- client.reset_mock()
- return client
+ client = loop.run_until_complete(async_mock_mqtt_component(hass))
+ client.reset_mock()
+ return client
@pytest.fixture
diff --git a/tests/fixtures/pushbullet_devices.json b/tests/fixtures/pushbullet_devices.json
index 04f336c8adf..576e748471a 100755
--- a/tests/fixtures/pushbullet_devices.json
+++ b/tests/fixtures/pushbullet_devices.json
@@ -1,43 +1,43 @@
-{
- "accounts": [],
- "blocks": [],
- "channels": [],
- "chats": [],
- "clients": [],
- "contacts": [],
- "devices": [{
- "active": true,
- "iden": "identity1",
- "created": 1.514520333770855e+09,
- "modified": 1.5151951594363022e+09,
- "type": "windows",
- "kind": "windows",
- "nickname": "DESKTOP",
- "manufacturer": "Microsoft",
- "model": "Windows 10 Home",
- "app_version": 396,
- "fingerprint": "{\"cpu\":\"AMD\",\"computer_name\":\"DESKTOP\"}",
- "pushable": true,
- "icon": "desktop",
- "remote_files": "disabled"
- }, {
- "active": true,
- "iden": "identity2",
- "created": 1.5144974875448499e+09,
- "modified": 1.514574792288634e+09,
- "type": "ios",
- "kind": "ios",
- "nickname": "My iPhone",
- "manufacturer": "Apple",
- "model": "iPhone",
- "app_version": 8646,
- "push_token": "production:mytoken",
- "pushable": true,
- "icon": "phone"
- }],
- "grants": [],
- "pushes": [],
- "profiles": [],
- "subscriptions": [],
- "texts": []
-}
+{
+ "accounts": [],
+ "blocks": [],
+ "channels": [],
+ "chats": [],
+ "clients": [],
+ "contacts": [],
+ "devices": [{
+ "active": true,
+ "iden": "identity1",
+ "created": 1.514520333770855e+09,
+ "modified": 1.5151951594363022e+09,
+ "type": "windows",
+ "kind": "windows",
+ "nickname": "DESKTOP",
+ "manufacturer": "Microsoft",
+ "model": "Windows 10 Home",
+ "app_version": 396,
+ "fingerprint": "{\"cpu\":\"AMD\",\"computer_name\":\"DESKTOP\"}",
+ "pushable": true,
+ "icon": "desktop",
+ "remote_files": "disabled"
+ }, {
+ "active": true,
+ "iden": "identity2",
+ "created": 1.5144974875448499e+09,
+ "modified": 1.514574792288634e+09,
+ "type": "ios",
+ "kind": "ios",
+ "nickname": "My iPhone",
+ "manufacturer": "Apple",
+ "model": "iPhone",
+ "app_version": 8646,
+ "push_token": "production:mytoken",
+ "pushable": true,
+ "icon": "phone"
+ }],
+ "grants": [],
+ "pushes": [],
+ "profiles": [],
+ "subscriptions": [],
+ "texts": []
+}
diff --git a/tests/fixtures/wunderground-error.json b/tests/fixtures/wunderground-error.json
new file mode 100644
index 00000000000..264ecbf8cd6
--- /dev/null
+++ b/tests/fixtures/wunderground-error.json
@@ -0,0 +1,11 @@
+{
+ "response": {
+ "version": "0.1",
+ "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
+ "features": {},
+ "error": {
+ "type": "keynotfound",
+ "description": "this key does not exist"
+ }
+ }
+}
diff --git a/tests/fixtures/wunderground-invalid.json b/tests/fixtures/wunderground-invalid.json
new file mode 100644
index 00000000000..59661c6694d
--- /dev/null
+++ b/tests/fixtures/wunderground-invalid.json
@@ -0,0 +1,18 @@
+{
+ "response": {
+ "version": "0.1",
+ "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
+ "features": {
+ "conditions": 1,
+ "alerts": 1,
+ "forecast": 1
+ }
+ },
+ "current_observation": {
+ "image": {
+ "url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png",
+ "title": "Weather Underground",
+ "link": "http://www.wunderground.com"
+ }
+ }
+}
diff --git a/tests/fixtures/wunderground-valid.json b/tests/fixtures/wunderground-valid.json
new file mode 100644
index 00000000000..7ac1081cb4e
--- /dev/null
+++ b/tests/fixtures/wunderground-valid.json
@@ -0,0 +1,90 @@
+{
+ "response": {
+ "version": "0.1",
+ "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
+ "features": {
+ "conditions": 1,
+ "alerts": 1,
+ "forecast": 1
+ }
+ },
+ "current_observation": {
+ "image": {
+ "url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png",
+ "title": "Weather Underground",
+ "link": "http://www.wunderground.com"
+ },
+ "feelslike_c": "40",
+ "weather": "Clear",
+ "icon_url": "http://icons.wxug.com/i/c/k/clear.gif",
+ "display_location": {
+ "city": "Holly Springs",
+ "country": "US",
+ "full": "Holly Springs, NC"
+ },
+ "observation_location": {
+ "elevation": "413 ft",
+ "full": "Twin Lake, Holly Springs, North Carolina"
+ }
+ },
+ "alerts": [
+ {
+ "type": "FLO",
+ "description": "Areal Flood Warning",
+ "date": "9:36 PM CDT on September 22, 2016",
+ "expires": "10:00 AM CDT on September 23, 2016",
+ "message": "This is a test alert message"
+ }
+ ],
+ "forecast": {
+ "txt_forecast": {
+ "date": "22:35 CEST",
+ "forecastday": [
+ {
+ "period": 0,
+ "icon_url": "http://icons.wxug.com/i/c/k/clear.gif",
+ "title": "Tuesday",
+ "fcttext": "Mostly Cloudy. Fog overnight.",
+ "fcttext_metric": "Mostly Cloudy. Fog overnight.",
+ "pop": "0"
+ }
+ ]
+ },
+ "simpleforecast": {
+ "forecastday": [
+ {
+ "date": {
+ "pretty": "19:00 CEST 4. Duben 2017"
+ },
+ "period": 1,
+ "high": {
+ "fahrenheit": "56",
+ "celsius": "13"
+ },
+ "low": {
+ "fahrenheit": "43",
+ "celsius": "6"
+ },
+ "conditions": "Mo\u017enost de\u0161t\u011b",
+ "icon_url": "http://icons.wxug.com/i/c/k/chancerain.gif",
+ "qpf_allday": {
+ "in": 0.03,
+ "mm": 1
+ },
+ "maxwind": {
+ "mph": 0,
+ "kph": 0,
+ "dir": "",
+ "degrees": 0
+ },
+ "avewind": {
+ "mph": 0,
+ "kph": 0,
+ "dir": "severn\u00ed",
+ "degrees": 0
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/tests/fixtures/yahooweather.json b/tests/fixtures/yahooweather.json
index f6ab2980618..7d8188764df 100644
--- a/tests/fixtures/yahooweather.json
+++ b/tests/fixtures/yahooweather.json
@@ -1,138 +1,138 @@
-{
- "query": {
- "count": 1,
- "created": "2017-11-17T13:40:47Z",
- "lang": "en-US",
- "results": {
- "channel": {
- "units": {
- "distance": "km",
- "pressure": "mb",
- "speed": "km/h",
- "temperature": "C"
- },
- "title": "Yahoo! Weather - San Diego, CA, US",
- "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-23511632/",
- "description": "Yahoo! Weather for San Diego, CA, US",
- "language": "en-us",
- "lastBuildDate": "Fri, 17 Nov 2017 05:40 AM PST",
- "ttl": "60",
- "location": {
- "city": "San Diego",
- "country": "United States",
- "region": " CA"
- },
- "wind": {
- "chill": "56",
- "direction": "0",
- "speed": "6.34"
- },
- "atmosphere": {
- "humidity": "71",
- "pressure": "33863.75",
- "rising": "0",
- "visibility": "22.91"
- },
- "astronomy": {
- "sunrise": "6:21 am",
- "sunset": "4:47 pm"
- },
- "image": {
- "title": "Yahoo! Weather",
- "width": "142",
- "height": "18",
- "link": "http://weather.yahoo.com",
- "url": "http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"
- },
- "item": {
- "title": "Conditions for San Diego, CA, US at 05:00 AM PST",
- "lat": "32.878101",
- "long": "-117.23497",
- "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-23511632/",
- "pubDate": "Fri, 17 Nov 2017 05:00 AM PST",
- "condition": {
- "code": "26",
- "date": "Fri, 17 Nov 2017 05:00 AM PST",
- "temp": "18",
- "text": "Cloudy"
- },
- "forecast": [{
- "code": "28",
- "date": "17 Nov 2017",
- "day": "Fri",
- "high": "23",
- "low": "16",
- "text": "Mostly Cloudy"
- }, {
- "code": "30",
- "date": "18 Nov 2017",
- "day": "Sat",
- "high": "22",
- "low": "13",
- "text": "Partly Cloudy"
- }, {
- "code": "30",
- "date": "19 Nov 2017",
- "day": "Sun",
- "high": "22",
- "low": "12",
- "text": "Partly Cloudy"
- }, {
- "code": "28",
- "date": "20 Nov 2017",
- "day": "Mon",
- "high": "21",
- "low": "11",
- "text": "Mostly Cloudy"
- }, {
- "code": "28",
- "date": "21 Nov 2017",
- "day": "Tue",
- "high": "24",
- "low": "14",
- "text": "Mostly Cloudy"
- }, {
- "code": "30",
- "date": "22 Nov 2017",
- "day": "Wed",
- "high": "27",
- "low": "15",
- "text": "Partly Cloudy"
- }, {
- "code": "34",
- "date": "23 Nov 2017",
- "day": "Thu",
- "high": "27",
- "low": "15",
- "text": "Mostly Sunny"
- }, {
- "code": "30",
- "date": "24 Nov 2017",
- "day": "Fri",
- "high": "23",
- "low": "16",
- "text": "Partly Cloudy"
- }, {
- "code": "30",
- "date": "25 Nov 2017",
- "day": "Sat",
- "high": "22",
- "low": "15",
- "text": "Partly Cloudy"
- }, {
- "code": "28",
- "date": "26 Nov 2017",
- "day": "Sun",
- "high": "24",
- "low": "13",
- "text": "Mostly Cloudy"
- }],
- "description": "\n
\nCurrent Conditions:\n
Cloudy\n
\n
\nForecast:\n
Fri - Mostly Cloudy. High: 23Low: 16\n
Sat - Partly Cloudy. High: 22Low: 13\n
Sun - Partly Cloudy. High: 22Low: 12\n
Mon - Mostly Cloudy. High: 21Low: 11\n
Tue - Mostly Cloudy. High: 24Low: 14\n
\n
\nFull Forecast at Yahoo! Weather\n
\n
\n
\n]]>",
- "guid": {
- "isPermaLink": "false"
- }
- }
- }
- }
- }
-}
+{
+ "query": {
+ "count": 1,
+ "created": "2017-11-17T13:40:47Z",
+ "lang": "en-US",
+ "results": {
+ "channel": {
+ "units": {
+ "distance": "km",
+ "pressure": "mb",
+ "speed": "km/h",
+ "temperature": "C"
+ },
+ "title": "Yahoo! Weather - San Diego, CA, US",
+ "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-23511632/",
+ "description": "Yahoo! Weather for San Diego, CA, US",
+ "language": "en-us",
+ "lastBuildDate": "Fri, 17 Nov 2017 05:40 AM PST",
+ "ttl": "60",
+ "location": {
+ "city": "San Diego",
+ "country": "United States",
+ "region": " CA"
+ },
+ "wind": {
+ "chill": "56",
+ "direction": "0",
+ "speed": "6.34"
+ },
+ "atmosphere": {
+ "humidity": "71",
+ "pressure": "33863.75",
+ "rising": "0",
+ "visibility": "22.91"
+ },
+ "astronomy": {
+ "sunrise": "6:21 am",
+ "sunset": "4:47 pm"
+ },
+ "image": {
+ "title": "Yahoo! Weather",
+ "width": "142",
+ "height": "18",
+ "link": "http://weather.yahoo.com",
+ "url": "http://l.yimg.com/a/i/brand/purplelogo//uh/us/news-wea.gif"
+ },
+ "item": {
+ "title": "Conditions for San Diego, CA, US at 05:00 AM PST",
+ "lat": "32.878101",
+ "long": "-117.23497",
+ "link": "http://us.rd.yahoo.com/dailynews/rss/weather/Country__Country/*https://weather.yahoo.com/country/state/city-23511632/",
+ "pubDate": "Fri, 17 Nov 2017 05:00 AM PST",
+ "condition": {
+ "code": "26",
+ "date": "Fri, 17 Nov 2017 05:00 AM PST",
+ "temp": "18",
+ "text": "Cloudy"
+ },
+ "forecast": [{
+ "code": "28",
+ "date": "17 Nov 2017",
+ "day": "Fri",
+ "high": "23",
+ "low": "16",
+ "text": "Mostly Cloudy"
+ }, {
+ "code": "30",
+ "date": "18 Nov 2017",
+ "day": "Sat",
+ "high": "22",
+ "low": "13",
+ "text": "Partly Cloudy"
+ }, {
+ "code": "30",
+ "date": "19 Nov 2017",
+ "day": "Sun",
+ "high": "22",
+ "low": "12",
+ "text": "Partly Cloudy"
+ }, {
+ "code": "28",
+ "date": "20 Nov 2017",
+ "day": "Mon",
+ "high": "21",
+ "low": "11",
+ "text": "Mostly Cloudy"
+ }, {
+ "code": "28",
+ "date": "21 Nov 2017",
+ "day": "Tue",
+ "high": "24",
+ "low": "14",
+ "text": "Mostly Cloudy"
+ }, {
+ "code": "30",
+ "date": "22 Nov 2017",
+ "day": "Wed",
+ "high": "27",
+ "low": "15",
+ "text": "Partly Cloudy"
+ }, {
+ "code": "34",
+ "date": "23 Nov 2017",
+ "day": "Thu",
+ "high": "27",
+ "low": "15",
+ "text": "Mostly Sunny"
+ }, {
+ "code": "30",
+ "date": "24 Nov 2017",
+ "day": "Fri",
+ "high": "23",
+ "low": "16",
+ "text": "Partly Cloudy"
+ }, {
+ "code": "30",
+ "date": "25 Nov 2017",
+ "day": "Sat",
+ "high": "22",
+ "low": "15",
+ "text": "Partly Cloudy"
+ }, {
+ "code": "28",
+ "date": "26 Nov 2017",
+ "day": "Sun",
+ "high": "24",
+ "low": "13",
+ "text": "Mostly Cloudy"
+ }],
+ "description": "\n
\nCurrent Conditions:\n
Cloudy\n
\n
\nForecast:\n
Fri - Mostly Cloudy. High: 23Low: 16\n
Sat - Partly Cloudy. High: 22Low: 13\n
Sun - Partly Cloudy. High: 22Low: 12\n
Mon - Mostly Cloudy. High: 21Low: 11\n
Tue - Mostly Cloudy. High: 24Low: 14\n
\n
\nFull Forecast at Yahoo! Weather\n
\n
\n
\n]]>",
+ "guid": {
+ "isPermaLink": "false"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py
index 2991e07a464..aa7b5170648 100644
--- a/tests/helpers/test_condition.py
+++ b/tests/helpers/test_condition.py
@@ -146,3 +146,21 @@ class TestConditionHelper:
return_value=dt.now().replace(hour=21)):
assert not condition.time(after=sixam, before=sixpm)
assert condition.time(after=sixpm, before=sixam)
+
+ def test_if_numeric_state_not_raise_on_unavailable(self):
+ """Test numeric_state doesn't raise on unavailable/unknown state."""
+ test = condition.from_config({
+ 'condition': 'numeric_state',
+ 'entity_id': 'sensor.temperature',
+ 'below': 42
+ })
+
+ with patch('homeassistant.helpers.condition._LOGGER.warning') \
+ as logwarn:
+ self.hass.states.set('sensor.temperature', 'unavailable')
+ assert not test(self.hass)
+ assert len(logwarn.mock_calls) == 0
+
+ self.hass.states.set('sensor.temperature', 'unknown')
+ assert not test(self.hass)
+ assert len(logwarn.mock_calls) == 0
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index 26262f50ac4..66f0597fc93 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -524,18 +524,14 @@ def test_enum():
def test_socket_timeout(): # pylint: disable=invalid-name
"""Test socket timeout validator."""
- TEST_CONF_TIMEOUT = 'timeout' # pylint: disable=invalid-name
-
- schema = vol.Schema(
- {vol.Required(TEST_CONF_TIMEOUT, default=None): cv.socket_timeout})
+ schema = vol.Schema(cv.socket_timeout)
with pytest.raises(vol.Invalid):
- schema({TEST_CONF_TIMEOUT: 0.0})
+ schema(0.0)
with pytest.raises(vol.Invalid):
- schema({TEST_CONF_TIMEOUT: -1})
+ schema(-1)
- assert _GLOBAL_DEFAULT_TIMEOUT == schema({TEST_CONF_TIMEOUT:
- None})[TEST_CONF_TIMEOUT]
+ assert _GLOBAL_DEFAULT_TIMEOUT == schema(None)
- assert schema({TEST_CONF_TIMEOUT: 1})[TEST_CONF_TIMEOUT] == 1.0
+ assert schema(1) == 1.0
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
index a54a6de511a..0681691ed67 100644
--- a/tests/helpers/test_entity_platform.py
+++ b/tests/helpers/test_entity_platform.py
@@ -19,16 +19,17 @@ from tests.common import (
_LOGGER = logging.getLogger(__name__)
DOMAIN = "test_domain"
+PLATFORM = 'test_platform'
class MockEntityPlatform(entity_platform.EntityPlatform):
"""Mock class with some mock defaults."""
def __init__(
- self, *, hass,
+ self, hass,
logger=None,
- domain='test',
- platform_name='test_platform',
+ domain=DOMAIN,
+ platform_name=PLATFORM,
scan_interval=timedelta(seconds=15),
parallel_updates=0,
entity_namespace=None,
@@ -331,7 +332,7 @@ def test_parallel_updates_async_platform_with_constant(hass):
@asyncio.coroutine
def test_parallel_updates_sync_platform(hass):
"""Warn we log when platform setup takes a long time."""
- platform = MockPlatform()
+ platform = MockPlatform(setup_platform=lambda *args: None)
loader.set_component('test_domain.platform', platform)
@@ -486,7 +487,26 @@ def test_overriding_name_from_registry(hass):
def test_registry_respect_entity_namespace(hass):
"""Test that the registry respects entity namespace."""
mock_registry(hass)
- platform = MockEntityPlatform(hass=hass, entity_namespace='ns')
+ platform = MockEntityPlatform(hass, entity_namespace='ns')
entity = MockEntity(unique_id='1234', name='Device Name')
yield from platform.async_add_entities([entity])
- assert entity.entity_id == 'test.ns_device_name'
+ assert entity.entity_id == 'test_domain.ns_device_name'
+
+
+@asyncio.coroutine
+def test_registry_respect_entity_disabled(hass):
+ """Test that the registry respects entity disabled."""
+ mock_registry(hass, {
+ 'test_domain.world': entity_registry.RegistryEntry(
+ entity_id='test_domain.world',
+ unique_id='1234',
+ # Using component.async_add_entities is equal to platform "domain"
+ platform='test_platform',
+ disabled_by=entity_registry.DISABLED_USER
+ )
+ })
+ platform = MockEntityPlatform(hass)
+ entity = MockEntity(unique_id='1234')
+ yield from platform.async_add_entities([entity])
+ assert entity.entity_id is None
+ assert hass.states.async_entity_ids() == []
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
index 7e1150638c1..cb8703d1fe6 100644
--- a/tests/helpers/test_entity_registry.py
+++ b/tests/helpers/test_entity_registry.py
@@ -148,6 +148,14 @@ test.named:
test.no_name:
platform: super_platform
unique_id: without-name
+test.disabled_user:
+ platform: super_platform
+ unique_id: disabled-user
+ disabled_by: user
+test.disabled_hass:
+ platform: super_platform
+ unique_id: disabled-hass
+ disabled_by: hass
"""
registry = entity_registry.EntityRegistry(hass)
@@ -162,3 +170,13 @@ test.no_name:
'test', 'super_platform', 'without-name')
assert entry_with_name.name == 'registry override'
assert entry_without_name.name is None
+ assert not entry_with_name.disabled
+
+ entry_disabled_hass = registry.async_get_or_create(
+ 'test', 'super_platform', 'disabled-hass')
+ entry_disabled_user = registry.async_get_or_create(
+ 'test', 'super_platform', 'disabled-user')
+ assert entry_disabled_hass.disabled
+ assert entry_disabled_hass.disabled_by == entity_registry.DISABLED_HASS
+ assert entry_disabled_user.disabled
+ assert entry_disabled_user.disabled_by == entity_registry.DISABLED_USER
diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py
index 385b0a5df05..a8ae20ad69b 100644
--- a/tests/helpers/test_script.py
+++ b/tests/helpers/test_script.py
@@ -71,13 +71,14 @@ class TestScriptHelper(unittest.TestCase):
script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA({
'event': event,
'event_data_template': {
- 'hello': """
- {% if is_world == 'yes' %}
- world
- {% else %}
- not world
- {% endif %}
- """
+ 'dict': {
+ 1: '{{ is_world }}',
+ 2: '{{ is_world }}{{ is_world }}',
+ 3: '{{ is_world }}{{ is_world }}{{ is_world }}',
+ },
+ 'list': [
+ '{{ is_world }}', '{{ is_world }}{{ is_world }}'
+ ]
}
}))
@@ -86,7 +87,14 @@ class TestScriptHelper(unittest.TestCase):
self.hass.block_till_done()
assert len(calls) == 1
- assert calls[0].data.get('hello') == 'world'
+ assert calls[0].data == {
+ 'dict': {
+ 1: 'yes',
+ 2: 'yesyes',
+ 3: 'yesyesyes',
+ },
+ 'list': ['yes', 'yesyes']
+ }
assert not script_obj.can_cancel
def test_calling_service(self):
diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py
index a454a5a64b4..728e683a43a 100644
--- a/tests/scripts/test_check_config.py
+++ b/tests/scripts/test_check_config.py
@@ -1,10 +1,10 @@
"""Test check_config script."""
import asyncio
import logging
-import os
import unittest
import homeassistant.scripts.check_config as check_config
+from homeassistant.loader import set_component
from tests.common import patch_yaml_files, get_test_config_dir
_LOGGER = logging.getLogger(__name__)
@@ -36,14 +36,6 @@ def change_yaml_files(check_dict):
check_dict['yaml_files'].append('...' + key[len(root):])
-def tearDownModule(self): # pylint: disable=invalid-name
- """Clean files."""
- # .HA_VERSION created during bootstrap's config update
- path = get_test_config_dir('.HA_VERSION')
- if os.path.isfile(path):
- os.remove(path)
-
-
class TestCheckConfig(unittest.TestCase):
"""Tests for the homeassistant.scripts.check_config module."""
@@ -58,6 +50,9 @@ class TestCheckConfig(unittest.TestCase):
# Py34: AssertionError
asyncio.set_event_loop(asyncio.new_event_loop())
+ # Will allow seeing full diff
+ self.maxDiff = None
+
# pylint: disable=no-self-use,invalid-name
def test_config_platform_valid(self):
"""Test a valid platform setup."""
@@ -124,6 +119,9 @@ class TestCheckConfig(unittest.TestCase):
def test_component_platform_not_found(self):
"""Test errors if component or platform not found."""
+ # Make sure they don't exist
+ set_component('beer', None)
+ set_component('light.beer', None)
files = {
'badcomponent.yaml': BASE_CONFIG + 'beer:',
'badplatform.yaml': BASE_CONFIG + 'light:\n platform: beer',
@@ -162,7 +160,6 @@ class TestCheckConfig(unittest.TestCase):
'secrets.yaml': ('logger: debug\n'
'http_pw: abc123'),
}
- self.maxDiff = None
with patch_yaml_files(files):
config_path = get_test_config_dir('secret.yaml')
@@ -182,8 +179,6 @@ class TestCheckConfig(unittest.TestCase):
'login_attempts_threshold': -1,
'server_host': '0.0.0.0',
'server_port': 8123,
- 'ssl_certificate': None,
- 'ssl_key': None,
'trusted_networks': [],
'use_x_forwarded_for': False}},
'except': {},
@@ -212,3 +207,20 @@ class TestCheckConfig(unittest.TestCase):
assert res['components'] == {}
assert res['secret_cache'] == {}
assert res['secrets'] == {}
+
+ def test_bootstrap_error(self): \
+ # pylint: disable=no-self-use,invalid-name
+ """Test a valid platform setup."""
+ files = {
+ 'badbootstrap.yaml': BASE_CONFIG + 'automation: !include no.yaml',
+ }
+ with patch_yaml_files(files):
+ res = check_config.check(get_test_config_dir('badbootstrap.yaml'))
+ change_yaml_files(res)
+
+ err = res['except'].pop(check_config.ERROR_STR)
+ assert len(err) == 1
+ assert res['except'] == {}
+ assert res['components'] == {}
+ assert res['secret_cache'] == {}
+ assert res['secrets'] == {}
diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py
new file mode 100644
index 00000000000..3a1fe1d9d3e
--- /dev/null
+++ b/tests/test_config_entries.py
@@ -0,0 +1,397 @@
+"""Test the config manager."""
+import asyncio
+from unittest.mock import MagicMock, patch, mock_open
+
+import pytest
+import voluptuous as vol
+
+from homeassistant import config_entries, loader
+from homeassistant.setup import async_setup_component
+
+from tests.common import MockModule, mock_coro, MockConfigEntry
+
+
+@pytest.fixture
+def manager(hass):
+ """Fixture of a loaded config manager."""
+ manager = config_entries.ConfigEntries(hass, {})
+ manager._entries = []
+ hass.config_entries = manager
+ return manager
+
+
+@asyncio.coroutine
+def test_call_setup_entry(hass):
+ """Test we call .setup_entry."""
+ MockConfigEntry(domain='comp').add_to_hass(hass)
+
+ mock_setup_entry = MagicMock(return_value=mock_coro(True))
+
+ loader.set_component(
+ 'comp',
+ MockModule('comp', async_setup_entry=mock_setup_entry))
+
+ result = yield from async_setup_component(hass, 'comp', {})
+ assert result
+ assert len(mock_setup_entry.mock_calls) == 1
+
+
+@asyncio.coroutine
+def test_remove_entry(manager):
+ """Test that we can remove an entry."""
+ mock_unload_entry = MagicMock(return_value=mock_coro(True))
+
+ loader.set_component(
+ 'test',
+ MockModule('comp', async_unload_entry=mock_unload_entry))
+
+ MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager)
+ MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager)
+ MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager)
+
+ assert [item.entry_id for item in manager.async_entries()] == \
+ ['test1', 'test2', 'test3']
+
+ result = yield from manager.async_remove('test2')
+
+ assert result == {
+ 'require_restart': False
+ }
+ assert [item.entry_id for item in manager.async_entries()] == \
+ ['test1', 'test3']
+
+ assert len(mock_unload_entry.mock_calls) == 1
+
+
+@asyncio.coroutine
+def test_remove_entry_raises(manager):
+ """Test if a component raises while removing entry."""
+ @asyncio.coroutine
+ def mock_unload_entry(hass, entry):
+ """Mock unload entry function."""
+ raise Exception("BROKEN")
+
+ loader.set_component(
+ 'test',
+ MockModule('comp', async_unload_entry=mock_unload_entry))
+
+ MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager)
+ MockConfigEntry(domain='test', entry_id='test2').add_to_manager(manager)
+ MockConfigEntry(domain='test', entry_id='test3').add_to_manager(manager)
+
+ assert [item.entry_id for item in manager.async_entries()] == \
+ ['test1', 'test2', 'test3']
+
+ result = yield from manager.async_remove('test2')
+
+ assert result == {
+ 'require_restart': True
+ }
+ assert [item.entry_id for item in manager.async_entries()] == \
+ ['test1', 'test3']
+
+
+@asyncio.coroutine
+def test_add_entry_calls_setup_entry(hass, manager):
+ """Test we call setup_config_entry."""
+ mock_setup_entry = MagicMock(return_value=mock_coro(True))
+
+ loader.set_component(
+ 'comp',
+ MockModule('comp', async_setup_entry=mock_setup_entry))
+
+ class TestFlow(config_entries.ConfigFlowHandler):
+
+ VERSION = 1
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_create_entry(
+ title='title',
+ data={
+ 'token': 'supersecret'
+ })
+
+ with patch.dict(config_entries.HANDLERS, {'comp': TestFlow}):
+ yield from manager.flow.async_init('comp')
+ yield from hass.async_block_till_done()
+
+ assert len(mock_setup_entry.mock_calls) == 1
+ p_hass, p_entry = mock_setup_entry.mock_calls[0][1]
+
+ assert p_hass is hass
+ assert p_entry.data == {
+ 'token': 'supersecret'
+ }
+
+
+@asyncio.coroutine
+def test_entries_gets_entries(manager):
+ """Test entries are filtered by domain."""
+ MockConfigEntry(domain='test').add_to_manager(manager)
+ entry1 = MockConfigEntry(domain='test2')
+ entry1.add_to_manager(manager)
+ entry2 = MockConfigEntry(domain='test2')
+ entry2.add_to_manager(manager)
+
+ assert manager.async_entries('test2') == [entry1, entry2]
+
+
+@asyncio.coroutine
+def test_domains_gets_uniques(manager):
+ """Test we only return each domain once."""
+ MockConfigEntry(domain='test').add_to_manager(manager)
+ MockConfigEntry(domain='test2').add_to_manager(manager)
+ MockConfigEntry(domain='test2').add_to_manager(manager)
+ MockConfigEntry(domain='test').add_to_manager(manager)
+ MockConfigEntry(domain='test3').add_to_manager(manager)
+
+ assert manager.async_domains() == ['test', 'test2', 'test3']
+
+
+@asyncio.coroutine
+def test_saving_and_loading(hass):
+ """Test that we're saving and loading correctly."""
+ class TestFlow(config_entries.ConfigFlowHandler):
+ VERSION = 5
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_create_entry(
+ title='Test Title',
+ data={
+ 'token': 'abcd'
+ }
+ )
+
+ with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
+ yield from hass.config_entries.flow.async_init('test')
+
+ class Test2Flow(config_entries.ConfigFlowHandler):
+ VERSION = 3
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_create_entry(
+ title='Test 2 Title',
+ data={
+ 'username': 'bla'
+ }
+ )
+
+ json_path = 'homeassistant.util.json.open'
+
+ with patch('homeassistant.config_entries.HANDLERS.get',
+ return_value=Test2Flow), \
+ patch.object(config_entries, 'SAVE_DELAY', 0):
+ yield from hass.config_entries.flow.async_init('test')
+
+ with patch(json_path, mock_open(), create=True) as mock_write:
+ # To trigger the call_later
+ yield from asyncio.sleep(0, loop=hass.loop)
+ # To execute the save
+ yield from hass.async_block_till_done()
+
+ # Mock open calls are: open file, context enter, write, context leave
+ written = mock_write.mock_calls[2][1][0]
+
+ # Now load written data in new config manager
+ manager = config_entries.ConfigEntries(hass, {})
+
+ with patch('os.path.isfile', return_value=True), \
+ patch(json_path, mock_open(read_data=written), create=True):
+ yield from manager.async_load()
+
+ # Ensure same order
+ for orig, loaded in zip(hass.config_entries.async_entries(),
+ manager.async_entries()):
+ assert orig.version == loaded.version
+ assert orig.domain == loaded.domain
+ assert orig.title == loaded.title
+ assert orig.data == loaded.data
+ assert orig.source == loaded.source
+
+
+#######################
+# FLOW MANAGER TESTS #
+#######################
+
+@asyncio.coroutine
+def test_configure_reuses_handler_instance(manager):
+ """Test that we reuse instances."""
+ class TestFlow(config_entries.ConfigFlowHandler):
+ handle_count = 0
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ self.handle_count += 1
+ return self.async_show_form(
+ title=str(self.handle_count),
+ step_id='init')
+
+ with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
+ form = yield from manager.flow.async_init('test')
+ assert form['title'] == '1'
+ form = yield from manager.flow.async_configure(form['flow_id'])
+ assert form['title'] == '2'
+ assert len(manager.flow.async_progress()) == 1
+ assert len(manager.async_entries()) == 0
+
+
+@asyncio.coroutine
+def test_configure_two_steps(manager):
+ """Test that we reuse instances."""
+ class TestFlow(config_entries.ConfigFlowHandler):
+ VERSION = 1
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ if user_input is not None:
+ self.init_data = user_input
+ return self.async_step_second()
+ return self.async_show_form(
+ title='title',
+ step_id='init',
+ data_schema=vol.Schema([str])
+ )
+
+ @asyncio.coroutine
+ def async_step_second(self, user_input=None):
+ if user_input is not None:
+ return self.async_create_entry(
+ title='Test Entry',
+ data=self.init_data + user_input
+ )
+ return self.async_show_form(
+ title='title',
+ step_id='second',
+ data_schema=vol.Schema([str])
+ )
+
+ with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
+ form = yield from manager.flow.async_init('test')
+
+ with pytest.raises(vol.Invalid):
+ form = yield from manager.flow.async_configure(
+ form['flow_id'], 'INCORRECT-DATA')
+
+ form = yield from manager.flow.async_configure(
+ form['flow_id'], ['INIT-DATA'])
+ form = yield from manager.flow.async_configure(
+ form['flow_id'], ['SECOND-DATA'])
+ assert form['type'] == config_entries.RESULT_TYPE_CREATE_ENTRY
+ assert len(manager.flow.async_progress()) == 0
+ assert len(manager.async_entries()) == 1
+ entry = manager.async_entries()[0]
+ assert entry.domain == 'test'
+ assert entry.data == ['INIT-DATA', 'SECOND-DATA']
+
+
+@asyncio.coroutine
+def test_show_form(manager):
+ """Test that abort removes the flow from progress."""
+ schema = vol.Schema({
+ vol.Required('username'): str,
+ vol.Required('password'): str
+ })
+
+ class TestFlow(config_entries.ConfigFlowHandler):
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_show_form(
+ title='Hello form',
+ step_id='init',
+ description='test-description',
+ data_schema=schema,
+ errors={
+ 'username': 'Should be unique.'
+ }
+ )
+
+ with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
+ form = yield from manager.flow.async_init('test')
+ assert form['type'] == 'form'
+ assert form['title'] == 'Hello form'
+ assert form['description'] == 'test-description'
+ assert form['data_schema'] is schema
+ assert form['errors'] == {
+ 'username': 'Should be unique.'
+ }
+
+
+@asyncio.coroutine
+def test_abort_removes_instance(manager):
+ """Test that abort removes the flow from progress."""
+ class TestFlow(config_entries.ConfigFlowHandler):
+ is_new = True
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ old = self.is_new
+ self.is_new = False
+ return self.async_abort(reason=str(old))
+
+ with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
+ form = yield from manager.flow.async_init('test')
+ assert form['reason'] == 'True'
+ assert len(manager.flow.async_progress()) == 0
+ assert len(manager.async_entries()) == 0
+ form = yield from manager.flow.async_init('test')
+ assert form['reason'] == 'True'
+ assert len(manager.flow.async_progress()) == 0
+ assert len(manager.async_entries()) == 0
+
+
+@asyncio.coroutine
+def test_create_saves_data(manager):
+ """Test creating a config entry."""
+ class TestFlow(config_entries.ConfigFlowHandler):
+ VERSION = 5
+
+ @asyncio.coroutine
+ def async_step_init(self, user_input=None):
+ return self.async_create_entry(
+ title='Test Title',
+ data='Test Data'
+ )
+
+ with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
+ yield from manager.flow.async_init('test')
+ assert len(manager.flow.async_progress()) == 0
+ assert len(manager.async_entries()) == 1
+
+ entry = manager.async_entries()[0]
+ assert entry.version == 5
+ assert entry.domain == 'test'
+ assert entry.title == 'Test Title'
+ assert entry.data == 'Test Data'
+ assert entry.source == config_entries.SOURCE_USER
+
+
+@asyncio.coroutine
+def test_discovery_init_flow(manager):
+ """Test a flow initialized by discovery."""
+ class TestFlow(config_entries.ConfigFlowHandler):
+ VERSION = 5
+
+ @asyncio.coroutine
+ def async_step_discovery(self, info):
+ return self.async_create_entry(title=info['id'], data=info)
+
+ data = {
+ 'id': 'hello',
+ 'token': 'secret'
+ }
+
+ with patch.dict(config_entries.HANDLERS, {'test': TestFlow}):
+ yield from manager.flow.async_init(
+ 'test', source=config_entries.SOURCE_DISCOVERY, data=data)
+ assert len(manager.flow.async_progress()) == 0
+ assert len(manager.async_entries()) == 1
+
+ entry = manager.async_entries()[0]
+ assert entry.version == 5
+ assert entry.domain == 'test'
+ assert entry.title == 'hello'
+ assert entry.data == data
+ assert entry.source == config_entries.SOURCE_DISCOVERY
diff --git a/virtualization/vagrant/Vagrantfile b/virtualization/vagrant/Vagrantfile
index 21d5bd04adc..e50c4e6de00 100644
--- a/virtualization/vagrant/Vagrantfile
+++ b/virtualization/vagrant/Vagrantfile
@@ -2,7 +2,7 @@
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
- config.vm.box = "debian/contrib-jessie64"
+ config.vm.box = "debian/contrib-stretch64"
config.vm.synced_folder "../../", "/home-assistant"
config.vm.synced_folder "./config", "/root/.homeassistant"
config.vm.network "forwarded_port", guest: 8123, host: 8123
diff --git a/virtualization/vagrant/home-assistant@.service b/virtualization/vagrant/home-assistant@.service
index 8e520952db9..91b7307f30f 100644
--- a/virtualization/vagrant/home-assistant@.service
+++ b/virtualization/vagrant/home-assistant@.service
@@ -16,5 +16,8 @@ ExecStart=/usr/bin/hass --runner
SendSIGKILL=no
RestartForceExitStatus=100
+# on vagrant (vboxfs), disable sendfile https://www.virtualbox.org/ticket/9069
+Environment=AIOHTTP_NOSENDFILE=1
+
[Install]
WantedBy=multi-user.target
diff --git a/virtualization/vagrant/provision.sh b/virtualization/vagrant/provision.sh
index da5d48c6f18..d4ef4e0b446 100755
--- a/virtualization/vagrant/provision.sh
+++ b/virtualization/vagrant/provision.sh
@@ -105,7 +105,7 @@ main() {
vagrant up --provision; exit ;;
esac
# ...otherwise we assume it's the Vagrant provisioner
- if [ $(hostname) != "contrib-jessie" ]; then usage; exit; fi
+ if [ $(hostname) != "contrib-jessie" ] && [ $(hostname) != "contrib-stretch" ]; then usage; exit; fi
if ! [ -f $SETUP_DONE ]; then setup; fi
if [ -f $RESTART ]; then restart; fi
if [ -f $RUN_TESTS ]; then run_tests; fi