diff --git a/.coveragerc b/.coveragerc
index 84ca187fb3a..4b19519038f 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -145,6 +145,9 @@ omit =
homeassistant/components/maxcube.py
homeassistant/components/*/maxcube.py
+ homeassistant/components/mercedesme.py
+ homeassistant/components/*/mercedesme.py
+
homeassistant/components/mochad.py
homeassistant/components/*/mochad.py
@@ -377,6 +380,7 @@ omit =
homeassistant/components/fan/xiaomi_miio.py
homeassistant/components/feedreader.py
homeassistant/components/foursquare.py
+ homeassistant/components/goalfeed.py
homeassistant/components/ifttt.py
homeassistant/components/image_processing/dlib_face_detect.py
homeassistant/components/image_processing/dlib_face_identify.py
@@ -435,6 +439,7 @@ omit =
homeassistant/components/media_player/kodi.py
homeassistant/components/media_player/lg_netcast.py
homeassistant/components/media_player/liveboxplaytv.py
+ homeassistant/components/media_player/mediaroom.py
homeassistant/components/media_player/mpchc.py
homeassistant/components/media_player/mpd.py
homeassistant/components/media_player/nad.py
@@ -449,7 +454,6 @@ omit =
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rio.py
homeassistant/components/media_player/russound_rnet.py
- homeassistant/components/media_player/samsungtv.py
homeassistant/components/media_player/snapcast.py
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py
@@ -506,6 +510,7 @@ omit =
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote/harmony.py
homeassistant/components/remote/itach.py
+ homeassistant/components/remote/xiaomi_miio.py
homeassistant/components/scene/hunterdouglas_powerview.py
homeassistant/components/scene/lifx_cloud.py
homeassistant/components/sensor/airvisual.py
@@ -592,6 +597,7 @@ omit =
homeassistant/components/sensor/pi_hole.py
homeassistant/components/sensor/plex.py
homeassistant/components/sensor/pocketcasts.py
+ homeassistant/components/sensor/pollen.py
homeassistant/components/sensor/pushbullet.py
homeassistant/components/sensor/pvoutput.py
homeassistant/components/sensor/pyload.py
diff --git a/.gitignore b/.gitignore
index c8a6fed2ddf..0d55cae3c9d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,9 @@ Icon
# Thumbnails
._*
+# IntelliJ IDEA
.idea
+*.iml
# pytest
.cache
@@ -98,3 +100,6 @@ desktop.ini
/home-assistant.pyproj
/home-assistant.sln
/.vs/*
+
+# mypy
+/.mypy_cache/*
diff --git a/CODEOWNERS b/CODEOWNERS
index 9ec7ce0742c..6e088a84e5d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -41,7 +41,7 @@ homeassistant/components/*/zwave.py @home-assistant/z-wave
homeassistant/components/hassio.py @home-assistant/hassio
-# Indiviudal components
+# Individual components
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
homeassistant/components/camera/yi.py @bachya
homeassistant/components/climate/ephember.py @ttroy50
@@ -61,6 +61,7 @@ homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/gearbest.py @HerrHofrat
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/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git
diff --git a/docs/source/_templates/links.html b/docs/source/_templates/links.html
index 272809d1920..53a8d1e425d 100644
--- a/docs/source/_templates/links.html
+++ b/docs/source/_templates/links.html
@@ -2,5 +2,5 @@
Homepage
Community Forums
GitHub
- Gitter
+ Discord
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py
index b7301e13bea..1cf6ecf7b98 100644
--- a/homeassistant/__main__.py
+++ b/homeassistant/__main__.py
@@ -182,7 +182,8 @@ def check_pid(pid_file: str) -> None:
"""Check that Home Assistant is not already running."""
# Check pid file
try:
- pid = int(open(pid_file, 'r').readline())
+ with open(pid_file, 'r') as file:
+ pid = int(file.readline())
except IOError:
# PID File does not exist
return
@@ -204,7 +205,8 @@ def write_pid(pid_file: str) -> None:
"""Create a PID File."""
pid = os.getpid()
try:
- open(pid_file, 'w').write(str(pid))
+ with open(pid_file, 'w') as file:
+ file.write(str(pid))
except IOError:
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
sys.exit(1)
diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py
index 6db147a5f59..a1c6811afe7 100644
--- a/homeassistant/components/__init__.py
+++ b/homeassistant/components/__init__.py
@@ -133,7 +133,7 @@ def async_setup(hass, config):
# have been processed. If a service does not exist it causes a 10
# second delay while we're blocking waiting for a response.
# But services can be registered on other HA instances that are
- # listening to the bus too. So as a in between solution, we'll
+ # listening to the bus too. So as an in between solution, we'll
# block only if the service is defined in the current HA instance.
blocking = hass.services.has_service(domain, service.service)
diff --git a/homeassistant/components/alert.py b/homeassistant/components/alert.py
index 27d1625fd6b..eb941e22877 100644
--- a/homeassistant/components/alert.py
+++ b/homeassistant/components/alert.py
@@ -277,7 +277,7 @@ class Alert(ToggleEntity):
yield from self.async_update_ha_state()
@asyncio.coroutine
- def async_toggle(self):
+ def async_toggle(self, **kwargs):
"""Async toggle alert."""
if self._ack:
return self.async_turn_on()
diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py
index 2fae0b323a0..354a612c4b8 100644
--- a/homeassistant/components/alexa/smart_home.py
+++ b/homeassistant/components/alexa/smart_home.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
- CONF_UNIT_OF_MEASUREMENT)
+ CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
_LOGGER = logging.getLogger(__name__)
@@ -40,6 +40,7 @@ CONF_DESCRIPTION = 'description'
CONF_DISPLAY_CATEGORIES = 'display_categories'
HANDLERS = Registry()
+ENTITY_ADAPTERS = Registry()
class _DisplayCategory(object):
@@ -50,8 +51,8 @@ class _DisplayCategory(object):
# Describes a combination of devices set to a specific state, when the
# state change must occur in a specific order. For example, a "watch
- # Neflix" scene might require the: 1. TV to be powered on & 2. Input set to
- # HDMI1. Applies to Scenes
+ # Netflix" scene might require the: 1. TV to be powered on & 2. Input set
+ # to HDMI1. Applies to Scenes
ACTIVITY_TRIGGER = "ACTIVITY_TRIGGER"
# Indicates media devices with video or photo capabilities.
@@ -133,10 +134,36 @@ def _capability(interface,
return result
-class _EntityCapabilities(object):
+class _UnsupportedInterface(Exception):
+ """This entity does not support the requested Smart Home API interface."""
+
+
+class _UnsupportedProperty(Exception):
+ """This entity does not support the requested Smart Home API property."""
+
+
+class _AlexaEntity(object):
+ """An adaptation of an entity, expressed in Alexa's terms.
+
+ The API handlers should manipulate entities only through this interface.
+ """
+
def __init__(self, config, entity):
self.config = config
self.entity = entity
+ self.entity_conf = config.entity_config.get(entity.entity_id, {})
+
+ def friendly_name(self):
+ """Return the Alexa API friendly name."""
+ return self.entity_conf.get(CONF_NAME, self.entity.name)
+
+ def description(self):
+ """Return the Alexa API description."""
+ return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id)
+
+ def entity_id(self):
+ """Return the Alexa API entity id."""
+ return self.entity.entity_id.replace('.', '#')
def display_categories(self):
"""Return a list of display categories."""
@@ -154,17 +181,217 @@ class _EntityCapabilities(object):
"""
raise NotImplementedError
- def capabilities(self):
- """Return a list of supported capabilities.
+ def get_interface(self, capability):
+ """Return the given _AlexaInterface.
- If the returned list is empty, the entity will not be discovered.
+ Raises _UnsupportedInterface.
+ """
+ pass
- You might find _capability() useful.
+ def interfaces(self):
+ """Return a list of supported interfaces.
+
+ Used for discovery. The list should contain _AlexaInterface instances.
+ If the list is empty, this entity will not be discovered.
"""
raise NotImplementedError
-class _GenericCapabilities(_EntityCapabilities):
+class _AlexaInterface(object):
+ def __init__(self, entity):
+ self.entity = entity
+
+ def name(self):
+ """Return the Alexa API name of this interface."""
+ raise NotImplementedError
+
+ @staticmethod
+ def properties_supported():
+ """Return what properties this entity supports."""
+ return []
+
+ @staticmethod
+ def properties_proactively_reported():
+ """Return True if properties asynchronously reported."""
+ return False
+
+ @staticmethod
+ def properties_retrievable():
+ """Return True if properties can be retrieved."""
+ return False
+
+ @staticmethod
+ def get_property(name):
+ """Read and return a property.
+
+ Return value should be a dict, or raise _UnsupportedProperty.
+
+ Properties can also have a timeOfSample and uncertaintyInMilliseconds,
+ but returning those metadata is not yet implemented.
+ """
+ raise _UnsupportedProperty(name)
+
+ @staticmethod
+ def supports_deactivation():
+ """Applicable only to scenes."""
+ return None
+
+ def serialize_discovery(self):
+ """Serialize according to the Discovery API."""
+ result = {
+ 'type': 'AlexaInterface',
+ 'interface': self.name(),
+ 'version': '3',
+ 'properties': {
+ 'supported': self.properties_supported(),
+ 'proactivelyReported': self.properties_proactively_reported(),
+ 'retrievable': self.properties_retrievable(),
+ },
+ }
+
+ # pylint: disable=assignment-from-none
+ supports_deactivation = self.supports_deactivation()
+ if supports_deactivation is not None:
+ result['supportsDeactivation'] = supports_deactivation
+ return result
+
+ def serialize_properties(self):
+ """Return properties serialized for an API response."""
+ for prop in self.properties_supported():
+ prop_name = prop['name']
+ yield {
+ 'name': prop_name,
+ 'namespace': self.name(),
+ 'value': self.get_property(prop_name),
+ }
+
+
+class _AlexaPowerController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.PowerController'
+
+ def properties_supported(self):
+ return [{'name': 'powerState'}]
+
+ def properties_retrievable(self):
+ return True
+
+ def get_property(self, name):
+ if name != 'powerState':
+ raise _UnsupportedProperty(name)
+
+ if self.entity.state == STATE_ON:
+ return 'ON'
+ return 'OFF'
+
+
+class _AlexaLockController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.LockController'
+
+ def properties_supported(self):
+ return [{'name': 'lockState'}]
+
+ def properties_retrievable(self):
+ return True
+
+ def get_property(self, name):
+ if name != 'lockState':
+ raise _UnsupportedProperty(name)
+
+ if self.entity.state == STATE_LOCKED:
+ return 'LOCKED'
+ elif self.entity.state == STATE_UNLOCKED:
+ return 'UNLOCKED'
+ return 'JAMMED'
+
+
+class _AlexaSceneController(_AlexaInterface):
+ def __init__(self, entity, supports_deactivation):
+ _AlexaInterface.__init__(self, entity)
+ self.supports_deactivation = lambda: supports_deactivation
+
+ def name(self):
+ return 'Alexa.SceneController'
+
+
+class _AlexaBrightnessController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.BrightnessController'
+
+ def properties_supported(self):
+ return [{'name': 'brightness'}]
+
+ def properties_retrievable(self):
+ return True
+
+ def get_property(self, name):
+ if name != 'brightness':
+ raise _UnsupportedProperty(name)
+
+ return round(self.entity.attributes['brightness'] / 255.0 * 100)
+
+
+class _AlexaColorController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.ColorController'
+
+
+class _AlexaColorTemperatureController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.ColorTemperatureController'
+
+
+class _AlexaPercentageController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.PercentageController'
+
+
+class _AlexaSpeaker(_AlexaInterface):
+ def name(self):
+ return 'Alexa.Speaker'
+
+
+class _AlexaStepSpeaker(_AlexaInterface):
+ def name(self):
+ return 'Alexa.StepSpeaker'
+
+
+class _AlexaPlaybackController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.PlaybackController'
+
+
+class _AlexaInputController(_AlexaInterface):
+ def name(self):
+ return 'Alexa.InputController'
+
+
+class _AlexaTemperatureSensor(_AlexaInterface):
+ def name(self):
+ return 'Alexa.TemperatureSensor'
+
+ def properties_supported(self):
+ return [{'name': 'temperature'}]
+
+ def properties_retrievable(self):
+ return True
+
+ def get_property(self, name):
+ if name != 'temperature':
+ raise _UnsupportedProperty(name)
+
+ unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
+ return {
+ 'value': float(self.entity.state),
+ 'scale': API_TEMP_UNITS[unit],
+ }
+
+
+@ENTITY_ADAPTERS.register(alert.DOMAIN)
+@ENTITY_ADAPTERS.register(automation.DOMAIN)
+@ENTITY_ADAPTERS.register(input_boolean.DOMAIN)
+class _GenericCapabilities(_AlexaEntity):
"""A generic, on/off device.
The choice of last resort.
@@ -173,78 +400,87 @@ class _GenericCapabilities(_EntityCapabilities):
def default_display_categories(self):
return [_DisplayCategory.OTHER]
- def capabilities(self):
- return [_capability('Alexa.PowerController')]
+ def interfaces(self):
+ return [_AlexaPowerController(self.entity)]
-class _SwitchCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(switch.DOMAIN)
+class _SwitchCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.SWITCH]
- def capabilities(self):
- return [_capability('Alexa.PowerController')]
+ def interfaces(self):
+ return [_AlexaPowerController(self.entity)]
-class _CoverCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(cover.DOMAIN)
+class _CoverCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.DOOR]
- def capabilities(self):
- capabilities = [_capability('Alexa.PowerController')]
+ def interfaces(self):
+ yield _AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & cover.SUPPORT_SET_POSITION:
- capabilities.append(_capability('Alexa.PercentageController'))
- return capabilities
+ yield _AlexaPercentageController(self.entity)
-class _LightCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(light.DOMAIN)
+class _LightCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.LIGHT]
- def capabilities(self):
- capabilities = [_capability('Alexa.PowerController')]
+ def interfaces(self):
+ yield _AlexaPowerController(self.entity)
+
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & light.SUPPORT_BRIGHTNESS:
- capabilities.append(_capability('Alexa.BrightnessController'))
+ yield _AlexaBrightnessController(self.entity)
if supported & light.SUPPORT_RGB_COLOR:
- capabilities.append(_capability('Alexa.ColorController'))
+ yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_XY_COLOR:
- capabilities.append(_capability('Alexa.ColorController'))
+ yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP:
- capabilities.append(
- _capability('Alexa.ColorTemperatureController'))
- return capabilities
+ yield _AlexaColorTemperatureController(self.entity)
-class _FanCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(fan.DOMAIN)
+class _FanCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.OTHER]
- def capabilities(self):
- capabilities = [_capability('Alexa.PowerController')]
+ def interfaces(self):
+ yield _AlexaPowerController(self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & fan.SUPPORT_SET_SPEED:
- capabilities.append(_capability('Alexa.PercentageController'))
- return capabilities
+ yield _AlexaPercentageController(self.entity)
-class _LockCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(lock.DOMAIN)
+class _LockCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.SMARTLOCK]
- def capabilities(self):
- return [_capability('Alexa.LockController')]
+ def interfaces(self):
+ return [_AlexaLockController(self.entity)]
-class _MediaPlayerCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(media_player.DOMAIN)
+class _MediaPlayerCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.TV]
- def capabilities(self):
- capabilities = [_capability('Alexa.PowerController')]
+ def interfaces(self):
+ yield _AlexaPowerController(self.entity)
+
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.SUPPORT_VOLUME_SET:
- capabilities.append(_capability('Alexa.Speaker'))
+ yield _AlexaSpeaker(self.entity)
+
+ step_volume_features = (media_player.SUPPORT_VOLUME_MUTE |
+ media_player.SUPPORT_VOLUME_STEP)
+ if supported & step_volume_features:
+ yield _AlexaStepSpeaker(self.entity)
playback_features = (media_player.SUPPORT_PLAY |
media_player.SUPPORT_PAUSE |
@@ -252,89 +488,62 @@ class _MediaPlayerCapabilities(_EntityCapabilities):
media_player.SUPPORT_NEXT_TRACK |
media_player.SUPPORT_PREVIOUS_TRACK)
if supported & playback_features:
- capabilities.append(_capability('Alexa.PlaybackController'))
+ yield _AlexaPlaybackController(self.entity)
- return capabilities
+ if supported & media_player.SUPPORT_SELECT_SOURCE:
+ yield _AlexaInputController(self.entity)
-class _SceneCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(scene.DOMAIN)
+class _SceneCapabilities(_AlexaEntity):
+ def description(self):
+ # Required description as per Amazon Scene docs
+ scene_fmt = '{} (Scene connected via Home Assistant)'
+ return scene_fmt.format(_AlexaEntity.description(self))
+
def default_display_categories(self):
return [_DisplayCategory.SCENE_TRIGGER]
- def capabilities(self):
- return [_capability('Alexa.SceneController')]
+ def interfaces(self):
+ return [_AlexaSceneController(self.entity,
+ supports_deactivation=False)]
-class _ScriptCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(script.DOMAIN)
+class _ScriptCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.ACTIVITY_TRIGGER]
- def capabilities(self):
+ def interfaces(self):
can_cancel = bool(self.entity.attributes.get('can_cancel'))
- return [_capability('Alexa.SceneController',
- supports_deactivation=can_cancel)]
+ return [_AlexaSceneController(self.entity,
+ supports_deactivation=can_cancel)]
-class _GroupCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(group.DOMAIN)
+class _GroupCapabilities(_AlexaEntity):
def default_display_categories(self):
return [_DisplayCategory.SCENE_TRIGGER]
- def capabilities(self):
- return [_capability('Alexa.SceneController',
- supports_deactivation=True)]
+ def interfaces(self):
+ return [_AlexaSceneController(self.entity,
+ supports_deactivation=True)]
-class _SensorCapabilities(_EntityCapabilities):
+@ENTITY_ADAPTERS.register(sensor.DOMAIN)
+class _SensorCapabilities(_AlexaEntity):
def default_display_categories(self):
# although there are other kinds of sensors, all but temperature
# sensors are currently ignored.
return [_DisplayCategory.TEMPERATURE_SENSOR]
- def capabilities(self):
- capabilities = []
-
+ def interfaces(self):
attrs = self.entity.attributes
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
TEMP_FAHRENHEIT,
TEMP_CELSIUS,
):
- capabilities.append(_capability(
- 'Alexa.TemperatureSensor',
- retrievable=True,
- properties_supported=[{'name': 'temperature'}]))
-
- return capabilities
-
-
-class _UnknownEntityDomainError(Exception):
- pass
-
-
-def _capabilities_for_entity(config, entity):
- """Return an _EntityCapabilities appropriate for given entity.
-
- raises _UnknownEntityDomainError if the given domain is unsupported.
- """
- if entity.domain not in _CAPABILITIES_FOR_DOMAIN:
- raise _UnknownEntityDomainError()
- return _CAPABILITIES_FOR_DOMAIN[entity.domain](config, entity)
-
-
-_CAPABILITIES_FOR_DOMAIN = {
- alert.DOMAIN: _GenericCapabilities,
- automation.DOMAIN: _GenericCapabilities,
- cover.DOMAIN: _CoverCapabilities,
- fan.DOMAIN: _FanCapabilities,
- group.DOMAIN: _GroupCapabilities,
- input_boolean.DOMAIN: _GenericCapabilities,
- light.DOMAIN: _LightCapabilities,
- lock.DOMAIN: _LockCapabilities,
- media_player.DOMAIN: _MediaPlayerCapabilities,
- scene.DOMAIN: _SceneCapabilities,
- script.DOMAIN: _ScriptCapabilities,
- switch.DOMAIN: _SwitchCapabilities,
- sensor.DOMAIN: _SensorCapabilities,
-}
+ yield _AlexaTemperatureSensor(self.entity)
class _Cause(object):
@@ -468,7 +677,7 @@ def api_message(request,
}
}
- # If a correlation token exsits, add it to header / Need by Async requests
+ # If a correlation token exists, add it to header / Need by Async requests
token = request[API_HEADER].get('correlationToken')
if token:
response[API_EVENT][API_HEADER]['correlationToken'] = token
@@ -511,36 +720,26 @@ def async_api_discovery(hass, config, request):
entity.entity_id)
continue
- try:
- entity_capabilities = _capabilities_for_entity(config, entity)
- except _UnknownEntityDomainError:
+ if entity.domain not in ENTITY_ADAPTERS:
continue
-
- entity_conf = config.entity_config.get(entity.entity_id, {})
-
- friendly_name = entity_conf.get(CONF_NAME, entity.name)
- description = entity_conf.get(CONF_DESCRIPTION, entity.entity_id)
-
- # Required description as per Amazon Scene docs
- if entity.domain == scene.DOMAIN:
- scene_fmt = '{} (Scene connected via Home Assistant)'
- description = scene_fmt.format(description)
+ alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
endpoint = {
- 'displayCategories': entity_capabilities.display_categories(),
+ 'displayCategories': alexa_entity.display_categories(),
'additionalApplianceDetails': {},
- 'endpointId': entity.entity_id.replace('.', '#'),
- 'friendlyName': friendly_name,
- 'description': description,
+ 'endpointId': alexa_entity.entity_id(),
+ 'friendlyName': alexa_entity.friendly_name(),
+ 'description': alexa_entity.description(),
'manufacturerName': 'Home Assistant',
}
- alexa_capabilities = entity_capabilities.capabilities()
- if not alexa_capabilities:
+ endpoint['capabilities'] = [
+ i.serialize_discovery() for i in alexa_entity.interfaces()]
+
+ if not endpoint['capabilities']:
_LOGGER.debug("Not exposing %s because it has no capabilities",
entity.entity_id)
continue
- endpoint['capabilities'] = alexa_capabilities
discovery_endpoints.append(endpoint)
return api_message(
@@ -624,7 +823,7 @@ def async_api_set_brightness(hass, config, request, entity):
@extract_entity
@asyncio.coroutine
def async_api_adjust_brightness(hass, config, request, entity):
- """Process a adjust brightness request."""
+ """Process an adjust brightness request."""
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
# read current state
@@ -812,7 +1011,7 @@ def async_api_set_percentage(hass, config, request, entity):
@extract_entity
@asyncio.coroutine
def async_api_adjust_percentage(hass, config, request, entity):
- """Process a adjust percentage request."""
+ """Process an adjust percentage request."""
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
@@ -873,7 +1072,7 @@ def async_api_lock(hass, config, request, entity):
@extract_entity
@asyncio.coroutine
def async_api_unlock(hass, config, request, entity):
- """Process a unlock request."""
+ """Process an unlock request."""
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False)
@@ -900,11 +1099,46 @@ def async_api_set_volume(hass, config, request, entity):
return api_message(request)
+@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
+@extract_entity
+@asyncio.coroutine
+def async_api_select_input(hass, config, request, entity):
+ """Process a set input request."""
+ media_input = request[API_PAYLOAD]['input']
+
+ # attempt to map the ALL UPPERCASE payload name to a source
+ source_list = entity.attributes[media_player.ATTR_INPUT_SOURCE_LIST] or []
+ for source in source_list:
+ # response will always be space separated, so format the source in the
+ # most likely way to find a match
+ formatted_source = source.lower().replace('-', ' ').replace('_', ' ')
+ if formatted_source in media_input.lower():
+ media_input = source
+ break
+ else:
+ msg = 'failed to map input {} to a media source on {}'.format(
+ media_input, entity.entity_id)
+ _LOGGER.error(msg)
+ return api_error(
+ request, error_type='INVALID_VALUE', error_message=msg)
+
+ data = {
+ ATTR_ENTITY_ID: entity.entity_id,
+ media_player.ATTR_INPUT_SOURCE: media_input,
+ }
+
+ yield from hass.services.async_call(
+ entity.domain, media_player.SERVICE_SELECT_SOURCE,
+ data, blocking=False)
+
+ return api_message(request)
+
+
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@extract_entity
@asyncio.coroutine
def async_api_adjust_volume(hass, config, request, entity):
- """Process a adjust volume request."""
+ """Process an adjust volume request."""
volume_delta = int(request[API_PAYLOAD]['volume'])
current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
@@ -929,6 +1163,30 @@ def async_api_adjust_volume(hass, config, request, entity):
return api_message(request)
+@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
+@extract_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]['volume'] / 100), 2)
+
+ current_level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL)
+
+ volume = current_level + volume_step
+
+ 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)
+
+ return api_message(request)
+
+
+@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@extract_entity
@asyncio.coroutine
@@ -1033,18 +1291,13 @@ def async_api_previous(hass, config, request, entity):
@asyncio.coroutine
def async_api_reportstate(hass, config, request, entity):
"""Process a ReportState request."""
- unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
- temp_property = {
- 'namespace': 'Alexa.TemperatureSensor',
- 'name': 'temperature',
- 'value': {
- 'value': float(entity.state),
- 'scale': API_TEMP_UNITS[unit],
- },
- }
+ alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
+ properties = []
+ for interface in alexa_entity.interfaces():
+ properties.extend(interface.serialize_properties())
return api_message(
request,
name='StateReport',
- context={'properties': [temp_property]}
+ context={'properties': properties}
)
diff --git a/homeassistant/components/android_ip_webcam.py b/homeassistant/components/android_ip_webcam.py
index 2883fca9ab6..5fbd5a764e9 100644
--- a/homeassistant/components/android_ip_webcam.py
+++ b/homeassistant/components/android_ip_webcam.py
@@ -251,7 +251,7 @@ class AndroidIPCamEntity(Entity):
"""The Android device running IP Webcam."""
def __init__(self, host, ipcam):
- """Initialize the data oject."""
+ """Initialize the data object."""
self._host = host
self._ipcam = ipcam
diff --git a/homeassistant/components/apcupsd.py b/homeassistant/components/apcupsd.py
index dd29e7d602f..7e2b4cda28f 100644
--- a/homeassistant/components/apcupsd.py
+++ b/homeassistant/components/apcupsd.py
@@ -66,7 +66,7 @@ class APCUPSdData(object):
"""
def __init__(self, host, port):
- """Initialize the data oject."""
+ """Initialize the data object."""
from apcaccess import status
self._host = host
self._port = port
diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py
index a928ed108c9..7e51ec8c045 100644
--- a/homeassistant/components/arlo.py
+++ b/homeassistant/components/arlo.py
@@ -47,7 +47,7 @@ def setup(hass, config):
return False
hass.data[DATA_ARLO] = arlo
except (ConnectTimeout, HTTPError) as ex:
- _LOGGER.error("Unable to connect to Netgar Arlo: %s", str(ex))
+ _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}
'
'You will need to restart hass after fixing.'
diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py
index 5e69dcc9109..1d0849b255e 100644
--- a/homeassistant/components/binary_sensor/bloomsky.py
+++ b/homeassistant/components/binary_sensor/bloomsky.py
@@ -50,7 +50,6 @@ class BloomSkySensor(BinarySensorDevice):
self._device_id = device['DeviceID']
self._sensor_name = sensor_name
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
- self._unique_id = 'bloomsky_binary_sensor {}'.format(self._name)
self._state = None
@property
@@ -58,11 +57,6 @@ class BloomSkySensor(BinarySensorDevice):
"""Return the name of the BloomSky device and this sensor."""
return self._name
- @property
- def unique_id(self):
- """Return the unique ID for this sensor."""
- return self._unique_id
-
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py
index 3c02dfb3508..0d7c3e086bb 100644
--- a/homeassistant/components/binary_sensor/deconz.py
+++ b/homeassistant/components/binary_sensor/deconz.py
@@ -65,6 +65,11 @@ class DeconzBinarySensor(BinarySensorDevice):
"""Return the name of the sensor."""
return self._sensor.name
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this sensor."""
+ return self._sensor.uniqueid
+
@property
def device_class(self):
"""Return the class of the sensor."""
diff --git a/homeassistant/components/binary_sensor/ecobee.py b/homeassistant/components/binary_sensor/ecobee.py
index 214efb870b9..15efa21b226 100644
--- a/homeassistant/components/binary_sensor/ecobee.py
+++ b/homeassistant/components/binary_sensor/ecobee.py
@@ -50,11 +50,6 @@ class EcobeeBinarySensor(BinarySensorDevice):
"""Return the status of the sensor."""
return self._state == 'true'
- @property
- def unique_id(self):
- """Return the unique ID of this sensor."""
- return "binary_sensor_ecobee_{}_{}".format(self._name, self.index)
-
@property
def device_class(self):
"""Return the class of this sensor, from DEVICE_CLASSES."""
diff --git a/homeassistant/components/binary_sensor/ffmpeg_motion.py b/homeassistant/components/binary_sensor/ffmpeg_motion.py
index 47b1be988bf..75a9fa1d046 100644
--- a/homeassistant/components/binary_sensor/ffmpeg_motion.py
+++ b/homeassistant/components/binary_sensor/ffmpeg_motion.py
@@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
- """Set up the FFmpeg binary moition sensor."""
+ """Set up the FFmpeg binary motion sensor."""
manager = hass.data[DATA_FFMPEG]
if not manager.async_run_test(config.get(CONF_INPUT)):
diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py
index df488cc0ed6..ec64bdf07b8 100644
--- a/homeassistant/components/binary_sensor/hikvision.py
+++ b/homeassistant/components/binary_sensor/hikvision.py
@@ -118,7 +118,7 @@ class HikvisionData(object):
"""Hikvision device event stream object."""
def __init__(self, hass, url, port, name, username, password):
- """Initialize the data oject."""
+ """Initialize the data object."""
from pyhik.hikvision import HikCamera
self._url = url
self._port = port
@@ -212,7 +212,7 @@ class HikvisionBinarySensor(BinarySensorDevice):
@property
def unique_id(self):
"""Return an unique ID."""
- return '{}.{}'.format(self.__class__, self._id)
+ return self._id
@property
def is_on(self):
diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/binary_sensor/hive.py
index 6223ebe50a1..2d4cbd8d070 100644
--- a/homeassistant/components/binary_sensor/hive.py
+++ b/homeassistant/components/binary_sensor/hive.py
@@ -59,5 +59,5 @@ class HiveBinarySensorEntity(BinarySensorDevice):
self.node_device_type)
def update(self):
- """Update all Node data frome Hive."""
+ """Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
diff --git a/homeassistant/components/binary_sensor/ihc.py b/homeassistant/components/binary_sensor/ihc.py
index 14e45f88cf1..04f8c0d00dd 100644
--- a/homeassistant/components/binary_sensor/ihc.py
+++ b/homeassistant/components/binary_sensor/ihc.py
@@ -69,7 +69,8 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice):
"""
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
- sensor_type: str, inverting: bool, product: Element=None):
+ sensor_type: str, inverting: bool,
+ 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/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py
index 0702ce8bb9e..1874be6ec41 100644
--- a/homeassistant/components/binary_sensor/insteon_plm.py
+++ b/homeassistant/components/binary_sensor/insteon_plm.py
@@ -83,5 +83,5 @@ class InsteonPLMBinarySensorDevice(BinarySensorDevice):
@callback
def async_binarysensor_update(self, message):
"""Receive notification from transport that new data exists."""
- _LOGGER.info("Received update calback from PLM for %s", self._address)
+ _LOGGER.info("Received update callback from PLM for %s", self._address)
self._hass.async_add_job(self.async_update_ha_state())
diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py
index 89d9b7e5c8f..4dddb9bdbef 100644
--- a/homeassistant/components/binary_sensor/isy994.py
+++ b/homeassistant/components/binary_sensor/isy994.py
@@ -67,8 +67,8 @@ def setup_platform(hass, config: ConfigType,
elif subnode_id == 2:
parent_device.add_negative_node(node)
elif device_type == 'moisture':
- # Moisure nodes have a subnode 2, but we ignore it because it's
- # just the inverse of the primary node.
+ # Moisture nodes have a subnode 2, but we ignore it because
+ # it's just the inverse of the primary node.
if subnode_id == 4:
# Heartbeat node
device = ISYBinarySensorHeartbeat(node, parent_device)
diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py
new file mode 100644
index 00000000000..a6c8da56ce8
--- /dev/null
+++ b/homeassistant/components/binary_sensor/mercedesme.py
@@ -0,0 +1,94 @@
+"""
+Support for Mercedes cars with Mercedes ME.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/binary_sensor.mercedesme/
+"""
+import logging
+import datetime
+
+from homeassistant.components.binary_sensor import (BinarySensorDevice)
+from homeassistant.components.mercedesme import (
+ DATA_MME, MercedesMeEntity, BINARY_SENSORS)
+
+DEPENDENCIES = ['mercedesme']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup the sensor platform."""
+ data = hass.data[DATA_MME].data
+
+ if not data.cars:
+ _LOGGER.error("No cars found. Check component log.")
+ return
+
+ devices = []
+ for car in data.cars:
+ for key, value in sorted(BINARY_SENSORS.items()):
+ devices.append(MercedesMEBinarySensor(
+ data, key, value[0], car["vin"], None))
+
+ add_devices(devices, True)
+
+
+class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice):
+ """Representation of a Sensor."""
+
+ @property
+ def is_on(self):
+ """Return the state of the binary sensor."""
+ return self._state
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._internal_name == "windowsClosed":
+ return {
+ "window_front_left": self._car["windowStatusFrontLeft"],
+ "window_front_right": self._car["windowStatusFrontRight"],
+ "window_rear_left": self._car["windowStatusRearLeft"],
+ "window_rear_right": self._car["windowStatusRearRight"],
+ "original_value": self._car[self._internal_name],
+ "last_update": datetime.datetime.fromtimestamp(
+ self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
+ "car": self._car["license"]
+ }
+ elif self._internal_name == "tireWarningLight":
+ return {
+ "front_right_tire_pressure_kpa":
+ self._car["frontRightTirePressureKpa"],
+ "front_left_tire_pressure_kpa":
+ self._car["frontLeftTirePressureKpa"],
+ "rear_right_tire_pressure_kpa":
+ self._car["rearRightTirePressureKpa"],
+ "rear_left_tire_pressure_kpa":
+ self._car["rearLeftTirePressureKpa"],
+ "original_value": self._car[self._internal_name],
+ "last_update": datetime.datetime.fromtimestamp(
+ self._car["lastUpdate"]
+ ).strftime('%Y-%m-%d %H:%M:%S'),
+ "car": self._car["license"],
+ }
+ return {
+ "original_value": self._car[self._internal_name],
+ "last_update": datetime.datetime.fromtimestamp(
+ self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
+ "car": self._car["license"]
+ }
+
+ def update(self):
+ """Fetch new state data for the sensor."""
+ self._car = next(
+ car for car in self._data.cars if car["vin"] == self._vin)
+
+ if self._internal_name == "windowsClosed":
+ self._state = bool(self._car[self._internal_name] == "CLOSED")
+ elif self._internal_name == "tireWarningLight":
+ self._state = bool(self._car[self._internal_name] != "INACTIVE")
+ else:
+ self._state = self._car[self._internal_name] is True
+
+ _LOGGER.debug("Updated %s Value: %s IsOn: %s",
+ self._internal_name, self._state, self.is_on)
diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py
index 983c879338d..e033355f655 100644
--- a/homeassistant/components/binary_sensor/mqtt.py
+++ b/homeassistant/components/binary_sensor/mqtt.py
@@ -14,8 +14,8 @@ import homeassistant.components.mqtt as mqtt
from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import (
- CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF,
- CONF_DEVICE_CLASS)
+ CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON,
+ CONF_PAYLOAD_OFF, CONF_DEVICE_CLASS)
from homeassistant.components.mqtt import (
CONF_STATE_TOPIC, CONF_AVAILABILITY_TOPIC, CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability)
@@ -24,8 +24,10 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'MQTT Binary sensor'
+
DEFAULT_PAYLOAD_OFF = 'OFF'
DEFAULT_PAYLOAD_ON = 'ON'
+DEFAULT_FORCE_UPDATE = False
DEPENDENCIES = ['mqtt']
@@ -34,6 +36,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
@@ -53,6 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
config.get(CONF_AVAILABILITY_TOPIC),
config.get(CONF_DEVICE_CLASS),
config.get(CONF_QOS),
+ config.get(CONF_FORCE_UPDATE),
config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_PAYLOAD_AVAILABLE),
@@ -65,7 +69,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
"""Representation a binary sensor that is updated by MQTT."""
def __init__(self, name, state_topic, availability_topic, device_class,
- qos, payload_on, payload_off, payload_available,
+ qos, force_update, payload_on, payload_off, payload_available,
payload_not_available, value_template):
"""Initialize the MQTT binary sensor."""
super().__init__(availability_topic, qos, payload_available,
@@ -77,6 +81,7 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
self._payload_on = payload_on
self._payload_off = payload_off
self._qos = qos
+ self._force_update = force_update
self._template = value_template
@asyncio.coroutine
@@ -94,6 +99,11 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
self._state = True
elif payload == self._payload_off:
self._state = False
+ else: # Payload is not for this entity
+ _LOGGER.warning('No matching payload found'
+ ' for entity: %s with state_topic: %s',
+ self._name, self._state_topic)
+ return
self.async_schedule_update_ha_state()
@@ -119,3 +129,8 @@ class MqttBinarySensor(MqttAvailability, BinarySensorDevice):
def device_class(self):
"""Return the class of this sensor."""
return self._device_class
+
+ @property
+ def force_update(self):
+ """Force update."""
+ return self._force_update
diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py
index e597f1d0bbe..4d8aaa7d0d9 100644
--- a/homeassistant/components/binary_sensor/netatmo.py
+++ b/homeassistant/components/binary_sensor/netatmo.py
@@ -131,10 +131,8 @@ class NetatmoBinarySensor(BinarySensorDevice):
self._name += ' / ' + module_name
self._sensor_name = sensor
self._name += ' ' + sensor
- camera_id = data.camera_data.cameraByName(
+ self._unique_id = data.camera_data.cameraByName(
camera=camera_name, home=home)['id']
- self._unique_id = "Netatmo_binary_sensor {0} - {1}".format(
- self._name, camera_id)
self._cameratype = camera_type
self._state = None
diff --git a/homeassistant/components/binary_sensor/pilight.py b/homeassistant/components/binary_sensor/pilight.py
index c4c26d3a122..d2c46c795a8 100644
--- a/homeassistant/components/binary_sensor/pilight.py
+++ b/homeassistant/components/binary_sensor/pilight.py
@@ -97,7 +97,7 @@ class PilightBinarySensor(BinarySensorDevice):
def _handle_code(self, call):
"""Handle received code by the pilight-daemon.
- If the code matches the defined playload
+ If the code matches the defined payload
of this sensor the sensor state is changed accordingly.
"""
# Check if received code matches defined playoad
@@ -162,10 +162,10 @@ class PilightTriggerSensor(BinarySensorDevice):
def _handle_code(self, call):
"""Handle received code by the pilight-daemon.
- If the code matches the defined playload
+ If the code matches the defined payload
of this sensor the sensor state is changed accordingly.
"""
- # Check if received code matches defined playoad
+ # Check if received code matches defined payload
# True if payload is contained in received code dict
payload_ok = True
for key in self._payload:
diff --git a/homeassistant/components/binary_sensor/raincloud.py b/homeassistant/components/binary_sensor/raincloud.py
index f75f7644c4e..288b46c2370 100644
--- a/homeassistant/components/binary_sensor/raincloud.py
+++ b/homeassistant/components/binary_sensor/raincloud.py
@@ -39,7 +39,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensor_type))
else:
- # create an sensor for each zone managed by faucet
+ # create a sensor for each zone managed by faucet
for zone in raincloud.controller.faucet.zones:
sensors.append(RainCloudBinarySensor(zone, sensor_type))
diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py
index 92213a9b590..68ffbf77af2 100644
--- a/homeassistant/components/binary_sensor/template.py
+++ b/homeassistant/components/binary_sensor/template.py
@@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA)
from homeassistant.const import (
ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE,
+ CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE,
CONF_SENSORS, CONF_DEVICE_CLASS, EVENT_HOMEASSISTANT_START)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
@@ -29,6 +30,8 @@ CONF_DELAY_OFF = 'delay_off'
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(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
@@ -38,11 +41,6 @@ SENSOR_SCHEMA = vol.Schema({
vol.All(cv.time_period, cv.positive_timedelta),
})
-SENSOR_SCHEMA = vol.All(
- cv.deprecated(ATTR_ENTITY_ID),
- SENSOR_SCHEMA,
-)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
})
@@ -55,6 +53,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
for device, device_config in config[CONF_SENSORS].items():
value_template = device_config[CONF_VALUE_TEMPLATE]
+ icon_template = device_config.get(CONF_ICON_TEMPLATE)
+ entity_picture_template = device_config.get(
+ CONF_ENTITY_PICTURE_TEMPLATE)
entity_ids = (device_config.get(ATTR_ENTITY_ID) or
value_template.extract_entities())
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
@@ -65,10 +66,17 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if value_template is not None:
value_template.hass = hass
+ if icon_template is not None:
+ icon_template.hass = hass
+
+ if entity_picture_template is not None:
+ entity_picture_template.hass = hass
+
sensors.append(
BinarySensorTemplate(
hass, device, friendly_name, device_class, value_template,
- entity_ids, delay_on, delay_off)
+ icon_template, entity_picture_template, entity_ids,
+ delay_on, delay_off)
)
if not sensors:
_LOGGER.error("No sensors added")
@@ -82,7 +90,8 @@ class BinarySensorTemplate(BinarySensorDevice):
"""A virtual binary sensor that triggers from another sensor."""
def __init__(self, hass, device, friendly_name, device_class,
- value_template, entity_ids, delay_on, delay_off):
+ value_template, icon_template, entity_picture_template,
+ entity_ids, delay_on, delay_off):
"""Initialize the Template binary sensor."""
self.hass = hass
self.entity_id = async_generate_entity_id(
@@ -91,6 +100,10 @@ class BinarySensorTemplate(BinarySensorDevice):
self._device_class = device_class
self._template = value_template
self._state = None
+ self._icon_template = icon_template
+ self._entity_picture_template = entity_picture_template
+ self._icon = None
+ self._entity_picture = None
self._entities = entity_ids
self._delay_on = delay_on
self._delay_off = delay_off
@@ -119,6 +132,16 @@ class BinarySensorTemplate(BinarySensorDevice):
"""Return the name of the sensor."""
return self._name
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return self._icon
+
+ @property
+ def entity_picture(self):
+ """Return the entity_picture to use in the frontend, if any."""
+ return self._entity_picture
+
@property
def is_on(self):
"""Return true if sensor is on."""
@@ -137,8 +160,9 @@ class BinarySensorTemplate(BinarySensorDevice):
@callback
def _async_render(self):
"""Get the state of template."""
+ state = None
try:
- return self._template.async_render().lower() == 'true'
+ state = (self._template.async_render().lower() == 'true')
except TemplateError as ex:
if ex.args and ex.args[0].startswith(
"UndefinedError: 'None' has no attribute"):
@@ -148,6 +172,29 @@ class BinarySensorTemplate(BinarySensorDevice):
return
_LOGGER.error("Could not render template %s: %s", self._name, ex)
+ for property_name, template in (
+ ('_icon', self._icon_template),
+ ('_entity_picture', self._entity_picture_template)):
+ if template is None:
+ continue
+
+ try:
+ setattr(self, property_name, template.async_render())
+ except TemplateError as ex:
+ friendly_property_name = property_name[1:].replace('_', ' ')
+ if ex.args and ex.args[0].startswith(
+ "UndefinedError: 'None' has no attribute"):
+ # Common during HA startup - so just a warning
+ _LOGGER.warning('Could not render %s template %s,'
+ ' the state is unknown.',
+ friendly_property_name, self._name)
+ else:
+ _LOGGER.error('Could not render %s template %s: %s',
+ friendly_property_name, self._name, ex)
+ return state
+
+ return state
+
@callback
def async_check_state(self):
"""Update the state from the template."""
diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py
index 36e8868661d..79c36fb2ef2 100644
--- a/homeassistant/components/binary_sensor/threshold.py
+++ b/homeassistant/components/binary_sensor/threshold.py
@@ -126,11 +126,12 @@ class ThresholdSensor(BinarySensorDevice):
@property
def threshold_type(self):
"""Return the type of threshold this sensor represents."""
- if self._threshold_lower and self._threshold_upper:
+ if self._threshold_lower is not None and \
+ self._threshold_upper is not None:
return TYPE_RANGE
- elif self._threshold_lower:
+ elif self._threshold_lower is not None:
return TYPE_LOWER
- elif self._threshold_upper:
+ elif self._threshold_upper is not None:
return TYPE_UPPER
@property
diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py
index 1ec9e703eab..cc1f602d871 100644
--- a/homeassistant/components/binary_sensor/wemo.py
+++ b/homeassistant/components/binary_sensor/wemo.py
@@ -58,11 +58,11 @@ class WemoBinarySensor(BinarySensorDevice):
@property
def unique_id(self):
"""Return the id of this WeMo device."""
- return '{}.{}'.format(self.__class__, self.wemo.serialnumber)
+ return self.wemo.serialnumber
@property
def name(self):
- """Return the name of the sevice if any."""
+ """Return the name of the service if any."""
return self.wemo.name
@property
diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py
index ad7c29badf9..de7896e595b 100644
--- a/homeassistant/components/binary_sensor/zha.py
+++ b/homeassistant/components/binary_sensor/zha.py
@@ -32,7 +32,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if discovery_info is None:
return
- from bellows.zigbee.zcl.clusters.security import IasZone
+ from zigpy.zcl.clusters.security import IasZone
in_clusters = discovery_info['in_clusters']
@@ -63,7 +63,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
"""Initialize the ZHA binary sensor."""
super().__init__(**kwargs)
self._device_class = device_class
- from bellows.zigbee.zcl.clusters.security import IasZone
+ from zigpy.zcl.clusters.security import IasZone
self._ias_zone_cluster = self._in_clusters[IasZone.cluster_id]
@property
@@ -78,7 +78,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
"""Return the class of this device, from component DEVICE_CLASSES."""
return self._device_class
- def cluster_command(self, aps_frame, tsn, command_id, args):
+ def cluster_command(self, tsn, command_id, args):
"""Handle commands received to this cluster."""
if command_id == 0:
self._state = args[0] & 3
diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py
index 8b2401aa589..ba798ce7902 100644
--- a/homeassistant/components/calendar/caldav.py
+++ b/homeassistant/components/calendar/caldav.py
@@ -174,7 +174,7 @@ class WebDavCalendarData(object):
@staticmethod
def is_matching(vevent, search):
- """Return if the event matches the filter critera."""
+ """Return if the event matches the filter criteria."""
if search is None:
return True
diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py
index 81191e3025e..f1c80612f3b 100644
--- a/homeassistant/components/calendar/todoist.py
+++ b/homeassistant/components/calendar/todoist.py
@@ -411,7 +411,7 @@ class TodoistProjectData(object):
The "best" event is determined by the following criteria:
* A proposed event must not be completed
- * A proposed event must have a end date (otherwise we go with
+ * A proposed event must have an end date (otherwise we go with
the event at index 0, selected above)
* A proposed event must be on the same day or earlier as our
current event
diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py
index 1bb88050b2f..a531d25841b 100644
--- a/homeassistant/components/camera/__init__.py
+++ b/homeassistant/components/camera/__init__.py
@@ -91,13 +91,13 @@ def async_snapshot(hass, filename, entity_id=None):
@bind_hass
@asyncio.coroutine
def async_get_image(hass, entity_id, timeout=10):
- """Fetch a image from a camera entity."""
+ """Fetch an image from a camera entity."""
websession = async_get_clientsession(hass)
state = hass.states.get(entity_id)
if state is None:
raise HomeAssistantError(
- "No entity '{0}' for grab a image".format(entity_id))
+ "No entity '{0}' for grab an image".format(entity_id))
url = "{0}{1}".format(
hass.config.api.base_url,
diff --git a/homeassistant/components/camera/abode.py b/homeassistant/components/camera/abode.py
index 3c0c0a54e0e..ee739810a61 100644
--- a/homeassistant/components/camera/abode.py
+++ b/homeassistant/components/camera/abode.py
@@ -22,7 +22,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90)
_LOGGER = logging.getLogger(__name__)
-def setup_platform(hass, config, add_devices, discoveryy_info=None):
+def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Abode camera devices."""
import abodepy.helpers.constants as CONST
import abodepy.helpers.timeline as TIMELINE
diff --git a/homeassistant/components/camera/canary.py b/homeassistant/components/camera/canary.py
index 302758eee94..a230e0f6d4a 100644
--- a/homeassistant/components/camera/canary.py
+++ b/homeassistant/components/camera/canary.py
@@ -4,19 +4,30 @@ Support for Canary camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.canary/
"""
+import asyncio
import logging
+from datetime import timedelta
-import requests
+import voluptuous as vol
-from homeassistant.components.camera import Camera
+from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
+from homeassistant.components.ffmpeg import DATA_FFMPEG
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
+from homeassistant.util import Throttle
-DEPENDENCIES = ['canary']
+CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
+
+DEPENDENCIES = ['canary', 'ffmpeg']
_LOGGER = logging.getLogger(__name__)
-ATTR_MOTION_START_TIME = "motion_start_time"
-ATTR_MOTION_END_TIME = "motion_end_time"
+MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
+})
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -25,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
devices = []
for location in data.locations:
- entries = data.get_motion_entries(location.location_id)
- if entries:
- devices.append(CanaryCamera(data, location.location_id,
- DEFAULT_TIMEOUT))
+ for device in location.devices:
+ if device.is_online:
+ devices.append(
+ CanaryCamera(hass, data, location, device, DEFAULT_TIMEOUT,
+ config.get(CONF_FFMPEG_ARGUMENTS)))
add_devices(devices, True)
@@ -36,60 +48,65 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CanaryCamera(Camera):
"""An implementation of a Canary security camera."""
- def __init__(self, data, location_id, timeout):
+ def __init__(self, hass, data, location, device, timeout, ffmpeg_args):
"""Initialize a Canary security camera."""
super().__init__()
+
+ self._ffmpeg = hass.data[DATA_FFMPEG]
+ self._ffmpeg_arguments = ffmpeg_args
self._data = data
- self._location_id = location_id
+ self._location = location
+ self._device = device
self._timeout = timeout
-
- self._location = None
- self._motion_entry = None
- self._image_content = None
-
- def camera_image(self):
- """Update the status of the camera and return bytes of camera image."""
- self.update()
- return self._image_content
+ self._live_stream_session = None
@property
def name(self):
"""Return the name of this device."""
- return self._location.name
+ return self._device.name
@property
def is_recording(self):
"""Return true if the device is recording."""
return self._location.is_recording
- @property
- def device_state_attributes(self):
- """Return device specific state attributes."""
- if self._motion_entry is None:
- return None
-
- return {
- ATTR_MOTION_START_TIME: self._motion_entry.start_time,
- ATTR_MOTION_END_TIME: self._motion_entry.end_time,
- }
-
- def update(self):
- """Update the status of the camera."""
- self._data.update()
- self._location = self._data.get_location(self._location_id)
-
- entries = self._data.get_motion_entries(self._location_id)
- if entries:
- current = entries[0]
- previous = self._motion_entry
-
- if previous is None or previous.entry_id != current.entry_id:
- self._motion_entry = current
- self._image_content = requests.get(
- current.thumbnails[0].image_url,
- timeout=self._timeout).content
-
@property
def motion_detection_enabled(self):
"""Return the camera motion detection status."""
return not self._location.is_recording
+
+ @asyncio.coroutine
+ def async_camera_image(self):
+ """Return a still image response from the camera."""
+ self.renew_live_stream_session()
+
+ from haffmpeg import ImageFrame, IMAGE_JPEG
+ ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop)
+ image = yield from asyncio.shield(ffmpeg.get_image(
+ self._live_stream_session.live_stream_url,
+ output_format=IMAGE_JPEG,
+ extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop)
+ return image
+
+ @asyncio.coroutine
+ def handle_async_mjpeg_stream(self, request):
+ """Generate an HTTP MJPEG stream from the camera."""
+ if self._live_stream_session is None:
+ return
+
+ from haffmpeg import CameraMjpeg
+ stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
+ yield from stream.open_camera(
+ self._live_stream_session.live_stream_url,
+ extra_cmd=self._ffmpeg_arguments)
+
+ yield from async_aiohttp_proxy_stream(
+ self.hass, request, stream,
+ 'multipart/x-mixed-replace;boundary=ffserver')
+ yield from stream.close()
+
+ @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW)
+ def renew_live_stream_session(self):
+ """Renew live stream session."""
+ self._live_stream_session = self._data.get_live_stream_session(
+ self._device)
diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py
index c1ec2db0a08..0a9a3fbdca4 100644
--- a/homeassistant/components/camera/netatmo.py
+++ b/homeassistant/components/camera/netatmo.py
@@ -64,13 +64,11 @@ class NetatmoCamera(Camera):
self._name = home + ' / ' + camera_name
else:
self._name = camera_name
- camera_id = data.camera_data.cameraByName(
- camera=camera_name, home=home)['id']
- self._unique_id = "Welcome_camera {0} - {1}".format(
- self._name, camera_id)
self._vpnurl, self._localurl = self._data.camera_data.cameraUrls(
camera=camera_name
)
+ self._unique_id = data.camera_data.cameraByName(
+ camera=camera_name, home=home)['id']
self._cameratype = camera_type
def camera_image(self):
@@ -117,5 +115,5 @@ class NetatmoCamera(Camera):
@property
def unique_id(self):
- """Return the unique ID for this sensor."""
+ """Return the unique ID for this camera."""
return self._unique_id
diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py
index 6e3d3622a3f..f37e7778414 100644
--- a/homeassistant/components/camera/rpi_camera.py
+++ b/homeassistant/components/camera/rpi_camera.py
@@ -28,7 +28,7 @@ CONF_VERTICAL_FLIP = 'vertical_flip'
DEFAULT_HORIZONTAL_FLIP = 0
DEFAULT_IMAGE_HEIGHT = 480
-DEFAULT_IMAGE_QUALITIY = 7
+DEFAULT_IMAGE_QUALITY = 7
DEFAULT_IMAGE_ROTATION = 0
DEFAULT_IMAGE_WIDTH = 640
DEFAULT_NAME = 'Raspberry Pi Camera'
@@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(vol.Coerce(int), vol.Range(min=0, max=1)),
vol.Optional(CONF_IMAGE_HEIGHT, default=DEFAULT_IMAGE_HEIGHT):
vol.Coerce(int),
- vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITIY):
+ vol.Optional(CONF_IMAGE_QUALITY, default=DEFAULT_IMAGE_QUALITY):
vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
vol.Optional(CONF_IMAGE_ROTATION, default=DEFAULT_IMAGE_ROTATION):
vol.All(vol.Coerce(int), vol.Range(min=0, max=359)),
@@ -131,7 +131,7 @@ class RaspberryCamera(Camera):
stderr=subprocess.STDOUT)
def camera_image(self):
- """Return raspstill image response."""
+ """Return raspistill image response."""
with open(self._config[CONF_FILE_PATH], 'rb') as file:
return file.read()
diff --git a/homeassistant/components/camera/uvc.py b/homeassistant/components/camera/uvc.py
index 8d79fa04a9a..f7dc4cfd973 100644
--- a/homeassistant/components/camera/uvc.py
+++ b/homeassistant/components/camera/uvc.py
@@ -127,6 +127,9 @@ class UnifiVideoCamera(Camera):
else:
client_cls = uvc_camera.UVCCameraClient
+ if caminfo['username'] is None:
+ caminfo['username'] = 'ubnt'
+
camera = None
for addr in addrs:
try:
diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py
old mode 100755
new mode 100644
index 72f40cb83a4..b4bcad0064d
--- a/homeassistant/components/camera/xeoma.py
+++ b/homeassistant/components/camera/xeoma.py
@@ -14,7 +14,7 @@ from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.helpers import config_validation as cv
-REQUIREMENTS = ['pyxeoma==1.2']
+REQUIREMENTS = ['pyxeoma==1.3']
_LOGGER = logging.getLogger(__name__)
@@ -22,6 +22,8 @@ CONF_CAMERAS = 'cameras'
CONF_HIDE = 'hide'
CONF_IMAGE_NAME = 'image_name'
CONF_NEW_VERSION = 'new_version'
+CONF_VIEWER_PASSWORD = 'viewer_password'
+CONF_VIEWER_USERNAME = 'viewer_username'
CAMERAS_SCHEMA = vol.Schema({
vol.Required(CONF_IMAGE_NAME): cv.string,
@@ -48,9 +50,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
host = config[CONF_HOST]
login = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
- new_version = config[CONF_NEW_VERSION]
- xeoma = Xeoma(host, new_version, login, password)
+ xeoma = Xeoma(host, login, password)
try:
yield from xeoma.async_test_connection()
@@ -59,9 +60,12 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
{
CONF_IMAGE_NAME: image_name,
CONF_HIDE: False,
- CONF_NAME: image_name
+ CONF_NAME: image_name,
+ CONF_VIEWER_USERNAME: username,
+ CONF_VIEWER_PASSWORD: pw
+
}
- for image_name in discovered_image_names
+ for image_name, username, pw in discovered_image_names
]
for cam in config[CONF_CAMERAS]:
@@ -77,8 +81,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
cameras = list(filter(lambda c: not c[CONF_HIDE], discovered_cameras))
async_add_devices(
- [XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME])
- for camera in cameras])
+ [XeomaCamera(xeoma, camera[CONF_IMAGE_NAME], camera[CONF_NAME],
+ camera[CONF_VIEWER_USERNAME],
+ camera[CONF_VIEWER_PASSWORD]) for camera in cameras])
except XeomaError as err:
_LOGGER.error("Error: %s", err.message)
return
@@ -87,12 +92,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class XeomaCamera(Camera):
"""Implementation of a Xeoma camera."""
- def __init__(self, xeoma, image, name):
+ def __init__(self, xeoma, image, name, username, password):
"""Initialize a Xeoma camera."""
super().__init__()
self._xeoma = xeoma
self._name = name
self._image = image
+ self._username = username
+ self._password = password
self._last_image = None
@asyncio.coroutine
@@ -100,7 +107,8 @@ class XeomaCamera(Camera):
"""Return a still image response from the camera."""
from pyxeoma.xeoma import XeomaError
try:
- image = yield from self._xeoma.async_get_camera_image(self._image)
+ image = yield from self._xeoma.async_get_camera_image(
+ self._image, self._username, self._password)
self._last_image = image
except XeomaError as err:
_LOGGER.error("Error fetching image: %s", err.message)
diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py
index 8ab7218e201..dfef4976eb8 100644
--- a/homeassistant/components/canary.py
+++ b/homeassistant/components/canary.py
@@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT
from homeassistant.helpers import discovery
from homeassistant.util import Throttle
-REQUIREMENTS = ['py-canary==0.2.3']
+REQUIREMENTS = ['py-canary==0.4.0']
_LOGGER = logging.getLogger(__name__)
@@ -111,7 +111,18 @@ class CanaryData(object):
"""Return a list of readings based on device_id."""
return self._readings_by_device_id.get(device_id, [])
+ def get_reading(self, device_id, sensor_type):
+ """Return reading for device_id and sensor type."""
+ readings = self._readings_by_device_id.get(device_id, [])
+ return next((
+ reading.value for reading in readings
+ if reading.sensor_type == sensor_type), None)
+
def set_location_mode(self, location_id, mode_name, is_private=False):
"""Set location mode."""
self._api.set_location_mode(location_id, mode_name, is_private)
self.update(no_throttle=True)
+
+ def get_live_stream_session(self, device):
+ """Return live stream session."""
+ return self._api.get_live_stream_session(device)
diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py
index fea1fcee3a3..0ed4ebe8942 100644
--- a/homeassistant/components/climate/daikin.py
+++ b/homeassistant/components/climate/daikin.py
@@ -183,11 +183,6 @@ class DaikinClimate(ClimateDevice):
self._force_refresh = True
self._api.device.set(values)
- @property
- def unique_id(self):
- """Return the ID of this AC."""
- return "{}.{}".format(self.__class__, self._api.ip_address)
-
@property
def supported_features(self):
"""Return the list of supported features."""
diff --git a/homeassistant/components/climate/demo.py b/homeassistant/components/climate/demo.py
index c3ba523468f..102155babea 100644
--- a/homeassistant/components/climate/demo.py
+++ b/homeassistant/components/climate/demo.py
@@ -14,23 +14,19 @@ from homeassistant.components.climate import (
SUPPORT_ON_OFF)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
-SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY |
- SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
- SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_FAN_MODE |
- SUPPORT_OPERATION_MODE | SUPPORT_AUX_HEAT |
- SUPPORT_SWING_MODE | SUPPORT_TARGET_TEMPERATURE_HIGH |
- SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_ON_OFF)
+SUPPORT_FLAGS = SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Demo climate devices."""
add_devices([
DemoClimate('HeatPump', 68, TEMP_FAHRENHEIT, None, None, 77,
- 'Auto Low', None, None, 'Auto', 'heat', None, None, None),
+ None, None, None, None, 'heat', None, None,
+ None, True),
DemoClimate('Hvac', 21, TEMP_CELSIUS, True, None, 22, 'On High',
- 67, 54, 'Off', 'cool', False, None, None),
- DemoClimate('Ecobee', None, TEMP_CELSIUS, None, None, 23, 'Auto Low',
- None, None, 'Auto', 'auto', None, 24, 21)
+ 67, 54, 'Off', 'cool', False, None, None, None),
+ DemoClimate('Ecobee', None, TEMP_CELSIUS, None, 'home', 23, 'Auto Low',
+ None, None, 'Auto', 'auto', None, 24, 21, None)
])
@@ -40,9 +36,37 @@ class DemoClimate(ClimateDevice):
def __init__(self, name, target_temperature, unit_of_measurement,
away, hold, current_temperature, current_fan_mode,
target_humidity, current_humidity, current_swing_mode,
- current_operation, aux, target_temp_high, target_temp_low):
+ current_operation, aux, target_temp_high, target_temp_low,
+ is_on):
"""Initialize the climate device."""
self._name = name
+ self._support_flags = SUPPORT_FLAGS
+ if target_temperature is not None:
+ self._support_flags = \
+ self._support_flags | SUPPORT_TARGET_TEMPERATURE
+ if away is not None:
+ self._support_flags = self._support_flags | SUPPORT_AWAY_MODE
+ if hold is not None:
+ self._support_flags = self._support_flags | SUPPORT_HOLD_MODE
+ if current_fan_mode is not None:
+ self._support_flags = self._support_flags | SUPPORT_FAN_MODE
+ if target_humidity is not None:
+ self._support_flags = \
+ self._support_flags | SUPPORT_TARGET_HUMIDITY
+ if current_swing_mode is not None:
+ self._support_flags = self._support_flags | SUPPORT_SWING_MODE
+ if current_operation is not None:
+ self._support_flags = self._support_flags | SUPPORT_OPERATION_MODE
+ if aux is not None:
+ self._support_flags = self._support_flags | SUPPORT_AUX_HEAT
+ if target_temp_high is not None:
+ self._support_flags = \
+ self._support_flags | SUPPORT_TARGET_TEMPERATURE_HIGH
+ if target_temp_low is not None:
+ self._support_flags = \
+ self._support_flags | SUPPORT_TARGET_TEMPERATURE_LOW
+ if is_on is not None:
+ self._support_flags = self._support_flags | SUPPORT_ON_OFF
self._target_temperature = target_temperature
self._target_humidity = target_humidity
self._unit_of_measurement = unit_of_measurement
@@ -59,12 +83,12 @@ class DemoClimate(ClimateDevice):
self._swing_list = ['Auto', '1', '2', '3', 'Off']
self._target_temperature_high = target_temp_high
self._target_temperature_low = target_temp_low
- self._on = True
+ self._on = is_on
@property
def supported_features(self):
"""Return the list of supported features."""
- return SUPPORT_FLAGS
+ return self._support_flags
@property
def should_poll(self):
@@ -207,7 +231,7 @@ class DemoClimate(ClimateDevice):
self.schedule_update_ha_state()
def turn_aux_heat_on(self):
- """Turn auxillary heater on."""
+ """Turn auxiliary heater on."""
self._aux = True
self.schedule_update_ha_state()
diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py
index b0685b337be..6a4253ceca7 100644
--- a/homeassistant/components/climate/ecobee.py
+++ b/homeassistant/components/climate/ecobee.py
@@ -13,7 +13,9 @@ from homeassistant.components.climate import (
DOMAIN, STATE_COOL, STATE_HEAT, STATE_AUTO, STATE_IDLE, ClimateDevice,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE,
- SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH)
+ SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH,
+ SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH,
+ SUPPORT_TARGET_TEMPERATURE_LOW)
from homeassistant.const import (
ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
@@ -46,7 +48,9 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE |
SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE |
- SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH)
+ SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH |
+ SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH |
+ SUPPORT_TARGET_TEMPERATURE_LOW)
def setup_platform(hass, config, add_devices, discovery_info=None):
diff --git a/homeassistant/components/climate/econet.py b/homeassistant/components/climate/econet.py
index bb92a92467a..0591178391a 100644
--- a/homeassistant/components/climate/econet.py
+++ b/homeassistant/components/climate/econet.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyeconet==0.0.4']
+REQUIREMENTS = ['pyeconet==0.0.5']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py
index 9b3b7d650a9..9c712c632e6 100644
--- a/homeassistant/components/climate/eq3btsmart.py
+++ b/homeassistant/components/climate/eq3btsmart.py
@@ -55,7 +55,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# pylint: disable=import-error
class EQ3BTSmartThermostat(ClimateDevice):
- """Representation of a eQ-3 Bluetooth Smart thermostat."""
+ """Representation of an eQ-3 Bluetooth Smart thermostat."""
def __init__(self, _mac, _name):
"""Initialize the thermostat."""
diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py
index 9445fc7cfc9..c66e611c8e9 100644
--- a/homeassistant/components/climate/generic_thermostat.py
+++ b/homeassistant/components/climate/generic_thermostat.py
@@ -156,7 +156,7 @@ class GenericThermostat(ClimateDevice):
# If we have no initial temperature, restore
if self._target_temp is None:
# If we have a previously saved temperature
- if old_state.attributes[ATTR_TEMPERATURE] is None:
+ if old_state.attributes.get(ATTR_TEMPERATURE) is None:
if self.ac_mode:
self._target_temp = self.max_temp
else:
@@ -166,15 +166,15 @@ class GenericThermostat(ClimateDevice):
else:
self._target_temp = float(
old_state.attributes[ATTR_TEMPERATURE])
- if old_state.attributes[ATTR_AWAY_MODE] is not None:
+ if old_state.attributes.get(ATTR_AWAY_MODE) is not None:
self._is_away = str(
old_state.attributes[ATTR_AWAY_MODE]) == STATE_ON
if (self._initial_operation_mode is None and
old_state.attributes[ATTR_OPERATION_MODE] is not None):
self._current_operation = \
old_state.attributes[ATTR_OPERATION_MODE]
- if self._current_operation != STATE_OFF:
- self._enabled = True
+ self._enabled = self._current_operation != STATE_OFF
+
else:
# No previous state, try and restore defaults
if self._target_temp is None:
@@ -249,7 +249,7 @@ class GenericThermostat(ClimateDevice):
else:
_LOGGER.error("Unrecognized operation mode: %s", operation_mode)
return
- # Ensure we updae the current operation after changing the mode
+ # Ensure we update the current operation after changing the mode
self.schedule_update_ha_state()
@asyncio.coroutine
diff --git a/homeassistant/components/climate/heatmiser.py b/homeassistant/components/climate/heatmiser.py
index b05c880cc37..19c033a319f 100644
--- a/homeassistant/components/climate/heatmiser.py
+++ b/homeassistant/components/climate/heatmiser.py
@@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
serport = connection.connection(ipaddress, port)
serport.open()
- for thermostat, tstat in tstats.items():
+ for tstat in tstats.values():
add_devices([
HeatmiserV3Thermostat(
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py
index b8ac66d91b3..760ef131049 100644
--- a/homeassistant/components/climate/hive.py
+++ b/homeassistant/components/climate/hive.py
@@ -174,5 +174,5 @@ class HiveClimateEntity(ClimateDevice):
entity.handle_update(self.data_updatesource)
def update(self):
- """Update all Node data frome Hive."""
+ """Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
diff --git a/homeassistant/components/climate/melissa.py b/homeassistant/components/climate/melissa.py
new file mode 100644
index 00000000000..2b3b3bfbab1
--- /dev/null
+++ b/homeassistant/components/climate/melissa.py
@@ -0,0 +1,259 @@
+"""
+Support for Melissa Climate A/C.
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/climate.melissa/
+"""
+import logging
+
+from homeassistant.components.climate import (
+ ClimateDevice, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE,
+ SUPPORT_ON_OFF, STATE_AUTO, STATE_HEAT, STATE_COOL, STATE_DRY,
+ STATE_FAN_ONLY, SUPPORT_FAN_MODE
+)
+from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH
+from homeassistant.components.melissa import DATA_MELISSA
+from homeassistant.const import (
+ TEMP_CELSIUS, STATE_ON, STATE_OFF, STATE_IDLE, ATTR_TEMPERATURE,
+ PRECISION_WHOLE
+)
+
+DEPENDENCIES = ['melissa']
+
+_LOGGER = logging.getLogger(__name__)
+
+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
+]
+
+FAN_MODES = [
+ STATE_AUTO, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM
+]
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Iterate through and add all Melissa devices."""
+ api = hass.data[DATA_MELISSA]
+ devices = api.fetch_devices().values()
+
+ all_devices = []
+
+ for device in devices:
+ all_devices.append(MelissaClimate(
+ api, device['serial_number'], device))
+
+ add_devices(all_devices)
+
+
+class MelissaClimate(ClimateDevice):
+ """Representation of a Melissa Climate device."""
+
+ def __init__(self, api, serial_number, init_data):
+ """Initialize the climate device."""
+ self._name = init_data['name']
+ self._api = api
+ self._serial_number = serial_number
+ self._data = init_data['controller_log']
+ self._state = None
+ self._cur_settings = None
+
+ @property
+ def name(self):
+ """Return the name of the thermostat, if any."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return current state."""
+ if self._cur_settings is not None:
+ return self._cur_settings[self._api.STATE] in (
+ self._api.STATE_ON, self._api.STATE_IDLE)
+ return None
+
+ @property
+ def current_fan_mode(self):
+ """Return the current fan mode."""
+ if self._cur_settings is not None:
+ return self.melissa_fan_to_hass(
+ self._cur_settings[self._api.FAN])
+
+ @property
+ def current_temperature(self):
+ """Return the current temperature."""
+ if self._data:
+ return self._data[self._api.TEMP]
+
+ @property
+ def target_temperature_step(self):
+ """Return the supported step of target temperature."""
+ return PRECISION_WHOLE
+
+ @property
+ def current_operation(self):
+ """Return the current operation mode."""
+ if self._cur_settings is not None:
+ return self.melissa_op_to_hass(
+ self._cur_settings[self._api.MODE])
+
+ @property
+ def operation_list(self):
+ """Return the list of available operation modes."""
+ return OP_MODES
+
+ @property
+ def fan_list(self):
+ """List of available fan modes."""
+ return FAN_MODES
+
+ @property
+ def target_temperature(self):
+ """Return the temperature we try to reach."""
+ if self._cur_settings is not None:
+ return self._cur_settings[self._api.TEMP]
+
+ @property
+ def state(self):
+ """Return current state."""
+ if self._cur_settings is not None:
+ return self.melissa_state_to_hass(
+ self._cur_settings[self._api.STATE])
+
+ @property
+ def temperature_unit(self):
+ """Return the unit of measurement which this thermostat uses."""
+ return TEMP_CELSIUS
+
+ @property
+ def min_temp(self):
+ """Return the minimum supported temperature for the thermostat."""
+ return 16
+
+ @property
+ def max_temp(self):
+ """Return the maximum supported temperature for the thermostat."""
+ return 30
+
+ @property
+ def supported_features(self):
+ """Return the list of supported features."""
+ return SUPPORT_FLAGS
+
+ def set_temperature(self, **kwargs):
+ """Set new target temperature."""
+ temp = kwargs.get(ATTR_TEMPERATURE)
+ self.send({self._api.TEMP: temp})
+
+ def set_fan_mode(self, fan):
+ """Set fan mode."""
+ fan_mode = self.hass_fan_to_melissa(fan)
+ self.send({self._api.FAN: fan_mode})
+
+ def set_operation_mode(self, operation_mode):
+ """Set operation mode."""
+ mode = self.hass_mode_to_melissa(operation_mode)
+ self.send({self._api.MODE: mode})
+
+ def turn_on(self):
+ """Turn on device."""
+ self.send({self._api.STATE: self._api.STATE_ON})
+
+ def turn_off(self):
+ """Turn off device."""
+ self.send({self._api.STATE: self._api.STATE_OFF})
+
+ def send(self, value):
+ """Sending action to service."""
+ try:
+ old_value = self._cur_settings.copy()
+ self._cur_settings.update(value)
+ except AttributeError:
+ old_value = None
+ if not self._api.send(self._serial_number, self._cur_settings):
+ self._cur_settings = old_value
+ return False
+ else:
+ return True
+
+ def update(self):
+ """Get latest data from Melissa."""
+ try:
+ self._data = self._api.status(cached=True)[self._serial_number]
+ self._cur_settings = self._api.cur_settings(
+ self._serial_number
+ )['controller']['_relation']['command_log']
+ except KeyError:
+ _LOGGER.warning(
+ 'Unable to update entity %s', self.entity_id)
+
+ def melissa_state_to_hass(self, state):
+ """Translate Melissa states to hass states."""
+ if state == self._api.STATE_ON:
+ return STATE_ON
+ elif state == self._api.STATE_OFF:
+ return STATE_OFF
+ elif state == self._api.STATE_IDLE:
+ return STATE_IDLE
+ else:
+ 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:
+ return STATE_HEAT
+ elif mode == self._api.MODE_COOL:
+ return STATE_COOL
+ elif mode == self._api.MODE_DRY:
+ 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
+
+ def melissa_fan_to_hass(self, fan):
+ """Translate Melissa fan modes to hass modes."""
+ if fan == self._api.FAN_AUTO:
+ return STATE_AUTO
+ elif fan == self._api.FAN_LOW:
+ return SPEED_LOW
+ elif fan == self._api.FAN_MEDIUM:
+ 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
+
+ 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:
+ return self._api.MODE_HEAT
+ elif mode == STATE_COOL:
+ return self._api.MODE_COOL
+ elif mode == STATE_DRY:
+ return self._api.MODE_DRY
+ elif mode == STATE_FAN_ONLY:
+ return self._api.MODE_FAN
+ else:
+ _LOGGER.warning("Melissa have no setting for %s mode", mode)
+
+ def hass_fan_to_melissa(self, fan):
+ """Translate hass fan modes to melissa modes."""
+ if fan == STATE_AUTO:
+ return self._api.FAN_AUTO
+ elif fan == SPEED_LOW:
+ return self._api.FAN_LOW
+ elif fan == SPEED_MEDIUM:
+ return self._api.FAN_MEDIUM
+ elif fan == SPEED_HIGH:
+ return self._api.FAN_HIGH
+ else:
+ _LOGGER.warning("Melissa have no setting for %s fan mode", fan)
diff --git a/homeassistant/components/climate/mqtt.py b/homeassistant/components/climate/mqtt.py
index 3656bf7b475..5929cec3b05 100644
--- a/homeassistant/components/climate/mqtt.py
+++ b/homeassistant/components/climate/mqtt.py
@@ -565,7 +565,7 @@ class MqttClimate(MqttAvailability, ClimateDevice):
@asyncio.coroutine
def async_turn_aux_heat_on(self):
- """Turn auxillary heater on."""
+ """Turn auxiliary heater on."""
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
self._payload_on, self._qos, self._retain)
@@ -576,7 +576,7 @@ class MqttClimate(MqttAvailability, ClimateDevice):
@asyncio.coroutine
def async_turn_aux_heat_off(self):
- """Turn auxillary heater off."""
+ """Turn auxiliary heater off."""
if self._topic[CONF_AUX_COMMAND_TOPIC] is not None:
mqtt.async_publish(self.hass, self._topic[CONF_AUX_COMMAND_TOPIC],
self._payload_off, self._qos, self._retain)
diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py
index ff1400a8fae..5553db70f0d 100644
--- a/homeassistant/components/climate/mysensors.py
+++ b/homeassistant/components/climate/mysensors.py
@@ -139,8 +139,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
self.gateway.set_child_value(
self.node_id, self.child_id, value_type, value)
if self.gateway.optimistic:
- # O
- # ptimistically assume that device has changed state
+ # Optimistically assume that device has changed state
self._values[value_type] = value
self.schedule_update_ha_state()
diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py
index b4492821b1f..d8d7d6c901a 100644
--- a/homeassistant/components/climate/nest.py
+++ b/homeassistant/components/climate/nest.py
@@ -97,6 +97,11 @@ class NestThermostat(ClimateDevice):
"""Return the list of supported features."""
return SUPPORT_FLAGS
+ @property
+ def unique_id(self):
+ """Unique ID for this device."""
+ return self.device.serial
+
@property
def name(self):
"""Return the name of the nest, if any."""
diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py
index 7155aaf5924..5d54b39e773 100644
--- a/homeassistant/components/climate/netatmo.py
+++ b/homeassistant/components/climate/netatmo.py
@@ -24,7 +24,7 @@ CONF_RELAY = 'relay'
CONF_THERMOSTAT = 'thermostat'
DEFAULT_AWAY_TEMPERATURE = 14
-# # The default offeset is 2 hours (when you use the thermostat itself)
+# # The default offset is 2 hours (when you use the thermostat itself)
DEFAULT_TIME_OFFSET = 7200
# # Return cached results if last scan was less then this time ago
# # NetAtmo Data is uploaded to server every hour
diff --git a/homeassistant/components/climate/oem.py b/homeassistant/components/climate/oem.py
index 0cbdc8f2ce6..59f8db03318 100644
--- a/homeassistant/components/climate/oem.py
+++ b/homeassistant/components/climate/oem.py
@@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class ThermostatDevice(ClimateDevice):
- """Interface class for the oemthermostat modul."""
+ """Interface class for the oemthermostat module."""
def __init__(self, hass, thermostat, name, away_temp):
"""Initialize the device."""
diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py
index 25492cb0895..868511c0ac4 100644
--- a/homeassistant/components/climate/tado.py
+++ b/homeassistant/components/climate/tado.py
@@ -249,7 +249,7 @@ class TadoClimate(ClimateDevice):
data = self._store.get_data(self._data_id)
if data is None:
- _LOGGER.debug("Recieved no data for zone %s", self.zone_name)
+ _LOGGER.debug("Received no data for zone %s", self.zone_name)
return
if 'sensorDataPoints' in data:
@@ -294,7 +294,7 @@ class TadoClimate(ClimateDevice):
overlay = False
overlay_data = None
- termination = self._current_operation
+ termination = CONST_MODE_SMART_SCHEDULE
cooling = False
fan_speed = CONST_MODE_OFF
@@ -317,7 +317,7 @@ class TadoClimate(ClimateDevice):
fan_speed = setting_data['fanSpeed']
if self._device_is_active:
- # If you set mode manualy to off, there will be an overlay
+ # If you set mode manually to off, there will be an overlay
# and a termination, but we want to see the mode "OFF"
self._overlay_mode = termination
self._current_operation = termination
diff --git a/homeassistant/components/climate/touchline.py b/homeassistant/components/climate/touchline.py
index cc45e26a1cf..f9c5676629b 100644
--- a/homeassistant/components/climate/touchline.py
+++ b/homeassistant/components/climate/touchline.py
@@ -13,7 +13,7 @@ from homeassistant.components.climate import (
from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pytouchline==0.6']
+REQUIREMENTS = ['pytouchline==0.7']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py
index 92e5c71b6c5..6db1d53bc50 100644
--- a/homeassistant/components/climate/venstar.py
+++ b/homeassistant/components/climate/venstar.py
@@ -20,7 +20,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['venstarcolortouch==0.5']
+REQUIREMENTS = ['venstarcolortouch==0.6']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index a5bbf805d42..e17c9ee1b1e 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -16,8 +16,7 @@ import voluptuous as vol
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, CONF_REGION, CONF_MODE, CONF_NAME, CONF_TYPE)
-from homeassistant.helpers import entityfilter
-from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers import entityfilter, config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import dt as dt_util
from homeassistant.components.alexa import smart_home as alexa_sh
@@ -105,12 +104,7 @@ def async_setup(hass, config):
)
cloud = hass.data[DOMAIN] = Cloud(hass, **kwargs)
-
- success = yield from cloud.initialize()
-
- if not success:
- return False
-
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, cloud.async_start)
yield from http_api.async_setup(hass)
return True
@@ -192,19 +186,6 @@ class Cloud:
return self._gactions_config
- @asyncio.coroutine
- def initialize(self):
- """Initialize and load cloud info."""
- jwt_success = yield from self._fetch_jwt_keyset()
-
- if not jwt_success:
- return False
-
- self.hass.bus.async_listen_once(
- EVENT_HOMEASSISTANT_START, self._start_cloud)
-
- return True
-
def path(self, *parts):
"""Get config path inside cloud dir.
@@ -234,19 +215,34 @@ class Cloud:
'refresh_token': self.refresh_token,
}, indent=4))
- def _start_cloud(self, event):
+ @asyncio.coroutine
+ def async_start(self, _):
"""Start the cloud component."""
- # Ensure config dir exists
- path = self.hass.config.path(CONFIG_DIR)
- if not os.path.isdir(path):
- os.mkdir(path)
+ success = yield from self._fetch_jwt_keyset()
- user_info = self.user_info_path
- if not os.path.isfile(user_info):
+ # Fetching keyset can fail if internet is not up yet.
+ if not success:
+ self.hass.helpers.async_call_later(5, self.async_start)
return
- with open(user_info, 'rt') as file:
- info = json.loads(file.read())
+ def load_config():
+ """Load config."""
+ # Ensure config dir exists
+ path = self.hass.config.path(CONFIG_DIR)
+ if not os.path.isdir(path):
+ os.mkdir(path)
+
+ user_info = self.user_info_path
+ if not os.path.isfile(user_info):
+ return None
+
+ with open(user_info, 'rt') as file:
+ return json.loads(file.read())
+
+ info = yield from self.hass.async_add_job(load_config)
+
+ if info is None:
+ return
# Validate tokens
try:
diff --git a/homeassistant/components/coinbase.py b/homeassistant/components/coinbase.py
index bdb091325cf..10123752c99 100644
--- a/homeassistant/components/coinbase.py
+++ b/homeassistant/components/coinbase.py
@@ -5,16 +5,17 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/coinbase/
"""
from datetime import timedelta
-
import logging
+
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_API_KEY
-from homeassistant.util import Throttle
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['coinbase==2.0.7']
-REQUIREMENTS = ['coinbase==2.0.6']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'coinbase'
@@ -46,14 +47,13 @@ def setup(hass, config):
api_secret = config[DOMAIN].get(CONF_API_SECRET)
exchange_currencies = config[DOMAIN].get(CONF_EXCHANGE_CURRENCIES)
- hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key,
- api_secret)
+ hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(
+ api_key, api_secret)
if not hasattr(coinbase_data, 'accounts'):
return False
for account in coinbase_data.accounts.data:
- load_platform(hass, 'sensor', DOMAIN,
- {'account': account}, config)
+ load_platform(hass, 'sensor', DOMAIN, {'account': account}, config)
for currency in exchange_currencies:
if currency not in coinbase_data.exchange_rates.rates:
_LOGGER.warning("Currency %s not found", currency)
diff --git a/homeassistant/components/cover/homematic.py b/homeassistant/components/cover/homematic.py
index 1fa215e5fb9..2736b656a15 100644
--- a/homeassistant/components/cover/homematic.py
+++ b/homeassistant/components/cover/homematic.py
@@ -68,7 +68,7 @@ class HMCover(HMDevice, CoverDevice):
self._hmdevice.stop(self._channel)
def _init_data_struct(self):
- """Generate a data dictoinary (self._data) from metadata."""
+ """Generate a data dictionary (self._data) from metadata."""
self._state = "LEVEL"
self._data.update({self._state: STATE_UNKNOWN})
if "LEVEL_2" in self._hmdevice.WRITENODE:
diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py
index b187b8409c2..7d77b1bc3be 100644
--- a/homeassistant/components/cover/isy994.py
+++ b/homeassistant/components/cover/isy994.py
@@ -42,7 +42,7 @@ def setup_platform(hass, config: ConfigType,
class ISYCoverDevice(ISYDevice, CoverDevice):
"""Representation of an ISY994 cover device."""
- def __init__(self, node: object):
+ def __init__(self, node: object) -> None:
"""Initialize the ISY994 cover device."""
super().__init__(node)
diff --git a/homeassistant/components/cover/knx.py b/homeassistant/components/cover/knx.py
index 79c57c41e90..a6cd1263a73 100644
--- a/homeassistant/components/cover/knx.py
+++ b/homeassistant/components/cover/knx.py
@@ -74,7 +74,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
@callback
def async_add_devices_config(hass, config, async_add_devices):
- """Set up cover for KNX platform configured within plattform."""
+ """Set up cover for KNX platform configured within platform."""
import xknx
cover = xknx.devices.Cover(
hass.data[DATA_KNX].xknx,
diff --git a/homeassistant/components/cover/lutron.py b/homeassistant/components/cover/lutron.py
index 08a2ef8c5ad..4e38681a310 100644
--- a/homeassistant/components/cover/lutron.py
+++ b/homeassistant/components/cover/lutron.py
@@ -63,7 +63,7 @@ class LutronCover(LutronDevice, CoverDevice):
def update(self):
"""Call when forcing a refresh of the device."""
- # Reading the property (rather than last_level()) fetchs value
+ # Reading the property (rather than last_level()) fetches value
level = self._lutron_device.level
_LOGGER.debug("Lutron ID: %d updated to %f",
self._lutron_device.id, level)
diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py
index 9b75f03c232..e55072dbc73 100644
--- a/homeassistant/components/cover/mqtt.py
+++ b/homeassistant/components/cover/mqtt.py
@@ -214,16 +214,6 @@ class MqttCover(MqttAvailability, CoverDevice):
self.async_schedule_update_ha_state()
- @callback
- def availability_message_received(topic, payload, qos):
- """Handle new MQTT availability messages."""
- if payload == self._payload_available:
- self._available = True
- elif payload == self._payload_not_available:
- self._available = False
-
- self.async_schedule_update_ha_state()
-
if self._state_topic is None:
# Force into optimistic mode.
self._optimistic = True
@@ -232,11 +222,6 @@ class MqttCover(MqttAvailability, CoverDevice):
self.hass, self._state_topic,
state_message_received, self._qos)
- if self._availability_topic is not None:
- yield from mqtt.async_subscribe(
- self.hass, self._availability_topic,
- availability_message_received, self._qos)
-
if self._tilt_status_topic is None:
self._tilt_optimistic = True
else:
diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py
index 1ee3ea00476..981312140eb 100644
--- a/homeassistant/components/cover/rpi_gpio.py
+++ b/homeassistant/components/cover/rpi_gpio.py
@@ -89,11 +89,6 @@ class RPiGPIOCover(CoverDevice):
rpi_gpio.setup_input(self._state_pin, self._state_pull_mode)
rpi_gpio.write_output(self._relay_pin, not self._invert_relay)
- @property
- def unique_id(self):
- """Return the ID of this cover."""
- return '{}.{}'.format(self.__class__, self._name)
-
@property
def name(self):
"""Return the name of the cover if any."""
diff --git a/homeassistant/components/cover/services.yaml b/homeassistant/components/cover/services.yaml
index 41be271fff0..1a3e020ed87 100644
--- a/homeassistant/components/cover/services.yaml
+++ b/homeassistant/components/cover/services.yaml
@@ -51,8 +51,8 @@ set_cover_tilt_position:
entity_id:
description: Name(s) of cover(s) to set cover tilt position.
example: 'cover.living_room'
- position:
- description: Position of the cover (0 to 100).
+ tilt_position:
+ description: Tilt position of the cover (0 to 100).
example: 30
stop_cover_tilt:
diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py
index fd2b5847292..19bd9f01417 100644
--- a/homeassistant/components/cover/tahoma.py
+++ b/homeassistant/components/cover/tahoma.py
@@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.tahoma/
"""
import logging
-from datetime import timedelta
from homeassistant.components.cover import CoverDevice
from homeassistant.components.tahoma import (
@@ -15,8 +14,6 @@ DEPENDENCIES = ['tahoma']
_LOGGER = logging.getLogger(__name__)
-SCAN_INTERVAL = timedelta(seconds=60)
-
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up Tahoma covers."""
diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py
index a7db472f191..f4728a12a3b 100644
--- a/homeassistant/components/cover/template.py
+++ b/homeassistant/components/cover/template.py
@@ -67,11 +67,6 @@ COVER_SCHEMA = vol.Schema({
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
-COVER_SCHEMA = vol.All(
- cv.deprecated(CONF_ENTITY_ID),
- COVER_SCHEMA,
-)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
})
diff --git a/homeassistant/components/cover/zwave.py b/homeassistant/components/cover/zwave.py
index 3c038125616..15100957242 100644
--- a/homeassistant/components/cover/zwave.py
+++ b/homeassistant/components/cover/zwave.py
@@ -158,7 +158,7 @@ class ZwaveGarageDoorBarrier(ZwaveGarageDoorBase):
@property
def is_closing(self):
- """Return true if cover is in an closing state."""
+ """Return true if cover is in a closing state."""
return self._state == "Closing"
@property
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 269b8136020..9d7d253c328 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -17,7 +17,7 @@ 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==25']
+REQUIREMENTS = ['pydeconz==27']
_LOGGER = logging.getLogger(__name__)
@@ -38,7 +38,7 @@ SERVICE_DATA = 'data'
SERVICE_SCHEMA = vol.Schema({
vol.Required(SERVICE_FIELD): cv.string,
- vol.Required(SERVICE_DATA): cv.string,
+ vol.Required(SERVICE_DATA): dict,
})
CONFIG_INSTRUCTIONS = """
diff --git a/homeassistant/components/device_tracker/actiontec.py b/homeassistant/components/device_tracker/actiontec.py
index 64e1a60ad08..781e486a40e 100644
--- a/homeassistant/components/device_tracker/actiontec.py
+++ b/homeassistant/components/device_tracker/actiontec.py
@@ -42,7 +42,7 @@ Device = namedtuple('Device', ['mac', 'ip', 'last_update'])
class ActiontecDeviceScanner(DeviceScanner):
- """This class queries a an actiontec router for connected devices."""
+ """This class queries an actiontec router for connected devices."""
def __init__(self, config):
"""Initialize the scanner."""
diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py
index 2196dd78fdb..fb47b26a687 100644
--- a/homeassistant/components/device_tracker/asuswrt.py
+++ b/homeassistant/components/device_tracker/asuswrt.py
@@ -242,7 +242,7 @@ class _Connection:
return self._connected
def connect(self):
- """Mark currenct connection state as connected."""
+ """Mark current connection state as connected."""
self._connected = True
def disconnect(self):
diff --git a/homeassistant/components/device_tracker/huawei_router.py b/homeassistant/components/device_tracker/huawei_router.py
index b78683696cf..357dd0d36cf 100644
--- a/homeassistant/components/device_tracker/huawei_router.py
+++ b/homeassistant/components/device_tracker/huawei_router.py
@@ -2,7 +2,7 @@
Support for HUAWEI routers.
For more details about this platform, please refer to the documentation at
-https://home-assistant.io/components/device_tracker.huawei/
+https://home-assistant.io/components/device_tracker.huawei_router/
"""
import base64
import logging
@@ -119,7 +119,7 @@ class HuaweiDeviceScanner(DeviceScanner):
cnt = requests.post('http://{}/asp/GetRandCount.asp'.format(self.host))
cnt_str = str(cnt.content, cnt.apparent_encoding, errors='replace')
- _LOGGER.debug("Loggin in")
+ _LOGGER.debug("Logging in")
cookie = requests.post('http://{}/login.cgi'.format(self.host),
data=[('UserName', self.username),
('PassWord', self.password),
diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py
new file mode 100644
index 00000000000..0aa2be96290
--- /dev/null
+++ b/homeassistant/components/device_tracker/mercedesme.py
@@ -0,0 +1,71 @@
+"""
+Support for Mercedes cars with Mercedes ME.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/device_tracker.mercedesme/
+"""
+import logging
+from datetime import timedelta
+
+from homeassistant.components.mercedesme import DATA_MME
+from homeassistant.helpers.event import track_time_interval
+from homeassistant.util import Throttle
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['mercedesme']
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30)
+
+
+def setup_scanner(hass, config, see, discovery_info=None):
+ """Set up the Mercedes ME tracker."""
+ if discovery_info is None:
+ return False
+
+ data = hass.data[DATA_MME].data
+
+ if not data.cars:
+ return False
+
+ MercedesMEDeviceTracker(hass, config, see, data)
+
+ return True
+
+
+class MercedesMEDeviceTracker(object):
+ """A class representing a Mercedes ME device tracker."""
+
+ def __init__(self, hass, config, see, data):
+ """Initialize the Mercedes ME device tracker."""
+ self.see = see
+ self.data = data
+ self.update_info()
+
+ track_time_interval(
+ hass, self.update_info, MIN_TIME_BETWEEN_SCANS)
+
+ @Throttle(MIN_TIME_BETWEEN_SCANS)
+ def update_info(self, now=None):
+ """Update the device info."""
+ for device in self.data.cars:
+ _LOGGER.debug("Updating %s", device["vin"])
+ location = self.data.get_location(device["vin"])
+ if location is None:
+ return False
+ dev_id = device["vin"]
+ name = device["license"]
+
+ lat = location['positionLat']['value']
+ lon = location['positionLong']['value']
+ attrs = {
+ 'trackr_id': dev_id,
+ 'id': dev_id,
+ 'name': name
+ }
+ self.see(
+ dev_id=dev_id, host_name=name,
+ gps=(lat, lon), attributes=attrs
+ )
+
+ return True
diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py
index 7ac84125863..1805559c252 100644
--- a/homeassistant/components/device_tracker/mikrotik.py
+++ b/homeassistant/components/device_tracker/mikrotik.py
@@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
-REQUIREMENTS = ['librouteros==1.0.4']
+REQUIREMENTS = ['librouteros==1.0.5']
MTK_DEFAULT_API_PORT = '8728'
diff --git a/homeassistant/components/device_tracker/mqtt.py b/homeassistant/components/device_tracker/mqtt.py
index aab5b43acea..2e2d9b10d98 100644
--- a/homeassistant/components/device_tracker/mqtt.py
+++ b/homeassistant/components/device_tracker/mqtt.py
@@ -31,17 +31,14 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
devices = config[CONF_DEVICES]
qos = config[CONF_QOS]
- dev_id_lookup = {}
-
- @callback
- def async_tracker_message_received(topic, payload, qos):
- """Handle received MQTT message."""
- hass.async_add_job(
- async_see(dev_id=dev_id_lookup[topic], location_name=payload))
-
for dev_id, topic in devices.items():
- dev_id_lookup[topic] = dev_id
+ @callback
+ def async_message_received(topic, payload, qos, dev_id=dev_id):
+ """Handle received MQTT message."""
+ hass.async_add_job(
+ async_see(dev_id=dev_id, location_name=payload))
+
yield from mqtt.async_subscribe(
- hass, topic, async_tracker_message_received, qos)
+ hass, topic, async_message_received, qos)
return True
diff --git a/homeassistant/components/device_tracker/mqtt_json.py b/homeassistant/components/device_tracker/mqtt_json.py
index 0ef4f1835b6..7bcad60236a 100644
--- a/homeassistant/components/device_tracker/mqtt_json.py
+++ b/homeassistant/components/device_tracker/mqtt_json.py
@@ -41,32 +41,26 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
devices = config[CONF_DEVICES]
qos = config[CONF_QOS]
- dev_id_lookup = {}
-
- @callback
- def async_tracker_message_received(topic, payload, qos):
- """Handle received MQTT message."""
- dev_id = dev_id_lookup[topic]
-
- try:
- data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
- except vol.MultipleInvalid:
- _LOGGER.error("Skipping update for following data "
- "because of missing or malformatted data: %s",
- payload)
- return
- except ValueError:
- _LOGGER.error("Error parsing JSON payload: %s", payload)
- return
-
- kwargs = _parse_see_args(dev_id, data)
- hass.async_add_job(
- async_see(**kwargs))
-
for dev_id, topic in devices.items():
- dev_id_lookup[topic] = dev_id
+ @callback
+ def async_message_received(topic, payload, qos, dev_id=dev_id):
+ """Handle received MQTT message."""
+ try:
+ data = GPS_JSON_PAYLOAD_SCHEMA(json.loads(payload))
+ except vol.MultipleInvalid:
+ _LOGGER.error("Skipping update for following data "
+ "because of missing or malformatted data: %s",
+ payload)
+ return
+ except ValueError:
+ _LOGGER.error("Error parsing JSON payload: %s", payload)
+ return
+
+ kwargs = _parse_see_args(dev_id, data)
+ hass.async_add_job(async_see(**kwargs))
+
yield from mqtt.async_subscribe(
- hass, topic, async_tracker_message_received, qos)
+ hass, topic, async_message_received, qos)
return True
diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py
index 1742a0aed95..e99524c36db 100644
--- a/homeassistant/components/device_tracker/owntracks.py
+++ b/homeassistant/components/device_tracker/owntracks.py
@@ -143,6 +143,8 @@ def _parse_see_args(message, subscribe_topic):
kwargs['attributes']['tid'] = message['tid']
if 'addr' in message:
kwargs['attributes']['address'] = message['addr']
+ if 'cog' in message:
+ kwargs['attributes']['course'] = message['cog']
if 't' in message:
if message['t'] == 'c':
kwargs['attributes'][ATTR_SOURCE_TYPE] = SOURCE_TYPE_GPS
diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py
index 2306a66070b..e66bb95a11a 100644
--- a/homeassistant/components/device_tracker/ubus.py
+++ b/homeassistant/components/device_tracker/ubus.py
@@ -46,8 +46,8 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
-def _refresh_on_acccess_denied(func):
- """If remove rebooted, it lost our session so rebuld one and try again."""
+def _refresh_on_access_denied(func):
+ """If remove rebooted, it lost our session so rebuild one and try again."""
def decorator(self, *args, **kwargs):
"""Wrap the function to refresh session_id on PermissionError."""
try:
@@ -95,16 +95,15 @@ class UbusDeviceScanner(DeviceScanner):
"""Must be implemented depending on the software."""
raise NotImplementedError
- @_refresh_on_acccess_denied
+ @_refresh_on_access_denied
def get_device_name(self, mac):
"""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)
- self.mac2name = None
return name
- @_refresh_on_acccess_denied
+ @_refresh_on_access_denied
def _update_info(self):
"""Ensure the information from the router is up to date.
@@ -122,13 +121,18 @@ class UbusDeviceScanner(DeviceScanner):
self.last_results = []
results = 0
+ # for each access point
for hostapd in self.hostapd:
result = _req_json_rpc(
self.url, self.session_id, 'call', hostapd, 'get_clients')
if result:
results = results + 1
- self.last_results.extend(result['clients'].keys())
+ # Check for each device is authorized (valid wpa key)
+ for key in result['clients'].keys():
+ device = result['clients'][key]
+ if device['authorized']:
+ self.last_results.append(key)
return bool(results)
diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py
index a3e81b3ef51..d5b6b044f1f 100644
--- a/homeassistant/components/device_tracker/unifi.py
+++ b/homeassistant/components/device_tracker/unifi.py
@@ -75,7 +75,7 @@ def get_scanner(hass, config):
class UnifiScanner(DeviceScanner):
"""Provide device_tracker support from Unifi WAP client data."""
- def __init__(self, controller, detection_time: timedelta):
+ def __init__(self, controller, detection_time: timedelta) -> None:
"""Initialize the scanner."""
self._detection_time = detection_time
self._controller = controller
diff --git a/homeassistant/components/dominos.py b/homeassistant/components/dominos.py
index 0d6645f37c1..4bdb4c80add 100644
--- a/homeassistant/components/dominos.py
+++ b/homeassistant/components/dominos.py
@@ -1,7 +1,7 @@
"""
Support for Dominos Pizza ordering.
-The Dominos Pizza component ceates a service which can be invoked to order
+The Dominos Pizza component creates a service which can be invoked to order
from their menu
For more details about this platform, please refer to the documentation at
diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py
index 56933d198f2..be7adc034a0 100644
--- a/homeassistant/components/doorbird.py
+++ b/homeassistant/components/doorbird.py
@@ -73,6 +73,7 @@ def setup(hass, config):
class DoorbirdRequestView(HomeAssistantView):
"""Provide a page for the device to call."""
+ requires_auth = False
url = API_URL
name = API_URL[1:].replace('/', ':')
extra_urls = [API_URL + '/{sensor}']
diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py
index d832bbdfdd1..b7354b4f0a7 100644
--- a/homeassistant/components/downloader.py
+++ b/homeassistant/components/downloader.py
@@ -79,7 +79,7 @@ def setup(hass, config):
if req.status_code != 200:
_LOGGER.warning(
- "downloading '%s' failed, stauts_code=%d",
+ "downloading '%s' failed, status_code=%d",
url,
req.status_code)
diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py
index 88cbf1bd57b..7ae4ec862bb 100644
--- a/homeassistant/components/eight_sleep.py
+++ b/homeassistant/components/eight_sleep.py
@@ -193,7 +193,7 @@ class EightSleepUserEntity(Entity):
"""The Eight Sleep device entity."""
def __init__(self, eight):
- """Initialize the data oject."""
+ """Initialize the data object."""
self._eight = eight
@asyncio.coroutine
@@ -217,7 +217,7 @@ class EightSleepHeatEntity(Entity):
"""The Eight Sleep device entity."""
def __init__(self, eight):
- """Initialize the data oject."""
+ """Initialize the data object."""
self._eight = eight
@asyncio.coroutine
diff --git a/homeassistant/components/emoncms_history.py b/homeassistant/components/emoncms_history.py
index 34d9fd0f458..6a92ab64044 100644
--- a/homeassistant/components/emoncms_history.py
+++ b/homeassistant/components/emoncms_history.py
@@ -59,7 +59,7 @@ def setup(hass, config):
payload, fullurl, req.status_code)
def update_emoncms(time):
- """Send whitelisted entities states reguarly to Emoncms."""
+ """Send whitelisted entities states regularly to Emoncms."""
payload_dict = {}
for entity_id in whitelist:
diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py
index b2206f80766..9fba21b81dc 100644
--- a/homeassistant/components/emulated_hue/__init__.py
+++ b/homeassistant/components/emulated_hue/__init__.py
@@ -39,6 +39,9 @@ CONF_OFF_MAPS_TO_ON_DOMAINS = 'off_maps_to_on_domains'
CONF_EXPOSE_BY_DEFAULT = 'expose_by_default'
CONF_EXPOSED_DOMAINS = 'exposed_domains'
CONF_TYPE = 'type'
+CONF_ENTITIES = 'entities'
+CONF_ENTITY_NAME = 'name'
+CONF_ENTITY_HIDDEN = 'hidden'
TYPE_ALEXA = 'alexa'
TYPE_GOOGLE = 'google_home'
@@ -52,6 +55,11 @@ DEFAULT_EXPOSED_DOMAINS = [
]
DEFAULT_TYPE = TYPE_GOOGLE
+CONFIG_ENTITY_SCHEMA = vol.Schema({
+ vol.Optional(CONF_ENTITY_NAME): cv.string,
+ vol.Optional(CONF_ENTITY_HIDDEN): cv.boolean
+})
+
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST_IP): cv.string,
@@ -63,11 +71,14 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_EXPOSE_BY_DEFAULT): cv.boolean,
vol.Optional(CONF_EXPOSED_DOMAINS): cv.ensure_list,
vol.Optional(CONF_TYPE, default=DEFAULT_TYPE):
- vol.Any(TYPE_ALEXA, TYPE_GOOGLE)
+ vol.Any(TYPE_ALEXA, TYPE_GOOGLE),
+ vol.Optional(CONF_ENTITIES):
+ vol.Schema({cv.entity_id: CONFIG_ENTITY_SCHEMA})
})
}, extra=vol.ALLOW_EXTRA)
ATTR_EMULATED_HUE = 'emulated_hue'
+ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden'
@@ -183,6 +194,8 @@ class Config(object):
self.advertise_port = conf.get(
CONF_ADVERTISE_PORT) or self.listen_port
+ self.entities = conf.get(CONF_ENTITIES, {})
+
def entity_id_to_number(self, entity_id):
"""Get a unique number for the entity id."""
if self.type == TYPE_ALEXA:
@@ -215,6 +228,14 @@ class Config(object):
assert isinstance(number, str)
return self.numbers.get(number)
+ def get_entity_name(self, entity):
+ """Get the name of an entity."""
+ if entity.entity_id in self.entities and \
+ CONF_ENTITY_NAME in self.entities[entity.entity_id]:
+ return self.entities[entity.entity_id][CONF_ENTITY_NAME]
+
+ return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
+
def is_entity_exposed(self, entity):
"""Determine if an entity should be exposed on the emulated bridge.
@@ -227,6 +248,12 @@ class Config(object):
domain = entity.domain.lower()
explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None)
explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None)
+
+ if entity.entity_id in self.entities and \
+ CONF_ENTITY_HIDDEN in self.entities[entity.entity_id]:
+ explicit_hidden = \
+ self.entities[entity.entity_id][CONF_ENTITY_HIDDEN]
+
if explicit_expose is True or explicit_hidden is False:
expose = True
elif explicit_expose is False or explicit_hidden is True:
diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py
index 7b98ca7deaa..5d97ef3cea4 100644
--- a/homeassistant/components/emulated_hue/hue_api.py
+++ b/homeassistant/components/emulated_hue/hue_api.py
@@ -24,9 +24,6 @@ from homeassistant.components.http import HomeAssistantView
_LOGGER = logging.getLogger(__name__)
-ATTR_EMULATED_HUE = 'emulated_hue'
-ATTR_EMULATED_HUE_NAME = 'emulated_hue_name'
-
HUE_API_STATE_ON = 'on'
HUE_API_STATE_BRI = 'bri'
@@ -77,7 +74,7 @@ class HueAllLightsStateView(HomeAssistantView):
number = self.config.entity_id_to_number(entity.entity_id)
json_response[number] = entity_to_json(
- entity, state, brightness)
+ self.config, entity, state, brightness)
return self.json(json_response)
@@ -110,7 +107,7 @@ class HueOneLightStateView(HomeAssistantView):
state, brightness = get_entity_state(self.config, entity)
- json_response = entity_to_json(entity, state, brightness)
+ json_response = entity_to_json(self.config, entity, state, brightness)
return self.json(json_response)
@@ -344,10 +341,8 @@ def get_entity_state(config, entity):
return (final_state, final_brightness)
-def entity_to_json(entity, is_on=None, brightness=None):
+def entity_to_json(config, entity, is_on=None, brightness=None):
"""Convert an entity to its Hue bridge JSON representation."""
- name = entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name)
-
return {
'state':
{
@@ -356,7 +351,7 @@ def entity_to_json(entity, is_on=None, brightness=None):
'reachable': True
},
'type': 'Dimmable light',
- 'name': name,
+ 'name': config.get_entity_name(entity),
'modelid': 'HASS123',
'uniqueid': entity.entity_id,
'swversion': '123'
diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py
index eccc800319c..6e6d377986d 100644
--- a/homeassistant/components/fan/__init__.py
+++ b/homeassistant/components/fan/__init__.py
@@ -205,7 +205,7 @@ def async_setup(hass, config: dict):
@asyncio.coroutine
def async_handle_fan_service(service):
- """Hande service call for fans."""
+ """Handle service call for fans."""
method = SERVICE_TO_METHOD.get(service.service)
params = service.data.copy()
diff --git a/homeassistant/components/fan/comfoconnect.py b/homeassistant/components/fan/comfoconnect.py
index ab32e588c03..c6d1232801f 100644
--- a/homeassistant/components/fan/comfoconnect.py
+++ b/homeassistant/components/fan/comfoconnect.py
@@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class ComfoConnectFan(FanEntity):
"""Representation of the ComfoConnect fan platform."""
- def __init__(self, hass, name, ccb: ComfoConnectBridge):
+ def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None:
"""Initialize the ComfoConnect fan."""
from pycomfoconnect import SENSOR_FAN_SPEED_MODE
@@ -93,7 +93,7 @@ class ComfoConnectFan(FanEntity):
speed = SPEED_LOW
self.set_speed(speed)
- def turn_off(self) -> None:
+ def turn_off(self, **kwargs) -> None:
"""Turn off the fan (to away)."""
self.set_speed(SPEED_OFF)
diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py
index 85e603c8c81..e6f9424d852 100644
--- a/homeassistant/components/fan/insteon_local.py
+++ b/homeassistant/components/fan/insteon_local.py
@@ -60,7 +60,7 @@ class InsteonLocalFanDevice(FanEntity):
@property
def unique_id(self):
"""Return the ID of this Insteon node."""
- return 'insteon_local_{}_fan'.format(self.node.device_id)
+ return self.node.device_id
@property
def speed(self) -> str:
diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py
index 910e33627a6..942aff4ec57 100644
--- a/homeassistant/components/fan/xiaomi_miio.py
+++ b/homeassistant/components/fan/xiaomi_miio.py
@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.3.4']
+REQUIREMENTS = ['python-miio==0.3.5']
ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity'
@@ -200,7 +200,7 @@ class XiaomiAirPurifier(FanEntity):
@asyncio.coroutine
def _try_command(self, mask_error, func, *args, **kwargs):
- """Call a air purifier command handling error messages."""
+ """Call an air purifier command handling error messages."""
from miio import DeviceException
try:
result = yield from self.hass.async_add_job(
diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py
index 3d73901b4d8..2c0e146491a 100644
--- a/homeassistant/components/feedreader.py
+++ b/homeassistant/components/feedreader.py
@@ -153,8 +153,7 @@ class StoredData(object):
with self._lock, open(self._data_file, 'rb') as myfile:
self._data = pickle.load(myfile) or {}
self._cache_outdated = False
- # pylint: disable=bare-except
- except:
+ except: # noqa: E722 # pylint: disable=bare-except
_LOGGER.error("Error loading data from pickled file %s",
self._data_file)
@@ -172,8 +171,7 @@ class StoredData(object):
url, self._data_file)
try:
pickle.dump(self._data, myfile)
- # pylint: disable=bare-except
- except:
+ except: # noqa: E722 # pylint: disable=bare-except
_LOGGER.error(
"Error saving pickled data to %s", self._data_file)
self._cache_outdated = True
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index a601dcbdc51..eedd33478a7 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -23,7 +23,7 @@ from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
from homeassistant.core import callback
from homeassistant.loader import bind_hass
-REQUIREMENTS = ['home-assistant-frontend==20180130.0', 'user-agents==1.1.0']
+REQUIREMENTS = ['home-assistant-frontend==20180209.0', 'user-agents==1.1.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@@ -408,7 +408,7 @@ def async_setup_themes(hass, themes):
@callback
def set_theme(call):
- """Set backend-prefered theme."""
+ """Set backend-preferred theme."""
data = call.data
name = data[CONF_NAME]
if name == DEFAULT_THEME or name in hass.data[DATA_THEMES]:
@@ -585,11 +585,13 @@ def _is_latest(js_option, request):
return useragent.os.version[0] >= 12
family_min_version = {
- 'Chrome': 50, # Probably can reduce this
- 'Firefox': 43, # Array.protopype.includes added in 43
- 'Opera': 40, # Probably can reduce this
- 'Edge': 14, # Array.protopype.includes added in 14
- 'Safari': 10, # many features not supported by 9
+ 'Chrome': 54, # Object.values
+ 'Chrome Mobile': 54,
+ 'Firefox': 47, # Object.values
+ 'Firefox Mobile': 47,
+ 'Opera': 41, # Object.values
+ 'Edge': 14, # Array.prototype.includes added in 14
+ 'Safari': 10, # Many features not supported by 9
}
version = family_min_version.get(useragent.browser.family)
return version and useragent.browser.version[0] >= version
diff --git a/homeassistant/components/goalfeed.py b/homeassistant/components/goalfeed.py
new file mode 100644
index 00000000000..f360d4ffba9
--- /dev/null
+++ b/homeassistant/components/goalfeed.py
@@ -0,0 +1,62 @@
+"""
+Component for the Goalfeed service.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/goalfeed/
+"""
+import json
+
+import requests
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
+
+REQUIREMENTS = ['pysher==0.2.0']
+
+DOMAIN = 'goalfeed'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+GOALFEED_HOST = 'feed.goalfeed.ca'
+GOALFEED_AUTH_ENDPOINT = 'https://goalfeed.ca/feed/auth'
+GOALFEED_APP_ID = 'bfd4ed98c1ff22c04074'
+
+
+def setup(hass, config):
+ """Set up the Goalfeed component."""
+ import pysher
+ conf = config[DOMAIN]
+ username = conf.get(CONF_USERNAME)
+ password = conf.get(CONF_PASSWORD)
+
+ def goal_handler(data):
+ """Handle goal events."""
+ goal = json.loads(json.loads(data))
+
+ hass.bus.fire('goal', event_data=goal)
+
+ def connect_handler(data):
+ """Handle connection."""
+ post_data = {
+ 'username': username,
+ 'password': password,
+ 'connection_info': data}
+ resp = requests.post(GOALFEED_AUTH_ENDPOINT, post_data,
+ timeout=30).json()
+
+ channel = pusher.subscribe('private-goals', resp['auth'])
+ channel.bind('goal', goal_handler)
+
+ pusher = pysher.Pusher(GOALFEED_APP_ID, secure=False, port=8080,
+ custom_host=GOALFEED_HOST)
+
+ pusher.connection.bind('pusher:connection_established', connect_handler)
+ pusher.connect()
+
+ return True
diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py
index f7923067270..30151ee1a56 100644
--- a/homeassistant/components/google.py
+++ b/homeassistant/components/google.py
@@ -128,7 +128,7 @@ def do_authentication(hass, config):
"""Keep trying to validate the user_code until it expires."""
if now >= dt.as_local(dev_flow.user_code_expiry):
hass.components.persistent_notification.create(
- 'Authenication code expired, please restart '
+ 'Authentication code expired, please restart '
'Home-Assistant and try again',
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index d8e9f668c8e..b718c009160 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -37,6 +37,7 @@ from .const import (
)
HANDLERS = Registry()
+QUERY_HANDLERS = Registry()
_LOGGER = logging.getLogger(__name__)
# Mapping is [actions schema, primary trait, optional features]
@@ -79,7 +80,7 @@ class SmartHomeError(Exception):
"""Log error code."""
super(SmartHomeError, self).__init__(msg)
_LOGGER.error(
- "An error has ocurred in Google SmartHome: %s."
+ "An error has occurred in Google SmartHome: %s."
"Error code: %s", msg, code
)
self.code = code
@@ -96,7 +97,7 @@ class Config:
def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
- """Convert a hass entity into an google actions device."""
+ """Convert a hass entity into a google actions device."""
entity_config = config.entity_config.get(entity.entity_id, {})
google_domain = entity_config.get(CONF_TYPE)
class_data = MAPPING_COMPONENT.get(
@@ -177,120 +178,145 @@ def entity_to_device(entity: Entity, config: Config, units: UnitSystem):
return device
-def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
- """Take an entity and return a properly formatted device object."""
- def celsius(deg: Optional[float]) -> Optional[float]:
- """Convert a float to Celsius and rounds to one decimal place."""
- if deg is None:
- return None
- return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
+def celsius(deg: Optional[float], units: UnitSystem) -> Optional[float]:
+ """Convert a float to Celsius and rounds to one decimal place."""
+ if deg is None:
+ return None
+ return round(METRIC_SYSTEM.temperature(deg, units.temperature_unit), 1)
- if entity.domain == sensor.DOMAIN:
- entity_config = config.entity_config.get(entity.entity_id, {})
- google_domain = entity_config.get(CONF_TYPE)
- if google_domain == climate.DOMAIN:
- # check if we have a string value to convert it to number
- value = entity.state
- if isinstance(entity.state, str):
- try:
- value = float(value)
- except ValueError:
- value = None
-
- if value is None:
- raise SmartHomeError(
- ERROR_NOT_SUPPORTED,
- "Invalid value {} for the climate sensor"
- .format(entity.state)
- )
-
- # detect if we report temperature or humidity
- unit_of_measurement = entity.attributes.get(
- ATTR_UNIT_OF_MEASUREMENT,
- units.temperature_unit
- )
- if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]:
- value = celsius(value)
- attr = 'thermostatTemperatureAmbient'
- elif unit_of_measurement == '%':
- attr = 'thermostatHumidityAmbient'
- else:
- raise SmartHomeError(
- ERROR_NOT_SUPPORTED,
- "Unit {} is not supported by the climate sensor"
- .format(unit_of_measurement)
- )
-
- return {attr: value}
+@QUERY_HANDLERS.register(sensor.DOMAIN)
+def query_response_sensor(
+ entity: Entity, config: Config, units: UnitSystem) -> dict:
+ """Convert a sensor entity to a QUERY response."""
+ entity_config = config.entity_config.get(entity.entity_id, {})
+ google_domain = entity_config.get(CONF_TYPE)
+ if google_domain != climate.DOMAIN:
raise SmartHomeError(
ERROR_NOT_SUPPORTED,
"Sensor type {} is not supported".format(google_domain)
)
- if entity.domain == climate.DOMAIN:
- mode = entity.attributes.get(climate.ATTR_OPERATION_MODE).lower()
- if mode not in CLIMATE_SUPPORTED_MODES:
- mode = 'heat'
- response = {
- 'thermostatMode': mode,
- 'thermostatTemperatureSetpoint':
- celsius(entity.attributes.get(climate.ATTR_TEMPERATURE)),
- 'thermostatTemperatureAmbient':
- celsius(entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)),
- 'thermostatTemperatureSetpointHigh':
- celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)),
- 'thermostatTemperatureSetpointLow':
- celsius(entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)),
- 'thermostatHumidityAmbient':
- entity.attributes.get(climate.ATTR_CURRENT_HUMIDITY),
- }
- return {k: v for k, v in response.items() if v is not None}
+ # check if we have a string value to convert it to number
+ value = entity.state
+ if isinstance(entity.state, str):
+ try:
+ value = float(value)
+ except ValueError:
+ value = None
- final_state = entity.state != STATE_OFF
- final_brightness = entity.attributes.get(light.ATTR_BRIGHTNESS, 255
- if final_state else 0)
+ if value is None:
+ raise SmartHomeError(
+ ERROR_NOT_SUPPORTED,
+ "Invalid value {} for the climate sensor"
+ .format(entity.state)
+ )
- if entity.domain == media_player.DOMAIN:
- level = entity.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL, 1.0
- if final_state else 0.0)
- # Convert 0.0-1.0 to 0-255
- final_brightness = round(min(1.0, level) * 255)
+ # detect if we report temperature or humidity
+ unit_of_measurement = entity.attributes.get(
+ ATTR_UNIT_OF_MEASUREMENT,
+ units.temperature_unit
+ )
+ if unit_of_measurement in [TEMP_FAHRENHEIT, TEMP_CELSIUS]:
+ value = celsius(value, units)
+ attr = 'thermostatTemperatureAmbient'
+ elif unit_of_measurement == '%':
+ attr = 'thermostatHumidityAmbient'
+ else:
+ raise SmartHomeError(
+ ERROR_NOT_SUPPORTED,
+ "Unit {} is not supported by the climate sensor"
+ .format(unit_of_measurement)
+ )
- if final_brightness is None:
- final_brightness = 255 if final_state else 0
+ return {attr: value}
- final_brightness = 100 * (final_brightness / 255)
- query_response = {
- "on": final_state,
- "online": True,
- "brightness": int(final_brightness)
+@QUERY_HANDLERS.register(climate.DOMAIN)
+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()
+ if mode not in CLIMATE_SUPPORTED_MODES:
+ mode = 'heat'
+ attrs = entity.attributes
+ response = {
+ 'thermostatMode': mode,
+ 'thermostatTemperatureSetpoint':
+ celsius(attrs.get(climate.ATTR_TEMPERATURE), units),
+ 'thermostatTemperatureAmbient':
+ celsius(attrs.get(climate.ATTR_CURRENT_TEMPERATURE), units),
+ 'thermostatTemperatureSetpointHigh':
+ celsius(attrs.get(climate.ATTR_TARGET_TEMP_HIGH), units),
+ 'thermostatTemperatureSetpointLow':
+ celsius(attrs.get(climate.ATTR_TARGET_TEMP_LOW), units),
+ 'thermostatHumidityAmbient':
+ attrs.get(climate.ATTR_CURRENT_HUMIDITY),
}
+ return {k: v for k, v in response.items() if v is not None}
+
+
+@QUERY_HANDLERS.register(media_player.DOMAIN)
+def query_response_media_player(
+ entity: Entity, config: Config, units: UnitSystem) -> dict:
+ """Convert a media_player entity to a QUERY response."""
+ level = entity.attributes.get(
+ media_player.ATTR_MEDIA_VOLUME_LEVEL,
+ 1.0 if entity.state != STATE_OFF else 0.0)
+ # Convert 0.0-1.0 to 0-255
+ brightness = int(level * 100)
+
+ return {'brightness': brightness}
+
+
+@QUERY_HANDLERS.register(light.DOMAIN)
+def query_response_light(
+ entity: Entity, config: Config, units: UnitSystem) -> dict:
+ """Convert a light entity to a QUERY response."""
+ response = {} # type: Dict[str, Any]
+
+ brightness = entity.attributes.get(light.ATTR_BRIGHTNESS)
+ if brightness is not None:
+ response['brightness'] = int(100 * (brightness / 255))
supported_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported_features & \
(light.SUPPORT_COLOR_TEMP | light.SUPPORT_RGB_COLOR):
- query_response["color"] = {}
+ response['color'] = {}
if entity.attributes.get(light.ATTR_COLOR_TEMP) is not None:
- query_response["color"]["temperature"] = \
+ response['color']['temperature'] = \
int(round(color.color_temperature_mired_to_kelvin(
entity.attributes.get(light.ATTR_COLOR_TEMP))))
if entity.attributes.get(light.ATTR_COLOR_NAME) is not None:
- query_response["color"]["name"] = \
+ response['color']['name'] = \
entity.attributes.get(light.ATTR_COLOR_NAME)
if entity.attributes.get(light.ATTR_RGB_COLOR) is not None:
color_rgb = entity.attributes.get(light.ATTR_RGB_COLOR)
if color_rgb is not None:
- query_response["color"]["spectrumRGB"] = \
+ response['color']['spectrumRGB'] = \
int(color.color_rgb_to_hex(
color_rgb[0], color_rgb[1], color_rgb[2]), 16)
- return query_response
+ return response
+
+
+def query_device(entity: Entity, config: Config, units: UnitSystem) -> dict:
+ """Take an entity and return a properly formatted device object."""
+ state = entity.state != STATE_OFF
+ defaults = {
+ 'on': state,
+ 'online': True
+ }
+
+ handler = QUERY_HANDLERS.get(entity.domain)
+ if callable(handler):
+ defaults.update(handler(entity, config, units))
+
+ return defaults
# erroneous bug on old pythons and pylint
@@ -429,7 +455,7 @@ def async_devices_query(hass, config, payload):
devices = {}
for device in payload.get('devices', []):
devid = device.get('id')
- # In theory this should never happpen
+ # In theory this should never happen
if not devid:
_LOGGER.error('Device missing ID: %s', device)
continue
@@ -438,11 +464,11 @@ def async_devices_query(hass, config, payload):
if not state:
# If we can't find a state, the device is offline
devices[devid] = {'online': False}
-
- try:
- devices[devid] = query_device(state, config, hass.config.units)
- except SmartHomeError as error:
- devices[devid] = {'errorCode': error.code}
+ else:
+ try:
+ devices[devid] = query_device(state, config, hass.config.units)
+ except SmartHomeError as error:
+ devices[devid] = {'errorCode': error.code}
return {'devices': devices}
diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py
index a8529f18b69..5e4dfdb0bdc 100644
--- a/homeassistant/components/group/__init__.py
+++ b/homeassistant/components/group/__init__.py
@@ -247,7 +247,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None):
@asyncio.coroutine
def async_setup(hass, config):
- """Set up all groups found definded in the configuration."""
+ """Set up all groups found defined in the configuration."""
component = hass.data.get(DOMAIN)
if component is None:
@@ -371,7 +371,6 @@ def async_setup(hass, config):
@asyncio.coroutine
def _async_process_config(hass, config, component):
"""Process group configuration."""
- groups = []
for object_id, conf in config.get(DOMAIN, {}).items():
name = conf.get(CONF_NAME, object_id)
entity_ids = conf.get(CONF_ENTITIES) or []
@@ -381,13 +380,9 @@ def _async_process_config(hass, config, component):
# Don't create tasks and await them all. The order is important as
# groups get a number based on creation order.
- group = yield from Group.async_create_group(
+ yield from Group.async_create_group(
hass, name, entity_ids, icon=icon, view=view,
control=control, object_id=object_id)
- groups.append(group)
-
- if groups:
- yield from component.async_add_entities(groups)
class Group(Entity):
diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py
index f94dd8816a7..8e2464d0922 100644
--- a/homeassistant/components/hdmi_cec.py
+++ b/homeassistant/components/hdmi_cec.py
@@ -320,7 +320,7 @@ def setup(hass: HomeAssistant, base_config):
class CecDevice(Entity):
"""Representation of a HDMI CEC device entity."""
- def __init__(self, hass: HomeAssistant, device, logical):
+ def __init__(self, hass: HomeAssistant, device, logical) -> None:
"""Initialize the device."""
self._device = device
self.hass = hass
diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py
index db2a43d8728..9c08984a23e 100644
--- a/homeassistant/components/homematic/__init__.py
+++ b/homeassistant/components/homematic/__init__.py
@@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.loader import bind_hass
-REQUIREMENTS = ['pyhomematic==0.1.38']
+REQUIREMENTS = ['pyhomematic==0.1.39']
DOMAIN = 'homematic'
_LOGGER = logging.getLogger(__name__)
@@ -218,7 +218,7 @@ SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
@bind_hass
def virtualkey(hass, address, channel, param, interface=None):
- """Send virtual keypress to homematic controlller."""
+ """Send virtual keypress to homematic controller."""
data = {
ATTR_ADDRESS: address,
ATTR_CHANNEL: channel,
@@ -256,7 +256,7 @@ def set_device_value(hass, address, channel, param, value, interface=None):
@bind_hass
def set_install_mode(hass, interface, mode=None, time=None, address=None):
- """Call setInstallMode XML-RPC method of supplied inteface."""
+ """Call setInstallMode XML-RPC method of supplied interface."""
data = {
key: value for key, value in (
(ATTR_INTERFACE, interface),
@@ -466,7 +466,7 @@ def _system_callback_handler(hass, config, src, *args):
hass, discovery_type, addresses, interface)
# When devices of this type are found
- # they are setup in HASS and an discovery event is fired
+ # they are setup in HASS and a discovery event is fired
if found_devices:
discovery.load_platform(hass, component_name, DOMAIN, {
ATTR_DISCOVER_DEVICES: found_devices
@@ -665,7 +665,7 @@ class HMHub(Entity):
self.schedule_update_ha_state()
def _update_variables(self, now):
- """Retrive all variable data and update hmvariable states."""
+ """Retrieve all variable data and update hmvariable states."""
variables = self._homematic.getAllSystemVariables(self._name)
if variables is None:
return
diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml
index bf4d99af9e7..c2946b51842 100644
--- a/homeassistant/components/homematic/services.yaml
+++ b/homeassistant/components/homematic/services.yaml
@@ -13,7 +13,7 @@ virtualkey:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
example: PRESS_LONG
interface:
- description: (Optional) for set a interface value.
+ description: (Optional) for set an interface value.
example: Interfaces name from config
set_variable_value:
@@ -42,7 +42,7 @@ set_device_value:
description: Event to send i.e. PRESS_LONG, PRESS_SHORT
example: PRESS_LONG
interface:
- description: (Optional) for set a interface value
+ description: (Optional) for set an interface value
example: Interfaces name from config
value:
description: New value
diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py
index ce5bfca3ac1..a6a412b6ba2 100644
--- a/homeassistant/components/http/auth.py
+++ b/homeassistant/components/http/auth.py
@@ -23,7 +23,7 @@ def auth_middleware(request, handler):
# If no password set, just always set authenticated=True
if request.app['hass'].http.api_password is None:
request[KEY_AUTHENTICATED] = True
- return handler(request)
+ return (yield from handler(request))
# Check authentication
authenticated = False
@@ -46,7 +46,7 @@ def auth_middleware(request, handler):
authenticated = True
request[KEY_AUTHENTICATED] = authenticated
- return handler(request)
+ return (yield from handler(request))
def is_trusted_ip(request):
diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py
index c9b094e3f2e..b34df1897f0 100644
--- a/homeassistant/components/http/static.py
+++ b/homeassistant/components/http/static.py
@@ -6,7 +6,7 @@ from aiohttp import hdrs
from aiohttp.web import FileResponse, middleware
from aiohttp.web_exceptions import HTTPNotFound
from aiohttp.web_urldispatcher import StaticResource
-from yarl import unquote
+from yarl import URL
_FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE)
@@ -16,7 +16,7 @@ class CachingStaticResource(StaticResource):
@asyncio.coroutine
def _handle(self, request):
- filename = unquote(request.match_info['filename'])
+ filename = URL(request.match_info['filename']).path
try:
# PyLint is wrong about resolve not being a member.
# pylint: disable=no-member
@@ -67,7 +67,7 @@ def staticresource_middleware(request, handler):
"""Middleware to strip out fingerprint from fingerprinted assets."""
path = request.path
if not path.startswith('/static/') and not path.startswith('/frontend'):
- return handler(request)
+ return (yield from handler(request))
fingerprinted = _FINGERPRINT.match(request.match_info['filename'])
@@ -75,4 +75,4 @@ def staticresource_middleware(request, handler):
request.match_info['filename'] = \
'{}.{}'.format(*fingerprinted.groups())
- return handler(request)
+ return (yield from handler(request))
diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py
index 48827851f92..999dda42015 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):
+ product: Element=None) -> None:
"""Initialize IHC attributes."""
self.ihc_controller = ihc_controller
self._name = name
diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py
index 646bfcf421f..2c2b8364823 100644
--- a/homeassistant/components/image_processing/__init__.py
+++ b/homeassistant/components/image_processing/__init__.py
@@ -60,7 +60,7 @@ SERVICE_SCAN_SCHEMA = vol.Schema({
@bind_hass
def scan(hass, entity_id=None):
- """Force process a image."""
+ """Force process an image."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else None
hass.services.call(DOMAIN, SERVICE_SCAN, data)
diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py
index 40aac61914b..6770ff1bdf6 100644
--- a/homeassistant/components/image_processing/microsoft_face_detect.py
+++ b/homeassistant/components/image_processing/microsoft_face_detect.py
@@ -36,7 +36,7 @@ def validate_attributes(list_attributes):
"""Validate face attributes."""
for attr in list_attributes:
if attr not in SUPPORTED_ATTRIBUTES:
- raise vol.Invalid("Invalid attribtue {0}".format(attr))
+ raise vol.Invalid("Invalid attribute {0}".format(attr))
return list_attributes
diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py
index 0cdd1675274..258731326ee 100644
--- a/homeassistant/components/image_processing/microsoft_face_identify.py
+++ b/homeassistant/components/image_processing/microsoft_face_identify.py
@@ -201,7 +201,7 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
return
# Parse data
- knwon_faces = []
+ known_faces = []
total = 0
for face in detect:
total += 1
@@ -215,9 +215,9 @@ class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity):
name = s_name
break
- knwon_faces.append({
+ known_faces.append({
ATTR_NAME: name,
ATTR_CONFIDENCE: data['confidence'] * 100,
})
- self.async_process_faces(knwon_faces, total)
+ self.async_process_faces(known_faces, total)
diff --git a/homeassistant/components/image_processing/openalpr_cloud.py b/homeassistant/components/image_processing/openalpr_cloud.py
index 2fdc3d72f2e..dbf36dcd86e 100644
--- a/homeassistant/components/image_processing/openalpr_cloud.py
+++ b/homeassistant/components/image_processing/openalpr_cloud.py
@@ -109,12 +109,14 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity):
websession = async_get_clientsession(self.hass)
params = self._params.copy()
- params['image_bytes'] = str(b64encode(image), 'utf-8')
+ body = {
+ 'image_bytes': str(b64encode(image), 'utf-8')
+ }
try:
with async_timeout.timeout(self.timeout, loop=self.hass.loop):
request = yield from websession.post(
- OPENALPR_API_URL, params=params
+ OPENALPR_API_URL, params=params, data=body
)
data = yield from request.json()
diff --git a/homeassistant/components/image_processing/seven_segments.py b/homeassistant/components/image_processing/seven_segments.py
index 60b2eadee92..1ef8a4bb847 100644
--- a/homeassistant/components/image_processing/seven_segments.py
+++ b/homeassistant/components/image_processing/seven_segments.py
@@ -66,7 +66,7 @@ class ImageProcessingSsocr(ImageProcessingEntity):
if name:
self._name = name
else:
- self._name = "SevenSegement OCR {0}".format(
+ self._name = "SevenSegment OCR {0}".format(
split_entity_id(camera_entity)[1])
self._state = None
diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py
index 82f98449411..526b8057ce1 100644
--- a/homeassistant/components/influxdb.py
+++ b/homeassistant/components/influxdb.py
@@ -4,10 +4,11 @@ A component which allows you to send data to an Influx database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/influxdb/
"""
-from datetime import timedelta
-from functools import partial, wraps
import logging
import re
+import queue
+import threading
+import time
import requests.exceptions
import voluptuous as vol
@@ -15,13 +16,13 @@ import voluptuous as vol
from homeassistant.const import (
CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE,
CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL,
- EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN)
+ EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, STATE_UNAVAILABLE,
+ STATE_UNKNOWN)
from homeassistant.helpers import state as state_helper
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_values import EntityValues
-from homeassistant.util import utcnow
-REQUIREMENTS = ['influxdb==4.1.1']
+REQUIREMENTS = ['influxdb==5.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -39,14 +40,17 @@ CONF_RETRY_QUEUE = 'retry_queue_limit'
DEFAULT_DATABASE = 'home_assistant'
DEFAULT_VERIFY_SSL = True
DOMAIN = 'influxdb'
+
TIMEOUT = 5
+RETRY_DELAY = 20
+QUEUE_BACKLOG_SECONDS = 10
COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema({
vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
- DOMAIN: vol.Schema({
+ DOMAIN: vol.All(cv.deprecated(CONF_RETRY_QUEUE), vol.Schema({
vol.Optional(CONF_HOST): cv.string,
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
@@ -78,7 +82,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}),
vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}):
vol.Schema({cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY}),
- }),
+ })),
}, extra=vol.ALLOW_EXTRA)
RE_DIGIT_TAIL = re.compile(r'^[^\.]*\d+\.?\d+[^\.]*$')
@@ -127,7 +131,6 @@ def setup(hass, config):
conf[CONF_COMPONENT_CONFIG_DOMAIN],
conf[CONF_COMPONENT_CONFIG_GLOB])
max_tries = conf.get(CONF_RETRY_COUNT)
- queue_limit = conf.get(CONF_RETRY_QUEUE)
try:
influx = InfluxDBClient(**kwargs)
@@ -137,22 +140,21 @@ def setup(hass, config):
_LOGGER.error("Database host is not accessible due to '%s', please "
"check your entries in the configuration file (host, "
"port, etc.) and verify that the database exists and is "
- "READ/WRITE.", exc)
+ "READ/WRITE", exc)
return False
- def influx_event_listener(event):
- """Listen for new messages on the bus and sends them to Influx."""
+ def influx_handle_event(event):
+ """Send an event to Influx."""
state = event.data.get('new_state')
if state is None or state.state in (
STATE_UNKNOWN, '', STATE_UNAVAILABLE) or \
- state.entity_id in blacklist_e or \
- state.domain in blacklist_d:
- return
+ state.entity_id in blacklist_e or state.domain in blacklist_d:
+ return True
try:
if (whitelist_e and state.entity_id not in whitelist_e) or \
(whitelist_d and state.domain not in whitelist_d):
- return
+ return True
_include_state = _include_value = False
@@ -222,93 +224,78 @@ def setup(hass, config):
json_body[0]['tags'].update(tags)
- _write_data(json_body)
-
- @RetryOnError(hass, retry_limit=max_tries, retry_delay=20,
- queue_limit=queue_limit)
- def _write_data(json_body):
- """Write the data."""
try:
influx.write_points(json_body)
- except exceptions.InfluxDBClientError:
- _LOGGER.exception("Error saving event %s to InfluxDB", json_body)
+ return True
+ except (exceptions.InfluxDBClientError, IOError):
+ return False
- hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener)
+ instance = hass.data[DOMAIN] = InfluxThread(
+ hass, influx_handle_event, max_tries)
+ instance.start()
+
+ def shutdown(event):
+ """Shut down the thread."""
+ instance.queue.put(None)
+ instance.join()
+
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
return True
-class RetryOnError(object):
- """A class for retrying a failed task a certain amount of tries.
+class InfluxThread(threading.Thread):
+ """A threaded event handler class."""
- This method decorator makes a method retrying on errors. If there was an
- uncaught exception, it schedules another try to execute the task after a
- retry delay. It does this up to the maximum number of retries.
+ def __init__(self, hass, event_handler, max_tries):
+ """Initialize the listener."""
+ threading.Thread.__init__(self, name='InfluxDB')
+ self.queue = queue.Queue()
+ self.event_handler = event_handler
+ self.max_tries = max_tries
+ hass.bus.listen(EVENT_STATE_CHANGED, self._event_listener)
- It can be used for all probable "self-healing" problems like network
- outages. The task will be rescheduled using HAs scheduling mechanism.
+ def _event_listener(self, event):
+ """Listen for new messages on the bus and queue them for Influx."""
+ item = (time.monotonic(), event)
+ self.queue.put(item)
- It takes a Hass instance, a maximum number of retries and a retry delay
- in seconds as arguments.
+ def run(self):
+ """Process incoming events."""
+ queue_seconds = QUEUE_BACKLOG_SECONDS + self.max_tries*RETRY_DELAY
- The queue limit defines the maximum number of calls that are allowed to
- be queued at a time. If this number is reached, every new call discards
- an old one.
- """
+ write_error = False
+ dropped = False
- def __init__(self, hass, retry_limit=0, retry_delay=20, queue_limit=100):
- """Initialize the decorator."""
- self.hass = hass
- self.retry_limit = retry_limit
- self.retry_delay = timedelta(seconds=retry_delay)
- self.queue_limit = queue_limit
+ while True:
+ item = self.queue.get()
- def __call__(self, method):
- """Decorate the target method."""
- from homeassistant.helpers.event import track_point_in_utc_time
+ if item is None:
+ self.queue.task_done()
+ return
- @wraps(method)
- def wrapper(*args, **kwargs):
- """Wrap method."""
- # pylint: disable=protected-access
- if not hasattr(wrapper, "_retry_queue"):
- wrapper._retry_queue = []
+ timestamp, event = item
+ age = time.monotonic() - timestamp
- def scheduled(retry=0, untrack=None, event=None):
- """Call the target method.
+ if age < queue_seconds:
+ for retry in range(self.max_tries+1):
+ if self.event_handler(event):
+ if write_error:
+ _LOGGER.error("Resumed writing to InfluxDB")
+ write_error = False
+ dropped = False
+ break
+ elif retry < self.max_tries:
+ time.sleep(RETRY_DELAY)
+ elif not write_error:
+ _LOGGER.error("Error writing to InfluxDB")
+ write_error = True
+ elif not dropped:
+ _LOGGER.warning("Dropping old events to catch up")
+ dropped = True
- It is called directly at the first time and then called
- scheduled within the Hass mainloop.
- """
- if untrack is not None:
- wrapper._retry_queue.remove(untrack)
+ self.queue.task_done()
- # pylint: disable=broad-except
- try:
- method(*args, **kwargs)
- except Exception as ex:
- if retry == self.retry_limit:
- raise
- if len(wrapper._retry_queue) >= self.queue_limit:
- last = wrapper._retry_queue.pop(0)
- if 'remove' in last:
- func = last['remove']
- func()
- if 'exc' in last:
- _LOGGER.error(
- "Retry queue overflow, drop oldest entry: %s",
- str(last['exc']))
-
- target = utcnow() + self.retry_delay
- tracking = {'target': target}
- remove = track_point_in_utc_time(self.hass,
- partial(scheduled,
- retry + 1,
- tracking),
- target)
- tracking['remove'] = remove
- tracking["exc"] = ex
- wrapper._retry_queue.append(tracking)
-
- scheduled()
- return wrapper
+ def block_till_done(self):
+ """Block till all events processed."""
+ self.queue.join()
diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py
index 43feeb8c4f4..56761b5af4e 100644
--- a/homeassistant/components/input_boolean.py
+++ b/homeassistant/components/input_boolean.py
@@ -137,7 +137,7 @@ class InputBoolean(ToggleEntity):
@property
def icon(self):
- """Returh the icon to be used for this entity."""
+ """Return the icon to be used for this entity."""
return self._icon
@property
diff --git a/homeassistant/components/input_text.py b/homeassistant/components/input_text.py
index 583181fe453..6433a01fb6d 100644
--- a/homeassistant/components/input_text.py
+++ b/homeassistant/components/input_text.py
@@ -11,7 +11,7 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
- ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME)
+ ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME, CONF_MODE)
from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -26,10 +26,14 @@ CONF_INITIAL = 'initial'
CONF_MIN = 'min'
CONF_MAX = 'max'
+MODE_TEXT = 'text'
+MODE_PASSWORD = 'password'
+
ATTR_VALUE = 'value'
ATTR_MIN = 'min'
ATTR_MAX = 'max'
ATTR_PATTERN = 'pattern'
+ATTR_MODE = 'mode'
SERVICE_SET_VALUE = 'set_value'
@@ -63,6 +67,8 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(ATTR_PATTERN): cv.string,
+ vol.Optional(CONF_MODE, default=MODE_TEXT):
+ vol.In([MODE_TEXT, MODE_PASSWORD]),
}, _cv_input_text)
})
}, required=True, extra=vol.ALLOW_EXTRA)
@@ -92,10 +98,11 @@ def async_setup(hass, config):
icon = cfg.get(CONF_ICON)
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
pattern = cfg.get(ATTR_PATTERN)
+ mode = cfg.get(CONF_MODE)
entities.append(InputText(
object_id, name, initial, minimum, maximum, icon, unit,
- pattern))
+ pattern, mode))
if not entities:
return False
@@ -122,7 +129,7 @@ class InputText(Entity):
"""Represent a text box."""
def __init__(self, object_id, name, initial, minimum, maximum, icon,
- unit, pattern):
+ unit, pattern, mode):
"""Initialize a text input."""
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
self._name = name
@@ -132,6 +139,7 @@ class InputText(Entity):
self._icon = icon
self._unit = unit
self._pattern = pattern
+ self._mode = mode
@property
def should_poll(self):
@@ -165,6 +173,7 @@ class InputText(Entity):
ATTR_MIN: self._minimum,
ATTR_MAX: self._maximum,
ATTR_PATTERN: self._pattern,
+ ATTR_MODE: self._mode,
}
@asyncio.coroutine
diff --git a/homeassistant/components/ios.py b/homeassistant/components/ios.py
index ebabcdb0e79..fe3c934659b 100644
--- a/homeassistant/components/ios.py
+++ b/homeassistant/components/ios.py
@@ -72,7 +72,7 @@ ATTR_DEVICE_SYSTEM_VERSION = 'systemVersion'
ATTR_DEVICE_TYPE = 'type'
ATTR_DEVICE_SYSTEM_NAME = 'systemName'
-ATTR_APP_BUNDLE_IDENTIFER = 'bundleIdentifer'
+ATTR_APP_BUNDLE_IDENTIFIER = 'bundleIdentifier'
ATTR_APP_BUILD_NUMBER = 'buildNumber'
ATTR_APP_VERSION_NUMBER = 'versionNumber'
@@ -136,7 +136,7 @@ IDENTIFY_DEVICE_SCHEMA = vol.Schema({
IDENTIFY_DEVICE_SCHEMA_CONTAINER = vol.All(dict, IDENTIFY_DEVICE_SCHEMA)
IDENTIFY_APP_SCHEMA = vol.Schema({
- vol.Required(ATTR_APP_BUNDLE_IDENTIFER): cv.string,
+ vol.Required(ATTR_APP_BUNDLE_IDENTIFIER): cv.string,
vol.Required(ATTR_APP_BUILD_NUMBER): cv.positive_int,
vol.Optional(ATTR_APP_VERSION_NUMBER): cv.string
}, extra=vol.ALLOW_EXTRA)
@@ -182,7 +182,7 @@ def enabled_push_ids():
"""Return a list of push enabled target push IDs."""
push_ids = list()
# pylint: disable=unused-variable
- for device_name, device in CONFIG_FILE[ATTR_DEVICES].items():
+ for device in CONFIG_FILE[ATTR_DEVICES].values():
if device.get(ATTR_PUSH_ID) is not None:
push_ids.append(device.get(ATTR_PUSH_ID))
return push_ids
diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota.py
index 237493c7919..442be6e22e7 100644
--- a/homeassistant/components/iota.py
+++ b/homeassistant/components/iota.py
@@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pyota==2.0.3']
+REQUIREMENTS = ['pyota==2.0.4']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py
index 28cfac39154..d85883e472a 100644
--- a/homeassistant/components/isy994.py
+++ b/homeassistant/components/isy994.py
@@ -123,7 +123,7 @@ SUPPORTED_DOMAINS = ['binary_sensor', 'sensor', 'lock', 'fan', 'cover',
'light', 'switch']
SUPPORTED_PROGRAM_DOMAINS = ['binary_sensor', 'lock', 'fan', 'cover', 'switch']
-# ISY Scenes are more like Swithes than Hass Scenes
+# ISY Scenes are more like Switches than Hass Scenes
# (they can turn off, and report their state)
SCENE_DOMAIN = 'switch'
diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py
index b761b04c705..cfeceb0c991 100644
--- a/homeassistant/components/light/__init__.py
+++ b/homeassistant/components/light/__init__.py
@@ -254,7 +254,7 @@ def async_setup(hass, config):
@asyncio.coroutine
def async_handle_light_service(service):
- """Hande a turn light on or off service call."""
+ """Handle a turn light on or off service call."""
# Get the validated data
params = service.data.copy()
diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py
index f214d47fa1b..5344c3dce6d 100644
--- a/homeassistant/components/light/avion.py
+++ b/homeassistant/components/light/avion.py
@@ -83,7 +83,7 @@ class AvionLight(Light):
@property
def unique_id(self):
"""Return the ID of this light."""
- return "{}.{}".format(self.__class__, self._address)
+ return self._address
@property
def name(self):
diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py
index 6b22190dce9..529917c36e2 100644
--- a/homeassistant/components/light/deconz.py
+++ b/homeassistant/components/light/deconz.py
@@ -9,7 +9,7 @@ import asyncio
from homeassistant.components.deconz import DOMAIN as DECONZ_DATA
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
- ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
+ ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH,
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light)
from homeassistant.core import callback
@@ -100,6 +100,11 @@ class DeconzLight(Light):
"""Return the name of the light."""
return self._light.name
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this light."""
+ return self._light.uniqueid
+
@property
def supported_features(self):
"""Flag supported features."""
@@ -129,6 +134,9 @@ class DeconzLight(Light):
data['xy'] = xyb[0], xyb[1]
data['bri'] = xyb[2]
+ if ATTR_XY_COLOR in kwargs:
+ data['xy'] = kwargs[ATTR_XY_COLOR]
+
if ATTR_BRIGHTNESS in kwargs:
data['bri'] = kwargs[ATTR_BRIGHTNESS]
diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py
index 6d502e15d6f..03441dd8ea6 100644
--- a/homeassistant/components/light/decora.py
+++ b/homeassistant/components/light/decora.py
@@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def retry(method):
"""Retry bluetooth commands."""
@wraps(method)
- def wrapper_retry(device, *args, **kwds):
+ def wrapper_retry(device, *args, **kwargs):
"""Try send command and retry on error."""
# pylint: disable=import-error
import decora
@@ -46,7 +46,7 @@ def retry(method):
if time.monotonic() - initial >= 10:
return None
try:
- return method(device, *args, **kwds)
+ return method(device, *args, **kwargs)
except (decora.decoraException, AttributeError,
bluepy.btle.BTLEException):
_LOGGER.warning("Decora connect error for device %s. "
@@ -88,7 +88,7 @@ class DecoraLight(Light):
@property
def unique_id(self):
"""Return the ID of this light."""
- return "{}.{}".format(self.__class__, self._address)
+ return self._address
@property
def name(self):
diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py
index c48de4deaf8..075b98117f8 100644
--- a/homeassistant/components/light/flux_led.py
+++ b/homeassistant/components/light/flux_led.py
@@ -46,7 +46,7 @@ EFFECT_GREEN_BLUE_CROSS_FADE = 'gb_cross_fade'
EFFECT_COLORSTROBE = 'colorstrobe'
EFFECT_RED_STROBE = 'red_strobe'
EFFECT_GREEN_STROBE = 'green_strobe'
-EFFECT_BLUE_STOBE = 'blue_strobe'
+EFFECT_BLUE_STROBE = 'blue_strobe'
EFFECT_YELLOW_STROBE = 'yellow_strobe'
EFFECT_CYAN_STROBE = 'cyan_strobe'
EFFECT_PURPLE_STROBE = 'purple_strobe'
@@ -68,7 +68,7 @@ EFFECT_MAP = {
EFFECT_COLORSTROBE: 0x30,
EFFECT_RED_STROBE: 0x31,
EFFECT_GREEN_STROBE: 0x32,
- EFFECT_BLUE_STOBE: 0x33,
+ EFFECT_BLUE_STROBE: 0x33,
EFFECT_YELLOW_STROBE: 0x34,
EFFECT_CYAN_STROBE: 0x35,
EFFECT_PURPLE_STROBE: 0x36,
@@ -167,11 +167,6 @@ class FluxLight(Light):
"""Return True if entity is available."""
return self._bulb is not None
- @property
- def unique_id(self):
- """Return the ID of this light."""
- return '{}.{}'.format(self.__class__, self._ipaddr)
-
@property
def name(self):
"""Return the name of the device if any."""
diff --git a/homeassistant/components/light/greenwave.py b/homeassistant/components/light/greenwave.py
index 5ad7fd4c317..8e9d93657ce 100644
--- a/homeassistant/components/light/greenwave.py
+++ b/homeassistant/components/light/greenwave.py
@@ -38,18 +38,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
tokenfile = hass.config.path('.greenwave')
if config.get(CONF_VERSION) == 3:
if os.path.exists(tokenfile):
- tokenfile = open(tokenfile)
- token = tokenfile.read()
- tokenfile.close()
+ with open(tokenfile) as tokenfile:
+ token = tokenfile.read()
else:
try:
token = greenwave.grab_token(host, 'hass', 'homeassistant')
except PermissionError:
_LOGGER.error('The Gateway Is Not In Sync Mode')
raise
- tokenfile = open(tokenfile, "w+")
- tokenfile.write(token)
- tokenfile.close()
+ with open(tokenfile, "w+") as tokenfile:
+ tokenfile.write(token)
else:
token = None
bulbs = greenwave.grab_bulbs(host, token)
diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py
index 8fafb88a7db..5ba162a20d2 100644
--- a/homeassistant/components/light/hive.py
+++ b/homeassistant/components/light/hive.py
@@ -137,5 +137,5 @@ class HiveDeviceLight(Light):
return supported_features
def update(self):
- """Update all Node data frome Hive."""
+ """Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py
index 807c19fffdb..a3db1ff30ff 100644
--- a/homeassistant/components/light/homematic.py
+++ b/homeassistant/components/light/homematic.py
@@ -1,5 +1,5 @@
"""
-Support for Homematic lighs.
+Support for Homematic lights.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.homematic/
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index cbabaafd3fb..07ba069d831 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -228,14 +228,7 @@ class HueLight(Light):
@property
def unique_id(self):
"""Return the ID of this Hue light."""
- lid = self.info.get('uniqueid')
-
- if lid is None:
- default_type = 'Group' if self.is_group else 'Light'
- ltype = self.info.get('type', default_type)
- lid = '{}.{}.{}'.format(self.name, ltype, self.light_id)
-
- return '{}.{}'.format(self.__class__, lid)
+ return self.info.get('uniqueid')
@property
def name(self):
diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py
index a2eed36a089..ba78546cf77 100644
--- a/homeassistant/components/light/iglo.py
+++ b/homeassistant/components/light/iglo.py
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the iGlo lighs."""
+ """Set up the iGlo lights."""
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
port = config.get(CONF_PORT)
diff --git a/homeassistant/components/light/ihc.py b/homeassistant/components/light/ihc.py
index f23ae77c8b2..ead0f153562 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):
+ 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/insteon_local.py b/homeassistant/components/light/insteon_local.py
index 88d621d4060..bd7814df8f3 100644
--- a/homeassistant/components/light/insteon_local.py
+++ b/homeassistant/components/light/insteon_local.py
@@ -57,7 +57,7 @@ class InsteonLocalDimmerDevice(Light):
@property
def unique_id(self):
"""Return the ID of this Insteon node."""
- return 'insteon_local_{}'.format(self.node.device_id)
+ return self.node.device_id
@property
def brightness(self):
diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py
index 51de9f03df5..f0ef0ce1b7e 100644
--- a/homeassistant/components/light/insteon_plm.py
+++ b/homeassistant/components/light/insteon_plm.py
@@ -101,7 +101,7 @@ class InsteonPLMDimmerDevice(Light):
@callback
def async_light_update(self, message):
"""Receive notification from transport that new data exists."""
- _LOGGER.info("Received update calback from PLM for %s", self._address)
+ _LOGGER.info("Received update callback from PLM for %s", self._address)
self._hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
diff --git a/homeassistant/components/light/isy994.py b/homeassistant/components/light/isy994.py
index a6191b05c7c..cee8155c322 100644
--- a/homeassistant/components/light/isy994.py
+++ b/homeassistant/components/light/isy994.py
@@ -27,7 +27,7 @@ def setup_platform(hass, config: ConfigType,
class ISYLightDevice(ISYDevice, Light):
- """Representation of an ISY994 light devie."""
+ """Representation of an ISY994 light device."""
def __init__(self, node: object) -> None:
"""Initialize the ISY994 light device."""
diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py
index 732cfe2a644..8c9e78ab2b0 100644
--- a/homeassistant/components/light/knx.py
+++ b/homeassistant/components/light/knx.py
@@ -57,7 +57,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
@callback
def async_add_devices_config(hass, config, async_add_devices):
- """Set up light for KNX platform configured within plattform."""
+ """Set up light for KNX platform configured within platform."""
import xknx
light = xknx.devices.Light(
hass.data[DATA_KNX].xknx,
diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py
index 090341e4255..71a261e3806 100644
--- a/homeassistant/components/light/lifx.py
+++ b/homeassistant/components/light/lifx.py
@@ -397,6 +397,11 @@ class LIFXLight(Light):
"""Return the availability of the device."""
return self.registered
+ @property
+ def unique_id(self):
+ """Return a unique ID."""
+ return self.device.mac_addr
+
@property
def name(self):
"""Return the name of the device."""
diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py
index aad2abdd183..0c6b1143bbd 100644
--- a/homeassistant/components/light/limitlessled.py
+++ b/homeassistant/components/light/limitlessled.py
@@ -16,7 +16,7 @@ from homeassistant.components.light import (
SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['limitlessled==1.0.8']
+REQUIREMENTS = ['limitlessled==1.0.9']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/light/mochad.py b/homeassistant/components/light/mochad.py
index efc62b05434..576e244103f 100644
--- a/homeassistant/components/light/mochad.py
+++ b/homeassistant/components/light/mochad.py
@@ -62,7 +62,7 @@ class MochadLight(Light):
@property
def brightness(self):
- """Return the birghtness of this light between 0..255."""
+ """Return the brightness of this light between 0..255."""
return self._brightness
def _get_device_status(self):
diff --git a/homeassistant/components/light/template.py b/homeassistant/components/light/template.py
index ed7ba1978cc..d4f2b93e6b5 100644
--- a/homeassistant/components/light/template.py
+++ b/homeassistant/components/light/template.py
@@ -44,11 +44,6 @@ LIGHT_SCHEMA = vol.Schema({
vol.Optional(CONF_ENTITY_ID): cv.entity_ids
})
-LIGHT_SCHEMA = vol.All(
- cv.deprecated(CONF_ENTITY_ID),
- LIGHT_SCHEMA,
-)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_LIGHTS): vol.Schema({cv.slug: LIGHT_SCHEMA}),
})
diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py
index 07d4b63e99a..c39748e4430 100644
--- a/homeassistant/components/light/tikteck.py
+++ b/homeassistant/components/light/tikteck.py
@@ -70,7 +70,7 @@ class TikteckLight(Light):
@property
def unique_id(self):
"""Return the ID of this light."""
- return "{}.{}".format(self.__class__, self._address)
+ return self._address
@property
def name(self):
diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py
index 30ad3a4d268..6aee02ee914 100644
--- a/homeassistant/components/light/tplink.py
+++ b/homeassistant/components/light/tplink.py
@@ -75,7 +75,7 @@ def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]:
class TPLinkSmartBulb(Light):
"""Representation of a TPLink Smart Bulb."""
- def __init__(self, smartbulb: 'SmartBulb', name):
+ def __init__(self, smartbulb: 'SmartBulb', name) -> None:
"""Initialize the bulb."""
self.smartbulb = smartbulb
self._name = name
diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py
index 693e40c0292..540c718b04d 100644
--- a/homeassistant/components/light/wemo.py
+++ b/homeassistant/components/light/wemo.py
@@ -76,8 +76,7 @@ class WemoLight(Light):
@property
def unique_id(self):
"""Return the ID of this light."""
- deviceid = self.device.uniqueID
- return '{}.{}'.format(self.__class__, deviceid)
+ return self.device.uniqueID
@property
def name(self):
@@ -176,7 +175,7 @@ class WemoDimmer(Light):
@property
def unique_id(self):
"""Return the ID of this WeMo dimmer."""
- return "{}.{}".format(self.__class__, self.wemo.serialnumber)
+ return self.wemo.serialnumber
@property
def name(self):
diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py
index d1664d13072..efe37d3d577 100644
--- a/homeassistant/components/light/xiaomi_aqara.py
+++ b/homeassistant/components/light/xiaomi_aqara.py
@@ -51,13 +51,13 @@ class XiaomiGatewayLight(XiaomiDevice, Light):
return True
rgbhexstr = "%x" % value
- if len(rgbhexstr) == 7:
- rgbhexstr = '0' + rgbhexstr
- elif len(rgbhexstr) != 8:
- _LOGGER.error('Light RGB data error.'
- ' Must be 8 characters. Received: %s', rgbhexstr)
+ if len(rgbhexstr) > 8:
+ _LOGGER.error("Light RGB data error."
+ " Can't be more than 8 characters. Received: %s",
+ rgbhexstr)
return False
+ rgbhexstr = rgbhexstr.zfill(8)
rgbhex = bytes.fromhex(rgbhexstr)
rgba = struct.unpack('BBBB', rgbhex)
brightness = rgba[0]
diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py
index 43c8860e77b..a3c5fa9f62e 100644
--- a/homeassistant/components/light/xiaomi_miio.py
+++ b/homeassistant/components/light/xiaomi_miio.py
@@ -7,6 +7,7 @@ https://home-assistant.io/components/light.xiaomi_miio/
import asyncio
from functools import partial
import logging
+from math import ceil
import voluptuous as vol
@@ -29,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.3.4']
+REQUIREMENTS = ['python-miio==0.3.5']
# The light does not accept cct values < 1
CCT_MIN = 1
@@ -204,11 +205,11 @@ class XiaomiPhilipsGenericLight(Light):
"""Turn the light on."""
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
- percent_brightness = int(100 * brightness / 255)
+ percent_brightness = ceil(100 * brightness / 255.0)
_LOGGER.debug(
"Setting brightness: %s %s%%",
- self.brightness, percent_brightness)
+ brightness, percent_brightness)
result = yield from self._try_command(
"Setting brightness failed: %s",
@@ -235,7 +236,7 @@ class XiaomiPhilipsGenericLight(Light):
_LOGGER.debug("Got new state: %s", state)
self._state = state.is_on
- self._brightness = int(255 * 0.01 * state.brightness)
+ self._brightness = ceil((255/100.0) * state.brightness)
except DeviceException as ex:
_LOGGER.error("Got exception while fetching the state: %s", ex)
@@ -306,11 +307,11 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
if ATTR_BRIGHTNESS in kwargs:
brightness = kwargs[ATTR_BRIGHTNESS]
- percent_brightness = int(100 * brightness / 255)
+ percent_brightness = ceil(100 * brightness / 255.0)
_LOGGER.debug(
"Setting brightness: %s %s%%",
- self.brightness, percent_brightness)
+ brightness, percent_brightness)
result = yield from self._try_command(
"Setting brightness failed: %s",
@@ -331,7 +332,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light):
_LOGGER.debug("Got new state: %s", state)
self._state = state.is_on
- self._brightness = int(255 * 0.01 * state.brightness)
+ self._brightness = ceil((255/100.0) * state.brightness)
self._color_temp = self.translate(
state.color_temperature,
CCT_MIN, CCT_MAX,
diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py
index c31bfec4927..33c84df14be 100644
--- a/homeassistant/components/light/yeelight.py
+++ b/homeassistant/components/light/yeelight.py
@@ -175,11 +175,6 @@ class YeelightLight(Light):
"""Return the list of supported effects."""
return YEELIGHT_EFFECT_LIST
- @property
- def unique_id(self) -> str:
- """Return the ID of this light."""
- return "{}.{}".format(self.__class__, self._ipaddr)
-
@property
def color_temp(self) -> int:
"""Return the color temperature."""
diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py
index b453218c7c9..7071c8c43bb 100644
--- a/homeassistant/components/light/zengge.py
+++ b/homeassistant/components/light/zengge.py
@@ -67,7 +67,7 @@ class ZenggeLight(Light):
@property
def unique_id(self):
"""Return the ID of this light."""
- return "{}.{}".format(self.__class__, self._address)
+ return self._address
@property
def name(self):
diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py
index c468d50ce6d..f50b3d7689b 100644
--- a/homeassistant/components/light/zha.py
+++ b/homeassistant/components/light/zha.py
@@ -61,7 +61,7 @@ class Light(zha.Entity, light.Light):
self._xy_color = None
self._brightness = None
- import bellows.zigbee.zcl.clusters as zcl_clusters
+ import zigpy.zcl.clusters as zcl_clusters
if zcl_clusters.general.LevelControl.cluster_id in self._in_clusters:
self._supported_features |= light.SUPPORT_BRIGHTNESS
self._supported_features |= light.SUPPORT_TRANSITION
diff --git a/homeassistant/components/lirc.py b/homeassistant/components/lirc.py
index 8b9ad0209da..ea4df658ef6 100644
--- a/homeassistant/components/lirc.py
+++ b/homeassistant/components/lirc.py
@@ -1,5 +1,5 @@
"""
-LIRC interface to receive signals from a infrared remote control.
+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/
diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py
index 80abce4ec3e..d03bbebd696 100644
--- a/homeassistant/components/lock/__init__.py
+++ b/homeassistant/components/lock/__init__.py
@@ -41,6 +41,11 @@ LOCK_SERVICE_SCHEMA = vol.Schema({
_LOGGER = logging.getLogger(__name__)
+PROP_TO_ATTR = {
+ 'changed_by': ATTR_CHANGED_BY,
+ 'code_format': ATTR_CODE_FORMAT,
+}
+
@bind_hass
def is_locked(hass, entity_id=None):
@@ -156,12 +161,11 @@ class LockDevice(Entity):
@property
def state_attributes(self):
"""Return the state attributes."""
- if self.code_format is None:
- return None
- state_attr = {
- ATTR_CODE_FORMAT: self.code_format,
- ATTR_CHANGED_BY: self.changed_by
- }
+ state_attr = {}
+ for prop, attr in PROP_TO_ATTR.items():
+ value = getattr(self, prop)
+ if value is not None:
+ state_attr[attr] = value
return state_attr
@property
diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py
index 1dc0861d737..9e1e2e54ad9 100644
--- a/homeassistant/components/logbook.py
+++ b/homeassistant/components/logbook.py
@@ -116,7 +116,7 @@ class LogbookView(HomeAssistantView):
extra_urls = ['/api/logbook/{datetime}']
def __init__(self, config):
- """Initilalize the logbook view."""
+ """Initialize the logbook view."""
self.config = config
@asyncio.coroutine
diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py
index 21898f7b16d..c2309401977 100644
--- a/homeassistant/components/logger.py
+++ b/homeassistant/components/logger.py
@@ -93,7 +93,7 @@ def async_setup(hass, config):
if LOGGER_LOGS in logfilter:
logs.update(logfilter[LOGGER_LOGS])
- # Add new logpoints mapped to correc severity
+ # Add new logpoints mapped to correct severity
for key, value in logpoints.items():
logs[key] = LOGSEVERITY[value]
diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py
index a1e68555649..8ff3746889e 100644
--- a/homeassistant/components/mailbox/__init__.py
+++ b/homeassistant/components/mailbox/__init__.py
@@ -133,7 +133,7 @@ class MailboxEntity(Entity):
class Mailbox(object):
- """Represent an mailbox device."""
+ """Represent a mailbox device."""
def __init__(self, hass, name):
"""Initialize mailbox object."""
diff --git a/homeassistant/components/mailbox/asterisk_mbox.py b/homeassistant/components/mailbox/asterisk_mbox.py
index a1953839f4f..2e807058edf 100644
--- a/homeassistant/components/mailbox/asterisk_mbox.py
+++ b/homeassistant/components/mailbox/asterisk_mbox.py
@@ -30,7 +30,7 @@ class AsteriskMailbox(Mailbox):
"""Asterisk VM Sensor."""
def __init__(self, hass, name):
- """Initialie Asterisk mailbox."""
+ """Initialize Asterisk mailbox."""
super().__init__(hass, name)
async_dispatcher_connect(
self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback)
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index de56c5140e9..f712007ccec 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.14']
+REQUIREMENTS = ['youtube_dl==2018.01.21']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index 91bcb4d8af0..06e89548785 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -88,6 +88,7 @@ MEDIA_TYPE_VIDEO = 'movie'
MEDIA_TYPE_EPISODE = 'episode'
MEDIA_TYPE_CHANNEL = 'channel'
MEDIA_TYPE_PLAYLIST = 'playlist'
+MEDIA_TYPE_URL = 'url'
SUPPORT_PAUSE = 1
SUPPORT_SEEK = 2
diff --git a/homeassistant/components/media_player/anthemav.py b/homeassistant/components/media_player/anthemav.py
index 293c6e51d52..474751c2574 100644
--- a/homeassistant/components/media_player/anthemav.py
+++ b/homeassistant/components/media_player/anthemav.py
@@ -49,7 +49,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
def async_anthemav_update_callback(message):
"""Receive notification from transport that new data exists."""
- _LOGGER.info("Received update calback from AVR: %s", message)
+ _LOGGER.info("Received update callback from AVR: %s", message)
hass.async_add_job(device.async_update_ha_state())
avr = yield from anthemav.Connection.create(
diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py
index ca6b152a37e..848c6abe91f 100644
--- a/homeassistant/components/media_player/bluesound.py
+++ b/homeassistant/components/media_player/bluesound.py
@@ -122,7 +122,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class BluesoundPlayer(MediaPlayerDevice):
- """Represenatation of a Bluesound Player."""
+ """Representation of a Bluesound Player."""
def __init__(self, hass, host, port=None, name=None, init_callback=None):
"""Initialize the media player."""
@@ -150,10 +150,10 @@ class BluesoundPlayer(MediaPlayerDevice):
self._port = DEFAULT_PORT
@staticmethod
- def _try_get_index(string, seach_string):
+ def _try_get_index(string, search_string):
"""Get the index."""
try:
- return string.index(seach_string)
+ return string.index(search_string)
except ValueError:
return -1
@@ -165,7 +165,7 @@ class BluesoundPlayer(MediaPlayerDevice):
try:
resp = yield from self.send_bluesound_command(
'SyncStatus', raise_timeout, raise_timeout)
- except:
+ except Exception:
raise
if not resp:
@@ -202,7 +202,7 @@ class BluesoundPlayer(MediaPlayerDevice):
except CancelledError:
_LOGGER.debug("Stopping the polling of node %s", self._name)
- except:
+ except Exception:
_LOGGER.exception("Unexpected error in %s", self._name)
raise
@@ -229,7 +229,7 @@ class BluesoundPlayer(MediaPlayerDevice):
_LOGGER.info("Node %s is offline, retrying later", self.host)
self._retry_remove = async_track_time_interval(
self._hass, self.async_init, NODE_RETRY_INITIATION)
- except:
+ except Exception:
_LOGGER.exception("Unexpected when initiating error in %s",
self.host)
raise
@@ -338,7 +338,7 @@ class BluesoundPlayer(MediaPlayerDevice):
@asyncio.coroutine
@Throttle(UPDATE_CAPTURE_INTERVAL)
def async_update_captures(self):
- """Update Capture cources."""
+ """Update Capture sources."""
resp = yield from self.send_bluesound_command(
'RadioBrowse?service=Capture')
if not resp:
diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py
index 2aaff646885..928062cb2dc 100644
--- a/homeassistant/components/media_player/cast.py
+++ b/homeassistant/components/media_player/cast.py
@@ -205,7 +205,7 @@ class CastDevice(MediaPlayerDevice):
@property
def media_album_artist(self):
- """Album arist of current playing media (Music track only)."""
+ """Album artist of current playing media (Music track only)."""
return self.media_status.album_artist if self.media_status else None
@property
diff --git a/homeassistant/components/media_player/clementine.py b/homeassistant/components/media_player/clementine.py
index d9688badcd1..057a23579ca 100644
--- a/homeassistant/components/media_player/clementine.py
+++ b/homeassistant/components/media_player/clementine.py
@@ -97,7 +97,7 @@ class ClementineDevice(MediaPlayerDevice):
self._track_artist = client.current_track['track_artist']
self._track_album_name = client.current_track['track_album']
- except:
+ except Exception:
self._state = STATE_OFF
raise
diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py
index fe25422360c..bcbee5c4ff7 100644
--- a/homeassistant/components/media_player/cmus.py
+++ b/homeassistant/components/media_player/cmus.py
@@ -77,7 +77,7 @@ class CmusDevice(MediaPlayerDevice):
"""Get the latest data and update the state."""
status = self.cmus.get_status_dict()
if not status:
- _LOGGER.warning("Recieved no status from cmus")
+ _LOGGER.warning("Received no status from cmus")
else:
self.status = status
diff --git a/homeassistant/components/media_player/denon.py b/homeassistant/components/media_player/denon.py
index 572405baa6e..d85bd51e7fb 100644
--- a/homeassistant/components/media_player/denon.py
+++ b/homeassistant/components/media_player/denon.py
@@ -108,7 +108,7 @@ class DenonDevice(MediaPlayerDevice):
if not line:
break
lines.append(line.decode('ASCII').strip())
- _LOGGER.debug("Recived: %s", line)
+ _LOGGER.debug("Received: %s", line)
if all_lines:
return lines
diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py
index ebb8a670488..a3fe62c5a42 100644
--- a/homeassistant/components/media_player/emby.py
+++ b/homeassistant/components/media_player/emby.py
@@ -182,7 +182,7 @@ class EmbyDevice(MediaPlayerDevice):
@property
def unique_id(self):
"""Return the id of this emby client."""
- return '{}.{}'.format(self.__class__, self.device_id)
+ return self.device_id
@property
def supports_remote_control(self):
@@ -273,7 +273,7 @@ class EmbyDevice(MediaPlayerDevice):
@property
def media_season(self):
- """Season of curent playing media (TV Show only)."""
+ """Season of current playing media (TV Show only)."""
return self.device.media_season
@property
diff --git a/homeassistant/components/media_player/hdmi_cec.py b/homeassistant/components/media_player/hdmi_cec.py
index 7054c83d36a..e1fffefed18 100644
--- a/homeassistant/components/media_player/hdmi_cec.py
+++ b/homeassistant/components/media_player/hdmi_cec.py
@@ -32,9 +32,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CecPlayerDevice(CecDevice, MediaPlayerDevice):
- """Representation of a HDMI device as a Media palyer."""
+ """Representation of a HDMI device as a Media player."""
- def __init__(self, hass: HomeAssistant, device, logical):
+ def __init__(self, hass: HomeAssistant, device, logical) -> None:
"""Initialize the HDMI device."""
CecDevice.__init__(self, hass, device, logical)
self.entity_id = "%s.%s_%s" % (
diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py
index 575ea414fa3..ca0979f1752 100644
--- a/homeassistant/components/media_player/itunes.py
+++ b/homeassistant/components/media_player/itunes.py
@@ -29,7 +29,7 @@ DOMAIN = 'itunes'
SUPPORT_ITUNES = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
- SUPPORT_PLAY_MEDIA | SUPPORT_PLAY
+ SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_TURN_OFF
SUPPORT_AIRPLAY = SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
@@ -115,6 +115,10 @@ class Itunes(object):
"""Skip back and returns the current state."""
return self._command('previous')
+ def stop(self):
+ """Stop playback and return the current state."""
+ return self._command('stop')
+
def play_playlist(self, playlist_id_or_name):
"""Set a playlist to be current and returns the current state."""
response = self._request('GET', '/playlists')
@@ -280,7 +284,7 @@ class ItunesDevice(MediaPlayerDevice):
"""Image url of current playing media."""
if self.player_state in (STATE_PLAYING, STATE_IDLE, STATE_PAUSED) and \
self.current_title is not None:
- return self.client.artwork_url()
+ return self.client.artwork_url() + '?id=' + self.content_id
return 'https://cloud.githubusercontent.com/assets/260/9829355' \
'/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png'
@@ -346,6 +350,11 @@ class ItunesDevice(MediaPlayerDevice):
response = self.client.play_playlist(media_id)
self.update_state(response)
+ def turn_off(self):
+ """Turn the media player off."""
+ response = self.client.stop()
+ self.update_state(response)
+
class AirPlayDevice(MediaPlayerDevice):
"""Representation an AirPlay device via an iTunes API instance."""
diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py
new file mode 100644
index 00000000000..3cf0ecdb232
--- /dev/null
+++ b/homeassistant/components/media_player/mediaroom.py
@@ -0,0 +1,201 @@
+"""
+Support for the Mediaroom Set-up-box.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/media_player.mediaroom/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.media_player import (
+ MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
+ SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA,
+ SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY,
+ SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE,
+ MediaPlayerDevice)
+from homeassistant.const import (
+ CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT,
+ STATE_PAUSED, STATE_PLAYING, STATE_STANDBY,
+ STATE_ON)
+import homeassistant.helpers.config_validation as cv
+REQUIREMENTS = ['pymediaroom==0.5']
+
+_LOGGER = logging.getLogger(__name__)
+
+NOTIFICATION_TITLE = 'Mediaroom Media Player Setup'
+NOTIFICATION_ID = 'mediaroom_notification'
+DEFAULT_NAME = 'Mediaroom STB'
+DEFAULT_TIMEOUT = 9
+DATA_MEDIAROOM = "mediaroom_known_stb"
+
+SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
+ SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \
+ SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \
+ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Mediaroom platform."""
+ hosts = []
+
+ known_hosts = hass.data.get(DATA_MEDIAROOM)
+ if known_hosts is None:
+ known_hosts = hass.data[DATA_MEDIAROOM] = []
+
+ host = config.get(CONF_HOST, None)
+ if host is None:
+ _LOGGER.info("Trying to discover Mediaroom STB")
+
+ from pymediaroom import Remote
+
+ host = Remote.discover(known_hosts)
+ if host is None:
+ _LOGGER.warning("Can't find any STB")
+ return
+ hosts.append(host)
+ known_hosts.append(host)
+
+ stbs = []
+
+ try:
+ for host in hosts:
+ stbs.append(MediaroomDevice(
+ config.get(CONF_NAME),
+ host,
+ config.get(CONF_OPTIMISTIC),
+ config.get(CONF_TIMEOUT)
+ ))
+
+ except ConnectionRefusedError:
+ hass.components.persistent_notification.create(
+ 'Error: Unable to initialize mediaroom at {}
'
+ 'Check its network connection or consider '
+ 'using auto discovery.
'
+ 'You will need to restart hass after fixing.'
+ ''.format(host),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+
+ add_devices(stbs)
+
+
+class MediaroomDevice(MediaPlayerDevice):
+ """Representation of a Mediaroom set-up-box on the network."""
+
+ def __init__(self, name, host, optimistic=False, timeout=DEFAULT_TIMEOUT):
+ """Initialize the device."""
+ from pymediaroom import Remote
+
+ self.stb = Remote(host, timeout=timeout)
+ _LOGGER.info(
+ "Found %s at %s%s", name, host,
+ " - I'm optimistic" if optimistic else "")
+ self._name = name
+ self._is_standby = not optimistic
+ self._current = None
+ self._optimistic = optimistic
+ self._state = STATE_STANDBY
+
+ def update(self):
+ """Retrieve latest state."""
+ if not self._optimistic:
+ self._is_standby = self.stb.get_standby()
+ if self._is_standby:
+ self._state = STATE_STANDBY
+ elif self._state not in [STATE_PLAYING, STATE_PAUSED]:
+ self._state = STATE_PLAYING
+ _LOGGER.debug(
+ "%s(%s) is [%s]",
+ self._name, self.stb.stb_ip, self._state)
+
+ def play_media(self, media_type, media_id, **kwargs):
+ """Play media."""
+ _LOGGER.debug(
+ "%s(%s) Play media: %s (%s)",
+ self._name, self.stb.stb_ip, media_id, media_type)
+ if media_type != MEDIA_TYPE_CHANNEL:
+ _LOGGER.error('invalid media type')
+ return
+ if media_id.isdigit():
+ media_id = int(media_id)
+ else:
+ return
+ self.stb.send_cmd(media_id)
+ self._state = STATE_PLAYING
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ # MediaPlayerDevice properties and methods
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+ @property
+ def supported_features(self):
+ """Flag media player features that are supported."""
+ return SUPPORT_MEDIAROOM
+
+ @property
+ def media_content_type(self):
+ """Return the content type of current playing media."""
+ return MEDIA_TYPE_CHANNEL
+
+ def turn_on(self):
+ """Turn on the receiver."""
+ self.stb.send_cmd('Power')
+ self._state = STATE_ON
+
+ def turn_off(self):
+ """Turn off the receiver."""
+ self.stb.send_cmd('Power')
+ self._state = STATE_STANDBY
+
+ def media_play(self):
+ """Send play command."""
+ _LOGGER.debug("media_play()")
+ self.stb.send_cmd('PlayPause')
+ self._state = STATE_PLAYING
+
+ def media_pause(self):
+ """Send pause command."""
+ self.stb.send_cmd('PlayPause')
+ self._state = STATE_PAUSED
+
+ def media_stop(self):
+ """Send stop command."""
+ self.stb.send_cmd('Stop')
+ self._state = STATE_PAUSED
+
+ def media_previous_track(self):
+ """Send Program Down command."""
+ self.stb.send_cmd('ProgDown')
+ self._state = STATE_PLAYING
+
+ def media_next_track(self):
+ """Send Program Up command."""
+ self.stb.send_cmd('ProgUp')
+ self._state = STATE_PLAYING
+
+ def volume_up(self):
+ """Send volume up command."""
+ self.stb.send_cmd('VolUp')
+
+ def volume_down(self):
+ """Send volume up command."""
+ self.stb.send_cmd('VolDown')
+
+ def mute_volume(self, mute):
+ """Send mute command."""
+ self.stb.send_cmd('Mute')
diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py
index d26fce0ea88..44d19ac6860 100644
--- a/homeassistant/components/media_player/monoprice.py
+++ b/homeassistant/components/media_player/monoprice.py
@@ -103,7 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class MonopriceZone(MediaPlayerDevice):
- """Representation of a a Monoprice amplifier zone."""
+ """Representation of a Monoprice amplifier zone."""
def __init__(self, monoprice, sources, zone_id, zone_name):
"""Initialize new zone."""
diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py
index 97ebe5be92b..432d9ce108f 100644
--- a/homeassistant/components/media_player/onkyo.py
+++ b/homeassistant/components/media_player/onkyo.py
@@ -6,6 +6,9 @@ https://home-assistant.io/components/media_player.onkyo/
"""
import logging
+# pylint: disable=unused-import
+from typing import List # noqa: F401
+
import voluptuous as vol
from homeassistant.components.media_player import (
diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py
index 8c946ec0f0f..21a897f4d35 100644
--- a/homeassistant/components/media_player/panasonic_viera.py
+++ b/homeassistant/components/media_player/panasonic_viera.py
@@ -11,14 +11,15 @@ import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY,
- SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MEDIA_TYPE_URL,
+ SUPPORT_PLAY_MEDIA, SUPPORT_STOP,
SUPPORT_VOLUME_STEP, MediaPlayerDevice, PLATFORM_SCHEMA)
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.2',
- 'wakeonlan==0.2.2']
+REQUIREMENTS = ['panasonic_viera==0.3',
+ 'wakeonlan==1.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -30,7 +31,8 @@ DEFAULT_PORT = 55000
SUPPORT_VIERATV = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
- SUPPORT_TURN_OFF | SUPPORT_PLAY
+ SUPPORT_TURN_OFF | SUPPORT_PLAY | \
+ SUPPORT_PLAY_MEDIA | SUPPORT_STOP
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
@@ -51,6 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
if discovery_info:
_LOGGER.debug('%s', discovery_info)
+ name = discovery_info.get('name')
host = discovery_info.get('host')
port = discovery_info.get('port')
remote = RemoteControl(host, port)
@@ -69,9 +72,9 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
def __init__(self, mac, name, remote):
"""Initialize the Panasonic device."""
- from wakeonlan import wol
+ import wakeonlan
# Save a reference to the imported class
- self._wol = wol
+ self._wol = wakeonlan
self._mac = mac
self._name = name
self._muted = False
@@ -183,3 +186,19 @@ class PanasonicVieraTVDevice(MediaPlayerDevice):
def media_previous_track(self):
"""Send the previous track command."""
self.send_key('NRC_REW-ONOFF')
+
+ def play_media(self, media_type, media_id, **kwargs):
+ """Play media."""
+ _LOGGER.debug("Play media: %s (%s)", media_id, media_type)
+
+ if media_type == MEDIA_TYPE_URL:
+ try:
+ self._remote.open_webpage(media_id)
+ except (TimeoutError, OSError):
+ self._state = STATE_OFF
+ else:
+ _LOGGER.warning("Unsupported media_type: %s", media_type)
+
+ def media_stop(self):
+ """Stop playback."""
+ self.send_key('NRC_CANCEL-ONOFF')
diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py
index d8450d31ea4..24981555007 100644
--- a/homeassistant/components/media_player/philips_js.py
+++ b/homeassistant/components/media_player/philips_js.py
@@ -12,10 +12,11 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK,
- SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE,
- SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice)
+ SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
+ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice)
from homeassistant.const import (
CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, STATE_UNKNOWN)
+from homeassistant.helpers.script import Script
from homeassistant.util import Throttle
REQUIREMENTS = ['ha-philipsjs==0.0.1']
@@ -30,14 +31,16 @@ SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \
SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY
+CONF_ON_ACTION = 'turn_on_action'
+
DEFAULT_DEVICE = 'default'
DEFAULT_HOST = '127.0.0.1'
DEFAULT_NAME = 'Philips TV'
-BASE_URL = 'http://{0}:1925/1/{1}'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
})
@@ -48,16 +51,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
+ turn_on_action = config.get(CONF_ON_ACTION)
tvapi = haphilipsjs.PhilipsTV(host)
+ on_script = Script(hass, turn_on_action) if turn_on_action else None
- add_devices([PhilipsTV(tvapi, name)])
+ add_devices([PhilipsTV(tvapi, name, on_script)])
class PhilipsTV(MediaPlayerDevice):
"""Representation of a Philips TV exposing the JointSpace API."""
- def __init__(self, tv, name):
+ def __init__(self, tv, name, on_script):
"""Initialize the Philips TV."""
self._tv = tv
self._name = name
@@ -74,6 +79,7 @@ class PhilipsTV(MediaPlayerDevice):
self._source_mapping = {}
self._watching_tv = None
self._channel_name = None
+ self._on_script = on_script
@property
def name(self):
@@ -88,9 +94,10 @@ class PhilipsTV(MediaPlayerDevice):
@property
def supported_features(self):
"""Flag media player features that are supported."""
+ is_supporting_turn_on = SUPPORT_TURN_ON if self._on_script else 0
if self._watching_tv:
- return SUPPORT_PHILIPS_JS_TV
- return SUPPORT_PHILIPS_JS
+ return SUPPORT_PHILIPS_JS_TV | is_supporting_turn_on
+ return SUPPORT_PHILIPS_JS | is_supporting_turn_on
@property
def state(self):
@@ -126,6 +133,11 @@ class PhilipsTV(MediaPlayerDevice):
"""Boolean if volume is currently muted."""
return self._muted
+ def turn_on(self):
+ """Turn on the device."""
+ if self._on_script:
+ self._on_script.run()
+
def turn_off(self):
"""Turn off the device."""
self._tv.sendKey('Standby')
diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py
index c96b0f3c2ae..b2a89341cf0 100644
--- a/homeassistant/components/media_player/plex.py
+++ b/homeassistant/components/media_player/plex.py
@@ -175,7 +175,7 @@ def setup_plexserver(
else:
plex_clients[machine_identifier].refresh(None, session)
- for machine_identifier, client in plex_clients.items():
+ for client in plex_clients.values():
# force devices to idle that do not have a valid session
if client.session is None:
client.force_idle()
@@ -459,8 +459,7 @@ class PlexClient(MediaPlayerDevice):
@property
def unique_id(self):
"""Return the id of this plex client."""
- return '{}.{}'.format(self.__class__, self.machine_identifier or
- self.name)
+ return self.machine_identifier
@property
def name(self):
diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py
index 57f25873ae7..4afd578211e 100644
--- a/homeassistant/components/media_player/samsungtv.py
+++ b/homeassistant/components/media_player/samsungtv.py
@@ -8,6 +8,9 @@ import logging
import socket
from datetime import timedelta
+import sys
+
+import subprocess
import voluptuous as vol
from homeassistant.components.media_player import (
@@ -20,7 +23,7 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv
from homeassistant.util import dt as dt_util
-REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==0.2.2']
+REQUIREMENTS = ['samsungctl==0.6.0', 'wakeonlan==1.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -89,13 +92,13 @@ class SamsungTVDevice(MediaPlayerDevice):
"""Initialize the Samsung device."""
from samsungctl import exceptions
from samsungctl import Remote
- from wakeonlan import wol
+ import wakeonlan
# Save a reference to the imported classes
self._exceptions_class = exceptions
self._remote_class = Remote
self._name = name
self._mac = mac
- self._wol = wol
+ self._wol = wakeonlan
# Assume that the TV is not muted
self._muted = False
# Assume that the TV is in Play mode
@@ -122,12 +125,19 @@ class SamsungTVDevice(MediaPlayerDevice):
def update(self):
"""Update state of device."""
+ if sys.platform == 'win32':
+ _ping_cmd = ['ping', '-n 1', '-w', '1000', self._config['host']]
+ else:
+ _ping_cmd = ['ping', '-n', '-q', '-c1', '-W1',
+ self._config['host']]
+
+ ping = subprocess.Popen(
+ _ping_cmd,
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.settimeout(self._config[CONF_TIMEOUT])
- sock.connect((self._config['host'], self._config['port']))
- self._state = STATE_ON
- except socket.error:
+ ping.communicate()
+ self._state = STATE_ON if ping.returncode == 0 else STATE_OFF
+ except subprocess.CalledProcessError:
self._state = STATE_OFF
def get_remote(self):
diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py
index 22f701de1cc..1fd61b3ead1 100644
--- a/homeassistant/components/media_player/squeezebox.py
+++ b/homeassistant/components/media_player/squeezebox.py
@@ -45,7 +45,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
SERVICE_CALL_METHOD = 'squeezebox_call_method'
-DATA_SQUEEZEBOX = 'squeexebox'
+DATA_SQUEEZEBOX = 'squeezebox'
+
+KNOWN_SERVERS = 'squeezebox_known_servers'
ATTR_PARAMETERS = 'parameters'
@@ -67,6 +69,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the squeezebox platform."""
import socket
+ known_servers = hass.data.get(KNOWN_SERVERS)
+ if known_servers is None:
+ hass.data[KNOWN_SERVERS] = known_servers = set()
+
if DATA_SQUEEZEBOX not in hass.data:
hass.data[DATA_SQUEEZEBOX] = []
@@ -92,6 +98,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"Could not communicate with %s:%d: %s", host, port, error)
return False
+ if ipaddr in known_servers:
+ return
+
+ known_servers.add(ipaddr)
_LOGGER.debug("Creating LMS object for %s", ipaddr)
lms = LogitechMediaServer(hass, host, port, username, password)
@@ -473,7 +483,7 @@ class SqueezeBoxDevice(MediaPlayerDevice):
return self.async_query('playlist', 'play', media_id)
def _add_uri_to_playlist(self, media_id):
- """Add a items to the existing playlist."""
+ """Add an item to the existing playlist."""
return self.async_query('playlist', 'add', media_id)
def async_set_shuffle(self, shuffle):
diff --git a/homeassistant/components/media_player/vlc.py b/homeassistant/components/media_player/vlc.py
index d3346495015..abd8252d813 100644
--- a/homeassistant/components/media_player/vlc.py
+++ b/homeassistant/components/media_player/vlc.py
@@ -9,8 +9,10 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
- SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET,
- SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC)
+ SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_STOP, SUPPORT_VOLUME_MUTE,
+ SUPPORT_VOLUME_SET, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA,
+ MEDIA_TYPE_MUSIC)
+
from homeassistant.const import (CONF_NAME, STATE_IDLE, STATE_PAUSED,
STATE_PLAYING)
import homeassistant.helpers.config_validation as cv
@@ -24,7 +26,7 @@ CONF_ARGUMENTS = 'arguments'
DEFAULT_NAME = 'Vlc'
SUPPORT_VLC = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
- SUPPORT_PLAY_MEDIA | SUPPORT_PLAY
+ SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_STOP
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME): cv.string,
@@ -146,6 +148,11 @@ class VlcDevice(MediaPlayerDevice):
self._vlc.pause()
self._state = STATE_PAUSED
+ def media_stop(self):
+ """Send stop command."""
+ self._vlc.stop()
+ self._state = STATE_IDLE
+
def play_media(self, media_type, media_id, **kwargs):
"""Play media from a URL or file."""
if not media_type == MEDIA_TYPE_MUSIC:
diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py
index eda0bc2b326..84b957533fe 100644
--- a/homeassistant/components/media_player/volumio.py
+++ b/homeassistant/components/media_player/volumio.py
@@ -3,7 +3,10 @@ Volumio Platform.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.volumio/
+
+Volumio rest API: https://volumio.github.io/docs/API/REST_API.html
"""
+from datetime import timedelta
import logging
import asyncio
import aiohttp
@@ -13,11 +16,13 @@ import voluptuous as vol
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP,
- SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC)
+ SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC,
+ SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST)
from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_IDLE, CONF_HOST, CONF_PORT, CONF_NAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.util import Throttle
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
@@ -30,8 +35,10 @@ TIMEOUT = 10
SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \
- SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY
+ SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | \
+ SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE | SUPPORT_CLEAR_PLAYLIST
+PLAYLIST_UPDATE_INTERVAL = timedelta(seconds=15)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
@@ -63,6 +70,8 @@ class Volumio(MediaPlayerDevice):
self._state = {}
self.async_update()
self._lastvol = self._state.get('volume', 0)
+ self._playlists = []
+ self._currentplaylist = None
@asyncio.coroutine
def send_volumio_msg(self, method, params=None):
@@ -96,6 +105,7 @@ class Volumio(MediaPlayerDevice):
def async_update(self):
"""Update state."""
resp = yield from self.send_volumio_msg('getState')
+ yield from self._async_update_playlists()
if resp is False:
return
self._state = resp.copy()
@@ -157,7 +167,7 @@ class Volumio(MediaPlayerDevice):
def volume_level(self):
"""Volume level of the media player (0..1)."""
volume = self._state.get('volume', None)
- if volume is not None:
+ if volume is not None and volume != "":
volume = volume / 100
return volume
@@ -171,6 +181,16 @@ class Volumio(MediaPlayerDevice):
"""Return the name of the device."""
return self._name
+ @property
+ def source_list(self):
+ """Return the list of available input sources."""
+ return self._playlists
+
+ @property
+ def source(self):
+ """Name of the current input source."""
+ return self._currentplaylist
+
@property
def supported_features(self):
"""Flag of media commands that are supported."""
@@ -199,14 +219,42 @@ class Volumio(MediaPlayerDevice):
return self.send_volumio_msg(
'commands', params={'cmd': 'volume', 'volume': int(volume * 100)})
+ def async_volume_up(self):
+ """Service to send the Volumio the command for volume up."""
+ return self.send_volumio_msg(
+ 'commands', params={'cmd': 'volume', 'volume': 'plus'})
+
+ def async_volume_down(self):
+ """Service to send the Volumio the command for volume down."""
+ return self.send_volumio_msg(
+ 'commands', params={'cmd': 'volume', 'volume': 'minus'})
+
def async_mute_volume(self, mute):
"""Send mute command to media player."""
mutecmd = 'mute' if mute else 'unmute'
if mute:
- # mute is implemenhted as 0 volume, do save last volume level
+ # mute is implemented as 0 volume, do save last volume level
self._lastvol = self._state['volume']
return self.send_volumio_msg(
'commands', params={'cmd': 'volume', 'volume': mutecmd})
return self.send_volumio_msg(
'commands', params={'cmd': 'volume', 'volume': self._lastvol})
+
+ def async_select_source(self, source):
+ """Choose a different available playlist and play it."""
+ self._currentplaylist = source
+ return self.send_volumio_msg(
+ 'commands', params={'cmd': 'playplaylist', 'name': source})
+
+ def async_clear_playlist(self):
+ """Clear players playlist."""
+ self._currentplaylist = None
+ return self.send_volumio_msg('commands',
+ params={'cmd': 'clearQueue'})
+
+ @asyncio.coroutine
+ @Throttle(PLAYLIST_UPDATE_INTERVAL)
+ def _async_update_playlists(self, **kwargs):
+ """Update available Volumio playlists."""
+ self._playlists = yield from self.send_volumio_msg('listplaylists')
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index 55179ed60a9..3ccd3c7dbe9 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -9,6 +9,9 @@ from datetime import timedelta
import logging
from urllib.parse import urlparse
+# pylint: disable=unused-import
+from typing import Dict # noqa: F401
+
import voluptuous as vol
from homeassistant.components.media_player import (
@@ -23,7 +26,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
import homeassistant.util as util
-REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2', 'wakeonlan==0.2.2']
+REQUIREMENTS = ['pylgtv==0.1.7', 'websockets==3.2']
_CONFIGURING = {} # type: Dict[str, str]
_LOGGER = logging.getLogger(__name__)
@@ -171,6 +174,7 @@ class LgWebOSDevice(MediaPlayerDevice):
self._state = STATE_UNKNOWN
self._source_list = {}
self._app_list = {}
+ self._channel = None
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
@@ -186,10 +190,12 @@ class LgWebOSDevice(MediaPlayerDevice):
self._state = STATE_OFF
self._current_source = None
self._current_source_id = None
+ self._channel = None
if self._state is not STATE_OFF:
self._muted = self._client.get_muted()
self._volume = self._client.get_volume()
+ self._channel = self._client.get_current_channel()
self._source_list = {}
self._app_list = {}
@@ -222,6 +228,7 @@ class LgWebOSDevice(MediaPlayerDevice):
self._state = STATE_OFF
self._current_source = None
self._current_source_id = None
+ self._channel = None
@property
def name(self):
@@ -258,6 +265,14 @@ class LgWebOSDevice(MediaPlayerDevice):
"""Content type of current playing media."""
return MEDIA_TYPE_CHANNEL
+ @property
+ def media_title(self):
+ """Title of current playing media."""
+ if (self._channel is not None) and ('channelName' in self._channel):
+ return self._channel['channelName']
+ else:
+ return None
+
@property
def media_image_url(self):
"""Image url of current playing media."""
diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py
index 577988bc58c..f102d8a490d 100644
--- a/homeassistant/components/media_player/yamaha.py
+++ b/homeassistant/components/media_player/yamaha.py
@@ -149,11 +149,6 @@ class YamahaDevice(MediaPlayerDevice):
self._name = name
self._zone = receiver.zone
- @property
- def unique_id(self):
- """Return an unique ID."""
- return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone)
-
def update(self):
"""Get the latest details from the device."""
self._play_status = self.receiver.play_status()
@@ -310,7 +305,7 @@ class YamahaDevice(MediaPlayerDevice):
NOTE: this might take a while, because the only API interface
for setting the net radio station emulates button pressing and
- navigating through the net radio menu hiearchy. And each sub
+ navigating through the net radio menu hierarchy. And each sub
menu must be fetched by the receiver from the vtuner service.
"""
diff --git a/homeassistant/components/melissa.py b/homeassistant/components/melissa.py
new file mode 100644
index 00000000000..ae82b96222e
--- /dev/null
+++ b/homeassistant/components/melissa.py
@@ -0,0 +1,44 @@
+"""
+Support for Melissa climate.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/melissa/
+"""
+import logging
+
+import voluptuous as vol
+
+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"]
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = "melissa"
+DATA_MELISSA = 'MELISSA'
+
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up the Melissa Climate component."""
+ import melissa
+
+ conf = config[DOMAIN]
+ username = conf.get(CONF_USERNAME)
+ password = conf.get(CONF_PASSWORD)
+
+ api = melissa.Melissa(username=username, password=password)
+ hass.data[DATA_MELISSA] = api
+
+ load_platform(hass, 'sensor', DOMAIN, {})
+ load_platform(hass, 'climate', DOMAIN, {})
+ return True
diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py
new file mode 100644
index 00000000000..a228486e2c8
--- /dev/null
+++ b/homeassistant/components/mercedesme.py
@@ -0,0 +1,154 @@
+"""
+Support for MercedesME System.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/mercedesme/
+"""
+import asyncio
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+import homeassistant.helpers.config_validation as cv
+
+from homeassistant.const import (
+ CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS)
+from homeassistant.helpers import discovery
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect, dispatcher_send)
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.event import track_time_interval
+
+REQUIREMENTS = ['mercedesmejsonpy==0.1.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+BINARY_SENSORS = {
+ 'doorsClosed': ['Doors closed'],
+ 'windowsClosed': ['Windows closed'],
+ 'locked': ['Doors locked'],
+ 'tireWarningLight': ['Tire Warning']
+}
+
+SENSORS = {
+ 'fuelLevelPercent': ['Fuel Level', '%'],
+ 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS],
+ 'latestTrip': ['Latest Trip', None],
+ 'odometerKm': ['Odometer', LENGTH_KILOMETERS],
+ 'serviceIntervalDays': ['Next Service', 'days']
+}
+
+DATA_MME = 'mercedesme'
+DOMAIN = 'mercedesme'
+
+NOTIFICATION_ID = 'mercedesme_integration_notification'
+NOTIFICATION_TITLE = 'Mercedes me integration setup'
+
+SIGNAL_UPDATE_MERCEDESME = "mercedesme_update"
+
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_SCAN_INTERVAL, default=30):
+ vol.All(cv.positive_int, vol.Clamp(min=10))
+ })
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up MercedesMe System."""
+ from mercedesmejsonpy.controller import Controller
+ from mercedesmejsonpy import Exceptions
+
+ conf = config[DOMAIN]
+ username = conf.get(CONF_USERNAME)
+ password = conf.get(CONF_PASSWORD)
+ scan_interval = conf.get(CONF_SCAN_INTERVAL)
+
+ try:
+ mercedesme_api = Controller(username, password, scan_interval)
+ if not mercedesme_api.is_valid_session:
+ raise Exceptions.MercedesMeException(500)
+ hass.data[DATA_MME] = MercedesMeHub(mercedesme_api)
+ except Exceptions.MercedesMeException as ex:
+ if ex.code == 401:
+ hass.components.persistent_notification.create(
+ "Error:
Please check username and password."
+ "You will need to restart Home Assistant after fixing.",
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ else:
+ hass.components.persistent_notification.create(
+ "Error:
Can't communicate with Mercedes me API.
"
+ "Error code: {} Reason: {}"
+ "You will need to restart Home Assistant after fixing."
+ "".format(ex.code, ex.message),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+
+ _LOGGER.error("Unable to communicate with Mercedes me API: %s",
+ ex.message)
+ return False
+
+ discovery.load_platform(hass, 'sensor', DOMAIN, {}, config)
+ discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config)
+ discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config)
+
+ def hub_refresh(event_time):
+ """Call Mercedes me API to refresh information."""
+ _LOGGER.info("Updating Mercedes me component.")
+ hass.data[DATA_MME].data.update()
+ dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME)
+
+ track_time_interval(
+ hass,
+ hub_refresh,
+ timedelta(seconds=scan_interval))
+
+ return True
+
+
+class MercedesMeHub(object):
+ """Representation of a base MercedesMe device."""
+
+ def __init__(self, data):
+ """Initialize the entity."""
+ self.data = data
+
+
+class MercedesMeEntity(Entity):
+ """Entity class for MercedesMe devices."""
+
+ def __init__(self, data, internal_name, sensor_name, vin, unit):
+ """Initialize the MercedesMe entity."""
+ self._car = None
+ self._data = data
+ self._state = False
+ self._name = sensor_name
+ self._internal_name = internal_name
+ self._unit = unit
+ self._vin = vin
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @asyncio.coroutine
+ def async_added_to_hass(self):
+ """Register callbacks."""
+ async_dispatcher_connect(
+ self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback)
+
+ def _update_callback(self):
+ """Callback update method."""
+ # If the method is made a callback this should be changed
+ # to the async version. Check core.callback
+ self.schedule_update_ha_state(True)
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit
diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py
index e61ed05ce10..5a0bf2af1c4 100644
--- a/homeassistant/components/microsoft_face.py
+++ b/homeassistant/components/microsoft_face.py
@@ -337,7 +337,7 @@ class MicrosoftFace(object):
@asyncio.coroutine
def call_api(self, method, function, data=None, binary=False,
params=None):
- """Make a api call."""
+ """Make an api call."""
headers = {"Ocp-Apim-Subscription-Key": self._api_key}
url = self._server_url.format(function)
diff --git a/homeassistant/components/mochad.py b/homeassistant/components/mochad.py
index 3cc4eda7675..9f53f84e020 100644
--- a/homeassistant/components/mochad.py
+++ b/homeassistant/components/mochad.py
@@ -14,7 +14,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.const import (CONF_HOST, CONF_PORT)
-REQUIREMENTS = ['pymochad==0.1.1']
+REQUIREMENTS = ['pymochad==0.2.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py
index 91053b41bf6..390da7ed0e0 100644
--- a/homeassistant/components/mysensors.py
+++ b/homeassistant/components/mysensors.py
@@ -76,12 +76,12 @@ def is_socket_address(value):
def has_parent_dir(value):
- """Validate that value is in an existing directory which is writetable."""
+ """Validate that value is in an existing directory which is writeable."""
parent = os.path.dirname(os.path.realpath(value))
is_dir_writable = os.path.isdir(parent) and os.access(parent, os.W_OK)
if not is_dir_writable:
raise vol.Invalid(
- '{} directory does not exist or is not writetable'.format(parent))
+ '{} directory does not exist or is not writeable'.format(parent))
return value
diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py
index 512819b7e74..37028decf71 100644
--- a/homeassistant/components/nest.py
+++ b/homeassistant/components/nest.py
@@ -183,7 +183,7 @@ class NestDevice(object):
"Connection error logging into the nest web service.")
def smoke_co_alarms(self):
- """Generate a list of smoke co alarams."""
+ """Generate a list of smoke co alarms."""
try:
for structure in self.nest.structures:
if structure.name in self.local_structure:
diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py
index 6ef758b7bb5..e7a727bc5e2 100644
--- a/homeassistant/components/notify/apns.py
+++ b/homeassistant/components/notify/apns.py
@@ -112,13 +112,13 @@ class ApnsDevice(object):
self.device_disabled = True
def __eq__(self, other):
- """Return the comparision."""
+ """Return the comparison."""
if isinstance(other, self.__class__):
return self.push_id == other.push_id and self.name == other.name
return NotImplemented
def __ne__(self, other):
- """Return the comparision."""
+ """Return the comparison."""
return not self.__eq__(other)
diff --git a/homeassistant/components/notify/gntp.py b/homeassistant/components/notify/gntp.py
index b7e5b1b813a..1a2b65f958f 100644
--- a/homeassistant/components/notify/gntp.py
+++ b/homeassistant/components/notify/gntp.py
@@ -44,7 +44,8 @@ def get_service(hass, config, discovery_info=None):
if config.get(CONF_APP_ICON) is None:
icon_file = os.path.join(os.path.dirname(__file__), "..", "frontend",
"www_static", "icons", "favicon-192x192.png")
- app_icon = open(icon_file, 'rb').read()
+ with open(icon_file, 'rb') as file:
+ app_icon = file.read()
else:
app_icon = config.get(CONF_APP_ICON)
diff --git a/homeassistant/components/notify/knx.py b/homeassistant/components/notify/knx.py
index c5dbcb0d4ad..d14d8dcf8ad 100644
--- a/homeassistant/components/notify/knx.py
+++ b/homeassistant/components/notify/knx.py
@@ -51,7 +51,7 @@ def async_get_service_discovery(hass, discovery_info):
@callback
def async_get_service_config(hass, config):
- """Set up notification for KNX platform configured within plattform."""
+ """Set up notification for KNX platform configured within platform."""
import xknx
notification = xknx.devices.Notification(
hass.data[DATA_KNX].xknx,
diff --git a/homeassistant/components/notify/kodi.py b/homeassistant/components/notify/kodi.py
index 05f4c5d17f3..3eb492f7fa6 100644
--- a/homeassistant/components/notify/kodi.py
+++ b/homeassistant/components/notify/kodi.py
@@ -51,7 +51,7 @@ def async_get_service(hass, config, discovery_info=None):
encryption = config.get(CONF_PROXY_SSL)
if host.startswith('http://') or host.startswith('https://'):
- host = host.lstrip('http://').lstrip('https://')
+ host = host[host.index('://') + 3:]
_LOGGER.warning(
"Kodi host name should no longer contain http:// See updated "
"definitions here: "
diff --git a/homeassistant/components/notify/pushsafer.py b/homeassistant/components/notify/pushsafer.py
index 78a600ab8d6..30068854f2e 100644
--- a/homeassistant/components/notify/pushsafer.py
+++ b/homeassistant/components/notify/pushsafer.py
@@ -5,20 +5,41 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.pushsafer/
"""
import logging
+import base64
+import mimetypes
import requests
+from requests.auth import HTTPBasicAuth
import voluptuous as vol
from homeassistant.components.notify import (
- ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService)
+ ATTR_TITLE, ATTR_TITLE_DEFAULT, ATTR_TARGET, ATTR_DATA,
+ PLATFORM_SCHEMA, BaseNotificationService)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
_RESOURCE = 'https://www.pushsafer.com/api'
+_ALLOWED_IMAGES = ['image/gif', 'image/jpeg', 'image/png']
CONF_DEVICE_KEY = 'private_key'
+CONF_TIMEOUT = 15
-DEFAULT_TIMEOUT = 10
+# Top level attributes in 'data'
+ATTR_SOUND = 'sound'
+ATTR_VIBRATION = 'vibration'
+ATTR_ICON = 'icon'
+ATTR_ICONCOLOR = 'iconcolor'
+ATTR_URL = 'url'
+ATTR_URLTITLE = 'urltitle'
+ATTR_TIME2LIVE = 'time2live'
+ATTR_PICTURE1 = 'picture1'
+
+# Attributes contained in picture1
+ATTR_PICTURE1_URL = 'url'
+ATTR_PICTURE1_PATH = 'path'
+ATTR_PICTURE1_USERNAME = 'username'
+ATTR_PICTURE1_PASSWORD = 'password'
+ATTR_PICTURE1_AUTH = 'auth'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE_KEY): cv.string,
@@ -27,21 +48,118 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_service(hass, config, discovery_info=None):
"""Get the Pushsafer.com notification service."""
- return PushsaferNotificationService(config.get(CONF_DEVICE_KEY))
+ return PushsaferNotificationService(config.get(CONF_DEVICE_KEY),
+ hass.config.is_allowed_path)
class PushsaferNotificationService(BaseNotificationService):
"""Implementation of the notification service for Pushsafer.com."""
- def __init__(self, private_key):
+ def __init__(self, private_key, is_allowed_path):
"""Initialize the service."""
self._private_key = private_key
+ self.is_allowed_path = is_allowed_path
def send_message(self, message='', **kwargs):
- """Send a message to a user."""
+ """Send a message to specified target."""
+ if kwargs.get(ATTR_TARGET) is None:
+ targets = ["a"]
+ _LOGGER.debug("No target specified. Sending push to all")
+ else:
+ targets = kwargs.get(ATTR_TARGET)
+ _LOGGER.debug("%s target(s) specified", len(targets))
+
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
- payload = {'k': self._private_key, 't': title, 'm': message}
- response = requests.get(_RESOURCE, params=payload,
- timeout=DEFAULT_TIMEOUT)
- if response.status_code != 200:
- _LOGGER.error("Not possible to send notification")
+ data = kwargs.get(ATTR_DATA, {})
+
+ # Converting the specified image to base64
+ picture1 = data.get(ATTR_PICTURE1)
+ picture1_encoded = ""
+ if picture1 is not None:
+ _LOGGER.debug("picture1 is available")
+ url = picture1.get(ATTR_PICTURE1_URL, None)
+ local_path = picture1.get(ATTR_PICTURE1_PATH, None)
+ username = picture1.get(ATTR_PICTURE1_USERNAME)
+ password = picture1.get(ATTR_PICTURE1_PASSWORD)
+ auth = picture1.get(ATTR_PICTURE1_AUTH)
+
+ if url is not None:
+ _LOGGER.debug("Loading image from url %s", url)
+ picture1_encoded = self.load_from_url(url, username,
+ password, auth)
+ elif local_path is not None:
+ _LOGGER.debug("Loading image from file %s", local_path)
+ picture1_encoded = self.load_from_file(local_path)
+ else:
+ _LOGGER.warning("missing url or local_path for picture1")
+ else:
+ _LOGGER.debug("picture1 is not specified")
+
+ payload = {
+ 'k': self._private_key,
+ 't': title,
+ 'm': message,
+ 's': data.get(ATTR_SOUND, ""),
+ 'v': data.get(ATTR_VIBRATION, ""),
+ 'i': data.get(ATTR_ICON, ""),
+ 'c': data.get(ATTR_ICONCOLOR, ""),
+ 'u': data.get(ATTR_URL, ""),
+ 'ut': data.get(ATTR_URLTITLE, ""),
+ 'l': data.get(ATTR_TIME2LIVE, ""),
+ 'p': picture1_encoded
+ }
+
+ for target in targets:
+ payload['d'] = target
+ response = requests.post(_RESOURCE, data=payload,
+ timeout=CONF_TIMEOUT)
+ if response.status_code != 200:
+ _LOGGER.error("Pushsafer failed with: %s", response.text)
+ else:
+ _LOGGER.debug("Push send: %s", response.json())
+
+ @classmethod
+ def get_base64(cls, filebyte, mimetype):
+ """Convert the image to the expected base64 string of pushsafer."""
+ if mimetype not in _ALLOWED_IMAGES:
+ _LOGGER.warning("%s is a not supported mimetype for images",
+ mimetype)
+ return None
+
+ base64_image = base64.b64encode(filebyte).decode('utf8')
+ return "data:{};base64,{}".format(mimetype, base64_image)
+
+ def load_from_url(self, url=None, username=None, password=None, auth=None):
+ """Load image/document/etc from URL."""
+ if url is not None:
+ _LOGGER.debug("Downloading image from %s", url)
+ if username is not None and password is not None:
+ auth_ = HTTPBasicAuth(username, password)
+ response = requests.get(url, auth=auth_,
+ timeout=CONF_TIMEOUT)
+ else:
+ response = requests.get(url, timeout=CONF_TIMEOUT)
+ return self.get_base64(response.content,
+ response.headers['content-type'])
+ else:
+ _LOGGER.warning("url not found in param")
+
+ return None
+
+ def load_from_file(self, local_path=None):
+ """Load image/document/etc from a local path."""
+ try:
+ if local_path is not None:
+ _LOGGER.debug("Loading image from local path")
+ if self.is_allowed_path(local_path):
+ file_mimetype = mimetypes.guess_type(local_path)
+ _LOGGER.debug("Detected mimetype %s", file_mimetype)
+ with open(local_path, "rb") as binary_file:
+ data = binary_file.read()
+ return self.get_base64(data, file_mimetype[0])
+ else:
+ _LOGGER.warning("Local path not found in params!")
+ except OSError as error:
+ _LOGGER.error("Can't load from local path: %s", error)
+
+ return None
diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py
index 6cb98e45274..c6f4fa0dd5f 100644
--- a/homeassistant/components/notify/twitter.py
+++ b/homeassistant/components/notify/twitter.py
@@ -19,7 +19,7 @@ from homeassistant.components.notify import (
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
from homeassistant.helpers.event import async_track_point_in_time
-REQUIREMENTS = ['TwitterAPI==2.4.6']
+REQUIREMENTS = ['TwitterAPI==2.4.8']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/panel_iframe.py b/homeassistant/components/panel_iframe.py
index e4be19c53ed..6ddf00cf7d4 100644
--- a/homeassistant/components/panel_iframe.py
+++ b/homeassistant/components/panel_iframe.py
@@ -8,22 +8,28 @@ import asyncio
import voluptuous as vol
+from homeassistant.const import (CONF_ICON, CONF_URL)
import homeassistant.helpers.config_validation as cv
-DOMAIN = 'panel_iframe'
DEPENDENCIES = ['frontend']
+DOMAIN = 'panel_iframe'
+
CONF_TITLE = 'title'
-CONF_ICON = 'icon'
-CONF_URL = 'url'
+
+CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required."
+CONF_RELATIVE_URL_REGEX = r'\A/'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
cv.slug: {
vol.Optional(CONF_TITLE): cv.string,
vol.Optional(CONF_ICON): cv.icon,
- # pylint: disable=no-value-for-parameter
- vol.Required(CONF_URL): vol.Url(),
+ vol.Required(CONF_URL): vol.Any(
+ vol.Match(
+ CONF_RELATIVE_URL_REGEX,
+ msg=CONF_RELATIVE_URL_ERROR_MSG),
+ cv.url),
}})}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py
index 3000820d28c..71e8232e8c2 100644
--- a/homeassistant/components/pilight.py
+++ b/homeassistant/components/pilight.py
@@ -130,7 +130,7 @@ class CallRateDelayThrottle(object):
it should not block the mainloop.
"""
- def __init__(self, hass, delay_seconds: float):
+ def __init__(self, hass, delay_seconds: float) -> None:
"""Initialize the delay handler."""
self._delay = timedelta(seconds=max(0.0, delay_seconds))
self._queue = []
diff --git a/homeassistant/components/plant.py b/homeassistant/components/plant.py
index 7df990fa0e5..24b8c682d02 100644
--- a/homeassistant/components/plant.py
+++ b/homeassistant/components/plant.py
@@ -92,7 +92,7 @@ CONFIG_SCHEMA = vol.Schema({
# Flag for enabling/disabling the loading of the history from the database.
-# This feature is turned off right now as it's tests are not 100% stable.
+# This feature is turned off right now as its tests are not 100% stable.
ENABLE_LOAD_HISTORY = False
@@ -336,9 +336,9 @@ class DailyHistory(object):
self._max_dict = dict()
self.max = None
- def add_measurement(self, value, timestamp=datetime.now()):
+ def add_measurement(self, value, timestamp=None):
"""Add a new measurement for a certain day."""
- day = timestamp.date()
+ day = (timestamp or datetime.now()).date()
if value is None:
return
if self._days is None:
diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py
index d5d6f657bc6..4d5f27082de 100644
--- a/homeassistant/components/qwikswitch.py
+++ b/homeassistant/components/qwikswitch.py
@@ -171,7 +171,7 @@ def setup(hass, config):
def qs_callback(item):
"""Typically a button press or update signal."""
if qsusb is None: # Shutting down
- _LOGGER.info("Botton press or updating signal done")
+ _LOGGER.info("Button press or updating signal done")
return
# If button pressed, fire a hass event
diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird.py
index 882731d4f2c..76dda6fd366 100644
--- a/homeassistant/components/rainbird.py
+++ b/homeassistant/components/rainbird.py
@@ -27,7 +27,7 @@ CONFIG_SCHEMA = vol.Schema({
def setup(hass, config):
- """Set up the Rain Bird componenent."""
+ """Set up the Rain Bird component."""
conf = config[DOMAIN]
server = conf.get(CONF_HOST)
password = conf.get(CONF_PASSWORD)
@@ -38,8 +38,8 @@ def setup(hass, config):
_LOGGER.debug("Rain Bird Controller set to: %s", server)
- initialstatus = controller.currentIrrigation()
- if initialstatus == -1:
+ initial_status = controller.currentIrrigation()
+ if initial_status == -1:
_LOGGER.error("Error getting state. Possible configuration issues")
return False
diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py
index 1c9524223e5..b2628f954fc 100644
--- a/homeassistant/components/recorder/__init__.py
+++ b/homeassistant/components/recorder/__init__.py
@@ -16,7 +16,7 @@ import queue
import threading
import time
-from typing import Dict, Optional
+from typing import Any, Dict, Optional # noqa: F401
import voluptuous as vol
@@ -34,7 +34,7 @@ from . import migration, purge
from .const import DATA_INSTANCE
from .util import session_scope
-REQUIREMENTS = ['sqlalchemy==1.2.1']
+REQUIREMENTS = ['sqlalchemy==1.2.2']
_LOGGER = logging.getLogger(__name__)
@@ -76,10 +76,10 @@ FILTER_SCHEMA = vol.Schema({
CONFIG_SCHEMA = vol.Schema({
DOMAIN: FILTER_SCHEMA.extend({
- vol.Inclusive(CONF_PURGE_KEEP_DAYS, 'purge'):
- vol.All(vol.Coerce(int), vol.Range(min=1)),
- vol.Inclusive(CONF_PURGE_INTERVAL, 'purge'):
+ vol.Optional(CONF_PURGE_KEEP_DAYS):
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)),
vol.Optional(CONF_DB_URL): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
@@ -122,6 +122,12 @@ 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(
@@ -162,6 +168,7 @@ class Recorder(threading.Thread):
self.hass = hass
self.keep_days = keep_days
self.purge_interval = purge_interval
+ self.did_vacuum = False
self.queue = queue.Queue() # type: Any
self.recording_start = dt_util.utcnow()
self.db_url = uri
diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py
index fad6a7de70d..06bd81c2309 100644
--- a/homeassistant/components/recorder/purge.py
+++ b/homeassistant/components/recorder/purge.py
@@ -55,11 +55,12 @@ def purge_old_data(instance, purge_days):
# Execute sqlite vacuum command to free up space on disk
_LOGGER.debug("DB engine driver: %s", instance.engine.driver)
- if instance.engine.driver == 'pysqlite':
+ if instance.engine.driver == 'pysqlite' and not instance.did_vacuum:
from sqlalchemy import exc
_LOGGER.info("Vacuuming SQLite to free space")
try:
instance.engine.execute("VACUUM")
+ instance.did_vacuum = True
except exc.OperationalError as err:
_LOGGER.error("Error vacuuming SQLite: %s.", err)
diff --git a/homeassistant/components/remote/kira.py b/homeassistant/components/remote/kira.py
index 7a04949dbeb..42d4ce77054 100644
--- a/homeassistant/components/remote/kira.py
+++ b/homeassistant/components/remote/kira.py
@@ -48,8 +48,8 @@ class KiraRemote(Entity):
def send_command(self, command, **kwargs):
"""Send a command to one device."""
- for singel_command in command:
- code_tuple = (singel_command,
+ for single_command in command:
+ code_tuple = (single_command,
kwargs.get(remote.ATTR_DEVICE))
_LOGGER.info("Sending Command: %s to %s", *code_tuple)
self._kira.sendCode(code_tuple)
diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml
index 2a1deebdc7b..25ad626f96d 100644
--- a/homeassistant/components/remote/services.yaml
+++ b/homeassistant/components/remote/services.yaml
@@ -49,3 +49,16 @@ harmony_sync:
entity_id:
description: Name(s) of entities to sync.
example: 'remote.family_room'
+
+xiaomi_miio_learn_command:
+ description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.'
+ fields:
+ entity_id:
+ description: 'Name of the entity to learn command from.'
+ example: 'remote.xiaomi_miio'
+ slot:
+ description: 'Define the slot used to save the IR command (Value from 1 to 1000000)'
+ example: '1'
+ timeout:
+ description: 'Define the timeout in seconds, before which the command must be learned.'
+ example: '30'
diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py
new file mode 100644
index 00000000000..aa05246c9cd
--- /dev/null
+++ b/homeassistant/components/remote/xiaomi_miio.py
@@ -0,0 +1,255 @@
+"""
+Support for the Xiaomi IR Remote (Chuangmi IR).
+
+For more details about this platform, please refer to the documentation
+https://home-assistant.io/components/remote.xiaomi_miio/
+"""
+import asyncio
+import logging
+import time
+
+from datetime import timedelta
+
+import voluptuous as vol
+
+from homeassistant.components.remote import (
+ PLATFORM_SCHEMA, DOMAIN, ATTR_NUM_REPEATS, ATTR_DELAY_SECS,
+ DEFAULT_DELAY_SECS, RemoteDevice)
+from homeassistant.const import (
+ CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT,
+ ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.util.dt import utcnow
+
+REQUIREMENTS = ['python-miio==0.3.5']
+
+_LOGGER = logging.getLogger(__name__)
+
+SERVICE_LEARN = 'xiaomi_miio_learn_command'
+PLATFORM = 'xiaomi_miio'
+
+CONF_SLOT = 'slot'
+CONF_COMMANDS = 'commands'
+
+DEFAULT_TIMEOUT = 10
+DEFAULT_SLOT = 1
+
+LEARN_COMMAND_SCHEMA = vol.Schema({
+ vol.Required(ATTR_ENTITY_ID): vol.All(str),
+ vol.Optional(CONF_TIMEOUT, default=10):
+ vol.All(int, vol.Range(min=0)),
+ vol.Optional(CONF_SLOT, default=1):
+ vol.All(int, vol.Range(min=1, max=1000000)),
+})
+
+COMMAND_SCHEMA = vol.Schema({
+ vol.Required(CONF_COMMAND): vol.All(cv.ensure_list, [cv.string])
+ })
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_NAME): cv.string,
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT):
+ vol.All(int, vol.Range(min=0)),
+ vol.Optional(CONF_SLOT, default=DEFAULT_SLOT):
+ vol.All(int, vol.Range(min=1, max=1000000)),
+ vol.Optional(ATTR_HIDDEN, default=True): cv.boolean,
+ vol.Required(CONF_TOKEN): vol.All(str, vol.Length(min=32, max=32)),
+ vol.Optional(CONF_COMMANDS, default={}):
+ vol.Schema({cv.slug: COMMAND_SCHEMA}),
+}, extra=vol.ALLOW_EXTRA)
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+ """Set up the Xiaomi IR Remote (Chuangmi IR) platform."""
+ from miio import ChuangmiIr, DeviceException
+
+ host = config.get(CONF_HOST)
+ token = config.get(CONF_TOKEN)
+
+ # Create handler
+ _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
+ device = ChuangmiIr(host, token)
+
+ # Check that we can communicate with device.
+ try:
+ device.info()
+ except DeviceException as ex:
+ _LOGGER.error("Token not accepted by device : %s", ex)
+ return
+
+ if PLATFORM not in hass.data:
+ hass.data[PLATFORM] = {}
+
+ friendly_name = config.get(CONF_NAME, "xiaomi_miio_" +
+ host.replace('.', '_'))
+ slot = config.get(CONF_SLOT)
+ timeout = config.get(CONF_TIMEOUT)
+
+ hidden = config.get(ATTR_HIDDEN)
+
+ xiaomi_miio_remote = XiaomiMiioRemote(
+ friendly_name, device, slot, timeout,
+ hidden, config.get(CONF_COMMANDS))
+
+ hass.data[PLATFORM][host] = xiaomi_miio_remote
+
+ async_add_devices([xiaomi_miio_remote])
+
+ @asyncio.coroutine
+ def async_service_handler(service):
+ """Handle a learn command."""
+ if service.service != SERVICE_LEARN:
+ _LOGGER.error("We should not handle service: %s", service.service)
+ return
+
+ entity_id = service.data.get(ATTR_ENTITY_ID)
+ entity = None
+ for remote in hass.data[PLATFORM].values():
+ if remote.entity_id == entity_id:
+ entity = remote
+
+ if not entity:
+ _LOGGER.error("entity_id: '%s' not found", entity_id)
+ return
+
+ device = entity.device
+
+ slot = service.data.get(CONF_SLOT, entity.slot)
+
+ yield from hass.async_add_job(device.learn, slot)
+
+ timeout = service.data.get(CONF_TIMEOUT, entity.timeout)
+
+ _LOGGER.info("Press the key you want Home Assistant to learn")
+ start_time = utcnow()
+ while (utcnow() - start_time) < timedelta(seconds=timeout):
+ message = yield from hass.async_add_job(
+ device.read, slot)
+ _LOGGER.debug("Message recieved from device: '%s'", message)
+
+ if 'code' in message and message['code']:
+ log_msg = "Received command is: {}".format(message['code'])
+ _LOGGER.info(log_msg)
+ hass.components.persistent_notification.async_create(
+ log_msg, title='Xiaomi Miio Remote')
+ return
+
+ if ('error' in message and
+ message['error']['message'] == "learn timeout"):
+ yield from hass.async_add_job(device.learn, slot)
+
+ yield from asyncio.sleep(1, loop=hass.loop)
+
+ _LOGGER.error("Timeout. No infrared command captured")
+ hass.components.persistent_notification.async_create(
+ "Timeout. No infrared command captured",
+ title='Xiaomi Miio Remote')
+
+ hass.services.async_register(DOMAIN, SERVICE_LEARN, async_service_handler,
+ schema=LEARN_COMMAND_SCHEMA)
+
+
+class XiaomiMiioRemote(RemoteDevice):
+ """Representation of a Xiaomi Miio Remote device."""
+
+ def __init__(self, friendly_name, device,
+ slot, timeout, hidden, commands):
+ """Initialize the remote."""
+ self._name = friendly_name
+ self._device = device
+ self._is_hidden = hidden
+ self._slot = slot
+ self._timeout = timeout
+ self._state = False
+ self._commands = commands
+
+ @property
+ def name(self):
+ """Return the name of the remote."""
+ return self._name
+
+ @property
+ def device(self):
+ """Return the remote object."""
+ return self._device
+
+ @property
+ def hidden(self):
+ """Return if we should hide entity."""
+ return self._is_hidden
+
+ @property
+ def slot(self):
+ """Return the slot to save learned command."""
+ return self._slot
+
+ @property
+ def timeout(self):
+ """Return the timeout for learning command."""
+ return self._timeout
+
+ @property
+ def is_on(self):
+ """Return False if device is unreachable, else True."""
+ from miio import DeviceException
+ try:
+ self.device.info()
+ return True
+ except DeviceException:
+ return False
+
+ @property
+ def should_poll(self):
+ """We should not be polled for device up state."""
+ return False
+
+ @property
+ def device_state_attributes(self):
+ """Hide remote by default."""
+ if self._is_hidden:
+ return {'hidden': 'true'}
+ else:
+ return
+
+ # pylint: disable=R0201
+ @asyncio.coroutine
+ def async_turn_on(self, **kwargs):
+ """Turn the device on."""
+ _LOGGER.error("Device does not support turn_on, " +
+ "please use 'remote.send_command' to send commands.")
+
+ @asyncio.coroutine
+ def async_turn_off(self, **kwargs):
+ """Turn the device off."""
+ _LOGGER.error("Device does not support turn_off, " +
+ "please use 'remote.send_command' to send commands.")
+
+ # pylint: enable=R0201
+ def _send_command(self, payload):
+ """Send a command."""
+ from miio import DeviceException
+
+ _LOGGER.debug("Sending payload: '%s'", payload)
+ try:
+ self.device.play(payload)
+ except DeviceException as ex:
+ _LOGGER.error(
+ "Transmit of IR command failed, %s, exception: %s",
+ payload, ex)
+
+ def send_command(self, command, **kwargs):
+ """Wrapper for _send_command."""
+ num_repeats = kwargs.get(ATTR_NUM_REPEATS)
+
+ delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
+
+ for _ in range(num_repeats):
+ for payload in command:
+ if payload in self._commands:
+ for local_payload in self._commands[payload][CONF_COMMAND]:
+ self._send_command(local_payload)
+ else:
+ self._send_command(payload)
+ time.sleep(delay)
diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py
index 73922d56040..d97d4f38f02 100644
--- a/homeassistant/components/rflink.py
+++ b/homeassistant/components/rflink.py
@@ -390,7 +390,7 @@ class RflinkCommand(RflinkDevice):
"""Cancel queued signal repetition commands.
For example when user changed state while repetitions are still
- queued for broadcast. Or when a incoming Rflink command (remote
+ queued for broadcast. Or when an incoming Rflink command (remote
switch) changes the state.
"""
# cancel any outstanding tasks from the previous state change
diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py
index 7d2e428c56b..de8a0c00d80 100644
--- a/homeassistant/components/rfxtrx.py
+++ b/homeassistant/components/rfxtrx.py
@@ -77,7 +77,7 @@ def setup(hass, config):
"""Set up the RFXtrx component."""
# Declare the Handle event
def handle_receive(event):
- """Handle revieved messages from RFXtrx gateway."""
+ """Handle received messages from RFXtrx gateway."""
# Log RFXCOM event
if not event.device.id_string:
return
@@ -171,7 +171,7 @@ def get_pt2262_cmd(device_id, data_bits):
# pylint: disable=unused-variable
def get_pt2262_device(device_id):
"""Look for the device which id matches the given device_id parameter."""
- for dev_id, device in RFX_DEVICES.items():
+ for device in RFX_DEVICES.values():
if (hasattr(device, 'is_lighting4') and
device.masked_id == get_pt2262_deviceid(device_id,
device.data_bits)):
diff --git a/homeassistant/components/scsgate.py b/homeassistant/components/scsgate.py
index 8c5c6570515..a7193b40949 100644
--- a/homeassistant/components/scsgate.py
+++ b/homeassistant/components/scsgate.py
@@ -87,7 +87,7 @@ class SCSGate(object):
self._logger.debug("Received message {}".format(message))
if not isinstance(message, StateMessage) and \
not isinstance(message, ScenarioTriggeredMessage):
- msg = "Ignored message {} - not releavant type".format(
+ msg = "Ignored message {} - not relevant type".format(
message)
self._logger.debug(msg)
return
@@ -109,7 +109,7 @@ class SCSGate(object):
self._logger.error(msg)
else:
self._logger.info(
- "Ignoring state message for device {} because unknonw".format(
+ "Ignoring state message for device {} because unknown".format(
message.entity))
@property
diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py
index 20899396052..6b224492ffb 100644
--- a/homeassistant/components/sensor/alpha_vantage.py
+++ b/homeassistant/components/sensor/alpha_vantage.py
@@ -89,6 +89,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev = []
for symbol in symbols:
try:
+ _LOGGER.debug("Configuring timeseries for symbols: %s",
+ symbol[CONF_SYMBOL])
timeseries.get_intraday(symbol[CONF_SYMBOL])
except ValueError:
_LOGGER.error(
@@ -100,6 +102,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
from_cur = conversion.get(CONF_FROM)
to_cur = conversion.get(CONF_TO)
try:
+ _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:
@@ -110,6 +114,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
dev.append(AlphaVantageForeignExchange(forex, conversion))
add_devices(dev, True)
+ _LOGGER.debug("Setup completed")
class AlphaVantageSensor(Entity):
@@ -158,8 +163,10 @@ class AlphaVantageSensor(Entity):
def update(self):
"""Get the latest data and updates the states."""
+ _LOGGER.debug("Requesting new data for symbol %s", self._symbol)
all_values, _ = self._timeseries.get_intraday(self._symbol)
self.values = next(iter(all_values.values()))
+ _LOGGER.debug("Received new values for symbol %s", self._symbol)
class AlphaVantageForeignExchange(Entity):
@@ -210,5 +217,11 @@ 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.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)
diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py
index 44557978117..db7ab7c2e9e 100644
--- a/homeassistant/components/sensor/blink.py
+++ b/homeassistant/components/sensor/blink.py
@@ -61,11 +61,6 @@ class BlinkSensor(Entity):
"""Return the camera's current state."""
return self._state
- @property
- def unique_id(self):
- """Return the unique camera sensor identifier."""
- return "sensor_{}_{}".format(self._name, self.index)
-
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py
index 660cb5ede6e..ce44abdb087 100644
--- a/homeassistant/components/sensor/bloomsky.py
+++ b/homeassistant/components/sensor/bloomsky.py
@@ -64,7 +64,6 @@ class BloomSkySensor(Entity):
self._device_id = device['DeviceID']
self._sensor_name = sensor_name
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
- self._unique_id = 'bloomsky_sensor {}'.format(self._name)
self._state = None
@property
@@ -72,11 +71,6 @@ class BloomSkySensor(Entity):
"""Return the name of the BloomSky device and this sensor."""
return self._name
- @property
- def unique_id(self):
- """Return the unique ID for this sensor."""
- return self._unique_id
-
@property
def state(self):
"""Return the current state, eg. value, of this sensor."""
diff --git a/homeassistant/components/sensor/bme680.py b/homeassistant/components/sensor/bme680.py
index 09d8ec4659c..470d7749ea2 100644
--- a/homeassistant/components/sensor/bme680.py
+++ b/homeassistant/components/sensor/bme680.py
@@ -1,7 +1,7 @@
"""
Support for BME680 Sensor over SMBus.
-Temperature, humidity, pressure and volitile gas support.
+Temperature, humidity, pressure and volatile gas support.
Air Quality calculation based on humidity and volatile gas.
For more details about this platform, please refer to the documentation at
@@ -238,7 +238,7 @@ class BME680Handler:
self._gas_sensor_running = True
- # Pause to allow inital data read for device validation.
+ # Pause to allow initial data read for device validation.
sleep(1)
start_time = time()
diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py
index d23236c2df8..1440e2496fe 100644
--- a/homeassistant/components/sensor/broadlink.py
+++ b/homeassistant/components/sensor/broadlink.py
@@ -129,7 +129,7 @@ class BroadlinkData(object):
if retry < 1:
_LOGGER.error(error)
return
- except vol.Invalid:
+ except (vol.Invalid, vol.MultipleInvalid):
pass # Continue quietly if device returned malformed data
if retry > 0 and self._auth():
self._update(retry-1)
diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py
index b0d2c27ae5d..ded8f36203e 100644
--- a/homeassistant/components/sensor/canary.py
+++ b/homeassistant/components/sensor/canary.py
@@ -4,13 +4,27 @@ Support for Canary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.canary/
"""
+
from homeassistant.components.canary import DATA_CANARY
-from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS
+from homeassistant.const import TEMP_CELSIUS
from homeassistant.helpers.entity import Entity
DEPENDENCIES = ['canary']
-SENSOR_VALUE_PRECISION = 1
+SENSOR_VALUE_PRECISION = 2
+ATTR_AIR_QUALITY = "air_quality"
+
+# Sensor types are defined like so:
+# sensor type name, unit_of_measurement, icon
+SENSOR_TYPES = [
+ ["temperature", TEMP_CELSIUS, "mdi:thermometer"],
+ ["humidity", "%", "mdi:water-percent"],
+ ["air_quality", None, "mdi:weather-windy"],
+]
+
+STATE_AIR_QUALITY_NORMAL = "normal"
+STATE_AIR_QUALITY_ABNORMAL = "abnormal"
+STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal"
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -18,11 +32,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
data = hass.data[DATA_CANARY]
devices = []
- from canary.api import SensorType
for location in data.locations:
for device in location.devices:
if device.is_online:
- for sensor_type in SensorType:
+ for sensor_type in SENSOR_TYPES:
devices.append(CanarySensor(data, sensor_type, location,
device))
@@ -37,10 +50,9 @@ class CanarySensor(Entity):
self._data = data
self._sensor_type = sensor_type
self._device_id = device.device_id
- self._is_celsius = location.is_celsius
self._sensor_value = None
- sensor_type_name = sensor_type.value.replace("_", " ").title()
+ sensor_type_name = sensor_type[0].replace("_", " ").title()
self._name = '{} {} {}'.format(location.name,
device.name,
sensor_type_name)
@@ -58,28 +70,51 @@ class CanarySensor(Entity):
@property
def unique_id(self):
"""Return the unique ID of this sensor."""
- return "sensor_canary_{}_{}".format(self._device_id,
- self._sensor_type.value)
+ return "{}_{}".format(self._device_id, self._sensor_type[0])
@property
def unit_of_measurement(self):
- """Return the unit of measurement this sensor expresses itself in."""
- from canary.api import SensorType
- if self._sensor_type == SensorType.TEMPERATURE:
- return TEMP_CELSIUS if self._is_celsius else TEMP_FAHRENHEIT
- elif self._sensor_type == SensorType.HUMIDITY:
- return "%"
- elif self._sensor_type == SensorType.AIR_QUALITY:
- return ""
+ """Return the unit of measurement."""
+ return self._sensor_type[1]
+
+ @property
+ def icon(self):
+ """Icon for the sensor."""
+ return self._sensor_type[2]
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._sensor_type[0] == "air_quality" \
+ and self._sensor_value is not None:
+ air_quality = None
+ if self._sensor_value <= .4:
+ air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL
+ elif self._sensor_value <= .59:
+ air_quality = STATE_AIR_QUALITY_ABNORMAL
+ elif self._sensor_value <= 1.0:
+ air_quality = STATE_AIR_QUALITY_NORMAL
+
+ return {
+ ATTR_AIR_QUALITY: air_quality
+ }
+
return None
def update(self):
"""Get the latest state of the sensor."""
self._data.update()
- readings = self._data.get_readings(self._device_id)
- value = next((
- reading.value for reading in readings
- if reading.sensor_type == self._sensor_type), None)
+ from canary.api import SensorType
+ canary_sensor_type = None
+ if self._sensor_type[0] == "air_quality":
+ canary_sensor_type = SensorType.AIR_QUALITY
+ elif self._sensor_type[0] == "temperature":
+ canary_sensor_type = SensorType.TEMPERATURE
+ elif self._sensor_type[0] == "humidity":
+ canary_sensor_type = SensorType.HUMIDITY
+
+ value = self._data.get_reading(self._device_id, canary_sensor_type)
+
if value is not None:
self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION)
diff --git a/homeassistant/components/sensor/coinbase.py b/homeassistant/components/sensor/coinbase.py
index d66c7d4e4b6..32e1d8f211a 100644
--- a/homeassistant/components/sensor/coinbase.py
+++ b/homeassistant/components/sensor/coinbase.py
@@ -4,21 +4,22 @@ Support for Coinbase sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.coinbase/
"""
-from homeassistant.helpers.entity import Entity
from homeassistant.const import ATTR_ATTRIBUTION
+from homeassistant.helpers.entity import Entity
-
-DEPENDENCIES = ['coinbase']
-
-DATA_COINBASE = 'coinbase_cache'
-
-CONF_ATTRIBUTION = "Data provided by coinbase.com"
ATTR_NATIVE_BALANCE = "Balance in native currency"
BTC_ICON = 'mdi:currency-btc'
-ETH_ICON = 'mdi:currency-eth'
+
COIN_ICON = 'mdi:coin'
+CONF_ATTRIBUTION = "Data provided by coinbase.com"
+
+DATA_COINBASE = 'coinbase_cache'
+DEPENDENCIES = ['coinbase']
+
+ETH_ICON = 'mdi:currency-eth'
+
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Coinbase sensors."""
@@ -26,13 +27,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return
if 'account' in discovery_info:
account = discovery_info['account']
- sensor = AccountSensor(hass.data[DATA_COINBASE],
- account['name'],
- account['balance']['currency'])
+ sensor = AccountSensor(
+ hass.data[DATA_COINBASE], account['name'],
+ account['balance']['currency'])
if 'exchange_currency' in discovery_info:
- sensor = ExchangeRateSensor(hass.data[DATA_COINBASE],
- discovery_info['exchange_currency'],
- discovery_info['native_currency'])
+ sensor = ExchangeRateSensor(
+ hass.data[DATA_COINBASE], discovery_info['exchange_currency'],
+ discovery_info['native_currency'])
add_devices([sensor], True)
@@ -78,8 +79,8 @@ class AccountSensor(Entity):
"""Return the state attributes of the sensor."""
return {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
- ATTR_NATIVE_BALANCE: "{} {}".format(self._native_balance,
- self._native_currency)
+ ATTR_NATIVE_BALANCE: "{} {}".format(
+ self._native_balance, self._native_currency),
}
def update(self):
diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py
index 9d69583f673..f8ada07eec6 100644
--- a/homeassistant/components/sensor/coinmarketcap.py
+++ b/homeassistant/components/sensor/coinmarketcap.py
@@ -16,7 +16,7 @@ from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_CURRENCY)
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['coinmarketcap==4.1.2']
+REQUIREMENTS = ['coinmarketcap==4.2.1']
_LOGGER = logging.getLogger(__name__)
@@ -26,6 +26,7 @@ ATTR_MARKET_CAP = 'market_cap'
ATTR_NAME = 'name'
ATTR_PERCENT_CHANGE_24H = 'percent_change_24h'
ATTR_PERCENT_CHANGE_7D = 'percent_change_7d'
+ATTR_PERCENT_CHANGE_1H = 'percent_change_1h'
ATTR_PRICE = 'price'
ATTR_SYMBOL = 'symbol'
ATTR_TOTAL_SUPPLY = 'total_supply'
@@ -105,6 +106,7 @@ class CoinMarketCapSensor(Entity):
'market_cap_{}'.format(self.data.display_currency)),
ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'),
ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'),
+ ATTR_PERCENT_CHANGE_1H: self._ticker.get('percent_change_1h'),
ATTR_SYMBOL: self._ticker.get('symbol'),
ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'),
}
diff --git a/homeassistant/components/sensor/comfoconnect.py b/homeassistant/components/sensor/comfoconnect.py
index 9df28d861ee..ad6b07fb3da 100644
--- a/homeassistant/components/sensor/comfoconnect.py
+++ b/homeassistant/components/sensor/comfoconnect.py
@@ -96,7 +96,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class ComfoConnectSensor(Entity):
"""Representation of a ComfoConnect sensor."""
- def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type):
+ def __init__(self, hass, name, ccb: ComfoConnectBridge,
+ sensor_type) -> None:
"""Initialize the ComfoConnect sensor."""
self._ccb = ccb
self._sensor_type = sensor_type
diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py
index ad571110e88..0b2f6495b45 100644
--- a/homeassistant/components/sensor/daikin.py
+++ b/homeassistant/components/sensor/daikin.py
@@ -57,7 +57,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DaikinClimateSensor(Entity):
"""Representation of a Sensor."""
- def __init__(self, api, monitored_state, units: UnitSystem, name=None):
+ def __init__(self, api, monitored_state, units: UnitSystem,
+ name=None) -> None:
"""Initialize the sensor."""
self._api = api
self._sensor = SENSOR_TYPES.get(monitored_state)
@@ -94,11 +95,6 @@ class DaikinClimateSensor(Entity):
return value
- @property
- def unique_id(self):
- """Return the ID of this AC."""
- return "{}.{}".format(self.__class__, self._api.ip_address)
-
@property
def icon(self):
"""Icon to use in the frontend, if any."""
diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py
index 39a258c5e6a..e224feb7db7 100644
--- a/homeassistant/components/sensor/darksky.py
+++ b/homeassistant/components/sensor/darksky.py
@@ -56,6 +56,9 @@ SENSOR_TYPES = {
'precip_probability': ['Precip Probability',
'%', '%', '%', '%', '%', 'mdi:water-percent',
['currently', 'minutely', 'hourly', 'daily']],
+ 'precip_accumulation': ['Precip Accumulation',
+ 'cm', 'in', 'cm', 'cm', 'cm', 'mdi:weather-snowy',
+ ['hourly', 'daily']],
'temperature': ['Temperature',
'°C', '°F', '°C', '°C', '°C', 'mdi:thermometer',
['currently', 'hourly']],
@@ -269,7 +272,8 @@ class DarkSkySensor(Entity):
'temperature_max',
'apparent_temperature_min',
'apparent_temperature_max',
- 'precip_intensity_max']):
+ 'precip_intensity_max',
+ 'precip_accumulation']):
self.forecast_data.update_daily()
daily = self.forecast_data.data_daily
if self.type == 'daily_summary':
@@ -309,6 +313,7 @@ class DarkSkySensor(Entity):
'temperature_min', 'temperature_max',
'apparent_temperature_min',
'apparent_temperature_max',
+ 'precip_accumulation',
'pressure', 'ozone', 'uvIndex']):
return round(state, 1)
return state
diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py
index 7c2c1e0895f..b3adaa412ff 100644
--- a/homeassistant/components/sensor/deconz.py
+++ b/homeassistant/components/sensor/deconz.py
@@ -74,6 +74,11 @@ class DeconzSensor(Entity):
"""Return the name of the sensor."""
return self._sensor.name
+ @property
+ def unique_id(self):
+ """Return a unique identifier for this sensor."""
+ return self._sensor.uniqueid
+
@property
def device_class(self):
"""Return the class of the sensor."""
@@ -139,6 +144,11 @@ class DeconzBattery(Entity):
"""Return the name of the battery."""
return self._name
+ @property
+ def unique_id(self):
+ """Return a unique identifier for the device."""
+ return self._device.uniqueid
+
@property
def device_class(self):
"""Return the class of the sensor."""
diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py
index 278cf5382c1..2b125155892 100644
--- a/homeassistant/components/sensor/deutsche_bahn.py
+++ b/homeassistant/components/sensor/deutsche_bahn.py
@@ -14,12 +14,14 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['schiene==0.20']
+REQUIREMENTS = ['schiene==0.21']
_LOGGER = logging.getLogger(__name__)
CONF_DESTINATION = 'to'
CONF_START = 'from'
+CONF_ONLY_DIRECT = 'only_direct'
+DEFAULT_ONLY_DIRECT = False
ICON = 'mdi:train'
@@ -28,6 +30,7 @@ SCAN_INTERVAL = timedelta(minutes=2)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DESTINATION): cv.string,
vol.Required(CONF_START): cv.string,
+ vol.Optional(CONF_ONLY_DIRECT, default=DEFAULT_ONLY_DIRECT): cv.boolean,
})
@@ -35,17 +38,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Deutsche Bahn Sensor."""
start = config.get(CONF_START)
destination = config.get(CONF_DESTINATION)
+ only_direct = config.get(CONF_ONLY_DIRECT)
- add_devices([DeutscheBahnSensor(start, destination)], True)
+ add_devices([DeutscheBahnSensor(start, destination, only_direct)], True)
class DeutscheBahnSensor(Entity):
"""Implementation of a Deutsche Bahn sensor."""
- def __init__(self, start, goal):
+ def __init__(self, start, goal, only_direct):
"""Initialize the sensor."""
self._name = '{} to {}'.format(start, goal)
- self.data = SchieneData(start, goal)
+ self.data = SchieneData(start, goal, only_direct)
self._state = None
@property
@@ -84,19 +88,21 @@ class DeutscheBahnSensor(Entity):
class SchieneData(object):
"""Pull data from the bahn.de web page."""
- def __init__(self, start, goal):
+ def __init__(self, start, goal, only_direct):
"""Initialize the sensor."""
import schiene
self.start = start
self.goal = goal
+ self.only_direct = only_direct
self.schiene = schiene.Schiene()
self.connections = [{}]
def update(self):
"""Update the connection data."""
self.connections = self.schiene.connections(
- self.start, self.goal, dt_util.as_local(dt_util.utcnow()))
+ self.start, self.goal, dt_util.as_local(dt_util.utcnow()),
+ self.only_direct)
if not self.connections:
self.connections = [{}]
@@ -108,6 +114,6 @@ class SchieneData(object):
con.pop('details')
delay = con.get('delay', {'delay_departure': 0,
'delay_arrival': 0})
- # IMHO only delay_departure is useful
con['delay'] = delay['delay_departure']
+ con['delay_arrival'] = delay['delay_arrival']
con['ontime'] = con.get('ontime', False)
diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py
index 5b20ac0f4d0..32c888bad3b 100644
--- a/homeassistant/components/sensor/dsmr.py
+++ b/homeassistant/components/sensor/dsmr.py
@@ -87,14 +87,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
async_add_devices(devices)
def update_entities_telegram(telegram):
- """Update entities with latests telegram and trigger state update."""
+ """Update entities with latest telegram and trigger state update."""
# Make all device entities aware of new telegram
for device in devices:
device.telegram = telegram
hass.async_add_job(device.async_update_ha_state())
- # Creates a asyncio.Protocol factory for reading DSMR telegrams from serial
- # and calls update_entities_telegram to update entities on arrival
+ # 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]:
reader_factory = partial(
create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT],
@@ -122,7 +122,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if transport:
# Register listener to close transport on HA shutdown
- stop_listerer = hass.bus.async_listen_once(
+ stop_listener = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, transport.close)
# Wait for reader to close
@@ -131,8 +131,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
if hass.state != CoreState.stopping:
# Unexpected disconnect
if transport:
- # remove listerer
- stop_listerer()
+ # remove listener
+ stop_listener()
# Reflect disconnect state in devices state by setting an
# empty telegram resulting in `unknown` states
diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py
index 27d4bdd2f5a..eee959fceba 100644
--- a/homeassistant/components/sensor/ebox.py
+++ b/homeassistant/components/sensor/ebox.py
@@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
ebox_data = EBoxData(username, password)
ebox_data.update()
except requests.exceptions.HTTPError as error:
- _LOGGER.error("Failt login: %s", error)
+ _LOGGER.error("Failed login: %s", error)
return False
name = config.get(CONF_NAME)
diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py
index a0c6f7a92e4..dad770d5bab 100644
--- a/homeassistant/components/sensor/ecobee.py
+++ b/homeassistant/components/sensor/ecobee.py
@@ -50,18 +50,13 @@ class EcobeeSensor(Entity):
@property
def name(self):
"""Return the name of the Ecobee sensor."""
- return self._name.rstrip()
+ return self._name
@property
def state(self):
"""Return the state of the sensor."""
return self._state
- @property
- def unique_id(self):
- """Return the unique ID of this sensor."""
- return "sensor_ecobee_{}_{}".format(self._name, self.index)
-
@property
def unit_of_measurement(self):
"""Return the unit of measurement this sensor expresses itself in."""
diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py
index fe05da3ccdd..ef06458cd84 100644
--- a/homeassistant/components/sensor/eddystone_temperature.py
+++ b/homeassistant/components/sensor/eddystone_temperature.py
@@ -122,7 +122,7 @@ class EddystoneTemp(Entity):
class Monitor(object):
- """Continously scan for BLE advertisements."""
+ """Continuously scan for BLE advertisements."""
def __init__(self, hass, devices, bt_device_id):
"""Construct interface object."""
@@ -150,7 +150,7 @@ class Monitor(object):
self.scanning = False
def start(self):
- """Continously scan for BLE advertisements."""
+ """Continuously scan for BLE advertisements."""
if not self.scanning:
self.scanner.start()
self.scanning = True
diff --git a/homeassistant/components/sensor/eight_sleep.py b/homeassistant/components/sensor/eight_sleep.py
index e6f4addf003..e0a42fdb6a8 100644
--- a/homeassistant/components/sensor/eight_sleep.py
+++ b/homeassistant/components/sensor/eight_sleep.py
@@ -65,7 +65,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class EightHeatSensor(EightSleepHeatEntity):
- """Representation of a eight sleep heat-based sensor."""
+ """Representation of an eight sleep heat-based sensor."""
def __init__(self, name, eight, sensor):
"""Initialize the sensor."""
@@ -116,7 +116,7 @@ class EightHeatSensor(EightSleepHeatEntity):
class EightUserSensor(EightSleepUserEntity):
- """Representation of a eight sleep user-based sensor."""
+ """Representation of an eight sleep user-based sensor."""
def __init__(self, name, eight, sensor, units):
"""Initialize the sensor."""
@@ -232,7 +232,7 @@ class EightUserSensor(EightSleepUserEntity):
class EightRoomSensor(EightSleepUserEntity):
- """Representation of a eight sleep room sensor."""
+ """Representation of an eight sleep room sensor."""
def __init__(self, name, eight, sensor, units):
"""Initialize the sensor."""
diff --git a/homeassistant/components/sensor/emoncms.py b/homeassistant/components/sensor/emoncms.py
index fc1daf151c7..cd02137f4d5 100644
--- a/homeassistant/components/sensor/emoncms.py
+++ b/homeassistant/components/sensor/emoncms.py
@@ -153,7 +153,7 @@ class EmonCmsSensor(Entity):
@property
def device_state_attributes(self):
- """Return the atrributes of the sensor."""
+ """Return the attributes of the sensor."""
return {
ATTR_FEEDID: self._elem["id"],
ATTR_TAG: self._elem["tag"],
diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py
index 07c085cd18d..4fc79745b99 100644
--- a/homeassistant/components/sensor/fido.py
+++ b/homeassistant/components/sensor/fido.py
@@ -50,12 +50,12 @@ SENSOR_TYPES = {
'text_int_used': ['International text used',
MESSAGES, 'mdi:message-alert'],
'text_int_limit': ['International text limit',
- MESSAGES, 'mdi:message-alart'],
- 'text_int_remaining': ['Internaltional remaining',
+ MESSAGES, 'mdi:message-alert'],
+ 'text_int_remaining': ['International remaining',
MESSAGES, 'mdi:message-alert'],
'talk_used': ['Talk used', MINUTES, 'mdi:cellphone'],
'talk_limit': ['Talk limit', MINUTES, 'mdi:cellphone'],
- 'talt_remaining': ['Talk remaining', MINUTES, 'mdi:cellphone'],
+ 'talk_remaining': ['Talk remaining', MINUTES, 'mdi:cellphone'],
'other_talk_used': ['Other Talk used', MINUTES, 'mdi:cellphone'],
'other_talk_limit': ['Other Talk limit', MINUTES, 'mdi:cellphone'],
'other_talk_remaining': ['Other Talk remaining', MINUTES, 'mdi:cellphone'],
diff --git a/homeassistant/components/sensor/fritzbox_callmonitor.py b/homeassistant/components/sensor/fritzbox_callmonitor.py
index ea6382ce795..b443bd56f03 100644
--- a/homeassistant/components/sensor/fritzbox_callmonitor.py
+++ b/homeassistant/components/sensor/fritzbox_callmonitor.py
@@ -70,8 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
phonebook = FritzBoxPhonebook(
host=host, port=port, username=username, password=password,
phonebook_id=phonebook_id, prefixes=prefixes)
- # pylint: disable=bare-except
- except:
+ except: # noqa: E722 # pylint: disable=bare-except
phonebook = None
_LOGGER.warning("Phonebook with ID %s not found on Fritz!Box",
phonebook_id)
diff --git a/homeassistant/components/sensor/gearbest.py b/homeassistant/components/sensor/gearbest.py
index 2bc7e5b3b3a..aa1d2d9eff0 100644
--- a/homeassistant/components/sensor/gearbest.py
+++ b/homeassistant/components/sensor/gearbest.py
@@ -1,5 +1,5 @@
"""
-Parse prices of a item from gearbest.
+Parse prices of an item from gearbest.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.gearbest/
diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py
index af31c14789a..cae2eaf7437 100644
--- a/homeassistant/components/sensor/hive.py
+++ b/homeassistant/components/sensor/hive.py
@@ -48,5 +48,5 @@ class HiveSensorEntity(Entity):
return self.session.sensor.hub_online_status(self.node_id)
def update(self):
- """Update all Node data frome Hive."""
+ """Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
diff --git a/homeassistant/components/sensor/ihc.py b/homeassistant/components/sensor/ihc.py
index 3ad86e51f97..b6440a407a4 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):
+ 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/influxdb.py b/homeassistant/components/sensor/influxdb.py
index 8adf85f0a2e..c0d492984e0 100644
--- a/homeassistant/components/sensor/influxdb.py
+++ b/homeassistant/components/sensor/influxdb.py
@@ -4,25 +4,25 @@ InfluxDB component which allows you to get data from an Influx database.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.influxdb/
"""
-import logging
from datetime import timedelta
+import logging
import voluptuous as vol
-from homeassistant.components.sensor import PLATFORM_SCHEMA
-from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_USERNAME,
- CONF_PASSWORD, CONF_SSL, CONF_VERIFY_SSL,
- CONF_NAME, CONF_UNIT_OF_MEASUREMENT,
- CONF_VALUE_TEMPLATE)
-from homeassistant.const import STATE_UNKNOWN
-from homeassistant.util import Throttle
+from homeassistant.components.influxdb import CONF_DB_NAME
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL,
+ CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE,
+ CONF_VERIFY_SSL, STATE_UNKNOWN)
from homeassistant.exceptions import TemplateError
-from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['influxdb==4.1.1']
+REQUIREMENTS = ['influxdb==5.0.0']
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8086
@@ -32,13 +32,13 @@ DEFAULT_VERIFY_SSL = False
DEFAULT_GROUP_FUNCTION = 'mean'
DEFAULT_FIELD = 'value'
-CONF_DB_NAME = 'database'
CONF_QUERIES = 'queries'
CONF_GROUP_FUNCTION = 'group_function'
CONF_FIELD = 'field'
CONF_MEASUREMENT_NAME = 'measurement'
CONF_WHERE = 'where'
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
_QUERY_SCHEME = vol.Schema({
vol.Required(CONF_NAME): cv.string,
@@ -62,9 +62,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean
})
-# Return cached results if last scan was less then this time ago
-MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
-
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the InfluxDB component."""
@@ -122,7 +119,7 @@ class InfluxSensor(Entity):
except exceptions.InfluxDBClientError as exc:
_LOGGER.error("Database host is not accessible due to '%s', please"
" check your entries in the configuration file and"
- " that the database exists and is READ/WRITE.", exc)
+ " that the database exists and is READ/WRITE", exc)
self.connected = False
@property
diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py
index 9a23da48a6b..398c0b350ee 100644
--- a/homeassistant/components/sensor/ios.py
+++ b/homeassistant/components/sensor/ios.py
@@ -58,7 +58,7 @@ class IOSSensor(Entity):
def unique_id(self):
"""Return the unique ID of this sensor."""
device_id = self._device[ios.ATTR_DEVICE_ID]
- return "sensor_ios_battery_{}_{}".format(self.type, device_id)
+ return "{}_{}".format(self.type, device_id)
@property
def unit_of_measurement(self):
diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py
index 76f026bba10..39c9d8a3b9d 100644
--- a/homeassistant/components/sensor/isy994.py
+++ b/homeassistant/components/sensor/isy994.py
@@ -317,11 +317,6 @@ class ISYWeatherDevice(ISYDevice):
"""Initialize the ISY994 weather device."""
super().__init__(node)
- @property
- def unique_id(self) -> str:
- """Return the unique identifier for the node."""
- return self._node.name
-
@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 0a455099597..70afa6fe1e1 100644
--- a/homeassistant/components/sensor/knx.py
+++ b/homeassistant/components/sensor/knx.py
@@ -52,7 +52,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
@callback
def async_add_devices_config(hass, config, async_add_devices):
- """Set up sensor for KNX platform configured within plattform."""
+ """Set up sensor for KNX platform configured within platform."""
import xknx
sensor = xknx.devices.Sensor(
hass.data[DATA_KNX].xknx,
diff --git a/homeassistant/components/sensor/melissa.py b/homeassistant/components/sensor/melissa.py
new file mode 100644
index 00000000000..58313428861
--- /dev/null
+++ b/homeassistant/components/sensor/melissa.py
@@ -0,0 +1,98 @@
+"""
+Support for Melissa climate Sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.melissa/
+"""
+import logging
+
+from homeassistant.components.melissa import DATA_MELISSA
+from homeassistant.const import TEMP_CELSIUS
+from homeassistant.helpers.entity import Entity
+
+DEPENDENCIES = ['melissa']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup the melissa sensor platform."""
+ sensors = []
+ api = hass.data[DATA_MELISSA]
+ devices = api.fetch_devices().values()
+
+ for device in devices:
+ sensors.append(MelissaTemperatureSensor(device, api))
+ sensors.append(MelissaHumiditySensor(device, api))
+ add_devices(sensors)
+
+
+class MelissaSensor(Entity):
+ """Representation of a Melissa Sensor."""
+
+ _type = 'generic'
+
+ def __init__(self, device, api):
+ """Initialize the sensor."""
+ self._api = api
+ self._state = None
+ self._name = '{0} {1}'.format(
+ device['name'],
+ self._type
+ )
+ self._serial = device['serial_number']
+ self._data = device['controller_log']
+
+ @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
+
+ def update(self):
+ """Fetch status from melissa."""
+ self._data = self._api.status(cached=True)
+
+
+class MelissaTemperatureSensor(MelissaSensor):
+ """Representation of a Melissa temperature Sensor."""
+
+ _type = 'temperature'
+ _unit = TEMP_CELSIUS
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit
+
+ def update(self):
+ """Fetch new state data for the sensor."""
+ super().update()
+ try:
+ self._state = self._data[self._serial]['temp']
+ except KeyError:
+ _LOGGER.warning("Unable to get temperature for %s", self.entity_id)
+
+
+class MelissaHumiditySensor(MelissaSensor):
+ """Representation of a Melissa humidity Sensor."""
+
+ _type = 'humidity'
+ _unit = '%'
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit
+
+ def update(self):
+ """Fetch new state data for the sensor."""
+ super().update()
+ try:
+ self._state = self._data[self._serial]['humidity']
+ except KeyError:
+ _LOGGER.warning("Unable to get humidity for %s", self.entity_id)
diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py
new file mode 100644
index 00000000000..bc368745e40
--- /dev/null
+++ b/homeassistant/components/sensor/mercedesme.py
@@ -0,0 +1,83 @@
+"""
+Support for Mercedes cars with Mercedes ME.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/sensor.mercedesme/
+"""
+import logging
+import datetime
+
+from homeassistant.components.mercedesme import (
+ DATA_MME, MercedesMeEntity, SENSORS)
+
+
+DEPENDENCIES = ['mercedesme']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup the sensor platform."""
+ if discovery_info is None:
+ return
+
+ data = hass.data[DATA_MME].data
+
+ if not data.cars:
+ return
+
+ devices = []
+ for car in data.cars:
+ for key, value in sorted(SENSORS.items()):
+ devices.append(
+ MercedesMESensor(data, key, value[0], car["vin"], value[1]))
+
+ add_devices(devices, True)
+
+
+class MercedesMESensor(MercedesMeEntity):
+ """Representation of a Sensor."""
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ def update(self):
+ """Get the latest data and updates the states."""
+ _LOGGER.debug("Updating %s", self._internal_name)
+
+ self._car = next(
+ car for car in self._data.cars if car["vin"] == self._vin)
+
+ if self._internal_name == "latestTrip":
+ self._state = self._car["latestTrip"]["id"]
+ else:
+ self._state = self._car[self._internal_name]
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self._internal_name == "latestTrip":
+ return {
+ "duration_seconds":
+ self._car["latestTrip"]["durationSeconds"],
+ "distance_traveled_km":
+ self._car["latestTrip"]["distanceTraveledKm"],
+ "started_at": datetime.datetime.fromtimestamp(
+ self._car["latestTrip"]["startedAt"]
+ ).strftime('%Y-%m-%d %H:%M:%S'),
+ "average_speed_km_per_hr":
+ self._car["latestTrip"]["averageSpeedKmPerHr"],
+ "finished": self._car["latestTrip"]["finished"],
+ "last_update": datetime.datetime.fromtimestamp(
+ self._car["lastUpdate"]
+ ).strftime('%Y-%m-%d %H:%M:%S'),
+ "car": self._car["license"]
+ }
+
+ return {
+ "last_update": datetime.datetime.fromtimestamp(
+ self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'),
+ "car": self._car["license"]
+ }
diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py
index 772e59f266e..b6366de6432 100644
--- a/homeassistant/components/sensor/metoffice.py
+++ b/homeassistant/components/sensor/metoffice.py
@@ -47,7 +47,7 @@ CONDITION_CLASSES = {
DEFAULT_NAME = "Met Office"
-VISIBILTY_CLASSES = {
+VISIBILITY_CLASSES = {
'VP': '<1',
'PO': '1-4',
'MO': '4-10',
@@ -144,7 +144,7 @@ class MetOfficeCurrentSensor(Entity):
"""Return the state of the sensor."""
if (self._condition == 'visibility_distance' and
hasattr(self.data.data, 'visibility')):
- return VISIBILTY_CLASSES.get(self.data.data.visibility.value)
+ return VISIBILITY_CLASSES.get(self.data.data.visibility.value)
if hasattr(self.data.data, self._condition):
variable = getattr(self.data.data, self._condition)
if self._condition == 'weather':
diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py
index 56f8c3cfe47..ec68588f241 100644
--- a/homeassistant/components/sensor/miflora.py
+++ b/homeassistant/components/sensor/miflora.py
@@ -16,7 +16,7 @@ from homeassistant.const import (
)
-REQUIREMENTS = ['miflora==0.2.0']
+REQUIREMENTS = ['miflora==0.3.0']
_LOGGER = logging.getLogger(__name__)
@@ -138,12 +138,16 @@ class MiFloraSensor(Entity):
This uses a rolling median over 3 values to filter out outliers.
"""
+ from miflora.backends import BluetoothBackendException
try:
_LOGGER.debug("Polling data for %s", self.name)
data = self.poller.parameter_value(self.parameter)
except IOError as ioerr:
_LOGGER.info("Polling error %s", ioerr)
return
+ except BluetoothBackendException as bterror:
+ _LOGGER.info("Polling error %s", bterror)
+ return
if data is not None:
_LOGGER.debug("%s = %s", self.name, data)
diff --git a/homeassistant/components/sensor/modem_callerid.py b/homeassistant/components/sensor/modem_callerid.py
index 0b71540f346..f80ea5853c8 100644
--- a/homeassistant/components/sensor/modem_callerid.py
+++ b/homeassistant/components/sensor/modem_callerid.py
@@ -18,7 +18,7 @@ REQUIREMENTS = ['basicmodem==0.7']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Modem CallerID'
-ICON = 'mdi:phone-clasic'
+ICON = 'mdi:phone-classic'
DEFAULT_DEVICE = '/dev/ttyACM0'
STATE_RING = 'ring'
diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py
index 8ace931a8cc..c20e0a59408 100644
--- a/homeassistant/components/sensor/netatmo.py
+++ b/homeassistant/components/sensor/netatmo.py
@@ -113,8 +113,7 @@ class NetAtmoSensor(Entity):
module_id = self.netatmo_data.\
station_data.moduleByName(module=module_name)['_id']
self.module_id = module_id[1]
- self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format(
- self._name, module_id, self.type)
+ self._unique_id = '{}-{}'.format(self.module_id, self.type)
@property
def name(self):
diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py
index 2228a8eab60..a9fb3ae7a6f 100644
--- a/homeassistant/components/sensor/nut.py
+++ b/homeassistant/components/sensor/nut.py
@@ -243,7 +243,7 @@ class PyNUTData(object):
"""
def __init__(self, host, port, alias, username, password):
- """Initialize the data oject."""
+ """Initialize the data object."""
from pynut2.nut2 import PyNUTClient, PyNUTError
self._host = host
self._port = port
diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py
index 1f58eb4c13e..8a07d3484d5 100644
--- a/homeassistant/components/sensor/onewire.py
+++ b/homeassistant/components/sensor/onewire.py
@@ -66,8 +66,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device_file, 'temperature'))
else:
for family_file_path in glob(os.path.join(base_dir, '*', 'family')):
- family_file = open(family_file_path, "r")
- family = family_file.read()
+ with open(family_file_path, "r") as family_file:
+ family = family_file.read()
if family in DEVICE_SENSORS:
for sensor_key, sensor_value in DEVICE_SENSORS[family].items():
sensor_id = os.path.split(
diff --git a/homeassistant/components/sensor/openevse.py b/homeassistant/components/sensor/openevse.py
index 6ded982eea1..9086f37f8b2 100644
--- a/homeassistant/components/sensor/openevse.py
+++ b/homeassistant/components/sensor/openevse.py
@@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'status': ['Charging Status', None],
'charge_time': ['Charge Time Elapsed', 'minutes'],
- 'ambient_temp': ['Ambient Termperature', TEMP_CELSIUS],
+ 'ambient_temp': ['Ambient Temperature', TEMP_CELSIUS],
'ir_temp': ['IR Temperature', TEMP_CELSIUS],
'rtc_temp': ['RTC Temperature', TEMP_CELSIUS],
'usage_session': ['Usage this Session', 'kWh'],
diff --git a/homeassistant/components/sensor/pilight.py b/homeassistant/components/sensor/pilight.py
index 86ca496eb62..5b5385f14ef 100644
--- a/homeassistant/components/sensor/pilight.py
+++ b/homeassistant/components/sensor/pilight.py
@@ -79,10 +79,10 @@ class PilightSensor(Entity):
def _handle_code(self, call):
"""Handle received code by the pilight-daemon.
- If the code matches the defined playload
+ If the code matches the defined payload
of this sensor the sensor state is changed accordingly.
"""
- # Check if received code matches defined playoad
+ # Check if received code matches defined payload
# True if payload is contained in received code dict, not
# all items have to match
if self._payload.items() <= call.data.items():
diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py
new file mode 100644
index 00000000000..3998af7e32f
--- /dev/null
+++ b/homeassistant/components/sensor/pollen.py
@@ -0,0 +1,322 @@
+"""
+Support for Pollen.com allergen and cold/flu sensors.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.pollen/
+"""
+import logging
+from datetime import timedelta
+from statistics import mean
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS
+)
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['pypollencom==1.1.1']
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_ALLERGEN_GENUS = 'primary_allergen_genus'
+ATTR_ALLERGEN_NAME = 'primary_allergen_name'
+ATTR_ALLERGEN_TYPE = 'primary_allergen_type'
+ATTR_CITY = 'city'
+ATTR_OUTLOOK = 'outlook'
+ATTR_RATING = 'rating'
+ATTR_SEASON = 'season'
+ATTR_TREND = 'trend'
+ATTR_ZIP_CODE = 'zip_code'
+
+CONF_ZIP_CODE = 'zip_code'
+
+DEFAULT_ATTRIBUTION = 'Data provided by IQVIAâ„¢'
+
+MIN_TIME_UPDATE_AVERAGES = timedelta(hours=12)
+MIN_TIME_UPDATE_INDICES = timedelta(minutes=10)
+
+CONDITIONS = {
+ 'allergy_average_forecasted': (
+ 'Allergy Index: Forecasted Average',
+ 'AllergyAverageSensor',
+ 'allergy_average_data',
+ {'data_attr': 'extended_data'},
+ 'mdi:flower'
+ ),
+ 'allergy_average_historical': (
+ 'Allergy Index: Historical Average',
+ 'AllergyAverageSensor',
+ 'allergy_average_data',
+ {'data_attr': 'historic_data'},
+ 'mdi:flower'
+ ),
+ 'allergy_index_today': (
+ 'Allergy Index: Today',
+ 'AllergyIndexSensor',
+ 'allergy_index_data',
+ {'key': 'Today'},
+ 'mdi:flower'
+ ),
+ 'allergy_index_tomorrow': (
+ 'Allergy Index: Tomorrow',
+ 'AllergyIndexSensor',
+ 'allergy_index_data',
+ {'key': 'Tomorrow'},
+ 'mdi:flower'
+ ),
+ 'allergy_index_yesterday': (
+ 'Allergy Index: Yesterday',
+ 'AllergyIndexSensor',
+ 'allergy_index_data',
+ {'key': 'Yesterday'},
+ 'mdi:flower'
+ ),
+ 'disease_average_forecasted': (
+ 'Cold & Flu: Forecasted Average',
+ 'AllergyAverageSensor',
+ 'disease_average_data',
+ {'data_attr': 'extended_data'},
+ 'mdi:snowflake'
+ )
+}
+
+RATING_MAPPING = [{
+ 'label': 'Low',
+ 'minimum': 0.0,
+ 'maximum': 2.4
+}, {
+ 'label': 'Low/Medium',
+ 'minimum': 2.5,
+ 'maximum': 4.8
+}, {
+ 'label': 'Medium',
+ 'minimum': 4.9,
+ 'maximum': 7.2
+}, {
+ 'label': 'Medium/High',
+ 'minimum': 7.3,
+ 'maximum': 9.6
+}, {
+ 'label': 'High',
+ 'minimum': 9.7,
+ 'maximum': 12
+}]
+
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_ZIP_CODE): cv.positive_int,
+ vol.Required(CONF_MONITORED_CONDITIONS):
+ vol.All(cv.ensure_list, [vol.In(CONDITIONS)]),
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Configure the platform and add the sensors."""
+ from pypollencom import Client
+
+ _LOGGER.debug('Configuration data: %s', config)
+
+ client = Client(config[CONF_ZIP_CODE])
+ datas = {
+ 'allergy_average_data': AllergyAveragesData(client),
+ 'allergy_index_data': AllergyIndexData(client),
+ 'disease_average_data': DiseaseData(client)
+ }
+
+ for data in datas.values():
+ data.update()
+
+ sensors = []
+ for condition in config[CONF_MONITORED_CONDITIONS]:
+ name, sensor_class, data_key, params, icon = CONDITIONS[condition]
+ sensors.append(globals()[sensor_class](
+ datas[data_key],
+ params,
+ name,
+ icon
+ ))
+
+ add_devices(sensors, True)
+
+
+def calculate_trend(list_of_nums):
+ """Calculate the most common rating as a trend."""
+ ratings = list(
+ r['label'] for n in list_of_nums
+ for r in RATING_MAPPING
+ if r['minimum'] <= n <= r['maximum'])
+ return max(set(ratings), key=ratings.count)
+
+
+class BaseSensor(Entity):
+ """Define a base class for all of our sensors."""
+
+ def __init__(self, data, data_params, name, icon):
+ """Initialize the sensor."""
+ self._attrs = {}
+ self._icon = icon
+ self._name = name
+ self._data_params = data_params
+ self._state = None
+ self._unit = None
+ self.data = data
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ self._attrs.update({ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION})
+ return self._attrs
+
+ @property
+ def icon(self):
+ """Return the icon."""
+ return self._icon
+
+ @property
+ def name(self):
+ """Return the name."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit the value is expressed in."""
+ return self._unit
+
+
+class AllergyAverageSensor(BaseSensor):
+ """Define a sensor to show allergy average information."""
+
+ def update(self):
+ """Update the status of the sensor."""
+ self.data.update()
+
+ data_attr = getattr(self.data, self._data_params['data_attr'])
+ indices = [
+ p['Index']
+ for p in data_attr['Location']['periods']
+ ]
+ average = round(mean(indices), 1)
+
+ self._attrs[ATTR_TREND] = calculate_trend(indices)
+ self._attrs[ATTR_CITY] = data_attr['Location']['City'].title()
+ self._attrs[ATTR_STATE] = data_attr['Location']['State']
+ self._attrs[ATTR_ZIP_CODE] = data_attr['Location']['ZIP']
+
+ [rating] = [
+ i['label'] for i in RATING_MAPPING
+ if i['minimum'] <= average <= i['maximum']
+ ]
+ self._attrs[ATTR_RATING] = rating
+
+ self._state = average
+ self._unit = 'index'
+
+
+class AllergyIndexSensor(BaseSensor):
+ """Define a sensor to show allergy index information."""
+
+ def update(self):
+ """Update the status of the sensor."""
+ self.data.update()
+
+ location_data = self.data.current_data['Location']
+ [period] = [
+ p for p in location_data['periods']
+ if p['Type'] == self._data_params['key']
+ ]
+
+ self._attrs[ATTR_ALLERGEN_GENUS] = period['Triggers'][0]['Genus']
+ self._attrs[ATTR_ALLERGEN_NAME] = period['Triggers'][0]['Name']
+ self._attrs[ATTR_ALLERGEN_TYPE] = period['Triggers'][0]['PlantType']
+ self._attrs[ATTR_OUTLOOK] = self.data.outlook_data['Outlook']
+ self._attrs[ATTR_SEASON] = self.data.outlook_data['Season']
+ self._attrs[ATTR_TREND] = self.data.outlook_data[
+ 'Trend'].title()
+ self._attrs[ATTR_CITY] = location_data['City'].title()
+ self._attrs[ATTR_STATE] = location_data['State']
+ self._attrs[ATTR_ZIP_CODE] = location_data['ZIP']
+
+ [rating] = [
+ i['label'] for i in RATING_MAPPING
+ if i['minimum'] <= period['Index'] <= i['maximum']
+ ]
+ self._attrs[ATTR_RATING] = rating
+
+ self._state = period['Index']
+ self._unit = 'index'
+
+
+class DataBase(object):
+ """Define a generic data object."""
+
+ def __init__(self, client):
+ """Initialize."""
+ self._client = client
+
+ def _get_client_data(self, module, operation):
+ """Get data from a particular point in the API."""
+ from pypollencom.exceptions import HTTPError
+
+ try:
+ data = getattr(getattr(self._client, module), operation)()
+ _LOGGER.debug('Received "%s_%s" data: %s', module,
+ operation, data)
+ except HTTPError as exc:
+ _LOGGER.error('An error occurred while retrieving data')
+ _LOGGER.debug(exc)
+
+ return data
+
+
+class AllergyAveragesData(DataBase):
+ """Define an object to averages on future and historical allergy data."""
+
+ def __init__(self, client):
+ """Initialize."""
+ super().__init__(client)
+ self.extended_data = None
+ self.historic_data = None
+
+ @Throttle(MIN_TIME_UPDATE_AVERAGES)
+ def update(self):
+ """Update with new data."""
+ self.extended_data = self._get_client_data('allergens', 'extended')
+ self.historic_data = self._get_client_data('allergens', 'historic')
+
+
+class AllergyIndexData(DataBase):
+ """Define an object to retrieve current allergy index info."""
+
+ def __init__(self, client):
+ """Initialize."""
+ super().__init__(client)
+ self.current_data = None
+ self.outlook_data = None
+
+ @Throttle(MIN_TIME_UPDATE_INDICES)
+ def update(self):
+ """Update with new index data."""
+ self.current_data = self._get_client_data('allergens', 'current')
+ self.outlook_data = self._get_client_data('allergens', 'outlook')
+
+
+class DiseaseData(DataBase):
+ """Define an object to retrieve current disease index info."""
+
+ def __init__(self, client):
+ """Initialize."""
+ super().__init__(client)
+ self.extended_data = None
+
+ @Throttle(MIN_TIME_UPDATE_INDICES)
+ def update(self):
+ """Update with new cold/flu data."""
+ self.extended_data = self._get_client_data('disease', 'extended')
diff --git a/homeassistant/components/sensor/pushbullet.py b/homeassistant/components/sensor/pushbullet.py
index 086698b06bf..415174ac273 100644
--- a/homeassistant/components/sensor/pushbullet.py
+++ b/homeassistant/components/sensor/pushbullet.py
@@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the Pushbllet Sensor platform."""
+ """Set up the Pushbullet Sensor platform."""
from pushbullet import PushBullet
from pushbullet import InvalidKeyError
try:
diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py
index 20460f9063c..3caebad2007 100644
--- a/homeassistant/components/sensor/qnap.py
+++ b/homeassistant/components/sensor/qnap.py
@@ -197,7 +197,6 @@ class QNAPStatsAPI(object):
self.data = {}
- # pylint: disable=bare-except
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Update API information and store locally."""
@@ -207,7 +206,7 @@ class QNAPStatsAPI(object):
self.data["smart_drive_health"] = self._api.get_smart_disk_health()
self.data["volumes"] = self._api.get_volumes()
self.data["bandwidth"] = self._api.get_bandwidth()
- except:
+ except: # noqa: E722 # pylint: disable=bare-except
_LOGGER.exception("Failed to fetch QNAP stats from the NAS")
diff --git a/homeassistant/components/sensor/radarr.py b/homeassistant/components/sensor/radarr.py
index 3b2c818a7b3..8adaeb3c71b 100644
--- a/homeassistant/components/sensor/radarr.py
+++ b/homeassistant/components/sensor/radarr.py
@@ -162,7 +162,7 @@ class RadarrSensor(Entity):
self.ssl, self.host, self.port, self.urlbase, start, end),
headers={'X-Api-Key': self.apikey}, timeout=10)
except OSError:
- _LOGGER.error("Host %s is not available", self.host)
+ _LOGGER.warning("Host %s is not available", self.host)
self._available = False
self._state = None
return
diff --git a/homeassistant/components/sensor/raincloud.py b/homeassistant/components/sensor/raincloud.py
index d3b8b7207e3..c03aa0a2aec 100644
--- a/homeassistant/components/sensor/raincloud.py
+++ b/homeassistant/components/sensor/raincloud.py
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
RainCloudSensor(raincloud.controller.faucet,
sensor_type))
else:
- # create an sensor for each zone managed by a faucet
+ # create a sensor for each zone managed by a faucet
for zone in raincloud.controller.faucet.zones:
sensors.append(RainCloudSensor(zone, sensor_type))
diff --git a/homeassistant/components/sensor/sonarr.py b/homeassistant/components/sensor/sonarr.py
index 42460a83d6f..090addb5b6e 100644
--- a/homeassistant/components/sensor/sonarr.py
+++ b/homeassistant/components/sensor/sonarr.py
@@ -181,7 +181,7 @@ class SonarrSensor(Entity):
headers={'X-Api-Key': self.apikey},
timeout=10)
except OSError:
- _LOGGER.error("Host %s is not available", self.host)
+ _LOGGER.warning("Host %s is not available", self.host)
self._available = False
self._state = None
return
diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py
new file mode 100644
index 00000000000..99da8c3c680
--- /dev/null
+++ b/homeassistant/components/sensor/sql.py
@@ -0,0 +1,145 @@
+"""
+Sensor from an SQL Query.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.sql/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE)
+from homeassistant.components.recorder import (
+ CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+
+_LOGGER = logging.getLogger(__name__)
+
+REQUIREMENTS = ['sqlalchemy==1.2.2']
+
+CONF_QUERIES = 'queries'
+CONF_QUERY = 'query'
+CONF_COLUMN_NAME = 'column'
+
+_QUERY_SCHEME = vol.Schema({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_QUERY): cv.string,
+ vol.Required(CONF_COLUMN_NAME): cv.string,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
+ vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
+})
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_QUERIES): [_QUERY_SCHEME],
+ vol.Optional(CONF_DB_URL): cv.string,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup the sensor platform."""
+ db_url = config.get(CONF_DB_URL, None)
+ if not db_url:
+ db_url = DEFAULT_URL.format(
+ hass_config_path=hass.config.path(DEFAULT_DB_FILE))
+
+ import sqlalchemy
+ from sqlalchemy.orm import sessionmaker, scoped_session
+
+ try:
+ engine = sqlalchemy.create_engine(db_url)
+ sessionmaker = scoped_session(sessionmaker(bind=engine))
+
+ # run a dummy query just to test the db_url
+ sess = sessionmaker()
+ sess.execute("SELECT 1;")
+
+ except sqlalchemy.exc.SQLAlchemyError as err:
+ _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err)
+ return
+
+ queries = []
+
+ for query in config.get(CONF_QUERIES):
+ name = query.get(CONF_NAME)
+ query_str = query.get(CONF_QUERY)
+ unit = query.get(CONF_UNIT_OF_MEASUREMENT)
+ value_template = query.get(CONF_VALUE_TEMPLATE)
+ column_name = query.get(CONF_COLUMN_NAME)
+
+ if value_template is not None:
+ value_template.hass = hass
+
+ sensor = SQLSensor(
+ name, sessionmaker, query_str, column_name, unit, value_template
+ )
+ queries.append(sensor)
+
+ add_devices(queries, True)
+
+
+class SQLSensor(Entity):
+ """An SQL sensor."""
+
+ def __init__(self, name, sessmaker, query, column, unit, value_template):
+ """Initialize SQL sensor."""
+ self._name = name
+ if "LIMIT" in query:
+ self._query = query
+ else:
+ self._query = query.replace(";", " LIMIT 1;")
+ self._unit_of_measurement = unit
+ self._template = value_template
+ self._column_name = column
+ self.sessionmaker = sessmaker
+ self._state = None
+ self._attributes = None
+
+ @property
+ def name(self):
+ """Return the name of the query."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the query's current state."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit_of_measurement
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ return self._attributes
+
+ def update(self):
+ """Retrieve sensor data from the query."""
+ import sqlalchemy
+ try:
+ sess = self.sessionmaker()
+ result = sess.execute(self._query)
+ except sqlalchemy.exc.SQLAlchemyError as err:
+ _LOGGER.error("Error executing query %s: %s", self._query, err)
+ return
+
+ for res in result:
+ _LOGGER.debug(res.items())
+ data = res[self._column_name]
+ self._attributes = {k: str(v) for k, v in res.items()}
+
+ if data is None:
+ _LOGGER.error("%s returned no results", self._query)
+ return
+
+ if self._template is not None:
+ self._state = self._template.async_render_with_possible_json_value(
+ data, None)
+ else:
+ self._state = data
+
+ sess.close()
diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py
index 19281d36d88..b26fd5cc804 100644
--- a/homeassistant/components/sensor/statistics.py
+++ b/homeassistant/components/sensor/statistics.py
@@ -34,6 +34,8 @@ ATTR_VARIANCE = 'variance'
ATTR_STANDARD_DEVIATION = 'standard_deviation'
ATTR_SAMPLING_SIZE = 'sampling_size'
ATTR_TOTAL = 'total'
+ATTR_MAX_AGE = 'max_age'
+ATTR_MIN_AGE = 'min_age'
CONF_SAMPLING_SIZE = 'sampling_size'
CONF_MAX_AGE = 'max_age'
@@ -88,10 +90,11 @@ class StatisticsSensor(Entity):
self.median = self.mean = self.variance = self.stdev = 0
self.min = self.max = self.total = self.count = 0
self.average_change = self.change = 0
+ self.max_age = self.min_age = 0
if 'recorder' in self._hass.config.components:
# only use the database if it's configured
- hass.async_add_job(self._initzialize_from_database)
+ hass.async_add_job(self._initialize_from_database)
@callback
# pylint: disable=invalid-name
@@ -111,8 +114,7 @@ class StatisticsSensor(Entity):
try:
self.states.append(float(new_state.state))
if self._max_age is not None:
- now = dt_util.utcnow()
- self.ages.append(now)
+ self.ages.append(new_state.last_updated)
self.count = self.count + 1
except ValueError:
self.count = self.count + 1
@@ -141,7 +143,7 @@ class StatisticsSensor(Entity):
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
if not self.is_binary:
- return {
+ state = {
ATTR_MEAN: self.mean,
ATTR_COUNT: self.count,
ATTR_MAX_VALUE: self.max,
@@ -154,6 +156,13 @@ class StatisticsSensor(Entity):
ATTR_CHANGE: self.change,
ATTR_AVERAGE_CHANGE: self.average_change,
}
+ # Only return min/max age if we have a age span
+ if self._max_age:
+ state.update({
+ ATTR_MAX_AGE: self.max_age,
+ ATTR_MIN_AGE: self.min_age,
+ })
+ return state
@property
def icon(self):
@@ -190,6 +199,7 @@ class StatisticsSensor(Entity):
self.stdev = self.variance = STATE_UNKNOWN
if self.states:
+ self.count = len(self.states)
self.total = round(sum(self.states), 2)
self.min = min(self.states)
self.max = max(self.states)
@@ -197,12 +207,15 @@ class StatisticsSensor(Entity):
self.average_change = self.change
if len(self.states) > 1:
self.average_change /= len(self.states) - 1
+ if self._max_age is not None:
+ self.max_age = max(self.ages)
+ self.min_age = min(self.ages)
else:
self.min = self.max = self.total = STATE_UNKNOWN
self.average_change = self.change = STATE_UNKNOWN
@asyncio.coroutine
- def _initzialize_from_database(self):
+ def _initialize_from_database(self):
"""Initialize the list of states from the database.
The query will get the list of states in DESCENDING order so that we
diff --git a/homeassistant/components/sensor/synologydsm.py b/homeassistant/components/sensor/synologydsm.py
index cfc868de664..f5a41c7b8ce 100644
--- a/homeassistant/components/sensor/synologydsm.py
+++ b/homeassistant/components/sensor/synologydsm.py
@@ -136,7 +136,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class SynoApi(object):
"""Class to interface with Synology DSM API."""
- # pylint: disable=bare-except
def __init__(self, host, port, username, password, temp_unit):
"""Initialize the API wrapper class."""
from SynologyDSM import SynologyDSM
@@ -144,7 +143,7 @@ class SynoApi(object):
try:
self._api = SynologyDSM(host, port, username, password)
- except:
+ except: # noqa: E722 # pylint: disable=bare-except
_LOGGER.error("Error setting up Synology DSM")
# Will be updated when update() gets called.
diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py
index 57e03cf153f..ea8595e3991 100644
--- a/homeassistant/components/sensor/systemmonitor.py
+++ b/homeassistant/components/sensor/systemmonitor.py
@@ -24,27 +24,27 @@ CONF_ARG = 'arg'
SENSOR_TYPES = {
'disk_free': ['Disk free', 'GiB', 'mdi:harddisk'],
- 'disk_use': ['Disk used', 'GiB', 'mdi:harddisk'],
- 'disk_use_percent': ['Disk used', '%', 'mdi:harddisk'],
+ 'disk_use': ['Disk use', 'GiB', 'mdi:harddisk'],
+ 'disk_use_percent': ['Disk use (percent)', '%', 'mdi:harddisk'],
'ipv4_address': ['IPv4 address', '', 'mdi:server-network'],
'ipv6_address': ['IPv6 address', '', 'mdi:server-network'],
'last_boot': ['Last boot', '', 'mdi:clock'],
- 'load_15m': ['Average load (15m)', '', 'mdi:memory'],
- 'load_1m': ['Average load (1m)', '', 'mdi:memory'],
- 'load_5m': ['Average load (5m)', '', 'mdi:memory'],
- 'memory_free': ['RAM available', 'MiB', 'mdi:memory'],
- 'memory_use': ['RAM used', 'MiB', 'mdi:memory'],
- 'memory_use_percent': ['RAM used', '%', 'mdi:memory'],
- 'network_in': ['Received', 'MiB', 'mdi:server-network'],
- 'network_out': ['Sent', 'MiB', 'mdi:server-network'],
- 'packets_in': ['Packets received', ' ', 'mdi:server-network'],
- 'packets_out': ['Packets sent', ' ', 'mdi:server-network'],
+ 'load_15m': ['Load (15m)', '', 'mdi:memory'],
+ 'load_1m': ['Load (1m)', '', 'mdi:memory'],
+ 'load_5m': ['Load (5m)', '', 'mdi:memory'],
+ 'memory_free': ['Memory free', 'MiB', 'mdi:memory'],
+ 'memory_use': ['Memory use', 'MiB', 'mdi:memory'],
+ 'memory_use_percent': ['Memory use (percent)', '%', 'mdi:memory'],
+ 'network_in': ['Network in', 'MiB', 'mdi:server-network'],
+ 'network_out': ['Network out', 'MiB', 'mdi:server-network'],
+ 'packets_in': ['Packets in', ' ', 'mdi:server-network'],
+ 'packets_out': ['Packets out', ' ', 'mdi:server-network'],
'process': ['Process', ' ', 'mdi:memory'],
- 'processor_use': ['CPU used', '%', 'mdi:memory'],
+ 'processor_use': ['Processor use', '%', 'mdi:memory'],
'since_last_boot': ['Since last boot', '', 'mdi:clock'],
'swap_free': ['Swap free', 'GiB', 'mdi:harddisk'],
- 'swap_use': ['Swap used', 'GiB', 'mdi:harddisk'],
- 'swap_use_percent': ['Swap used', '%', 'mdi:harddisk'],
+ 'swap_use': ['Swap use', 'GiB', 'mdi:harddisk'],
+ 'swap_use_percent': ['Swap use (percent)', '%', 'mdi:harddisk'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py
index 8c7259ff800..7acdc1a20bd 100644
--- a/homeassistant/components/sensor/tado.py
+++ b/homeassistant/components/sensor/tado.py
@@ -147,7 +147,7 @@ class TadoSensor(Entity):
data = self._store.get_data(self._data_id)
if data is None:
- _LOGGER.debug("Recieved no data for zone %s", self.zone_name)
+ _LOGGER.debug("Received no data for zone %s", self.zone_name)
return
unit = TEMP_CELSIUS
diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py
index d0b038fd230..39d1cbc75a3 100644
--- a/homeassistant/components/sensor/tahoma.py
+++ b/homeassistant/components/sensor/tahoma.py
@@ -9,7 +9,6 @@ import logging
from datetime import timedelta
from homeassistant.helpers.entity import Entity
-from homeassistant.components.sensor import ENTITY_ID_FORMAT
from homeassistant.components.tahoma import (
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
@@ -36,7 +35,6 @@ class TahomaSensor(TahomaDevice, Entity):
"""Initialize the sensor."""
self.current_value = None
super().__init__(tahoma_device, controller)
- self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id)
@property
def state(self):
diff --git a/homeassistant/components/sensor/ted5000.py b/homeassistant/components/sensor/ted5000.py
index 08681fd37f2..55d520cf6ca 100644
--- a/homeassistant/components/sensor/ted5000.py
+++ b/homeassistant/components/sensor/ted5000.py
@@ -1,5 +1,5 @@
"""
-Support gahtering ted500 information.
+Support gathering ted500 information.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.ted5000/
diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py
index 1d9bf0b7a9a..b347439e08d 100644
--- a/homeassistant/components/sensor/template.py
+++ b/homeassistant/components/sensor/template.py
@@ -31,11 +31,6 @@ SENSOR_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
})
-SENSOR_SCHEMA = vol.All(
- cv.deprecated(ATTR_ENTITY_ID),
- SENSOR_SCHEMA,
-)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}),
})
diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py
index 4534c8d6203..74e74262710 100644
--- a/homeassistant/components/sensor/tesla.py
+++ b/homeassistant/components/sensor/tesla.py
@@ -10,7 +10,8 @@ import logging
from homeassistant.components.sensor import ENTITY_ID_FORMAT
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN
from homeassistant.components.tesla import TeslaDevice
-from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
+from homeassistant.const import (
+ TEMP_CELSIUS, TEMP_FAHRENHEIT, LENGTH_KILOMETERS, LENGTH_MILES)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
@@ -66,18 +67,23 @@ class TeslaSensor(TeslaDevice, Entity):
"""Update the state from the sensor."""
_LOGGER.debug("Updating sensor: %s", self._name)
self.tesla_device.update()
+ units = self.tesla_device.measurement
+
if self.tesla_device.bin_type == 0x4:
if self.type == 'outside':
self.current_value = self.tesla_device.get_outside_temp()
else:
self.current_value = self.tesla_device.get_inside_temp()
-
- tesla_temp_units = self.tesla_device.measurement
-
- if tesla_temp_units == 'F':
+ if units == 'F':
self._unit = TEMP_FAHRENHEIT
else:
self._unit = TEMP_CELSIUS
else:
- self.current_value = self.tesla_device.battery_level()
- self._unit = "%"
+ self.current_value = self.tesla_device.get_value()
+ if self.tesla_device.bin_type == 0x5:
+ self._unit = units
+ elif self.tesla_device.bin_type in (0xA, 0xB):
+ if units == 'LENGTH_MILES':
+ self._unit = LENGTH_MILES
+ else:
+ self._unit = LENGTH_KILOMETERS
diff --git a/homeassistant/components/sensor/travisci.py b/homeassistant/components/sensor/travisci.py
index 5f341760bb6..1ca08e7c0aa 100644
--- a/homeassistant/components/sensor/travisci.py
+++ b/homeassistant/components/sensor/travisci.py
@@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors = []
- # non specificy repository selected, then show all associated
+ # non specific repository selected, then show all associated
if not repositories:
all_repos = travis.repos(member=user.login)
repositories = [repo.slug for repo in all_repos]
diff --git a/homeassistant/components/sensor/uk_transport.py b/homeassistant/components/sensor/uk_transport.py
index 9b35afb418c..72d34411d5c 100644
--- a/homeassistant/components/sensor/uk_transport.py
+++ b/homeassistant/components/sensor/uk_transport.py
@@ -132,7 +132,7 @@ class UkTransportSensor(Entity):
_LOGGER.warning('Invalid response from API')
elif 'error' in response.json():
if 'exceeded' in response.json()['error']:
- self._state = 'Useage limites exceeded'
+ self._state = 'Usage limits exceeded'
if 'invalid' in response.json()['error']:
self._state = 'Credentials invalid'
else:
diff --git a/homeassistant/components/sensor/waqi.py b/homeassistant/components/sensor/waqi.py
index 318a22cfa2a..bf2e263a0bb 100644
--- a/homeassistant/components/sensor/waqi.py
+++ b/homeassistant/components/sensor/waqi.py
@@ -84,7 +84,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
dev.append(waqi_sensor)
except (aiohttp.client_exceptions.ClientConnectorError,
asyncio.TimeoutError):
- _LOGGER.exception('Failed to connct to WAQI servers.')
+ _LOGGER.exception('Failed to connect to WAQI servers.')
raise PlatformNotReady
async_add_devices(dev, True)
diff --git a/homeassistant/components/sensor/waterfurnace.py b/homeassistant/components/sensor/waterfurnace.py
index 7d8c71f8d51..24c45ec1ff3 100644
--- a/homeassistant/components/sensor/waterfurnace.py
+++ b/homeassistant/components/sensor/waterfurnace.py
@@ -19,7 +19,7 @@ from homeassistant.util import slugify
class WFSensorConfig(object):
"""Water Furnace Sensor configuration."""
- def __init__(self, friendly_name, field, icon="mdi:guage",
+ def __init__(self, friendly_name, field, icon="mdi:gauge",
unit_of_measurement=None):
"""Initialize configuration."""
self.friendly_name = friendly_name
diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py
index 8bb449b2ec1..d0d9758c13a 100644
--- a/homeassistant/components/sensor/wunderground.py
+++ b/homeassistant/components/sensor/wunderground.py
@@ -55,7 +55,7 @@ class WUSensorConfig(object):
https://www.wunderground.com/weather/api/d/docs?d=data/index
value (function(WUndergroundData)): callback that
extracts desired value from WUndergroundData object
- unit_of_measurement (string): unit of meassurement
+ unit_of_measurement (string): unit of measurement
entity_picture (string): value or callback returning
URL of entity picture
icon (string): icon name or URL
@@ -84,7 +84,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig):
dictionary.
icon (string): icon name or URL, if None sensor
will use current weather symbol
- unit_of_measurement (string): unit of meassurement
+ unit_of_measurement (string): unit of measurement
"""
super().__init__(
friendly_name,
@@ -230,7 +230,7 @@ class WUAlmanacSensorConfig(WUSensorConfig):
value_type (string): "record" or "normal"
wu_unit (string): unit name in WU API
icon (string): icon name or URL
- unit_of_measurement (string): unit of meassurement
+ unit_of_measurement (string): unit of measurement
"""
super().__init__(
friendly_name=friendly_name,
diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py
index f5c20fa5a1c..c2498d88822 100644
--- a/homeassistant/components/sensor/xiaomi_aqara.py
+++ b/homeassistant/components/sensor/xiaomi_aqara.py
@@ -71,7 +71,7 @@ class XiaomiSensor(XiaomiDevice):
value /= 100
elif self._data_key in ['illumination']:
value = max(value - 300, 0)
- if self._data_key == 'temperature' and (value < -20 or value > 60):
+ if self._data_key == 'temperature' and (value < -50 or value > 60):
return False
elif self._data_key == 'humidity' and (value <= 0 or value > 100):
return False
diff --git a/homeassistant/components/sensor/zamg.py b/homeassistant/components/sensor/zamg.py
index 4b63d769243..df5ff5e8d37 100644
--- a/homeassistant/components/sensor/zamg.py
+++ b/homeassistant/components/sensor/zamg.py
@@ -236,7 +236,7 @@ def closest_station(lat, lon, cache_dir):
stations = zamg_stations(cache_dir)
def comparable_dist(zamg_id):
- """Calculate the psudeo-distance from lat/lon."""
+ """Calculate the pseudo-distance from lat/lon."""
station_lat, station_lon = stations[zamg_id]
return (lat - station_lat) ** 2 + (lon - station_lon) ** 2
diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py
index cd2847c1fa6..a1820f7d7dd 100644
--- a/homeassistant/components/sensor/zha.py
+++ b/homeassistant/components/sensor/zha.py
@@ -31,7 +31,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
@asyncio.coroutine
def make_sensor(discovery_info):
"""Create ZHA sensors factory."""
- from bellows.zigbee.zcl.clusters.measurement import TemperatureMeasurement
+ from zigpy.zcl.clusters.measurement import TemperatureMeasurement
in_clusters = discovery_info['in_clusters']
if TemperatureMeasurement.cluster_id in in_clusters:
sensor = TemperatureSensor(**discovery_info)
diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py
index 8ec023057d1..31259325c04 100644
--- a/homeassistant/components/shopping_list.py
+++ b/homeassistant/components/shopping_list.py
@@ -1,4 +1,4 @@
-"""Component to manage a shoppling list."""
+"""Component to manage a shopping list."""
import asyncio
import json
import logging
diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py
index 5c35e43881e..d085b1279cb 100644
--- a/homeassistant/components/snips.py
+++ b/homeassistant/components/snips.py
@@ -155,7 +155,7 @@ def async_setup(hass, config):
def resolve_slot_values(slot):
- """Convert snips builtin types to useable values."""
+ """Convert snips builtin types to usable values."""
if 'value' in slot['value']:
value = slot['value']['value']
else:
diff --git a/homeassistant/components/spc.py b/homeassistant/components/spc.py
index 141d06768e3..72477a5a65f 100644
--- a/homeassistant/components/spc.py
+++ b/homeassistant/components/spc.py
@@ -151,7 +151,7 @@ def _ws_process_message(message, async_callback, *args):
"Unsuccessful websocket message delivered, ignoring: %s", message)
try:
yield from async_callback(message['data']['sia'], *args)
- except: # pylint: disable=bare-except
+ except: # noqa: E722 # pylint: disable=bare-except
_LOGGER.exception("Exception in callback, ignoring")
diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py
index 58361b2e8b2..8fd70ec7ed8 100644
--- a/homeassistant/components/switch/acer_projector.py
+++ b/homeassistant/components/switch/acer_projector.py
@@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class AcerSwitch(SwitchDevice):
- """Represents an Acer Projector as an switch."""
+ """Represents an Acer Projector as a switch."""
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
"""Init of the Acer projector."""
@@ -103,13 +103,13 @@ class AcerSwitch(SwitchDevice):
# need to wait for timeout
ret = self.ser.read_until(size=20).decode('utf-8')
except serial.SerialException:
- _LOGGER.error('Problem comunicating with %s', self._serial_port)
+ _LOGGER.error('Problem communicating with %s', self._serial_port)
self.ser.close()
return ret
def _write_read_format(self, msg):
- """Write msg, obtain awnser and format output."""
- # awnsers are formatted as ***\rawnser\r***
+ """Write msg, obtain answer and format output."""
+ # answers are formatted as ***\answer\r***
awns = self._write_read(msg)
match = re.search(r'\r(.+)\r', awns)
if match:
diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py
index 5841642cc00..e79b7c3f34c 100644
--- a/homeassistant/components/switch/broadlink.py
+++ b/homeassistant/components/switch/broadlink.py
@@ -100,7 +100,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
packet = yield from hass.async_add_job(
broadlink_device.check_data)
if packet:
- log_msg = "Recieved packet is: {}".\
+ log_msg = "Received packet is: {}".\
format(b64encode(packet).decode('utf8'))
_LOGGER.info(log_msg)
hass.components.persistent_notification.async_create(
@@ -144,7 +144,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hass.services.register(DOMAIN, SERVICE_LEARN + '_' +
ip_addr.replace('.', '_'), _learn_command)
hass.services.register(DOMAIN, SERVICE_SEND + '_' +
- ip_addr.replace('.', '_'), _send_packet)
+ ip_addr.replace('.', '_'), _send_packet,
+ vol.Schema({'packet': cv.ensure_list}))
switches = []
for object_id, device_config in devices.items():
switches.append(
diff --git a/homeassistant/components/switch/digitalloggers.py b/homeassistant/components/switch/digitalloggers.py
index 0625a42f765..f3af70c6222 100644
--- a/homeassistant/components/switch/digitalloggers.py
+++ b/homeassistant/components/switch/digitalloggers.py
@@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class DINRelay(SwitchDevice):
- """Representation of a individual DIN III relay port."""
+ """Representation of an individual DIN III relay port."""
def __init__(self, controller_name, parent_device, outlet):
"""Initialize the DIN III Relay switch."""
diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py
index ff432f2efc8..acc0c3ac423 100644
--- a/homeassistant/components/switch/flux.py
+++ b/homeassistant/components/switch/flux.py
@@ -33,7 +33,7 @@ CONF_START_CT = 'start_colortemp'
CONF_SUNSET_CT = 'sunset_colortemp'
CONF_STOP_CT = 'stop_colortemp'
CONF_BRIGHTNESS = 'brightness'
-CONF_DISABLE_BRIGTNESS_ADJUST = 'disable_brightness_adjust'
+CONF_DISABLE_BRIGHTNESS_ADJUST = 'disable_brightness_adjust'
CONF_INTERVAL = 'interval'
MODE_XY = 'xy'
@@ -48,7 +48,7 @@ PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_LIGHTS): cv.entity_ids,
vol.Optional(CONF_NAME, default="Flux"): cv.string,
vol.Optional(CONF_START_TIME): cv.time,
- vol.Optional(CONF_STOP_TIME, default=datetime.time(22, 0)): cv.time,
+ vol.Optional(CONF_STOP_TIME): cv.time,
vol.Optional(CONF_START_CT, default=4000):
vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)),
vol.Optional(CONF_SUNSET_CT, default=3000):
@@ -57,7 +57,7 @@ PLATFORM_SCHEMA = vol.Schema({
vol.All(vol.Coerce(int), vol.Range(min=1000, max=40000)),
vol.Optional(CONF_BRIGHTNESS):
vol.All(vol.Coerce(int), vol.Range(min=0, max=255)),
- vol.Optional(CONF_DISABLE_BRIGTNESS_ADJUST): cv.boolean,
+ vol.Optional(CONF_DISABLE_BRIGHTNESS_ADJUST): cv.boolean,
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.Any(MODE_XY, MODE_MIRED, MODE_RGB),
vol.Optional(CONF_INTERVAL, default=30): cv.positive_int,
@@ -105,7 +105,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sunset_colortemp = config.get(CONF_SUNSET_CT)
stop_colortemp = config.get(CONF_STOP_CT)
brightness = config.get(CONF_BRIGHTNESS)
- disable_brightness_adjust = config.get(CONF_DISABLE_BRIGTNESS_ADJUST)
+ disable_brightness_adjust = config.get(CONF_DISABLE_BRIGHTNESS_ADJUST)
mode = config.get(CONF_MODE)
interval = config.get(CONF_INTERVAL)
transition = config.get(ATTR_TRANSITION)
@@ -184,9 +184,7 @@ class FluxSwitch(SwitchDevice):
sunset = get_astral_event_date(self.hass, 'sunset', now.date())
start_time = self.find_start_time(now)
- stop_time = now.replace(
- hour=self._stop_time.hour, minute=self._stop_time.minute,
- second=0)
+ stop_time = self.find_stop_time(now)
if stop_time <= start_time:
# stop_time does not happen in the same day as start_time
@@ -210,7 +208,7 @@ class FluxSwitch(SwitchDevice):
else:
temp = self._start_colortemp + temp_offset
else:
- # Nightime
+ # Night time
time_state = 'night'
if now < stop_time:
@@ -270,3 +268,13 @@ class FluxSwitch(SwitchDevice):
else:
sunrise = get_astral_event_date(self.hass, 'sunrise', now.date())
return sunrise
+
+ def find_stop_time(self, now):
+ """Return dusk or stop_time if given."""
+ if self._stop_time:
+ dusk = now.replace(
+ hour=self._stop_time.hour, minute=self._stop_time.minute,
+ second=0)
+ else:
+ dusk = get_astral_event_date(self.hass, 'dusk', now.date())
+ return dusk
diff --git a/homeassistant/components/switch/hdmi_cec.py b/homeassistant/components/switch/hdmi_cec.py
index a100b582e64..65a7a762c0f 100644
--- a/homeassistant/components/switch/hdmi_cec.py
+++ b/homeassistant/components/switch/hdmi_cec.py
@@ -30,7 +30,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class CecSwitchDevice(CecDevice, SwitchDevice):
"""Representation of a HDMI device as a Switch."""
- def __init__(self, hass: HomeAssistant, device, logical):
+ def __init__(self, hass: HomeAssistant, device, logical) -> None:
"""Initialize the HDMI device."""
CecDevice.__init__(self, hass, device, logical)
self.entity_id = "%s.%s_%s" % (
diff --git a/homeassistant/components/switch/hive.py b/homeassistant/components/switch/hive.py
index 97d4320280d..67ebe95ba8e 100644
--- a/homeassistant/components/switch/hive.py
+++ b/homeassistant/components/switch/hive.py
@@ -65,5 +65,5 @@ class HiveDevicePlug(SwitchDevice):
entity.handle_update(self.data_updatesource)
def update(self):
- """Update all Node data frome Hive."""
+ """Update all Node data from Hive."""
self.session.core.update_data(self.node_id)
diff --git a/homeassistant/components/switch/ihc.py b/homeassistant/components/switch/ihc.py
index 4bab1378acd..eab88035c73 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):
+ 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/insteon_local.py b/homeassistant/components/switch/insteon_local.py
index c20a638c00f..4456436ea61 100644
--- a/homeassistant/components/switch/insteon_local.py
+++ b/homeassistant/components/switch/insteon_local.py
@@ -54,7 +54,7 @@ class InsteonLocalSwitchDevice(SwitchDevice):
@property
def unique_id(self):
"""Return the ID of this Insteon node."""
- return 'insteon_local_{}'.format(self.node.device_id)
+ return self.node.device_id
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py
index ed7d0ffc479..0b584e14b8d 100644
--- a/homeassistant/components/switch/insteon_plm.py
+++ b/homeassistant/components/switch/insteon_plm.py
@@ -83,7 +83,7 @@ class InsteonPLMSwitchDevice(SwitchDevice):
@callback
def async_switch_update(self, message):
"""Receive notification from transport that new data exists."""
- _LOGGER.info('Received update calback from PLM for %s', self._address)
+ _LOGGER.info('Received update callback from PLM for %s', self._address)
self._hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
diff --git a/homeassistant/components/switch/knx.py b/homeassistant/components/switch/knx.py
index e0b656aafe9..01c08767ca0 100644
--- a/homeassistant/components/switch/knx.py
+++ b/homeassistant/components/switch/knx.py
@@ -51,7 +51,7 @@ def async_add_devices_discovery(hass, discovery_info, async_add_devices):
@callback
def async_add_devices_config(hass, config, async_add_devices):
- """Set up switch for KNX platform configured within plattform."""
+ """Set up switch for KNX platform configured within platform."""
import xknx
switch = xknx.devices.Switch(
hass.data[DATA_KNX].xknx,
diff --git a/homeassistant/components/switch/neato.py b/homeassistant/components/switch/neato.py
index 62bc5f99d01..a797abb47fc 100644
--- a/homeassistant/components/switch/neato.py
+++ b/homeassistant/components/switch/neato.py
@@ -1,5 +1,5 @@
"""
-Support for Neato Connected Vaccums switches.
+Support for Neato Connected Vacuums switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.neato/
@@ -68,7 +68,7 @@ class NeatoConnectedSwitch(ToggleEntity):
self._schedule_state = STATE_ON
else:
self._schedule_state = STATE_OFF
- _LOGGER.debug("Shedule state: %s", self._schedule_state)
+ _LOGGER.debug("Schedule state: %s", self._schedule_state)
@property
def name(self):
diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py
index 201aee0f58c..1ce599366a1 100644
--- a/homeassistant/components/switch/pilight.py
+++ b/homeassistant/components/switch/pilight.py
@@ -19,9 +19,9 @@ from homeassistant.helpers.restore_state import async_get_last_state
_LOGGER = logging.getLogger(__name__)
CONF_OFF_CODE = 'off_code'
-CONF_OFF_CODE_RECIEVE = 'off_code_receive'
+CONF_OFF_CODE_RECEIVE = 'off_code_receive'
CONF_ON_CODE = 'on_code'
-CONF_ON_CODE_RECIEVE = 'on_code_receive'
+CONF_ON_CODE_RECEIVE = 'on_code_receive'
CONF_SYSTEMCODE = 'systemcode'
CONF_UNIT = 'unit'
CONF_UNITCODE = 'unitcode'
@@ -48,9 +48,9 @@ SWITCHES_SCHEMA = vol.Schema({
vol.Required(CONF_ON_CODE): COMMAND_SCHEMA,
vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA,
vol.Optional(CONF_NAME): cv.string,
- vol.Optional(CONF_OFF_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list,
+ vol.Optional(CONF_OFF_CODE_RECEIVE, default=[]): vol.All(cv.ensure_list,
[COMMAND_SCHEMA]),
- vol.Optional(CONF_ON_CODE_RECIEVE, default=[]): vol.All(cv.ensure_list,
+ vol.Optional(CONF_ON_CODE_RECEIVE, default=[]): vol.All(cv.ensure_list,
[COMMAND_SCHEMA])
})
@@ -72,8 +72,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
properties.get(CONF_NAME, dev_name),
properties.get(CONF_ON_CODE),
properties.get(CONF_OFF_CODE),
- properties.get(CONF_ON_CODE_RECIEVE),
- properties.get(CONF_OFF_CODE_RECIEVE)
+ properties.get(CONF_ON_CODE_RECEIVE),
+ properties.get(CONF_OFF_CODE_RECEIVE)
)
)
diff --git a/homeassistant/components/switch/pulseaudio_loopback.py b/homeassistant/components/switch/pulseaudio_loopback.py
index 03f9e84b3c8..007e74e14fd 100644
--- a/homeassistant/components/switch/pulseaudio_loopback.py
+++ b/homeassistant/components/switch/pulseaudio_loopback.py
@@ -131,7 +131,7 @@ class PAServer():
self._send_command(str.format(UNLOAD_CMD, module_idx), False)
def get_module_idx(self, sink_name, source_name):
- """For a sink/source, return it's module id in our cache, if found."""
+ """For a sink/source, return its module id in our cache, if found."""
result = re.search(str.format(MOD_REGEX, re.escape(sink_name),
re.escape(source_name)),
self._current_module_state)
diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py
index a1ce83b597a..d8d424be361 100644
--- a/homeassistant/components/switch/rachio.py
+++ b/homeassistant/components/switch/rachio.py
@@ -134,7 +134,7 @@ class RachioIro(object):
return self._running
def list_zones(self, include_disabled=False):
- """Return alist of the zones connected to the device, incl. data."""
+ """Return a list of the zones connected to the device, incl. data."""
if not self._zones:
self._zones = [RachioZone(self.rachio, self, zone['id'],
self.manual_run_mins)
diff --git a/homeassistant/components/switch/rainbird.py b/homeassistant/components/switch/rainbird.py
index ee283b3c269..9aa24b9360b 100644
--- a/homeassistant/components/switch/rainbird.py
+++ b/homeassistant/components/switch/rainbird.py
@@ -52,7 +52,7 @@ class RainBirdSwitch(SwitchDevice):
self._devid = dev_id
self._zone = int(dev.get(CONF_ZONE))
self._name = dev.get(CONF_FRIENDLY_NAME,
- "Sprinker {}".format(self._zone))
+ "Sprinkler {}".format(self._zone))
self._state = None
self._duration = dev.get(CONF_TRIGGER_TIME)
self._attributes = {
diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py
index f373a6aad84..a18d6544acc 100644
--- a/homeassistant/components/switch/raincloud.py
+++ b/homeassistant/components/switch/raincloud.py
@@ -35,7 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
- # create an sensor for each zone managed by faucet
+ # create a sensor for each zone managed by faucet
for zone in raincloud.controller.faucet.zones:
sensors.append(
RainCloudSwitch(default_watering_timer,
diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py
index 9425b61f0e5..3147ded96bd 100644
--- a/homeassistant/components/switch/rainmachine.py
+++ b/homeassistant/components/switch/rainmachine.py
@@ -180,8 +180,7 @@ class RainMachineEntity(SwitchDevice):
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
- return '{}.{}.{}'.format(self.__class__, self._device_name,
- self.rainmachine_id)
+ return self.rainmachine_id
@aware_throttle('local')
def _local_update(self) -> None:
diff --git a/homeassistant/components/switch/scsgate.py b/homeassistant/components/switch/scsgate.py
index dfcf1816b7b..8b2734612de 100644
--- a/homeassistant/components/switch/scsgate.py
+++ b/homeassistant/components/switch/scsgate.py
@@ -155,7 +155,7 @@ class SCSGateSwitch(SwitchDevice):
class SCSGateScenarioSwitch(object):
"""Provides a SCSGate scenario switch.
- This switch is always in a 'off" state, when toggled it's used to trigger
+ This switch is always in an 'off" state, when toggled it's used to trigger
events.
"""
diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py
index 64dafdcadef..93ebf98e9ac 100644
--- a/homeassistant/components/switch/template.py
+++ b/homeassistant/components/switch/template.py
@@ -38,11 +38,6 @@ SWITCH_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids
})
-SWITCH_SCHEMA = vol.All(
- cv.deprecated(ATTR_ENTITY_ID),
- SWITCH_SCHEMA,
-)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SWITCHES): vol.Schema({cv.slug: SWITCH_SCHEMA}),
})
diff --git a/homeassistant/components/switch/tplink.py b/homeassistant/components/switch/tplink.py
index f67aaec9796..14faa98fb59 100644
--- a/homeassistant/components/switch/tplink.py
+++ b/homeassistant/components/switch/tplink.py
@@ -50,8 +50,7 @@ class SmartPlugSwitch(SwitchDevice):
"""Initialize the switch."""
self.smartplug = smartplug
self._name = name
- if leds_on is not None:
- self.smartplug.led = leds_on
+ self._leds_on = leds_on
self._state = None
self._available = True
# Set up emeter cache
@@ -94,6 +93,10 @@ class SmartPlugSwitch(SwitchDevice):
self._state = self.smartplug.state == \
self.smartplug.SWITCH_STATE_ON
+ if self._leds_on is not None:
+ self.smartplug.led = self._leds_on
+ self._leds_on = None
+
# Pull the name from the device if a name was not specified
if self._name == DEFAULT_NAME:
self._name = self.smartplug.alias
diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py
index d94ff8c268b..ecaff14e2e2 100644
--- a/homeassistant/components/switch/wake_on_lan.py
+++ b/homeassistant/components/switch/wake_on_lan.py
@@ -15,7 +15,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.script import Script
from homeassistant.const import (CONF_HOST, CONF_NAME)
-REQUIREMENTS = ['wakeonlan==0.2.2']
+REQUIREMENTS = ['wakeonlan==1.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -53,7 +53,7 @@ class WOLSwitch(SwitchDevice):
def __init__(self, hass, name, host, mac_address,
off_action, broadcast_address):
"""Initialize the WOL switch."""
- from wakeonlan import wol
+ import wakeonlan
self._hass = hass
self._name = name
self._host = host
@@ -61,7 +61,7 @@ class WOLSwitch(SwitchDevice):
self._broadcast_address = broadcast_address
self._off_script = Script(hass, off_action) if off_action else None
self._state = False
- self._wol = wol
+ self._wol = wakeonlan
@property
def should_poll(self):
diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py
index 7b97ece337b..4339c92bb60 100644
--- a/homeassistant/components/switch/wemo.py
+++ b/homeassistant/components/switch/wemo.py
@@ -81,7 +81,7 @@ class WemoSwitch(SwitchDevice):
@property
def unique_id(self):
"""Return the ID of this WeMo switch."""
- return "{}.{}".format(self.__class__, self.wemo.serialnumber)
+ return self.wemo.serialnumber
@property
def name(self):
diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py
index f2fdf3177aa..87871079a9c 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.4']
+REQUIREMENTS = ['python-miio==0.3.5']
ATTR_POWER = 'power'
ATTR_TEMPERATURE = 'temperature'
diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py
index 5c8fe3109a6..1dad1f3a1eb 100644
--- a/homeassistant/components/system_log/__init__.py
+++ b/homeassistant/components/system_log/__init__.py
@@ -16,6 +16,7 @@ import voluptuous as vol
from homeassistant import __path__ as HOMEASSISTANT_PATH
from homeassistant.components.http import HomeAssistantView
import homeassistant.helpers.config_validation as cv
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
CONF_MAX_ENTRIES = 'max_entries'
CONF_MESSAGE = 'message'
@@ -27,6 +28,8 @@ DEFAULT_MAX_ENTRIES = 50
DEPENDENCIES = ['http']
DOMAIN = 'system_log'
+EVENT_SYSTEM_LOG = 'system_log_event'
+
SERVICE_CLEAR = 'clear'
SERVICE_WRITE = 'write'
@@ -46,67 +49,6 @@ SERVICE_WRITE_SCHEMA = vol.Schema({
})
-class LogErrorHandler(logging.Handler):
- """Log handler for error messages."""
-
- def __init__(self, maxlen):
- """Initialize a new LogErrorHandler."""
- super().__init__()
- self.records = deque(maxlen=maxlen)
-
- def emit(self, record):
- """Save error and warning logs.
-
- Everything logged with error or warning is saved in local buffer. A
- default upper limit is set to 50 (older entries are discarded) but can
- be changed if needed.
- """
- if record.levelno >= logging.WARN:
- stack = []
- if not record.exc_info:
- try:
- stack = [f for f, _, _, _ in traceback.extract_stack()]
- except ValueError:
- # On Python 3.4 under py.test getting the stack might fail.
- pass
- self.records.appendleft([record, stack])
-
-
-@asyncio.coroutine
-def async_setup(hass, config):
- """Set up the logger component."""
- conf = config.get(DOMAIN)
-
- if conf is None:
- conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
-
- handler = LogErrorHandler(conf.get(CONF_MAX_ENTRIES))
- logging.getLogger().addHandler(handler)
-
- hass.http.register_view(AllErrorsView(handler))
-
- @asyncio.coroutine
- def async_service_handler(service):
- """Handle logger services."""
- if service.service == 'clear':
- handler.records.clear()
- return
- if service.service == 'write':
- logger = logging.getLogger(
- service.data.get(CONF_LOGGER, '{}.external'.format(__name__)))
- level = service.data[CONF_LEVEL]
- getattr(logger, level)(service.data[CONF_MESSAGE])
-
- hass.services.async_register(
- DOMAIN, SERVICE_CLEAR, async_service_handler,
- schema=SERVICE_CLEAR_SCHEMA)
- hass.services.async_register(
- DOMAIN, SERVICE_WRITE, async_service_handler,
- schema=SERVICE_WRITE_SCHEMA)
-
- return True
-
-
def _figure_out_source(record, call_stack, hass):
paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir]
try:
@@ -134,10 +76,11 @@ def _figure_out_source(record, call_stack, hass):
# Iterate through the stack call (in reverse) and find the last call from
# a file in Home Assistant. Try to figure out where error happened.
+ paths_re = r'(?:{})/(.*)'.format('|'.join([re.escape(x) for x in paths]))
for pathname in reversed(stack):
# Try to match with a file within Home Assistant
- match = re.match(r'(?:{})/(.*)'.format('|'.join(paths)), pathname)
+ match = re.match(paths_re, pathname)
if match:
return match.group(1)
# Ok, we don't know what this is
@@ -151,14 +94,86 @@ def _exception_as_string(exc_info):
return buf.getvalue()
-def _convert(record, call_stack, hass):
- return {
- 'timestamp': record.created,
- 'level': record.levelname,
- 'message': record.getMessage(),
- 'exception': _exception_as_string(record.exc_info),
- 'source': _figure_out_source(record, call_stack, hass),
- }
+class LogErrorHandler(logging.Handler):
+ """Log handler for error messages."""
+
+ def __init__(self, hass, maxlen):
+ """Initialize a new LogErrorHandler."""
+ super().__init__()
+ self.hass = hass
+ self.records = deque(maxlen=maxlen)
+
+ def _create_entry(self, record, call_stack):
+ return {
+ 'timestamp': record.created,
+ 'level': record.levelname,
+ 'message': record.getMessage(),
+ 'exception': _exception_as_string(record.exc_info),
+ 'source': _figure_out_source(record, call_stack, self.hass),
+ }
+
+ def emit(self, record):
+ """Save error and warning logs.
+
+ Everything logged with error or warning is saved in local buffer. A
+ default upper limit is set to 50 (older entries are discarded) but can
+ be changed if needed.
+ """
+ if record.levelno >= logging.WARN:
+ stack = []
+ if not record.exc_info:
+ try:
+ stack = [f for f, _, _, _ in traceback.extract_stack()]
+ except ValueError:
+ # On Python 3.4 under py.test getting the stack might fail.
+ pass
+
+ entry = self._create_entry(record, stack)
+ self.records.appendleft(entry)
+ self.hass.bus.fire(EVENT_SYSTEM_LOG, entry)
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Set up the logger component."""
+ conf = config.get(DOMAIN)
+ if conf is None:
+ conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN]
+
+ handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES))
+ logging.getLogger().addHandler(handler)
+
+ hass.http.register_view(AllErrorsView(handler))
+
+ @asyncio.coroutine
+ def async_service_handler(service):
+ """Handle logger services."""
+ if service.service == 'clear':
+ handler.records.clear()
+ return
+ if service.service == 'write':
+ logger = logging.getLogger(
+ service.data.get(CONF_LOGGER, '{}.external'.format(__name__)))
+ level = service.data[CONF_LEVEL]
+ getattr(logger, level)(service.data[CONF_MESSAGE])
+
+ @asyncio.coroutine
+ def async_shutdown_handler(event):
+ """Remove logging handler when Home Assistant is shutdown."""
+ # This is needed as older logger instances will remain
+ logging.getLogger().removeHandler(handler)
+
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
+ async_shutdown_handler)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_CLEAR, async_service_handler,
+ schema=SERVICE_CLEAR_SCHEMA)
+ hass.services.async_register(
+ DOMAIN, SERVICE_WRITE, async_service_handler,
+ schema=SERVICE_WRITE_SCHEMA)
+
+ return True
class AllErrorsView(HomeAssistantView):
@@ -174,5 +189,6 @@ class AllErrorsView(HomeAssistantView):
@asyncio.coroutine
def get(self, request):
"""Get all errors and warnings."""
- return self.json([_convert(x[0], x[1], request.app['hass'])
- for x in self.handler.records])
+ # deque is not serializable (it's just "list-like") so it must be
+ # converted to a list before it can be serialized to json
+ return self.json(list(self.handler.records))
diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado.py
index 1f5125d724e..cfba0a5c0c4 100644
--- a/homeassistant/components/tado.py
+++ b/homeassistant/components/tado.py
@@ -119,8 +119,10 @@ class TadoDataStore:
def reset_zone_overlay(self, zone_id):
"""Wrap for resetZoneOverlay(..)."""
- return self.tado.resetZoneOverlay(zone_id)
+ self.tado.resetZoneOverlay(zone_id)
+ self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg
def set_zone_overlay(self, zone_id, mode, temperature=None, duration=None):
"""Wrap for setZoneOverlay(..)."""
- return self.tado.setZoneOverlay(zone_id, mode, temperature, duration)
+ self.tado.setZoneOverlay(zone_id, mode, temperature, duration)
+ self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg
diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py
index 4eacee08ec1..28a54f40d56 100644
--- a/homeassistant/components/tahoma.py
+++ b/homeassistant/components/tahoma.py
@@ -13,9 +13,8 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE
from homeassistant.helpers import discovery
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
-from homeassistant.util import (slugify)
-REQUIREMENTS = ['tahoma-api==0.0.10']
+REQUIREMENTS = ['tahoma-api==0.0.11']
_LOGGER = logging.getLogger(__name__)
@@ -65,7 +64,7 @@ def setup(hass, config):
api.get_setup()
devices = api.get_devices()
except RequestException:
- _LOGGER.exception("Cannot fetch informations from Tahoma API")
+ _LOGGER.exception("Cannot fetch information from Tahoma API")
return False
hass.data[DOMAIN] = {
@@ -101,15 +100,8 @@ class TahomaDevice(Entity):
"""Initialize the device."""
self.tahoma_device = tahoma_device
self.controller = controller
- self._unique_id = TAHOMA_ID_FORMAT.format(
- slugify(tahoma_device.label), slugify(tahoma_device.url))
self._name = self.tahoma_device.label
- @property
- def unique_id(self):
- """Return the unique ID for this cover."""
- return self._unique_id
-
@property
def name(self):
"""Return the name of the device."""
diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py
index cb314c4a2b4..170e1517a6d 100644
--- a/homeassistant/components/telegram_bot/__init__.py
+++ b/homeassistant/components/telegram_bot/__init__.py
@@ -330,7 +330,7 @@ class TelegramNotificationService:
This can be one of (message_id, inline_message_id) from a msg dict,
returning a tuple.
**You can use 'last' as message_id** to edit
- the last sended message in the chat_id.
+ the message last sent in the chat_id.
"""
message_id = inline_message_id = None
if ATTR_MESSAGEID in msg_data:
@@ -354,7 +354,7 @@ class TelegramNotificationService:
chat_ids = [t for t in target if t in self.allowed_chat_ids]
if chat_ids:
return chat_ids
- _LOGGER.warning("Unallowed targets: %s, using default: %s",
+ _LOGGER.warning("Disallowed targets: %s, using default: %s",
target, self._default_user)
return [self._default_user]
diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml
index dc864c9f61a..4c144fe42db 100644
--- a/homeassistant/components/telegram_bot/services.yaml
+++ b/homeassistant/components/telegram_bot/services.yaml
@@ -25,7 +25,7 @@ send_message:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or ["Text button1:/button1, Text button2:/button2", "Text button3:/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
send_photo:
@@ -56,7 +56,7 @@ send_photo:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
send_video:
@@ -87,7 +87,7 @@ send_video:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
send_document:
@@ -118,7 +118,7 @@ send_document:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
send_location:
@@ -140,7 +140,7 @@ send_location:
description: List of rows of commands, comma-separated, to make a custom keyboard.
example: '["/command1, /command2", "/command3"]'
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
edit_message:
@@ -165,7 +165,7 @@ edit_message:
description: Disables link previews for links in the message.
example: true
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
edit_caption:
@@ -181,7 +181,7 @@ edit_caption:
description: Message body of the notification.
example: The garage door has been open for 10 minutes.
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
edit_replymarkup:
@@ -194,7 +194,7 @@ edit_replymarkup:
description: The chat_id where to edit the reply_markup.
example: 12345
inline_keyboard:
- description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with asociated callback data.
+ description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data.
example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]'
answer_callback_query:
diff --git a/homeassistant/components/tesla.py b/homeassistant/components/tesla.py
index 4f76b70432e..76b5c00d9d4 100644
--- a/homeassistant/components/tesla.py
+++ b/homeassistant/components/tesla.py
@@ -16,7 +16,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
-REQUIREMENTS = ['teslajsonpy==0.0.19']
+REQUIREMENTS = ['teslajsonpy==0.0.23']
DOMAIN = 'tesla'
diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py
index d85b7d189c5..532b4529eca 100644
--- a/homeassistant/components/tts/__init__.py
+++ b/homeassistant/components/tts/__init__.py
@@ -29,7 +29,7 @@ from homeassistant.helpers import config_per_platform
import homeassistant.helpers.config_validation as cv
from homeassistant.setup import async_prepare_setup_platform
-REQUIREMENTS = ['mutagen==1.39']
+REQUIREMENTS = ['mutagen==1.40.0']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py
index 85b223864e9..084a7229212 100644
--- a/homeassistant/components/tts/google.py
+++ b/homeassistant/components/tts/google.py
@@ -87,7 +87,7 @@ class GoogleProvider(Provider):
url_param = {
'ie': 'UTF-8',
'tl': language,
- 'q': yarl.quote(part),
+ 'q': yarl.URL(part).raw_path,
'tk': part_token,
'total': len(message_parts),
'idx': idx,
diff --git a/homeassistant/components/usps.py b/homeassistant/components/usps.py
index 0c4eba54e35..58f858b0975 100644
--- a/homeassistant/components/usps.py
+++ b/homeassistant/components/usps.py
@@ -69,7 +69,7 @@ class USPSData(object):
"""
def __init__(self, session, name):
- """Initialize the data oject."""
+ """Initialize the data object."""
self.session = session
self.name = name
self.packages = []
diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py
index 29099db5cd5..2a4eb2d5e7f 100644
--- a/homeassistant/components/vacuum/neato.py
+++ b/homeassistant/components/vacuum/neato.py
@@ -1,5 +1,5 @@
"""
-Support for Neato Connected Vaccums.
+Support for Neato Connected Vacuums.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/vacuum.neato/
diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py
index 06690a0909f..d64f7a754ee 100644
--- a/homeassistant/components/vacuum/xiaomi_miio.py
+++ b/homeassistant/components/vacuum/xiaomi_miio.py
@@ -19,12 +19,12 @@ 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.4']
+REQUIREMENTS = ['python-miio==0.3.5']
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Xiaomi Vacuum cleaner'
-ICON = 'mdi:google-circles-group'
+ICON = 'mdi:roomba'
PLATFORM = 'xiaomi_miio'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py
index dcd4ed518d0..3e36d0a3028 100644
--- a/homeassistant/components/volvooncall.py
+++ b/homeassistant/components/volvooncall.py
@@ -93,7 +93,7 @@ def setup(hass, config):
hass, component, DOMAIN, (vehicle.vin, attr), config)
def update_vehicle(vehicle):
- """Revieve updated information on vehicle."""
+ """Receive updated information on vehicle."""
state.vehicles[vehicle.vin] = vehicle
if vehicle.vin not in state.entities:
discover_vehicle(vehicle)
diff --git a/homeassistant/components/wake_on_lan.py b/homeassistant/components/wake_on_lan.py
index 7da0f3054f3..4e729c7ccc7 100644
--- a/homeassistant/components/wake_on_lan.py
+++ b/homeassistant/components/wake_on_lan.py
@@ -13,7 +13,7 @@ import voluptuous as vol
from homeassistant.const import CONF_MAC
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['wakeonlan==0.2.2']
+REQUIREMENTS = ['wakeonlan==1.0.0']
DOMAIN = "wake_on_lan"
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +31,7 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema({
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the wake on LAN component."""
- from wakeonlan import wol
+ import wakeonlan
@asyncio.coroutine
def send_magic_packet(call):
@@ -42,11 +42,11 @@ def async_setup(hass, config):
mac_address, broadcast_address)
if broadcast_address is not None:
yield from hass.async_add_job(
- partial(wol.send_magic_packet, mac_address,
+ partial(wakeonlan.send_magic_packet, mac_address,
ip_address=broadcast_address))
else:
yield from hass.async_add_job(
- partial(wol.send_magic_packet, mac_address))
+ partial(wakeonlan.send_magic_packet, mac_address))
hass.services.async_register(
DOMAIN, SERVICE_SEND_MAGIC_PACKET, send_magic_packet,
diff --git a/homeassistant/components/waterfurnace.py b/homeassistant/components/waterfurnace.py
index 346bdcdfb97..a587285e0ba 100644
--- a/homeassistant/components/waterfurnace.py
+++ b/homeassistant/components/waterfurnace.py
@@ -18,7 +18,7 @@ from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
-REQUIREMENTS = ["waterfurnace==0.3.0"]
+REQUIREMENTS = ["waterfurnace==0.4.0"]
_LOGGER = logging.getLogger(__name__)
@@ -83,6 +83,8 @@ class WaterFurnaceData(threading.Thread):
def run(self):
"""Thread run loop."""
+ import waterfurnace.waterfurnace as wf
+
@callback
def register():
"""Connect to hass for shutdown."""
@@ -110,8 +112,11 @@ class WaterFurnaceData(threading.Thread):
try:
self.data = self.client.read()
- except ConnectionError:
- # attempt to log back in if there was a session expiration.
+ except wf.WFException:
+ # WFExceptions are things the WF library understands
+ # that pretty much can all be solved by logging in and
+ # back out again.
+ _LOGGER.exception("Failed to read data, attempting to recover")
try:
self.client.login()
except Exception: # pylint: disable=broad-except
@@ -127,10 +132,6 @@ class WaterFurnaceData(threading.Thread):
"Lost our connection to websocket, trying again")
time.sleep(SCAN_INTERVAL.seconds)
- except Exception: # pylint: disable=broad-except
- _LOGGER.exception("Error updating waterfurnace data.")
- time.sleep(SCAN_INTERVAL.seconds)
-
else:
self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC)
time.sleep(SCAN_INTERVAL.seconds)
diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py
index 0566cc03662..21f67ce080a 100644
--- a/homeassistant/components/weather/darksky.py
+++ b/homeassistant/components/weather/darksky.py
@@ -1,5 +1,5 @@
"""
-Patform for retrieving meteorological data from Dark Sky.
+Platform for retrieving meteorological data from Dark Sky.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/weather.darksky/
diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py
index 1ff5eeaa535..c8a1bdf8f68 100644
--- a/homeassistant/components/weather/openweathermap.py
+++ b/homeassistant/components/weather/openweathermap.py
@@ -21,12 +21,14 @@ REQUIREMENTS = ['pyowm==2.8.0']
_LOGGER = logging.getLogger(__name__)
+ATTR_FORECAST_CONDITION = 'condition'
ATTRIBUTION = 'Data provided by OpenWeatherMap'
DEFAULT_NAME = 'OpenWeatherMap'
MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
+MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS = 3
CONDITION_CLASSES = {
'cloudy': [804],
@@ -137,10 +139,18 @@ class OpenWeatherMapWeather(WeatherEntity):
@property
def forecast(self):
"""Return the forecast array."""
- return [{
- ATTR_FORECAST_TIME: entry.get_reference_time('iso'),
- ATTR_FORECAST_TEMP: entry.get_temperature('celsius').get('temp')}
- for entry in self.forecast_data.get_weathers()]
+ data = []
+ for entry in self.forecast_data.get_weathers():
+ data.append({
+ ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000,
+ ATTR_FORECAST_TEMP:
+ entry.get_temperature('celsius').get('temp')
+ })
+ if (len(data) - 1) % MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS == 0:
+ data[len(data) - 1][ATTR_FORECAST_CONDITION] = \
+ [k for k, v in CONDITION_CLASSES.items()
+ if entry.get_weather_code() in v][0]
+ return data
def update(self):
"""Get the latest data from OWM and updates the states."""
@@ -180,7 +190,7 @@ class WeatherData(object):
@Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES)
def update_forecast(self):
- """Get the lastest forecast from OpenWeatherMap."""
+ """Get the latest forecast from OpenWeatherMap."""
from pyowm.exceptions.api_call_error import APICallError
try:
diff --git a/homeassistant/components/weblink.py b/homeassistant/components/weblink.py
index f55fe1f0bb5..a20b0fc9b0c 100644
--- a/homeassistant/components/weblink.py
+++ b/homeassistant/components/weblink.py
@@ -16,12 +16,15 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_ENTITIES = 'entities'
+CONF_RELATIVE_URL_ERROR_MSG = "Invalid relative URL. Absolute path required."
+CONF_RELATIVE_URL_REGEX = r'\A/'
DOMAIN = 'weblink'
ENTITIES_SCHEMA = vol.Schema({
- # pylint: disable=no-value-for-parameter
- vol.Required(CONF_URL): vol.Url(),
+ vol.Required(CONF_URL): vol.Any(
+ vol.Match(CONF_RELATIVE_URL_REGEX, msg=CONF_RELATIVE_URL_ERROR_MSG),
+ cv.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 a4bfc46bf83..030d1bee579 100644
--- a/homeassistant/components/websocket_api.py
+++ b/homeassistant/components/websocket_api.py
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/developers/websocket_api/
"""
import asyncio
+from concurrent import futures
from contextlib import suppress
from functools import partial
import json
@@ -120,6 +121,11 @@ BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({
TYPE_PING)
}, extra=vol.ALLOW_EXTRA)
+# Define the possible errors that occur when connections are cancelled.
+# Originally, this was just asyncio.CancelledError, but issue #9546 showed
+# that futures.CancelledErrors can also occur in some situations.
+CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError)
+
def auth_ok_message():
"""Return an auth_ok message."""
@@ -231,7 +237,7 @@ class ActiveConnection:
def _writer(self):
"""Write outgoing messages."""
# Exceptions if Socket disconnected or cancelled by connection handler
- with suppress(RuntimeError, asyncio.CancelledError):
+ with suppress(RuntimeError, *CANCELLATION_ERRORS):
while not self.wsock.closed:
message = yield from self.to_write.get()
if message is None:
@@ -363,7 +369,7 @@ class ActiveConnection:
self.log_error(msg)
self._writer_task.cancel()
- except asyncio.CancelledError:
+ except CANCELLATION_ERRORS:
self.debug("Connection cancelled by server")
except asyncio.QueueFull:
diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py
index f87537a1938..58d44b31994 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 enum
import logging
import voluptuous as vol
@@ -14,13 +15,26 @@ from homeassistant import const as ha_const
from homeassistant.helpers import discovery, entity
from homeassistant.util import slugify
-REQUIREMENTS = ['bellows==0.4.0']
+REQUIREMENTS = [
+ 'bellows==0.5.0',
+ 'zigpy==0.0.1',
+ 'zigpy-xbee==0.0.1',
+]
DOMAIN = 'zha'
+
+class RadioType(enum.Enum):
+ """Possible options for radio type in config."""
+
+ ezsp = 'ezsp'
+ xbee = 'xbee'
+
+
CONF_BAUDRATE = 'baudrate'
CONF_DATABASE = 'database_path'
CONF_DEVICE_CONFIG = 'device_config'
+CONF_RADIO_TYPE = 'radio_type'
CONF_USB_PATH = 'usb_path'
DATA_DEVICE_CONFIG = 'zha_device_config'
@@ -30,6 +44,8 @@ DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
+ vol.Optional(CONF_RADIO_TYPE, default=RadioType.ezsp):
+ cv.enum(RadioType),
CONF_USB_PATH: cv.string,
vol.Optional(CONF_BAUDRATE, default=57600): cv.positive_int,
CONF_DATABASE: cv.string,
@@ -67,16 +83,22 @@ def async_setup(hass, config):
"""
global APPLICATION_CONTROLLER
- import bellows.ezsp
- from bellows.zigbee.application import ControllerApplication
-
- ezsp_ = bellows.ezsp.EZSP()
usb_path = config[DOMAIN].get(CONF_USB_PATH)
baudrate = config[DOMAIN].get(CONF_BAUDRATE)
- yield from ezsp_.connect(usb_path, baudrate)
+ radio_type = config[DOMAIN].get(CONF_RADIO_TYPE)
+ if radio_type == RadioType.ezsp:
+ import bellows.ezsp
+ from bellows.zigbee.application import ControllerApplication
+ radio = bellows.ezsp.EZSP()
+ elif radio_type == RadioType.xbee:
+ import zigpy_xbee.api
+ from zigpy_xbee.zigbee.application import ControllerApplication
+ radio = zigpy_xbee.api.XBee()
+
+ yield from radio.connect(usb_path, baudrate)
database = config[DOMAIN].get(CONF_DATABASE)
- APPLICATION_CONTROLLER = ControllerApplication(ezsp_, database)
+ APPLICATION_CONTROLLER = ControllerApplication(radio, database)
listener = ApplicationListener(hass, config)
APPLICATION_CONTROLLER.add_listener(listener)
yield from APPLICATION_CONTROLLER.startup(auto_form=True)
@@ -130,7 +152,7 @@ class ApplicationListener:
@asyncio.coroutine
def async_device_initialized(self, device, join):
"""Handle device joined and basic information discovered (async)."""
- import bellows.zigbee.profiles
+ import zigpy.profiles
import homeassistant.components.zha.const as zha_const
zha_const.populate_data()
@@ -146,8 +168,8 @@ class ApplicationListener:
node_config = self._config[DOMAIN][CONF_DEVICE_CONFIG].get(
device_key, {})
- if endpoint.profile_id in bellows.zigbee.profiles.PROFILES:
- profile = bellows.zigbee.profiles.PROFILES[endpoint.profile_id]
+ if endpoint.profile_id in zigpy.profiles.PROFILES:
+ profile = zigpy.profiles.PROFILES[endpoint.profile_id]
if zha_const.DEVICE_CLASS.get(endpoint.profile_id,
{}).get(endpoint.device_type,
None):
@@ -253,7 +275,7 @@ class Entity(entity.Entity):
"""Handle an attribute updated on this cluster."""
pass
- def zdo_command(self, aps_frame, tsn, command_id, args):
+ def zdo_command(self, tsn, command_id, args):
"""Handle a ZDO command received on this cluster."""
pass
diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py
index b1659536e32..a8d4671ebf7 100644
--- a/homeassistant/components/zha/const.py
+++ b/homeassistant/components/zha/const.py
@@ -11,8 +11,8 @@ def populate_data():
These cannot be module level, as importing bellows must be done in a
in a function.
"""
- from bellows.zigbee import zcl
- from bellows.zigbee.profiles import PROFILES, zha, zll
+ from zigpy import zcl
+ from zigpy.profiles import PROFILES, zha, zll
DEVICE_CLASS[zha.PROFILE_ID] = {
zha.DeviceType.ON_OFF_SWITCH: 'switch',
diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py
index 9b80581b85e..0149bb9287a 100644
--- a/homeassistant/components/zwave/__init__.py
+++ b/homeassistant/components/zwave/__init__.py
@@ -33,7 +33,7 @@ from . import workaround
from .discovery_schemas import DISCOVERY_SCHEMAS
from .util import check_node_schema, check_value_schema, node_name
-REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.0.35']
+REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3']
_LOGGER = logging.getLogger(__name__)
@@ -170,7 +170,7 @@ def _obj_to_dict(obj):
"""Convert an object into a hash for debug."""
return {key: getattr(obj, key) for key
in dir(obj)
- if key[0] != '_' and not hasattr(getattr(obj, key), '__call__')}
+ if key[0] != '_' and not callable(getattr(obj, key))}
def _value_name(value):
@@ -219,7 +219,7 @@ def get_config_value(node, value_index, tries=5):
and value.index == value_index):
return value.data
except RuntimeError:
- # If we get an runtime error the dict has changed while
+ # If we get a runtime error the dict has changed while
# we was looking for a value, just do it again
return None if tries <= 0 else get_config_value(
node, value_index, tries=tries - 1)
@@ -865,8 +865,8 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
self.values.primary.set_change_verified(False)
self._name = _value_name(self.values.primary)
- self._unique_id = "ZWAVE-{}-{}".format(self.node.node_id,
- self.values.primary.object_id)
+ self._unique_id = "{}-{}".format(self.node.node_id,
+ self.values.primary.object_id)
self._update_attributes()
dispatcher.connect(
diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py
index 2815be45df2..e2524aefadf 100644
--- a/homeassistant/components/zwave/const.py
+++ b/homeassistant/components/zwave/const.py
@@ -182,8 +182,8 @@ SPECIFIC_TYPE_NOT_USED = 0 # Available in all Generic types
GENERIC_TYPE_AV_CONTROL_POINT = 3
SPECIFIC_TYPE_DOORBELL = 18
-SPECIFIC_TYPE_SATELLITE_RECIEVER = 4
-SPECIFIC_TYPE_SATELLITE_RECIEVER_V2 = 17
+SPECIFIC_TYPE_SATELLITE_RECEIVER = 4
+SPECIFIC_TYPE_SATELLITE_RECEIVER_V2 = 17
GENERIC_TYPE_DISPLAY = 4
SPECIFIC_TYPE_SIMPLE_DISPLAY = 1
diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml
index ba8e177c9f7..61855143d59 100644
--- a/homeassistant/components/zwave/services.yaml
+++ b/homeassistant/components/zwave/services.yaml
@@ -4,7 +4,7 @@ change_association:
description: Change an association in the Z-Wave network.
fields:
association:
- description: Specify add or remove assosication
+ description: Specify add or remove association
example: add
node_id:
description: Node id of the node to set association for.
@@ -30,14 +30,14 @@ heal_network:
description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW.log for progress.
fields:
return_routes:
- description: Wheter or not to update the return routes from the nodes to the controller. Defaults to False.
+ description: Whether or not to update the return routes from the nodes to the controller. Defaults to False.
example: True
heal_node:
description: Start a Z-Wave node heal. Refer to OZW.log for progress.
fields:
return_routes:
- description: Wheter or not to update the return routes from the node to the controller. Defaults to False.
+ description: Whether or not to update the return routes from the node to the controller. Defaults to False.
example: True
remove_node:
diff --git a/homeassistant/config.py b/homeassistant/config.py
index 3f4c4c174d7..5e82ef1baa0 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -95,6 +95,10 @@ 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:
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 6470f50d460..1c923a35936 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,13 +1,12 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 62
-PATCH_VERSION = '1'
+MINOR_VERSION = 63
+PATCH_VERSION = '0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
REQUIRED_PYTHON_VER = (3, 4, 2)
REQUIRED_PYTHON_VER_WIN = (3, 5, 2)
-CONSTRAINT_FILE = 'package_constraints.txt'
# Format for platforms
PLATFORM_FORMAT = '{}.{}'
diff --git a/homeassistant/core.py b/homeassistant/core.py
index 18cf40d3854..b1cf9c51efd 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -66,7 +66,7 @@ def valid_entity_id(entity_id: str) -> bool:
def valid_state(state: str) -> bool:
- """Test if an state is valid."""
+ """Test if a state is valid."""
return len(state) < 256
@@ -777,7 +777,7 @@ class ServiceCall(object):
self.call_id = call_id
def __repr__(self):
- """Return the represenation of the service."""
+ """Return the representation of the service."""
if self.data:
return "".format(
self.domain, self.service, util.repr_helper(self.data))
diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py
index ee4176a8937..73a09464439 100644
--- a/homeassistant/helpers/deprecation.py
+++ b/homeassistant/helpers/deprecation.py
@@ -8,7 +8,7 @@ def deprecated_substitute(substitute_name):
When a property is added to replace an older property, this decorator can
be added to the new property, listing the old property as the substitute.
- If the old property is defined, it's value will be used instead, and a log
+ If the old property is defined, its value will be used instead, and a log
warning will be issued alerting the user of the impending change.
"""
def decorator(func):
diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py
index e3816fdaa6f..c7653d5d5b9 100644
--- a/homeassistant/helpers/entity.py
+++ b/homeassistant/helpers/entity.py
@@ -77,7 +77,7 @@ class Entity(object):
# Protect for multiple updates
_update_staged = False
- # Process updates pararell
+ # Process updates in parallel
parallel_updates = None
@property
@@ -91,7 +91,7 @@ class Entity(object):
@property
def unique_id(self) -> str:
"""Return an unique ID."""
- return "{}.{}".format(self.__class__, id(self))
+ return None
@property
def name(self) -> Optional[str]:
@@ -152,7 +152,7 @@ class Entity(object):
@property
def assumed_state(self) -> bool:
"""Return True if unable to access real state of the entity."""
- return None
+ return False
@property
def force_update(self) -> bool:
@@ -221,21 +221,41 @@ class Entity(object):
if device_attr is not None:
attr.update(device_attr)
- self._attr_setter('unit_of_measurement', str, ATTR_UNIT_OF_MEASUREMENT,
- attr)
+ unit_of_measurement = self.unit_of_measurement
+ if unit_of_measurement is not None:
+ attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement
- self._attr_setter('name', str, ATTR_FRIENDLY_NAME, attr)
- self._attr_setter('icon', str, ATTR_ICON, attr)
- self._attr_setter('entity_picture', str, ATTR_ENTITY_PICTURE, attr)
- self._attr_setter('hidden', bool, ATTR_HIDDEN, attr)
- self._attr_setter('assumed_state', bool, ATTR_ASSUMED_STATE, attr)
- self._attr_setter('supported_features', int, ATTR_SUPPORTED_FEATURES,
- attr)
- self._attr_setter('device_class', str, ATTR_DEVICE_CLASS, attr)
+ name = self.name
+ if name is not None:
+ attr[ATTR_FRIENDLY_NAME] = name
+
+ icon = self.icon
+ if icon is not None:
+ attr[ATTR_ICON] = icon
+
+ entity_picture = self.entity_picture
+ if entity_picture is not None:
+ attr[ATTR_ENTITY_PICTURE] = entity_picture
+
+ hidden = self.hidden
+ if hidden:
+ attr[ATTR_HIDDEN] = hidden
+
+ assumed_state = self.assumed_state
+ if assumed_state:
+ attr[ATTR_ASSUMED_STATE] = assumed_state
+
+ supported_features = self.supported_features
+ if supported_features is not None:
+ attr[ATTR_SUPPORTED_FEATURES] = supported_features
+
+ device_class = self.device_class
+ if device_class is not None:
+ attr[ATTR_DEVICE_CLASS] = str(device_class)
end = timer()
- if not self._slow_reported and end - start > 0.4:
+ if end - start > 0.4 and not self._slow_reported:
self._slow_reported = True
_LOGGER.warning("Updating state for %s (%s) took %.3f seconds. "
"Please report platform to the developers at "
@@ -246,10 +266,6 @@ class Entity(object):
if DATA_CUSTOMIZE in self.hass.data:
attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id))
- # Remove hidden property if false so it won't show up.
- if not attr.get(ATTR_HIDDEN, True):
- attr.pop(ATTR_HIDDEN)
-
# Convert temperature if we detect one
try:
unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT)
@@ -268,7 +284,7 @@ class Entity(object):
self.entity_id, state, attr, self.force_update)
def schedule_update_ha_state(self, force_refresh=False):
- """Schedule a update ha state change task.
+ """Schedule an update ha state change task.
That avoid executor dead looks.
"""
@@ -276,7 +292,7 @@ class Entity(object):
@callback
def async_schedule_update_ha_state(self, force_refresh=False):
- """Schedule a update ha state change task."""
+ """Schedule an update ha state change task."""
self.hass.async_add_job(self.async_update_ha_state(force_refresh))
@asyncio.coroutine
@@ -321,25 +337,24 @@ class Entity(object):
else:
self.hass.states.async_remove(self.entity_id)
- def _attr_setter(self, name, typ, attr, attrs):
- """Populate attributes based on properties."""
- if attr in attrs:
- return
-
- value = getattr(self, name)
-
- if value is None:
- return
-
- try:
- attrs[attr] = typ(value)
- except (TypeError, ValueError):
- pass
-
def __eq__(self, other):
"""Return the comparison."""
- return (isinstance(other, Entity) and
- other.unique_id == self.unique_id)
+ if not isinstance(other, self.__class__):
+ return False
+
+ # Can only decide equality if both have a unique id
+ if self.unique_id is None or other.unique_id is None:
+ return False
+
+ # Ensure they belong to the same platform
+ if self.platform is not None or other.platform is not None:
+ if self.platform is None or other.platform is None:
+ return False
+
+ if self.platform.platform != other.platform.platform:
+ return False
+
+ return self.unique_id == other.unique_id
def __repr__(self):
"""Return the representation."""
diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py
index 4a791d12e52..9dfbe580c16 100644
--- a/homeassistant/helpers/entity_component.py
+++ b/homeassistant/helpers/entity_component.py
@@ -6,24 +6,15 @@ from itertools import chain
from homeassistant import config as conf_util
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.const import (
- ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE,
- DEVICE_DEFAULT_NAME)
-from homeassistant.core import callback, valid_entity_id
-from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
+ ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE)
+from homeassistant.core import callback
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_per_platform, discovery
-from homeassistant.helpers.entity import async_generate_entity_id
-from homeassistant.helpers.event import (
- async_track_time_interval, async_track_point_in_time)
from homeassistant.helpers.service import extract_entity_ids
from homeassistant.util import slugify
-from homeassistant.util.async import (
- run_callback_threadsafe, run_coroutine_threadsafe)
-import homeassistant.util.dt as dt_util
+from .entity_platform import EntityPlatform
DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
-SLOW_SETUP_WARNING = 10
-SLOW_SETUP_MAX_WAIT = 60
-PLATFORM_NOT_READY_RETRIES = 10
class EntityComponent(object):
@@ -42,16 +33,23 @@ class EntityComponent(object):
"""Initialize an entity component."""
self.logger = logger
self.hass = hass
-
self.domain = domain
- self.entity_id_format = domain + '.{}'
self.scan_interval = scan_interval
self.group_name = group_name
self.config = None
self._platforms = {
- 'core': EntityPlatform(self, domain, self.scan_interval, 0, None),
+ 'core': EntityPlatform(
+ hass=hass,
+ logger=logger,
+ domain=domain,
+ platform_name='core',
+ scan_interval=self.scan_interval,
+ parallel_updates=0,
+ entity_namespace=None,
+ async_entities_added_callback=self._async_update_group,
+ )
}
self.async_add_entities = self._platforms['core'].async_add_entities
self.add_entities = self._platforms['core'].add_entities
@@ -106,17 +104,6 @@ class EntityComponent(object):
discovery.async_listen_platform(
self.hass, self.domain, component_platform_discovered)
- def extract_from_service(self, service, expand_group=True):
- """Extract all known entities from a service call.
-
- Will return all entities if no entities specified in call.
- Will return an empty list if entities specified but unknown.
- """
- return run_callback_threadsafe(
- self.hass.loop, self.async_extract_from_service, service,
- expand_group
- ).result()
-
@callback
def async_extract_from_service(self, service, expand_group=True):
"""Extract all known and available entities from a service call.
@@ -135,11 +122,8 @@ class EntityComponent(object):
@asyncio.coroutine
def _async_setup_platform(self, platform_type, platform_config,
- discovery_info=None, tries=0):
- """Set up a platform for this component.
-
- This method must be run in the event loop.
- """
+ discovery_info=None):
+ """Set up a platform for this component."""
platform = yield from async_prepare_setup_platform(
self.hass, self.config, self.domain, platform_type)
@@ -160,59 +144,23 @@ class EntityComponent(object):
if key not in self._platforms:
entity_platform = self._platforms[key] = EntityPlatform(
- self, platform_type, scan_interval, parallel_updates,
- entity_namespace)
+ hass=self.hass,
+ logger=self.logger,
+ domain=self.domain,
+ platform_name=platform_type,
+ scan_interval=scan_interval,
+ parallel_updates=parallel_updates,
+ entity_namespace=entity_namespace,
+ async_entities_added_callback=self._async_update_group,
+ )
else:
entity_platform = self._platforms[key]
- self.logger.info("Setting up %s.%s", self.domain, platform_type)
- warn_task = self.hass.loop.call_later(
- SLOW_SETUP_WARNING, self.logger.warning,
- "Setup of platform %s is taking over %s seconds.", platform_type,
- SLOW_SETUP_WARNING)
-
- try:
- if getattr(platform, 'async_setup_platform', None):
- task = platform.async_setup_platform(
- self.hass, platform_config,
- entity_platform.async_schedule_add_entities, discovery_info
- )
- else:
- # This should not be replaced with hass.async_add_job because
- # we don't want to track this task in case it blocks startup.
- task = self.hass.loop.run_in_executor(
- None, platform.setup_platform, self.hass, platform_config,
- entity_platform.schedule_add_entities, discovery_info
- )
- yield from asyncio.wait_for(
- asyncio.shield(task, loop=self.hass.loop),
- SLOW_SETUP_MAX_WAIT, loop=self.hass.loop)
- yield from entity_platform.async_block_entities_done()
- self.hass.config.components.add(
- '{}.{}'.format(self.domain, platform_type))
- except PlatformNotReady:
- tries += 1
- wait_time = min(tries, 6) * 30
- self.logger.warning(
- 'Platform %s not ready yet. Retrying in %d seconds.',
- platform_type, wait_time)
- async_track_point_in_time(
- self.hass, self._async_setup_platform(
- platform_type, platform_config, discovery_info, tries),
- dt_util.utcnow() + timedelta(seconds=wait_time))
- except asyncio.TimeoutError:
- self.logger.error(
- "Setup of platform %s is taking longer than %s seconds."
- " Startup will proceed without waiting any longer.",
- platform_type, SLOW_SETUP_MAX_WAIT)
- except Exception: # pylint: disable=broad-except
- self.logger.exception(
- "Error while setting up platform %s", platform_type)
- finally:
- warn_task.cancel()
+ yield from entity_platform.async_setup(
+ platform, platform_config, discovery_info)
@callback
- def async_update_group(self):
+ def _async_update_group(self):
"""Set up and/or update component group.
This method must be run in the event loop.
@@ -229,12 +177,8 @@ class EntityComponent(object):
visible=False, entity_ids=ids
)
- def reset(self):
- """Remove entities and reset the entity component to initial values."""
- run_coroutine_threadsafe(self.async_reset(), self.hass.loop).result()
-
@asyncio.coroutine
- def async_reset(self):
+ def _async_reset(self):
"""Remove entities and reset the entity component to initial values.
This method must be run in the event loop.
@@ -260,11 +204,6 @@ class EntityComponent(object):
if entity_id in platform.entities:
yield from platform.async_remove_entity(entity_id)
- def prepare_reload(self):
- """Prepare reloading this entity component."""
- return run_coroutine_threadsafe(
- self.async_prepare_reload(), loop=self.hass.loop).result()
-
@asyncio.coroutine
def async_prepare_reload(self):
"""Prepare reloading this entity component.
@@ -284,212 +223,5 @@ class EntityComponent(object):
if conf is None:
return None
- yield from self.async_reset()
+ yield from self._async_reset()
return conf
-
-
-class EntityPlatform(object):
- """Manage the entities for a single platform."""
-
- def __init__(self, component, platform, scan_interval, parallel_updates,
- entity_namespace):
- """Initialize the entity platform."""
- self.component = component
- self.platform = platform
- self.scan_interval = scan_interval
- self.parallel_updates = None
- self.entity_namespace = entity_namespace
- self.entities = {}
- self._tasks = []
- self._async_unsub_polling = None
- self._process_updates = asyncio.Lock(loop=component.hass.loop)
-
- if parallel_updates:
- self.parallel_updates = asyncio.Semaphore(
- parallel_updates, loop=component.hass.loop)
-
- @asyncio.coroutine
- def async_block_entities_done(self):
- """Wait until all entities add to hass."""
- if self._tasks:
- pending = [task for task in self._tasks if not task.done()]
- self._tasks.clear()
-
- if pending:
- yield from asyncio.wait(pending, loop=self.component.hass.loop)
-
- def schedule_add_entities(self, new_entities, update_before_add=False):
- """Add entities for a single platform."""
- run_callback_threadsafe(
- self.component.hass.loop,
- self.async_schedule_add_entities, list(new_entities),
- update_before_add
- ).result()
-
- @callback
- def async_schedule_add_entities(self, new_entities,
- update_before_add=False):
- """Add entities for a single platform async."""
- self._tasks.append(self.component.hass.async_add_job(
- self.async_add_entities(
- new_entities, update_before_add=update_before_add)
- ))
-
- def add_entities(self, new_entities, update_before_add=False):
- """Add entities for a single platform."""
- # That avoid deadlocks
- if update_before_add:
- self.component.logger.warning(
- "Call 'add_entities' with update_before_add=True "
- "only inside tests or you can run into a deadlock!")
-
- run_coroutine_threadsafe(
- self.async_add_entities(list(new_entities), update_before_add),
- self.component.hass.loop).result()
-
- @asyncio.coroutine
- def async_add_entities(self, new_entities, update_before_add=False):
- """Add entities for a single platform async.
-
- This method must be run in the event loop.
- """
- # handle empty list from component/platform
- if not new_entities:
- return
-
- component_entities = set(entity.entity_id for entity
- in self.component.entities)
-
- tasks = [
- self._async_add_entity(entity, update_before_add,
- component_entities)
- for entity in new_entities]
-
- yield from asyncio.wait(tasks, loop=self.component.hass.loop)
- self.component.async_update_group()
-
- if self._async_unsub_polling is not None or \
- not any(entity.should_poll for entity
- in self.entities.values()):
- return
-
- self._async_unsub_polling = async_track_time_interval(
- self.component.hass, self._update_entity_states, self.scan_interval
- )
-
- @asyncio.coroutine
- def _async_add_entity(self, entity, update_before_add, component_entities):
- """Helper method to add an entity to the platform."""
- if entity is None:
- raise ValueError('Entity cannot be None')
-
- # Do nothing if entity has already been added based on unique id.
- if entity in self.component.entities:
- return
-
- entity.hass = self.component.hass
- entity.platform = self
- entity.parallel_updates = self.parallel_updates
-
- # Update properties before we generate the entity_id
- if update_before_add:
- try:
- yield from entity.async_device_update(warning=False)
- except Exception: # pylint: disable=broad-except
- self.component.logger.exception(
- "%s: Error on device update!", self.platform)
- return
-
- # Write entity_id to entity
- if getattr(entity, 'entity_id', None) is None:
- object_id = entity.name or DEVICE_DEFAULT_NAME
-
- if self.entity_namespace is not None:
- object_id = '{} {}'.format(self.entity_namespace,
- object_id)
-
- entity.entity_id = async_generate_entity_id(
- self.component.entity_id_format, object_id,
- component_entities)
-
- # Make sure it is valid in case an entity set the value themselves
- if not valid_entity_id(entity.entity_id):
- raise HomeAssistantError(
- 'Invalid entity id: {}'.format(entity.entity_id))
- elif entity.entity_id in component_entities:
- raise HomeAssistantError(
- 'Entity id already exists: {}'.format(entity.entity_id))
-
- self.entities[entity.entity_id] = entity
- component_entities.add(entity.entity_id)
-
- if hasattr(entity, 'async_added_to_hass'):
- yield from entity.async_added_to_hass()
-
- yield from entity.async_update_ha_state()
-
- @asyncio.coroutine
- def async_reset(self):
- """Remove all entities and reset data.
-
- This method must be run in the event loop.
- """
- if not self.entities:
- return
-
- tasks = [self._async_remove_entity(entity_id)
- for entity_id in self.entities]
-
- yield from asyncio.wait(tasks, loop=self.component.hass.loop)
-
- if self._async_unsub_polling is not None:
- self._async_unsub_polling()
- self._async_unsub_polling = None
-
- @asyncio.coroutine
- def async_remove_entity(self, entity_id):
- """Remove entity id from platform."""
- yield from self._async_remove_entity(entity_id)
-
- # Clean up polling job if no longer needed
- if (self._async_unsub_polling is not None and
- not any(entity.should_poll for entity
- in self.entities.values())):
- self._async_unsub_polling()
- self._async_unsub_polling = None
-
- @asyncio.coroutine
- def _async_remove_entity(self, entity_id):
- """Remove entity id from platform."""
- entity = self.entities.pop(entity_id)
-
- if hasattr(entity, 'async_will_remove_from_hass'):
- yield from entity.async_will_remove_from_hass()
-
- self.component.hass.states.async_remove(entity_id)
-
- @asyncio.coroutine
- def _update_entity_states(self, now):
- """Update the states of all the polling entities.
-
- To protect from flooding the executor, we will update async entities
- in parallel and other entities sequential.
-
- This method must be run in the event loop.
- """
- if self._process_updates.locked():
- self.component.logger.warning(
- "Updating %s %s took longer than the scheduled update "
- "interval %s", self.platform, self.component.domain,
- self.scan_interval)
- return
-
- with (yield from self._process_updates):
- tasks = []
- for entity in self.entities.values():
- if not entity.should_poll:
- continue
- tasks.append(entity.async_update_ha_state(True))
-
- if tasks:
- yield from asyncio.wait(tasks, loop=self.component.hass.loop)
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
new file mode 100644
index 00000000000..3362f1e3b3f
--- /dev/null
+++ b/homeassistant/helpers/entity_platform.py
@@ -0,0 +1,317 @@
+"""Class to manage the entities for a single platform."""
+import asyncio
+from datetime import timedelta
+
+from homeassistant.const import DEVICE_DEFAULT_NAME
+from homeassistant.core import callback, valid_entity_id, split_entity_id
+from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
+from homeassistant.util.async import (
+ run_callback_threadsafe, run_coroutine_threadsafe)
+import homeassistant.util.dt as dt_util
+
+from .event import async_track_time_interval, async_track_point_in_time
+from .entity_registry import EntityRegistry
+
+SLOW_SETUP_WARNING = 10
+SLOW_SETUP_MAX_WAIT = 60
+PLATFORM_NOT_READY_RETRIES = 10
+DATA_REGISTRY = 'entity_registry'
+
+
+class EntityPlatform(object):
+ """Manage the entities for a single platform."""
+
+ def __init__(self, *, hass, logger, domain, platform_name, scan_interval,
+ parallel_updates, entity_namespace,
+ async_entities_added_callback):
+ """Initialize the entity platform.
+
+ hass: HomeAssistant
+ logger: Logger
+ domain: str
+ platform_name: str
+ scan_interval: timedelta
+ parallel_updates: int
+ entity_namespace: str
+ async_entities_added_callback: @callback method
+ """
+ self.hass = hass
+ self.logger = logger
+ self.domain = domain
+ self.platform_name = platform_name
+ self.scan_interval = scan_interval
+ self.parallel_updates = None
+ self.entity_namespace = entity_namespace
+ self.async_entities_added_callback = async_entities_added_callback
+ self.entities = {}
+ self._tasks = []
+ self._async_unsub_polling = None
+ self._process_updates = asyncio.Lock(loop=hass.loop)
+
+ if parallel_updates:
+ self.parallel_updates = asyncio.Semaphore(
+ parallel_updates, loop=hass.loop)
+
+ @asyncio.coroutine
+ def async_setup(self, platform, platform_config, discovery_info=None,
+ tries=0):
+ """Setup the platform."""
+ logger = self.logger
+ hass = self.hass
+ full_name = '{}.{}'.format(self.domain, self.platform_name)
+
+ logger.info("Setting up %s", full_name)
+ warn_task = hass.loop.call_later(
+ SLOW_SETUP_WARNING, logger.warning,
+ "Setup of platform %s is taking over %s seconds.",
+ self.platform_name, SLOW_SETUP_WARNING)
+
+ try:
+ if getattr(platform, 'async_setup_platform', None):
+ task = platform.async_setup_platform(
+ hass, platform_config,
+ self._async_schedule_add_entities, discovery_info
+ )
+ else:
+ # This should not be replaced with hass.async_add_job because
+ # we don't want to track this task in case it blocks startup.
+ task = hass.loop.run_in_executor(
+ None, platform.setup_platform, hass, platform_config,
+ self._schedule_add_entities, discovery_info
+ )
+ yield from asyncio.wait_for(
+ asyncio.shield(task, loop=hass.loop),
+ SLOW_SETUP_MAX_WAIT, loop=hass.loop)
+
+ # Block till all entities are done
+ if self._tasks:
+ pending = [task for task in self._tasks if not task.done()]
+ self._tasks.clear()
+
+ if pending:
+ yield from asyncio.wait(
+ pending, loop=self.hass.loop)
+
+ hass.config.components.add(full_name)
+ except PlatformNotReady:
+ tries += 1
+ wait_time = min(tries, 6) * 30
+ logger.warning(
+ 'Platform %s not ready yet. Retrying in %d seconds.',
+ self.platform_name, wait_time)
+ async_track_point_in_time(
+ hass, self.async_setup(
+ platform, platform_config, discovery_info, tries),
+ dt_util.utcnow() + timedelta(seconds=wait_time))
+ except asyncio.TimeoutError:
+ logger.error(
+ "Setup of platform %s is taking longer than %s seconds."
+ " Startup will proceed without waiting any longer.",
+ self.platform_name, SLOW_SETUP_MAX_WAIT)
+ except Exception: # pylint: disable=broad-except
+ logger.exception(
+ "Error while setting up platform %s", self.platform_name)
+ finally:
+ warn_task.cancel()
+
+ def _schedule_add_entities(self, new_entities, update_before_add=False):
+ """Synchronously schedule adding entities for a single platform."""
+ run_callback_threadsafe(
+ self.hass.loop,
+ self._async_schedule_add_entities, list(new_entities),
+ update_before_add
+ ).result()
+
+ @callback
+ def _async_schedule_add_entities(self, new_entities,
+ update_before_add=False):
+ """Schedule adding entities for a single platform async."""
+ self._tasks.append(self.hass.async_add_job(
+ self.async_add_entities(
+ new_entities, update_before_add=update_before_add)
+ ))
+
+ def add_entities(self, new_entities, update_before_add=False):
+ """Add entities for a single platform."""
+ # That avoid deadlocks
+ if update_before_add:
+ self.logger.warning(
+ "Call 'add_entities' with update_before_add=True "
+ "only inside tests or you can run into a deadlock!")
+
+ run_coroutine_threadsafe(
+ self.async_add_entities(list(new_entities), update_before_add),
+ self.hass.loop).result()
+
+ @asyncio.coroutine
+ def async_add_entities(self, new_entities, update_before_add=False):
+ """Add entities for a single platform async.
+
+ This method must be run in the event loop.
+ """
+ # handle empty list from component/platform
+ if not new_entities:
+ return
+
+ hass = self.hass
+ component_entities = set(hass.states.async_entity_ids(self.domain))
+
+ registry = hass.data.get(DATA_REGISTRY)
+
+ if registry is None:
+ registry = hass.data[DATA_REGISTRY] = EntityRegistry(hass)
+
+ yield from registry.async_ensure_loaded()
+
+ tasks = [
+ self._async_add_entity(entity, update_before_add,
+ component_entities, registry)
+ for entity in new_entities]
+
+ yield from asyncio.wait(tasks, loop=self.hass.loop)
+ self.async_entities_added_callback()
+
+ if self._async_unsub_polling is not None or \
+ not any(entity.should_poll for entity
+ in self.entities.values()):
+ return
+
+ self._async_unsub_polling = async_track_time_interval(
+ self.hass, self._update_entity_states, self.scan_interval
+ )
+
+ @asyncio.coroutine
+ def _async_add_entity(self, entity, update_before_add, component_entities,
+ registry):
+ """Helper method to add an entity to the platform."""
+ if entity is None:
+ raise ValueError('Entity cannot be None')
+
+ entity.hass = self.hass
+ entity.platform = self
+ entity.parallel_updates = self.parallel_updates
+
+ # Update properties before we generate the entity_id
+ if update_before_add:
+ try:
+ yield from entity.async_device_update(warning=False)
+ except Exception: # pylint: disable=broad-except
+ self.logger.exception(
+ "%s: Error on device update!", self.platform_name)
+ return
+
+ suggested_object_id = None
+
+ # Get entity_id from unique ID registration
+ if entity.unique_id is not None:
+ if entity.entity_id is not None:
+ suggested_object_id = split_entity_id(entity.entity_id)[1]
+ else:
+ suggested_object_id = entity.name
+
+ entry = registry.async_get_or_create(
+ self.domain, self.platform_name, entity.unique_id,
+ suggested_object_id=suggested_object_id)
+ entity.entity_id = entry.entity_id
+
+ # We won't generate an entity ID if the platform has already set one
+ # We will however make sure that platform cannot pick a registered ID
+ elif (entity.entity_id is not None and
+ registry.async_is_registered(entity.entity_id)):
+ # If entity already registered, convert entity id to suggestion
+ suggested_object_id = split_entity_id(entity.entity_id)[1]
+ entity.entity_id = None
+
+ # Generate entity ID
+ if entity.entity_id is None:
+ suggested_object_id = \
+ suggested_object_id or entity.name or DEVICE_DEFAULT_NAME
+
+ if self.entity_namespace is not None:
+ suggested_object_id = '{} {}'.format(self.entity_namespace,
+ suggested_object_id)
+
+ entity.entity_id = registry.async_generate_entity_id(
+ self.domain, suggested_object_id)
+
+ # Make sure it is valid in case an entity set the value themselves
+ if not valid_entity_id(entity.entity_id):
+ raise HomeAssistantError(
+ 'Invalid entity id: {}'.format(entity.entity_id))
+ elif entity.entity_id in component_entities:
+ raise HomeAssistantError(
+ 'Entity id already exists: {}'.format(entity.entity_id))
+
+ self.entities[entity.entity_id] = entity
+ component_entities.add(entity.entity_id)
+
+ if hasattr(entity, 'async_added_to_hass'):
+ yield from entity.async_added_to_hass()
+
+ yield from entity.async_update_ha_state()
+
+ @asyncio.coroutine
+ def async_reset(self):
+ """Remove all entities and reset data.
+
+ This method must be run in the event loop.
+ """
+ if not self.entities:
+ return
+
+ tasks = [self._async_remove_entity(entity_id)
+ for entity_id in self.entities]
+
+ yield from asyncio.wait(tasks, loop=self.hass.loop)
+
+ if self._async_unsub_polling is not None:
+ self._async_unsub_polling()
+ self._async_unsub_polling = None
+
+ @asyncio.coroutine
+ def async_remove_entity(self, entity_id):
+ """Remove entity id from platform."""
+ yield from self._async_remove_entity(entity_id)
+
+ # Clean up polling job if no longer needed
+ if (self._async_unsub_polling is not None and
+ not any(entity.should_poll for entity
+ in self.entities.values())):
+ self._async_unsub_polling()
+ self._async_unsub_polling = None
+
+ @asyncio.coroutine
+ def _async_remove_entity(self, entity_id):
+ """Remove entity id from platform."""
+ entity = self.entities.pop(entity_id)
+
+ if hasattr(entity, 'async_will_remove_from_hass'):
+ yield from entity.async_will_remove_from_hass()
+
+ self.hass.states.async_remove(entity_id)
+
+ @asyncio.coroutine
+ def _update_entity_states(self, now):
+ """Update the states of all the polling entities.
+
+ To protect from flooding the executor, we will update async entities
+ in parallel and other entities sequential.
+
+ This method must be run in the event loop.
+ """
+ if self._process_updates.locked():
+ self.logger.warning(
+ "Updating %s %s took longer than the scheduled update "
+ "interval %s", self.platform_name, self.domain,
+ self.scan_interval)
+ return
+
+ with (yield from self._process_updates):
+ tasks = []
+ for entity in self.entities.values():
+ if not entity.should_poll:
+ continue
+ tasks.append(entity.async_update_ha_state(True))
+
+ if tasks:
+ yield from asyncio.wait(tasks, loop=self.hass.loop)
diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py
new file mode 100644
index 00000000000..350c8273232
--- /dev/null
+++ b/homeassistant/helpers/entity_registry.py
@@ -0,0 +1,134 @@
+"""Provide a registry to track entity IDs.
+
+The Entity Registry keeps a registry of entities. Entities are uniquely
+identified by their domain, platform and a unique id provided by that platform.
+
+The Entity Registry will persist itself 10 seconds after a new entity is
+registered. Registering a new entity while a timer is in progress resets the
+timer.
+
+After initializing, call EntityRegistry.async_ensure_loaded to load the data
+from disk.
+"""
+import asyncio
+from collections import namedtuple, OrderedDict
+from itertools import chain
+import logging
+import os
+
+from ..core import callback, split_entity_id
+from ..util import ensure_unique_string, slugify
+from ..util.yaml import load_yaml, save_yaml
+
+PATH_REGISTRY = 'entity_registry.yaml'
+SAVE_DELAY = 10
+Entry = namedtuple('EntityRegistryEntry',
+ 'entity_id,unique_id,platform,domain')
+_LOGGER = logging.getLogger(__name__)
+
+
+class EntityRegistry:
+ """Class to hold a registry of entities."""
+
+ def __init__(self, hass):
+ """Initialize the registry."""
+ self.hass = hass
+ self.entities = None
+ self._load_task = None
+ self._sched_save = None
+
+ @callback
+ def async_is_registered(self, entity_id):
+ """Check if an entity_id is currently registered."""
+ return entity_id in self.entities
+
+ @callback
+ def async_generate_entity_id(self, domain, suggested_object_id):
+ """Generate an entity ID that does not conflict.
+
+ Conflicts checked against registered and currently existing entities.
+ """
+ return ensure_unique_string(
+ '{}.{}'.format(domain, slugify(suggested_object_id)),
+ chain(self.entities.keys(),
+ self.hass.states.async_entity_ids(domain))
+ )
+
+ @callback
+ def async_get_or_create(self, domain, platform, unique_id, *,
+ suggested_object_id=None):
+ """Get entity. Create if it doesn't exist."""
+ for entity in self.entities.values():
+ if entity.domain == domain and entity.platform == platform and \
+ entity.unique_id == unique_id:
+ return entity
+
+ entity_id = self.async_generate_entity_id(
+ domain, suggested_object_id or '{}_{}'.format(platform, unique_id))
+ entity = Entry(
+ entity_id=entity_id,
+ unique_id=unique_id,
+ platform=platform,
+ domain=domain,
+ )
+ self.entities[entity_id] = entity
+ _LOGGER.info('Registered new %s.%s entity: %s',
+ domain, platform, entity_id)
+ self.async_schedule_save()
+ return entity
+
+ @asyncio.coroutine
+ def async_ensure_loaded(self):
+ """Load the registry from disk."""
+ if self.entities is not None:
+ return
+
+ if self._load_task is None:
+ self._load_task = self.hass.async_add_job(self._async_load)
+
+ yield from self._load_task
+
+ @asyncio.coroutine
+ def _async_load(self):
+ """Load the entity registry."""
+ path = self.hass.config.path(PATH_REGISTRY)
+ entities = OrderedDict()
+
+ if os.path.isfile(path):
+ data = yield from self.hass.async_add_job(load_yaml, path)
+
+ for entity_id, info in data.items():
+ entities[entity_id] = Entry(
+ domain=split_entity_id(entity_id)[0],
+ entity_id=entity_id,
+ unique_id=info['unique_id'],
+ platform=info['platform']
+ )
+
+ self.entities = entities
+ self._load_task = None
+
+ @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 = OrderedDict()
+
+ for entry in self.entities.values():
+ data[entry.entity_id] = {
+ 'unique_id': entry.unique_id,
+ 'platform': entry.platform,
+ }
+
+ yield from self.hass.async_add_job(
+ save_yaml, self.hass.config.path(PATH_REGISTRY), data)
diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py
index 6cd1916d4c2..eab2d583f45 100644
--- a/homeassistant/helpers/event.py
+++ b/homeassistant/helpers/event.py
@@ -1,4 +1,5 @@
"""Helpers for listening to events."""
+from datetime import timedelta
import functools as ft
from homeassistant.loader import bind_hass
@@ -119,7 +120,7 @@ track_template = threaded_listener_factory(async_track_template)
@bind_hass
def async_track_same_state(hass, period, action, async_check_same_func,
entity_ids=MATCH_ALL):
- """Track the state of entities for a period and run a action.
+ """Track the state of entities for a period and run an action.
If async_check_func is None it use the state of orig_value.
Without entity_ids we track all state changes.
@@ -219,6 +220,14 @@ track_point_in_utc_time = threaded_listener_factory(
async_track_point_in_utc_time)
+@callback
+@bind_hass
+def async_call_later(hass, delay, action):
+ """Add a listener that is called in ."""
+ return async_track_point_in_utc_time(
+ hass, action, dt_util.utcnow() + timedelta(seconds=delay))
+
+
@callback
@bind_hass
def async_track_time_interval(hass, action, interval):
@@ -231,7 +240,7 @@ def async_track_time_interval(hass, action, interval):
@callback
def interval_listener(now):
- """Handle elaspsed intervals."""
+ """Handle elapsed intervals."""
nonlocal remove
remove = async_track_point_in_utc_time(
hass, interval_listener, next_interval())
diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py
index b8b6b29df81..1ef9aa15674 100644
--- a/homeassistant/helpers/script.py
+++ b/homeassistant/helpers/script.py
@@ -220,7 +220,7 @@ class Script():
def async_script_timeout(now):
"""Call after timeout is retrieve stop script."""
self._async_listener.remove(unsub)
- self._log("Timout reach, abort script.")
+ self._log("Timeout reached, abort script.")
self.async_stop()
unsub = async_track_point_in_utc_time(
diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py
index f5b626c8828..b89b1689c9e 100644
--- a/homeassistant/helpers/service.py
+++ b/homeassistant/helpers/service.py
@@ -74,6 +74,7 @@ def async_call_from_config(hass, config, blocking=False, variables=None,
config[CONF_SERVICE_DATA_TEMPLATE], variables))
except TemplateError as ex:
_LOGGER.error('Error rendering data template: %s', ex)
+ return
if CONF_SERVICE_ENTITY_ID in config:
service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID]
diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py
index 254a48c3d0a..255f760ebff 100644
--- a/homeassistant/helpers/state.py
+++ b/homeassistant/helpers/state.py
@@ -31,7 +31,7 @@ from homeassistant.components.cover import (
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_OPTION, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY,
SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER,
- SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
+ SERVICE_LOCK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP,
SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK,
SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_OPEN_COVER,
SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, STATE_ALARM_ARMED_AWAY,
@@ -78,6 +78,7 @@ SERVICE_TO_STATE = {
SERVICE_TURN_OFF: STATE_OFF,
SERVICE_MEDIA_PLAY: STATE_PLAYING,
SERVICE_MEDIA_PAUSE: STATE_PAUSED,
+ SERVICE_MEDIA_STOP: STATE_IDLE,
SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY,
SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME,
SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED,
diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py
index 7eb0a602139..b381e1c2b0e 100644
--- a/homeassistant/helpers/template.py
+++ b/homeassistant/helpers/template.py
@@ -438,7 +438,7 @@ def multiply(value, amount):
def logarithm(value, base=math.e):
- """Filter to get logarithm of the value with a spesific base."""
+ """Filter to get logarithm of the value with a specific base."""
try:
return math.log(float(value), float(base))
except (ValueError, TypeError):
diff --git a/homeassistant/loader.py b/homeassistant/loader.py
index ac20f94d243..a3ce2a13f56 100644
--- a/homeassistant/loader.py
+++ b/homeassistant/loader.py
@@ -19,7 +19,7 @@ import sys
from types import ModuleType
# pylint: disable=unused-import
-from typing import Dict, Optional, Sequence, Set # NOQA
+from typing import Dict, List, Optional, Sequence, Set # NOQA
from homeassistant.const import PLATFORM_FORMAT
from homeassistant.util import OrderedSet
@@ -148,7 +148,7 @@ def get_component(comp_name) -> Optional[ModuleType]:
# a namespace. We do not care about namespaces.
# This prevents that when only
# custom_components/switch/some_platform.py exists,
- # the import custom_components.switch would succeeed.
+ # the import custom_components.switch would succeed.
if module.__spec__.origin == 'namespace':
continue
diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py
index 819d8de48e0..5aa051f2bb5 100644
--- a/homeassistant/monkey_patch.py
+++ b/homeassistant/monkey_patch.py
@@ -37,7 +37,7 @@ def patch_weakref_tasks():
asyncio.tasks.Task._all_tasks = IgnoreCalls()
try:
del asyncio.tasks.Task.__del__
- except:
+ except: # noqa: E722
pass
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 243c6d418df..ee3a37bbd53 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -2,14 +2,14 @@ requests==2.18.4
pyyaml>=3.11,<4
pytz>=2017.02
pip>=8.0.3
-jinja2>=2.9.6
+jinja2>=2.10
voluptuous==0.10.5
typing>=3,<4
-aiohttp==2.3.7
-yarl==0.18.0
+aiohttp==2.3.10
+yarl==1.1.0
async_timeout==2.0.0
chardet==3.0.4
-astral==1.4
+astral==1.5
certifi>=2017.4.17
# Breaks Python 3.6 and is not needed for our supported Pythons
diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py
new file mode 100644
index 00000000000..aaf83870147
--- /dev/null
+++ b/homeassistant/requirements.py
@@ -0,0 +1,45 @@
+"""Module to handle installing requirements."""
+import asyncio
+from functools import partial
+import logging
+import os
+
+import homeassistant.util.package as pkg_util
+
+DATA_PIP_LOCK = 'pip_lock'
+CONSTRAINT_FILE = 'package_constraints.txt'
+_LOGGER = logging.getLogger(__name__)
+
+
+@asyncio.coroutine
+def async_process_requirements(hass, name, requirements):
+ """Install the requirements for a component or platform.
+
+ This method is a coroutine.
+ """
+ pip_lock = hass.data.get(DATA_PIP_LOCK)
+ if pip_lock is None:
+ pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop)
+
+ pip_install = partial(pkg_util.install_package,
+ **pip_kwargs(hass.config.config_dir))
+
+ with (yield from pip_lock):
+ for req in requirements:
+ ret = yield from hass.async_add_job(pip_install, req)
+ if not ret:
+ _LOGGER.error("Not initializing %s because could not install "
+ "requirement %s", name, req)
+ return False
+
+ return True
+
+
+def pip_kwargs(config_dir):
+ """Return keyword arguments for PIP install."""
+ kwargs = {
+ 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE)
+ }
+ if not pkg_util.running_under_virtualenv():
+ kwargs['target'] = os.path.join(config_dir, 'deps')
+ return kwargs
diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py
index be39540682c..815a5c8e55f 100644
--- a/homeassistant/scripts/__init__.py
+++ b/homeassistant/scripts/__init__.py
@@ -9,9 +9,8 @@ from typing import List
from homeassistant.bootstrap import mount_local_lib_path
from homeassistant.config import get_default_config_dir
-from homeassistant.const import CONSTRAINT_FILE
-from homeassistant.util.package import (
- install_package, running_under_virtualenv)
+from homeassistant import requirements
+from homeassistant.util.package import install_package
def run(args: List) -> int:
@@ -39,17 +38,14 @@ def run(args: List) -> int:
script = importlib.import_module('homeassistant.scripts.' + args[0])
config_dir = extract_config_dir()
- deps_dir = mount_local_lib_path(config_dir)
+ mount_local_lib_path(config_dir)
+ pip_kwargs = requirements.pip_kwargs(config_dir)
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
+
for req in getattr(script, 'REQUIREMENTS', []):
- if running_under_virtualenv():
- returncode = install_package(req, constraints=os.path.join(
- os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE))
- else:
- returncode = install_package(
- req, target=deps_dir, constraints=os.path.join(
- os.path.dirname(__file__), os.pardir, CONSTRAINT_FILE))
+ returncode = install_package(req, **pip_kwargs)
+
if not returncode:
print('Aborting script, could not install dependency', req)
return 1
diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py
index 27987a70ce6..5cfcf628ec5 100644
--- a/homeassistant/scripts/check_config.py
+++ b/homeassistant/scripts/check_config.py
@@ -13,7 +13,7 @@ from homeassistant import bootstrap, loader, setup, config as config_util
import homeassistant.util.yaml as yaml
from homeassistant.exceptions import HomeAssistantError
-REQUIREMENTS = ('colorlog==3.0.1',)
+REQUIREMENTS = ('colorlog==3.1.2',)
if system() == 'Windows': # Ensure colorama installed for colorlog on Windows
REQUIREMENTS += ('colorama<=1',)
@@ -134,7 +134,7 @@ def run(script_args: List) -> int:
for sfn, sdict in res['secret_cache'].items():
sss = []
- for skey, sval in sdict.items():
+ for skey in sdict:
if skey in flatsecret:
_LOGGER.error('Duplicated secrets in files %s and %s',
flatsecret[skey], sfn)
diff --git a/homeassistant/scripts/influxdb_import.py b/homeassistant/scripts/influxdb_import.py
index c21ac4adad9..e91aeb8a0d7 100644
--- a/homeassistant/scripts/influxdb_import.py
+++ b/homeassistant/scripts/influxdb_import.py
@@ -1,7 +1,8 @@
-"""Script to import recorded data into influxdb."""
+"""Script to import recorded data into an Influx database."""
import argparse
import json
import os
+import sys
from typing import List
@@ -11,11 +12,13 @@ import homeassistant.config as config_util
def run(script_args: List) -> int:
"""Run the actual script."""
from sqlalchemy import create_engine
+ from sqlalchemy import func
from sqlalchemy.orm import sessionmaker
from influxdb import InfluxDBClient
from homeassistant.components.recorder import models
from homeassistant.helpers import state as state_helper
from homeassistant.core import State
+ from homeassistant.core import HomeAssistantError
parser = argparse.ArgumentParser(
description="import data to influxDB.")
@@ -99,8 +102,8 @@ def run(script_args: List) -> int:
client = None
if not simulate:
- client = InfluxDBClient(args.host, args.port,
- args.username, args.password)
+ client = InfluxDBClient(
+ args.host, args.port, args.username, args.password)
client.switch_database(args.dbname)
config_dir = os.path.join(os.getcwd(), args.config) # type: str
@@ -116,105 +119,162 @@ def run(script_args: List) -> int:
if not os.path.exists(src_db) and not args.uri:
print("Fatal Error: Database '{}' does not exist "
- "and no uri given".format(src_db))
+ "and no URI given".format(src_db))
return 1
- uri = args.uri or "sqlite:///{}".format(src_db)
+ uri = args.uri or 'sqlite:///{}'.format(src_db)
engine = create_engine(uri, echo=False)
session_factory = sessionmaker(bind=engine)
session = session_factory()
step = int(args.step)
+ step_start = 0
tags = {}
if args.tags:
- tags.update(dict(elem.split(":") for elem in args.tags.split(",")))
- excl_entities = args.exclude_entities.split(",")
- excl_domains = args.exclude_domains.split(",")
+ tags.update(dict(elem.split(':') for elem in args.tags.split(',')))
+ excl_entities = args.exclude_entities.split(',')
+ excl_domains = args.exclude_domains.split(',')
override_measurement = args.override_measurement
default_measurement = args.default_measurement
- query = session.query(models.Events).filter(
- models.Events.event_type == "state_changed").order_by(
- models.Events.time_fired)
+ query = session.query(func.count(models.Events.event_type)).filter(
+ models.Events.event_type == 'state_changed')
+
+ total_events = query.scalar()
+ prefix_format = '{} of {}'
points = []
+ invalid_points = []
count = 0
from collections import defaultdict
entities = defaultdict(int)
+ print_progress(0, total_events, prefix_format.format(0, total_events))
- for event in query:
- event_data = json.loads(event.event_data)
- state = State.from_dict(event_data.get("new_state"))
+ while True:
- if not state or (
- excl_entities and state.entity_id in excl_entities) or (
- excl_domains and state.domain in excl_domains):
- session.expunge(event)
- continue
+ step_stop = step_start + step
+ if step_start > total_events:
+ print_progress(total_events, total_events, prefix_format.format(
+ total_events, total_events))
+ break
+ query = session.query(models.Events).filter(
+ models.Events.event_type == 'state_changed').order_by(
+ models.Events.time_fired).slice(step_start, step_stop)
- try:
- _state = float(state_helper.state_as_number(state))
- _state_key = "value"
- except ValueError:
- _state = state.state
- _state_key = "state"
+ for event in query:
+ event_data = json.loads(event.event_data)
- if override_measurement:
- measurement = override_measurement
- else:
- measurement = state.attributes.get('unit_of_measurement')
- if measurement in (None, ''):
- if default_measurement:
- measurement = default_measurement
- else:
- measurement = state.entity_id
+ if not ('entity_id' in event_data) or (
+ excl_entities and event_data[
+ 'entity_id'] in excl_entities) or (
+ excl_domains and event_data[
+ 'entity_id'].split('.')[0] in excl_domains):
+ session.expunge(event)
+ continue
- point = {
- 'measurement': measurement,
- 'tags': {
- 'domain': state.domain,
- 'entity_id': state.object_id,
- },
- 'time': event.time_fired,
- 'fields': {
- _state_key: _state,
+ try:
+ state = State.from_dict(event_data.get('new_state'))
+ except HomeAssistantError:
+ invalid_points.append(event_data)
+
+ if not state:
+ invalid_points.append(event_data)
+ continue
+
+ try:
+ _state = float(state_helper.state_as_number(state))
+ _state_key = 'value'
+ except ValueError:
+ _state = state.state
+ _state_key = 'state'
+
+ if override_measurement:
+ measurement = override_measurement
+ else:
+ measurement = state.attributes.get('unit_of_measurement')
+ if measurement in (None, ''):
+ if default_measurement:
+ measurement = default_measurement
+ else:
+ measurement = state.entity_id
+
+ point = {
+ 'measurement': measurement,
+ 'tags': {
+ 'domain': state.domain,
+ 'entity_id': state.object_id,
+ },
+ 'time': event.time_fired,
+ 'fields': {
+ _state_key: _state,
+ }
}
- }
- for key, value in state.attributes.items():
- if key != 'unit_of_measurement':
- # If the key is already in fields
- if key in point['fields']:
- key = key + "_"
- # Prevent column data errors in influxDB.
- # For each value we try to cast it as float
- # But if we can not do it we store the value
- # as string add "_str" postfix to the field key
- try:
- point['fields'][key] = float(value)
- except (ValueError, TypeError):
- new_key = "{}_str".format(key)
- point['fields'][new_key] = str(value)
+ for key, value in state.attributes.items():
+ if key != 'unit_of_measurement':
+ # If the key is already in fields
+ if key in point['fields']:
+ key = key + '_'
+ # Prevent column data errors in influxDB.
+ # For each value we try to cast it as float
+ # But if we can not do it we store the value
+ # as string add "_str" postfix to the field key
+ try:
+ point['fields'][key] = float(value)
+ except (ValueError, TypeError):
+ new_key = '{}_str'.format(key)
+ point['fields'][new_key] = str(value)
- entities[state.entity_id] += 1
- point['tags'].update(tags)
- points.append(point)
- session.expunge(event)
- if len(points) >= step:
+ entities[state.entity_id] += 1
+ point['tags'].update(tags)
+ points.append(point)
+ session.expunge(event)
+
+ if points:
if not simulate:
- print("Write {} points to the database".format(len(points)))
client.write_points(points)
count += len(points)
- points = []
+ # This prevents the progress bar from going over 100% when
+ # the last step happens
+ print_progress((step_start + len(
+ points)), total_events, prefix_format.format(
+ step_start, total_events))
+ else:
+ print_progress(
+ (step_start + step), total_events, prefix_format.format(
+ step_start, total_events))
- if points:
- if not simulate:
- print("Write {} points to the database".format(len(points)))
- client.write_points(points)
- count += len(points)
+ points = []
+ step_start += step
print("\nStatistics:")
print("\n".join(["{:6}: {}".format(v, k) for k, v
in sorted(entities.items(), key=lambda x: x[1])]))
- print("\nImport finished {} points written".format(count))
+ print("\nInvalid Points: {}".format(len(invalid_points)))
+ print("\nImport finished: {} points written".format(count))
return 0
+
+
+# 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:
+ """Print progress bar.
+
+ Call in a loop to create terminal progress bar
+ @params:
+ iteration - Required : current iteration (Int)
+ total - Required : total iterations (Int)
+ prefix - Optional : prefix string (Str)
+ suffix - Optional : suffix string (Str)
+ decimals - Optional : number of decimals in percent complete (Int)
+ barLength - Optional : character length of bar (Int)
+ """
+ filled_length = int(round(bar_length * iteration / float(total)))
+ percents = round(100.00 * (iteration / float(total)), decimals)
+ line = '#' * filled_length + '-' * (bar_length - filled_length)
+ sys.stdout.write('%s [%s] %s%s %s\r' % (prefix, line,
+ percents, '%', suffix))
+ sys.stdout.flush()
+ if iteration == total:
+ print('\n')
diff --git a/homeassistant/scripts/influxdb_migrator.py b/homeassistant/scripts/influxdb_migrator.py
index cad8f878ca6..f41240bad74 100644
--- a/homeassistant/scripts/influxdb_migrator.py
+++ b/homeassistant/scripts/influxdb_migrator.py
@@ -119,7 +119,7 @@ def run(script_args: List) -> int:
point_wt_time = 0
print("Migrating from {} to {}".format(old_dbname, args.dbname))
- # Walk into measurenebt
+ # Walk into measurement
for index, measurement in enumerate(measurements):
# Get tag list
diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py
index b46d135c107..64ad09bcd70 100644
--- a/homeassistant/scripts/keyring.py
+++ b/homeassistant/scripts/keyring.py
@@ -5,7 +5,7 @@ import os
from homeassistant.util.yaml import _SECRET_NAMESPACE
-REQUIREMENTS = ['keyring==10.6.0', 'keyrings.alt==2.3']
+REQUIREMENTS = ['keyring==11.0.0', 'keyrings.alt==2.3']
def run(args):
@@ -29,7 +29,7 @@ def run(args):
if args.action == 'info':
keyr = keyring.get_keyring()
- print('Keyring version {}\n'.format(keyring.__version__))
+ print('Keyring version {}\n'.format(REQUIREMENTS[0].split('==')[1]))
print('Active keyring : {}'.format(keyr.__module__))
config_name = os.path.join(platform.config_root(), 'keyringrc.cfg')
print('Config location : {}'.format(config_name))
diff --git a/homeassistant/setup.py b/homeassistant/setup.py
index 12a39e80517..3221ea35d48 100644
--- a/homeassistant/setup.py
+++ b/homeassistant/setup.py
@@ -1,27 +1,24 @@
"""All methods needed to bootstrap a Home Assistant instance."""
import asyncio
import logging.handlers
-import os
from timeit import default_timer as timer
from types import ModuleType
from typing import Optional, Dict
-import homeassistant.config as conf_util
-import homeassistant.core as core
-import homeassistant.loader as loader
-import homeassistant.util.package as pkg_util
+from homeassistant import requirements, core, loader, config as conf_util
from homeassistant.config import async_notify_setup_error
-from homeassistant.const import (
- EVENT_COMPONENT_LOADED, PLATFORM_FORMAT, CONSTRAINT_FILE)
+from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.async import run_coroutine_threadsafe
+
_LOGGER = logging.getLogger(__name__)
ATTR_COMPONENT = 'component'
DATA_SETUP = 'setup_tasks'
-DATA_PIP_LOCK = 'pip_lock'
+DATA_DEPS_REQS = 'deps_reqs_processed'
SLOW_SETUP_WARNING = 10
@@ -60,43 +57,6 @@ def async_setup_component(hass: core.HomeAssistant, domain: str,
return (yield from task)
-@asyncio.coroutine
-def _async_process_requirements(hass: core.HomeAssistant, name: str,
- requirements) -> bool:
- """Install the requirements for a component.
-
- This method is a coroutine.
- """
- if hass.config.skip_pip:
- return True
-
- pip_lock = hass.data.get(DATA_PIP_LOCK)
- if pip_lock is None:
- pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock(loop=hass.loop)
-
- def pip_install(mod):
- """Install packages."""
- if pkg_util.running_under_virtualenv():
- return pkg_util.install_package(
- mod, constraints=os.path.join(
- os.path.dirname(__file__), CONSTRAINT_FILE))
- return pkg_util.install_package(
- mod, target=hass.config.path('deps'),
- constraints=os.path.join(
- os.path.dirname(__file__), CONSTRAINT_FILE))
-
- with (yield from pip_lock):
- for req in requirements:
- ret = yield from hass.async_add_job(pip_install, req)
- if not ret:
- _LOGGER.error("Not initializing %s because could not install "
- "dependency %s", name, req)
- async_notify_setup_error(hass, name)
- return False
-
- return True
-
-
@asyncio.coroutine
def _async_process_dependencies(hass, config, name, dependencies):
"""Ensure all dependencies are set up."""
@@ -162,22 +122,11 @@ def _async_setup_component(hass: core.HomeAssistant,
log_error("Invalid config.")
return False
- if not hass.config.skip_pip and hasattr(component, 'REQUIREMENTS'):
- req_success = yield from _async_process_requirements(
- hass, domain, component.REQUIREMENTS)
- if not req_success:
- log_error("Could not install all requirements.")
- return False
-
- if hasattr(component, 'DEPENDENCIES'):
- dep_success = yield from _async_process_dependencies(
- hass, config, domain, component.DEPENDENCIES)
-
- if not dep_success:
- log_error("Could not setup all dependencies.")
- return False
-
- async_comp = hasattr(component, 'async_setup')
+ try:
+ yield from _process_deps_reqs(hass, config, domain, component)
+ except HomeAssistantError as err:
+ log_error(str(err))
+ return False
start = timer()
_LOGGER.info("Setting up %s", domain)
@@ -192,7 +141,7 @@ def _async_setup_component(hass: core.HomeAssistant,
domain, SLOW_SETUP_WARNING)
try:
- if async_comp:
+ if hasattr(component, 'async_setup'):
result = yield from component.async_setup(hass, processed_config)
else:
result = yield from hass.async_add_job(
@@ -256,21 +205,40 @@ def async_prepare_setup_platform(hass: core.HomeAssistant, config, domain: str,
elif platform_path in hass.config.components:
return platform
- # Load dependencies
- if hasattr(platform, 'DEPENDENCIES'):
- dep_success = yield from _async_process_dependencies(
- hass, config, platform_path, platform.DEPENDENCIES)
-
- if not dep_success:
- log_error("Could not setup all dependencies.")
- return None
-
- if not hass.config.skip_pip and hasattr(platform, 'REQUIREMENTS'):
- req_success = yield from _async_process_requirements(
- hass, platform_path, platform.REQUIREMENTS)
-
- if not req_success:
- log_error("Could not install all requirements.")
- return None
+ try:
+ yield from _process_deps_reqs(hass, config, platform_name, platform)
+ except HomeAssistantError as err:
+ log_error(str(err))
+ return None
return platform
+
+
+@asyncio.coroutine
+def _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.
+ """
+ processed = hass.data.get(DATA_DEPS_REQS)
+
+ if processed is None:
+ processed = hass.data[DATA_DEPS_REQS] = set()
+ elif name in processed:
+ return
+
+ if hasattr(module, 'DEPENDENCIES'):
+ dep_success = yield from _async_process_dependencies(
+ hass, config, name, module.DEPENDENCIES)
+
+ if not dep_success:
+ raise HomeAssistantError("Could not setup all dependencies.")
+
+ if not hass.config.skip_pip and hasattr(module, 'REQUIREMENTS'):
+ req_success = yield from requirements.async_process_requirements(
+ hass, name, module.REQUIREMENTS)
+
+ if not req_success:
+ raise HomeAssistantError("Could not install all requirements.")
+
+ processed.add(name)
diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py
index cb3ebeb7ee6..c4fea2846c5 100644
--- a/homeassistant/util/__init__.py
+++ b/homeassistant/util/__init__.py
@@ -227,7 +227,7 @@ class OrderedSet(MutableSet):
return '%s(%r)' % (self.__class__.__name__, list(self))
def __eq__(self, other):
- """Return the comparision."""
+ """Return the comparison."""
if isinstance(other, OrderedSet):
return len(self) == len(other) and list(self) == list(other)
return set(self) == set(other)
diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py
index 5e8b3382fb1..c3400bac9be 100644
--- a/homeassistant/util/dt.py
+++ b/homeassistant/util/dt.py
@@ -3,7 +3,7 @@ import datetime as dt
import re
# pylint: disable=unused-import
-from typing import Any, Union, Optional, Tuple # NOQA
+from typing import Any, Dict, Union, Optional, Tuple # NOQA
import pytz
diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py
index 8b07a344148..35b266cb104 100644
--- a/homeassistant/util/location.py
+++ b/homeassistant/util/location.py
@@ -107,7 +107,7 @@ def vincenty(point1: Tuple[float, float], point2: Tuple[float, float],
sinU2 = math.sin(U2)
cosU2 = math.cos(U2)
- for iteration in range(MAX_ITERATIONS):
+ for _ in range(MAX_ITERATIONS):
sinLambda = math.sin(Lambda)
cosLambda = math.cos(Lambda)
sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 +
diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py
index 7daaf937975..8a15c4f6320 100644
--- a/homeassistant/util/logging.py
+++ b/homeassistant/util/logging.py
@@ -23,7 +23,7 @@ class HideSensitiveDataFilter(logging.Filter):
# pylint: disable=invalid-name
class AsyncHandler(object):
- """Logging handler wrapper to add a async layer."""
+ """Logging handler wrapper to add an async layer."""
def __init__(self, loop, handler):
"""Initialize async logging handler wrapper."""
diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py
index 31b76365da4..ecef1087747 100644
--- a/homeassistant/util/unit_system.py
+++ b/homeassistant/util/unit_system.py
@@ -105,7 +105,7 @@ class UnitSystem(object):
raise TypeError('{} is not a numeric value.'.format(str(length)))
return distance_util.convert(length, from_unit,
- self.length_unit) # type: float
+ self.length_unit)
def as_dict(self) -> dict:
"""Convert the unit system to a dictionary."""
diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py
index 48d709bc549..d0d5199e0f4 100644
--- a/homeassistant/util/yaml.py
+++ b/homeassistant/util/yaml.py
@@ -83,6 +83,14 @@ def dump(_dict: dict) -> str:
.replace(': null\n', ':\n')
+def save_yaml(path, data):
+ """Save YAML to a file."""
+ # Dump before writing to not truncate the file if dumping fails
+ data = dump(data)
+ with open(path, 'w', encoding='utf-8') as outfile:
+ outfile.write(data)
+
+
def clear_secret_cache() -> None:
"""Clear the secret cache.
diff --git a/requirements_all.txt b/requirements_all.txt
index ad6602464a6..3ca5b9fc763 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -3,14 +3,14 @@ requests==2.18.4
pyyaml>=3.11,<4
pytz>=2017.02
pip>=8.0.3
-jinja2>=2.9.6
+jinja2>=2.10
voluptuous==0.10.5
typing>=3,<4
-aiohttp==2.3.7
-yarl==0.18.0
+aiohttp==2.3.10
+yarl==1.1.0
async_timeout==2.0.0
chardet==3.0.4
-astral==1.4
+astral==1.5
certifi>=2017.4.17
# homeassistant.components.nuimo_controller
@@ -50,7 +50,7 @@ SoCo==0.13
TravisPy==0.3.5
# homeassistant.components.notify.twitter
-TwitterAPI==2.4.6
+TwitterAPI==2.4.8
# homeassistant.components.notify.yessssms
YesssSMS==0.1.1b3
@@ -126,7 +126,7 @@ batinfo==0.4.2
beautifulsoup4==4.6.0
# homeassistant.components.zha
-bellows==0.4.0
+bellows==0.5.0
# homeassistant.components.blink
blinkpy==0.6.0
@@ -170,13 +170,13 @@ caldav==0.5.0
ciscosparkapi==0.4.2
# homeassistant.components.coinbase
-coinbase==2.0.6
+coinbase==2.0.7
# homeassistant.components.sensor.coinmarketcap
-coinmarketcap==4.1.2
+coinmarketcap==4.2.1
# homeassistant.scripts.check_config
-colorlog==3.0.1
+colorlog==3.1.2
# homeassistant.components.alarm_control_panel.concord232
# homeassistant.components.binary_sensor.concord232
@@ -352,7 +352,7 @@ hipnotify==1.0.8
holidays==0.9.3
# homeassistant.components.frontend
-home-assistant-frontend==20180130.0
+home-assistant-frontend==20180209.0
# homeassistant.components.camera.onvif
http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a
@@ -406,7 +406,7 @@ ihcsdk==2.1.1
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
-influxdb==4.1.1
+influxdb==5.0.0
# homeassistant.components.insteon_local
insteonlocal==0.53
@@ -425,7 +425,7 @@ jsonrpc-async==0.6
jsonrpc-websocket==0.5
# homeassistant.scripts.keyring
-keyring==10.6.0
+keyring==11.0.0
# homeassistant.scripts.keyring
keyrings.alt==2.3
@@ -441,7 +441,7 @@ libpurecoollink==0.4.2
libpyfoscam==1.0
# homeassistant.components.device_tracker.mikrotik
-librouteros==1.0.4
+librouteros==1.0.5
# homeassistant.components.media_player.soundtouch
libsoundtouch==0.7.2
@@ -453,7 +453,7 @@ liffylights==0.9.4
lightify==1.0.6.1
# homeassistant.components.light.limitlessled
-limitlessled==1.0.8
+limitlessled==1.0.9
# homeassistant.components.linode
linode-api==4.1.4b2
@@ -477,6 +477,9 @@ matrix-client==0.0.6
# homeassistant.components.maxcube
maxcube-api==0.1.0
+# homeassistant.components.mercedesme
+mercedesmejsonpy==0.1.2
+
# homeassistant.components.notify.message_bird
messagebird==1.2.0
@@ -485,7 +488,7 @@ messagebird==1.2.0
mficlient==0.3.0
# homeassistant.components.sensor.miflora
-miflora==0.2.0
+miflora==0.3.0
# homeassistant.components.upnp
miniupnpc==2.0.2
@@ -494,7 +497,7 @@ miniupnpc==2.0.2
motorparts==1.0.2
# homeassistant.components.tts
-mutagen==1.39
+mutagen==1.40.0
# homeassistant.components.mychevy
mychevy==0.1.1
@@ -551,7 +554,7 @@ orvibo==1.1.1
paho-mqtt==1.3.1
# homeassistant.components.media_player.panasonic_viera
-panasonic_viera==0.2
+panasonic_viera==0.3
# homeassistant.components.media_player.dunehd
pdunehd==1.3
@@ -615,11 +618,14 @@ pushetta==1.0.15
pwmled==1.2.1
# homeassistant.components.canary
-py-canary==0.2.3
+py-canary==0.4.0
# homeassistant.components.sensor.cpuspeed
py-cpuinfo==3.3.0
+# homeassistant.components.melissa
+py-melissa-climate==1.0.1
+
# homeassistant.components.camera.synology
py-synology==0.1.5
@@ -687,7 +693,7 @@ pycsspeechtts==1.0.2
pydaikin==0.4
# homeassistant.components.deconz
-pydeconz==25
+pydeconz==27
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -699,7 +705,7 @@ pydroid-ipcam==0.8
pyebox==0.1.0
# homeassistant.components.climate.econet
-pyeconet==0.0.4
+pyeconet==0.0.5
# homeassistant.components.eight_sleep
pyeight==0.0.7
@@ -732,7 +738,7 @@ pyhik==0.1.4
pyhiveapi==0.2.11
# homeassistant.components.homematic
-pyhomematic==0.1.38
+pyhomematic==0.1.39
# homeassistant.components.sensor.hydroquebec
pyhydroquebec==2.1.0
@@ -783,8 +789,11 @@ pylutron==0.1.0
# homeassistant.components.notify.mailgun
pymailgunner==1.4
+# homeassistant.components.media_player.mediaroom
+pymediaroom==0.5
+
# homeassistant.components.mochad
-pymochad==0.1.1
+pymochad==0.2.0
# homeassistant.components.modbus
pymodbus==1.3.1
@@ -821,7 +830,7 @@ pynut2==2.1.2
pynx584==0.4
# homeassistant.components.iota
-pyota==2.0.3
+pyota==2.0.4
# homeassistant.components.sensor.otp
pyotp==2.2.6
@@ -830,6 +839,9 @@ pyotp==2.2.6
# homeassistant.components.weather.openweathermap
pyowm==2.8.0
+# homeassistant.components.sensor.pollen
+pypollencom==1.1.1
+
# homeassistant.components.qwikswitch
pyqwikswitch==0.4
@@ -848,6 +860,9 @@ pyserial==3.1.1
# homeassistant.components.lock.sesame
pysesame==0.1.0
+# homeassistant.components.goalfeed
+pysher==0.2.0
+
# homeassistant.components.sensor.sma
pysma==0.1.3
@@ -903,9 +918,10 @@ python-juicenet==0.0.5
# homeassistant.components.fan.xiaomi_miio
# homeassistant.components.light.xiaomi_miio
+# homeassistant.components.remote.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
-python-miio==0.3.4
+python-miio==0.3.5
# homeassistant.components.media_player.mpd
python-mpd2==0.5.5
@@ -957,7 +973,7 @@ python-wink==1.7.3
python_opendata_transport==0.0.3
# homeassistant.components.zwave
-python_openzwave==0.4.0.35
+python_openzwave==0.4.3
# homeassistant.components.alarm_control_panel.egardia
pythonegardia==1.0.26
@@ -969,7 +985,7 @@ pythonwhois==2.4.3
pytile==1.1.0
# homeassistant.components.climate.touchline
-pytouchline==0.6
+pytouchline==0.7
# homeassistant.components.device_tracker.trackr
pytrackr==0.0.5
@@ -999,7 +1015,7 @@ pywebpush==1.5.0
pywemo==0.4.25
# homeassistant.components.camera.xeoma
-pyxeoma==1.2
+pyxeoma==1.3
# homeassistant.components.zabbix
pyzabbix==0.7.4
@@ -1056,7 +1072,7 @@ samsungctl==0.6.0
satel_integra==0.1.0
# homeassistant.components.sensor.deutsche_bahn
-schiene==0.20
+schiene==0.21
# homeassistant.components.scsgate
scsgate==0.1.0
@@ -1111,7 +1127,8 @@ speedtest-cli==1.0.7
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
-sqlalchemy==1.2.1
+# homeassistant.components.sensor.sql
+sqlalchemy==1.2.2
# homeassistant.components.statsd
statsd==3.2.1
@@ -1123,7 +1140,7 @@ steamodd==4.21
suds-py3==1.3.3.0
# homeassistant.components.tahoma
-tahoma-api==0.0.10
+tahoma-api==0.0.11
# homeassistant.components.sensor.tank_utility
tank_utility==1.4.0
@@ -1144,7 +1161,7 @@ tellduslive==0.10.4
temperusb==1.5.3
# homeassistant.components.tesla
-teslajsonpy==0.0.19
+teslajsonpy==0.0.23
# homeassistant.components.thingspeak
thingspeak==0.4.1
@@ -1181,7 +1198,7 @@ user-agents==1.1.0
uvcclient==0.10.1
# homeassistant.components.climate.venstar
-venstarcolortouch==0.5
+venstarcolortouch==0.6
# homeassistant.components.volvooncall
volvooncall==0.4.0
@@ -1198,9 +1215,8 @@ vultr==0.1.2
# homeassistant.components.wake_on_lan
# homeassistant.components.media_player.panasonic_viera
# homeassistant.components.media_player.samsungtv
-# homeassistant.components.media_player.webostv
# homeassistant.components.switch.wake_on_lan
-wakeonlan==0.2.2
+wakeonlan==1.0.0
# homeassistant.components.sensor.waqi
waqiasync==1.0.0
@@ -1209,7 +1225,7 @@ waqiasync==1.0.0
warrant==0.6.1
# homeassistant.components.waterfurnace
-waterfurnace==0.3.0
+waterfurnace==0.4.0
# homeassistant.components.media_player.gpmdp
websocket-client==0.37.0
@@ -1247,7 +1263,7 @@ yeelight==0.3.3
yeelightsunflower==0.0.8
# homeassistant.components.media_extractor
-youtube_dl==2018.01.14
+youtube_dl==2018.01.21
# homeassistant.components.light.zengge
zengge==0.2
@@ -1257,3 +1273,9 @@ zeroconf==0.19.1
# homeassistant.components.media_player.ziggo_mediabox_xl
ziggo-mediabox-xl==1.0.0
+
+# homeassistant.components.zha
+zigpy-xbee==0.0.1
+
+# homeassistant.components.zha
+zigpy==0.0.1
diff --git a/requirements_docs.txt b/requirements_docs.txt
index 04ebb074e03..c5c48e0bc73 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -1,3 +1,3 @@
-Sphinx==1.6.6
-sphinx-autodoc-typehints==1.2.3
+Sphinx==1.6.7
+sphinx-autodoc-typehints==1.2.4
sphinx-autodoc-annotation==1.0.post1
diff --git a/requirements_test.txt b/requirements_test.txt
index 22bb6623e16..cddf11a34b8 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,7 +1,7 @@
# linters such as flake8 and pylint should be pinned, as new releases
# make new things fail. Manually update these pins when pulling in a
# new version
-flake8==3.3
+flake8==3.5
pylint==1.6.5
mypy==0.560
pydocstyle==1.1.1
@@ -13,5 +13,5 @@ pytest-timeout>=1.2.1
pytest-sugar==0.9.0
requests_mock==1.4
mock-open==1.3.1
-flake8-docstrings==1.0.2
+flake8-docstrings==1.0.3
asynctest>=0.11.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index c162dc2fd02..1ae1b9f2e14 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -2,7 +2,7 @@
# linters such as flake8 and pylint should be pinned, as new releases
# make new things fail. Manually update these pins when pulling in a
# new version
-flake8==3.3
+flake8==3.5
pylint==1.6.5
mypy==0.560
pydocstyle==1.1.1
@@ -14,7 +14,7 @@ pytest-timeout>=1.2.1
pytest-sugar==0.9.0
requests_mock==1.4
mock-open==1.3.1
-flake8-docstrings==1.0.2
+flake8-docstrings==1.0.3
asynctest>=0.11.1
@@ -38,7 +38,7 @@ apns2==0.3.0
caldav==0.5.0
# homeassistant.components.sensor.coinmarketcap
-coinmarketcap==4.1.2
+coinmarketcap==4.2.1
# homeassistant.components.device_tracker.upc_connect
defusedxml==0.5.0
@@ -75,11 +75,11 @@ hbmqtt==0.9.1
holidays==0.9.3
# homeassistant.components.frontend
-home-assistant-frontend==20180130.0
+home-assistant-frontend==20180209.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
-influxdb==4.1.1
+influxdb==5.0.0
# homeassistant.components.dyson
libpurecoollink==0.4.2
@@ -121,7 +121,7 @@ prometheus_client==0.1.0
pushbullet.py==0.11.0
# homeassistant.components.canary
-py-canary==0.2.3
+py-canary==0.4.0
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -169,7 +169,8 @@ somecomfort==0.5.0
# homeassistant.components.recorder
# homeassistant.scripts.db_migrator
-sqlalchemy==1.2.1
+# homeassistant.components.sensor.sql
+sqlalchemy==1.2.2
# homeassistant.components.statsd
statsd==3.2.1
@@ -183,9 +184,8 @@ vultr==0.1.2
# homeassistant.components.wake_on_lan
# homeassistant.components.media_player.panasonic_viera
# homeassistant.components.media_player.samsungtv
-# homeassistant.components.media_player.webostv
# homeassistant.components.switch.wake_on_lan
-wakeonlan==0.2.2
+wakeonlan==1.0.0
# homeassistant.components.cloud
warrant==0.6.1
diff --git a/setup.py b/setup.py
index 4b19e47fb2c..5af84fc8e0e 100755
--- a/setup.py
+++ b/setup.py
@@ -2,14 +2,16 @@
"""Home Assistant setup script."""
import os
from setuptools import setup, find_packages
+import sys
+
+import homeassistant.const as hass_const
-from homeassistant.const import __version__
PROJECT_NAME = 'Home Assistant'
PROJECT_PACKAGE_NAME = 'homeassistant'
PROJECT_LICENSE = 'Apache License 2.0'
PROJECT_AUTHOR = 'The Home Assistant Authors'
-PROJECT_COPYRIGHT = ' 2013-2017, {}'.format(PROJECT_AUTHOR)
+PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR)
PROJECT_URL = 'https://home-assistant.io/'
PROJECT_EMAIL = 'hello@home-assistant.io'
PROJECT_DESCRIPTION = ('Open-source home automation platform '
@@ -41,7 +43,7 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH)
HERE = os.path.abspath(os.path.dirname(__file__))
-DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, __version__)
+DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__)
PACKAGES = find_packages(exclude=['tests', 'tests.*'])
@@ -50,20 +52,26 @@ REQUIRES = [
'pyyaml>=3.11,<4',
'pytz>=2017.02',
'pip>=8.0.3',
- 'jinja2>=2.9.6',
+ 'jinja2>=2.10',
'voluptuous==0.10.5',
'typing>=3,<4',
- 'aiohttp==2.3.7', # If updated, check if yarl also needs an update!
- 'yarl==0.18.0',
+ 'aiohttp==2.3.10', # If updated, check if yarl also needs an update!
+ 'yarl==1.1.0',
'async_timeout==2.0.0',
'chardet==3.0.4',
- 'astral==1.4',
+ 'astral==1.5',
'certifi>=2017.4.17',
]
+MIN_PY_VERSION = '.'.join(map(
+ str,
+ hass_const.REQUIRED_PYTHON_VER_WIN
+ if sys.platform.startswith('win')
+ else hass_const.REQUIRED_PYTHON_VER))
+
setup(
name=PROJECT_PACKAGE_NAME,
- version=__version__,
+ version=hass_const.__version__,
license=PROJECT_LICENSE,
url=PROJECT_URL,
download_url=DOWNLOAD_URL,
@@ -75,6 +83,7 @@ setup(
zip_safe=False,
platforms='any',
install_requires=REQUIRES,
+ python_requires='>={}'.format(MIN_PY_VERSION),
test_suite='tests',
keywords=['home', 'automation'],
entry_points={
diff --git a/tests/common.py b/tests/common.py
index 3823a1e2b4e..22af8ecb8a3 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -14,7 +14,9 @@ from aiohttp import web
from homeassistant import core as ha, loader
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
+from homeassistant.helpers import (
+ intent, dispatcher, entity, restore_state, entity_registry,
+ entity_platform)
from homeassistant.util.unit_system import METRIC_SYSTEM
import homeassistant.util.dt as date_util
import homeassistant.util.yaml as yaml
@@ -315,6 +317,14 @@ def mock_component(hass, component):
hass.config.components.add(component)
+def mock_registry(hass):
+ """Mock the Entity Registry."""
+ registry = entity_registry.EntityRegistry(hass)
+ registry.entities = {}
+ hass.data[entity_platform.DATA_REGISTRY] = registry
+ return registry
+
+
class MockModule(object):
"""Representation of a fake module."""
@@ -576,3 +586,40 @@ class MockDependency:
func(*args, **kwargs)
return run_mocked
+
+
+class MockEntity(entity.Entity):
+ """Mock Entity class."""
+
+ def __init__(self, **values):
+ """Initialize an entity."""
+ self._values = values
+
+ if 'entity_id' in values:
+ self.entity_id = values['entity_id']
+
+ @property
+ def name(self):
+ """Return the name of the entity."""
+ return self._handle('name')
+
+ @property
+ def should_poll(self):
+ """Return the ste of the polling."""
+ return self._handle('should_poll')
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of the entity."""
+ return self._handle('unique_id')
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._handle('available')
+
+ def _handle(self, attr):
+ """Helper for the attributes."""
+ if attr in self._values:
+ return self._values[attr]
+ return getattr(super(), attr)
diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py
index c47ed941b65..e4b29d43e48 100644
--- a/tests/components/alarm_control_panel/test_manual.py
+++ b/tests/components/alarm_control_panel/test_manual.py
@@ -34,7 +34,7 @@ class TestAlarmControlPanelManual(unittest.TestCase):
mock = MagicMock()
add_devices = mock.MagicMock()
demo.setup_platform(self.hass, {}, add_devices)
- self.assertEquals(add_devices.call_count, 1)
+ self.assertEqual(add_devices.call_count, 1)
def test_arm_home_no_pending(self):
"""Test arm home method."""
diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py
index a3587622b3d..2c8fafde155 100644
--- a/tests/components/alexa/test_intent.py
+++ b/tests/components/alexa/test_intent.py
@@ -98,8 +98,9 @@ def alexa_client(loop, hass, test_client):
return loop.run_until_complete(test_client(hass.http.app))
-def _intent_req(client, data={}):
- return client.post(intent.INTENTS_API_ENDPOINT, data=json.dumps(data),
+def _intent_req(client, data=None):
+ return client.post(intent.INTENTS_API_ENDPOINT,
+ data=json.dumps(data or {}),
headers={'content-type': 'application/json'})
diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py
index 3416dfbe367..71485231150 100644
--- a/tests/components/alexa/test_smart_home.py
+++ b/tests/components/alexa/test_smart_home.py
@@ -5,7 +5,9 @@ from uuid import uuid4
import pytest
-from homeassistant.const import TEMP_FAHRENHEIT, CONF_UNIT_OF_MEASUREMENT
+from homeassistant.const import (
+ TEMP_FAHRENHEIT, STATE_LOCKED, STATE_UNLOCKED,
+ STATE_UNKNOWN)
from homeassistant.setup import async_setup_component
from homeassistant.components import alexa
from homeassistant.components.alexa import smart_home
@@ -102,84 +104,12 @@ def test_wrong_version(hass):
@asyncio.coroutine
-def test_discovery_request(hass):
+def discovery_test(device, hass, expected_endpoints=1):
"""Test alexa discovery request."""
request = get_new_request('Alexa.Discovery', 'Discover')
# setup test devices
- hass.states.async_set(
- 'switch.test', 'on', {'friendly_name': "Test switch"})
-
- hass.states.async_set(
- 'light.test_1', 'on', {'friendly_name': "Test light 1"})
- hass.states.async_set(
- 'light.test_2', 'on', {
- 'friendly_name': "Test light 2", 'supported_features': 1
- })
- hass.states.async_set(
- 'light.test_3', 'on', {
- 'friendly_name': "Test light 3", 'supported_features': 19
- })
-
- hass.states.async_set(
- 'script.test', 'off', {'friendly_name': "Test script"})
- hass.states.async_set(
- 'script.test_2', 'off', {'friendly_name': "Test script 2",
- 'can_cancel': True})
-
- hass.states.async_set(
- 'input_boolean.test', 'off', {'friendly_name': "Test input boolean"})
-
- hass.states.async_set(
- 'scene.test', 'off', {'friendly_name': "Test scene"})
-
- hass.states.async_set(
- 'fan.test_1', 'off', {'friendly_name': "Test fan 1"})
-
- hass.states.async_set(
- 'fan.test_2', 'off', {
- 'friendly_name': "Test fan 2", 'supported_features': 1,
- 'speed_list': ['low', 'medium', 'high']
- })
-
- hass.states.async_set(
- 'lock.test', 'off', {'friendly_name': "Test lock"})
-
- hass.states.async_set(
- 'media_player.test', 'off', {
- 'friendly_name': "Test media player",
- 'supported_features': 20925,
- 'volume_level': 1
- })
-
- hass.states.async_set(
- 'alert.test', 'off', {'friendly_name': "Test alert"})
-
- hass.states.async_set(
- 'automation.test', 'off', {'friendly_name': "Test automation"})
-
- hass.states.async_set(
- 'group.test', 'off', {'friendly_name': "Test group"})
-
- hass.states.async_set(
- 'cover.test', 'off', {
- 'friendly_name': "Test cover", 'supported_features': 255,
- 'position': 85
- })
-
- hass.states.async_set(
- 'sensor.test_temp', '59', {
- 'friendly_name': "Test Temp Sensor",
- 'unit_of_measurement': TEMP_FAHRENHEIT,
- })
-
- # This sensor measures a quantity not applicable to Alexa, and should not
- # be discovered.
- hass.states.async_set(
- 'sensor.test_sickness', '0.1', {
- 'friendly_name': "Test Space Sickness Sensor",
- 'unit_of_measurement': 'garn',
- })
+ hass.states.async_set(*device)
msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
@@ -187,180 +117,580 @@ def test_discovery_request(hass):
assert 'event' in msg
msg = msg['event']
- assert len(msg['payload']['endpoints']) == 17
assert msg['header']['name'] == 'Discover.Response'
assert msg['header']['namespace'] == 'Alexa.Discovery'
+ endpoints = msg['payload']['endpoints']
+ assert len(endpoints) == expected_endpoints
- for appliance in msg['payload']['endpoints']:
- if appliance['endpointId'] == 'switch#test':
- assert appliance['displayCategories'][0] == "SWITCH"
- assert appliance['friendlyName'] == "Test switch"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.PowerController'
- continue
+ if expected_endpoints == 1:
+ return endpoints[0]
+ elif expected_endpoints > 1:
+ return endpoints
+ return None
- if appliance['endpointId'] == 'light#test_1':
- assert appliance['displayCategories'][0] == "LIGHT"
- assert appliance['friendlyName'] == "Test light 1"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.PowerController'
- continue
- if appliance['endpointId'] == 'light#test_2':
- assert appliance['displayCategories'][0] == "LIGHT"
- assert appliance['friendlyName'] == "Test light 2"
- assert len(appliance['capabilities']) == 2
+def assert_endpoint_capabilities(endpoint, *interfaces):
+ """Assert the endpoint supports the given interfaces.
- caps = set()
- for feature in appliance['capabilities']:
- caps.add(feature['interface'])
+ Returns a set of capabilities, in case you want to assert more things about
+ them.
+ """
+ capabilities = endpoint['capabilities']
+ supported = set(
+ feature['interface']
+ for feature in capabilities)
- assert 'Alexa.BrightnessController' in caps
- assert 'Alexa.PowerController' in caps
+ assert supported == set(interfaces)
+ return capabilities
- continue
- if appliance['endpointId'] == 'light#test_3':
- assert appliance['displayCategories'][0] == "LIGHT"
- assert appliance['friendlyName'] == "Test light 3"
- assert len(appliance['capabilities']) == 4
+@asyncio.coroutine
+def test_switch(hass):
+ """Test switch discovery."""
+ device = ('switch.test', 'on', {'friendly_name': "Test switch"})
+ appliance = yield from discovery_test(device, hass)
- caps = set()
- for feature in appliance['capabilities']:
- caps.add(feature['interface'])
+ assert appliance['endpointId'] == 'switch#test'
+ assert appliance['displayCategories'][0] == "SWITCH"
+ assert appliance['friendlyName'] == "Test switch"
+ assert_endpoint_capabilities(appliance, 'Alexa.PowerController')
- assert 'Alexa.BrightnessController' in caps
- assert 'Alexa.PowerController' in caps
- assert 'Alexa.ColorController' in caps
- assert 'Alexa.ColorTemperatureController' in caps
+ yield from assert_power_controller_works(
+ 'switch#test',
+ 'switch.turn_on',
+ 'switch.turn_off',
+ hass)
- continue
+ properties = yield from reported_properties(hass, 'switch#test')
+ properties.assert_equal('Alexa.PowerController', 'powerState', 'ON')
- if appliance['endpointId'] == 'script#test':
- assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER"
- assert appliance['friendlyName'] == "Test script"
- assert len(appliance['capabilities']) == 1
- capability = appliance['capabilities'][-1]
- assert capability['interface'] == 'Alexa.SceneController'
- assert not capability['supportsDeactivation']
- continue
- if appliance['endpointId'] == 'script#test_2':
- assert len(appliance['capabilities']) == 1
- capability = appliance['capabilities'][-1]
- assert capability['supportsDeactivation']
- continue
+@asyncio.coroutine
+def test_light(hass):
+ """Test light discovery."""
+ device = ('light.test_1', 'on', {'friendly_name': "Test light 1"})
+ appliance = yield from discovery_test(device, hass)
- if appliance['endpointId'] == 'input_boolean#test':
- assert appliance['displayCategories'][0] == "OTHER"
- assert appliance['friendlyName'] == "Test input boolean"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.PowerController'
- continue
+ assert appliance['endpointId'] == 'light#test_1'
+ assert appliance['displayCategories'][0] == "LIGHT"
+ assert appliance['friendlyName'] == "Test light 1"
+ assert_endpoint_capabilities(appliance, 'Alexa.PowerController')
- if appliance['endpointId'] == 'scene#test':
- assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
- assert appliance['friendlyName'] == "Test scene"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.SceneController'
- continue
+ yield from assert_power_controller_works(
+ 'light#test_1',
+ 'light.turn_on',
+ 'light.turn_off',
+ hass)
- if appliance['endpointId'] == 'fan#test_1':
- assert appliance['displayCategories'][0] == "OTHER"
- assert appliance['friendlyName'] == "Test fan 1"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.PowerController'
- continue
- if appliance['endpointId'] == 'fan#test_2':
- assert appliance['displayCategories'][0] == "OTHER"
- assert appliance['friendlyName'] == "Test fan 2"
- assert len(appliance['capabilities']) == 2
+@asyncio.coroutine
+def test_dimmable_light(hass):
+ """Test dimmable light discovery."""
+ device = (
+ 'light.test_2', 'on', {
+ 'brightness': 128,
+ 'friendly_name': "Test light 2", 'supported_features': 1
+ })
+ appliance = yield from discovery_test(device, hass)
- caps = set()
- for feature in appliance['capabilities']:
- caps.add(feature['interface'])
+ assert appliance['endpointId'] == 'light#test_2'
+ assert appliance['displayCategories'][0] == "LIGHT"
+ assert appliance['friendlyName'] == "Test light 2"
- assert 'Alexa.PercentageController' in caps
- assert 'Alexa.PowerController' in caps
- continue
+ assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.BrightnessController',
+ 'Alexa.PowerController',
+ )
- if appliance['endpointId'] == 'lock#test':
- assert appliance['displayCategories'][0] == "SMARTLOCK"
- assert appliance['friendlyName'] == "Test lock"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.LockController'
- continue
+ properties = yield from reported_properties(hass, 'light#test_2')
+ properties.assert_equal('Alexa.PowerController', 'powerState', 'ON')
+ properties.assert_equal('Alexa.BrightnessController', 'brightness', 50)
- if appliance['endpointId'] == 'media_player#test':
- assert appliance['displayCategories'][0] == "TV"
- assert appliance['friendlyName'] == "Test media player"
- assert len(appliance['capabilities']) == 3
- caps = set()
- for feature in appliance['capabilities']:
- caps.add(feature['interface'])
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.BrightnessController', 'SetBrightness', 'light#test_2',
+ 'light.turn_on',
+ hass,
+ payload={'brightness': '50'})
+ assert call.data['brightness_pct'] == 50
- assert 'Alexa.PowerController' in caps
- assert 'Alexa.Speaker' in caps
- assert 'Alexa.PlaybackController' in caps
- continue
- if appliance['endpointId'] == 'alert#test':
- assert appliance['displayCategories'][0] == "OTHER"
- assert appliance['friendlyName'] == "Test alert"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.PowerController'
- continue
+@asyncio.coroutine
+def test_color_light(hass):
+ """Test color light discovery."""
+ device = (
+ 'light.test_3',
+ 'on',
+ {
+ 'friendly_name': "Test light 3",
+ 'supported_features': 19,
+ 'min_mireds': 142,
+ 'color_temp': '333',
+ }
+ )
+ appliance = yield from discovery_test(device, hass)
- if appliance['endpointId'] == 'automation#test':
- assert appliance['displayCategories'][0] == "OTHER"
- assert appliance['friendlyName'] == "Test automation"
- assert len(appliance['capabilities']) == 1
- assert appliance['capabilities'][-1]['interface'] == \
- 'Alexa.PowerController'
- continue
+ assert appliance['endpointId'] == 'light#test_3'
+ assert appliance['displayCategories'][0] == "LIGHT"
+ assert appliance['friendlyName'] == "Test light 3"
- if appliance['endpointId'] == 'group#test':
- assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
- assert appliance['friendlyName'] == "Test group"
- assert len(appliance['capabilities']) == 1
- capability = appliance['capabilities'][-1]
- assert capability['interface'] == 'Alexa.SceneController'
- assert capability['supportsDeactivation'] is True
- continue
+ assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.BrightnessController',
+ 'Alexa.PowerController',
+ 'Alexa.ColorController',
+ 'Alexa.ColorTemperatureController',
+ )
- if appliance['endpointId'] == 'cover#test':
- assert appliance['displayCategories'][0] == "DOOR"
- assert appliance['friendlyName'] == "Test cover"
- assert len(appliance['capabilities']) == 2
+ # IncreaseColorTemperature and DecreaseColorTemperature have their own
+ # tests
- caps = set()
- for feature in appliance['capabilities']:
- caps.add(feature['interface'])
- assert 'Alexa.PercentageController' in caps
- assert 'Alexa.PowerController' in caps
- continue
+@asyncio.coroutine
+def test_script(hass):
+ """Test script discovery."""
+ device = ('script.test', 'off', {'friendly_name': "Test script"})
+ appliance = yield from discovery_test(device, hass)
- if appliance['endpointId'] == 'sensor#test_temp':
- assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR'
- assert appliance['friendlyName'] == 'Test Temp Sensor'
- assert len(appliance['capabilities']) == 1
- capability = appliance['capabilities'][0]
- assert capability['interface'] == 'Alexa.TemperatureSensor'
- assert capability['retrievable'] is True
- properties = capability['properties']
- assert {'name': 'temperature'} in properties['supported']
- continue
+ assert appliance['endpointId'] == 'script#test'
+ assert appliance['displayCategories'][0] == "ACTIVITY_TRIGGER"
+ assert appliance['friendlyName'] == "Test script"
- raise AssertionError("Unknown appliance!")
+ (capability,) = assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.SceneController')
+ assert not capability['supportsDeactivation']
+
+ yield from assert_scene_controller_works(
+ 'script#test',
+ 'script.turn_on',
+ None,
+ hass)
+
+
+@asyncio.coroutine
+def test_cancelable_script(hass):
+ """Test cancalable script discovery."""
+ device = (
+ 'script.test_2',
+ 'off',
+ {'friendly_name': "Test script 2", 'can_cancel': True},
+ )
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'script#test_2'
+ (capability,) = assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.SceneController')
+ assert capability['supportsDeactivation']
+
+ yield from assert_scene_controller_works(
+ 'script#test_2',
+ 'script.turn_on',
+ 'script.turn_off',
+ hass)
+
+
+@asyncio.coroutine
+def test_input_boolean(hass):
+ """Test input boolean discovery."""
+ device = (
+ 'input_boolean.test',
+ 'off',
+ {'friendly_name': "Test input boolean"},
+ )
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'input_boolean#test'
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test input boolean"
+ assert_endpoint_capabilities(appliance, 'Alexa.PowerController')
+
+ yield from assert_power_controller_works(
+ 'input_boolean#test',
+ 'input_boolean.turn_on',
+ 'input_boolean.turn_off',
+ hass)
+
+
+@asyncio.coroutine
+def test_scene(hass):
+ """Test scene discovery."""
+ device = ('scene.test', 'off', {'friendly_name': "Test scene"})
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'scene#test'
+ assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
+ assert appliance['friendlyName'] == "Test scene"
+
+ (capability,) = assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.SceneController')
+ assert not capability['supportsDeactivation']
+
+ yield from assert_scene_controller_works(
+ 'scene#test',
+ 'scene.turn_on',
+ None,
+ hass)
+
+
+@asyncio.coroutine
+def test_fan(hass):
+ """Test fan discovery."""
+ device = ('fan.test_1', 'off', {'friendly_name': "Test fan 1"})
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'fan#test_1'
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test fan 1"
+ assert_endpoint_capabilities(appliance, 'Alexa.PowerController')
+
+
+@asyncio.coroutine
+def test_variable_fan(hass):
+ """Test fan discovery.
+
+ This one has variable speed.
+ """
+ device = (
+ 'fan.test_2',
+ 'off', {
+ 'friendly_name': "Test fan 2",
+ 'supported_features': 1,
+ 'speed_list': ['low', 'medium', 'high'],
+ 'speed': 'high',
+ }
+ )
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'fan#test_2'
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test fan 2"
+
+ assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.PercentageController',
+ 'Alexa.PowerController',
+ )
+
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2',
+ 'fan.set_speed',
+ hass,
+ payload={'percentage': '50'})
+ assert call.data['speed'] == 'medium'
+
+ yield from assert_percentage_changes(
+ hass,
+ [('high', '-5'), ('off', '5'), ('low', '-80')],
+ 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2',
+ 'percentageDelta',
+ 'fan.set_speed',
+ 'speed')
+
+
+@asyncio.coroutine
+def test_lock(hass):
+ """Test lock discovery."""
+ device = ('lock.test', 'off', {'friendly_name': "Test lock"})
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'lock#test'
+ assert appliance['displayCategories'][0] == "SMARTLOCK"
+ assert appliance['friendlyName'] == "Test lock"
+ assert_endpoint_capabilities(appliance, 'Alexa.LockController')
+
+ yield from assert_request_calls_service(
+ 'Alexa.LockController', 'Lock', 'lock#test',
+ 'lock.lock',
+ hass)
+
+
+@asyncio.coroutine
+def test_media_player(hass):
+ """Test media player discovery."""
+ device = (
+ 'media_player.test',
+ 'off', {
+ 'friendly_name': "Test media player",
+ 'supported_features': 0x59bd,
+ 'volume_level': 0.75
+ }
+ )
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'media_player#test'
+ assert appliance['displayCategories'][0] == "TV"
+ assert appliance['friendlyName'] == "Test media player"
+
+ assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.InputController',
+ 'Alexa.PowerController',
+ 'Alexa.Speaker',
+ 'Alexa.StepSpeaker',
+ 'Alexa.PlaybackController',
+ )
+
+ yield from assert_power_controller_works(
+ 'media_player#test',
+ 'media_player.turn_on',
+ 'media_player.turn_off',
+ hass)
+
+ yield from assert_request_calls_service(
+ 'Alexa.PlaybackController', 'Play', 'media_player#test',
+ 'media_player.media_play',
+ hass)
+
+ yield from assert_request_calls_service(
+ 'Alexa.PlaybackController', 'Pause', 'media_player#test',
+ 'media_player.media_pause',
+ hass)
+
+ yield from assert_request_calls_service(
+ 'Alexa.PlaybackController', 'Stop', 'media_player#test',
+ 'media_player.media_stop',
+ hass)
+
+ yield from assert_request_calls_service(
+ 'Alexa.PlaybackController', 'Next', 'media_player#test',
+ 'media_player.media_next_track',
+ hass)
+
+ yield from assert_request_calls_service(
+ 'Alexa.PlaybackController', 'Previous', 'media_player#test',
+ 'media_player.media_previous_track',
+ hass)
+
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.Speaker', 'SetVolume', 'media_player#test',
+ 'media_player.volume_set',
+ hass,
+ payload={'volume': 50})
+ assert call.data['volume_level'] == 0.5
+
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.Speaker', 'SetMute', 'media_player#test',
+ 'media_player.volume_mute',
+ hass,
+ payload={'mute': True})
+ assert call.data['is_volume_muted']
+
+ call, _, = yield from assert_request_calls_service(
+ 'Alexa.Speaker', 'SetMute', 'media_player#test',
+ 'media_player.volume_mute',
+ hass,
+ payload={'mute': False})
+ assert not call.data['is_volume_muted']
+
+ yield from assert_percentage_changes(
+ hass,
+ [(0.7, '-5'), (0.8, '5'), (0, '-80')],
+ 'Alexa.Speaker', 'AdjustVolume', 'media_player#test',
+ 'volume',
+ 'media_player.volume_set',
+ 'volume_level')
+
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.StepSpeaker', 'SetMute', 'media_player#test',
+ 'media_player.volume_mute',
+ hass,
+ payload={'mute': True})
+ assert call.data['is_volume_muted']
+
+ call, _, = yield from assert_request_calls_service(
+ 'Alexa.StepSpeaker', 'SetMute', 'media_player#test',
+ 'media_player.volume_mute',
+ hass,
+ payload={'mute': False})
+ assert not call.data['is_volume_muted']
+
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.StepSpeaker', 'AdjustVolume', 'media_player#test',
+ 'media_player.volume_set',
+ hass,
+ payload={'volume': 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',
+ hass,
+ payload={'volume': -20})
+ assert call.data['volume_level'] == 0.55
+
+
+@asyncio.coroutine
+def test_alert(hass):
+ """Test alert discovery."""
+ device = ('alert.test', 'off', {'friendly_name': "Test alert"})
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'alert#test'
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test alert"
+ assert_endpoint_capabilities(appliance, 'Alexa.PowerController')
+
+ yield from assert_power_controller_works(
+ 'alert#test',
+ 'alert.turn_on',
+ 'alert.turn_off',
+ hass)
+
+
+@asyncio.coroutine
+def test_automation(hass):
+ """Test automation discovery."""
+ device = ('automation.test', 'off', {'friendly_name': "Test automation"})
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'automation#test'
+ assert appliance['displayCategories'][0] == "OTHER"
+ assert appliance['friendlyName'] == "Test automation"
+ assert_endpoint_capabilities(appliance, 'Alexa.PowerController')
+
+ yield from assert_power_controller_works(
+ 'automation#test',
+ 'automation.turn_on',
+ 'automation.turn_off',
+ hass)
+
+
+@asyncio.coroutine
+def test_group(hass):
+ """Test group discovery."""
+ device = ('group.test', 'off', {'friendly_name': "Test group"})
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'group#test'
+ assert appliance['displayCategories'][0] == "SCENE_TRIGGER"
+ assert appliance['friendlyName'] == "Test group"
+
+ (capability,) = assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.SceneController')
+ assert capability['supportsDeactivation']
+
+ yield from assert_scene_controller_works(
+ 'group#test',
+ 'homeassistant.turn_on',
+ 'homeassistant.turn_off',
+ hass)
+
+
+@asyncio.coroutine
+def test_cover(hass):
+ """Test cover discovery."""
+ device = (
+ 'cover.test',
+ 'off', {
+ 'friendly_name': "Test cover",
+ 'supported_features': 255,
+ 'position': 30,
+ }
+ )
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'cover#test'
+ assert appliance['displayCategories'][0] == "DOOR"
+ assert appliance['friendlyName'] == "Test cover"
+
+ assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.PercentageController',
+ 'Alexa.PowerController',
+ )
+
+ yield from assert_power_controller_works(
+ 'cover#test',
+ 'cover.open_cover',
+ 'cover.close_cover',
+ hass)
+
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.PercentageController', 'SetPercentage', 'cover#test',
+ 'cover.set_cover_position',
+ hass,
+ payload={'percentage': '50'})
+ assert call.data['position'] == 50
+
+ yield from assert_percentage_changes(
+ hass,
+ [(25, '-5'), (35, '5'), (0, '-80')],
+ 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test',
+ 'percentageDelta',
+ 'cover.set_cover_position',
+ 'position')
+
+
+@asyncio.coroutine
+def assert_percentage_changes(
+ hass,
+ adjustments,
+ namespace,
+ name,
+ endpoint,
+ parameter,
+ service,
+ changed_parameter):
+ """Assert an API request making percentage changes works.
+
+ AdjustPercentage, AdjustBrightness, etc. are examples of such requests.
+ """
+ for result_volume, adjustment in adjustments:
+ if parameter:
+ payload = {parameter: adjustment}
+ else:
+ payload = {}
+
+ call, _ = yield from assert_request_calls_service(
+ namespace, name, endpoint, service,
+ hass,
+ payload=payload)
+ assert call.data[changed_parameter] == result_volume
+
+
+@asyncio.coroutine
+def test_temp_sensor(hass):
+ """Test temperature sensor discovery."""
+ device = (
+ 'sensor.test_temp',
+ '42',
+ {
+ 'friendly_name': "Test Temp Sensor",
+ 'unit_of_measurement': TEMP_FAHRENHEIT,
+ }
+ )
+ appliance = yield from discovery_test(device, hass)
+
+ assert appliance['endpointId'] == 'sensor#test_temp'
+ assert appliance['displayCategories'][0] == 'TEMPERATURE_SENSOR'
+ assert appliance['friendlyName'] == 'Test Temp Sensor'
+
+ (capability,) = assert_endpoint_capabilities(
+ appliance,
+ 'Alexa.TemperatureSensor')
+ assert capability['interface'] == 'Alexa.TemperatureSensor'
+ properties = capability['properties']
+ assert properties['retrievable'] is True
+ assert {'name': 'temperature'} in properties['supported']
+
+ properties = yield from reported_properties(hass, 'sensor#test_temp')
+ properties.assert_equal('Alexa.TemperatureSensor', 'temperature',
+ {'value': 42.0, 'scale': 'FAHRENHEIT'})
+
+
+@asyncio.coroutine
+def test_unknown_sensor(hass):
+ """Test sensors of unknown quantities are not discovered."""
+ device = (
+ 'sensor.test_sickness', '0.1', {
+ 'friendly_name': "Test Space Sickness Sensor",
+ 'unit_of_measurement': 'garn',
+ })
+ yield from discovery_test(device, hass, expected_endpoints=0)
@asyncio.coroutine
@@ -440,7 +770,7 @@ def test_api_entity_not_exists(hass):
assert 'event' in msg
msg = msg['event']
- assert len(call_switch) == 0
+ assert not call_switch
assert msg['header']['name'] == 'ErrorResponse'
assert msg['header']['namespace'] == 'Alexa'
assert msg['payload']['type'] == 'NO_SUCH_ENDPOINT'
@@ -462,102 +792,95 @@ def test_api_function_not_implemented(hass):
@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover',
- 'input_boolean', 'light',
- 'switch'])
-def test_api_turn_on(hass, domain):
- """Test api turn on process."""
- request = get_new_request(
- 'Alexa.PowerController', 'TurnOn', '{}#test'.format(domain))
+def assert_request_fails(
+ namespace,
+ name,
+ endpoint,
+ service_not_called,
+ hass,
+ payload=None):
+ """Assert an API request returns an ErrorResponse."""
+ request = get_new_request(namespace, name, endpoint)
+ if payload:
+ request['directive']['payload'] = payload
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call_domain = domain
-
- if domain == 'cover':
- call = async_mock_service(hass, call_domain, 'open_cover')
- else:
- call = async_mock_service(hass, call_domain, 'turn_on')
+ domain, service_name = service_not_called.split('.')
+ call = async_mock_service(hass, domain, service_name)
msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
yield from hass.async_block_till_done()
+ assert not call
assert 'event' in msg
- msg = msg['event']
+ assert msg['event']['header']['name'] == 'ErrorResponse'
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
+ return msg
@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['alert', 'automation', 'cover', 'group',
- 'input_boolean', 'light', 'script',
- 'switch'])
-def test_api_turn_off(hass, domain):
- """Test api turn on process."""
- request = get_new_request(
- 'Alexa.PowerController', 'TurnOff', '{}#test'.format(domain))
+def assert_request_calls_service(
+ namespace,
+ name,
+ endpoint,
+ service,
+ hass,
+ response_type='Response',
+ payload=None):
+ """Assert an API request calls a hass service."""
+ request = get_new_request(namespace, name, endpoint)
+ if payload:
+ request['directive']['payload'] = payload
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'on', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call_domain = domain
-
- if domain == 'group':
- call_domain = 'homeassistant'
-
- if domain == 'cover':
- call = async_mock_service(hass, call_domain, 'close_cover')
- else:
- call = async_mock_service(hass, call_domain, 'turn_off')
+ domain, service_name = service.split('.')
+ call = async_mock_service(hass, domain, service_name)
msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
yield from hass.async_block_till_done()
- assert 'event' in msg
- msg = msg['event']
-
assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
+ assert 'event' in msg
+ assert call[0].data['entity_id'] == endpoint.replace('#', '.')
+ assert msg['event']['header']['name'] == response_type
+
+ return call[0], msg
@asyncio.coroutine
-def test_api_set_brightness(hass):
- """Test api set brightness process."""
- request = get_new_request(
- 'Alexa.BrightnessController', 'SetBrightness', 'light#test')
+def assert_power_controller_works(endpoint, on_service, off_service, hass):
+ """Assert PowerController API requests work."""
+ yield from assert_request_calls_service(
+ 'Alexa.PowerController', 'TurnOn', endpoint,
+ on_service, hass)
- # add payload
- request['directive']['payload']['brightness'] = '50'
+ yield from assert_request_calls_service(
+ 'Alexa.PowerController', 'TurnOff', endpoint,
+ off_service, hass)
- # setup test devices
- hass.states.async_set(
- 'light.test', 'off', {'friendly_name': "Test light"})
- call_light = async_mock_service(hass, 'light', 'turn_on')
+@asyncio.coroutine
+def assert_scene_controller_works(
+ endpoint,
+ activate_service,
+ deactivate_service,
+ hass):
+ """Assert SceneController API requests work."""
+ _, response = yield from assert_request_calls_service(
+ 'Alexa.SceneController', 'Activate', endpoint,
+ activate_service, hass,
+ response_type='ActivationStarted')
+ assert response['event']['payload']['cause']['type'] == 'VOICE_INTERACTION'
+ assert 'timestamp' in response['event']['payload']
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call_light) == 1
- assert call_light[0].data['entity_id'] == 'light.test'
- assert call_light[0].data['brightness_pct'] == 50
- assert msg['header']['name'] == 'Response'
+ if deactivate_service:
+ yield from assert_request_calls_service(
+ 'Alexa.SceneController', 'Deactivate', endpoint,
+ deactivate_service, hass,
+ response_type='DeactivationStarted')
+ cause_type = response['event']['payload']['cause']['type']
+ assert cause_type == 'VOICE_INTERACTION'
+ assert 'timestamp' in response['event']['payload']
@asyncio.coroutine
@@ -753,475 +1076,55 @@ def test_api_increase_color_temp(hass, result, initial):
@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['scene', 'group', 'script'])
-def test_api_activate(hass, domain):
- """Test api activate process."""
- request = get_new_request(
- 'Alexa.SceneController', 'Activate', '{}#test'.format(domain))
-
- # setup test devices
+def test_report_lock_state(hass):
+ """Test LockController implements lockState property."""
hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
+ 'lock.locked', STATE_LOCKED, {})
+ hass.states.async_set(
+ 'lock.unlocked', STATE_UNLOCKED, {})
+ hass.states.async_set(
+ 'lock.unknown', STATE_UNKNOWN, {})
- if domain == 'group':
- call_domain = 'homeassistant'
- else:
- call_domain = domain
+ properties = yield from reported_properties(hass, 'lock.locked')
+ properties.assert_equal('Alexa.LockController', 'lockState', 'LOCKED')
- call = async_mock_service(hass, call_domain, 'turn_on')
+ properties = yield from reported_properties(hass, 'lock.unlocked')
+ properties.assert_equal('Alexa.LockController', 'lockState', 'UNLOCKED')
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'ActivationStarted'
- assert msg['payload']['cause']['type'] == 'VOICE_INTERACTION'
- assert 'timestamp' in msg['payload']
+ properties = yield from reported_properties(hass, 'lock.unknown')
+ properties.assert_equal('Alexa.LockController', 'lockState', 'JAMMED')
@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['group', 'script'])
-def test_api_deactivate(hass, domain):
- """Test api deactivate process."""
- request = get_new_request(
- 'Alexa.SceneController', 'Deactivate', '{}#test'.format(domain))
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- if domain == 'group':
- call_domain = 'homeassistant'
- else:
- call_domain = domain
-
- call = async_mock_service(hass, call_domain, 'turn_off')
+def reported_properties(hass, endpoint):
+ """Use ReportState to get properties and return them.
+ The result is a _ReportedProperties instance, which has methods to make
+ assertions about the properties.
+ """
+ request = get_new_request('Alexa', 'ReportState', endpoint)
msg = yield from smart_home.async_handle_message(
hass, DEFAULT_CONFIG, request)
yield from hass.async_block_till_done()
+ return _ReportedProperties(msg['context']['properties'])
- assert 'event' in msg
- msg = msg['event']
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'DeactivationStarted'
- assert msg['payload']['cause']['type'] == 'VOICE_INTERACTION'
- assert 'timestamp' in msg['payload']
+class _ReportedProperties(object):
+ def __init__(self, properties):
+ self.properties = properties
+ def assert_equal(self, namespace, name, value):
+ """Assert a property is equal to a given value."""
+ for prop in self.properties:
+ if prop['namespace'] == namespace and prop['name'] == name:
+ assert prop['value'] == value
+ return prop
-@asyncio.coroutine
-def test_api_set_percentage_fan(hass):
- """Test api set percentage for fan process."""
- request = get_new_request(
- 'Alexa.PercentageController', 'SetPercentage', 'fan#test_2')
-
- # add payload
- request['directive']['payload']['percentage'] = '50'
-
- # setup test devices
- hass.states.async_set(
- 'fan.test_2', 'off', {'friendly_name': "Test fan"})
-
- call_fan = async_mock_service(hass, 'fan', 'set_speed')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call_fan) == 1
- assert call_fan[0].data['entity_id'] == 'fan.test_2'
- assert call_fan[0].data['speed'] == 'medium'
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-def test_api_set_percentage_cover(hass):
- """Test api set percentage for cover process."""
- request = get_new_request(
- 'Alexa.PercentageController', 'SetPercentage', 'cover#test')
-
- # add payload
- request['directive']['payload']['percentage'] = '50'
-
- # setup test devices
- hass.states.async_set(
- 'cover.test', 'closed', {
- 'friendly_name': "Test cover"
- })
-
- call_cover = async_mock_service(hass, 'cover', 'set_cover_position')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call_cover) == 1
- assert call_cover[0].data['entity_id'] == 'cover.test'
- assert call_cover[0].data['position'] == 50
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize(
- "result,adjust", [('high', '-5'), ('off', '5'), ('low', '-80')])
-def test_api_adjust_percentage_fan(hass, result, adjust):
- """Test api adjust percentage for fan process."""
- request = get_new_request(
- 'Alexa.PercentageController', 'AdjustPercentage', 'fan#test_2')
-
- # add payload
- request['directive']['payload']['percentageDelta'] = adjust
-
- # setup test devices
- hass.states.async_set(
- 'fan.test_2', 'on', {
- 'friendly_name': "Test fan 2", 'speed': 'high'
- })
-
- call_fan = async_mock_service(hass, 'fan', 'set_speed')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call_fan) == 1
- assert call_fan[0].data['entity_id'] == 'fan.test_2'
- assert call_fan[0].data['speed'] == result
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize(
- "result,adjust", [(25, '-5'), (35, '5'), (0, '-80')])
-def test_api_adjust_percentage_cover(hass, result, adjust):
- """Test api adjust percentage for cover process."""
- request = get_new_request(
- 'Alexa.PercentageController', 'AdjustPercentage', 'cover#test')
-
- # add payload
- request['directive']['payload']['percentageDelta'] = adjust
-
- # setup test devices
- hass.states.async_set(
- 'cover.test', 'closed', {
- 'friendly_name': "Test cover",
- 'position': 30
- })
-
- call_cover = async_mock_service(hass, 'cover', 'set_cover_position')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call_cover) == 1
- assert call_cover[0].data['entity_id'] == 'cover.test'
- assert call_cover[0].data['position'] == result
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['lock'])
-def test_api_lock(hass, domain):
- """Test api lock process."""
- request = get_new_request(
- 'Alexa.LockController', 'Lock', '{}#test'.format(domain))
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call = async_mock_service(hass, domain, 'lock')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['media_player'])
-def test_api_play(hass, domain):
- """Test api play process."""
- request = get_new_request(
- 'Alexa.PlaybackController', 'Play', '{}#test'.format(domain))
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call = async_mock_service(hass, domain, 'media_play')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['media_player'])
-def test_api_pause(hass, domain):
- """Test api pause process."""
- request = get_new_request(
- 'Alexa.PlaybackController', 'Pause', '{}#test'.format(domain))
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call = async_mock_service(hass, domain, 'media_pause')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['media_player'])
-def test_api_stop(hass, domain):
- """Test api stop process."""
- request = get_new_request(
- 'Alexa.PlaybackController', 'Stop', '{}#test'.format(domain))
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call = async_mock_service(hass, domain, 'media_stop')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['media_player'])
-def test_api_next(hass, domain):
- """Test api next process."""
- request = get_new_request(
- 'Alexa.PlaybackController', 'Next', '{}#test'.format(domain))
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call = async_mock_service(hass, domain, 'media_next_track')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['media_player'])
-def test_api_previous(hass, domain):
- """Test api previous process."""
- request = get_new_request(
- 'Alexa.PlaybackController', 'Previous', '{}#test'.format(domain))
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call = async_mock_service(hass, domain, 'media_previous_track')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-def test_api_set_volume(hass):
- """Test api set volume process."""
- request = get_new_request(
- 'Alexa.Speaker', 'SetVolume', 'media_player#test')
-
- # add payload
- request['directive']['payload']['volume'] = 50
-
- # setup test devices
- hass.states.async_set(
- 'media_player.test', 'off', {
- 'friendly_name': "Test media player", 'volume_level': 0
- })
-
- call_media_player = async_mock_service(hass, 'media_player', 'volume_set')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call_media_player) == 1
- assert call_media_player[0].data['entity_id'] == 'media_player.test'
- assert call_media_player[0].data['volume_level'] == 0.5
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize(
- "result,adjust", [(0.7, '-5'), (0.8, '5'), (0, '-80')])
-def test_api_adjust_volume(hass, result, adjust):
- """Test api adjust volume process."""
- request = get_new_request(
- 'Alexa.Speaker', 'AdjustVolume', 'media_player#test')
-
- # add payload
- request['directive']['payload']['volume'] = adjust
-
- # setup test devices
- hass.states.async_set(
- 'media_player.test', 'off', {
- 'friendly_name': "Test media player", 'volume_level': 0.75
- })
-
- call_media_player = async_mock_service(hass, 'media_player', 'volume_set')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call_media_player) == 1
- assert call_media_player[0].data['entity_id'] == 'media_player.test'
- assert call_media_player[0].data['volume_level'] == result
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-@pytest.mark.parametrize("domain", ['media_player'])
-def test_api_mute(hass, domain):
- """Test api mute process."""
- request = get_new_request(
- 'Alexa.Speaker', 'SetMute', '{}#test'.format(domain))
-
- request['directive']['payload']['mute'] = True
-
- # setup test devices
- hass.states.async_set(
- '{}.test'.format(domain), 'off', {
- 'friendly_name': "Test {}".format(domain)
- })
-
- call = async_mock_service(hass, domain, 'volume_mute')
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- assert 'event' in msg
- msg = msg['event']
-
- assert len(call) == 1
- assert call[0].data['entity_id'] == '{}.test'.format(domain)
- assert msg['header']['name'] == 'Response'
-
-
-@asyncio.coroutine
-def test_api_report_temperature(hass):
- """Test API ReportState response for a temperature sensor."""
- request = get_new_request('Alexa', 'ReportState', 'sensor#test')
-
- # setup test devices
- hass.states.async_set(
- 'sensor.test', '42', {
- 'friendly_name': 'test sensor',
- CONF_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT,
- })
-
- msg = yield from smart_home.async_handle_message(
- hass, DEFAULT_CONFIG, request)
- yield from hass.async_block_till_done()
-
- header = msg['event']['header']
- assert header['namespace'] == 'Alexa'
- assert header['name'] == 'StateReport'
-
- properties = msg['context']['properties']
- assert len(properties) == 1
- prop = properties[0]
- assert prop['namespace'] == 'Alexa.TemperatureSensor'
- assert prop['name'] == 'temperature'
- assert prop['value'] == {'value': 42.0, 'scale': 'FAHRENHEIT'}
+ assert False, 'property %s:%s not in %r' % (
+ namespace,
+ name,
+ self.properties,
+ )
@asyncio.coroutine
@@ -1275,7 +1178,7 @@ def test_unsupported_domain(hass):
assert 'event' in msg
msg = msg['event']
- assert len(msg['payload']['endpoints']) == 0
+ assert not msg['payload']['endpoints']
@asyncio.coroutine
@@ -1318,3 +1221,38 @@ def test_http_api_disabled(hass, test_client):
response = yield from do_http_discovery(config, hass, test_client)
assert response.status == 404
+
+
+@asyncio.coroutine
+@pytest.mark.parametrize(
+ "domain,payload,source_list,idx", [
+ ('media_player', 'GAME CONSOLE', ['tv', 'game console'], 1),
+ ('media_player', 'SATELLITE TV', ['satellite-tv', 'game console'], 0),
+ ('media_player', 'SATELLITE TV', ['satellite_tv', 'game console'], 0),
+ ('media_player', 'BAD DEVICE', ['satellite_tv', 'game console'], None),
+ ]
+)
+def test_api_select_input(hass, domain, payload, source_list, idx):
+ """Test api set input process."""
+ hass.states.async_set(
+ 'media_player.test', 'off', {
+ 'friendly_name': "Test media player",
+ 'source': 'unknown',
+ 'source_list': source_list,
+ })
+
+ # test where no source matches
+ if idx is None:
+ yield from assert_request_fails(
+ 'Alexa.InputController', 'SelectInput', 'media_player#test',
+ 'media_player.select_source',
+ hass,
+ payload={'input': payload})
+ return
+
+ call, _ = yield from assert_request_calls_service(
+ 'Alexa.InputController', 'SelectInput', 'media_player#test',
+ 'media_player.select_source',
+ hass,
+ payload={'input': payload})
+ assert call.data['source'] == source_list[idx]
diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py
index 58cfd2cbd70..63ca4b5cd1a 100644
--- a/tests/components/automation/test_numeric_state.py
+++ b/tests/components/automation/test_numeric_state.py
@@ -736,7 +736,7 @@ class TestAutomationNumericState(unittest.TestCase):
self.hass.block_till_done()
self.assertEqual(0, len(self.calls))
- def test_if_not_fires_on_entities_change_with_for_afte_stop(self):
+ def test_if_not_fires_on_entities_change_with_for_after_stop(self):
"""Test for not firing on entities change with for after stop."""
assert setup_component(self.hass, automation.DOMAIN, {
automation.DOMAIN: {
diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py
index bf54d24492a..22c84b88935 100644
--- a/tests/components/automation/test_state.py
+++ b/tests/components/automation/test_state.py
@@ -198,7 +198,7 @@ class TestAutomationState(unittest.TestCase):
automation.DOMAIN: {
'trigger': {
'platform': 'state',
- 'entity_id': 'test.anoter_entity',
+ 'entity_id': 'test.another_entity',
},
'action': {
'service': 'test.automation'
@@ -468,7 +468,7 @@ class TestAutomationState(unittest.TestCase):
self.assertEqual(1, len(self.calls))
def test_if_fires_on_for_condition(self):
- """Test for firing if contition is on."""
+ """Test for firing if condition is on."""
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=10)
with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow:
@@ -504,7 +504,7 @@ class TestAutomationState(unittest.TestCase):
self.assertEqual(1, len(self.calls))
def test_if_fires_on_for_condition_attribute_change(self):
- """Test for firing if contition is on with attribute change."""
+ """Test for firing if condition is on with attribute change."""
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=4)
point3 = point1 + timedelta(seconds=8)
diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py
index ac1d7bc5acf..355d088719f 100644
--- a/tests/components/automation/test_sun.py
+++ b/tests/components/automation/test_sun.py
@@ -127,7 +127,7 @@ class TestAutomationSun(unittest.TestCase):
self.assertEqual('sun - sunset - 0:30:00', self.calls[0].data['some'])
def test_sunrise_trigger_with_offset(self):
- """Test the runrise trigger with offset."""
+ """Test the sunrise trigger with offset."""
now = datetime(2015, 9, 13, 23, tzinfo=dt_util.UTC)
trigger_time = datetime(2015, 9, 16, 13, 30, tzinfo=dt_util.UTC)
diff --git a/tests/components/binary_sensor/test_aurora.py b/tests/components/binary_sensor/test_aurora.py
index ed68d23905f..1198aeb1357 100644
--- a/tests/components/binary_sensor/test_aurora.py
+++ b/tests/components/binary_sensor/test_aurora.py
@@ -28,7 +28,7 @@ class TestAuroraSensorSetUp(unittest.TestCase):
def test_setup_and_initial_state(self, mock_req):
"""Test that the component is created and initialized as expected."""
uri = re.compile(
- "http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt"
+ r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt"
)
mock_req.get(uri, text=load_fixture('aurora.txt'))
@@ -66,7 +66,7 @@ class TestAuroraSensorSetUp(unittest.TestCase):
def test_custom_threshold_works(self, mock_req):
"""Test that the config can take a custom forecast threshold."""
uri = re.compile(
- "http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt"
+ r"http://services\.swpc\.noaa\.gov/text/aurora-nowcast-map\.txt"
)
mock_req.get(uri, text=load_fixture('aurora.txt'))
diff --git a/tests/components/binary_sensor/test_mqtt.py b/tests/components/binary_sensor/test_mqtt.py
index 396020561ac..9b5cf7aa736 100644
--- a/tests/components/binary_sensor/test_mqtt.py
+++ b/tests/components/binary_sensor/test_mqtt.py
@@ -1,13 +1,15 @@
"""The tests for the MQTT binary sensor platform."""
import unittest
+import homeassistant.core as ha
from homeassistant.setup import setup_component
import homeassistant.components.binary_sensor as binary_sensor
-from homeassistant.const import (STATE_OFF, STATE_ON,
- STATE_UNAVAILABLE)
-from tests.common import (
- get_test_home_assistant, mock_mqtt_component, fire_mqtt_message)
+from homeassistant.const import STATE_OFF, STATE_ON
+from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE
+
+from tests.common import get_test_home_assistant, fire_mqtt_message
+from tests.common import mock_component, mock_mqtt_component
class TestSensorMQTT(unittest.TestCase):
@@ -141,3 +143,64 @@ class TestSensorMQTT(unittest.TestCase):
state = self.hass.states.get('binary_sensor.test')
self.assertEqual(STATE_UNAVAILABLE, state.state)
+
+ def test_force_update_disabled(self):
+ """Test force update option."""
+ mock_component(self.hass, 'mqtt')
+ assert setup_component(self.hass, binary_sensor.DOMAIN, {
+ binary_sensor.DOMAIN: {
+ 'platform': 'mqtt',
+ 'name': 'test',
+ 'state_topic': 'test-topic',
+ 'payload_on': 'ON',
+ 'payload_off': 'OFF'
+ }
+ })
+
+ events = []
+
+ @ha.callback
+ def callback(event):
+ """Verify event got called."""
+ events.append(event)
+
+ self.hass.bus.listen(EVENT_STATE_CHANGED, callback)
+
+ fire_mqtt_message(self.hass, 'test-topic', 'ON')
+ self.hass.block_till_done()
+ self.assertEqual(1, len(events))
+
+ fire_mqtt_message(self.hass, 'test-topic', 'ON')
+ self.hass.block_till_done()
+ self.assertEqual(1, len(events))
+
+ def test_force_update_enabled(self):
+ """Test force update option."""
+ mock_component(self.hass, 'mqtt')
+ assert setup_component(self.hass, binary_sensor.DOMAIN, {
+ binary_sensor.DOMAIN: {
+ 'platform': 'mqtt',
+ 'name': 'test',
+ 'state_topic': 'test-topic',
+ 'payload_on': 'ON',
+ 'payload_off': 'OFF',
+ 'force_update': True
+ }
+ })
+
+ events = []
+
+ @ha.callback
+ def callback(event):
+ """Verify event got called."""
+ events.append(event)
+
+ self.hass.bus.listen(EVENT_STATE_CHANGED, callback)
+
+ fire_mqtt_message(self.hass, 'test-topic', 'ON')
+ self.hass.block_till_done()
+ self.assertEqual(1, len(events))
+
+ fire_mqtt_message(self.hass, 'test-topic', 'ON')
+ self.hass.block_till_done()
+ self.assertEqual(2, len(events))
diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py
index 481226c4f73..c47f23bf902 100644
--- a/tests/components/binary_sensor/test_template.py
+++ b/tests/components/binary_sensor/test_template.py
@@ -98,13 +98,75 @@ class TestBinarySensorTemplate(unittest.TestCase):
}
})
+ def test_icon_template(self):
+ """Test icon template."""
+ with assert_setup_component(1):
+ assert setup.setup_component(self.hass, 'binary_sensor', {
+ 'binary_sensor': {
+ 'platform': 'template',
+ 'sensors': {
+ 'test_template_sensor': {
+ 'value_template': "State",
+ 'icon_template':
+ "{% if "
+ "states.binary_sensor.test_state.state == "
+ "'Works' %}"
+ "mdi:check"
+ "{% endif %}"
+ }
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.test_template_sensor')
+ assert state.attributes.get('icon') == ''
+
+ self.hass.states.set('binary_sensor.test_state', 'Works')
+ self.hass.block_till_done()
+ state = self.hass.states.get('binary_sensor.test_template_sensor')
+ assert state.attributes['icon'] == 'mdi:check'
+
+ def test_entity_picture_template(self):
+ """Test entity_picture template."""
+ with assert_setup_component(1):
+ assert setup.setup_component(self.hass, 'binary_sensor', {
+ 'binary_sensor': {
+ 'platform': 'template',
+ 'sensors': {
+ 'test_template_sensor': {
+ 'value_template': "State",
+ 'entity_picture_template':
+ "{% if "
+ "states.binary_sensor.test_state.state == "
+ "'Works' %}"
+ "/local/sensor.png"
+ "{% endif %}"
+ }
+ }
+ }
+ })
+
+ self.hass.start()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.test_template_sensor')
+ assert state.attributes.get('entity_picture') == ''
+
+ self.hass.states.set('binary_sensor.test_state', 'Works')
+ self.hass.block_till_done()
+ state = self.hass.states.get('binary_sensor.test_template_sensor')
+ assert state.attributes['entity_picture'] == '/local/sensor.png'
+
def test_attributes(self):
""""Test the attributes."""
vs = run_callback_threadsafe(
self.hass.loop, template.BinarySensorTemplate,
self.hass, 'parent', 'Parent', 'motion',
- template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL,
- None, None
+ template_hlpr.Template('{{ 1 > 1 }}', self.hass),
+ None, None, MATCH_ALL, None, None
).result()
self.assertFalse(vs.should_poll)
self.assertEqual('motion', vs.device_class)
@@ -156,8 +218,8 @@ class TestBinarySensorTemplate(unittest.TestCase):
vs = run_callback_threadsafe(
self.hass.loop, template.BinarySensorTemplate,
self.hass, 'parent', 'Parent', 'motion',
- template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL,
- None, None
+ template_hlpr.Template('{{ 1 > 1 }}', self.hass),
+ None, None, MATCH_ALL, None, None
).result()
mock_render.side_effect = TemplateError('foo')
run_callback_threadsafe(self.hass.loop, vs.async_check_state).result()
diff --git a/tests/components/binary_sensor/test_threshold.py b/tests/components/binary_sensor/test_threshold.py
index 38573b295d3..926b3c67983 100644
--- a/tests/components/binary_sensor/test_threshold.py
+++ b/tests/components/binary_sensor/test_threshold.py
@@ -333,3 +333,63 @@ class TestThresholdSensor(unittest.TestCase):
self.assertEqual('unknown', state.attributes.get('position'))
assert state.state == 'off'
+
+ def test_sensor_lower_zero_threshold(self):
+ """Test if a lower threshold of zero is set."""
+ config = {
+ 'binary_sensor': {
+ 'platform': 'threshold',
+ 'lower': '0',
+ 'entity_id': 'sensor.test_monitored',
+ }
+ }
+
+ assert setup_component(self.hass, 'binary_sensor', config)
+
+ self.hass.states.set('sensor.test_monitored', 16)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('lower', state.attributes.get('type'))
+ self.assertEqual(float(config['binary_sensor']['lower']),
+ state.attributes.get('lower'))
+
+ assert state.state == 'off'
+
+ self.hass.states.set('sensor.test_monitored', -3)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ assert state.state == 'on'
+
+ def test_sensor_upper_zero_threshold(self):
+ """Test if an upper threshold of zero is set."""
+ config = {
+ 'binary_sensor': {
+ 'platform': 'threshold',
+ 'upper': '0',
+ 'entity_id': 'sensor.test_monitored',
+ }
+ }
+
+ assert setup_component(self.hass, 'binary_sensor', config)
+
+ self.hass.states.set('sensor.test_monitored', -10)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('upper', state.attributes.get('type'))
+ self.assertEqual(float(config['binary_sensor']['upper']),
+ state.attributes.get('upper'))
+
+ assert state.state == 'off'
+
+ self.hass.states.set('sensor.test_monitored', 2)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ assert state.state == 'on'
diff --git a/tests/components/binary_sensor/test_vultr.py b/tests/components/binary_sensor/test_vultr.py
index 91d5da34901..a13944aef9f 100644
--- a/tests/components/binary_sensor/test_vultr.py
+++ b/tests/components/binary_sensor/test_vultr.py
@@ -78,7 +78,7 @@ class TestVultrBinarySensorSetup(unittest.TestCase):
for device in self.DEVICES:
- # Test pre data retieval
+ # Test pre data retrieval
if device.subscription == '555555':
self.assertEqual('Vultr {}', device.name)
diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py
index 1de825efd99..62c8ea8854f 100644
--- a/tests/components/calendar/test_google.py
+++ b/tests/components/calendar/test_google.py
@@ -1,423 +1,423 @@
-"""The tests for the google calendar component."""
-# pylint: disable=protected-access
-import logging
-import unittest
-from unittest.mock import patch
-
-import pytest
-
-import homeassistant.components.calendar as calendar_base
-import homeassistant.components.calendar.google as calendar
-import homeassistant.util.dt as dt_util
-from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON
-from homeassistant.helpers.template import DATE_STR_FORMAT
-from tests.common import get_test_home_assistant
-
-TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}}
-
-_LOGGER = logging.getLogger(__name__)
-
-
-class TestComponentsGoogleCalendar(unittest.TestCase):
- """Test the Google calendar."""
-
- hass = None # HomeAssistant
-
- # pylint: disable=invalid-name
- def setUp(self):
- """Setup things to be run when tests are started."""
- self.hass = get_test_home_assistant()
-
- # Set our timezone to CST/Regina so we can check calculations
- # This keeps UTC-6 all year round
- dt_util.set_default_time_zone(dt_util.get_time_zone('America/Regina'))
-
- # pylint: disable=invalid-name
- def tearDown(self):
- """Stop everything that was started."""
- dt_util.set_default_time_zone(dt_util.get_time_zone('UTC'))
-
- self.hass.stop()
-
- @patch('homeassistant.components.calendar.google.GoogleCalendarData')
- def test_all_day_event(self, mock_next_event):
- """Test that we can create an event trigger on device."""
- week_from_today = dt_util.dt.date.today() \
- + dt_util.dt.timedelta(days=7)
- event = {
- 'summary': 'Test All Day Event',
- 'start': {
- 'date': week_from_today.isoformat()
- },
- 'end': {
- 'date': (week_from_today + dt_util.dt.timedelta(days=1))
- .isoformat()
- },
- 'location': 'Test Cases',
- 'description': 'We\'re just testing that all day events get setup '
- 'correctly',
- 'kind': 'calendar#event',
- 'created': '2016-06-23T16:37:57.000Z',
- 'transparency': 'transparent',
- 'updated': '2016-06-24T01:57:21.045Z',
- 'reminders': {'useDefault': True},
- 'organizer': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True
- },
- 'sequence': 0,
- 'creator': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True
- },
- 'id': '_c8rinwq863h45qnucyoi43ny8',
- 'etag': '"2933466882090000"',
- 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
- 'iCalUID': 'cydrevtfuybguinhomj@google.com',
- 'status': 'confirmed'
- }
-
- mock_next_event.return_value.event = event
-
- device_name = 'Test All Day'
-
- cal = calendar.GoogleCalendarEventDevice(self.hass, None,
- '', {'name': device_name})
-
- self.assertEqual(cal.name, device_name)
-
- self.assertEqual(cal.state, STATE_OFF)
-
- self.assertFalse(cal.offset_reached())
-
- self.assertEqual(cal.device_state_attributes, {
- 'message': event['summary'],
- 'all_day': True,
- 'offset_reached': False,
- 'start_time': '{} 00:00:00'.format(event['start']['date']),
- 'end_time': '{} 00:00:00'.format(event['end']['date']),
- 'location': event['location'],
- 'description': event['description']
- })
-
- @patch('homeassistant.components.calendar.google.GoogleCalendarData')
- def test_future_event(self, mock_next_event):
- """Test that we can create an event trigger on device."""
- one_hour_from_now = dt_util.now() \
- + dt_util.dt.timedelta(minutes=30)
- event = {
- 'start': {
- 'dateTime': one_hour_from_now.isoformat()
- },
- 'end': {
- 'dateTime': (one_hour_from_now
- + dt_util.dt.timedelta(minutes=60))
- .isoformat()
- },
- 'summary': 'Test Event in 30 minutes',
- 'reminders': {'useDefault': True},
- 'id': 'aioehgni435lihje',
- 'status': 'confirmed',
- 'updated': '2016-11-05T15:52:07.329Z',
- 'organizer': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True,
- },
- 'created': '2016-11-05T15:52:07.000Z',
- 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
- 'sequence': 0,
- 'creator': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- },
- 'etag': '"2956722254658000"',
- 'kind': 'calendar#event',
- 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
- }
- mock_next_event.return_value.event = event
-
- device_name = 'Test Future Event'
- device_id = 'test_future_event'
-
- cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
- {'name': device_name})
-
- self.assertEqual(cal.name, device_name)
-
- self.assertEqual(cal.state, STATE_OFF)
-
- self.assertFalse(cal.offset_reached())
-
- self.assertEqual(cal.device_state_attributes, {
- 'message': event['summary'],
- 'all_day': False,
- 'offset_reached': False,
- 'start_time': one_hour_from_now.strftime(DATE_STR_FORMAT),
- 'end_time':
- (one_hour_from_now + dt_util.dt.timedelta(minutes=60))
- .strftime(DATE_STR_FORMAT),
- 'location': '',
- 'description': ''
- })
-
- @patch('homeassistant.components.calendar.google.GoogleCalendarData')
- def test_in_progress_event(self, mock_next_event):
- """Test that we can create an event trigger on device."""
- middle_of_event = dt_util.now() \
- - dt_util.dt.timedelta(minutes=30)
- event = {
- 'start': {
- 'dateTime': middle_of_event.isoformat()
- },
- 'end': {
- 'dateTime': (middle_of_event + dt_util.dt
- .timedelta(minutes=60))
- .isoformat()
- },
- 'summary': 'Test Event in Progress',
- 'reminders': {'useDefault': True},
- 'id': 'aioehgni435lihje',
- 'status': 'confirmed',
- 'updated': '2016-11-05T15:52:07.329Z',
- 'organizer': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True,
- },
- 'created': '2016-11-05T15:52:07.000Z',
- 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
- 'sequence': 0,
- 'creator': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- },
- 'etag': '"2956722254658000"',
- 'kind': 'calendar#event',
- 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
- }
-
- mock_next_event.return_value.event = event
-
- device_name = 'Test Event in Progress'
- device_id = 'test_event_in_progress'
-
- cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
- {'name': device_name})
-
- self.assertEqual(cal.name, device_name)
-
- self.assertEqual(cal.state, STATE_ON)
-
- self.assertFalse(cal.offset_reached())
-
- self.assertEqual(cal.device_state_attributes, {
- 'message': event['summary'],
- 'all_day': False,
- 'offset_reached': False,
- 'start_time': middle_of_event.strftime(DATE_STR_FORMAT),
- 'end_time':
- (middle_of_event + dt_util.dt.timedelta(minutes=60))
- .strftime(DATE_STR_FORMAT),
- 'location': '',
- 'description': ''
- })
-
- @patch('homeassistant.components.calendar.google.GoogleCalendarData')
- def test_offset_in_progress_event(self, mock_next_event):
- """Test that we can create an event trigger on device."""
- middle_of_event = dt_util.now() \
- + dt_util.dt.timedelta(minutes=14)
- event_summary = 'Test Event in Progress'
- event = {
- 'start': {
- 'dateTime': middle_of_event.isoformat()
- },
- 'end': {
- 'dateTime': (middle_of_event + dt_util.dt
- .timedelta(minutes=60))
- .isoformat()
- },
- 'summary': '{} !!-15'.format(event_summary),
- 'reminders': {'useDefault': True},
- 'id': 'aioehgni435lihje',
- 'status': 'confirmed',
- 'updated': '2016-11-05T15:52:07.329Z',
- 'organizer': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True,
- },
- 'created': '2016-11-05T15:52:07.000Z',
- 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
- 'sequence': 0,
- 'creator': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- },
- 'etag': '"2956722254658000"',
- 'kind': 'calendar#event',
- 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
- }
-
- mock_next_event.return_value.event = event
-
- device_name = 'Test Event in Progress'
- device_id = 'test_event_in_progress'
-
- cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
- {'name': device_name})
-
- self.assertEqual(cal.name, device_name)
-
- self.assertEqual(cal.state, STATE_OFF)
-
- self.assertTrue(cal.offset_reached())
-
- self.assertEqual(cal.device_state_attributes, {
- 'message': event_summary,
- 'all_day': False,
- 'offset_reached': True,
- 'start_time': middle_of_event.strftime(DATE_STR_FORMAT),
- 'end_time':
- (middle_of_event + dt_util.dt.timedelta(minutes=60))
- .strftime(DATE_STR_FORMAT),
- 'location': '',
- 'description': ''
- })
-
- @pytest.mark.skip
- @patch('homeassistant.components.calendar.google.GoogleCalendarData')
- def test_all_day_offset_in_progress_event(self, mock_next_event):
- """Test that we can create an event trigger on device."""
- tomorrow = dt_util.dt.date.today() \
- + dt_util.dt.timedelta(days=1)
-
- event_summary = 'Test All Day Event Offset In Progress'
- event = {
- 'summary': '{} !!-25:0'.format(event_summary),
- 'start': {
- 'date': tomorrow.isoformat()
- },
- 'end': {
- 'date': (tomorrow + dt_util.dt.timedelta(days=1))
- .isoformat()
- },
- 'location': 'Test Cases',
- 'description': 'We\'re just testing that all day events get setup '
- 'correctly',
- 'kind': 'calendar#event',
- 'created': '2016-06-23T16:37:57.000Z',
- 'transparency': 'transparent',
- 'updated': '2016-06-24T01:57:21.045Z',
- 'reminders': {'useDefault': True},
- 'organizer': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True
- },
- 'sequence': 0,
- 'creator': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True
- },
- 'id': '_c8rinwq863h45qnucyoi43ny8',
- 'etag': '"2933466882090000"',
- 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
- 'iCalUID': 'cydrevtfuybguinhomj@google.com',
- 'status': 'confirmed'
- }
-
- mock_next_event.return_value.event = event
-
- device_name = 'Test All Day Offset In Progress'
- device_id = 'test_all_day_offset_in_progress'
-
- cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
- {'name': device_name})
-
- self.assertEqual(cal.name, device_name)
-
- self.assertEqual(cal.state, STATE_OFF)
-
- self.assertTrue(cal.offset_reached())
-
- self.assertEqual(cal.device_state_attributes, {
- 'message': event_summary,
- 'all_day': True,
- 'offset_reached': True,
- 'start_time': '{} 06:00:00'.format(event['start']['date']),
- 'end_time': '{} 06:00:00'.format(event['end']['date']),
- 'location': event['location'],
- 'description': event['description']
- })
-
- @patch('homeassistant.components.calendar.google.GoogleCalendarData')
- def test_all_day_offset_event(self, mock_next_event):
- """Test that we can create an event trigger on device."""
- tomorrow = dt_util.dt.date.today() \
- + dt_util.dt.timedelta(days=2)
-
- offset_hours = (1 + dt_util.now().hour)
- event_summary = 'Test All Day Event Offset'
- event = {
- 'summary': '{} !!-{}:0'.format(event_summary, offset_hours),
- 'start': {
- 'date': tomorrow.isoformat()
- },
- 'end': {
- 'date': (tomorrow + dt_util.dt.timedelta(days=1))
- .isoformat()
- },
- 'location': 'Test Cases',
- 'description': 'We\'re just testing that all day events get setup '
- 'correctly',
- 'kind': 'calendar#event',
- 'created': '2016-06-23T16:37:57.000Z',
- 'transparency': 'transparent',
- 'updated': '2016-06-24T01:57:21.045Z',
- 'reminders': {'useDefault': True},
- 'organizer': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True
- },
- 'sequence': 0,
- 'creator': {
- 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
- 'displayName': 'Organizer Name',
- 'self': True
- },
- 'id': '_c8rinwq863h45qnucyoi43ny8',
- 'etag': '"2933466882090000"',
- 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
- 'iCalUID': 'cydrevtfuybguinhomj@google.com',
- 'status': 'confirmed'
- }
-
- mock_next_event.return_value.event = event
-
- device_name = 'Test All Day Offset'
- device_id = 'test_all_day_offset'
-
- cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
- {'name': device_name})
-
- self.assertEqual(cal.name, device_name)
-
- self.assertEqual(cal.state, STATE_OFF)
-
- self.assertFalse(cal.offset_reached())
-
- self.assertEqual(cal.device_state_attributes, {
- 'message': event_summary,
- 'all_day': True,
- 'offset_reached': False,
- 'start_time': '{} 00:00:00'.format(event['start']['date']),
- 'end_time': '{} 00:00:00'.format(event['end']['date']),
- 'location': event['location'],
- 'description': event['description']
- })
+"""The tests for the google calendar component."""
+# pylint: disable=protected-access
+import logging
+import unittest
+from unittest.mock import patch
+
+import pytest
+
+import homeassistant.components.calendar as calendar_base
+import homeassistant.components.calendar.google as calendar
+import homeassistant.util.dt as dt_util
+from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON
+from homeassistant.helpers.template import DATE_STR_FORMAT
+from tests.common import get_test_home_assistant
+
+TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}}
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TestComponentsGoogleCalendar(unittest.TestCase):
+ """Test the Google calendar."""
+
+ hass = None # HomeAssistant
+
+ # pylint: disable=invalid-name
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ # Set our timezone to CST/Regina so we can check calculations
+ # This keeps UTC-6 all year round
+ dt_util.set_default_time_zone(dt_util.get_time_zone('America/Regina'))
+
+ # pylint: disable=invalid-name
+ def tearDown(self):
+ """Stop everything that was started."""
+ dt_util.set_default_time_zone(dt_util.get_time_zone('UTC'))
+
+ self.hass.stop()
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_all_day_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ week_from_today = dt_util.dt.date.today() \
+ + dt_util.dt.timedelta(days=7)
+ event = {
+ 'summary': 'Test All Day Event',
+ 'start': {
+ 'date': week_from_today.isoformat()
+ },
+ 'end': {
+ 'date': (week_from_today + dt_util.dt.timedelta(days=1))
+ .isoformat()
+ },
+ 'location': 'Test Cases',
+ 'description': 'We\'re just testing that all day events get setup '
+ 'correctly',
+ 'kind': 'calendar#event',
+ 'created': '2016-06-23T16:37:57.000Z',
+ 'transparency': 'transparent',
+ 'updated': '2016-06-24T01:57:21.045Z',
+ 'reminders': {'useDefault': True},
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'id': '_c8rinwq863h45qnucyoi43ny8',
+ 'etag': '"2933466882090000"',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ 'iCalUID': 'cydrevtfuybguinhomj@google.com',
+ 'status': 'confirmed'
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test All Day'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None,
+ '', {'name': device_name})
+
+ self.assertEqual(cal.name, device_name)
+
+ self.assertEqual(cal.state, STATE_OFF)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEqual(cal.device_state_attributes, {
+ 'message': event['summary'],
+ 'all_day': True,
+ 'offset_reached': False,
+ 'start_time': '{} 00:00:00'.format(event['start']['date']),
+ 'end_time': '{} 00:00:00'.format(event['end']['date']),
+ 'location': event['location'],
+ 'description': event['description']
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_future_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ one_hour_from_now = dt_util.now() \
+ + dt_util.dt.timedelta(minutes=30)
+ event = {
+ 'start': {
+ 'dateTime': one_hour_from_now.isoformat()
+ },
+ 'end': {
+ 'dateTime': (one_hour_from_now
+ + dt_util.dt.timedelta(minutes=60))
+ .isoformat()
+ },
+ 'summary': 'Test Event in 30 minutes',
+ 'reminders': {'useDefault': True},
+ 'id': 'aioehgni435lihje',
+ 'status': 'confirmed',
+ 'updated': '2016-11-05T15:52:07.329Z',
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True,
+ },
+ 'created': '2016-11-05T15:52:07.000Z',
+ 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ },
+ 'etag': '"2956722254658000"',
+ 'kind': 'calendar#event',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ }
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test Future Event'
+ device_id = 'test_future_event'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEqual(cal.name, device_name)
+
+ self.assertEqual(cal.state, STATE_OFF)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEqual(cal.device_state_attributes, {
+ 'message': event['summary'],
+ 'all_day': False,
+ 'offset_reached': False,
+ 'start_time': one_hour_from_now.strftime(DATE_STR_FORMAT),
+ 'end_time':
+ (one_hour_from_now + dt_util.dt.timedelta(minutes=60))
+ .strftime(DATE_STR_FORMAT),
+ 'location': '',
+ 'description': ''
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_in_progress_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ middle_of_event = dt_util.now() \
+ - dt_util.dt.timedelta(minutes=30)
+ event = {
+ 'start': {
+ 'dateTime': middle_of_event.isoformat()
+ },
+ 'end': {
+ 'dateTime': (middle_of_event + dt_util.dt
+ .timedelta(minutes=60))
+ .isoformat()
+ },
+ 'summary': 'Test Event in Progress',
+ 'reminders': {'useDefault': True},
+ 'id': 'aioehgni435lihje',
+ 'status': 'confirmed',
+ 'updated': '2016-11-05T15:52:07.329Z',
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True,
+ },
+ 'created': '2016-11-05T15:52:07.000Z',
+ 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ },
+ 'etag': '"2956722254658000"',
+ 'kind': 'calendar#event',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test Event in Progress'
+ device_id = 'test_event_in_progress'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEqual(cal.name, device_name)
+
+ self.assertEqual(cal.state, STATE_ON)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEqual(cal.device_state_attributes, {
+ 'message': event['summary'],
+ 'all_day': False,
+ 'offset_reached': False,
+ 'start_time': middle_of_event.strftime(DATE_STR_FORMAT),
+ 'end_time':
+ (middle_of_event + dt_util.dt.timedelta(minutes=60))
+ .strftime(DATE_STR_FORMAT),
+ 'location': '',
+ 'description': ''
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_offset_in_progress_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ middle_of_event = dt_util.now() \
+ + dt_util.dt.timedelta(minutes=14)
+ event_summary = 'Test Event in Progress'
+ event = {
+ 'start': {
+ 'dateTime': middle_of_event.isoformat()
+ },
+ 'end': {
+ 'dateTime': (middle_of_event + dt_util.dt
+ .timedelta(minutes=60))
+ .isoformat()
+ },
+ 'summary': '{} !!-15'.format(event_summary),
+ 'reminders': {'useDefault': True},
+ 'id': 'aioehgni435lihje',
+ 'status': 'confirmed',
+ 'updated': '2016-11-05T15:52:07.329Z',
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True,
+ },
+ 'created': '2016-11-05T15:52:07.000Z',
+ 'iCalUID': 'dsfohuygtfvgbhnuju@google.com',
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ },
+ 'etag': '"2956722254658000"',
+ 'kind': 'calendar#event',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test Event in Progress'
+ device_id = 'test_event_in_progress'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEqual(cal.name, device_name)
+
+ self.assertEqual(cal.state, STATE_OFF)
+
+ self.assertTrue(cal.offset_reached())
+
+ self.assertEqual(cal.device_state_attributes, {
+ 'message': event_summary,
+ 'all_day': False,
+ 'offset_reached': True,
+ 'start_time': middle_of_event.strftime(DATE_STR_FORMAT),
+ 'end_time':
+ (middle_of_event + dt_util.dt.timedelta(minutes=60))
+ .strftime(DATE_STR_FORMAT),
+ 'location': '',
+ 'description': ''
+ })
+
+ @pytest.mark.skip
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_all_day_offset_in_progress_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ tomorrow = dt_util.dt.date.today() \
+ + dt_util.dt.timedelta(days=1)
+
+ event_summary = 'Test All Day Event Offset In Progress'
+ event = {
+ 'summary': '{} !!-25:0'.format(event_summary),
+ 'start': {
+ 'date': tomorrow.isoformat()
+ },
+ 'end': {
+ 'date': (tomorrow + dt_util.dt.timedelta(days=1))
+ .isoformat()
+ },
+ 'location': 'Test Cases',
+ 'description': 'We\'re just testing that all day events get setup '
+ 'correctly',
+ 'kind': 'calendar#event',
+ 'created': '2016-06-23T16:37:57.000Z',
+ 'transparency': 'transparent',
+ 'updated': '2016-06-24T01:57:21.045Z',
+ 'reminders': {'useDefault': True},
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'id': '_c8rinwq863h45qnucyoi43ny8',
+ 'etag': '"2933466882090000"',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ 'iCalUID': 'cydrevtfuybguinhomj@google.com',
+ 'status': 'confirmed'
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test All Day Offset In Progress'
+ device_id = 'test_all_day_offset_in_progress'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEqual(cal.name, device_name)
+
+ self.assertEqual(cal.state, STATE_OFF)
+
+ self.assertTrue(cal.offset_reached())
+
+ self.assertEqual(cal.device_state_attributes, {
+ 'message': event_summary,
+ 'all_day': True,
+ 'offset_reached': True,
+ 'start_time': '{} 06:00:00'.format(event['start']['date']),
+ 'end_time': '{} 06:00:00'.format(event['end']['date']),
+ 'location': event['location'],
+ 'description': event['description']
+ })
+
+ @patch('homeassistant.components.calendar.google.GoogleCalendarData')
+ def test_all_day_offset_event(self, mock_next_event):
+ """Test that we can create an event trigger on device."""
+ tomorrow = dt_util.dt.date.today() \
+ + dt_util.dt.timedelta(days=2)
+
+ offset_hours = (1 + dt_util.now().hour)
+ event_summary = 'Test All Day Event Offset'
+ event = {
+ 'summary': '{} !!-{}:0'.format(event_summary, offset_hours),
+ 'start': {
+ 'date': tomorrow.isoformat()
+ },
+ 'end': {
+ 'date': (tomorrow + dt_util.dt.timedelta(days=1))
+ .isoformat()
+ },
+ 'location': 'Test Cases',
+ 'description': 'We\'re just testing that all day events get setup '
+ 'correctly',
+ 'kind': 'calendar#event',
+ 'created': '2016-06-23T16:37:57.000Z',
+ 'transparency': 'transparent',
+ 'updated': '2016-06-24T01:57:21.045Z',
+ 'reminders': {'useDefault': True},
+ 'organizer': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'sequence': 0,
+ 'creator': {
+ 'email': 'uvrttabwegnui4gtia3vyqb@import.calendar.google.com',
+ 'displayName': 'Organizer Name',
+ 'self': True
+ },
+ 'id': '_c8rinwq863h45qnucyoi43ny8',
+ 'etag': '"2933466882090000"',
+ 'htmlLink': 'https://www.google.com/calendar/event?eid=*******',
+ 'iCalUID': 'cydrevtfuybguinhomj@google.com',
+ 'status': 'confirmed'
+ }
+
+ mock_next_event.return_value.event = event
+
+ device_name = 'Test All Day Offset'
+ device_id = 'test_all_day_offset'
+
+ cal = calendar.GoogleCalendarEventDevice(self.hass, None, device_id,
+ {'name': device_name})
+
+ self.assertEqual(cal.name, device_name)
+
+ self.assertEqual(cal.state, STATE_OFF)
+
+ self.assertFalse(cal.offset_reached())
+
+ self.assertEqual(cal.device_state_attributes, {
+ 'message': event_summary,
+ 'all_day': True,
+ 'offset_reached': False,
+ 'start_time': '{} 00:00:00'.format(event['start']['date']),
+ 'end_time': '{} 00:00:00'.format(event['end']['date']),
+ 'location': event['location'],
+ 'description': event['description']
+ })
diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py
index 70e95dd7b93..87612da9faa 100644
--- a/tests/components/camera/test_init.py
+++ b/tests/components/camera/test_init.py
@@ -83,7 +83,7 @@ class TestGetImage(object):
@patch('homeassistant.components.camera.demo.DemoCamera.camera_image',
autospec=True, return_value=b'Test')
def test_get_image_from_camera(self, mock_camera):
- """Grab a image from camera entity."""
+ """Grab an image from camera entity."""
self.hass.start()
image = run_coroutine_threadsafe(camera.async_get_image(
diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py
index 812dd399a48..42ce7bd7add 100644
--- a/tests/components/camera/test_local_file.py
+++ b/tests/components/camera/test_local_file.py
@@ -8,10 +8,14 @@ from mock_open import MockOpen
from homeassistant.setup import async_setup_component
+from tests.common import mock_registry
+
@asyncio.coroutine
def test_loading_file(hass, test_client):
"""Test that it loads image from disk."""
+ mock_registry(hass)
+
with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \
mock.patch('os.access', mock.Mock(return_value=True)):
yield from async_setup_component(hass, 'camera', {
diff --git a/tests/components/climate/test_demo.py b/tests/components/climate/test_demo.py
index 9098494bf48..b2633a75583 100644
--- a/tests/components/climate/test_demo.py
+++ b/tests/components/climate/test_demo.py
@@ -224,10 +224,10 @@ class TestDemoClimate(unittest.TestCase):
def test_set_hold_mode_none(self):
"""Test setting the hold mode off/false."""
- climate.set_hold_mode(self.hass, None, ENTITY_ECOBEE)
+ climate.set_hold_mode(self.hass, 'off', ENTITY_ECOBEE)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_ECOBEE)
- self.assertEqual(None, state.attributes.get('hold_mode'))
+ self.assertEqual('off', state.attributes.get('hold_mode'))
def test_set_aux_heat_bad_attr(self):
"""Test setting the auxiliary heater without required attribute."""
diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py
index 190eb7e8522..abc9e6d74c2 100644
--- a/tests/components/climate/test_generic_thermostat.py
+++ b/tests/components/climate/test_generic_thermostat.py
@@ -14,6 +14,7 @@ from homeassistant.const import (
SERVICE_TURN_ON,
STATE_ON,
STATE_OFF,
+ STATE_IDLE,
TEMP_CELSIUS,
ATTR_TEMPERATURE
)
@@ -170,7 +171,7 @@ class TestClimateGenericThermostat(unittest.TestCase):
def test_setup_defaults_to_unknown(self):
"""Test the setting of defaults to unknown."""
- self.assertEqual('idle', self.hass.states.get(ENTITY).state)
+ self.assertEqual(STATE_IDLE, self.hass.states.get(ENTITY).state)
def test_default_setup_params(self):
"""Test the setup with default parameters."""
@@ -964,6 +965,7 @@ def test_restore_state(hass):
state = hass.states.get('climate.test_thermostat')
assert(state.attributes[ATTR_TEMPERATURE] == 20)
assert(state.attributes[climate.ATTR_OPERATION_MODE] == "off")
+ assert(state.state == STATE_OFF)
@asyncio.coroutine
@@ -990,3 +992,84 @@ def test_no_restore_state(hass):
state = hass.states.get('climate.test_thermostat')
assert(state.attributes[ATTR_TEMPERATURE] == 22)
+ assert(state.state == STATE_OFF)
+
+
+class TestClimateGenericThermostatRestoreState(unittest.TestCase):
+ """Test generic thermostat when restore state from HA startup."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.hass.config.temperature_unit = TEMP_CELSIUS
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop down everything that was started."""
+ self.hass.stop()
+
+ def test_restore_state_uncoherence_case(self):
+ """
+ Test restore from a strange state.
+
+ - Turn the generic thermostat off
+ - Restart HA and restore state from DB
+ """
+ self._mock_restore_cache(temperature=20)
+
+ self._setup_switch(False)
+ self._setup_sensor(15)
+ self._setup_climate()
+ self.hass.block_till_done()
+
+ state = self.hass.states.get(ENTITY)
+ self.assertEqual(20, state.attributes[ATTR_TEMPERATURE])
+ self.assertEqual(STATE_OFF,
+ state.attributes[climate.ATTR_OPERATION_MODE])
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(0, len(self.calls))
+
+ self._setup_switch(False)
+ self.hass.block_till_done()
+ state = self.hass.states.get(ENTITY)
+ self.assertEqual(STATE_OFF,
+ state.attributes[climate.ATTR_OPERATION_MODE])
+ self.assertEqual(STATE_OFF, state.state)
+
+ def _setup_climate(self):
+ assert setup_component(self.hass, climate.DOMAIN, {'climate': {
+ 'platform': 'generic_thermostat',
+ 'name': 'test',
+ 'cold_tolerance': 2,
+ 'hot_tolerance': 4,
+ 'away_temp': 30,
+ 'heater': ENT_SWITCH,
+ 'target_sensor': ENT_SENSOR,
+ 'ac_mode': True
+ }})
+
+ def _setup_sensor(self, temp, unit=TEMP_CELSIUS):
+ """Setup the test sensor."""
+ self.hass.states.set(ENT_SENSOR, temp, {
+ ATTR_UNIT_OF_MEASUREMENT: unit
+ })
+
+ def _setup_switch(self, is_on):
+ """Setup the test switch."""
+ self.hass.states.set(ENT_SWITCH, STATE_ON if is_on else STATE_OFF)
+ self.calls = []
+
+ @callback
+ def log_call(call):
+ """Log service calls."""
+ self.calls.append(call)
+
+ self.hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, log_call)
+ self.hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, log_call)
+
+ def _mock_restore_cache(self, temperature=20, operation_mode=STATE_OFF):
+ mock_restore_cache(self.hass, (
+ State(ENTITY, '0', {
+ ATTR_TEMPERATURE: str(temperature),
+ climate.ATTR_OPERATION_MODE: operation_mode,
+ ATTR_AWAY_MODE: "on"}),
+ ))
diff --git a/tests/components/climate/test_melissa.py b/tests/components/climate/test_melissa.py
new file mode 100644
index 00000000000..f8a044c2f4b
--- /dev/null
+++ b/tests/components/climate/test_melissa.py
@@ -0,0 +1,267 @@
+"""Test for Melissa climate component."""
+import unittest
+from unittest.mock import Mock, patch
+import json
+
+from asynctest import mock
+
+from homeassistant.components.climate import (
+ melissa, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
+ SUPPORT_ON_OFF, SUPPORT_FAN_MODE, STATE_HEAT, STATE_FAN_ONLY, STATE_DRY,
+ STATE_COOL, STATE_AUTO
+)
+from homeassistant.components.fan import SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH
+from homeassistant.components.melissa import DATA_MELISSA
+from homeassistant.const import (
+ TEMP_CELSIUS, STATE_ON, ATTR_TEMPERATURE, STATE_OFF, STATE_IDLE
+)
+from tests.common import get_test_home_assistant, load_fixture
+
+
+class TestMelissa(unittest.TestCase):
+ """Tests for Melissa climate."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Set up test variables."""
+ self.hass = get_test_home_assistant()
+ self._serial = '12345678'
+
+ self.api = Mock()
+ self.api.fetch_devices.return_value = json.loads(load_fixture(
+ 'melissa_fetch_devices.json'
+ ))
+ self.api.cur_settings.return_value = json.loads(load_fixture(
+ 'melissa_cur_settings.json'
+ ))
+ self.api.status.return_value = json.loads(load_fixture(
+ 'melissa_status.json'
+ ))
+ self.api.STATE_OFF = 0
+ self.api.STATE_ON = 1
+ self.api.STATE_IDLE = 2
+
+ self.api.MODE_AUTO = 0
+ self.api.MODE_FAN = 1
+ self.api.MODE_HEAT = 2
+ self.api.MODE_COOL = 3
+ self.api.MODE_DRY = 4
+
+ self.api.FAN_AUTO = 0
+ self.api.FAN_LOW = 1
+ self.api.FAN_MEDIUM = 2
+ self.api.FAN_HIGH = 3
+
+ self.api.STATE = 'state'
+ self.api.MODE = 'mode'
+ self.api.FAN = 'fan'
+ self.api.TEMP = 'temp'
+
+ device = self.api.fetch_devices()[self._serial]
+ self.thermostat = melissa.MelissaClimate(
+ self.api, device['serial_number'], device)
+ self.thermostat.update()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Teardown this test class. Stop hass."""
+ self.hass.stop()
+
+ @patch("homeassistant.components.climate.melissa.MelissaClimate")
+ def test_setup_platform(self, mocked_thermostat):
+ """Test setup_platform."""
+ device = self.api.fetch_devices()[self._serial]
+ thermostat = mocked_thermostat(self.api, device['serial_number'],
+ device)
+ thermostats = [thermostat]
+
+ self.hass.data[DATA_MELISSA] = self.api
+
+ config = {}
+ add_devices = Mock()
+ discovery_info = {}
+
+ melissa.setup_platform(self.hass, config, add_devices, discovery_info)
+ add_devices.assert_called_once_with(thermostats)
+
+ def test_get_name(self):
+ """Test name property."""
+ self.assertEqual("Melissa 12345678", self.thermostat.name)
+
+ def test_is_on(self):
+ """Test name property."""
+ self.assertTrue(self.thermostat.is_on)
+ self.thermostat._cur_settings = None
+ self.assertFalse(self.thermostat.is_on)
+
+ def test_current_fan_mode(self):
+ """Test current_fan_mode property."""
+ self.thermostat.update()
+ self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode)
+ self.thermostat._cur_settings = None
+ self.assertEqual(None, self.thermostat.current_fan_mode)
+
+ def test_current_temperature(self):
+ """Test current temperature."""
+ self.assertEqual(27.4, self.thermostat.current_temperature)
+
+ def test_current_temperature_no_data(self):
+ """Test current temperature without data."""
+ self.thermostat._data = None
+ self.assertIsNone(self.thermostat.current_temperature)
+
+ def test_target_temperature_step(self):
+ """Test current target_temperature_step."""
+ self.assertEqual(1, self.thermostat.target_temperature_step)
+
+ def test_current_operation(self):
+ """Test current operation."""
+ self.thermostat.update()
+ self.assertEqual(self.thermostat.current_operation, STATE_HEAT)
+ self.thermostat._cur_settings = None
+ self.assertEqual(None, self.thermostat.current_operation)
+
+ def test_operation_list(self):
+ """Test the operation list."""
+ self.assertEqual(
+ [STATE_AUTO, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, STATE_HEAT],
+ self.thermostat.operation_list
+ )
+
+ def test_fan_list(self):
+ """Test the fan list."""
+ self.assertEqual(
+ [STATE_AUTO, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM],
+ self.thermostat.fan_list
+ )
+
+ def test_target_temperature(self):
+ """Test target temperature."""
+ self.assertEqual(16, self.thermostat.target_temperature)
+ self.thermostat._cur_settings = None
+ self.assertEqual(None, self.thermostat.target_temperature)
+
+ def test_state(self):
+ """Test state."""
+ self.assertEqual(STATE_ON, self.thermostat.state)
+ self.thermostat._cur_settings = None
+ self.assertEqual(None, self.thermostat.state)
+
+ def test_temperature_unit(self):
+ """Test temperature unit."""
+ self.assertEqual(TEMP_CELSIUS, self.thermostat.temperature_unit)
+
+ def test_min_temp(self):
+ """Test min temp."""
+ self.assertEqual(16, self.thermostat.min_temp)
+
+ def test_max_temp(self):
+ """Test max temp."""
+ self.assertEqual(30, self.thermostat.max_temp)
+
+ def test_supported_features(self):
+ """Test supported_features property."""
+ features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
+ SUPPORT_ON_OFF | SUPPORT_FAN_MODE)
+ self.assertEqual(features, self.thermostat.supported_features)
+
+ def test_set_temperature(self):
+ """Test set_temperature."""
+ self.api.send.return_value = True
+ self.thermostat.update()
+ self.thermostat.set_temperature(**{ATTR_TEMPERATURE: 25})
+ self.assertEqual(25, self.thermostat.target_temperature)
+
+ def test_fan_mode(self):
+ """Test set_fan_mode."""
+ self.api.send.return_value = True
+ self.thermostat.set_fan_mode(SPEED_HIGH)
+ self.assertEqual(SPEED_HIGH, self.thermostat.current_fan_mode)
+
+ def test_set_operation_mode(self):
+ """Test set_operation_mode."""
+ self.api.send.return_value = True
+ self.thermostat.set_operation_mode(STATE_COOL)
+ self.assertEqual(STATE_COOL, self.thermostat.current_operation)
+
+ def test_turn_on(self):
+ """Test turn_on."""
+ self.thermostat.turn_on()
+ self.assertTrue(self.thermostat.state)
+
+ def test_turn_off(self):
+ """Test turn_off."""
+ self.thermostat.turn_off()
+ self.assertEqual(STATE_OFF, self.thermostat.state)
+
+ def test_send(self):
+ """Test send."""
+ self.thermostat.update()
+ self.assertTrue(self.thermostat.send(
+ {'fan': self.api.FAN_MEDIUM}))
+ self.assertEqual(SPEED_MEDIUM, self.thermostat.current_fan_mode)
+ self.api.send.return_value = False
+ 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.assertIsNone(self.thermostat._cur_settings)
+
+ @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning')
+ def test_update(self, mocked_warning):
+ """Test update."""
+ self.thermostat.update()
+ self.assertEqual(SPEED_LOW, self.thermostat.current_fan_mode)
+ self.assertEqual(STATE_HEAT, self.thermostat.current_operation)
+ self.thermostat._api.status.side_effect = KeyError('boom')
+ self.thermostat.update()
+ mocked_warning.assert_called_once_with(
+ 'Unable to update entity %s', self.thermostat.entity_id)
+
+ def test_melissa_state_to_hass(self):
+ """Test for translate melissa states to hass."""
+ self.assertEqual(STATE_OFF, self.thermostat.melissa_state_to_hass(0))
+ self.assertEqual(STATE_ON, self.thermostat.melissa_state_to_hass(1))
+ self.assertEqual(STATE_IDLE, self.thermostat.melissa_state_to_hass(2))
+ self.assertEqual(None,
+ self.thermostat.melissa_state_to_hass(3))
+
+ 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))
+ self.assertEqual(STATE_DRY, self.thermostat.melissa_op_to_hass(4))
+ self.assertEqual(
+ None, self.thermostat.melissa_op_to_hass(5))
+
+ def test_melissa_fan_to_hass(self):
+ """Test for translate melissa fan state to hass."""
+ self.assertEqual(STATE_AUTO, self.thermostat.melissa_fan_to_hass(0))
+ self.assertEqual(SPEED_LOW, self.thermostat.melissa_fan_to_hass(1))
+ self.assertEqual(SPEED_MEDIUM, self.thermostat.melissa_fan_to_hass(2))
+ self.assertEqual(SPEED_HIGH, self.thermostat.melissa_fan_to_hass(3))
+ self.assertEqual(None, self.thermostat.melissa_fan_to_hass(4))
+
+ @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))
+ self.assertEqual(3, self.thermostat.hass_mode_to_melissa(STATE_COOL))
+ self.assertEqual(4, self.thermostat.hass_mode_to_melissa(STATE_DRY))
+ self.thermostat.hass_mode_to_melissa("test")
+ mocked_warning.assert_called_once_with(
+ "Melissa have no setting for %s mode", "test")
+
+ @mock.patch('homeassistant.components.climate.melissa._LOGGER.warning')
+ def test_hass_fan_to_melissa(self, mocked_warning):
+ """Test for translate melissa states to hass."""
+ self.assertEqual(0, self.thermostat.hass_fan_to_melissa(STATE_AUTO))
+ self.assertEqual(1, self.thermostat.hass_fan_to_melissa(SPEED_LOW))
+ self.assertEqual(2, self.thermostat.hass_fan_to_melissa(SPEED_MEDIUM))
+ self.assertEqual(3, self.thermostat.hass_fan_to_melissa(SPEED_HIGH))
+ self.thermostat.hass_fan_to_melissa("test")
+ mocked_warning.assert_called_once_with(
+ "Melissa have no setting for %s fan mode", "test")
diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py
index 707e49f670f..7a4e9f2950e 100644
--- a/tests/components/cloud/__init__.py
+++ b/tests/components/cloud/__init__.py
@@ -1 +1 @@
-"""Tests for the cloud component."""
+"""Tests for the cloud component."""
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 7623b25d401..69cd540e7d5 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -14,8 +14,8 @@ from tests.common import mock_coro
@pytest.fixture
def cloud_client(hass, test_client):
"""Fixture that can fetch from the cloud client."""
- with patch('homeassistant.components.cloud.Cloud.initialize',
- return_value=mock_coro(True)):
+ with patch('homeassistant.components.cloud.Cloud.async_start',
+ return_value=mock_coro()):
hass.loop.run_until_complete(async_setup_component(hass, 'cloud', {
'cloud': {
'mode': 'development',
diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py
index 7d23d9faad4..70990519a0b 100644
--- a/tests/components/cloud/test_init.py
+++ b/tests/components/cloud/test_init.py
@@ -87,7 +87,7 @@ def test_initialize_loads_info(mock_os, hass):
with patch('homeassistant.components.cloud.open', mopen, create=True), \
patch('homeassistant.components.cloud.Cloud._decode_claims'):
- cl._start_cloud(None)
+ yield from cl.async_start(None)
assert cl.id_token == 'test-id-token'
assert cl.access_token == 'test-access-token'
diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py
index 529559f56af..53340ecede1 100644
--- a/tests/components/cloud/test_iot.py
+++ b/tests/components/cloud/test_iot.py
@@ -266,8 +266,8 @@ def test_handler_alexa(hass):
hass.states.async_set(
'switch.test2', 'on', {'friendly_name': "Test switch 2"})
- with patch('homeassistant.components.cloud.Cloud.initialize',
- return_value=mock_coro(True)):
+ with patch('homeassistant.components.cloud.Cloud.async_start',
+ return_value=mock_coro()):
setup = yield from async_setup_component(hass, 'cloud', {
'cloud': {
'alexa': {
@@ -309,8 +309,8 @@ def test_handler_google_actions(hass):
hass.states.async_set(
'switch.test2', 'on', {'friendly_name': "Test switch 2"})
- with patch('homeassistant.components.cloud.Cloud.initialize',
- return_value=mock_coro(True)):
+ with patch('homeassistant.components.cloud.Cloud.async_start',
+ return_value=mock_coro()):
setup = yield from async_setup_component(hass, 'cloud', {
'cloud': {
'google_actions': {
diff --git a/tests/components/cover/test_template.py b/tests/components/cover/test_template.py
index af114135da9..3d7aa3ce618 100644
--- a/tests/components/cover/test_template.py
+++ b/tests/components/cover/test_template.py
@@ -275,7 +275,7 @@ class TestTemplateCover(unittest.TestCase):
assert self.hass.states.all() == []
def test_template_open_and_close(self):
- """Test that if open_cover is specified, cose_cover is too."""
+ """Test that if open_cover is specified, close_cover is too."""
with assert_setup_component(0, 'cover'):
assert setup.setup_component(self.hass, 'cover', {
'cover': {
diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py
index 6e646e9862d..f8d3fdf128b 100644
--- a/tests/components/device_tracker/test_asuswrt.py
+++ b/tests/components/device_tracker/test_asuswrt.py
@@ -378,7 +378,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase):
telnet.login.assert_not_called()
def test_get_asuswrt_data(self):
- """Test aususwrt data fetch."""
+ """Test asuswrt data fetch."""
scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH)
scanner._get_wl = mock.Mock()
scanner._get_arp = mock.Mock()
diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py
index 78813d9ff0b..84cca1bb843 100644
--- a/tests/components/device_tracker/test_init.py
+++ b/tests/components/device_tracker/test_init.py
@@ -704,7 +704,7 @@ class TestComponentsDeviceTracker(unittest.TestCase):
@asyncio.coroutine
def test_async_added_to_hass(hass):
- """Test resoring state."""
+ """Test restoring state."""
attr = {
device_tracker.ATTR_LONGITUDE: 18,
device_tracker.ATTR_LATITUDE: -33,
diff --git a/tests/components/device_tracker/test_mqtt.py b/tests/components/device_tracker/test_mqtt.py
index eb461062971..78750e91f83 100644
--- a/tests/components/device_tracker/test_mqtt.py
+++ b/tests/components/device_tracker/test_mqtt.py
@@ -56,7 +56,7 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase):
def test_new_message(self):
"""Test new message."""
dev_id = 'paulus'
- enttiy_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
topic = '/location/paulus'
location = 'work'
@@ -69,4 +69,80 @@ class TestComponentsDeviceTrackerMQTT(unittest.TestCase):
})
fire_mqtt_message(self.hass, topic, location)
self.hass.block_till_done()
- self.assertEqual(location, self.hass.states.get(enttiy_id).state)
+ self.assertEqual(location, self.hass.states.get(entity_id).state)
+
+ def test_single_level_wildcard_topic(self):
+ """Test single level wildcard topic."""
+ dev_id = 'paulus'
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
+ subscription = '/location/+/paulus'
+ topic = '/location/room/paulus'
+ location = 'work'
+
+ self.hass.config.components = set(['mqtt', 'zone'])
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ self.assertEqual(location, self.hass.states.get(entity_id).state)
+
+ def test_multi_level_wildcard_topic(self):
+ """Test multi level wildcard topic."""
+ dev_id = 'paulus'
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
+ subscription = '/location/#'
+ topic = '/location/room/paulus'
+ location = 'work'
+
+ self.hass.config.components = set(['mqtt', 'zone'])
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ self.assertEqual(location, self.hass.states.get(entity_id).state)
+
+ def test_single_level_wildcard_topic_not_matching(self):
+ """Test not matching single level wildcard topic."""
+ dev_id = 'paulus'
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
+ subscription = '/location/+/paulus'
+ topic = '/location/paulus'
+ location = 'work'
+
+ self.hass.config.components = set(['mqtt', 'zone'])
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ self.assertIsNone(self.hass.states.get(entity_id))
+
+ def test_multi_level_wildcard_topic_not_matching(self):
+ """Test not matching multi level wildcard topic."""
+ dev_id = 'paulus'
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
+ subscription = '/location/#'
+ topic = '/somewhere/room/paulus'
+ location = 'work'
+
+ self.hass.config.components = set(['mqtt', 'zone'])
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ self.assertIsNone(self.hass.states.get(entity_id))
diff --git a/tests/components/device_tracker/test_mqtt_json.py b/tests/components/device_tracker/test_mqtt_json.py
index 1755f424d29..43f4fc3bbf3 100644
--- a/tests/components/device_tracker/test_mqtt_json.py
+++ b/tests/components/device_tracker/test_mqtt_json.py
@@ -123,3 +123,77 @@ class TestComponentsDeviceTrackerJSONMQTT(unittest.TestCase):
"Skipping update for following data because of missing "
"or malformatted data: {\"longitude\": 2.0}",
test_handle.output[0])
+
+ def test_single_level_wildcard_topic(self):
+ """Test single level wildcard topic."""
+ dev_id = 'zanzito'
+ subscription = 'location/+/zanzito'
+ topic = 'location/room/zanzito'
+ location = json.dumps(LOCATION_MESSAGE)
+
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt_json',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ state = self.hass.states.get('device_tracker.zanzito')
+ self.assertEqual(state.attributes.get('latitude'), 2.0)
+ self.assertEqual(state.attributes.get('longitude'), 1.0)
+
+ def test_multi_level_wildcard_topic(self):
+ """Test multi level wildcard topic."""
+ dev_id = 'zanzito'
+ subscription = 'location/#'
+ topic = 'location/zanzito'
+ location = json.dumps(LOCATION_MESSAGE)
+
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt_json',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ state = self.hass.states.get('device_tracker.zanzito')
+ self.assertEqual(state.attributes.get('latitude'), 2.0)
+ self.assertEqual(state.attributes.get('longitude'), 1.0)
+
+ def test_single_level_wildcard_topic_not_matching(self):
+ """Test not matching single level wildcard topic."""
+ dev_id = 'zanzito'
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
+ subscription = 'location/+/zanzito'
+ topic = 'location/zanzito'
+ location = json.dumps(LOCATION_MESSAGE)
+
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt_json',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ self.assertIsNone(self.hass.states.get(entity_id))
+
+ def test_multi_level_wildcard_topic_not_matching(self):
+ """Test not matching multi level wildcard topic."""
+ dev_id = 'zanzito'
+ entity_id = device_tracker.ENTITY_ID_FORMAT.format(dev_id)
+ subscription = 'location/#'
+ topic = 'somewhere/zanzito'
+ location = json.dumps(LOCATION_MESSAGE)
+
+ assert setup_component(self.hass, device_tracker.DOMAIN, {
+ device_tracker.DOMAIN: {
+ CONF_PLATFORM: 'mqtt_json',
+ 'devices': {dev_id: subscription}
+ }
+ })
+ fire_mqtt_message(self.hass, topic, location)
+ self.hass.block_till_done()
+ self.assertIsNone(self.hass.states.get(entity_id))
diff --git a/tests/components/device_tracker/test_owntracks.py b/tests/components/device_tracker/test_owntracks.py
index 44c0e0c6295..2239e13e220 100644
--- a/tests/components/device_tracker/test_owntracks.py
+++ b/tests/components/device_tracker/test_owntracks.py
@@ -433,7 +433,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_event_gps_entry_exit(self):
"""Test the entry event."""
- # Entering the owntrack circular region named "inner"
+ # Entering the owntracks circular region named "inner"
self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE)
# Enter uses the zone's gps co-ords
@@ -447,7 +447,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
# note that LOCATION_MESSAGE is actually pretty far
# from INNER_ZONE and has good accuracy. I haven't
# received a transition message though so I'm still
- # asssociated with the inner zone regardless of GPS.
+ # associated with the inner zone regardless of GPS.
self.assert_location_latitude(INNER_ZONE['latitude'])
self.assert_location_accuracy(INNER_ZONE['radius'])
self.assert_location_state('inner')
@@ -624,7 +624,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_event_entry_zone_loading_dash(self):
"""Test the event for zone landing."""
# Make sure the leading - is ignored
- # Ownracks uses this to switch on hold
+ # Owntracks uses this to switch on hold
message = build_message(
{'desc': "-inner"},
REGION_GPS_ENTER_MESSAGE)
@@ -673,10 +673,10 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_event_source_type_entry_exit(self):
"""Test the entry and exit events of source type."""
- # Entering the owntrack circular region named "inner"
+ # Entering the owntracks circular region named "inner"
self.send_message(EVENT_TOPIC, REGION_GPS_ENTER_MESSAGE)
- # source_type should be gps when enterings using gps.
+ # source_type should be gps when entering using gps.
self.assert_location_source_type('gps')
# owntracks shouldn't send beacon events with acc = 0
@@ -715,7 +715,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
# note that LOCATION_MESSAGE is actually pretty far
# from INNER_ZONE and has good accuracy. I haven't
# received a transition message though so I'm still
- # asssociated with the inner zone regardless of GPS.
+ # associated with the inner zone regardless of GPS.
self.assert_location_latitude(INNER_ZONE['latitude'])
self.assert_location_accuracy(INNER_ZONE['radius'])
self.assert_location_state('inner')
@@ -865,7 +865,7 @@ class TestDeviceTrackerOwnTracks(BaseMQTT):
def test_event_beacon_entry_zone_loading_dash(self):
"""Test the event for beacon zone landing."""
# Make sure the leading - is ignored
- # Ownracks uses this to switch on hold
+ # Owntracks uses this to switch on hold
message = build_message(
{'desc': "-inner"},
diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py
index b378118141a..8bc3a60146c 100644
--- a/tests/components/device_tracker/test_unifi_direct.py
+++ b/tests/components/device_tracker/test_unifi_direct.py
@@ -139,12 +139,12 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase):
devices = scanner._get_update() # pylint: disable=protected-access
self.assertTrue(devices is None)
- def test_good_reponse_parses(self):
+ def test_good_response_parses(self):
"""Test that the response form the AP parses to JSON correctly."""
response = _response_to_json(load_fixture('unifi_direct.txt'))
self.assertTrue(response != {})
- def test_bad_reponse_returns_none(self):
+ def test_bad_response_returns_none(self):
"""Test that a bad response form the AP parses to JSON correctly."""
self.assertTrue(_response_to_json("{(}") == {})
diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py
index 94a4566a17b..19f25b514db 100644
--- a/tests/components/device_tracker/test_xiaomi.py
+++ b/tests/components/device_tracker/test_xiaomi.py
@@ -1,265 +1,265 @@
-"""The tests for the Xiaomi router device tracker platform."""
-import logging
-import unittest
-from unittest import mock
-from unittest.mock import patch
-
-import requests
-
-from homeassistant.components.device_tracker import DOMAIN, xiaomi as xiaomi
-from homeassistant.components.device_tracker.xiaomi import get_scanner
-from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
- CONF_PLATFORM)
-from tests.common import get_test_home_assistant
-
-_LOGGER = logging.getLogger(__name__)
-
-INVALID_USERNAME = 'bob'
-TOKEN_TIMEOUT_USERNAME = 'tok'
-URL_AUTHORIZE = 'http://192.168.0.1/cgi-bin/luci/api/xqsystem/login'
-URL_LIST_END = 'api/misystem/devicelist'
-
-FIRST_CALL = True
-
-
-def mocked_requests(*args, **kwargs):
- """Mock requests.get invocations."""
- class MockResponse:
- """Class to represent a mocked response."""
-
- 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
-
- @property
- def content(self):
- """Return the content of the response."""
- return self.json()
-
- def raise_for_status(self):
- """Raise an HTTPError if status is not 200."""
- if self.status_code != 200:
- raise requests.HTTPError(self.status_code)
-
- data = kwargs.get('data')
- global FIRST_CALL
-
- if data and data.get('username', None) == INVALID_USERNAME:
- # deliver an invalid token
- return MockResponse({
- "code": "401",
- "msg": "Invalid token"
- }, 200)
- elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME:
- # deliver an expired token
- return MockResponse({
- "url": "/cgi-bin/luci/;stok=ef5860/web/home",
- "token": "timedOut",
- "code": "0"
- }, 200)
- elif str(args[0]).startswith(URL_AUTHORIZE):
- # deliver an authorized token
- return MockResponse({
- "url": "/cgi-bin/luci/;stok=ef5860/web/home",
- "token": "ef5860",
- "code": "0"
- }, 200)
- elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \
- and FIRST_CALL is True:
- FIRST_CALL = False
- # deliver an error when called with expired token
- return MockResponse({
- "code": "401",
- "msg": "Invalid token"
- }, 200)
- elif str(args[0]).endswith(URL_LIST_END):
- # deliver the device list
- return MockResponse({
- "mac": "1C:98:EC:0E:D5:A4",
- "list": [
- {
- "mac": "23:83:BF:F6:38:A0",
- "oname": "12255ff",
- "isap": 0,
- "parent": "",
- "authority": {
- "wan": 1,
- "pridisk": 0,
- "admin": 1,
- "lan": 0
- },
- "push": 0,
- "online": 1,
- "name": "Device1",
- "times": 0,
- "ip": [
- {
- "downspeed": "0",
- "online": "496957",
- "active": 1,
- "upspeed": "0",
- "ip": "192.168.0.25"
- }
- ],
- "statistics": {
- "downspeed": "0",
- "online": "496957",
- "upspeed": "0"
- },
- "icon": "",
- "type": 1
- },
- {
- "mac": "1D:98:EC:5E:D5:A6",
- "oname": "CdddFG58",
- "isap": 0,
- "parent": "",
- "authority": {
- "wan": 1,
- "pridisk": 0,
- "admin": 1,
- "lan": 0
- },
- "push": 0,
- "online": 1,
- "name": "Device2",
- "times": 0,
- "ip": [
- {
- "downspeed": "0",
- "online": "347325",
- "active": 1,
- "upspeed": "0",
- "ip": "192.168.0.3"
- }
- ],
- "statistics": {
- "downspeed": "0",
- "online": "347325",
- "upspeed": "0"
- },
- "icon": "",
- "type": 0
- },
- ],
- "code": 0
- }, 200)
- else:
- _LOGGER.debug('UNKNOWN ROUTE')
-
-
-class TestXiaomiDeviceScanner(unittest.TestCase):
- """Xiaomi device scanner test class."""
-
- def setUp(self):
- """Initialize values for this testcase class."""
- self.hass = get_test_home_assistant()
-
- def tearDown(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- @mock.patch(
- 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner',
- return_value=mock.MagicMock())
- def test_config(self, xiaomi_mock):
- """Testing minimal configuration."""
- config = {
- DOMAIN: xiaomi.PLATFORM_SCHEMA({
- CONF_PLATFORM: xiaomi.DOMAIN,
- CONF_HOST: '192.168.0.1',
- CONF_PASSWORD: 'passwordTest'
- })
- }
- xiaomi.get_scanner(self.hass, config)
- self.assertEqual(xiaomi_mock.call_count, 1)
- self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN]))
- call_arg = xiaomi_mock.call_args[0][0]
- self.assertEqual(call_arg['username'], 'admin')
- self.assertEqual(call_arg['password'], 'passwordTest')
- self.assertEqual(call_arg['host'], '192.168.0.1')
- self.assertEqual(call_arg['platform'], 'device_tracker')
-
- @mock.patch(
- 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner',
- return_value=mock.MagicMock())
- def test_config_full(self, xiaomi_mock):
- """Testing full configuration."""
- config = {
- DOMAIN: xiaomi.PLATFORM_SCHEMA({
- CONF_PLATFORM: xiaomi.DOMAIN,
- CONF_HOST: '192.168.0.1',
- CONF_USERNAME: 'alternativeAdminName',
- CONF_PASSWORD: 'passwordTest'
- })
- }
- xiaomi.get_scanner(self.hass, config)
- self.assertEqual(xiaomi_mock.call_count, 1)
- self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN]))
- call_arg = xiaomi_mock.call_args[0][0]
- self.assertEqual(call_arg['username'], 'alternativeAdminName')
- self.assertEqual(call_arg['password'], 'passwordTest')
- self.assertEqual(call_arg['host'], '192.168.0.1')
- self.assertEqual(call_arg['platform'], 'device_tracker')
-
- @patch('requests.get', side_effect=mocked_requests)
- @patch('requests.post', side_effect=mocked_requests)
- def test_invalid_credential(self, mock_get, mock_post):
- """"Testing invalid credential handling."""
- config = {
- DOMAIN: xiaomi.PLATFORM_SCHEMA({
- CONF_PLATFORM: xiaomi.DOMAIN,
- CONF_HOST: '192.168.0.1',
- CONF_USERNAME: INVALID_USERNAME,
- CONF_PASSWORD: 'passwordTest'
- })
- }
- self.assertIsNone(get_scanner(self.hass, config))
-
- @patch('requests.get', side_effect=mocked_requests)
- @patch('requests.post', side_effect=mocked_requests)
- def test_valid_credential(self, mock_get, mock_post):
- """"Testing valid refresh."""
- config = {
- DOMAIN: xiaomi.PLATFORM_SCHEMA({
- CONF_PLATFORM: xiaomi.DOMAIN,
- CONF_HOST: '192.168.0.1',
- CONF_USERNAME: 'admin',
- CONF_PASSWORD: 'passwordTest'
- })
- }
- scanner = get_scanner(self.hass, config)
- self.assertIsNotNone(scanner)
- self.assertEqual(2, len(scanner.scan_devices()))
- self.assertEqual("Device1",
- scanner.get_device_name("23:83:BF:F6:38:A0"))
- self.assertEqual("Device2",
- scanner.get_device_name("1D:98:EC:5E:D5:A6"))
-
- @patch('requests.get', side_effect=mocked_requests)
- @patch('requests.post', side_effect=mocked_requests)
- def test_token_timed_out(self, mock_get, mock_post):
- """"Testing refresh with a timed out token.
-
- New token is requested and list is downloaded a second time.
- """
- config = {
- DOMAIN: xiaomi.PLATFORM_SCHEMA({
- CONF_PLATFORM: xiaomi.DOMAIN,
- CONF_HOST: '192.168.0.1',
- CONF_USERNAME: TOKEN_TIMEOUT_USERNAME,
- CONF_PASSWORD: 'passwordTest'
- })
- }
- scanner = get_scanner(self.hass, config)
- self.assertIsNotNone(scanner)
- self.assertEqual(2, len(scanner.scan_devices()))
- self.assertEqual("Device1",
- scanner.get_device_name("23:83:BF:F6:38:A0"))
- self.assertEqual("Device2",
- scanner.get_device_name("1D:98:EC:5E:D5:A6"))
+"""The tests for the Xiaomi router device tracker platform."""
+import logging
+import unittest
+from unittest import mock
+from unittest.mock import patch
+
+import requests
+
+from homeassistant.components.device_tracker import DOMAIN, xiaomi as xiaomi
+from homeassistant.components.device_tracker.xiaomi import get_scanner
+from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
+ CONF_PLATFORM)
+from tests.common import get_test_home_assistant
+
+_LOGGER = logging.getLogger(__name__)
+
+INVALID_USERNAME = 'bob'
+TOKEN_TIMEOUT_USERNAME = 'tok'
+URL_AUTHORIZE = 'http://192.168.0.1/cgi-bin/luci/api/xqsystem/login'
+URL_LIST_END = 'api/misystem/devicelist'
+
+FIRST_CALL = True
+
+
+def mocked_requests(*args, **kwargs):
+ """Mock requests.get invocations."""
+ class MockResponse:
+ """Class to represent a mocked response."""
+
+ 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
+
+ @property
+ def content(self):
+ """Return the content of the response."""
+ return self.json()
+
+ def raise_for_status(self):
+ """Raise an HTTPError if status is not 200."""
+ if self.status_code != 200:
+ raise requests.HTTPError(self.status_code)
+
+ data = kwargs.get('data')
+ global FIRST_CALL
+
+ if data and data.get('username', None) == INVALID_USERNAME:
+ # deliver an invalid token
+ return MockResponse({
+ "code": "401",
+ "msg": "Invalid token"
+ }, 200)
+ elif data and data.get('username', None) == TOKEN_TIMEOUT_USERNAME:
+ # deliver an expired token
+ return MockResponse({
+ "url": "/cgi-bin/luci/;stok=ef5860/web/home",
+ "token": "timedOut",
+ "code": "0"
+ }, 200)
+ elif str(args[0]).startswith(URL_AUTHORIZE):
+ # deliver an authorized token
+ return MockResponse({
+ "url": "/cgi-bin/luci/;stok=ef5860/web/home",
+ "token": "ef5860",
+ "code": "0"
+ }, 200)
+ elif str(args[0]).endswith("timedOut/" + URL_LIST_END) \
+ and FIRST_CALL is True:
+ FIRST_CALL = False
+ # deliver an error when called with expired token
+ return MockResponse({
+ "code": "401",
+ "msg": "Invalid token"
+ }, 200)
+ elif str(args[0]).endswith(URL_LIST_END):
+ # deliver the device list
+ return MockResponse({
+ "mac": "1C:98:EC:0E:D5:A4",
+ "list": [
+ {
+ "mac": "23:83:BF:F6:38:A0",
+ "oname": "12255ff",
+ "isap": 0,
+ "parent": "",
+ "authority": {
+ "wan": 1,
+ "pridisk": 0,
+ "admin": 1,
+ "lan": 0
+ },
+ "push": 0,
+ "online": 1,
+ "name": "Device1",
+ "times": 0,
+ "ip": [
+ {
+ "downspeed": "0",
+ "online": "496957",
+ "active": 1,
+ "upspeed": "0",
+ "ip": "192.168.0.25"
+ }
+ ],
+ "statistics": {
+ "downspeed": "0",
+ "online": "496957",
+ "upspeed": "0"
+ },
+ "icon": "",
+ "type": 1
+ },
+ {
+ "mac": "1D:98:EC:5E:D5:A6",
+ "oname": "CdddFG58",
+ "isap": 0,
+ "parent": "",
+ "authority": {
+ "wan": 1,
+ "pridisk": 0,
+ "admin": 1,
+ "lan": 0
+ },
+ "push": 0,
+ "online": 1,
+ "name": "Device2",
+ "times": 0,
+ "ip": [
+ {
+ "downspeed": "0",
+ "online": "347325",
+ "active": 1,
+ "upspeed": "0",
+ "ip": "192.168.0.3"
+ }
+ ],
+ "statistics": {
+ "downspeed": "0",
+ "online": "347325",
+ "upspeed": "0"
+ },
+ "icon": "",
+ "type": 0
+ },
+ ],
+ "code": 0
+ }, 200)
+ else:
+ _LOGGER.debug('UNKNOWN ROUTE')
+
+
+class TestXiaomiDeviceScanner(unittest.TestCase):
+ """Xiaomi device scanner test class."""
+
+ def setUp(self):
+ """Initialize values for this testcase class."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @mock.patch(
+ 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner',
+ return_value=mock.MagicMock())
+ def test_config(self, xiaomi_mock):
+ """Testing minimal configuration."""
+ config = {
+ DOMAIN: xiaomi.PLATFORM_SCHEMA({
+ CONF_PLATFORM: xiaomi.DOMAIN,
+ CONF_HOST: '192.168.0.1',
+ CONF_PASSWORD: 'passwordTest'
+ })
+ }
+ xiaomi.get_scanner(self.hass, config)
+ self.assertEqual(xiaomi_mock.call_count, 1)
+ self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN]))
+ call_arg = xiaomi_mock.call_args[0][0]
+ self.assertEqual(call_arg['username'], 'admin')
+ self.assertEqual(call_arg['password'], 'passwordTest')
+ self.assertEqual(call_arg['host'], '192.168.0.1')
+ self.assertEqual(call_arg['platform'], 'device_tracker')
+
+ @mock.patch(
+ 'homeassistant.components.device_tracker.xiaomi.XiaomiDeviceScanner',
+ return_value=mock.MagicMock())
+ def test_config_full(self, xiaomi_mock):
+ """Testing full configuration."""
+ config = {
+ DOMAIN: xiaomi.PLATFORM_SCHEMA({
+ CONF_PLATFORM: xiaomi.DOMAIN,
+ CONF_HOST: '192.168.0.1',
+ CONF_USERNAME: 'alternativeAdminName',
+ CONF_PASSWORD: 'passwordTest'
+ })
+ }
+ xiaomi.get_scanner(self.hass, config)
+ self.assertEqual(xiaomi_mock.call_count, 1)
+ self.assertEqual(xiaomi_mock.call_args, mock.call(config[DOMAIN]))
+ call_arg = xiaomi_mock.call_args[0][0]
+ self.assertEqual(call_arg['username'], 'alternativeAdminName')
+ self.assertEqual(call_arg['password'], 'passwordTest')
+ self.assertEqual(call_arg['host'], '192.168.0.1')
+ self.assertEqual(call_arg['platform'], 'device_tracker')
+
+ @patch('requests.get', side_effect=mocked_requests)
+ @patch('requests.post', side_effect=mocked_requests)
+ def test_invalid_credential(self, mock_get, mock_post):
+ """"Testing invalid credential handling."""
+ config = {
+ DOMAIN: xiaomi.PLATFORM_SCHEMA({
+ CONF_PLATFORM: xiaomi.DOMAIN,
+ CONF_HOST: '192.168.0.1',
+ CONF_USERNAME: INVALID_USERNAME,
+ CONF_PASSWORD: 'passwordTest'
+ })
+ }
+ self.assertIsNone(get_scanner(self.hass, config))
+
+ @patch('requests.get', side_effect=mocked_requests)
+ @patch('requests.post', side_effect=mocked_requests)
+ def test_valid_credential(self, mock_get, mock_post):
+ """"Testing valid refresh."""
+ config = {
+ DOMAIN: xiaomi.PLATFORM_SCHEMA({
+ CONF_PLATFORM: xiaomi.DOMAIN,
+ CONF_HOST: '192.168.0.1',
+ CONF_USERNAME: 'admin',
+ CONF_PASSWORD: 'passwordTest'
+ })
+ }
+ scanner = get_scanner(self.hass, config)
+ self.assertIsNotNone(scanner)
+ self.assertEqual(2, len(scanner.scan_devices()))
+ self.assertEqual("Device1",
+ scanner.get_device_name("23:83:BF:F6:38:A0"))
+ self.assertEqual("Device2",
+ scanner.get_device_name("1D:98:EC:5E:D5:A6"))
+
+ @patch('requests.get', side_effect=mocked_requests)
+ @patch('requests.post', side_effect=mocked_requests)
+ def test_token_timed_out(self, mock_get, mock_post):
+ """"Testing refresh with a timed out token.
+
+ New token is requested and list is downloaded a second time.
+ """
+ config = {
+ DOMAIN: xiaomi.PLATFORM_SCHEMA({
+ CONF_PLATFORM: xiaomi.DOMAIN,
+ CONF_HOST: '192.168.0.1',
+ CONF_USERNAME: TOKEN_TIMEOUT_USERNAME,
+ CONF_PASSWORD: 'passwordTest'
+ })
+ }
+ scanner = get_scanner(self.hass, config)
+ self.assertIsNotNone(scanner)
+ self.assertEqual(2, len(scanner.scan_devices()))
+ self.assertEqual("Device1",
+ scanner.get_device_name("23:83:BF:F6:38:A0"))
+ self.assertEqual("Device2",
+ scanner.get_device_name("1D:98:EC:5E:D5:A6"))
diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py
index 383b4f7165d..cba3c835763 100644
--- a/tests/components/emulated_hue/test_hue_api.py
+++ b/tests/components/emulated_hue/test_hue_api.py
@@ -121,7 +121,14 @@ def hass_hue(loop, hass):
def hue_client(loop, hass_hue, test_client):
"""Create web client for emulated hue api."""
web_app = hass_hue.http.app
- config = Config(None, {'type': 'alexa'})
+ config = Config(None, {
+ emulated_hue.CONF_TYPE: emulated_hue.TYPE_ALEXA,
+ emulated_hue.CONF_ENTITIES: {
+ 'light.bed_light': {
+ emulated_hue.CONF_ENTITY_HIDDEN: True
+ }
+ }
+ })
HueUsernameView().register(web_app.router)
HueAllLightsStateView(config).register(web_app.router)
@@ -145,7 +152,7 @@ def test_discover_lights(hue_client):
# Make sure the lights we added to the config are there
assert 'light.ceiling_lights' in devices
- assert 'light.bed_light' in devices
+ assert 'light.bed_light' not in devices
assert 'script.set_kitchen_light' in devices
assert 'light.kitchen_lights' not in devices
assert 'media_player.living_room' in devices
@@ -186,19 +193,23 @@ def test_get_light_state(hass_hue, hue_client):
assert result_json['light.ceiling_lights']['state'][HUE_API_STATE_BRI] == \
127
- # Turn bedroom light off
+ # Turn office light off
yield from hass_hue.services.async_call(
light.DOMAIN, const.SERVICE_TURN_OFF,
{
- const.ATTR_ENTITY_ID: 'light.bed_light'
+ const.ATTR_ENTITY_ID: 'light.ceiling_lights'
},
blocking=True)
- bedroom_json = yield from perform_get_light_state(
- hue_client, 'light.bed_light', 200)
+ office_json = yield from perform_get_light_state(
+ hue_client, 'light.ceiling_lights', 200)
- assert bedroom_json['state'][HUE_API_STATE_ON] is False
- assert bedroom_json['state'][HUE_API_STATE_BRI] == 0
+ assert office_json['state'][HUE_API_STATE_ON] is False
+ assert office_json['state'][HUE_API_STATE_BRI] == 0
+
+ # Make sure bedroom light isn't accessible
+ yield from perform_get_light_state(
+ hue_client, 'light.bed_light', 404)
# Make sure kitchen light isn't accessible
yield from perform_get_light_state(
@@ -207,35 +218,41 @@ def test_get_light_state(hass_hue, hue_client):
@asyncio.coroutine
def test_put_light_state(hass_hue, hue_client):
- """Test the seeting of light states."""
+ """Test the setting of light states."""
yield from perform_put_test_on_ceiling_lights(hass_hue, hue_client)
# Turn the bedroom light on first
yield from hass_hue.services.async_call(
light.DOMAIN, const.SERVICE_TURN_ON,
- {const.ATTR_ENTITY_ID: 'light.bed_light',
+ {const.ATTR_ENTITY_ID: 'light.ceiling_lights',
light.ATTR_BRIGHTNESS: 153},
blocking=True)
- bed_light = hass_hue.states.get('light.bed_light')
- assert bed_light.state == STATE_ON
- assert bed_light.attributes[light.ATTR_BRIGHTNESS] == 153
+ ceiling_lights = hass_hue.states.get('light.ceiling_lights')
+ assert ceiling_lights.state == STATE_ON
+ assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 153
# Go through the API to turn it off
- bedroom_result = yield from perform_put_light_state(
+ ceiling_result = yield from perform_put_light_state(
hass_hue, hue_client,
- 'light.bed_light', False)
+ 'light.ceiling_lights', False)
- bedroom_result_json = yield from bedroom_result.json()
+ ceiling_result_json = yield from ceiling_result.json()
- assert bedroom_result.status == 200
- assert 'application/json' in bedroom_result.headers['content-type']
+ assert ceiling_result.status == 200
+ assert 'application/json' in ceiling_result.headers['content-type']
- assert len(bedroom_result_json) == 1
+ assert len(ceiling_result_json) == 1
# Check to make sure the state changed
- bed_light = hass_hue.states.get('light.bed_light')
- assert bed_light.state == STATE_OFF
+ ceiling_lights = hass_hue.states.get('light.ceiling_lights')
+ assert ceiling_lights.state == STATE_OFF
+
+ # Make sure we can't change the bedroom light state
+ bedroom_result = yield from perform_put_light_state(
+ hass_hue, hue_client,
+ 'light.bed_light', True)
+ assert bedroom_result.status == 404
# Make sure we can't change the kitchen light state
kitchen_result = yield from perform_put_light_state(
@@ -435,7 +452,7 @@ def perform_put_test_on_ceiling_lights(hass_hue, hue_client,
@asyncio.coroutine
def perform_get_light_state(client, entity_id, expected_status):
- """Test the gettting of a light state."""
+ """Test the getting of a light state."""
result = yield from client.get('/api/username/lights/{}'.format(entity_id))
assert result.status == expected_status
diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py
index 25bcbc1dd55..06613f1336a 100644
--- a/tests/components/emulated_hue/test_init.py
+++ b/tests/components/emulated_hue/test_init.py
@@ -1,128 +1,128 @@
-"""Test the Emulated Hue component."""
-import json
-
-from unittest.mock import patch, Mock, mock_open
-
-from homeassistant.components.emulated_hue import Config, _LOGGER
-
-
-def test_config_google_home_entity_id_to_number():
- """Test config adheres to the type."""
- conf = Config(Mock(), {
- 'type': 'google_home'
- })
-
- mop = mock_open(read_data=json.dumps({'1': 'light.test2'}))
- handle = mop()
-
- with patch('homeassistant.util.json.open', mop, create=True):
- number = conf.entity_id_to_number('light.test')
- assert number == '2'
- assert handle.write.call_count == 1
- assert json.loads(handle.write.mock_calls[0][1][0]) == {
- '1': 'light.test2',
- '2': 'light.test',
- }
-
- number = conf.entity_id_to_number('light.test')
- assert number == '2'
- assert handle.write.call_count == 1
-
- number = conf.entity_id_to_number('light.test2')
- assert number == '1'
- assert handle.write.call_count == 1
-
- entity_id = conf.number_to_entity_id('1')
- assert entity_id == 'light.test2'
-
-
-def test_config_google_home_entity_id_to_number_altered():
- """Test config adheres to the type."""
- conf = Config(Mock(), {
- 'type': 'google_home'
- })
-
- mop = mock_open(read_data=json.dumps({'21': 'light.test2'}))
- handle = mop()
-
- with patch('homeassistant.util.json.open', mop, create=True):
- number = conf.entity_id_to_number('light.test')
- assert number == '22'
- assert handle.write.call_count == 1
- assert json.loads(handle.write.mock_calls[0][1][0]) == {
- '21': 'light.test2',
- '22': 'light.test',
- }
-
- number = conf.entity_id_to_number('light.test')
- assert number == '22'
- assert handle.write.call_count == 1
-
- number = conf.entity_id_to_number('light.test2')
- assert number == '21'
- assert handle.write.call_count == 1
-
- entity_id = conf.number_to_entity_id('21')
- assert entity_id == 'light.test2'
-
-
-def test_config_google_home_entity_id_to_number_empty():
- """Test config adheres to the type."""
- conf = Config(Mock(), {
- 'type': 'google_home'
- })
-
- mop = mock_open(read_data='')
- handle = mop()
-
- with patch('homeassistant.util.json.open', mop, create=True):
- number = conf.entity_id_to_number('light.test')
- assert number == '1'
- assert handle.write.call_count == 1
- assert json.loads(handle.write.mock_calls[0][1][0]) == {
- '1': 'light.test',
- }
-
- number = conf.entity_id_to_number('light.test')
- assert number == '1'
- assert handle.write.call_count == 1
-
- number = conf.entity_id_to_number('light.test2')
- assert number == '2'
- assert handle.write.call_count == 2
-
- entity_id = conf.number_to_entity_id('2')
- assert entity_id == 'light.test2'
-
-
-def test_config_alexa_entity_id_to_number():
- """Test config adheres to the type."""
- conf = Config(None, {
- 'type': 'alexa'
- })
-
- number = conf.entity_id_to_number('light.test')
- assert number == 'light.test'
-
- number = conf.entity_id_to_number('light.test')
- assert number == 'light.test'
-
- number = conf.entity_id_to_number('light.test2')
- assert number == 'light.test2'
-
- entity_id = conf.number_to_entity_id('light.test')
- assert entity_id == 'light.test'
-
-
-def test_warning_config_google_home_listen_port():
- """Test we warn when non-default port is used for Google Home."""
- with patch.object(_LOGGER, 'warning') as mock_warn:
- Config(None, {
- 'type': 'google_home',
- 'host_ip': '123.123.123.123',
- 'listen_port': 8300
- })
-
- assert mock_warn.called
- assert mock_warn.mock_calls[0][1][0] == \
- "When targeting Google Home, listening port has to be port 80"
+"""Test the Emulated Hue component."""
+import json
+
+from unittest.mock import patch, Mock, mock_open
+
+from homeassistant.components.emulated_hue import Config, _LOGGER
+
+
+def test_config_google_home_entity_id_to_number():
+ """Test config adheres to the type."""
+ conf = Config(Mock(), {
+ 'type': 'google_home'
+ })
+
+ mop = mock_open(read_data=json.dumps({'1': 'light.test2'}))
+ handle = mop()
+
+ with patch('homeassistant.util.json.open', mop, create=True):
+ number = conf.entity_id_to_number('light.test')
+ assert number == '2'
+ assert handle.write.call_count == 1
+ assert json.loads(handle.write.mock_calls[0][1][0]) == {
+ '1': 'light.test2',
+ '2': 'light.test',
+ }
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == '2'
+ assert handle.write.call_count == 1
+
+ number = conf.entity_id_to_number('light.test2')
+ assert number == '1'
+ assert handle.write.call_count == 1
+
+ entity_id = conf.number_to_entity_id('1')
+ assert entity_id == 'light.test2'
+
+
+def test_config_google_home_entity_id_to_number_altered():
+ """Test config adheres to the type."""
+ conf = Config(Mock(), {
+ 'type': 'google_home'
+ })
+
+ mop = mock_open(read_data=json.dumps({'21': 'light.test2'}))
+ handle = mop()
+
+ with patch('homeassistant.util.json.open', mop, create=True):
+ number = conf.entity_id_to_number('light.test')
+ assert number == '22'
+ assert handle.write.call_count == 1
+ assert json.loads(handle.write.mock_calls[0][1][0]) == {
+ '21': 'light.test2',
+ '22': 'light.test',
+ }
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == '22'
+ assert handle.write.call_count == 1
+
+ number = conf.entity_id_to_number('light.test2')
+ assert number == '21'
+ assert handle.write.call_count == 1
+
+ entity_id = conf.number_to_entity_id('21')
+ assert entity_id == 'light.test2'
+
+
+def test_config_google_home_entity_id_to_number_empty():
+ """Test config adheres to the type."""
+ conf = Config(Mock(), {
+ 'type': 'google_home'
+ })
+
+ mop = mock_open(read_data='')
+ handle = mop()
+
+ with patch('homeassistant.util.json.open', mop, create=True):
+ number = conf.entity_id_to_number('light.test')
+ assert number == '1'
+ assert handle.write.call_count == 1
+ assert json.loads(handle.write.mock_calls[0][1][0]) == {
+ '1': 'light.test',
+ }
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == '1'
+ assert handle.write.call_count == 1
+
+ number = conf.entity_id_to_number('light.test2')
+ assert number == '2'
+ assert handle.write.call_count == 2
+
+ entity_id = conf.number_to_entity_id('2')
+ assert entity_id == 'light.test2'
+
+
+def test_config_alexa_entity_id_to_number():
+ """Test config adheres to the type."""
+ conf = Config(None, {
+ 'type': 'alexa'
+ })
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == 'light.test'
+
+ number = conf.entity_id_to_number('light.test')
+ assert number == 'light.test'
+
+ number = conf.entity_id_to_number('light.test2')
+ assert number == 'light.test2'
+
+ entity_id = conf.number_to_entity_id('light.test')
+ assert entity_id == 'light.test'
+
+
+def test_warning_config_google_home_listen_port():
+ """Test we warn when non-default port is used for Google Home."""
+ with patch.object(_LOGGER, 'warning') as mock_warn:
+ Config(None, {
+ 'type': 'google_home',
+ 'host_ip': '123.123.123.123',
+ 'listen_port': 8300
+ })
+
+ assert mock_warn.called
+ assert mock_warn.mock_calls[0][1][0] == \
+ "When targeting Google Home, listening port has to be port 80"
diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py
index 1cd895954de..b3032954431 100644
--- a/tests/components/emulated_hue/test_upnp.py
+++ b/tests/components/emulated_hue/test_upnp.py
@@ -87,10 +87,9 @@ class TestEmulatedHue(unittest.TestCase):
self.assertTrue('text/xml' in result.headers['content-type'])
# Make sure the XML is parsable
- # pylint: disable=bare-except
try:
ET.fromstring(result.text)
- except:
+ except: # noqa: E722 # pylint: disable=bare-except
self.fail('description.xml is not valid XML!')
def test_create_username(self):
diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py
index 0d87b491229..43c36d1ca2a 100644
--- a/tests/components/google_assistant/test_google_assistant.py
+++ b/tests/components/google_assistant/test_google_assistant.py
@@ -175,6 +175,8 @@ def test_query_request(hass_fixture, assistant_client):
'id': "light.bed_light",
}, {
'id': "light.kitchen_lights",
+ }, {
+ 'id': 'media_player.lounge_room',
}]
}
}]
@@ -187,12 +189,14 @@ def test_query_request(hass_fixture, assistant_client):
body = yield from result.json()
assert body.get('requestId') == reqid
devices = body['payload']['devices']
- assert len(devices) == 3
+ assert len(devices) == 4
assert devices['light.bed_light']['on'] is False
assert devices['light.ceiling_lights']['on'] is True
assert devices['light.ceiling_lights']['brightness'] == 70
assert devices['light.kitchen_lights']['color']['spectrumRGB'] == 16727919
assert devices['light.kitchen_lights']['color']['temperature'] == 4166
+ assert devices['media_player.lounge_room']['on'] is True
+ assert devices['media_player.lounge_room']['brightness'] == 100
@asyncio.coroutine
@@ -225,26 +229,36 @@ def test_query_climate_request(hass_fixture, assistant_client):
devices = body['payload']['devices']
assert devices == {
'climate.heatpump': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureSetpoint': 20.0,
'thermostatTemperatureAmbient': 25.0,
'thermostatMode': 'heat',
},
'climate.ecobee': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureSetpointHigh': 24,
'thermostatTemperatureAmbient': 23,
'thermostatMode': 'heat',
'thermostatTemperatureSetpointLow': 21
},
'climate.hvac': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureSetpoint': 21,
'thermostatTemperatureAmbient': 22,
'thermostatMode': 'cool',
'thermostatHumidityAmbient': 54,
},
'sensor.outside_temperature': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureAmbient': 15.6
},
'sensor.outside_humidity': {
+ 'on': True,
+ 'online': True,
'thermostatHumidityAmbient': 54.0
}
}
@@ -280,23 +294,31 @@ def test_query_climate_request_f(hass_fixture, assistant_client):
devices = body['payload']['devices']
assert devices == {
'climate.heatpump': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureSetpoint': -6.7,
'thermostatTemperatureAmbient': -3.9,
'thermostatMode': 'heat',
},
'climate.ecobee': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureSetpointHigh': -4.4,
'thermostatTemperatureAmbient': -5,
'thermostatMode': 'heat',
'thermostatTemperatureSetpointLow': -6.1,
},
'climate.hvac': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureSetpoint': -6.1,
'thermostatTemperatureAmbient': -5.6,
'thermostatMode': 'cool',
'thermostatHumidityAmbient': 54,
},
'sensor.outside_temperature': {
+ 'on': True,
+ 'online': True,
'thermostatTemperatureAmbient': -9.1
}
}
@@ -304,7 +326,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client):
@asyncio.coroutine
def test_execute_request(hass_fixture, assistant_client):
- """Test a execute request."""
+ """Test an execute request."""
reqid = '5711642932632160985'
data = {
'requestId':
@@ -317,6 +339,8 @@ def test_execute_request(hass_fixture, assistant_client):
"id": "light.ceiling_lights",
}, {
"id": "switch.decorative_lights",
+ }, {
+ "id": "media_player.lounge_room",
}],
"execution": [{
"command": "action.devices.commands.OnOff",
@@ -324,6 +348,17 @@ def test_execute_request(hass_fixture, assistant_client):
"on": False
}
}]
+ }, {
+ "devices": [{
+ "id": "media_player.walkman",
+ }],
+ "execution": [{
+ "command":
+ "action.devices.commands.BrightnessAbsolute",
+ "params": {
+ "brightness": 70
+ }
+ }]
}, {
"devices": [{
"id": "light.kitchen_lights",
@@ -380,7 +415,7 @@ def test_execute_request(hass_fixture, assistant_client):
body = yield from result.json()
assert body.get('requestId') == reqid
commands = body['payload']['commands']
- assert len(commands) == 6
+ assert len(commands) == 8
ceiling = hass_fixture.states.get('light.ceiling_lights')
assert ceiling.state == 'off'
@@ -394,3 +429,10 @@ def test_execute_request(hass_fixture, assistant_client):
assert bed.attributes.get(light.ATTR_RGB_COLOR) == (0, 255, 0)
assert hass_fixture.states.get('switch.decorative_lights').state == 'off'
+
+ walkman = hass_fixture.states.get('media_player.walkman')
+ assert walkman.state == 'playing'
+ assert walkman.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) == 0.7
+
+ lounge = hass_fixture.states.get('media_player.lounge_room')
+ assert lounge.state == 'off'
diff --git a/tests/components/image_processing/__init__.py b/tests/components/image_processing/__init__.py
index 6e79d49c251..63aee1dfbaf 100644
--- a/tests/components/image_processing/__init__.py
+++ b/tests/components/image_processing/__init__.py
@@ -1 +1 @@
-"""Test 'image_processing' component plaforms."""
+"""Test 'image_processing' component platforms."""
diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py
index 0594c436abd..628c5405eaa 100644
--- a/tests/components/image_processing/test_init.py
+++ b/tests/components/image_processing/test_init.py
@@ -82,7 +82,7 @@ class TestImageProcessing(object):
@patch('homeassistant.components.camera.demo.DemoCamera.camera_image',
autospec=True, return_value=b'Test')
def test_get_image_from_camera(self, mock_camera):
- """Grab a image from camera entity."""
+ """Grab an image from camera entity."""
self.hass.start()
ip.scan(self.hass, entity_id='image_processing.test')
@@ -275,7 +275,7 @@ class TestImageProcessingFace(object):
@patch('homeassistant.components.image_processing.demo.'
'DemoImageProcessingFace.confidence',
new_callable=PropertyMock(return_value=None))
- def test_face_event_call_no_confidence(self, mock_confi, aioclient_mock):
+ def test_face_event_call_no_confidence(self, mock_config, aioclient_mock):
"""Setup and scan a picture and test faces from event."""
aioclient_mock.get(self.url, content=b'image')
diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py
index e35ac8185d0..e840bce54f7 100644
--- a/tests/components/image_processing/test_openalpr_cloud.py
+++ b/tests/components/image_processing/test_openalpr_cloud.py
@@ -1,4 +1,4 @@
-"""The tests for the openalpr clooud platform."""
+"""The tests for the openalpr cloud platform."""
import asyncio
from unittest.mock import patch, PropertyMock
@@ -13,7 +13,7 @@ from tests.common import (
get_test_home_assistant, assert_setup_component, load_fixture)
-class TestOpenAlprCloudlSetup(object):
+class TestOpenAlprCloudSetup(object):
"""Test class for image processing."""
def setup_method(self):
@@ -149,8 +149,7 @@ class TestOpenAlprCloud(object):
'secret_key': "sk_abcxyz123456",
'tasks': "plate",
'return_image': 0,
- 'country': 'eu',
- 'image_bytes': "aW1hZ2U="
+ 'country': 'eu'
}
def teardown_method(self):
diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py
index 611f1240d45..5c28ea9988f 100644
--- a/tests/components/light/test_hue.py
+++ b/tests/components/light/test_hue.py
@@ -68,7 +68,7 @@ class TestSetup(unittest.TestCase):
"""Return a dict suitable for mocking api.get('lights')."""
mock_bridge_lights = lights
- for light_id, info in mock_bridge_lights.items():
+ for info in mock_bridge_lights.values():
if 'state' not in info:
info['state'] = {'on': False}
@@ -272,38 +272,20 @@ class TestSetup(unittest.TestCase):
hue_light.unthrottled_update_lights(
self.hass, mock_bridge_two, self.mock_add_devices)
- self.assertEquals(sorted(mock_bridge_one.lights.keys()), [1, 2])
- self.assertEquals(sorted(mock_bridge_two.lights.keys()), [1, 3])
+ self.assertEqual(sorted(mock_bridge_one.lights.keys()), [1, 2])
+ self.assertEqual(sorted(mock_bridge_two.lights.keys()), [1, 3])
- self.assertEquals(len(self.mock_add_devices.mock_calls), 2)
+ self.assertEqual(len(self.mock_add_devices.mock_calls), 2)
# first call
name, args, kwargs = self.mock_add_devices.mock_calls[0]
- self.assertEquals(len(args), 1)
- self.assertEquals(len(kwargs), 0)
-
- # one argument, a list of lights in bridge one; each of them is an
- # object of type HueLight so we can't straight up compare them
- lights = args[0]
- self.assertEquals(
- lights[0].unique_id,
- '{}.b1l1.Light.1'.format(hue_light.HueLight))
- self.assertEquals(
- lights[1].unique_id,
- '{}.b1l2.Light.2'.format(hue_light.HueLight))
+ self.assertEqual(len(args), 1)
+ self.assertEqual(len(kwargs), 0)
# second call works the same
name, args, kwargs = self.mock_add_devices.mock_calls[1]
- self.assertEquals(len(args), 1)
- self.assertEquals(len(kwargs), 0)
-
- lights = args[0]
- self.assertEquals(
- lights[0].unique_id,
- '{}.b2l1.Light.1'.format(hue_light.HueLight))
- self.assertEquals(
- lights[1].unique_id,
- '{}.b2l3.Light.3'.format(hue_light.HueLight))
+ self.assertEqual(len(args), 1)
+ self.assertEqual(len(kwargs), 0)
def test_process_lights_api_error(self):
"""Test the process_lights function when the bridge errors out."""
@@ -313,8 +295,8 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals([], ret)
- self.assertEquals(self.mock_bridge.lights, {})
+ self.assertEqual([], ret)
+ self.assertEqual(self.mock_bridge.lights, {})
def test_process_lights_no_lights(self):
"""Test the process_lights function when bridge returns no lights."""
@@ -325,9 +307,9 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals([], ret)
+ self.assertEqual([], ret)
mock_dispatcher_send.assert_not_called()
- self.assertEquals(self.mock_bridge.lights, {})
+ self.assertEqual(self.mock_bridge.lights, {})
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_lights_some_lights(self, mock_hue_light):
@@ -341,7 +323,7 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals(len(ret), 2)
+ self.assertEqual(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
@@ -353,7 +335,7 @@ class TestSetup(unittest.TestCase):
self.mock_bridge.allow_in_emulated_hue),
])
mock_dispatcher_send.assert_not_called()
- self.assertEquals(len(self.mock_bridge.lights), 2)
+ self.assertEqual(len(self.mock_bridge.lights), 2)
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_lights_new_light(self, mock_hue_light):
@@ -373,7 +355,7 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_lights(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals(len(ret), 1)
+ self.assertEqual(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
@@ -382,7 +364,7 @@ class TestSetup(unittest.TestCase):
])
mock_dispatcher_send.assert_called_once_with(
'hue_light_callback_bridge-id_1')
- self.assertEquals(len(self.mock_bridge.lights), 2)
+ self.assertEqual(len(self.mock_bridge.lights), 2)
def test_process_groups_api_error(self):
"""Test the process_groups function when the bridge errors out."""
@@ -392,8 +374,8 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals([], ret)
- self.assertEquals(self.mock_bridge.lightgroups, {})
+ self.assertEqual([], ret)
+ self.assertEqual(self.mock_bridge.lightgroups, {})
def test_process_groups_no_state(self):
"""Test the process_groups function when bridge returns no status."""
@@ -405,9 +387,9 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals([], ret)
+ self.assertEqual([], ret)
mock_dispatcher_send.assert_not_called()
- self.assertEquals(self.mock_bridge.lightgroups, {})
+ self.assertEqual(self.mock_bridge.lightgroups, {})
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_groups_some_groups(self, mock_hue_light):
@@ -421,7 +403,7 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals(len(ret), 2)
+ self.assertEqual(len(ret), 2)
mock_hue_light.assert_has_calls([
call(
1, {'state': 'on'}, self.mock_bridge, mock.ANY,
@@ -433,7 +415,7 @@ class TestSetup(unittest.TestCase):
self.mock_bridge.allow_in_emulated_hue, True),
])
mock_dispatcher_send.assert_not_called()
- self.assertEquals(len(self.mock_bridge.lightgroups), 2)
+ self.assertEqual(len(self.mock_bridge.lightgroups), 2)
@patch(HUE_LIGHT_NS + 'HueLight')
def test_process_groups_new_group(self, mock_hue_light):
@@ -453,7 +435,7 @@ class TestSetup(unittest.TestCase):
ret = hue_light.process_groups(
self.hass, self.mock_api, self.mock_bridge, None)
- self.assertEquals(len(ret), 1)
+ self.assertEqual(len(ret), 1)
mock_hue_light.assert_has_calls([
call(
2, {'state': 'off'}, self.mock_bridge, mock.ANY,
@@ -462,7 +444,7 @@ class TestSetup(unittest.TestCase):
])
mock_dispatcher_send.assert_called_once_with(
'hue_light_callback_bridge-id_1')
- self.assertEquals(len(self.mock_bridge.lightgroups), 2)
+ self.assertEqual(len(self.mock_bridge.lightgroups), 2)
class TestHueLight(unittest.TestCase):
@@ -506,60 +488,16 @@ class TestHueLight(unittest.TestCase):
def test_unique_id_for_light(self):
"""Test the unique_id method with lights."""
- class_name = ""
-
light = self.buildLight(info={'uniqueid': 'foobar'})
- self.assertEquals(
- class_name+'.foobar',
- light.unique_id)
+ self.assertEqual('foobar', light.unique_id)
light = self.buildLight(info={})
- self.assertEquals(
- class_name+'.Unnamed Device.Light.42',
- light.unique_id)
-
- light = self.buildLight(info={'name': 'my-name'})
- self.assertEquals(
- class_name+'.my-name.Light.42',
- light.unique_id)
-
- light = self.buildLight(info={'type': 'my-type'})
- self.assertEquals(
- class_name+'.Unnamed Device.my-type.42',
- light.unique_id)
-
- light = self.buildLight(info={'name': 'a name', 'type': 'my-type'})
- self.assertEquals(
- class_name+'.a name.my-type.42',
- light.unique_id)
+ self.assertIsNone(light.unique_id)
def test_unique_id_for_group(self):
"""Test the unique_id method with groups."""
- class_name = ""
-
light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True)
- self.assertEquals(
- class_name+'.foobar',
- light.unique_id)
+ self.assertEqual('foobar', light.unique_id)
light = self.buildLight(info={}, is_group=True)
- self.assertEquals(
- class_name+'.Unnamed Device.Group.42',
- light.unique_id)
-
- light = self.buildLight(info={'name': 'my-name'}, is_group=True)
- self.assertEquals(
- class_name+'.my-name.Group.42',
- light.unique_id)
-
- light = self.buildLight(info={'type': 'my-type'}, is_group=True)
- self.assertEquals(
- class_name+'.Unnamed Device.my-type.42',
- light.unique_id)
-
- light = self.buildLight(
- info={'name': 'a name', 'type': 'my-type'},
- is_group=True)
- self.assertEquals(
- class_name+'.a name.my-type.42',
- light.unique_id)
+ self.assertIsNone(light.unique_id)
diff --git a/tests/components/light/test_litejet.py b/tests/components/light/test_litejet.py
index 001c419066f..dd4b4b4a56e 100644
--- a/tests/components/light/test_litejet.py
+++ b/tests/components/light/test_litejet.py
@@ -156,7 +156,7 @@ class TestLiteJetLight(unittest.TestCase):
# (Requesting the level is not strictly needed with a deactivated
# event but the implementation happens to do it. This could be
- # changed to a assert_not_called in the future.)
+ # changed to an assert_not_called in the future.)
self.mock_lj.get_load_level.assert_called_with(
ENTITY_OTHER_LIGHT_NUMBER)
diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py
index 5cb0d0cdc1b..a06f8e7d093 100644
--- a/tests/components/light/test_mqtt_json.py
+++ b/tests/components/light/test_mqtt_json.py
@@ -1,579 +1,579 @@
-"""The tests for the MQTT JSON light platform.
-
-Configuration with RGB, brightness, color temp, effect, white value and XY:
-
-light:
- platform: mqtt_json
- name: mqtt_json_light_1
- state_topic: "home/rgb1"
- command_topic: "home/rgb1/set"
- brightness: true
- color_temp: true
- effect: true
- rgb: true
- white_value: true
- xy: true
-
-Configuration with RGB, brightness, color temp, effect, white value:
-
-light:
- platform: mqtt_json
- name: mqtt_json_light_1
- state_topic: "home/rgb1"
- command_topic: "home/rgb1/set"
- brightness: true
- color_temp: true
- effect: true
- rgb: true
- white_value: true
-
-Configuration with RGB, brightness, color temp and effect:
-
-light:
- platform: mqtt_json
- name: mqtt_json_light_1
- state_topic: "home/rgb1"
- command_topic: "home/rgb1/set"
- brightness: true
- color_temp: true
- effect: true
- rgb: true
-
-Configuration with RGB, brightness and color temp:
-
-light:
- platform: mqtt_json
- name: mqtt_json_light_1
- state_topic: "home/rgb1"
- command_topic: "home/rgb1/set"
- brightness: true
- rgb: true
- color_temp: true
-
-Configuration with RGB, brightness:
-
-light:
- platform: mqtt_json
- name: mqtt_json_light_1
- state_topic: "home/rgb1"
- command_topic: "home/rgb1/set"
- brightness: true
- rgb: true
-
-Config without RGB:
-
-light:
- platform: mqtt_json
- name: mqtt_json_light_1
- state_topic: "home/rgb1"
- command_topic: "home/rgb1/set"
- brightness: true
-
-Config without RGB and brightness:
-
-light:
- platform: mqtt_json
- name: mqtt_json_light_1
- state_topic: "home/rgb1"
- command_topic: "home/rgb1/set"
-
-Config with brightness and scale:
-
-light:
- platform: mqtt_json
- name: test
- state_topic: "mqtt_json_light_1"
- command_topic: "mqtt_json_light_1/set"
- brightness: true
- brightness_scale: 99
-"""
-
-import json
-import unittest
-
-from homeassistant.setup import setup_component
-from homeassistant.const import (
- STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE,
- ATTR_SUPPORTED_FEATURES)
-import homeassistant.components.light as light
-from tests.common import (
- get_test_home_assistant, mock_mqtt_component, fire_mqtt_message,
- assert_setup_component)
-
-
-class TestLightMQTTJSON(unittest.TestCase):
- """Test the MQTT JSON light."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Setup things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.mock_publish = mock_mqtt_component(self.hass)
-
- def tearDown(self): # pylint: disable=invalid-name
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_fail_setup_if_no_command_topic(self): \
- # pylint: disable=invalid-name
- """Test if setup fails with no command topic."""
- with assert_setup_component(0, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- }
- })
- self.assertIsNone(self.hass.states.get('light.test'))
-
- def test_no_color_brightness_color_temp_white_val_if_no_topics(self): \
- # pylint: disable=invalid-name
- """Test for no RGB, brightness, color temp, effect, white val or XY."""
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES))
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('color_temp'))
- self.assertIsNone(state.attributes.get('effect'))
- self.assertIsNone(state.attributes.get('white_value'))
- self.assertIsNone(state.attributes.get('xy_color'))
-
- fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('color_temp'))
- self.assertIsNone(state.attributes.get('effect'))
- self.assertIsNone(state.attributes.get('white_value'))
- self.assertIsNone(state.attributes.get('xy_color'))
-
- def test_controlling_state_via_topic(self): \
- # pylint: disable=invalid-name
- """Test the controlling of the state via topic."""
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'brightness': True,
- 'color_temp': True,
- 'effect': True,
- 'rgb': True,
- 'white_value': True,
- 'xy': True,
- 'qos': '0'
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertEqual(255, state.attributes.get(ATTR_SUPPORTED_FEATURES))
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('color_temp'))
- self.assertIsNone(state.attributes.get('effect'))
- self.assertIsNone(state.attributes.get('white_value'))
- self.assertIsNone(state.attributes.get('xy_color'))
- self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
-
- # Turn on the light, full white
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"color":{"r":255,"g":255,"b":255,'
- '"x":0.123,"y":0.123},'
- '"brightness":255,'
- '"color_temp":155,'
- '"effect":"colorloop",'
- '"white_value":150}')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
- self.assertEqual(255, state.attributes.get('brightness'))
- self.assertEqual(155, state.attributes.get('color_temp'))
- self.assertEqual('colorloop', state.attributes.get('effect'))
- self.assertEqual(150, state.attributes.get('white_value'))
- self.assertEqual([0.123, 0.123], state.attributes.get('xy_color'))
-
- # Turn the light off
- fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
-
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"brightness":100}')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.hass.block_till_done()
- self.assertEqual(100,
- light_state.attributes['brightness'])
-
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"color":{"r":125,"g":125,"b":125}}')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.assertEqual([125, 125, 125],
- light_state.attributes.get('rgb_color'))
-
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"color":{"x":0.135,"y":0.135}}')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.assertEqual([0.135, 0.135],
- light_state.attributes.get('xy_color'))
-
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"color_temp":155}')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.assertEqual(155, light_state.attributes.get('color_temp'))
-
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"effect":"colorloop"}')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.assertEqual('colorloop', light_state.attributes.get('effect'))
-
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"white_value":155}')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.assertEqual(155, light_state.attributes.get('white_value'))
-
- def test_sending_mqtt_commands_and_optimistic(self): \
- # pylint: disable=invalid-name
- """Test the sending of command in optimistic mode."""
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'command_topic': 'test_light_rgb/set',
- 'brightness': True,
- 'color_temp': True,
- 'effect': True,
- 'rgb': True,
- 'white_value': True,
- 'qos': 2
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES))
- self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE))
-
- 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])
- 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])
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
-
- light.turn_on(self.hass, 'light.test',
- brightness=50, color_temp=155, effect='colorloop',
- white_value=170)
- 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])
- # Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
- self.assertEqual(50, message_json["brightness"])
- self.assertEqual(155, message_json["color_temp"])
- self.assertEqual('colorloop', message_json["effect"])
- self.assertEqual(170, message_json["white_value"])
- self.assertEqual("ON", message_json["state"])
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual(50, state.attributes['brightness'])
- self.assertEqual(155, state.attributes['color_temp'])
- self.assertEqual('colorloop', state.attributes['effect'])
- self.assertEqual(170, state.attributes['white_value'])
-
- def test_flash_short_and_long(self): \
- # pylint: disable=invalid-name
- """Test for flash length being sent when included."""
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'flash_time_short': 5,
- 'flash_time_long': 15,
- 'qos': 0
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES))
-
- 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])
- # Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
- self.assertEqual(5, message_json["flash"])
- self.assertEqual("ON", message_json["state"])
-
- 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])
- # Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
- self.assertEqual(15, message_json["flash"])
- self.assertEqual("ON", message_json["state"])
-
- def test_transition(self):
- """Test for transition time being sent when included."""
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'qos': 0
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES))
-
- 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])
- # Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
- self.assertEqual(10, message_json["transition"])
- self.assertEqual("ON", message_json["state"])
-
- # Transition back off
- light.turn_off(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])
- # Get the sent message
- message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
- self.assertEqual(10, message_json["transition"])
- self.assertEqual("OFF", message_json["state"])
-
- def test_brightness_scale(self):
- """Test for brightness scaling."""
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_bright_scale',
- 'command_topic': 'test_light_bright_scale/set',
- 'brightness': True,
- 'brightness_scale': 99
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
-
- # Turn on the light
- fire_mqtt_message(self.hass, 'test_light_bright_scale',
- '{"state":"ON"}')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual(255, state.attributes.get('brightness'))
-
- # Turn on the light with brightness
- fire_mqtt_message(self.hass, 'test_light_bright_scale',
- '{"state":"ON",'
- '"brightness": 99}')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual(255, state.attributes.get('brightness'))
-
- def test_invalid_color_brightness_and_white_values(self): \
- # pylint: disable=invalid-name
- """Test that invalid color/brightness/white values are ignored."""
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'brightness': True,
- 'rgb': True,
- 'white_value': True,
- 'qos': '0'
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertEqual(185, state.attributes.get(ATTR_SUPPORTED_FEATURES))
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('white_value'))
- self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
-
- # Turn on the light
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"color":{"r":255,"g":255,"b":255},'
- '"brightness": 255,'
- '"white_value": 255}')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
- self.assertEqual(255, state.attributes.get('brightness'))
- self.assertEqual(255, state.attributes.get('white_value'))
-
- # Bad color values
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"color":{"r":"bad","g":"val","b":"test"}}')
- self.hass.block_till_done()
-
- # Color should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
-
- # Bad brightness values
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"brightness": "badValue"}')
- self.hass.block_till_done()
-
- # Brightness should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual(255, state.attributes.get('brightness'))
-
- # Bad white value
- fire_mqtt_message(self.hass, 'test_light_rgb',
- '{"state":"ON",'
- '"white_value": "badValue"}')
- self.hass.block_till_done()
-
- # White value should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual(255, state.attributes.get('white_value'))
-
- def test_default_availability_payload(self):
- """Test availability by default payload with defined topic."""
- self.assertTrue(setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'availability_topic': 'availability-topic'
- }
- }))
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'online')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertNotEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'offline')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
-
- def test_custom_availability_payload(self):
- """Test availability by custom payload with defined topic."""
- self.assertTrue(setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_json',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'availability_topic': 'availability-topic',
- 'payload_available': 'good',
- 'payload_not_available': 'nogood'
- }
- }))
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'good')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertNotEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'nogood')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
+"""The tests for the MQTT JSON light platform.
+
+Configuration with RGB, brightness, color temp, effect, white value and XY:
+
+light:
+ platform: mqtt_json
+ name: mqtt_json_light_1
+ state_topic: "home/rgb1"
+ command_topic: "home/rgb1/set"
+ brightness: true
+ color_temp: true
+ effect: true
+ rgb: true
+ white_value: true
+ xy: true
+
+Configuration with RGB, brightness, color temp, effect, white value:
+
+light:
+ platform: mqtt_json
+ name: mqtt_json_light_1
+ state_topic: "home/rgb1"
+ command_topic: "home/rgb1/set"
+ brightness: true
+ color_temp: true
+ effect: true
+ rgb: true
+ white_value: true
+
+Configuration with RGB, brightness, color temp and effect:
+
+light:
+ platform: mqtt_json
+ name: mqtt_json_light_1
+ state_topic: "home/rgb1"
+ command_topic: "home/rgb1/set"
+ brightness: true
+ color_temp: true
+ effect: true
+ rgb: true
+
+Configuration with RGB, brightness and color temp:
+
+light:
+ platform: mqtt_json
+ name: mqtt_json_light_1
+ state_topic: "home/rgb1"
+ command_topic: "home/rgb1/set"
+ brightness: true
+ rgb: true
+ color_temp: true
+
+Configuration with RGB, brightness:
+
+light:
+ platform: mqtt_json
+ name: mqtt_json_light_1
+ state_topic: "home/rgb1"
+ command_topic: "home/rgb1/set"
+ brightness: true
+ rgb: true
+
+Config without RGB:
+
+light:
+ platform: mqtt_json
+ name: mqtt_json_light_1
+ state_topic: "home/rgb1"
+ command_topic: "home/rgb1/set"
+ brightness: true
+
+Config without RGB and brightness:
+
+light:
+ platform: mqtt_json
+ name: mqtt_json_light_1
+ state_topic: "home/rgb1"
+ command_topic: "home/rgb1/set"
+
+Config with brightness and scale:
+
+light:
+ platform: mqtt_json
+ name: test
+ state_topic: "mqtt_json_light_1"
+ command_topic: "mqtt_json_light_1/set"
+ brightness: true
+ brightness_scale: 99
+"""
+
+import json
+import unittest
+
+from homeassistant.setup import setup_component
+from homeassistant.const import (
+ STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE,
+ ATTR_SUPPORTED_FEATURES)
+import homeassistant.components.light as light
+from tests.common import (
+ get_test_home_assistant, mock_mqtt_component, fire_mqtt_message,
+ assert_setup_component)
+
+
+class TestLightMQTTJSON(unittest.TestCase):
+ """Test the MQTT JSON light."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.mock_publish = mock_mqtt_component(self.hass)
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_fail_setup_if_no_command_topic(self): \
+ # pylint: disable=invalid-name
+ """Test if setup fails with no command topic."""
+ with assert_setup_component(0, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ }
+ })
+ self.assertIsNone(self.hass.states.get('light.test'))
+
+ def test_no_color_brightness_color_temp_white_val_if_no_topics(self): \
+ # pylint: disable=invalid-name
+ """Test for no RGB, brightness, color temp, effect, white val or XY."""
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('effect'))
+ self.assertIsNone(state.attributes.get('white_value'))
+ self.assertIsNone(state.attributes.get('xy_color'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('effect'))
+ self.assertIsNone(state.attributes.get('white_value'))
+ self.assertIsNone(state.attributes.get('xy_color'))
+
+ def test_controlling_state_via_topic(self): \
+ # pylint: disable=invalid-name
+ """Test the controlling of the state via topic."""
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'brightness': True,
+ 'color_temp': True,
+ 'effect': True,
+ 'rgb': True,
+ 'white_value': True,
+ 'xy': True,
+ 'qos': '0'
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(255, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('effect'))
+ self.assertIsNone(state.attributes.get('white_value'))
+ self.assertIsNone(state.attributes.get('xy_color'))
+ self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ # Turn on the light, full white
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"color":{"r":255,"g":255,"b":255,'
+ '"x":0.123,"y":0.123},'
+ '"brightness":255,'
+ '"color_temp":155,'
+ '"effect":"colorloop",'
+ '"white_value":150}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
+ self.assertEqual(255, state.attributes.get('brightness'))
+ self.assertEqual(155, state.attributes.get('color_temp'))
+ self.assertEqual('colorloop', state.attributes.get('effect'))
+ self.assertEqual(150, state.attributes.get('white_value'))
+ self.assertEqual([0.123, 0.123], state.attributes.get('xy_color'))
+
+ # Turn the light off
+ fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"brightness":100}')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.hass.block_till_done()
+ self.assertEqual(100,
+ light_state.attributes['brightness'])
+
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"color":{"r":125,"g":125,"b":125}}')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual([125, 125, 125],
+ light_state.attributes.get('rgb_color'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"color":{"x":0.135,"y":0.135}}')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual([0.135, 0.135],
+ light_state.attributes.get('xy_color'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"color_temp":155}')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual(155, light_state.attributes.get('color_temp'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"effect":"colorloop"}')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual('colorloop', light_state.attributes.get('effect'))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"white_value":155}')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual(155, light_state.attributes.get('white_value'))
+
+ def test_sending_mqtt_commands_and_optimistic(self): \
+ # pylint: disable=invalid-name
+ """Test the sending of command in optimistic mode."""
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'command_topic': 'test_light_rgb/set',
+ 'brightness': True,
+ 'color_temp': True,
+ 'effect': True,
+ 'rgb': True,
+ 'white_value': True,
+ 'qos': 2
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+ self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ 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])
+ 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])
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+
+ light.turn_on(self.hass, 'light.test',
+ brightness=50, color_temp=155, effect='colorloop',
+ white_value=170)
+ 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])
+ # Get the sent message
+ message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ self.assertEqual(50, message_json["brightness"])
+ self.assertEqual(155, message_json["color_temp"])
+ self.assertEqual('colorloop', message_json["effect"])
+ self.assertEqual(170, message_json["white_value"])
+ self.assertEqual("ON", message_json["state"])
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(50, state.attributes['brightness'])
+ self.assertEqual(155, state.attributes['color_temp'])
+ self.assertEqual('colorloop', state.attributes['effect'])
+ self.assertEqual(170, state.attributes['white_value'])
+
+ def test_flash_short_and_long(self): \
+ # pylint: disable=invalid-name
+ """Test for flash length being sent when included."""
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'flash_time_short': 5,
+ 'flash_time_long': 15,
+ 'qos': 0
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+
+ 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])
+ # Get the sent message
+ message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ self.assertEqual(5, message_json["flash"])
+ self.assertEqual("ON", message_json["state"])
+
+ 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])
+ # Get the sent message
+ message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ self.assertEqual(15, message_json["flash"])
+ self.assertEqual("ON", message_json["state"])
+
+ def test_transition(self):
+ """Test for transition time being sent when included."""
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'qos': 0
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(40, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+
+ 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])
+ # Get the sent message
+ message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ self.assertEqual(10, message_json["transition"])
+ self.assertEqual("ON", message_json["state"])
+
+ # Transition back off
+ light.turn_off(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])
+ # Get the sent message
+ message_json = json.loads(self.mock_publish.mock_calls[-2][1][1])
+ self.assertEqual(10, message_json["transition"])
+ self.assertEqual("OFF", message_json["state"])
+
+ def test_brightness_scale(self):
+ """Test for brightness scaling."""
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_bright_scale',
+ 'command_topic': 'test_light_bright_scale/set',
+ 'brightness': True,
+ 'brightness_scale': 99
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ # Turn on the light
+ fire_mqtt_message(self.hass, 'test_light_bright_scale',
+ '{"state":"ON"}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(255, state.attributes.get('brightness'))
+
+ # Turn on the light with brightness
+ fire_mqtt_message(self.hass, 'test_light_bright_scale',
+ '{"state":"ON",'
+ '"brightness": 99}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(255, state.attributes.get('brightness'))
+
+ def test_invalid_color_brightness_and_white_values(self): \
+ # pylint: disable=invalid-name
+ """Test that invalid color/brightness/white values are ignored."""
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'brightness': True,
+ 'rgb': True,
+ 'white_value': True,
+ 'qos': '0'
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertEqual(185, state.attributes.get(ATTR_SUPPORTED_FEATURES))
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('white_value'))
+ self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ # Turn on the light
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"color":{"r":255,"g":255,"b":255},'
+ '"brightness": 255,'
+ '"white_value": 255}')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
+ self.assertEqual(255, state.attributes.get('brightness'))
+ self.assertEqual(255, state.attributes.get('white_value'))
+
+ # Bad color values
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"color":{"r":"bad","g":"val","b":"test"}}')
+ self.hass.block_till_done()
+
+ # Color should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
+
+ # Bad brightness values
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"brightness": "badValue"}')
+ self.hass.block_till_done()
+
+ # Brightness should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(255, state.attributes.get('brightness'))
+
+ # Bad white value
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ '{"state":"ON",'
+ '"white_value": "badValue"}')
+ self.hass.block_till_done()
+
+ # White value should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(255, state.attributes.get('white_value'))
+
+ def test_default_availability_payload(self):
+ """Test availability by default payload with defined topic."""
+ self.assertTrue(setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'availability_topic': 'availability-topic'
+ }
+ }))
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'online')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertNotEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'offline')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
+
+ def test_custom_availability_payload(self):
+ """Test availability by custom payload with defined topic."""
+ self.assertTrue(setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_json',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'availability_topic': 'availability-topic',
+ 'payload_available': 'good',
+ 'payload_not_available': 'nogood'
+ }
+ }))
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'good')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertNotEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'nogood')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py
index be1f119fc14..0df9d8136e1 100644
--- a/tests/components/light/test_mqtt_template.py
+++ b/tests/components/light/test_mqtt_template.py
@@ -1,524 +1,524 @@
-"""The tests for the MQTT Template light platform.
-
-Configuration example with all features:
-
-light:
- platform: mqtt_template
- name: mqtt_template_light_1
- state_topic: 'home/rgb1'
- command_topic: 'home/rgb1/set'
- command_on_template: >
- on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }}
- command_off_template: 'off'
- state_template: '{{ value.split(",")[0] }}'
- brightness_template: '{{ value.split(",")[1] }}'
- color_temp_template: '{{ value.split(",")[2] }}'
- white_value_template: '{{ value.split(",")[3] }}'
- red_template: '{{ value.split(",")[4].split("-")[0] }}'
- green_template: '{{ value.split(",")[4].split("-")[1] }}'
- blue_template: '{{ value.split(",")[4].split("-")[2] }}'
-
-If your light doesn't support brightness feature, omit `brightness_template`.
-
-If your light doesn't support color temp feature, omit `color_temp_template`.
-
-If your light doesn't support white value feature, omit `white_value_template`.
-
-If your light doesn't support RGB feature, omit `(red|green|blue)_template`.
-"""
-import unittest
-
-from homeassistant.setup import setup_component
-from homeassistant.const import (
- STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE)
-import homeassistant.components.light as light
-from tests.common import (
- get_test_home_assistant, mock_mqtt_component, fire_mqtt_message,
- assert_setup_component)
-
-
-class TestLightMQTTTemplate(unittest.TestCase):
- """Test the MQTT Template light."""
-
- def setUp(self): # pylint: disable=invalid-name
- """Setup things to be run when tests are started."""
- self.hass = get_test_home_assistant()
- self.mock_publish = mock_mqtt_component(self.hass)
-
- def tearDown(self): # pylint: disable=invalid-name
- """Stop everything that was started."""
- self.hass.stop()
-
- def test_setup_fails(self): \
- # pylint: disable=invalid-name
- """Test that setup fails with missing required configuration items."""
- with assert_setup_component(0, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- }
- })
- self.assertIsNone(self.hass.states.get('light.test'))
-
- def test_state_change_via_topic(self): \
- # pylint: disable=invalid-name
- """Test state change via topic."""
- with assert_setup_component(1, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,'
- '{{ brightness|d }},'
- '{{ color_temp|d }},'
- '{{ white_value|d }},'
- '{{ red|d }}-'
- '{{ green|d }}-'
- '{{ blue|d }}',
- 'command_off_template': 'off',
- 'state_template': '{{ value.split(",")[0] }}'
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('color_temp'))
- self.assertIsNone(state.attributes.get('white_value'))
- self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
-
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('color_temp'))
- self.assertIsNone(state.attributes.get('white_value'))
-
- def test_state_brightness_color_effect_temp_white_change_via_topic(self): \
- # pylint: disable=invalid-name
- """Test state, bri, color, effect, color temp, white val change."""
- with assert_setup_component(1, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'effect_list': ['rainbow', 'colorloop'],
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,'
- '{{ brightness|d }},'
- '{{ color_temp|d }},'
- '{{ white_value|d }},'
- '{{ red|d }}-'
- '{{ green|d }}-'
- '{{ blue|d }},'
- '{{ effect|d }}',
- 'command_off_template': 'off',
- 'state_template': '{{ value.split(",")[0] }}',
- 'brightness_template': '{{ value.split(",")[1] }}',
- 'color_temp_template': '{{ value.split(",")[2] }}',
- 'white_value_template': '{{ value.split(",")[3] }}',
- 'red_template': '{{ value.split(",")[4].'
- 'split("-")[0] }}',
- 'green_template': '{{ value.split(",")[4].'
- 'split("-")[1] }}',
- 'blue_template': '{{ value.split(",")[4].'
- 'split("-")[2] }}',
- 'effect_template': '{{ value.split(",")[5] }}'
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('effect'))
- self.assertIsNone(state.attributes.get('color_temp'))
- self.assertIsNone(state.attributes.get('white_value'))
- self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
-
- # turn on the light, full white
- fire_mqtt_message(self.hass, 'test_light_rgb',
- 'on,255,145,123,255-128-64,')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual([255, 128, 64], state.attributes.get('rgb_color'))
- self.assertEqual(255, state.attributes.get('brightness'))
- self.assertEqual(145, state.attributes.get('color_temp'))
- self.assertEqual(123, state.attributes.get('white_value'))
- self.assertIsNone(state.attributes.get('effect'))
-
- # turn the light off
- fire_mqtt_message(self.hass, 'test_light_rgb', 'off')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
-
- # lower the brightness
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,100')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.hass.block_till_done()
- self.assertEqual(100, light_state.attributes['brightness'])
-
- # change the color temp
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,195')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.hass.block_till_done()
- self.assertEqual(195, light_state.attributes['color_temp'])
-
- # change the color
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,,41-42-43')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color'))
-
- # change the white value
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.hass.block_till_done()
- self.assertEqual(134, light_state.attributes['white_value'])
-
- # change the effect
- fire_mqtt_message(self.hass, 'test_light_rgb',
- 'on,,,,41-42-43,rainbow')
- self.hass.block_till_done()
-
- light_state = self.hass.states.get('light.test')
- self.assertEqual('rainbow', light_state.attributes.get('effect'))
-
- def test_optimistic(self): \
- # pylint: disable=invalid-name
- """Test optimistic mode."""
- with assert_setup_component(1, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,'
- '{{ brightness|d }},'
- '{{ color_temp|d }},'
- '{{ white_value|d }},'
- '{{ red|d }}-'
- '{{ green|d }}-'
- '{{ blue|d }}',
- 'command_off_template': 'off',
- 'qos': 2
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE))
-
- # turn on the light
- 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])
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
-
- # turn the light off
- 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])
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
-
- # turn on the light with brightness, color
- light.turn_on(self.hass, 'light.test', brightness=50,
- 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)
-
- # 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])
-
- # check the state
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual((75, 75, 75), state.attributes['rgb_color'])
- self.assertEqual(50, state.attributes['brightness'])
- self.assertEqual(200, state.attributes['color_temp'])
- self.assertEqual(139, state.attributes['white_value'])
-
- def test_flash(self): \
- # pylint: disable=invalid-name
- """Test flash."""
- with assert_setup_component(1, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,{{ flash }}',
- 'command_off_template': 'off',
- 'qos': 0
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
-
- # short flash
- 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)
-
- # 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)
-
- def test_transition(self):
- """Test for transition time being sent when included."""
- with assert_setup_component(1, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,{{ transition }}',
- 'command_off_template': 'off,{{ transition|d }}'
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
-
- # transition on
- 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)
-
- # 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)
-
- def test_invalid_values(self): \
- # pylint: disable=invalid-name
- """Test that invalid values are ignored."""
- with assert_setup_component(1, light.DOMAIN):
- assert setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'effect_list': ['rainbow', 'colorloop'],
- 'state_topic': 'test_light_rgb',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,'
- '{{ brightness|d }},'
- '{{ color_temp|d }},'
- '{{ red|d }}-'
- '{{ green|d }}-'
- '{{ blue|d }},'
- '{{ effect|d }}',
- 'command_off_template': 'off',
- 'state_template': '{{ value.split(",")[0] }}',
- 'brightness_template': '{{ value.split(",")[1] }}',
- 'color_temp_template': '{{ value.split(",")[2] }}',
- 'white_value_template': '{{ value.split(",")[3] }}',
- 'red_template': '{{ value.split(",")[4].'
- 'split("-")[0] }}',
- 'green_template': '{{ value.split(",")[4].'
- 'split("-")[1] }}',
- 'blue_template': '{{ value.split(",")[4].'
- 'split("-")[2] }}',
- 'effect_template': '{{ value.split(",")[5] }}',
- }
- })
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_OFF, state.state)
- self.assertIsNone(state.attributes.get('rgb_color'))
- self.assertIsNone(state.attributes.get('brightness'))
- self.assertIsNone(state.attributes.get('color_temp'))
- self.assertIsNone(state.attributes.get('effect'))
- self.assertIsNone(state.attributes.get('white_value'))
- self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
-
- # turn on the light, full white
- fire_mqtt_message(self.hass, 'test_light_rgb',
- 'on,255,215,222,255-255-255,rainbow')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
- self.assertEqual(255, state.attributes.get('brightness'))
- self.assertEqual(215, state.attributes.get('color_temp'))
- self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
- self.assertEqual(222, state.attributes.get('white_value'))
- self.assertEqual('rainbow', state.attributes.get('effect'))
-
- # bad state value
- fire_mqtt_message(self.hass, 'test_light_rgb', 'offf')
- self.hass.block_till_done()
-
- # state should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_ON, state.state)
-
- # bad brightness values
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,off,255-255-255')
- self.hass.block_till_done()
-
- # brightness should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual(255, state.attributes.get('brightness'))
-
- # bad color temp values
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,off,255-255-255')
- self.hass.block_till_done()
-
- # color temp should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual(215, state.attributes.get('color_temp'))
-
- # bad color values
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c')
- self.hass.block_till_done()
-
- # color should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
-
- # bad white value values
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,off,255-255-255')
- self.hass.block_till_done()
-
- # white value should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual(222, state.attributes.get('white_value'))
-
- # bad effect value
- fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white')
- self.hass.block_till_done()
-
- # effect should not have changed
- state = self.hass.states.get('light.test')
- self.assertEqual('rainbow', state.attributes.get('effect'))
-
- def test_default_availability_payload(self):
- """Test availability by default payload with defined topic."""
- self.assertTrue(setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,{{ transition }}',
- 'command_off_template': 'off,{{ transition|d }}',
- 'availability_topic': 'availability-topic'
- }
- }))
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'online')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertNotEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'offline')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
-
- def test_custom_availability_payload(self):
- """Test availability by custom payload with defined topic."""
- self.assertTrue(setup_component(self.hass, light.DOMAIN, {
- light.DOMAIN: {
- 'platform': 'mqtt_template',
- 'name': 'test',
- 'command_topic': 'test_light_rgb/set',
- 'command_on_template': 'on,{{ transition }}',
- 'command_off_template': 'off,{{ transition|d }}',
- 'availability_topic': 'availability-topic',
- 'payload_available': 'good',
- 'payload_not_available': 'nogood'
- }
- }))
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'good')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertNotEqual(STATE_UNAVAILABLE, state.state)
-
- fire_mqtt_message(self.hass, 'availability-topic', 'nogood')
- self.hass.block_till_done()
-
- state = self.hass.states.get('light.test')
- self.assertEqual(STATE_UNAVAILABLE, state.state)
+"""The tests for the MQTT Template light platform.
+
+Configuration example with all features:
+
+light:
+ platform: mqtt_template
+ name: mqtt_template_light_1
+ state_topic: 'home/rgb1'
+ command_topic: 'home/rgb1/set'
+ command_on_template: >
+ on,{{ brightness|d }},{{ red|d }}-{{ green|d }}-{{ blue|d }}
+ command_off_template: 'off'
+ state_template: '{{ value.split(",")[0] }}'
+ brightness_template: '{{ value.split(",")[1] }}'
+ color_temp_template: '{{ value.split(",")[2] }}'
+ white_value_template: '{{ value.split(",")[3] }}'
+ red_template: '{{ value.split(",")[4].split("-")[0] }}'
+ green_template: '{{ value.split(",")[4].split("-")[1] }}'
+ blue_template: '{{ value.split(",")[4].split("-")[2] }}'
+
+If your light doesn't support brightness feature, omit `brightness_template`.
+
+If your light doesn't support color temp feature, omit `color_temp_template`.
+
+If your light doesn't support white value feature, omit `white_value_template`.
+
+If your light doesn't support RGB feature, omit `(red|green|blue)_template`.
+"""
+import unittest
+
+from homeassistant.setup import setup_component
+from homeassistant.const import (
+ STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE)
+import homeassistant.components.light as light
+from tests.common import (
+ get_test_home_assistant, mock_mqtt_component, fire_mqtt_message,
+ assert_setup_component)
+
+
+class TestLightMQTTTemplate(unittest.TestCase):
+ """Test the MQTT Template light."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.mock_publish = mock_mqtt_component(self.hass)
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_setup_fails(self): \
+ # pylint: disable=invalid-name
+ """Test that setup fails with missing required configuration items."""
+ with assert_setup_component(0, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ }
+ })
+ self.assertIsNone(self.hass.states.get('light.test'))
+
+ def test_state_change_via_topic(self): \
+ # pylint: disable=invalid-name
+ """Test state change via topic."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,'
+ '{{ brightness|d }},'
+ '{{ color_temp|d }},'
+ '{{ white_value|d }},'
+ '{{ red|d }}-'
+ '{{ green|d }}-'
+ '{{ blue|d }}',
+ 'command_off_template': 'off',
+ 'state_template': '{{ value.split(",")[0] }}'
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('white_value'))
+ self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('white_value'))
+
+ def test_state_brightness_color_effect_temp_white_change_via_topic(self): \
+ # pylint: disable=invalid-name
+ """Test state, bri, color, effect, color temp, white val change."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'effect_list': ['rainbow', 'colorloop'],
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,'
+ '{{ brightness|d }},'
+ '{{ color_temp|d }},'
+ '{{ white_value|d }},'
+ '{{ red|d }}-'
+ '{{ green|d }}-'
+ '{{ blue|d }},'
+ '{{ effect|d }}',
+ 'command_off_template': 'off',
+ 'state_template': '{{ value.split(",")[0] }}',
+ 'brightness_template': '{{ value.split(",")[1] }}',
+ 'color_temp_template': '{{ value.split(",")[2] }}',
+ 'white_value_template': '{{ value.split(",")[3] }}',
+ 'red_template': '{{ value.split(",")[4].'
+ 'split("-")[0] }}',
+ 'green_template': '{{ value.split(",")[4].'
+ 'split("-")[1] }}',
+ 'blue_template': '{{ value.split(",")[4].'
+ 'split("-")[2] }}',
+ 'effect_template': '{{ value.split(",")[5] }}'
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('effect'))
+ self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('white_value'))
+ self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ # turn on the light, full white
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ 'on,255,145,123,255-128-64,')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual([255, 128, 64], state.attributes.get('rgb_color'))
+ self.assertEqual(255, state.attributes.get('brightness'))
+ self.assertEqual(145, state.attributes.get('color_temp'))
+ self.assertEqual(123, state.attributes.get('white_value'))
+ self.assertIsNone(state.attributes.get('effect'))
+
+ # turn the light off
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'off')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+
+ # lower the brightness
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,100')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.hass.block_till_done()
+ self.assertEqual(100, light_state.attributes['brightness'])
+
+ # change the color temp
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,195')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.hass.block_till_done()
+ self.assertEqual(195, light_state.attributes['color_temp'])
+
+ # change the color
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,,41-42-43')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color'))
+
+ # change the white value
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.hass.block_till_done()
+ self.assertEqual(134, light_state.attributes['white_value'])
+
+ # change the effect
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ 'on,,,,41-42-43,rainbow')
+ self.hass.block_till_done()
+
+ light_state = self.hass.states.get('light.test')
+ self.assertEqual('rainbow', light_state.attributes.get('effect'))
+
+ def test_optimistic(self): \
+ # pylint: disable=invalid-name
+ """Test optimistic mode."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,'
+ '{{ brightness|d }},'
+ '{{ color_temp|d }},'
+ '{{ white_value|d }},'
+ '{{ red|d }}-'
+ '{{ green|d }}-'
+ '{{ blue|d }}',
+ 'command_off_template': 'off',
+ 'qos': 2
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ # turn on the light
+ 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])
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+
+ # turn the light off
+ 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])
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+
+ # turn on the light with brightness, color
+ light.turn_on(self.hass, 'light.test', brightness=50,
+ 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)
+
+ # 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])
+
+ # check the state
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual((75, 75, 75), state.attributes['rgb_color'])
+ self.assertEqual(50, state.attributes['brightness'])
+ self.assertEqual(200, state.attributes['color_temp'])
+ self.assertEqual(139, state.attributes['white_value'])
+
+ def test_flash(self): \
+ # pylint: disable=invalid-name
+ """Test flash."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,{{ flash }}',
+ 'command_off_template': 'off',
+ 'qos': 0
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+
+ # short flash
+ 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)
+
+ # 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)
+
+ def test_transition(self):
+ """Test for transition time being sent when included."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,{{ transition }}',
+ 'command_off_template': 'off,{{ transition|d }}'
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+
+ # transition on
+ 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)
+
+ # 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)
+
+ def test_invalid_values(self): \
+ # pylint: disable=invalid-name
+ """Test that invalid values are ignored."""
+ with assert_setup_component(1, light.DOMAIN):
+ assert setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'effect_list': ['rainbow', 'colorloop'],
+ 'state_topic': 'test_light_rgb',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,'
+ '{{ brightness|d }},'
+ '{{ color_temp|d }},'
+ '{{ red|d }}-'
+ '{{ green|d }}-'
+ '{{ blue|d }},'
+ '{{ effect|d }}',
+ 'command_off_template': 'off',
+ 'state_template': '{{ value.split(",")[0] }}',
+ 'brightness_template': '{{ value.split(",")[1] }}',
+ 'color_temp_template': '{{ value.split(",")[2] }}',
+ 'white_value_template': '{{ value.split(",")[3] }}',
+ 'red_template': '{{ value.split(",")[4].'
+ 'split("-")[0] }}',
+ 'green_template': '{{ value.split(",")[4].'
+ 'split("-")[1] }}',
+ 'blue_template': '{{ value.split(",")[4].'
+ 'split("-")[2] }}',
+ 'effect_template': '{{ value.split(",")[5] }}',
+ }
+ })
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_OFF, state.state)
+ self.assertIsNone(state.attributes.get('rgb_color'))
+ self.assertIsNone(state.attributes.get('brightness'))
+ self.assertIsNone(state.attributes.get('color_temp'))
+ self.assertIsNone(state.attributes.get('effect'))
+ self.assertIsNone(state.attributes.get('white_value'))
+ self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE))
+
+ # turn on the light, full white
+ fire_mqtt_message(self.hass, 'test_light_rgb',
+ 'on,255,215,222,255-255-255,rainbow')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+ self.assertEqual(255, state.attributes.get('brightness'))
+ self.assertEqual(215, state.attributes.get('color_temp'))
+ self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
+ self.assertEqual(222, state.attributes.get('white_value'))
+ self.assertEqual('rainbow', state.attributes.get('effect'))
+
+ # bad state value
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'offf')
+ self.hass.block_till_done()
+
+ # state should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_ON, state.state)
+
+ # bad brightness values
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,off,255-255-255')
+ self.hass.block_till_done()
+
+ # brightness should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual(255, state.attributes.get('brightness'))
+
+ # bad color temp values
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,off,255-255-255')
+ self.hass.block_till_done()
+
+ # color temp should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual(215, state.attributes.get('color_temp'))
+
+ # bad color values
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c')
+ self.hass.block_till_done()
+
+ # color should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual([255, 255, 255], state.attributes.get('rgb_color'))
+
+ # bad white value values
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,off,255-255-255')
+ self.hass.block_till_done()
+
+ # white value should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual(222, state.attributes.get('white_value'))
+
+ # bad effect value
+ fire_mqtt_message(self.hass, 'test_light_rgb', 'on,255,a-b-c,white')
+ self.hass.block_till_done()
+
+ # effect should not have changed
+ state = self.hass.states.get('light.test')
+ self.assertEqual('rainbow', state.attributes.get('effect'))
+
+ def test_default_availability_payload(self):
+ """Test availability by default payload with defined topic."""
+ self.assertTrue(setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,{{ transition }}',
+ 'command_off_template': 'off,{{ transition|d }}',
+ 'availability_topic': 'availability-topic'
+ }
+ }))
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'online')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertNotEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'offline')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
+
+ def test_custom_availability_payload(self):
+ """Test availability by custom payload with defined topic."""
+ self.assertTrue(setup_component(self.hass, light.DOMAIN, {
+ light.DOMAIN: {
+ 'platform': 'mqtt_template',
+ 'name': 'test',
+ 'command_topic': 'test_light_rgb/set',
+ 'command_on_template': 'on,{{ transition }}',
+ 'command_off_template': 'off,{{ transition|d }}',
+ 'availability_topic': 'availability-topic',
+ 'payload_available': 'good',
+ 'payload_not_available': 'nogood'
+ }
+ }))
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'good')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertNotEqual(STATE_UNAVAILABLE, state.state)
+
+ fire_mqtt_message(self.hass, 'availability-topic', 'nogood')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('light.test')
+ self.assertEqual(STATE_UNAVAILABLE, state.state)
diff --git a/tests/components/light/test_rflink.py b/tests/components/light/test_rflink.py
index 25f83b1d123..a6e6d3c1a85 100644
--- a/tests/components/light/test_rflink.py
+++ b/tests/components/light/test_rflink.py
@@ -254,7 +254,7 @@ def test_signal_repetitions(hass, monkeypatch):
assert protocol.send_command_ack.call_count == 2
- # test if default apply to configured devcies
+ # test if default apply to configured devices
hass.async_add_job(
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: DOMAIN + '.test1'}))
diff --git a/tests/components/light/test_rfxtrx.py b/tests/components/light/test_rfxtrx.py
index eef54a6c258..a1f63e45748 100644
--- a/tests/components/light/test_rfxtrx.py
+++ b/tests/components/light/test_rfxtrx.py
@@ -248,7 +248,7 @@ class TestLightRfxtrx(unittest.TestCase):
rfxtrx_core.RECEIVED_EVT_SUBSCRIBERS[0](event)
self.assertEqual(2, len(rfxtrx_core.RFX_DEVICES))
- # trying to add a swicth
+ # trying to add a switch
event = rfxtrx_core.get_rfx_object('0b1100100118cdea02010f70')
event.data = bytearray([0x0b, 0x11, 0x00, 0x10, 0x01, 0x18,
0xcd, 0xea, 0x01, 0x01, 0x0f, 0x70])
diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py
index a260d160bb5..b925b74a7f0 100644
--- a/tests/components/light/test_zwave.py
+++ b/tests/components/light/test_zwave.py
@@ -208,7 +208,7 @@ def test_set_rgb_color(mock_openzwave):
node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts RGB only
+ # Supports RGB only
color_channels = MockValue(data=0x1c, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
@@ -226,7 +226,7 @@ def test_set_rgbw_color(mock_openzwave):
node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts RGBW
+ # Supports RGBW
color_channels = MockValue(data=0x1d, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
@@ -245,7 +245,7 @@ def test_zw098_set_color_temp(mock_openzwave):
command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts RGB, warm white, cold white
+ # Supports RGB, warm white, cold white
color_channels = MockValue(data=0x1f, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
@@ -267,7 +267,7 @@ def test_rgb_not_supported(mock_openzwave):
node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts color temperature only
+ # Supports color temperature only
color_channels = MockValue(data=0x01, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
@@ -302,7 +302,7 @@ def test_rgb_value_changed(mock_openzwave):
node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts RGB only
+ # Supports RGB only
color_channels = MockValue(data=0x1c, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
@@ -321,7 +321,7 @@ def test_rgbww_value_changed(mock_openzwave):
node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts RGB, Warm White
+ # Supports RGB, Warm White
color_channels = MockValue(data=0x1d, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
@@ -340,7 +340,7 @@ def test_rgbcw_value_changed(mock_openzwave):
node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts RGB, Cold White
+ # Supports RGB, Cold White
color_channels = MockValue(data=0x1e, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
@@ -360,7 +360,7 @@ def test_ct_value_changed(mock_openzwave):
command_classes=[const.COMMAND_CLASS_SWITCH_COLOR])
value = MockValue(data=0, node=node)
color = MockValue(data='#0000000000', node=node)
- # Suppoorts RGB, Cold White
+ # Supports RGB, Cold White
color_channels = MockValue(data=0x1f, node=node)
values = MockLightValues(primary=value, color=color,
color_channels=color_channels)
diff --git a/tests/components/media_player/test_mediaroom.py b/tests/components/media_player/test_mediaroom.py
new file mode 100644
index 00000000000..7c7922b87be
--- /dev/null
+++ b/tests/components/media_player/test_mediaroom.py
@@ -0,0 +1,32 @@
+"""The tests for the mediaroom media_player."""
+
+import unittest
+
+from homeassistant.setup import setup_component
+import homeassistant.components.media_player as media_player
+from tests.common import (
+ assert_setup_component, get_test_home_assistant)
+
+
+class TestMediaroom(unittest.TestCase):
+ """Tests the Mediaroom Component."""
+
+ def setUp(self):
+ """Initialize values for this test case class."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Stop everything that we started."""
+ self.hass.stop()
+
+ def test_mediaroom_config(self):
+ """Test set up the platform with basic configuration."""
+ config = {
+ media_player.DOMAIN: {
+ 'platform': 'mediaroom',
+ 'name': 'Living Room'
+ }
+ }
+ with assert_setup_component(1, media_player.DOMAIN) as result_config:
+ assert setup_component(self.hass, media_player.DOMAIN, config)
+ assert result_config[media_player.DOMAIN]
diff --git a/tests/components/media_player/test_samsungtv.py b/tests/components/media_player/test_samsungtv.py
new file mode 100644
index 00000000000..c3753eb53b5
--- /dev/null
+++ b/tests/components/media_player/test_samsungtv.py
@@ -0,0 +1,271 @@
+"""Tests for samsungtv Components."""
+import unittest
+from subprocess import CalledProcessError
+
+from asynctest import mock
+
+import tests.common
+from homeassistant.components.media_player import SUPPORT_TURN_ON
+from homeassistant.components.media_player.samsungtv import setup_platform, \
+ CONF_TIMEOUT, SamsungTVDevice, SUPPORT_SAMSUNGTV
+from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_ON, \
+ CONF_MAC, STATE_OFF
+from tests.common import MockDependency
+from homeassistant.util import dt as dt_util
+from datetime import timedelta
+
+WORKING_CONFIG = {
+ CONF_HOST: 'fake',
+ CONF_NAME: 'fake',
+ CONF_PORT: 8001,
+ CONF_TIMEOUT: 10,
+ CONF_MAC: 'fake'
+}
+
+DISCOVERY_INFO = {
+ 'name': 'fake',
+ 'model_name': 'fake',
+ 'host': 'fake'
+}
+
+
+class PackageException(Exception):
+ """Dummy Exception."""
+
+
+class TestSamsungTv(unittest.TestCase):
+ """Testing Samsungtv component."""
+
+ @MockDependency('samsungctl')
+ @MockDependency('wakeonlan')
+ def setUp(self, samsung_mock, wol_mock):
+ """Setting up test environment."""
+ self.hass = tests.common.get_test_home_assistant()
+ self.hass.start()
+ self.hass.block_till_done()
+ self.device = SamsungTVDevice(**WORKING_CONFIG)
+ self.device._exceptions_class = mock.Mock()
+ self.device._exceptions_class.UnhandledResponse = PackageException
+ self.device._exceptions_class.AccessDenied = PackageException
+ self.device._exceptions_class.ConnectionClosed = PackageException
+
+ def tearDown(self):
+ """Tear down test data."""
+ self.hass.stop()
+
+ @MockDependency('samsungctl')
+ @MockDependency('wakeonlan')
+ def test_setup(self, samsung_mock, wol_mock):
+ """Testing setup of platform."""
+ with mock.patch(
+ 'homeassistant.components.media_player.samsungtv.socket'):
+ add_devices = mock.Mock()
+ setup_platform(
+ self.hass, WORKING_CONFIG, add_devices)
+
+ @MockDependency('samsungctl')
+ @MockDependency('wakeonlan')
+ def test_setup_discovery(self, samsung_mock, wol_mock):
+ """Testing setup of platform with discovery."""
+ with mock.patch(
+ 'homeassistant.components.media_player.samsungtv.socket'):
+ add_devices = mock.Mock()
+ setup_platform(self.hass, {}, add_devices,
+ discovery_info=DISCOVERY_INFO)
+
+ @MockDependency('samsungctl')
+ @MockDependency('wakeonlan')
+ @mock.patch(
+ 'homeassistant.components.media_player.samsungtv._LOGGER.warning')
+ def test_setup_none(self, samsung_mock, wol_mock, mocked_warn):
+ """Testing setup of platform with no data."""
+ with mock.patch(
+ 'homeassistant.components.media_player.samsungtv.socket'):
+ add_devices = mock.Mock()
+ setup_platform(self.hass, {}, add_devices,
+ discovery_info=None)
+ mocked_warn.assert_called_once_with("Cannot determine device")
+ add_devices.assert_not_called()
+
+ @mock.patch(
+ 'homeassistant.components.media_player.samsungtv.subprocess.Popen'
+ )
+ def test_update_on(self, mocked_popen):
+ """Testing update tv on."""
+ ping = mock.Mock()
+ mocked_popen.return_value = ping
+ ping.returncode = 0
+ self.device.update()
+ self.assertEqual(STATE_ON, self.device._state)
+
+ @mock.patch(
+ 'homeassistant.components.media_player.samsungtv.subprocess.Popen'
+ )
+ def test_update_off(self, mocked_popen):
+ """Testing update tv off."""
+ ping = mock.Mock()
+ mocked_popen.return_value = ping
+ ping.returncode = 1
+ self.device.update()
+ self.assertEqual(STATE_OFF, self.device._state)
+ ping = mock.Mock()
+ ping.communicate = mock.Mock(
+ side_effect=CalledProcessError("BOOM", None))
+ mocked_popen.return_value = ping
+ self.device.update()
+ self.assertEqual(STATE_OFF, self.device._state)
+
+ def test_send_key(self):
+ """Test for send key."""
+ self.device.send_key('KEY_POWER')
+ self.assertEqual(STATE_ON, self.device._state)
+
+ def test_send_key_broken_pipe(self):
+ """Testing broken pipe Exception."""
+ _remote = mock.Mock()
+ self.device.get_remote = mock.Mock()
+ _remote.control = mock.Mock(
+ side_effect=BrokenPipeError("Boom"))
+ self.device.get_remote.return_value = _remote
+ self.device.send_key("HELLO")
+ self.assertIsNone(self.device._remote)
+ self.assertEqual(STATE_ON, self.device._state)
+
+ def test_send_key_os_error(self):
+ """Testing broken pipe Exception."""
+ _remote = mock.Mock()
+ self.device.get_remote = mock.Mock()
+ _remote.control = mock.Mock(
+ side_effect=OSError("Boom"))
+ self.device.get_remote.return_value = _remote
+ self.device.send_key("HELLO")
+ self.assertIsNone(self.device._remote)
+ self.assertEqual(STATE_OFF, self.device._state)
+
+ def test_power_off_in_progress(self):
+ """Test for power_off_in_progress."""
+ self.assertFalse(self.device._power_off_in_progress())
+ self.device._end_of_power_off = dt_util.utcnow() + timedelta(
+ seconds=15)
+ self.assertTrue(self.device._power_off_in_progress())
+
+ def test_name(self):
+ """Test for name property."""
+ self.assertEqual('fake', self.device.name)
+
+ def test_state(self):
+ """Test for state property."""
+ self.device._state = STATE_ON
+ self.assertEqual(STATE_ON, self.device.state)
+ self.device._state = STATE_OFF
+ self.assertEqual(STATE_OFF, self.device.state)
+
+ def test_is_volume_muted(self):
+ """Test for is_volume_muted property."""
+ self.device._muted = False
+ self.assertFalse(self.device.is_volume_muted)
+ self.device._muted = True
+ self.assertTrue(self.device.is_volume_muted)
+
+ def test_supported_features(self):
+ """Test for supported_features property."""
+ self.device._mac = None
+ self.assertEqual(SUPPORT_SAMSUNGTV, self.device.supported_features)
+ self.device._mac = "fake"
+ self.assertEqual(
+ SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON,
+ self.device.supported_features)
+
+ def test_turn_off(self):
+ """Test for turn_off."""
+ self.device.send_key = mock.Mock()
+ _remote = mock.Mock()
+ _remote.close = mock.Mock()
+ self.get_remote = mock.Mock(return_value=_remote)
+ self.device._end_of_power_off = None
+ self.device.turn_off()
+ self.assertIsNotNone(self.device._end_of_power_off)
+ self.device.send_key.assert_called_once_with('KEY_POWER')
+ self.device.send_key = mock.Mock()
+ self.device._config['method'] = 'legacy'
+ self.device.turn_off()
+ self.device.send_key.assert_called_once_with('KEY_POWEROFF')
+
+ @mock.patch(
+ 'homeassistant.components.media_player.samsungtv._LOGGER.debug')
+ def test_turn_off_os_error(self, mocked_debug):
+ """Test for turn_off with OSError."""
+ _remote = mock.Mock()
+ _remote.close = mock.Mock(side_effect=OSError("BOOM"))
+ self.device.get_remote = mock.Mock(return_value=_remote)
+ self.device.turn_off()
+ mocked_debug.assert_called_once_with("Could not establish connection.")
+
+ def test_volume_up(self):
+ """Test for volume_up."""
+ self.device.send_key = mock.Mock()
+ self.device.volume_up()
+ self.device.send_key.assert_called_once_with("KEY_VOLUP")
+
+ def test_volume_down(self):
+ """Test for volume_down."""
+ self.device.send_key = mock.Mock()
+ self.device.volume_down()
+ self.device.send_key.assert_called_once_with("KEY_VOLDOWN")
+
+ def test_mute_volume(self):
+ """Test for mute_volume."""
+ self.device.send_key = mock.Mock()
+ self.device.mute_volume(True)
+ self.device.send_key.assert_called_once_with("KEY_MUTE")
+
+ def test_media_play_pause(self):
+ """Test for media_next_track."""
+ self.device.send_key = mock.Mock()
+ self.device._playing = False
+ self.device.media_play_pause()
+ self.device.send_key.assert_called_once_with("KEY_PLAY")
+ self.assertTrue(self.device._playing)
+ self.device.send_key = mock.Mock()
+ self.device.media_play_pause()
+ self.device.send_key.assert_called_once_with("KEY_PAUSE")
+ self.assertFalse(self.device._playing)
+
+ def test_media_play(self):
+ """Test for media_play."""
+ self.device.send_key = mock.Mock()
+ self.device._playing = False
+ self.device.media_play()
+ self.device.send_key.assert_called_once_with("KEY_PLAY")
+ self.assertTrue(self.device._playing)
+
+ def test_media_pause(self):
+ """Test for media_pause."""
+ self.device.send_key = mock.Mock()
+ self.device._playing = True
+ self.device.media_pause()
+ self.device.send_key.assert_called_once_with("KEY_PAUSE")
+ self.assertFalse(self.device._playing)
+
+ def test_media_next_track(self):
+ """Test for media_next_track."""
+ self.device.send_key = mock.Mock()
+ self.device.media_next_track()
+ self.device.send_key.assert_called_once_with("KEY_FF")
+
+ def test_media_previous_track(self):
+ """Test for media_previous_track."""
+ self.device.send_key = mock.Mock()
+ self.device.media_previous_track()
+ self.device.send_key.assert_called_once_with("KEY_REWIND")
+
+ def test_turn_on(self):
+ """Test turn on."""
+ self.device.send_key = mock.Mock()
+ self.device._mac = None
+ self.device.turn_on()
+ self.device.send_key.assert_called_once_with('KEY_POWERON')
+ self.device._wol.send_magic_packet = mock.Mock()
+ self.device._mac = "fake"
+ self.device.turn_on()
+ self.device._wol.send_magic_packet.assert_called_once_with("fake")
diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py
index 815204e718a..d3ebc67931f 100644
--- a/tests/components/media_player/test_sonos.py
+++ b/tests/components/media_player/test_sonos.py
@@ -152,7 +152,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch('soco.discover')
def test_ensure_setup_config_interface_addr(self, discover_mock, *args):
- """Test a interface address config'd by the HASS config file."""
+ """Test an interface address config'd by the HASS config file."""
discover_mock.return_value = {SoCoMock('192.0.2.1')}
config = {
@@ -172,7 +172,7 @@ class TestSonosMediaPlayer(unittest.TestCase):
@mock.patch('soco.discover')
def test_ensure_setup_config_advertise_addr(self, discover_mock,
*args):
- """Test a advertise address config'd by the HASS config file."""
+ """Test an advertise address config'd by the HASS config file."""
discover_mock.return_value = {SoCoMock('192.0.2.1')}
config = {
diff --git a/tests/components/media_player/test_soundtouch.py b/tests/components/media_player/test_soundtouch.py
index a8242b39f7f..2da2622e08a 100644
--- a/tests/components/media_player/test_soundtouch.py
+++ b/tests/components/media_player/test_soundtouch.py
@@ -158,7 +158,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
self.hass.stop()
@mock.patch('libsoundtouch.soundtouch_device', side_effect=None)
- def test_ensure_setup_config(self, mocked_sountouch_device):
+ def test_ensure_setup_config(self, mocked_soundtouch_device):
"""Test setup OK with custom config."""
soundtouch.setup_platform(self.hass,
default_component(),
@@ -167,10 +167,10 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
self.assertEqual(len(all_devices), 1)
self.assertEqual(all_devices[0].name, 'soundtouch')
self.assertEqual(all_devices[0].config['port'], 8090)
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
@mock.patch('libsoundtouch.soundtouch_device', side_effect=None)
- def test_ensure_setup_discovery(self, mocked_sountouch_device):
+ def test_ensure_setup_discovery(self, mocked_soundtouch_device):
"""Test setup with discovery."""
new_device = {"port": "8090",
"host": "192.168.1.1",
@@ -184,11 +184,11 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
self.assertEqual(len(all_devices), 1)
self.assertEqual(all_devices[0].config['port'], 8090)
self.assertEqual(all_devices[0].config['host'], '192.168.1.1')
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
@mock.patch('libsoundtouch.soundtouch_device', side_effect=None)
def test_ensure_setup_discovery_no_duplicate(self,
- mocked_sountouch_device):
+ mocked_soundtouch_device):
"""Test setup OK if device already exists."""
soundtouch.setup_platform(self.hass,
default_component(),
@@ -213,20 +213,20 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock(),
existing_device # Existing device
)
- self.assertEqual(mocked_sountouch_device.call_count, 2)
+ self.assertEqual(mocked_soundtouch_device.call_count, 2)
self.assertEqual(len(self.hass.data[soundtouch.DATA_SOUNDTOUCH]), 2)
@mock.patch('libsoundtouch.device.SoundTouchDevice.volume')
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_update(self, mocked_sountouch_device, mocked_status,
+ def test_update(self, mocked_soundtouch_device, mocked_status,
mocked_volume):
"""Test update device state."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
self.hass.data[soundtouch.DATA_SOUNDTOUCH][0].update()
@@ -238,13 +238,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
side_effect=MockStatusPlaying)
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_playing_media(self, mocked_sountouch_device, mocked_status,
+ def test_playing_media(self, mocked_soundtouch_device, mocked_status,
mocked_volume):
"""Test playing media info."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
@@ -261,13 +261,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
side_effect=MockStatusUnknown)
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_playing_unknown_media(self, mocked_sountouch_device,
+ def test_playing_unknown_media(self, mocked_soundtouch_device,
mocked_status, mocked_volume):
"""Test playing media info."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
@@ -278,13 +278,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
side_effect=MockStatusPlayingRadio)
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_playing_radio(self, mocked_sountouch_device, mocked_status,
+ def test_playing_radio(self, mocked_soundtouch_device, mocked_status,
mocked_volume):
"""Test playing radio info."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
@@ -301,13 +301,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_get_volume_level(self, mocked_sountouch_device, mocked_status,
+ def test_get_volume_level(self, mocked_soundtouch_device, mocked_status,
mocked_volume):
"""Test volume level."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
@@ -318,13 +318,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
side_effect=MockStatusStandby)
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_get_state_off(self, mocked_sountouch_device, mocked_status,
+ def test_get_state_off(self, mocked_soundtouch_device, mocked_status,
mocked_volume):
"""Test state device is off."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
@@ -335,13 +335,13 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
side_effect=MockStatusPause)
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_get_state_pause(self, mocked_sountouch_device, mocked_status,
+ def test_get_state_pause(self, mocked_soundtouch_device, mocked_status,
mocked_volume):
"""Test state device is paused."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
@@ -352,25 +352,25 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_is_muted(self, mocked_sountouch_device, mocked_status,
+ def test_is_muted(self, mocked_soundtouch_device, mocked_status,
mocked_volume):
"""Test device volume is muted."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
self.assertEqual(all_devices[0].is_volume_muted, True)
@mock.patch('libsoundtouch.soundtouch_device')
- def test_media_commands(self, mocked_sountouch_device):
+ def test_media_commands(self, mocked_soundtouch_device):
"""Test supported media commands."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
self.assertEqual(all_devices[0].supported_features, 17853)
@@ -379,7 +379,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_should_turn_off(self, mocked_sountouch_device, mocked_status,
+ def test_should_turn_off(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_power_off):
"""Test device is turned off."""
soundtouch.setup_platform(self.hass,
@@ -387,7 +387,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].turn_off()
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 1)
self.assertEqual(mocked_power_off.call_count, 1)
@@ -397,7 +397,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_should_turn_on(self, mocked_sountouch_device, mocked_status,
+ def test_should_turn_on(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_power_on):
"""Test device is turned on."""
soundtouch.setup_platform(self.hass,
@@ -405,7 +405,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].turn_on()
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 1)
self.assertEqual(mocked_power_on.call_count, 1)
@@ -415,7 +415,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_volume_up(self, mocked_sountouch_device, mocked_status,
+ def test_volume_up(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_volume_up):
"""Test volume up."""
soundtouch.setup_platform(self.hass,
@@ -423,7 +423,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].volume_up()
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 2)
self.assertEqual(mocked_volume_up.call_count, 1)
@@ -433,7 +433,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_volume_down(self, mocked_sountouch_device, mocked_status,
+ def test_volume_down(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_volume_down):
"""Test volume down."""
soundtouch.setup_platform(self.hass,
@@ -441,7 +441,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].volume_down()
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 2)
self.assertEqual(mocked_volume_down.call_count, 1)
@@ -451,7 +451,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_set_volume_level(self, mocked_sountouch_device, mocked_status,
+ def test_set_volume_level(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_set_volume):
"""Test set volume level."""
soundtouch.setup_platform(self.hass,
@@ -459,7 +459,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].set_volume_level(0.17)
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 2)
mocked_set_volume.assert_called_with(17)
@@ -469,7 +469,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_mute(self, mocked_sountouch_device, mocked_status, mocked_volume,
+ def test_mute(self, mocked_soundtouch_device, mocked_status, mocked_volume,
mocked_mute):
"""Test mute volume."""
soundtouch.setup_platform(self.hass,
@@ -477,7 +477,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].mute_volume(None)
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 2)
self.assertEqual(mocked_mute.call_count, 1)
@@ -487,7 +487,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_play(self, mocked_sountouch_device, mocked_status, mocked_volume,
+ def test_play(self, mocked_soundtouch_device, mocked_status, mocked_volume,
mocked_play):
"""Test play command."""
soundtouch.setup_platform(self.hass,
@@ -495,7 +495,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].media_play()
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 1)
self.assertEqual(mocked_play.call_count, 1)
@@ -505,15 +505,15 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_pause(self, mocked_sountouch_device, mocked_status, mocked_volume,
- mocked_pause):
+ def test_pause(self, mocked_soundtouch_device, mocked_status,
+ mocked_volume, mocked_pause):
"""Test pause command."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].media_pause()
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 1)
self.assertEqual(mocked_pause.call_count, 1)
@@ -523,7 +523,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_play_pause_play(self, mocked_sountouch_device, mocked_status,
+ def test_play_pause_play(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_play_pause):
"""Test play/pause."""
soundtouch.setup_platform(self.hass,
@@ -531,7 +531,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].media_play_pause()
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 1)
self.assertEqual(mocked_play_pause.call_count, 1)
@@ -542,7 +542,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_next_previous_track(self, mocked_sountouch_device, mocked_status,
+ def test_next_previous_track(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_next_track,
mocked_previous_track):
"""Test next/previous track."""
@@ -550,7 +550,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
default_component(),
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices[0].media_next_track()
@@ -567,14 +567,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_play_media(self, mocked_sountouch_device, mocked_status,
+ def test_play_media(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_presets, mocked_select_preset):
"""Test play preset 1."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices[0].play_media('PLAYLIST', 1)
@@ -589,14 +589,14 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_play_media_url(self, mocked_sountouch_device, mocked_status,
+ def test_play_media_url(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_play_url):
"""Test play preset 1."""
soundtouch.setup_platform(self.hass,
default_component(),
mock.MagicMock())
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
- self.assertEqual(mocked_sountouch_device.call_count, 1)
+ self.assertEqual(mocked_soundtouch_device.call_count, 1)
self.assertEqual(mocked_status.call_count, 1)
self.assertEqual(mocked_volume.call_count, 1)
all_devices[0].play_media('MUSIC', "http://fqdn/file.mp3")
@@ -607,7 +607,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_play_everywhere(self, mocked_sountouch_device, mocked_status,
+ def test_play_everywhere(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_create_zone):
"""Test play everywhere."""
soundtouch.setup_platform(self.hass,
@@ -619,7 +619,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].entity_id = "media_player.entity_1"
all_devices[1].entity_id = "media_player.entity_2"
- self.assertEqual(mocked_sountouch_device.call_count, 2)
+ self.assertEqual(mocked_soundtouch_device.call_count, 2)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 2)
@@ -647,7 +647,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_create_zone(self, mocked_sountouch_device, mocked_status,
+ def test_create_zone(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_create_zone):
"""Test creating a zone."""
soundtouch.setup_platform(self.hass,
@@ -659,7 +659,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].entity_id = "media_player.entity_1"
all_devices[1].entity_id = "media_player.entity_2"
- self.assertEqual(mocked_sountouch_device.call_count, 2)
+ self.assertEqual(mocked_soundtouch_device.call_count, 2)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 2)
@@ -689,7 +689,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_remove_zone_slave(self, mocked_sountouch_device, mocked_status,
+ def test_remove_zone_slave(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_remove_zone_slave):
"""Test adding a slave to an existing zone."""
soundtouch.setup_platform(self.hass,
@@ -701,7 +701,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].entity_id = "media_player.entity_1"
all_devices[1].entity_id = "media_player.entity_2"
- self.assertEqual(mocked_sountouch_device.call_count, 2)
+ self.assertEqual(mocked_soundtouch_device.call_count, 2)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 2)
@@ -731,7 +731,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
@mock.patch('libsoundtouch.device.SoundTouchDevice.status')
@mock.patch('libsoundtouch.soundtouch_device',
side_effect=_mock_soundtouch_device)
- def test_add_zone_slave(self, mocked_sountouch_device, mocked_status,
+ def test_add_zone_slave(self, mocked_soundtouch_device, mocked_status,
mocked_volume, mocked_add_zone_slave):
"""Test removing a slave from a zone."""
soundtouch.setup_platform(self.hass,
@@ -743,7 +743,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase):
all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH]
all_devices[0].entity_id = "media_player.entity_1"
all_devices[1].entity_id = "media_player.entity_2"
- self.assertEqual(mocked_sountouch_device.call_count, 2)
+ self.assertEqual(mocked_soundtouch_device.call_count, 2)
self.assertEqual(mocked_status.call_count, 2)
self.assertEqual(mocked_volume.call_count, 2)
diff --git a/tests/components/media_player/test_yamaha.py b/tests/components/media_player/test_yamaha.py
index 3322f6021e7..e17241485db 100644
--- a/tests/components/media_player/test_yamaha.py
+++ b/tests/components/media_player/test_yamaha.py
@@ -44,7 +44,7 @@ class TestYamahaMediaPlayer(unittest.TestCase):
self.hass.stop()
def enable_output(self, port, enabled):
- """Enable ouput on a specific port."""
+ """Enable output on a specific port."""
data = {
'entity_id': 'media_player.yamaha_receiver_main_zone',
'port': port,
diff --git a/tests/components/notify/test_facebook.py b/tests/components/notify/test_facebook.py
index 7bc7a55869a..b94a4c38a40 100644
--- a/tests/components/notify/test_facebook.py
+++ b/tests/components/notify/test_facebook.py
@@ -6,7 +6,7 @@ import homeassistant.components.notify.facebook as facebook
class TestFacebook(unittest.TestCase):
- """Tests for Facebook notifification service."""
+ """Tests for Facebook notification service."""
def setUp(self):
"""Set up test variables."""
diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py
index 7c558f2803d..5ac9b3adb81 100644
--- a/tests/components/recorder/test_migrate.py
+++ b/tests/components/recorder/test_migrate.py
@@ -25,7 +25,7 @@ def create_engine_test(*args, **kwargs):
@asyncio.coroutine
def test_schema_update_calls(hass):
- """Test that schema migrations occurr in correct order."""
+ """Test that schema migrations occur in correct order."""
with patch('sqlalchemy.create_engine', new=create_engine_test), \
patch('homeassistant.components.recorder.migration._apply_update') as \
update:
diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py
index b35b5630d60..79e2bf4ee35 100644
--- a/tests/components/sensor/test_canary.py
+++ b/tests/components/sensor/test_canary.py
@@ -3,13 +3,13 @@ import copy
import unittest
from unittest.mock import Mock
-from canary.api import SensorType
from homeassistant.components.canary import DATA_CANARY
from homeassistant.components.sensor import canary
-from homeassistant.components.sensor.canary import CanarySensor
+from homeassistant.components.sensor.canary import CanarySensor, \
+ SENSOR_TYPES, ATTR_AIR_QUALITY, STATE_AIR_QUALITY_NORMAL, \
+ STATE_AIR_QUALITY_ABNORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL
from tests.common import (get_test_home_assistant)
-from tests.components.test_canary import mock_device, mock_reading, \
- mock_location
+from tests.components.test_canary import mock_device, mock_location
VALID_CONFIG = {
"canary": {
@@ -55,38 +55,33 @@ class TestCanarySensorSetup(unittest.TestCase):
self.assertEqual(6, len(self.DEVICES))
- def test_celsius_temperature_sensor(self):
- """Test temperature sensor with celsius."""
- device = mock_device(10, "Family Room")
- location = mock_location("Home", True)
-
- data = Mock()
- data.get_readings.return_value = [
- mock_reading(SensorType.TEMPERATURE, 21.1234)]
-
- sensor = CanarySensor(data, SensorType.TEMPERATURE, location, device)
- sensor.update()
-
- self.assertEqual("Home Family Room Temperature", sensor.name)
- self.assertEqual("sensor_canary_10_temperature", sensor.unique_id)
- self.assertEqual("°C", sensor.unit_of_measurement)
- self.assertEqual(21.1, sensor.state)
-
- def test_fahrenheit_temperature_sensor(self):
+ def test_temperature_sensor(self):
"""Test temperature sensor with fahrenheit."""
device = mock_device(10, "Family Room")
location = mock_location("Home", False)
data = Mock()
- data.get_readings.return_value = [
- mock_reading(SensorType.TEMPERATURE, 21.1567)]
+ data.get_reading.return_value = 21.1234
- sensor = CanarySensor(data, SensorType.TEMPERATURE, location, device)
+ sensor = CanarySensor(data, SENSOR_TYPES[0], location, device)
sensor.update()
self.assertEqual("Home Family Room Temperature", sensor.name)
- self.assertEqual("°F", sensor.unit_of_measurement)
- self.assertEqual(21.2, sensor.state)
+ self.assertEqual("°C", sensor.unit_of_measurement)
+ self.assertEqual(21.12, sensor.state)
+
+ def test_temperature_sensor_with_none_sensor_value(self):
+ """Test temperature sensor with fahrenheit."""
+ device = mock_device(10, "Family Room")
+ location = mock_location("Home", False)
+
+ data = Mock()
+ data.get_reading.return_value = None
+
+ sensor = CanarySensor(data, SENSOR_TYPES[0], location, device)
+ sensor.update()
+
+ self.assertEqual(None, sensor.state)
def test_humidity_sensor(self):
"""Test humidity sensor."""
@@ -94,28 +89,79 @@ class TestCanarySensorSetup(unittest.TestCase):
location = mock_location("Home")
data = Mock()
- data.get_readings.return_value = [
- mock_reading(SensorType.HUMIDITY, 50.4567)]
+ data.get_reading.return_value = 50.4567
- sensor = CanarySensor(data, SensorType.HUMIDITY, location, device)
+ sensor = CanarySensor(data, SENSOR_TYPES[1], location, device)
sensor.update()
self.assertEqual("Home Family Room Humidity", sensor.name)
self.assertEqual("%", sensor.unit_of_measurement)
- self.assertEqual(50.5, sensor.state)
+ self.assertEqual(50.46, sensor.state)
- def test_air_quality_sensor(self):
+ def test_air_quality_sensor_with_very_abnormal_reading(self):
"""Test air quality sensor."""
device = mock_device(10, "Family Room")
location = mock_location("Home")
data = Mock()
- data.get_readings.return_value = [
- mock_reading(SensorType.AIR_QUALITY, 50.4567)]
+ data.get_reading.return_value = 0.4
- sensor = CanarySensor(data, SensorType.AIR_QUALITY, location, device)
+ sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
sensor.update()
self.assertEqual("Home Family Room Air Quality", sensor.name)
- self.assertEqual("", sensor.unit_of_measurement)
- self.assertEqual(50.5, sensor.state)
+ self.assertEqual(None, sensor.unit_of_measurement)
+ self.assertEqual(0.4, sensor.state)
+
+ air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
+ self.assertEqual(STATE_AIR_QUALITY_VERY_ABNORMAL, air_quality)
+
+ def test_air_quality_sensor_with_abnormal_reading(self):
+ """Test air quality sensor."""
+ device = mock_device(10, "Family Room")
+ location = mock_location("Home")
+
+ data = Mock()
+ data.get_reading.return_value = 0.59
+
+ sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
+ sensor.update()
+
+ self.assertEqual("Home Family Room Air Quality", sensor.name)
+ self.assertEqual(None, sensor.unit_of_measurement)
+ self.assertEqual(0.59, sensor.state)
+
+ air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
+ self.assertEqual(STATE_AIR_QUALITY_ABNORMAL, air_quality)
+
+ def test_air_quality_sensor_with_normal_reading(self):
+ """Test air quality sensor."""
+ device = mock_device(10, "Family Room")
+ location = mock_location("Home")
+
+ data = Mock()
+ data.get_reading.return_value = 1.0
+
+ sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
+ sensor.update()
+
+ self.assertEqual("Home Family Room Air Quality", sensor.name)
+ self.assertEqual(None, sensor.unit_of_measurement)
+ self.assertEqual(1.0, sensor.state)
+
+ air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY]
+ self.assertEqual(STATE_AIR_QUALITY_NORMAL, air_quality)
+
+ def test_air_quality_sensor_with_none_sensor_value(self):
+ """Test air quality sensor."""
+ device = mock_device(10, "Family Room")
+ location = mock_location("Home")
+
+ data = Mock()
+ data.get_reading.return_value = None
+
+ sensor = CanarySensor(data, SENSOR_TYPES[2], location, device)
+ sensor.update()
+
+ self.assertEqual(None, sensor.state)
+ self.assertEqual(None, sensor.device_state_attributes)
diff --git a/tests/components/sensor/test_dsmr.py b/tests/components/sensor/test_dsmr.py
index 86e637ab1ae..e5fca461a23 100644
--- a/tests/components/sensor/test_dsmr.py
+++ b/tests/components/sensor/test_dsmr.py
@@ -110,7 +110,7 @@ def test_derivative():
yield from entity.async_update()
assert entity.state == STATE_UNKNOWN, \
- 'state after first update shoudl still be unknown'
+ 'state after first update should still be unknown'
entity.telegram = {
'1.0.0': MBusObject([
diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py
index 00e8f2ba525..aa048f7a62e 100644
--- a/tests/components/sensor/test_file.py
+++ b/tests/components/sensor/test_file.py
@@ -9,7 +9,7 @@ from mock_open import MockOpen
from homeassistant.setup import setup_component
from homeassistant.const import STATE_UNKNOWN
-from tests.common import get_test_home_assistant
+from tests.common import get_test_home_assistant, mock_registry
class TestFileSensor(unittest.TestCase):
@@ -18,6 +18,7 @@ class TestFileSensor(unittest.TestCase):
def setup_method(self, method):
"""Set up things to be run when tests are started."""
self.hass = get_test_home_assistant()
+ mock_registry(self.hass)
def teardown_method(self, method):
"""Stop everything that was started."""
diff --git a/tests/components/sensor/test_hddtemp.py b/tests/components/sensor/test_hddtemp.py
index 3be35f3281c..1b65af7fd7e 100644
--- a/tests/components/sensor/test_hddtemp.py
+++ b/tests/components/sensor/test_hddtemp.py
@@ -1,216 +1,216 @@
-"""The tests for the hddtemp platform."""
-import socket
-
-import unittest
-from unittest.mock import patch
-
-from homeassistant.setup import setup_component
-
-from tests.common import get_test_home_assistant
-
-VALID_CONFIG_MINIMAL = {
- 'sensor': {
- 'platform': 'hddtemp',
- }
-}
-
-VALID_CONFIG_NAME = {
- 'sensor': {
- 'platform': 'hddtemp',
- 'name': 'FooBar',
- }
-}
-
-VALID_CONFIG_ONE_DISK = {
- 'sensor': {
- 'platform': 'hddtemp',
- 'disks': [
- '/dev/sdd1',
- ],
- }
-}
-
-VALID_CONFIG_WRONG_DISK = {
- 'sensor': {
- 'platform': 'hddtemp',
- 'disks': [
- '/dev/sdx1',
- ],
- }
-}
-
-VALID_CONFIG_MULTIPLE_DISKS = {
- 'sensor': {
- 'platform': 'hddtemp',
- 'host': 'foobar.local',
- 'disks': [
- '/dev/sda1',
- '/dev/sdb1',
- '/dev/sdc1',
- ],
- }
-}
-
-VALID_CONFIG_HOST = {
- 'sensor': {
- 'platform': 'hddtemp',
- 'host': 'alice.local',
- }
-}
-
-VALID_CONFIG_HOST_UNREACHABLE = {
- 'sensor': {
- 'platform': 'hddtemp',
- 'host': 'bob.local',
- }
-}
-
-
-class TelnetMock():
- """Mock class for the telnetlib.Telnet object."""
-
- def __init__(self, host, port, timeout=0):
- """Initialize Telnet object."""
- self.host = host
- self.port = port
- self.timeout = timeout
- self.sample_data = bytes('|/dev/sda1|WDC WD30EZRX-12DC0B0|29|C|' +
- '|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|' +
- '|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|' +
- '|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|',
- 'ascii')
-
- def read_all(self):
- """Return sample values."""
- if self.host == 'alice.local':
- raise ConnectionRefusedError
- elif self.host == 'bob.local':
- raise socket.gaierror
- else:
- return self.sample_data
- return None
-
-
-class TestHDDTempSensor(unittest.TestCase):
- """Test the hddtemp sensor."""
-
- def setUp(self):
- """Set up things to run when tests begin."""
- self.hass = get_test_home_assistant()
- self.config = VALID_CONFIG_ONE_DISK
- self.reference = {'/dev/sda1': {'device': '/dev/sda1',
- 'temperature': '29',
- 'unit_of_measurement': '°C',
- 'model': 'WDC WD30EZRX-12DC0B0', },
- '/dev/sdb1': {'device': '/dev/sdb1',
- 'temperature': '32',
- 'unit_of_measurement': '°C',
- 'model': 'WDC WD15EADS-11P7B2', },
- '/dev/sdc1': {'device': '/dev/sdc1',
- 'temperature': '29',
- 'unit_of_measurement': '°C',
- 'model': 'WDC WD20EARX-22MMMB0', },
- '/dev/sdd1': {'device': '/dev/sdd1',
- 'temperature': '32',
- 'unit_of_measurement': '°C',
- 'model': 'WDC WD15EARS-00Z5B1', }, }
-
- def tearDown(self):
- """Stop everything that was started."""
- self.hass.stop()
-
- @patch('telnetlib.Telnet', new=TelnetMock)
- def test_hddtemp_min_config(self):
- """Test minimal hddtemp configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL)
-
- entity = self.hass.states.all()[0].entity_id
- state = self.hass.states.get(entity)
-
- reference = self.reference[state.attributes.get('device')]
-
- self.assertEqual(state.state, reference['temperature'])
- self.assertEqual(state.attributes.get('device'), reference['device'])
- self.assertEqual(state.attributes.get('model'), reference['model'])
- self.assertEqual(state.attributes.get('unit_of_measurement'),
- reference['unit_of_measurement'])
- self.assertEqual(state.attributes.get('friendly_name'),
- 'HD Temperature ' + reference['device'])
-
- @patch('telnetlib.Telnet', new=TelnetMock)
- def test_hddtemp_rename_config(self):
- """Test hddtemp configuration with different name."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME)
-
- entity = self.hass.states.all()[0].entity_id
- state = self.hass.states.get(entity)
-
- reference = self.reference[state.attributes.get('device')]
-
- self.assertEqual(state.attributes.get('friendly_name'),
- 'FooBar ' + reference['device'])
-
- @patch('telnetlib.Telnet', new=TelnetMock)
- def test_hddtemp_one_disk(self):
- """Test hddtemp one disk configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_ONE_DISK)
-
- state = self.hass.states.get('sensor.hd_temperature_devsdd1')
-
- reference = self.reference[state.attributes.get('device')]
-
- self.assertEqual(state.state, reference['temperature'])
- self.assertEqual(state.attributes.get('device'), reference['device'])
- self.assertEqual(state.attributes.get('model'), reference['model'])
- self.assertEqual(state.attributes.get('unit_of_measurement'),
- reference['unit_of_measurement'])
- self.assertEqual(state.attributes.get('friendly_name'),
- 'HD Temperature ' + reference['device'])
-
- @patch('telnetlib.Telnet', new=TelnetMock)
- def test_hddtemp_wrong_disk(self):
- """Test hddtemp wrong disk configuration."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_WRONG_DISK)
-
- self.assertEqual(len(self.hass.states.all()), 1)
- state = self.hass.states.get('sensor.hd_temperature_devsdx1')
- self.assertEqual(state.attributes.get('friendly_name'),
- 'HD Temperature ' + '/dev/sdx1')
-
- @patch('telnetlib.Telnet', new=TelnetMock)
- def test_hddtemp_multiple_disks(self):
- """Test hddtemp multiple disk configuration."""
- assert setup_component(self.hass,
- 'sensor', VALID_CONFIG_MULTIPLE_DISKS)
-
- for sensor in ['sensor.hd_temperature_devsda1',
- 'sensor.hd_temperature_devsdb1',
- 'sensor.hd_temperature_devsdc1']:
-
- state = self.hass.states.get(sensor)
-
- reference = self.reference[state.attributes.get('device')]
-
- self.assertEqual(state.state,
- reference['temperature'])
- self.assertEqual(state.attributes.get('device'),
- reference['device'])
- self.assertEqual(state.attributes.get('model'),
- reference['model'])
- self.assertEqual(state.attributes.get('unit_of_measurement'),
- reference['unit_of_measurement'])
- self.assertEqual(state.attributes.get('friendly_name'),
- 'HD Temperature ' + reference['device'])
-
- @patch('telnetlib.Telnet', new=TelnetMock)
- def test_hddtemp_host_refused(self):
- """Test hddtemp if host unreachable."""
- assert setup_component(self.hass, 'sensor', VALID_CONFIG_HOST)
- self.assertEqual(len(self.hass.states.all()), 0)
-
- @patch('telnetlib.Telnet', new=TelnetMock)
- def test_hddtemp_host_unreachable(self):
- """Test hddtemp if host unreachable."""
- assert setup_component(self.hass, 'sensor',
- VALID_CONFIG_HOST_UNREACHABLE)
- self.assertEqual(len(self.hass.states.all()), 0)
+"""The tests for the hddtemp platform."""
+import socket
+
+import unittest
+from unittest.mock import patch
+
+from homeassistant.setup import setup_component
+
+from tests.common import get_test_home_assistant
+
+VALID_CONFIG_MINIMAL = {
+ 'sensor': {
+ 'platform': 'hddtemp',
+ }
+}
+
+VALID_CONFIG_NAME = {
+ 'sensor': {
+ 'platform': 'hddtemp',
+ 'name': 'FooBar',
+ }
+}
+
+VALID_CONFIG_ONE_DISK = {
+ 'sensor': {
+ 'platform': 'hddtemp',
+ 'disks': [
+ '/dev/sdd1',
+ ],
+ }
+}
+
+VALID_CONFIG_WRONG_DISK = {
+ 'sensor': {
+ 'platform': 'hddtemp',
+ 'disks': [
+ '/dev/sdx1',
+ ],
+ }
+}
+
+VALID_CONFIG_MULTIPLE_DISKS = {
+ 'sensor': {
+ 'platform': 'hddtemp',
+ 'host': 'foobar.local',
+ 'disks': [
+ '/dev/sda1',
+ '/dev/sdb1',
+ '/dev/sdc1',
+ ],
+ }
+}
+
+VALID_CONFIG_HOST = {
+ 'sensor': {
+ 'platform': 'hddtemp',
+ 'host': 'alice.local',
+ }
+}
+
+VALID_CONFIG_HOST_UNREACHABLE = {
+ 'sensor': {
+ 'platform': 'hddtemp',
+ 'host': 'bob.local',
+ }
+}
+
+
+class TelnetMock():
+ """Mock class for the telnetlib.Telnet object."""
+
+ def __init__(self, host, port, timeout=0):
+ """Initialize Telnet object."""
+ self.host = host
+ self.port = port
+ self.timeout = timeout
+ self.sample_data = bytes('|/dev/sda1|WDC WD30EZRX-12DC0B0|29|C|' +
+ '|/dev/sdb1|WDC WD15EADS-11P7B2|32|C|' +
+ '|/dev/sdc1|WDC WD20EARX-22MMMB0|29|C|' +
+ '|/dev/sdd1|WDC WD15EARS-00Z5B1|89|F|',
+ 'ascii')
+
+ def read_all(self):
+ """Return sample values."""
+ if self.host == 'alice.local':
+ raise ConnectionRefusedError
+ elif self.host == 'bob.local':
+ raise socket.gaierror
+ else:
+ return self.sample_data
+ return None
+
+
+class TestHDDTempSensor(unittest.TestCase):
+ """Test the hddtemp sensor."""
+
+ def setUp(self):
+ """Set up things to run when tests begin."""
+ self.hass = get_test_home_assistant()
+ self.config = VALID_CONFIG_ONE_DISK
+ self.reference = {'/dev/sda1': {'device': '/dev/sda1',
+ 'temperature': '29',
+ 'unit_of_measurement': '°C',
+ 'model': 'WDC WD30EZRX-12DC0B0', },
+ '/dev/sdb1': {'device': '/dev/sdb1',
+ 'temperature': '32',
+ 'unit_of_measurement': '°C',
+ 'model': 'WDC WD15EADS-11P7B2', },
+ '/dev/sdc1': {'device': '/dev/sdc1',
+ 'temperature': '29',
+ 'unit_of_measurement': '°C',
+ 'model': 'WDC WD20EARX-22MMMB0', },
+ '/dev/sdd1': {'device': '/dev/sdd1',
+ 'temperature': '32',
+ 'unit_of_measurement': '°C',
+ 'model': 'WDC WD15EARS-00Z5B1', }, }
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @patch('telnetlib.Telnet', new=TelnetMock)
+ def test_hddtemp_min_config(self):
+ """Test minimal hddtemp configuration."""
+ assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL)
+
+ entity = self.hass.states.all()[0].entity_id
+ state = self.hass.states.get(entity)
+
+ reference = self.reference[state.attributes.get('device')]
+
+ self.assertEqual(state.state, reference['temperature'])
+ self.assertEqual(state.attributes.get('device'), reference['device'])
+ self.assertEqual(state.attributes.get('model'), reference['model'])
+ self.assertEqual(state.attributes.get('unit_of_measurement'),
+ reference['unit_of_measurement'])
+ self.assertEqual(state.attributes.get('friendly_name'),
+ 'HD Temperature ' + reference['device'])
+
+ @patch('telnetlib.Telnet', new=TelnetMock)
+ def test_hddtemp_rename_config(self):
+ """Test hddtemp configuration with different name."""
+ assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME)
+
+ entity = self.hass.states.all()[0].entity_id
+ state = self.hass.states.get(entity)
+
+ reference = self.reference[state.attributes.get('device')]
+
+ self.assertEqual(state.attributes.get('friendly_name'),
+ 'FooBar ' + reference['device'])
+
+ @patch('telnetlib.Telnet', new=TelnetMock)
+ def test_hddtemp_one_disk(self):
+ """Test hddtemp one disk configuration."""
+ assert setup_component(self.hass, 'sensor', VALID_CONFIG_ONE_DISK)
+
+ state = self.hass.states.get('sensor.hd_temperature_devsdd1')
+
+ reference = self.reference[state.attributes.get('device')]
+
+ self.assertEqual(state.state, reference['temperature'])
+ self.assertEqual(state.attributes.get('device'), reference['device'])
+ self.assertEqual(state.attributes.get('model'), reference['model'])
+ self.assertEqual(state.attributes.get('unit_of_measurement'),
+ reference['unit_of_measurement'])
+ self.assertEqual(state.attributes.get('friendly_name'),
+ 'HD Temperature ' + reference['device'])
+
+ @patch('telnetlib.Telnet', new=TelnetMock)
+ def test_hddtemp_wrong_disk(self):
+ """Test hddtemp wrong disk configuration."""
+ assert setup_component(self.hass, 'sensor', VALID_CONFIG_WRONG_DISK)
+
+ self.assertEqual(len(self.hass.states.all()), 1)
+ state = self.hass.states.get('sensor.hd_temperature_devsdx1')
+ self.assertEqual(state.attributes.get('friendly_name'),
+ 'HD Temperature ' + '/dev/sdx1')
+
+ @patch('telnetlib.Telnet', new=TelnetMock)
+ def test_hddtemp_multiple_disks(self):
+ """Test hddtemp multiple disk configuration."""
+ assert setup_component(self.hass,
+ 'sensor', VALID_CONFIG_MULTIPLE_DISKS)
+
+ for sensor in ['sensor.hd_temperature_devsda1',
+ 'sensor.hd_temperature_devsdb1',
+ 'sensor.hd_temperature_devsdc1']:
+
+ state = self.hass.states.get(sensor)
+
+ reference = self.reference[state.attributes.get('device')]
+
+ self.assertEqual(state.state,
+ reference['temperature'])
+ self.assertEqual(state.attributes.get('device'),
+ reference['device'])
+ self.assertEqual(state.attributes.get('model'),
+ reference['model'])
+ self.assertEqual(state.attributes.get('unit_of_measurement'),
+ reference['unit_of_measurement'])
+ self.assertEqual(state.attributes.get('friendly_name'),
+ 'HD Temperature ' + reference['device'])
+
+ @patch('telnetlib.Telnet', new=TelnetMock)
+ def test_hddtemp_host_refused(self):
+ """Test hddtemp if host unreachable."""
+ assert setup_component(self.hass, 'sensor', VALID_CONFIG_HOST)
+ self.assertEqual(len(self.hass.states.all()), 0)
+
+ @patch('telnetlib.Telnet', new=TelnetMock)
+ def test_hddtemp_host_unreachable(self):
+ """Test hddtemp if host unreachable."""
+ assert setup_component(self.hass, 'sensor',
+ VALID_CONFIG_HOST_UNREACHABLE)
+ self.assertEqual(len(self.hass.states.all()), 0)
diff --git a/tests/components/sensor/test_melissa.py b/tests/components/sensor/test_melissa.py
new file mode 100644
index 00000000000..55b3e7f70f4
--- /dev/null
+++ b/tests/components/sensor/test_melissa.py
@@ -0,0 +1,89 @@
+"""Test for Melissa climate component."""
+import unittest
+import json
+from unittest.mock import Mock
+
+from homeassistant.components.melissa import DATA_MELISSA
+from homeassistant.components.sensor import melissa
+from homeassistant.components.sensor.melissa import MelissaTemperatureSensor, \
+ MelissaHumiditySensor
+from homeassistant.const import TEMP_CELSIUS
+from tests.common import get_test_home_assistant, load_fixture
+
+
+class TestMelissa(unittest.TestCase):
+ """Tests for Melissa climate."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Set up test variables."""
+ self.hass = get_test_home_assistant()
+ self._serial = '12345678'
+
+ self.api = Mock()
+ self.api.fetch_devices.return_value = json.loads(load_fixture(
+ 'melissa_fetch_devices.json'
+ ))
+ self.api.status.return_value = json.loads(load_fixture(
+ 'melissa_status.json'
+ ))
+
+ self.api.TEMP = 'temp'
+ self.api.HUMIDITY = 'humidity'
+ device = self.api.fetch_devices()[self._serial]
+ self.temp = MelissaTemperatureSensor(device, self.api)
+ self.hum = MelissaHumiditySensor(device, self.api)
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Teardown this test class. Stop hass."""
+ self.hass.stop()
+
+ def test_setup_platform(self):
+ """Test setup_platform."""
+ self.hass.data[DATA_MELISSA] = self.api
+
+ config = {}
+ add_devices = Mock()
+ discovery_info = {}
+
+ melissa.setup_platform(self.hass, config, add_devices, discovery_info)
+
+ def test_name(self):
+ """Test name property."""
+ device = self.api.fetch_devices()[self._serial]
+ self.assertEqual(self.temp.name, '{0} {1}'.format(
+ device['name'],
+ self.temp._type
+ ))
+ self.assertEqual(self.hum.name, '{0} {1}'.format(
+ device['name'],
+ self.hum._type
+ ))
+
+ def test_state(self):
+ """Test state property."""
+ device = self.api.status()[self._serial]
+ self.temp.update()
+ self.assertEqual(self.temp.state, device[self.api.TEMP])
+ self.hum.update()
+ self.assertEqual(self.hum.state, device[self.api.HUMIDITY])
+
+ def test_unit_of_measurement(self):
+ """Test unit of measurement property."""
+ self.assertEqual(self.temp.unit_of_measurement, TEMP_CELSIUS)
+ self.assertEqual(self.hum.unit_of_measurement, '%')
+
+ def test_update(self):
+ """Test for update."""
+ self.temp.update()
+ self.assertEqual(self.temp.state, 27.4)
+ self.hum.update()
+ self.assertEqual(self.hum.state, 18.7)
+
+ def test_update_keyerror(self):
+ """Test for faulty update."""
+ self.temp._api.status.return_value = {}
+ self.temp.update()
+ self.assertEqual(None, self.temp.state)
+ self.hum._api.status.return_value = {}
+ self.hum.update()
+ self.assertEqual(None, self.hum.state)
diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py
index efcd44658c3..b23d89e3057 100644
--- a/tests/components/sensor/test_mqtt.py
+++ b/tests/components/sensor/test_mqtt.py
@@ -110,7 +110,7 @@ class TestSensorMQTT(unittest.TestCase):
self.assertEqual('unknown', state.state)
def test_setting_sensor_value_via_mqtt_json_message(self):
- """Test the setting of the value via MQTT with JSON playload."""
+ """Test the setting of the value via MQTT with JSON payload."""
mock_component(self.hass, 'mqtt')
assert setup_component(self.hass, sensor.DOMAIN, {
sensor.DOMAIN: {
@@ -244,7 +244,7 @@ class TestSensorMQTT(unittest.TestCase):
self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now})
def test_setting_sensor_attribute_via_mqtt_json_message(self):
- """Test the setting of attribute via MQTT with JSON playload."""
+ """Test the setting of attribute via MQTT with JSON payload."""
mock_component(self.hass, 'mqtt')
assert setup_component(self.hass, sensor.DOMAIN, {
sensor.DOMAIN: {
diff --git a/tests/components/sensor/test_random.py b/tests/components/sensor/test_random.py
index eeefef74c02..e04fc31af84 100644
--- a/tests/components/sensor/test_random.py
+++ b/tests/components/sensor/test_random.py
@@ -18,7 +18,7 @@ class TestRandomSensor(unittest.TestCase):
self.hass.stop()
def test_random_sensor(self):
- """Test the Randowm number sensor."""
+ """Test the Random number sensor."""
config = {
'sensor': {
'platform': 'random',
diff --git a/tests/components/sensor/test_ring.py b/tests/components/sensor/test_ring.py
index fb31dc7c53c..0cce0ea681d 100644
--- a/tests/components/sensor/test_ring.py
+++ b/tests/components/sensor/test_ring.py
@@ -50,7 +50,7 @@ class TestRingSensorSetup(unittest.TestCase):
@requests_mock.Mocker()
def test_sensor(self, mock):
- """Test the Ring senskor class and methods."""
+ """Test the Ring sensor class and methods."""
mock.post('https://api.ring.com/clients_api/session',
text=load_fixture('ring_session.json'))
mock.get('https://api.ring.com/clients_api/ring_devices',
diff --git a/tests/components/sensor/test_season.py b/tests/components/sensor/test_season.py
index 9dda0d2f2cb..5c071982f7f 100644
--- a/tests/components/sensor/test_season.py
+++ b/tests/components/sensor/test_season.py
@@ -73,7 +73,7 @@ class TestSeason(unittest.TestCase):
"""Stop everything that was started."""
self.hass.stop()
- def test_season_should_be_summer_northern_astonomical(self):
+ def test_season_should_be_summer_northern_astronomical(self):
"""Test that season should be summer."""
# A known day in summer
summer_day = datetime(2017, 9, 3, 0, 0)
@@ -91,7 +91,7 @@ class TestSeason(unittest.TestCase):
self.assertEqual(season.STATE_SUMMER,
current_season)
- def test_season_should_be_autumn_northern_astonomical(self):
+ def test_season_should_be_autumn_northern_astronomical(self):
"""Test that season should be autumn."""
# A known day in autumn
autumn_day = datetime(2017, 9, 23, 0, 0)
@@ -109,7 +109,7 @@ class TestSeason(unittest.TestCase):
self.assertEqual(season.STATE_AUTUMN,
current_season)
- def test_season_should_be_winter_northern_astonomical(self):
+ def test_season_should_be_winter_northern_astronomical(self):
"""Test that season should be winter."""
# A known day in winter
winter_day = datetime(2017, 12, 25, 0, 0)
@@ -127,7 +127,7 @@ class TestSeason(unittest.TestCase):
self.assertEqual(season.STATE_WINTER,
current_season)
- def test_season_should_be_spring_northern_astonomical(self):
+ def test_season_should_be_spring_northern_astronomical(self):
"""Test that season should be spring."""
# A known day in spring
spring_day = datetime(2017, 4, 1, 0, 0)
@@ -145,7 +145,7 @@ class TestSeason(unittest.TestCase):
self.assertEqual(season.STATE_SPRING,
current_season)
- def test_season_should_be_winter_southern_astonomical(self):
+ def test_season_should_be_winter_southern_astronomical(self):
"""Test that season should be winter."""
# A known day in winter
winter_day = datetime(2017, 9, 3, 0, 0)
@@ -163,7 +163,7 @@ class TestSeason(unittest.TestCase):
self.assertEqual(season.STATE_WINTER,
current_season)
- def test_season_should_be_spring_southern_astonomical(self):
+ def test_season_should_be_spring_southern_astronomical(self):
"""Test that season should be spring."""
# A known day in spring
spring_day = datetime(2017, 9, 23, 0, 0)
@@ -181,7 +181,7 @@ class TestSeason(unittest.TestCase):
self.assertEqual(season.STATE_SPRING,
current_season)
- def test_season_should_be_summer_southern_astonomical(self):
+ def test_season_should_be_summer_southern_astronomical(self):
"""Test that season should be summer."""
# A known day in summer
summer_day = datetime(2017, 12, 25, 0, 0)
@@ -199,7 +199,7 @@ class TestSeason(unittest.TestCase):
self.assertEqual(season.STATE_SUMMER,
current_season)
- def test_season_should_be_autumn_southern_astonomical(self):
+ def test_season_should_be_autumn_southern_astronomical(self):
"""Test that season should be spring."""
# A known day in spring
autumn_day = datetime(2017, 4, 1, 0, 0)
diff --git a/tests/components/sensor/test_sql.py b/tests/components/sensor/test_sql.py
new file mode 100644
index 00000000000..ebf2d749e67
--- /dev/null
+++ b/tests/components/sensor/test_sql.py
@@ -0,0 +1,37 @@
+"""The test for the sql sensor platform."""
+import unittest
+
+from homeassistant.setup import setup_component
+
+from tests.common import get_test_home_assistant
+
+
+class TestSQLSensor(unittest.TestCase):
+ """Test the SQL sensor."""
+
+ def setUp(self):
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ def teardown_method(self, method):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_query(self):
+ """Test the SQL sensor."""
+ config = {
+ 'sensor': {
+ 'platform': 'sql',
+ 'db_url': 'sqlite://',
+ 'queries': [{
+ 'name': 'count_tables',
+ 'query': 'SELECT count(*) value FROM sqlite_master;',
+ 'column': 'value',
+ }]
+ }
+ }
+
+ assert setup_component(self.hass, 'sensor', config)
+
+ state = self.hass.states.get('sensor.count_tables')
+ self.assertEqual(state.state, '0')
diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py
index 0d2a486cb4f..a1e600860f9 100644
--- a/tests/components/switch/test_flux.py
+++ b/tests/components/switch/test_flux.py
@@ -238,7 +238,8 @@ class TestSwitchFlux(unittest.TestCase):
switch.DOMAIN: {
'platform': 'flux',
'name': 'flux',
- 'lights': [dev1.entity_id]
+ 'lights': [dev1.entity_id],
+ 'stop_time': '22:00'
}
})
turn_on_calls = mock_service(
@@ -638,7 +639,8 @@ class TestSwitchFlux(unittest.TestCase):
'name': 'flux',
'lights': [dev1.entity_id],
'start_colortemp': '1000',
- 'stop_colortemp': '6000'
+ 'stop_colortemp': '6000',
+ 'stop_time': '22:00'
}
})
turn_on_calls = mock_service(
@@ -686,7 +688,8 @@ class TestSwitchFlux(unittest.TestCase):
'platform': 'flux',
'name': 'flux',
'lights': [dev1.entity_id],
- 'brightness': 255
+ 'brightness': 255,
+ 'stop_time': '22:00'
}
})
turn_on_calls = mock_service(
diff --git a/tests/components/switch/test_wake_on_lan.py b/tests/components/switch/test_wake_on_lan.py
index 2c4be648c8c..167c3bb35ac 100644
--- a/tests/components/switch/test_wake_on_lan.py
+++ b/tests/components/switch/test_wake_on_lan.py
@@ -1,192 +1,192 @@
-"""The tests for the wake on lan switch platform."""
-import unittest
-from unittest.mock import patch
-
-from homeassistant.setup import setup_component
-from homeassistant.const import STATE_ON, STATE_OFF
-import homeassistant.components.switch as switch
-
-from tests.common import get_test_home_assistant, mock_service
-
-
-TEST_STATE = None
-
-
-def send_magic_packet(*macs, **kwargs):
- """Fake call for sending magic packets."""
- return
-
-
-def call(cmd, stdout, stderr):
- """Return fake subprocess return codes."""
- if cmd[5] == 'validhostname' and TEST_STATE:
- return 0
- return 2
-
-
-def system():
- """Fake system call to test the windows platform."""
- return 'Windows'
-
-
-class TestWOLSwitch(unittest.TestCase):
- """Test the wol switch."""
-
- 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()
-
- @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet)
- @patch('subprocess.call', new=call)
- def test_valid_hostname(self):
- """Test with valid hostname."""
- global TEST_STATE
- TEST_STATE = False
- self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
- 'switch': {
- 'platform': 'wake_on_lan',
- 'mac_address': '00-01-02-03-04-05',
- 'host': 'validhostname',
- }
- }))
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_OFF, state.state)
-
- TEST_STATE = True
-
- switch.turn_on(self.hass, 'switch.wake_on_lan')
- self.hass.block_till_done()
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_ON, state.state)
-
- switch.turn_off(self.hass, 'switch.wake_on_lan')
- self.hass.block_till_done()
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_ON, state.state)
-
- @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet)
- @patch('subprocess.call', new=call)
- @patch('platform.system', new=system)
- def test_valid_hostname_windows(self):
- """Test with valid hostname on windows."""
- global TEST_STATE
- TEST_STATE = False
- self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
- 'switch': {
- 'platform': 'wake_on_lan',
- 'mac_address': '00-01-02-03-04-05',
- 'host': 'validhostname',
- }
- }))
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_OFF, state.state)
-
- TEST_STATE = True
-
- switch.turn_on(self.hass, 'switch.wake_on_lan')
- self.hass.block_till_done()
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_ON, state.state)
-
- @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet)
- @patch('subprocess.call', new=call)
- def test_minimal_config(self):
- """Test with minimal config."""
- self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
- 'switch': {
- 'platform': 'wake_on_lan',
- 'mac_address': '00-01-02-03-04-05',
- }
- }))
-
- @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet)
- @patch('subprocess.call', new=call)
- def test_broadcast_config(self):
- """Test with broadcast address config."""
- self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
- 'switch': {
- 'platform': 'wake_on_lan',
- 'mac_address': '00-01-02-03-04-05',
- 'broadcast_address': '255.255.255.255',
- }
- }))
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_OFF, state.state)
-
- switch.turn_on(self.hass, 'switch.wake_on_lan')
- self.hass.block_till_done()
-
- @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet)
- @patch('subprocess.call', new=call)
- def test_off_script(self):
- """Test with turn off script."""
- global TEST_STATE
- TEST_STATE = False
- self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
- 'switch': {
- 'platform': 'wake_on_lan',
- 'mac_address': '00-01-02-03-04-05',
- 'host': 'validhostname',
- 'turn_off': {
- 'service': 'shell_command.turn_off_TARGET',
- },
- }
- }))
- calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET')
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_OFF, state.state)
-
- TEST_STATE = True
-
- switch.turn_on(self.hass, 'switch.wake_on_lan')
- self.hass.block_till_done()
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_ON, state.state)
- assert len(calls) == 0
-
- TEST_STATE = False
-
- switch.turn_off(self.hass, 'switch.wake_on_lan')
- self.hass.block_till_done()
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_OFF, state.state)
- assert len(calls) == 1
-
- @patch('wakeonlan.wol.send_magic_packet', new=send_magic_packet)
- @patch('subprocess.call', new=call)
- @patch('platform.system', new=system)
- def test_invalid_hostname_windows(self):
- """Test with invalid hostname on windows."""
- global TEST_STATE
- TEST_STATE = False
- self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
- 'switch': {
- 'platform': 'wake_on_lan',
- 'mac_address': '00-01-02-03-04-05',
- 'host': 'invalidhostname',
- }
- }))
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_OFF, state.state)
-
- TEST_STATE = True
-
- switch.turn_on(self.hass, 'switch.wake_on_lan')
- self.hass.block_till_done()
-
- state = self.hass.states.get('switch.wake_on_lan')
- self.assertEqual(STATE_OFF, state.state)
+"""The tests for the wake on lan switch platform."""
+import unittest
+from unittest.mock import patch
+
+from homeassistant.setup import setup_component
+from homeassistant.const import STATE_ON, STATE_OFF
+import homeassistant.components.switch as switch
+
+from tests.common import get_test_home_assistant, mock_service
+
+
+TEST_STATE = None
+
+
+def send_magic_packet(*macs, **kwargs):
+ """Fake call for sending magic packets."""
+ return
+
+
+def call(cmd, stdout, stderr):
+ """Return fake subprocess return codes."""
+ if cmd[5] == 'validhostname' and TEST_STATE:
+ return 0
+ return 2
+
+
+def system():
+ """Fake system call to test the windows platform."""
+ return 'Windows'
+
+
+class TestWOLSwitch(unittest.TestCase):
+ """Test the wol switch."""
+
+ 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()
+
+ @patch('wakeonlan.send_magic_packet', new=send_magic_packet)
+ @patch('subprocess.call', new=call)
+ def test_valid_hostname(self):
+ """Test with valid hostname."""
+ global TEST_STATE
+ TEST_STATE = False
+ self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
+ 'switch': {
+ 'platform': 'wake_on_lan',
+ 'mac_address': '00-01-02-03-04-05',
+ 'host': 'validhostname',
+ }
+ }))
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_OFF, state.state)
+
+ TEST_STATE = True
+
+ switch.turn_on(self.hass, 'switch.wake_on_lan')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_ON, state.state)
+
+ switch.turn_off(self.hass, 'switch.wake_on_lan')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_ON, state.state)
+
+ @patch('wakeonlan.send_magic_packet', new=send_magic_packet)
+ @patch('subprocess.call', new=call)
+ @patch('platform.system', new=system)
+ def test_valid_hostname_windows(self):
+ """Test with valid hostname on windows."""
+ global TEST_STATE
+ TEST_STATE = False
+ self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
+ 'switch': {
+ 'platform': 'wake_on_lan',
+ 'mac_address': '00-01-02-03-04-05',
+ 'host': 'validhostname',
+ }
+ }))
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_OFF, state.state)
+
+ TEST_STATE = True
+
+ switch.turn_on(self.hass, 'switch.wake_on_lan')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_ON, state.state)
+
+ @patch('wakeonlan.send_magic_packet', new=send_magic_packet)
+ @patch('subprocess.call', new=call)
+ def test_minimal_config(self):
+ """Test with minimal config."""
+ self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
+ 'switch': {
+ 'platform': 'wake_on_lan',
+ 'mac_address': '00-01-02-03-04-05',
+ }
+ }))
+
+ @patch('wakeonlan.send_magic_packet', new=send_magic_packet)
+ @patch('subprocess.call', new=call)
+ def test_broadcast_config(self):
+ """Test with broadcast address config."""
+ self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
+ 'switch': {
+ 'platform': 'wake_on_lan',
+ 'mac_address': '00-01-02-03-04-05',
+ 'broadcast_address': '255.255.255.255',
+ }
+ }))
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_OFF, state.state)
+
+ switch.turn_on(self.hass, 'switch.wake_on_lan')
+ self.hass.block_till_done()
+
+ @patch('wakeonlan.send_magic_packet', new=send_magic_packet)
+ @patch('subprocess.call', new=call)
+ def test_off_script(self):
+ """Test with turn off script."""
+ global TEST_STATE
+ TEST_STATE = False
+ self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
+ 'switch': {
+ 'platform': 'wake_on_lan',
+ 'mac_address': '00-01-02-03-04-05',
+ 'host': 'validhostname',
+ 'turn_off': {
+ 'service': 'shell_command.turn_off_TARGET',
+ },
+ }
+ }))
+ calls = mock_service(self.hass, 'shell_command', 'turn_off_TARGET')
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_OFF, state.state)
+
+ TEST_STATE = True
+
+ switch.turn_on(self.hass, 'switch.wake_on_lan')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_ON, state.state)
+ assert len(calls) == 0
+
+ TEST_STATE = False
+
+ switch.turn_off(self.hass, 'switch.wake_on_lan')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_OFF, state.state)
+ assert len(calls) == 1
+
+ @patch('wakeonlan.send_magic_packet', new=send_magic_packet)
+ @patch('subprocess.call', new=call)
+ @patch('platform.system', new=system)
+ def test_invalid_hostname_windows(self):
+ """Test with invalid hostname on windows."""
+ global TEST_STATE
+ TEST_STATE = False
+ self.assertTrue(setup_component(self.hass, switch.DOMAIN, {
+ 'switch': {
+ 'platform': 'wake_on_lan',
+ 'mac_address': '00-01-02-03-04-05',
+ 'host': 'invalidhostname',
+ }
+ }))
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_OFF, state.state)
+
+ TEST_STATE = True
+
+ switch.turn_on(self.hass, 'switch.wake_on_lan')
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('switch.wake_on_lan')
+ self.assertEqual(STATE_OFF, state.state)
diff --git a/tests/components/test_alert.py b/tests/components/test_alert.py
index a94e5747483..d9eb33be37d 100644
--- a/tests/components/test_alert.py
+++ b/tests/components/test_alert.py
@@ -107,7 +107,7 @@ class TestAlert(unittest.TestCase):
self.assertEqual(STATE_ON, self.hass.states.get(ENTITY_ID).state)
def test_hidden(self):
- """Test entity hidding."""
+ """Test entity hiding."""
assert setup_component(self.hass, alert.DOMAIN, TEST_CONFIG)
hidden = self.hass.states.get(ENTITY_ID).attributes.get('hidden')
self.assertTrue(hidden)
diff --git a/tests/components/test_canary.py b/tests/components/test_canary.py
index 67122813fb7..2c496c26e11 100644
--- a/tests/components/test_canary.py
+++ b/tests/components/test_canary.py
@@ -17,12 +17,12 @@ def mock_device(device_id, name, is_online=True):
return device
-def mock_location(name, is_celsius=True, devices=[]):
+def mock_location(name, is_celsius=True, devices=None):
"""Mock Canary Location class."""
location = MagicMock()
type(location).name = PropertyMock(return_value=name)
type(location).is_celsius = PropertyMock(return_value=is_celsius)
- type(location).devices = PropertyMock(return_value=devices)
+ type(location).devices = PropertyMock(return_value=devices or [])
return location
diff --git a/tests/components/test_dialogflow.py b/tests/components/test_dialogflow.py
index a52c841e0cc..0acf0833543 100644
--- a/tests/components/test_dialogflow.py
+++ b/tests/components/test_dialogflow.py
@@ -435,7 +435,7 @@ class TestDialogflow(unittest.TestCase):
self.assertEqual("virgo", call.data.get("hello"))
def test_intent_with_no_action(self):
- """Test a intent with no defined action."""
+ """Test an intent with no defined action."""
data = {
"id": REQUEST_ID,
"timestamp": REQUEST_TIMESTAMP,
@@ -480,7 +480,7 @@ class TestDialogflow(unittest.TestCase):
"You have not defined an action in your Dialogflow intent.", text)
def test_intent_with_unknown_action(self):
- """Test a intent with an action not defined in the conf."""
+ """Test an intent with an action not defined in the conf."""
data = {
"id": REQUEST_ID,
"timestamp": REQUEST_TIMESTAMP,
diff --git a/tests/components/test_ffmpeg.py b/tests/components/test_ffmpeg.py
index 9cc706b5690..5a5fdffd5a3 100644
--- a/tests/components/test_ffmpeg.py
+++ b/tests/components/test_ffmpeg.py
@@ -97,7 +97,7 @@ def test_setup_component_test_register_no_startup(hass):
@asyncio.coroutine
-def test_setup_component_test_servcie_start(hass):
+def test_setup_component_test_service_start(hass):
"""Setup ffmpeg component test service start."""
with assert_setup_component(2):
yield from async_setup_component(
@@ -113,7 +113,7 @@ def test_setup_component_test_servcie_start(hass):
@asyncio.coroutine
-def test_setup_component_test_servcie_stop(hass):
+def test_setup_component_test_service_stop(hass):
"""Setup ffmpeg component test service stop."""
with assert_setup_component(2):
yield from async_setup_component(
@@ -129,7 +129,7 @@ def test_setup_component_test_servcie_stop(hass):
@asyncio.coroutine
-def test_setup_component_test_servcie_restart(hass):
+def test_setup_component_test_service_restart(hass):
"""Setup ffmpeg component test service restart."""
with assert_setup_component(2):
yield from async_setup_component(
@@ -146,7 +146,7 @@ def test_setup_component_test_servcie_restart(hass):
@asyncio.coroutine
-def test_setup_component_test_servcie_start_with_entity(hass):
+def test_setup_component_test_service_start_with_entity(hass):
"""Setup ffmpeg component test service start."""
with assert_setup_component(2):
yield from async_setup_component(
diff --git a/tests/components/test_hassio.py b/tests/components/test_hassio.py
index fb869569670..8fb017309de 100644
--- a/tests/components/test_hassio.py
+++ b/tests/components/test_hassio.py
@@ -211,7 +211,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock):
@asyncio.coroutine
def test_service_register(hassio_env, hass):
- """Check if service will be settup."""
+ """Check if service will be setup."""
assert (yield from async_setup_component(hass, 'hassio', {}))
assert hass.services.has_service('hassio', 'addon_start')
assert hass.services.has_service('hassio', 'addon_stop')
diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py
index b8568f3aaba..30129ec7998 100644
--- a/tests/components/test_hue.py
+++ b/tests/components/test_hue.py
@@ -36,7 +36,7 @@ class TestSetup(unittest.TestCase):
self.assertTrue(setup_component(
self.hass, hue.DOMAIN, {}))
mock_phue.Bridge.assert_not_called()
- self.assertEquals({}, self.hass.data[hue.DOMAIN])
+ self.assertEqual({}, self.hass.data[hue.DOMAIN])
@MockDependency('phue')
def test_setup_with_host(self, mock_phue):
@@ -59,7 +59,7 @@ class TestSetup(unittest.TestCase):
{'bridge_id': '127.0.0.1'})
self.assertTrue(hue.DOMAIN in self.hass.data)
- self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+ self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_setup_with_phue_conf(self, mock_phue):
@@ -86,7 +86,7 @@ class TestSetup(unittest.TestCase):
{'bridge_id': '127.0.0.1'})
self.assertTrue(hue.DOMAIN in self.hass.data)
- self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+ self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_setup_with_multiple_hosts(self, mock_phue):
@@ -122,7 +122,7 @@ class TestSetup(unittest.TestCase):
], any_order=True)
self.assertTrue(hue.DOMAIN in self.hass.data)
- self.assertEquals(2, len(self.hass.data[hue.DOMAIN]))
+ self.assertEqual(2, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_bridge_discovered(self, mock_phue):
@@ -145,7 +145,7 @@ class TestSetup(unittest.TestCase):
{'bridge_id': '192.168.0.10'})
self.assertTrue(hue.DOMAIN in self.hass.data)
- self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+ self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
@MockDependency('phue')
def test_bridge_configure_and_discovered(self, mock_phue):
@@ -175,7 +175,7 @@ class TestSetup(unittest.TestCase):
mock_load.assert_has_calls(calls_to_mock_load)
self.assertTrue(hue.DOMAIN in self.hass.data)
- self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+ self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
# Then we discover the same bridge
hue.bridge_discovered(self.hass, mock_service, discovery_info)
@@ -189,7 +189,7 @@ class TestSetup(unittest.TestCase):
# Still only one
self.assertTrue(hue.DOMAIN in self.hass.data)
- self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+ self.assertEqual(1, len(self.hass.data[hue.DOMAIN]))
class TestHueBridge(unittest.TestCase):
diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py
index d768136592e..4d12e436c02 100644
--- a/tests/components/test_influxdb.py
+++ b/tests/components/test_influxdb.py
@@ -1,15 +1,10 @@
"""The tests for the InfluxDB component."""
-import unittest
import datetime
+import unittest
from unittest import mock
-from datetime import timedelta
-from unittest.mock import MagicMock
-
import influxdb as influx_client
-from homeassistant.util import dt as dt_util
-from homeassistant import core as ha
from homeassistant.setup import setup_component
import homeassistant.components.influxdb as influxdb
from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, \
@@ -169,6 +164,8 @@ class TestInfluxDB(unittest.TestCase):
body[0]['fields']['value'] = out[1]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
+
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
)
@@ -203,6 +200,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
)
@@ -212,18 +210,6 @@ class TestInfluxDB(unittest.TestCase):
)
mock_client.return_value.write_points.reset_mock()
- def test_event_listener_fail_write(self, mock_client):
- """Test the event listener for write failures."""
- self._setup()
-
- state = mock.MagicMock(
- state=1, domain='fake', entity_id='fake.entity-id',
- object_id='entity', attributes={})
- event = mock.MagicMock(data={'new_state': state}, time_fired=12345)
- mock_client.return_value.write_points.side_effect = \
- influx_client.exceptions.InfluxDBClientError('foo')
- self.handler_method(event)
-
def test_event_listener_states(self, mock_client):
"""Test the event listener against ignored states."""
self._setup()
@@ -245,6 +231,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
if state_state == 1:
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
@@ -278,6 +265,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
if entity_id == 'ok':
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
@@ -312,6 +300,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
if domain == 'ok':
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
@@ -356,6 +345,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
if entity_id == 'included':
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
@@ -401,6 +391,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
if domain == 'fake':
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
@@ -456,6 +447,7 @@ class TestInfluxDB(unittest.TestCase):
body[0]['fields']['value'] = out[1]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
)
@@ -498,6 +490,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
if entity_id == 'ok':
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
@@ -543,6 +536,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
)
@@ -588,6 +582,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
)
@@ -648,6 +643,7 @@ class TestInfluxDB(unittest.TestCase):
},
}]
self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
self.assertEqual(
mock_client.return_value.write_points.call_count, 1
)
@@ -659,7 +655,16 @@ class TestInfluxDB(unittest.TestCase):
def test_scheduled_write(self, mock_client):
"""Test the event listener to retry after write failures."""
- self._setup(max_retries=1)
+ config = {
+ 'influxdb': {
+ 'host': 'host',
+ 'username': 'user',
+ 'password': 'pass',
+ 'max_retries': 1
+ }
+ }
+ assert setup_component(self.hass, influxdb.DOMAIN, config)
+ self.handler_method = self.hass.bus.listen.call_args_list[0][0][1]
state = mock.MagicMock(
state=1, domain='fake', entity_id='entity.id', object_id='entity',
@@ -668,152 +673,47 @@ class TestInfluxDB(unittest.TestCase):
mock_client.return_value.write_points.side_effect = \
IOError('foo')
- start = dt_util.utcnow()
-
- self.handler_method(event)
+ # Write fails
+ with mock.patch.object(influxdb.time, 'sleep') as mock_sleep:
+ self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
+ assert mock_sleep.called
json_data = mock_client.return_value.write_points.call_args[0][0]
- self.assertEqual(mock_client.return_value.write_points.call_count, 1)
-
- shifted_time = start + (timedelta(seconds=20 + 1))
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
- {ha.ATTR_NOW: shifted_time})
- self.hass.block_till_done()
self.assertEqual(mock_client.return_value.write_points.call_count, 2)
mock_client.return_value.write_points.assert_called_with(json_data)
- shifted_time = shifted_time + (timedelta(seconds=20 + 1))
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
- {ha.ATTR_NOW: shifted_time})
- self.hass.block_till_done()
- self.assertEqual(mock_client.return_value.write_points.call_count, 2)
+ # Write works again
+ mock_client.return_value.write_points.side_effect = None
+ with mock.patch.object(influxdb.time, 'sleep') as mock_sleep:
+ self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
+ assert not mock_sleep.called
+ self.assertEqual(mock_client.return_value.write_points.call_count, 3)
+ def test_queue_backlog_full(self, mock_client):
+ """Test the event listener to drop old events."""
+ self._setup()
-class TestRetryOnErrorDecorator(unittest.TestCase):
- """Test the RetryOnError decorator."""
+ state = mock.MagicMock(
+ state=1, domain='fake', entity_id='entity.id', object_id='entity',
+ attributes={})
+ event = mock.MagicMock(data={'new_state': state}, time_fired=12345)
- def setUp(self):
- """Setup things to be run when tests are started."""
- self.hass = get_test_home_assistant()
+ monotonic_time = 0
- def tearDown(self):
- """Clear data."""
- self.hass.stop()
+ def fast_monotonic():
+ """Monotonic time that ticks fast enough to cause a timeout."""
+ nonlocal monotonic_time
+ monotonic_time += 60
+ return monotonic_time
- def test_no_retry(self):
- """Test that it does not retry if configured."""
- mock_method = MagicMock()
- wrapped = influxdb.RetryOnError(self.hass)(mock_method)
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 1)
- mock_method.assert_called_with(1, 2, test=3)
+ with mock.patch('homeassistant.components.influxdb.time.monotonic',
+ new=fast_monotonic):
+ self.handler_method(event)
+ self.hass.data[influxdb.DOMAIN].block_till_done()
- mock_method.side_effect = Exception()
- self.assertRaises(Exception, wrapped, 1, 2, test=3)
- self.assertEqual(mock_method.call_count, 2)
- mock_method.assert_called_with(1, 2, test=3)
+ self.assertEqual(
+ mock_client.return_value.write_points.call_count, 0
+ )
- def test_single_retry(self):
- """Test that retry stops after a single try if configured."""
- mock_method = MagicMock()
- retryer = influxdb.RetryOnError(self.hass, retry_limit=1)
- wrapped = retryer(mock_method)
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 1)
- mock_method.assert_called_with(1, 2, test=3)
-
- start = dt_util.utcnow()
- shifted_time = start + (timedelta(seconds=20 + 1))
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
- {ha.ATTR_NOW: shifted_time})
- self.hass.block_till_done()
- self.assertEqual(mock_method.call_count, 1)
-
- mock_method.side_effect = Exception()
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 2)
- mock_method.assert_called_with(1, 2, test=3)
-
- for cnt in range(3):
- start = dt_util.utcnow()
- shifted_time = start + (timedelta(seconds=20 + 1))
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
- {ha.ATTR_NOW: shifted_time})
- self.hass.block_till_done()
- self.assertEqual(mock_method.call_count, 3)
- mock_method.assert_called_with(1, 2, test=3)
-
- def test_multi_retry(self):
- """Test that multiple retries work."""
- mock_method = MagicMock()
- retryer = influxdb.RetryOnError(self.hass, retry_limit=4)
- wrapped = retryer(mock_method)
- mock_method.side_effect = Exception()
-
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 1)
- mock_method.assert_called_with(1, 2, test=3)
-
- for cnt in range(3):
- start = dt_util.utcnow()
- shifted_time = start + (timedelta(seconds=20 + 1))
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
- {ha.ATTR_NOW: shifted_time})
- self.hass.block_till_done()
- self.assertEqual(mock_method.call_count, cnt + 2)
- mock_method.assert_called_with(1, 2, test=3)
-
- def test_max_queue(self):
- """Test the maximum queue length."""
- # make a wrapped method
- mock_method = MagicMock()
- retryer = influxdb.RetryOnError(
- self.hass, retry_limit=4, queue_limit=3)
- wrapped = retryer(mock_method)
- mock_method.side_effect = Exception()
-
- # call it once, call fails, queue fills to 1
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 1)
- mock_method.assert_called_with(1, 2, test=3)
- self.assertEqual(len(wrapped._retry_queue), 1)
-
- # two more calls that failed. queue is 3
- wrapped(1, 2, test=3)
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 3)
- self.assertEqual(len(wrapped._retry_queue), 3)
-
- # another call, queue gets limited to 3
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 4)
- self.assertEqual(len(wrapped._retry_queue), 3)
-
- # time passes
- start = dt_util.utcnow()
- shifted_time = start + (timedelta(seconds=20 + 1))
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
- {ha.ATTR_NOW: shifted_time})
- self.hass.block_till_done()
-
- # only the three queued calls where repeated
- self.assertEqual(mock_method.call_count, 7)
- self.assertEqual(len(wrapped._retry_queue), 3)
-
- # another call, queue stays limited
- wrapped(1, 2, test=3)
- self.assertEqual(mock_method.call_count, 8)
- self.assertEqual(len(wrapped._retry_queue), 3)
-
- # disable the side effect
- mock_method.side_effect = None
-
- # time passes, all calls should succeed
- start = dt_util.utcnow()
- shifted_time = start + (timedelta(seconds=20 + 1))
- self.hass.bus.fire(ha.EVENT_TIME_CHANGED,
- {ha.ATTR_NOW: shifted_time})
- self.hass.block_till_done()
-
- # three queued calls succeeded, queue empty.
- self.assertEqual(mock_method.call_count, 11)
- self.assertEqual(len(wrapped._retry_queue), 0)
+ mock_client.return_value.write_points.reset_mock()
diff --git a/tests/components/test_init.py b/tests/components/test_init.py
index 06ba8a57508..dde141b6495 100644
--- a/tests/components/test_init.py
+++ b/tests/components/test_init.py
@@ -1,4 +1,4 @@
-"""The testd for Core components."""
+"""The tests for Core components."""
# pylint: disable=protected-access
import asyncio
import unittest
diff --git a/tests/components/test_input_text.py b/tests/components/test_input_text.py
index be22e1122ea..c288375ec8f 100644
--- a/tests/components/test_input_text.py
+++ b/tests/components/test_input_text.py
@@ -64,6 +64,41 @@ class TestInputText(unittest.TestCase):
state = self.hass.states.get(entity_id)
self.assertEqual('testing', str(state.state))
+ def test_mode(self):
+ """Test mode settings."""
+ self.assertTrue(
+ setup_component(self.hass, DOMAIN, {DOMAIN: {
+ 'test_default_text': {
+ 'initial': 'test',
+ 'min': 3,
+ 'max': 10,
+ },
+ 'test_explicit_text': {
+ 'initial': 'test',
+ 'min': 3,
+ 'max': 10,
+ 'mode': 'text',
+ },
+ 'test_explicit_password': {
+ 'initial': 'test',
+ 'min': 3,
+ 'max': 10,
+ 'mode': 'password',
+ },
+ }}))
+
+ state = self.hass.states.get('input_text.test_default_text')
+ assert state
+ self.assertEqual('text', state.attributes['mode'])
+
+ state = self.hass.states.get('input_text.test_explicit_text')
+ assert state
+ self.assertEqual('text', state.attributes['mode'])
+
+ state = self.hass.states.get('input_text.test_explicit_password')
+ assert state
+ self.assertEqual('password', state.attributes['mode'])
+
@asyncio.coroutine
def test_restore_state(hass):
diff --git a/tests/components/test_melissa.py b/tests/components/test_melissa.py
new file mode 100644
index 00000000000..e39ceb1add1
--- /dev/null
+++ b/tests/components/test_melissa.py
@@ -0,0 +1,38 @@
+"""The test for the Melissa Climate component."""
+import unittest
+from tests.common import get_test_home_assistant, MockDependency
+
+from homeassistant.components import melissa
+
+VALID_CONFIG = {
+ "melissa": {
+ "username": "********",
+ "password": "********",
+ }
+}
+
+
+class TestMelissa(unittest.TestCase):
+ """Test the Melissa component."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Initialize the values for this test class."""
+ self.hass = get_test_home_assistant()
+ self.config = VALID_CONFIG
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Teardown this test class. Stop hass."""
+ self.hass.stop()
+
+ @MockDependency("melissa")
+ def test_setup(self, mocked_melissa):
+ """Test setting up the Melissa component."""
+ melissa.setup(self.hass, self.config)
+
+ mocked_melissa.Melissa.assert_called_with(
+ username="********", password="********")
+ self.assertIn(melissa.DATA_MELISSA, self.hass.data)
+ self.assertIsInstance(
+ self.hass.data[melissa.DATA_MELISSA], type(
+ mocked_melissa.Melissa())
+ )
diff --git a/tests/components/test_nuheat.py b/tests/components/test_nuheat.py
index 91a8b326bf9..2b0f249f4c9 100644
--- a/tests/components/test_nuheat.py
+++ b/tests/components/test_nuheat.py
@@ -35,11 +35,11 @@ class TestNuHeat(unittest.TestCase):
mocked_nuheat.NuHeat.assert_called_with("warm", "feet")
self.assertIn(nuheat.DOMAIN, self.hass.data)
- self.assertEquals(2, len(self.hass.data[nuheat.DOMAIN]))
+ self.assertEqual(2, len(self.hass.data[nuheat.DOMAIN]))
self.assertIsInstance(
self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat())
)
- self.assertEquals(self.hass.data[nuheat.DOMAIN][1], "thermostat123")
+ self.assertEqual(self.hass.data[nuheat.DOMAIN][1], "thermostat123")
mocked_load.assert_called_with(
self.hass, "climate", nuheat.DOMAIN, {}, self.config
diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py
index 805d73e1820..ef702b96f4b 100644
--- a/tests/components/test_panel_iframe.py
+++ b/tests/components/test_panel_iframe.py
@@ -11,11 +11,11 @@ from tests.common import get_test_home_assistant
class TestPanelIframe(unittest.TestCase):
"""Test the panel_iframe component."""
- def setup_method(self, method):
+ def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
- def teardown_method(self, method):
+ def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
@@ -50,6 +50,11 @@ class TestPanelIframe(unittest.TestCase):
'title': 'Weather',
'url': 'https://www.wunderground.com/us/ca/san-diego',
},
+ 'api': {
+ 'icon': 'mdi:weather',
+ 'title': 'Api',
+ 'url': '/api',
+ },
},
})
@@ -72,3 +77,12 @@ class TestPanelIframe(unittest.TestCase):
'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
'url_path': 'weather',
}
+
+ assert panels.get('api').to_response(self.hass, None) == {
+ 'component_name': 'iframe',
+ 'config': {'url': '/api'},
+ 'icon': 'mdi:weather',
+ 'title': 'Api',
+ 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html',
+ 'url_path': 'api',
+ }
diff --git a/tests/components/test_pilight.py b/tests/components/test_pilight.py
index 7bdd44136e8..010136ee0e7 100644
--- a/tests/components/test_pilight.py
+++ b/tests/components/test_pilight.py
@@ -304,7 +304,7 @@ class TestPilight(unittest.TestCase):
with assert_setup_component(4):
whitelist = {
'protocol': [PilightDaemonSim.test_message['protocol'],
- 'other_protocoll'],
+ 'other_protocol'],
'id': [PilightDaemonSim.test_message['message']['id']]}
self.assertTrue(setup_component(
self.hass, pilight.DOMAIN,
@@ -330,7 +330,7 @@ class TestPilight(unittest.TestCase):
"""Check whitelist filter with unmatched data, should not work."""
with assert_setup_component(4):
whitelist = {
- 'protocol': ['wrong_protocoll'],
+ 'protocol': ['wrong_protocol'],
'id': [PilightDaemonSim.test_message['message']['id']]}
self.assertTrue(setup_component(
self.hass, pilight.DOMAIN,
diff --git a/tests/components/test_plant.py b/tests/components/test_plant.py
index f5a042ac8c1..ee1372509d9 100644
--- a/tests/components/test_plant.py
+++ b/tests/components/test_plant.py
@@ -101,11 +101,11 @@ class TestPlant(unittest.TestCase):
{ATTR_UNIT_OF_MEASUREMENT: 'us/cm'})
self.hass.block_till_done()
state = self.hass.states.get('plant.'+plant_name)
- self.assertEquals(STATE_PROBLEM, state.state)
- self.assertEquals(5, state.attributes[plant.READING_MOISTURE])
+ self.assertEqual(STATE_PROBLEM, state.state)
+ self.assertEqual(5, state.attributes[plant.READING_MOISTURE])
@pytest.mark.skipif(plant.ENABLE_LOAD_HISTORY is False,
- reason="tests for loading from DB are instable, thus"
+ reason="tests for loading from DB are unstable, thus"
"this feature is turned of until tests become"
"stable")
def test_load_from_db(self):
@@ -132,10 +132,10 @@ class TestPlant(unittest.TestCase):
self.hass.block_till_done()
state = self.hass.states.get('plant.'+plant_name)
- self.assertEquals(STATE_UNKNOWN, state.state)
+ self.assertEqual(STATE_UNKNOWN, state.state)
max_brightness = state.attributes.get(
plant.ATTR_MAX_BRIGHTNESS_HISTORY)
- self.assertEquals(30, max_brightness)
+ self.assertEqual(30, max_brightness)
def test_brightness_history(self):
"""Test the min_brightness check."""
@@ -149,19 +149,19 @@ class TestPlant(unittest.TestCase):
{ATTR_UNIT_OF_MEASUREMENT: 'lux'})
self.hass.block_till_done()
state = self.hass.states.get('plant.'+plant_name)
- self.assertEquals(STATE_PROBLEM, state.state)
+ self.assertEqual(STATE_PROBLEM, state.state)
self.hass.states.set(BRIGHTNESS_ENTITY, 600,
{ATTR_UNIT_OF_MEASUREMENT: 'lux'})
self.hass.block_till_done()
state = self.hass.states.get('plant.'+plant_name)
- self.assertEquals(STATE_OK, state.state)
+ self.assertEqual(STATE_OK, state.state)
self.hass.states.set(BRIGHTNESS_ENTITY, 100,
{ATTR_UNIT_OF_MEASUREMENT: 'lux'})
self.hass.block_till_done()
state = self.hass.states.get('plant.'+plant_name)
- self.assertEquals(STATE_OK, state.state)
+ self.assertEqual(STATE_OK, state.state)
class TestDailyHistory(unittest.TestCase):
@@ -195,4 +195,4 @@ class TestDailyHistory(unittest.TestCase):
for i in range(len(days)):
dh.add_measurement(values[i], days[i])
- self.assertEquals(max_values[i], dh.max)
+ self.assertEqual(max_values[i], dh.max)
diff --git a/tests/components/test_remember_the_milk.py b/tests/components/test_remember_the_milk.py
index 1b6619aca9c..d9db61efd40 100644
--- a/tests/components/test_remember_the_milk.py
+++ b/tests/components/test_remember_the_milk.py
@@ -54,7 +54,7 @@ class TestConfiguration(unittest.TestCase):
def test_invalid_data(self):
"""Test starts with invalid data and should not raise an exception."""
with patch("builtins.open",
- mock_open(read_data='random charachters')),\
+ mock_open(read_data='random characters')),\
patch("os.path.isfile", Mock(return_value=True)):
config = rtm.RememberTheMilkConfiguration(self.hass)
self.assertIsNotNone(config)
diff --git a/tests/components/test_rflink.py b/tests/components/test_rflink.py
index e7907fc6b54..9f6573920ca 100644
--- a/tests/components/test_rflink.py
+++ b/tests/components/test_rflink.py
@@ -47,7 +47,7 @@ def mock_rflink(hass, config, domain, monkeypatch, failures=None,
'rflink.protocol.create_rflink_connection',
mock_create)
- # verify instanstiation of component with given config
+ # verify instantiation of component with given config
with assert_setup_component(platform_count, domain):
yield from async_setup_component(hass, domain, config)
diff --git a/tests/components/test_rss_feed_template.py b/tests/components/test_rss_feed_template.py
index 60eb2530ea1..8b16b5519e9 100644
--- a/tests/components/test_rss_feed_template.py
+++ b/tests/components/test_rss_feed_template.py
@@ -25,7 +25,7 @@ def mock_http_client(loop, hass, test_client):
@asyncio.coroutine
-def test_get_noexistant_feed(mock_http_client):
+def test_get_nonexistant_feed(mock_http_client):
"""Test if we can retrieve the correct rss feed."""
resp = yield from mock_http_client.get('/api/rss_template/otherfeed')
assert resp.status == 404
diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py
index a3e7d662483..d119c60dba2 100644
--- a/tests/components/test_system_log.py
+++ b/tests/components/test_system_log.py
@@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
import pytest
+from homeassistant.core import callback
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import system_log
@@ -13,7 +14,7 @@ _LOGGER = logging.getLogger('test_logger')
@pytest.fixture(autouse=True)
@asyncio.coroutine
-def setup_test_case(hass):
+def setup_test_case(hass, test_client):
"""Setup system_log component before test case."""
config = {'system_log': {'max_entries': 2}}
yield from async_setup_component(hass, system_log.DOMAIN, config)
@@ -34,7 +35,7 @@ def get_error_log(hass, test_client, expected_count):
def _generate_and_log_exception(exception, log):
try:
raise Exception(exception)
- except: # pylint: disable=bare-except
+ except: # noqa: E722 # pylint: disable=bare-except
_LOGGER.exception(log)
@@ -85,6 +86,25 @@ def test_error(hass, test_client):
assert_log(log, '', 'error message', 'ERROR')
+@asyncio.coroutine
+def test_error_posted_as_event(hass, test_client):
+ """Test that error are posted as events."""
+ events = []
+
+ @callback
+ def event_listener(event):
+ """Listen to events of type system_log_event."""
+ events.append(event)
+
+ hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener)
+
+ _LOGGER.error('error message')
+ yield from hass.async_block_till_done()
+
+ assert len(events) == 1
+ assert_log(events[0].data, '', 'error message', 'ERROR')
+
+
@asyncio.coroutine
def test_critical(hass, test_client):
"""Test that critical are logged and retrieved correctly."""
@@ -189,10 +209,10 @@ def log_error_from_test_path(path):
@asyncio.coroutine
def test_homeassistant_path(hass, test_client):
"""Test error logged from homeassistant path."""
- log_error_from_test_path('venv_path/homeassistant/component/component.py')
-
with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH',
new=['venv_path/homeassistant']):
+ log_error_from_test_path(
+ 'venv_path/homeassistant/component/component.py')
log = (yield from get_error_log(hass, test_client, 1))[0]
assert log['source'] == 'component/component.py'
@@ -200,9 +220,8 @@ def test_homeassistant_path(hass, test_client):
@asyncio.coroutine
def test_config_path(hass, test_client):
"""Test error logged from config path."""
- log_error_from_test_path('config/custom_component/test.py')
-
with patch.object(hass.config, 'config_dir', new='config'):
+ log_error_from_test_path('config/custom_component/test.py')
log = (yield from get_error_log(hass, test_client, 1))[0]
assert log['source'] == 'custom_component/test.py'
@@ -210,9 +229,8 @@ def test_config_path(hass, test_client):
@asyncio.coroutine
def test_netdisco_path(hass, test_client):
"""Test error logged from netdisco path."""
- log_error_from_test_path('venv_path/netdisco/disco_component.py')
-
with patch.dict('sys.modules',
netdisco=MagicMock(__path__=['venv_path/netdisco'])):
+ log_error_from_test_path('venv_path/netdisco/disco_component.py')
log = (yield from get_error_log(hass, test_client, 1))[0]
assert log['source'] == 'disco_component.py'
diff --git a/tests/components/test_weblink.py b/tests/components/test_weblink.py
index e8768342db9..249e81d37af 100644
--- a/tests/components/test_weblink.py
+++ b/tests/components/test_weblink.py
@@ -26,6 +26,71 @@ class TestComponentWeblink(unittest.TestCase):
}
}))
+ def test_bad_config_relative_url(self):
+ """Test if new entity is created."""
+ self.assertFalse(setup_component(self.hass, 'weblink', {
+ 'weblink': {
+ 'entities': [
+ {
+ weblink.CONF_NAME: 'My router',
+ weblink.CONF_URL: '../states/group.bla'
+ },
+ ],
+ }
+ }))
+
+ def test_bad_config_relative_file(self):
+ """Test if new entity is created."""
+ self.assertFalse(setup_component(self.hass, 'weblink', {
+ 'weblink': {
+ 'entities': [
+ {
+ weblink.CONF_NAME: 'My group',
+ weblink.CONF_URL: 'group.bla'
+ },
+ ],
+ }
+ }))
+
+ def test_good_config_absolute_path(self):
+ """Test if new entity is created."""
+ self.assertTrue(setup_component(self.hass, 'weblink', {
+ 'weblink': {
+ 'entities': [
+ {
+ weblink.CONF_NAME: 'My second URL',
+ weblink.CONF_URL: '/states/group.bla'
+ },
+ ],
+ }
+ }))
+
+ def test_good_config_path_short(self):
+ """Test if new entity is created."""
+ self.assertTrue(setup_component(self.hass, 'weblink', {
+ 'weblink': {
+ 'entities': [
+ {
+ weblink.CONF_NAME: 'My third URL',
+ weblink.CONF_URL: '/states'
+ },
+ ],
+ }
+ }))
+
+ def test_good_config_path_directory(self):
+ """Test if new entity is created."""
+ self.assertTrue(setup_component(self.hass, 'weblink', {
+ 'weblink': {
+ 'entities': [
+ {
+ weblink.CONF_NAME: 'My last URL',
+ weblink.CONF_URL: '/states/bla/'
+ },
+ ],
+ }
+ }))
+
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/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py
index e08229631cf..5b4ef4dcf53 100644
--- a/tests/components/tts/test_yandextts.py
+++ b/tests/components/tts/test_yandextts.py
@@ -224,7 +224,7 @@ class TestTTSYandexPlatform(object):
assert len(calls) == 0
- def test_service_say_specifed_speaker(self, aioclient_mock):
+ def test_service_say_specified_speaker(self, aioclient_mock):
"""Test service call say."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
@@ -259,7 +259,7 @@ class TestTTSYandexPlatform(object):
assert len(aioclient_mock.mock_calls) == 1
assert len(calls) == 1
- def test_service_say_specifed_emotion(self, aioclient_mock):
+ def test_service_say_specified_emotion(self, aioclient_mock):
"""Test service call say."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py
index e548efb0eb2..828385b9ded 100644
--- a/tests/components/zwave/test_init.py
+++ b/tests/components/zwave/test_init.py
@@ -198,7 +198,7 @@ def test_device_entity(hass, mock_openzwave):
yield from hass.async_block_till_done()
assert not device.should_poll
- assert device.unique_id == "ZWAVE-10-11"
+ assert device.unique_id == "10-11"
assert device.name == 'Mock Node Sensor'
assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123
@@ -899,7 +899,7 @@ class TestZWaveServices(unittest.TestCase):
assert value.label == "New Label"
def test_set_poll_intensity_enable(self):
- """Test zwave set_poll_intensity service, succsessful set."""
+ """Test zwave set_poll_intensity service, successful set."""
node = MockNode(node_id=14)
value = MockValue(index=12, value_id=123456, poll_intensity=0)
node.values = {123456: value}
diff --git a/tests/fixtures/melissa_cur_settings.json b/tests/fixtures/melissa_cur_settings.json
new file mode 100644
index 00000000000..9d7fb615330
--- /dev/null
+++ b/tests/fixtures/melissa_cur_settings.json
@@ -0,0 +1,28 @@
+{
+ "controller": {
+ "id": 1,
+ "user_id": 1,
+ "serial_number": "12345678",
+ "mac": "12345678",
+ "firmware_version": "V1SHTHF",
+ "name": "Melissa 12345678",
+ "type": "melissa",
+ "room_id": null,
+ "created": "2016-07-06 18:59:46",
+ "deleted_at": null,
+ "online": true,
+ "_relation": {
+ "command_log": {
+ "state": 1,
+ "mode": 2,
+ "temp": 16,
+ "fan": 1
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/v1/controllers/12345678"
+ }
+ }
+}
diff --git a/tests/fixtures/melissa_fetch_devices.json b/tests/fixtures/melissa_fetch_devices.json
new file mode 100644
index 00000000000..4b106a613f7
--- /dev/null
+++ b/tests/fixtures/melissa_fetch_devices.json
@@ -0,0 +1,27 @@
+{
+ "12345678": {
+ "user_id": 1,
+ "serial_number": "12345678",
+ "mac": "12345678",
+ "firmware_version": "V1SHTHF",
+ "name": "Melissa 12345678",
+ "type": "melissa",
+ "room_id": null,
+ "created": "2016-07-06 18:59:46",
+ "id": 1,
+ "online": true,
+ "brand_id": 1,
+ "controller_log": {
+ "temp": 27.4,
+ "created": "2018-01-08T21:01:14.281Z",
+ "raw_temperature": 28928,
+ "humidity": 18.7,
+ "raw_humidity": 12946
+ },
+ "_links": {
+ "self": {
+ "href": "/v1/controllers"
+ }
+ }
+ }
+}
diff --git a/tests/fixtures/melissa_status.json b/tests/fixtures/melissa_status.json
new file mode 100644
index 00000000000..ac240b3df12
--- /dev/null
+++ b/tests/fixtures/melissa_status.json
@@ -0,0 +1,8 @@
+{
+ "12345678": {
+ "temp": 27.4,
+ "raw_temperature": 28928,
+ "humidity": 18.7,
+ "raw_humidity": 12946
+ }
+}
diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py
index e413fc145ca..4211e3da31b 100644
--- a/tests/helpers/test_entity.py
+++ b/tests/helpers/test_entity.py
@@ -232,8 +232,8 @@ def test_async_schedule_update_ha_state(hass):
@asyncio.coroutine
-def test_async_pararell_updates_with_zero(hass):
- """Test pararell updates with 0 (disabled)."""
+def test_async_parallel_updates_with_zero(hass):
+ """Test parallel updates with 0 (disabled)."""
updates = []
test_lock = asyncio.Event(loop=hass.loop)
@@ -269,11 +269,11 @@ def test_async_pararell_updates_with_zero(hass):
@asyncio.coroutine
-def test_async_pararell_updates_with_one(hass):
- """Test pararell updates with 1 (sequential)."""
+def test_async_parallel_updates_with_one(hass):
+ """Test parallel updates with 1 (sequential)."""
updates = []
test_lock = asyncio.Lock(loop=hass.loop)
- test_semephore = asyncio.Semaphore(1, loop=hass.loop)
+ test_semaphore = asyncio.Semaphore(1, loop=hass.loop)
yield from test_lock.acquire()
@@ -284,7 +284,7 @@ def test_async_pararell_updates_with_one(hass):
self.entity_id = entity_id
self.hass = hass
self._count = count
- self.parallel_updates = test_semephore
+ self.parallel_updates = test_semaphore
@asyncio.coroutine
def async_update(self):
@@ -332,11 +332,11 @@ def test_async_pararell_updates_with_one(hass):
@asyncio.coroutine
-def test_async_pararell_updates_with_two(hass):
- """Test pararell updates with 2 (pararell)."""
+def test_async_parallel_updates_with_two(hass):
+ """Test parallel updates with 2 (parallel)."""
updates = []
test_lock = asyncio.Lock(loop=hass.loop)
- test_semephore = asyncio.Semaphore(2, loop=hass.loop)
+ test_semaphore = asyncio.Semaphore(2, loop=hass.loop)
yield from test_lock.acquire()
@@ -347,7 +347,7 @@ def test_async_pararell_updates_with_two(hass):
self.entity_id = entity_id
self.hass = hass
self._count = count
- self.parallel_updates = test_semephore
+ self.parallel_updates = test_semaphore
@asyncio.coroutine
def async_update(self):
diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py
index 3109ea776bc..ef92da3172b 100644
--- a/tests/helpers/test_entity_component.py
+++ b/tests/helpers/test_entity_component.py
@@ -4,67 +4,27 @@ import asyncio
from collections import OrderedDict
import logging
import unittest
-from unittest.mock import patch, Mock, MagicMock
+from unittest.mock import patch, Mock
from datetime import timedelta
import homeassistant.core as ha
import homeassistant.loader as loader
from homeassistant.exceptions import PlatformNotReady
from homeassistant.components import group
-from homeassistant.helpers.entity import Entity, generate_entity_id
-from homeassistant.helpers.entity_component import (
- EntityComponent, DEFAULT_SCAN_INTERVAL, SLOW_SETUP_WARNING)
-from homeassistant.helpers import entity_component
+from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.setup import setup_component
from homeassistant.helpers import discovery
import homeassistant.util.dt as dt_util
from tests.common import (
- get_test_home_assistant, MockPlatform, MockModule, fire_time_changed,
- mock_coro, async_fire_time_changed)
+ get_test_home_assistant, MockPlatform, MockModule, mock_coro,
+ async_fire_time_changed, MockEntity)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "test_domain"
-class EntityTest(Entity):
- """Test for the Entity component."""
-
- def __init__(self, **values):
- """Initialize an entity."""
- self._values = values
-
- if 'entity_id' in values:
- self.entity_id = values['entity_id']
-
- @property
- def name(self):
- """Return the name of the entity."""
- return self._handle('name')
-
- @property
- def should_poll(self):
- """Return the ste of the polling."""
- return self._handle('should_poll')
-
- @property
- def unique_id(self):
- """Return the unique ID of the entity."""
- return self._handle('unique_id')
-
- @property
- def available(self):
- """Return True if entity is available."""
- return self._handle('available')
-
- def _handle(self, attr):
- """Helper for the attributes."""
- if attr in self._values:
- return self._values[attr]
- return getattr(super(), attr)
-
-
class TestHelpersEntityComponent(unittest.TestCase):
"""Test homeassistant.helpers.entity_component module."""
@@ -85,7 +45,7 @@ class TestHelpersEntityComponent(unittest.TestCase):
# No group after setup
assert len(self.hass.states.entity_ids()) == 0
- component.add_entities([EntityTest()])
+ component.add_entities([MockEntity()])
self.hass.block_till_done()
# group exists
@@ -98,7 +58,7 @@ class TestHelpersEntityComponent(unittest.TestCase):
('test_domain.unnamed_device',)
# group extended
- component.add_entities([EntityTest(name='goodbye')])
+ component.add_entities([MockEntity(name='goodbye')])
self.hass.block_till_done()
assert len(self.hass.states.entity_ids()) == 3
@@ -108,175 +68,6 @@ class TestHelpersEntityComponent(unittest.TestCase):
assert group.attributes.get('entity_id') == \
('test_domain.goodbye', 'test_domain.unnamed_device')
- def test_polling_only_updates_entities_it_should_poll(self):
- """Test the polling of only updated entities."""
- component = EntityComponent(
- _LOGGER, DOMAIN, self.hass, timedelta(seconds=20))
-
- no_poll_ent = EntityTest(should_poll=False)
- no_poll_ent.async_update = Mock()
- poll_ent = EntityTest(should_poll=True)
- poll_ent.async_update = Mock()
-
- component.add_entities([no_poll_ent, poll_ent])
-
- no_poll_ent.async_update.reset_mock()
- poll_ent.async_update.reset_mock()
-
- fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20))
- self.hass.block_till_done()
-
- assert not no_poll_ent.async_update.called
- assert poll_ent.async_update.called
-
- def test_polling_updates_entities_with_exception(self):
- """Test the updated entities that not break with a exception."""
- component = EntityComponent(
- _LOGGER, DOMAIN, self.hass, timedelta(seconds=20))
-
- update_ok = []
- update_err = []
-
- def update_mock():
- """Mock normal update."""
- update_ok.append(None)
-
- def update_mock_err():
- """Mock error update."""
- update_err.append(None)
- raise AssertionError("Fake error update")
-
- ent1 = EntityTest(should_poll=True)
- ent1.update = update_mock_err
- ent2 = EntityTest(should_poll=True)
- ent2.update = update_mock
- ent3 = EntityTest(should_poll=True)
- ent3.update = update_mock
- ent4 = EntityTest(should_poll=True)
- ent4.update = update_mock
-
- component.add_entities([ent1, ent2, ent3, ent4])
-
- update_ok.clear()
- update_err.clear()
-
- fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20))
- self.hass.block_till_done()
-
- assert len(update_ok) == 3
- assert len(update_err) == 1
-
- def test_update_state_adds_entities(self):
- """Test if updating poll entities cause an entity to be added works."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
-
- ent1 = EntityTest()
- ent2 = EntityTest(should_poll=True)
-
- component.add_entities([ent2])
- assert 1 == len(self.hass.states.entity_ids())
- ent2.update = lambda *_: component.add_entities([ent1])
-
- fire_time_changed(
- self.hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL
- )
- self.hass.block_till_done()
-
- assert 2 == len(self.hass.states.entity_ids())
-
- def test_update_state_adds_entities_with_update_befor_add_true(self):
- """Test if call update before add to state machine."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
-
- ent = EntityTest()
- ent.update = Mock(spec_set=True)
-
- component.add_entities([ent], True)
- self.hass.block_till_done()
-
- assert 1 == len(self.hass.states.entity_ids())
- assert ent.update.called
-
- def test_update_state_adds_entities_with_update_befor_add_false(self):
- """Test if not call update before add to state machine."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
-
- ent = EntityTest()
- ent.update = Mock(spec_set=True)
-
- component.add_entities([ent], False)
- self.hass.block_till_done()
-
- assert 1 == len(self.hass.states.entity_ids())
- assert not ent.update.called
-
- def test_not_adding_duplicate_entities(self):
- """Test for not adding duplicate entities."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
-
- assert 0 == len(self.hass.states.entity_ids())
-
- component.add_entities([EntityTest(unique_id='not_very_unique')])
-
- assert 1 == len(self.hass.states.entity_ids())
-
- component.add_entities([EntityTest(unique_id='not_very_unique')])
-
- assert 1 == len(self.hass.states.entity_ids())
-
- def test_not_assigning_entity_id_if_prescribes_one(self):
- """Test for not assigning an entity ID."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
-
- assert 'hello.world' not in self.hass.states.entity_ids()
-
- component.add_entities([EntityTest(entity_id='hello.world')])
-
- assert 'hello.world' in self.hass.states.entity_ids()
-
- def test_extract_from_service_returns_all_if_no_entity_id(self):
- """Test the extraction of everything from service."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
- component.add_entities([
- EntityTest(name='test_1'),
- EntityTest(name='test_2'),
- ])
-
- call = ha.ServiceCall('test', 'service')
-
- assert ['test_domain.test_1', 'test_domain.test_2'] == \
- sorted(ent.entity_id for ent in
- component.extract_from_service(call))
-
- def test_extract_from_service_filter_out_non_existing_entities(self):
- """Test the extraction of non existing entities from service."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
- component.add_entities([
- EntityTest(name='test_1'),
- EntityTest(name='test_2'),
- ])
-
- call = ha.ServiceCall('test', 'service', {
- 'entity_id': ['test_domain.test_2', 'test_domain.non_exist']
- })
-
- assert ['test_domain.test_2'] == \
- [ent.entity_id for ent in component.extract_from_service(call)]
-
- def test_extract_from_service_no_group_expand(self):
- """Test not expanding a group."""
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
- test_group = group.Group.create_group(
- self.hass, 'test_group', ['light.Ceiling', 'light.Kitchen'])
- component.add_entities([test_group])
-
- call = ha.ServiceCall('test', 'service', {
- 'entity_id': ['group.test_group']
- })
-
- extracted = component.extract_from_service(call, expand_group=False)
- self.assertEqual([test_group], extracted)
-
def test_setup_loads_platforms(self):
"""Test the loading of the platforms."""
component_setup = Mock(return_value=True)
@@ -344,13 +135,13 @@ class TestHelpersEntityComponent(unittest.TestCase):
assert ('platform_test', {}, {'msg': 'discovery_info'}) == \
mock_setup.call_args[0]
- @patch('homeassistant.helpers.entity_component.'
+ @patch('homeassistant.helpers.entity_platform.'
'async_track_time_interval')
def test_set_scan_interval_via_config(self, mock_track):
"""Test the setting of the scan interval via configuration."""
def platform_setup(hass, config, add_devices, discovery_info=None):
"""Test the platform setup."""
- add_devices([EntityTest(should_poll=True)])
+ add_devices([MockEntity(should_poll=True)])
loader.set_component('test_domain.platform',
MockPlatform(platform_setup))
@@ -368,38 +159,13 @@ class TestHelpersEntityComponent(unittest.TestCase):
assert mock_track.called
assert timedelta(seconds=30) == mock_track.call_args[0][2]
- @patch('homeassistant.helpers.entity_component.'
- 'async_track_time_interval')
- def test_set_scan_interval_via_platform(self, mock_track):
- """Test the setting of the scan interval via platform."""
- def platform_setup(hass, config, add_devices, discovery_info=None):
- """Test the platform setup."""
- add_devices([EntityTest(should_poll=True)])
-
- platform = MockPlatform(platform_setup)
- platform.SCAN_INTERVAL = timedelta(seconds=30)
-
- loader.set_component('test_domain.platform', platform)
-
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
-
- component.setup({
- DOMAIN: {
- 'platform': 'platform',
- }
- })
-
- self.hass.block_till_done()
- assert mock_track.called
- assert timedelta(seconds=30) == mock_track.call_args[0][2]
-
def test_set_entity_namespace_via_config(self):
"""Test setting an entity namespace."""
def platform_setup(hass, config, add_devices, discovery_info=None):
"""Test the platform setup."""
add_devices([
- EntityTest(name='beer'),
- EntityTest(name=None),
+ MockEntity(name='beer'),
+ MockEntity(name=None),
])
platform = MockPlatform(platform_setup)
@@ -420,83 +186,16 @@ class TestHelpersEntityComponent(unittest.TestCase):
assert sorted(self.hass.states.entity_ids()) == \
['test_domain.yummy_beer', 'test_domain.yummy_unnamed_device']
- def test_adding_entities_with_generator_and_thread_callback(self):
- """Test generator in add_entities that calls thread method.
-
- We should make sure we resolve the generator to a list before passing
- it into an async context.
- """
- component = EntityComponent(_LOGGER, DOMAIN, self.hass)
-
- def create_entity(number):
- """Create entity helper."""
- entity = EntityTest()
- entity.entity_id = generate_entity_id(component.entity_id_format,
- 'Number', hass=self.hass)
- return entity
-
- component.add_entities(create_entity(i) for i in range(2))
-
-
-@asyncio.coroutine
-def test_platform_warn_slow_setup(hass):
- """Warn we log when platform setup takes a long time."""
- platform = MockPlatform()
-
- loader.set_component('test_domain.platform', platform)
-
- component = EntityComponent(_LOGGER, DOMAIN, hass)
-
- with patch.object(hass.loop, 'call_later', MagicMock()) \
- as mock_call:
- yield from component.async_setup({
- DOMAIN: {
- 'platform': 'platform',
- }
- })
- assert mock_call.called
-
- timeout, logger_method = mock_call.mock_calls[0][1][:2]
-
- assert timeout == SLOW_SETUP_WARNING
- assert logger_method == _LOGGER.warning
-
- assert mock_call().cancel.called
-
-
-@asyncio.coroutine
-def test_platform_error_slow_setup(hass, caplog):
- """Don't block startup more than SLOW_SETUP_MAX_WAIT."""
- with patch.object(entity_component, 'SLOW_SETUP_MAX_WAIT', 0):
- called = []
-
- @asyncio.coroutine
- def setup_platform(*args):
- called.append(1)
- yield from asyncio.sleep(1, loop=hass.loop)
-
- platform = MockPlatform(async_setup_platform=setup_platform)
- component = EntityComponent(_LOGGER, DOMAIN, hass)
- loader.set_component('test_domain.test_platform', platform)
- yield from component.async_setup({
- DOMAIN: {
- 'platform': 'test_platform',
- }
- })
- assert len(called) == 1
- assert 'test_domain.test_platform' not in hass.config.components
- assert 'test_platform is taking longer than 0 seconds' in caplog.text
-
@asyncio.coroutine
def test_extract_from_service_available_device(hass):
"""Test the extraction of entity from service and device is available."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
yield from component.async_add_entities([
- EntityTest(name='test_1'),
- EntityTest(name='test_2', available=False),
- EntityTest(name='test_3'),
- EntityTest(name='test_4', available=False),
+ MockEntity(name='test_1'),
+ MockEntity(name='test_2', available=False),
+ MockEntity(name='test_3'),
+ MockEntity(name='test_4', available=False),
])
call_1 = ha.ServiceCall('test', 'service')
@@ -514,26 +213,6 @@ def test_extract_from_service_available_device(hass):
component.async_extract_from_service(call_2))
-@asyncio.coroutine
-def test_updated_state_used_for_entity_id(hass):
- """Test that first update results used for entity ID generation."""
- component = EntityComponent(_LOGGER, DOMAIN, hass)
-
- class EntityTestNameFetcher(EntityTest):
- """Mock entity that fetches a friendly name."""
-
- @asyncio.coroutine
- def async_update(self):
- """Mock update that assigns a name."""
- self._values['name'] = "Living Room"
-
- yield from component.async_add_entities([EntityTestNameFetcher()], True)
-
- entity_ids = hass.states.async_entity_ids()
- assert 1 == len(entity_ids)
- assert entity_ids[0] == "test_domain.living_room"
-
-
@asyncio.coroutine
def test_platform_not_ready(hass):
"""Test that we retry when platform not ready."""
@@ -579,108 +258,50 @@ def test_platform_not_ready(hass):
@asyncio.coroutine
-def test_pararell_updates_async_platform(hass):
- """Warn we log when platform setup takes a long time."""
- platform = MockPlatform()
-
- @asyncio.coroutine
- def mock_update(*args, **kwargs):
- pass
-
- platform.async_setup_platform = mock_update
-
- loader.set_component('test_domain.platform', platform)
-
+def test_extract_from_service_returns_all_if_no_entity_id(hass):
+ """Test the extraction of everything from service."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
- component._platforms = {}
+ yield from component.async_add_entities([
+ MockEntity(name='test_1'),
+ MockEntity(name='test_2'),
+ ])
- yield from component.async_setup({
- DOMAIN: {
- 'platform': 'platform',
- }
+ call = ha.ServiceCall('test', 'service')
+
+ assert ['test_domain.test_1', 'test_domain.test_2'] == \
+ sorted(ent.entity_id for ent in
+ component.async_extract_from_service(call))
+
+
+@asyncio.coroutine
+def test_extract_from_service_filter_out_non_existing_entities(hass):
+ """Test the extraction of non existing entities from service."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ yield from component.async_add_entities([
+ MockEntity(name='test_1'),
+ MockEntity(name='test_2'),
+ ])
+
+ call = ha.ServiceCall('test', 'service', {
+ 'entity_id': ['test_domain.test_2', 'test_domain.non_exist']
})
- handle = list(component._platforms.values())[-1]
-
- assert handle.parallel_updates is None
+ assert ['test_domain.test_2'] == \
+ [ent.entity_id for ent
+ in component.async_extract_from_service(call)]
@asyncio.coroutine
-def test_pararell_updates_async_platform_with_constant(hass):
- """Warn we log when platform setup takes a long time."""
- platform = MockPlatform()
-
- @asyncio.coroutine
- def mock_update(*args, **kwargs):
- pass
-
- platform.async_setup_platform = mock_update
- platform.PARALLEL_UPDATES = 1
-
- loader.set_component('test_domain.platform', platform)
-
+def test_extract_from_service_no_group_expand(hass):
+ """Test not expanding a group."""
component = EntityComponent(_LOGGER, DOMAIN, hass)
- component._platforms = {}
+ test_group = yield from group.Group.async_create_group(
+ hass, 'test_group', ['light.Ceiling', 'light.Kitchen'])
+ yield from component.async_add_entities([test_group])
- yield from component.async_setup({
- DOMAIN: {
- 'platform': 'platform',
- }
+ call = ha.ServiceCall('test', 'service', {
+ 'entity_id': ['group.test_group']
})
- handle = list(component._platforms.values())[-1]
-
- assert handle.parallel_updates is not None
-
-
-@asyncio.coroutine
-def test_pararell_updates_sync_platform(hass):
- """Warn we log when platform setup takes a long time."""
- platform = MockPlatform()
-
- loader.set_component('test_domain.platform', platform)
-
- component = EntityComponent(_LOGGER, DOMAIN, hass)
- component._platforms = {}
-
- yield from component.async_setup({
- DOMAIN: {
- 'platform': 'platform',
- }
- })
-
- handle = list(component._platforms.values())[-1]
-
- assert handle.parallel_updates is not None
-
-
-@asyncio.coroutine
-def test_raise_error_on_update(hass):
- """Test the add entity if they raise an error on update."""
- updates = []
- component = EntityComponent(_LOGGER, DOMAIN, hass)
- entity1 = EntityTest(name='test_1')
- entity2 = EntityTest(name='test_2')
-
- def _raise():
- """Helper to raise a exception."""
- raise AssertionError
-
- entity1.update = _raise
- entity2.update = lambda: updates.append(1)
-
- yield from component.async_add_entities([entity1, entity2], True)
-
- assert len(updates) == 1
- assert 1 in updates
-
-
-@asyncio.coroutine
-def test_async_remove_with_platform(hass):
- """Remove an entity from a platform."""
- component = EntityComponent(_LOGGER, DOMAIN, hass)
- entity1 = EntityTest(name='test_1')
- yield from component.async_add_entities([entity1])
- assert len(hass.states.async_entity_ids()) == 1
- yield from entity1.async_remove()
- assert len(hass.states.async_entity_ids()) == 0
+ extracted = component.async_extract_from_service(call, expand_group=False)
+ assert extracted == [test_group]
diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py
new file mode 100644
index 00000000000..4c27cc45a00
--- /dev/null
+++ b/tests/helpers/test_entity_platform.py
@@ -0,0 +1,435 @@
+"""Tests for the EntityPlatform helper."""
+import asyncio
+import logging
+import unittest
+from unittest.mock import patch, Mock, MagicMock
+from datetime import timedelta
+
+import homeassistant.loader as loader
+from homeassistant.helpers.entity import generate_entity_id
+from homeassistant.helpers.entity_component import (
+ EntityComponent, DEFAULT_SCAN_INTERVAL)
+from homeassistant.helpers import entity_platform
+
+import homeassistant.util.dt as dt_util
+
+from tests.common import (
+ get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry,
+ MockEntity)
+
+_LOGGER = logging.getLogger(__name__)
+DOMAIN = "test_domain"
+
+
+class TestHelpersEntityPlatform(unittest.TestCase):
+ """Test homeassistant.helpers.entity_component module."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Initialize a test Home Assistant instance."""
+ self.hass = get_test_home_assistant()
+
+ def tearDown(self): # pylint: disable=invalid-name
+ """Clean up the test Home Assistant instance."""
+ self.hass.stop()
+
+ def test_polling_only_updates_entities_it_should_poll(self):
+ """Test the polling of only updated entities."""
+ component = EntityComponent(
+ _LOGGER, DOMAIN, self.hass, timedelta(seconds=20))
+
+ no_poll_ent = MockEntity(should_poll=False)
+ no_poll_ent.async_update = Mock()
+ poll_ent = MockEntity(should_poll=True)
+ poll_ent.async_update = Mock()
+
+ component.add_entities([no_poll_ent, poll_ent])
+
+ no_poll_ent.async_update.reset_mock()
+ poll_ent.async_update.reset_mock()
+
+ fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20))
+ self.hass.block_till_done()
+
+ assert not no_poll_ent.async_update.called
+ assert poll_ent.async_update.called
+
+ def test_polling_updates_entities_with_exception(self):
+ """Test the updated entities that not break with an exception."""
+ component = EntityComponent(
+ _LOGGER, DOMAIN, self.hass, timedelta(seconds=20))
+
+ update_ok = []
+ update_err = []
+
+ def update_mock():
+ """Mock normal update."""
+ update_ok.append(None)
+
+ def update_mock_err():
+ """Mock error update."""
+ update_err.append(None)
+ raise AssertionError("Fake error update")
+
+ ent1 = MockEntity(should_poll=True)
+ ent1.update = update_mock_err
+ ent2 = MockEntity(should_poll=True)
+ ent2.update = update_mock
+ ent3 = MockEntity(should_poll=True)
+ ent3.update = update_mock
+ ent4 = MockEntity(should_poll=True)
+ ent4.update = update_mock
+
+ component.add_entities([ent1, ent2, ent3, ent4])
+
+ update_ok.clear()
+ update_err.clear()
+
+ fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20))
+ self.hass.block_till_done()
+
+ assert len(update_ok) == 3
+ assert len(update_err) == 1
+
+ def test_update_state_adds_entities(self):
+ """Test if updating poll entities cause an entity to be added works."""
+ component = EntityComponent(_LOGGER, DOMAIN, self.hass)
+
+ ent1 = MockEntity()
+ ent2 = MockEntity(should_poll=True)
+
+ component.add_entities([ent2])
+ assert 1 == len(self.hass.states.entity_ids())
+ ent2.update = lambda *_: component.add_entities([ent1])
+
+ fire_time_changed(
+ self.hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL
+ )
+ self.hass.block_till_done()
+
+ assert 2 == len(self.hass.states.entity_ids())
+
+ def test_update_state_adds_entities_with_update_before_add_true(self):
+ """Test if call update before add to state machine."""
+ component = EntityComponent(_LOGGER, DOMAIN, self.hass)
+
+ ent = MockEntity()
+ ent.update = Mock(spec_set=True)
+
+ component.add_entities([ent], True)
+ self.hass.block_till_done()
+
+ assert 1 == len(self.hass.states.entity_ids())
+ assert ent.update.called
+
+ def test_update_state_adds_entities_with_update_before_add_false(self):
+ """Test if not call update before add to state machine."""
+ component = EntityComponent(_LOGGER, DOMAIN, self.hass)
+
+ ent = MockEntity()
+ ent.update = Mock(spec_set=True)
+
+ component.add_entities([ent], False)
+ self.hass.block_till_done()
+
+ assert 1 == len(self.hass.states.entity_ids())
+ assert not ent.update.called
+
+ @patch('homeassistant.helpers.entity_platform.'
+ 'async_track_time_interval')
+ def test_set_scan_interval_via_platform(self, mock_track):
+ """Test the setting of the scan interval via platform."""
+ def platform_setup(hass, config, add_devices, discovery_info=None):
+ """Test the platform setup."""
+ add_devices([MockEntity(should_poll=True)])
+
+ platform = MockPlatform(platform_setup)
+ platform.SCAN_INTERVAL = timedelta(seconds=30)
+
+ loader.set_component('test_domain.platform', platform)
+
+ component = EntityComponent(_LOGGER, DOMAIN, self.hass)
+
+ component.setup({
+ DOMAIN: {
+ 'platform': 'platform',
+ }
+ })
+
+ self.hass.block_till_done()
+ assert mock_track.called
+ assert timedelta(seconds=30) == mock_track.call_args[0][2]
+
+ def test_adding_entities_with_generator_and_thread_callback(self):
+ """Test generator in add_entities that calls thread method.
+
+ We should make sure we resolve the generator to a list before passing
+ it into an async context.
+ """
+ component = EntityComponent(_LOGGER, DOMAIN, self.hass)
+
+ def create_entity(number):
+ """Create entity helper."""
+ entity = MockEntity()
+ entity.entity_id = generate_entity_id(DOMAIN + '.{}',
+ 'Number', hass=self.hass)
+ return entity
+
+ component.add_entities(create_entity(i) for i in range(2))
+
+
+@asyncio.coroutine
+def test_platform_warn_slow_setup(hass):
+ """Warn we log when platform setup takes a long time."""
+ platform = MockPlatform()
+
+ loader.set_component('test_domain.platform', platform)
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+
+ with patch.object(hass.loop, 'call_later', MagicMock()) \
+ as mock_call:
+ yield from component.async_setup({
+ DOMAIN: {
+ 'platform': 'platform',
+ }
+ })
+ assert mock_call.called
+
+ timeout, logger_method = mock_call.mock_calls[0][1][:2]
+
+ assert timeout == entity_platform.SLOW_SETUP_WARNING
+ assert logger_method == _LOGGER.warning
+
+ assert mock_call().cancel.called
+
+
+@asyncio.coroutine
+def test_platform_error_slow_setup(hass, caplog):
+ """Don't block startup more than SLOW_SETUP_MAX_WAIT."""
+ with patch.object(entity_platform, 'SLOW_SETUP_MAX_WAIT', 0):
+ called = []
+
+ @asyncio.coroutine
+ def setup_platform(*args):
+ called.append(1)
+ yield from asyncio.sleep(1, loop=hass.loop)
+
+ platform = MockPlatform(async_setup_platform=setup_platform)
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ loader.set_component('test_domain.test_platform', platform)
+ yield from component.async_setup({
+ DOMAIN: {
+ 'platform': 'test_platform',
+ }
+ })
+ assert len(called) == 1
+ assert 'test_domain.test_platform' not in hass.config.components
+ assert 'test_platform is taking longer than 0 seconds' in caplog.text
+
+
+@asyncio.coroutine
+def test_updated_state_used_for_entity_id(hass):
+ """Test that first update results used for entity ID generation."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+
+ class MockEntityNameFetcher(MockEntity):
+ """Mock entity that fetches a friendly name."""
+
+ @asyncio.coroutine
+ def async_update(self):
+ """Mock update that assigns a name."""
+ self._values['name'] = "Living Room"
+
+ yield from component.async_add_entities([MockEntityNameFetcher()], True)
+
+ entity_ids = hass.states.async_entity_ids()
+ assert 1 == len(entity_ids)
+ assert entity_ids[0] == "test_domain.living_room"
+
+
+@asyncio.coroutine
+def test_parallel_updates_async_platform(hass):
+ """Warn we log when platform setup takes a long time."""
+ platform = MockPlatform()
+
+ @asyncio.coroutine
+ def mock_update(*args, **kwargs):
+ pass
+
+ platform.async_setup_platform = mock_update
+
+ loader.set_component('test_domain.platform', platform)
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ component._platforms = {}
+
+ yield from component.async_setup({
+ DOMAIN: {
+ 'platform': 'platform',
+ }
+ })
+
+ handle = list(component._platforms.values())[-1]
+
+ assert handle.parallel_updates is None
+
+
+@asyncio.coroutine
+def test_parallel_updates_async_platform_with_constant(hass):
+ """Warn we log when platform setup takes a long time."""
+ platform = MockPlatform()
+
+ @asyncio.coroutine
+ def mock_update(*args, **kwargs):
+ pass
+
+ platform.async_setup_platform = mock_update
+ platform.PARALLEL_UPDATES = 1
+
+ loader.set_component('test_domain.platform', platform)
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ component._platforms = {}
+
+ yield from component.async_setup({
+ DOMAIN: {
+ 'platform': 'platform',
+ }
+ })
+
+ handle = list(component._platforms.values())[-1]
+
+ assert handle.parallel_updates is not None
+
+
+@asyncio.coroutine
+def test_parallel_updates_sync_platform(hass):
+ """Warn we log when platform setup takes a long time."""
+ platform = MockPlatform()
+
+ loader.set_component('test_domain.platform', platform)
+
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ component._platforms = {}
+
+ yield from component.async_setup({
+ DOMAIN: {
+ 'platform': 'platform',
+ }
+ })
+
+ handle = list(component._platforms.values())[-1]
+
+ assert handle.parallel_updates is not None
+
+
+@asyncio.coroutine
+def test_raise_error_on_update(hass):
+ """Test the add entity if they raise an error on update."""
+ updates = []
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entity1 = MockEntity(name='test_1')
+ entity2 = MockEntity(name='test_2')
+
+ def _raise():
+ """Helper to raise an exception."""
+ raise AssertionError
+
+ entity1.update = _raise
+ entity2.update = lambda: updates.append(1)
+
+ yield from component.async_add_entities([entity1, entity2], True)
+
+ assert len(updates) == 1
+ assert 1 in updates
+
+
+@asyncio.coroutine
+def test_async_remove_with_platform(hass):
+ """Remove an entity from a platform."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ entity1 = MockEntity(name='test_1')
+ yield from component.async_add_entities([entity1])
+ assert len(hass.states.async_entity_ids()) == 1
+ yield from entity1.async_remove()
+ assert len(hass.states.async_entity_ids()) == 0
+
+
+@asyncio.coroutine
+def test_not_adding_duplicate_entities_with_unique_id(hass):
+ """Test for not adding duplicate entities."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+
+ yield from component.async_add_entities([
+ MockEntity(name='test1', unique_id='not_very_unique')])
+
+ assert len(hass.states.async_entity_ids()) == 1
+
+ yield from component.async_add_entities([
+ MockEntity(name='test2', unique_id='not_very_unique')])
+
+ assert len(hass.states.async_entity_ids()) == 1
+
+
+@asyncio.coroutine
+def test_using_prescribed_entity_id(hass):
+ """Test for using predefined entity ID."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ yield from component.async_add_entities([
+ MockEntity(name='bla', entity_id='hello.world')])
+ assert 'hello.world' in hass.states.async_entity_ids()
+
+
+@asyncio.coroutine
+def test_using_prescribed_entity_id_with_unique_id(hass):
+ """Test for ammending predefined entity ID because currently exists."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+
+ yield from component.async_add_entities([
+ MockEntity(entity_id='test_domain.world')])
+ yield from component.async_add_entities([
+ MockEntity(entity_id='test_domain.world', unique_id='bla')])
+
+ assert 'test_domain.world_2' in hass.states.async_entity_ids()
+
+
+@asyncio.coroutine
+def test_using_prescribed_entity_id_which_is_registered(hass):
+ """Test not allowing predefined entity ID that already registered."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ registry = mock_registry(hass)
+ # Register test_domain.world
+ registry.async_get_or_create(
+ DOMAIN, 'test', '1234', suggested_object_id='world')
+
+ # This entity_id will be rewritten
+ yield from component.async_add_entities([
+ MockEntity(entity_id='test_domain.world')])
+
+ assert 'test_domain.world_2' in hass.states.async_entity_ids()
+
+
+@asyncio.coroutine
+def test_name_which_conflict_with_registered(hass):
+ """Test not generating conflicting entity ID based on name."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ registry = mock_registry(hass)
+
+ # Register test_domain.world
+ registry.async_get_or_create(
+ DOMAIN, 'test', '1234', suggested_object_id='world')
+
+ yield from component.async_add_entities([
+ MockEntity(name='world')])
+
+ assert 'test_domain.world_2' in hass.states.async_entity_ids()
+
+
+@asyncio.coroutine
+def test_entity_with_name_and_entity_id_getting_registered(hass):
+ """Ensure that entity ID is used for registration."""
+ component = EntityComponent(_LOGGER, DOMAIN, hass)
+ yield from component.async_add_entities([
+ MockEntity(unique_id='1234', name='bla',
+ entity_id='test_domain.world')])
+ assert 'test_domain.world' in hass.states.async_entity_ids()
diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py
new file mode 100644
index 00000000000..d19a3f3fe49
--- /dev/null
+++ b/tests/helpers/test_entity_registry.py
@@ -0,0 +1,135 @@
+"""Tests for the Entity Registry."""
+import asyncio
+from unittest.mock import patch, mock_open
+
+import pytest
+
+from homeassistant.helpers import entity_registry
+
+from tests.common import mock_registry
+
+
+@pytest.fixture
+def registry(hass):
+ """Return an empty, loaded, registry."""
+ return mock_registry(hass)
+
+
+@asyncio.coroutine
+def test_get_or_create_returns_same_entry(registry):
+ """Make sure we do not duplicate entries."""
+ entry = registry.async_get_or_create('light', 'hue', '1234')
+ entry2 = registry.async_get_or_create('light', 'hue', '1234')
+
+ assert len(registry.entities) == 1
+ assert entry is entry2
+ assert entry.entity_id == 'light.hue_1234'
+
+
+@asyncio.coroutine
+def test_get_or_create_suggested_object_id(registry):
+ """Test that suggested_object_id works."""
+ entry = registry.async_get_or_create(
+ 'light', 'hue', '1234', suggested_object_id='beer')
+
+ assert entry.entity_id == 'light.beer'
+
+
+@asyncio.coroutine
+def test_get_or_create_suggested_object_id_conflict_register(registry):
+ """Test that we don't generate an entity id that is already registered."""
+ entry = registry.async_get_or_create(
+ 'light', 'hue', '1234', suggested_object_id='beer')
+ entry2 = registry.async_get_or_create(
+ 'light', 'hue', '5678', suggested_object_id='beer')
+
+ assert entry.entity_id == 'light.beer'
+ assert entry2.entity_id == 'light.beer_2'
+
+
+@asyncio.coroutine
+def test_get_or_create_suggested_object_id_conflict_existing(hass, registry):
+ """Test that we don't generate an entity id that currently exists."""
+ hass.states.async_set('light.hue_1234', 'on')
+ entry = registry.async_get_or_create('light', 'hue', '1234')
+ assert entry.entity_id == 'light.hue_1234_2'
+
+
+@asyncio.coroutine
+def test_create_triggers_save(hass, registry):
+ """Test that registering entry triggers a save."""
+ with patch.object(hass.loop, 'call_later') as mock_call_later:
+ registry.async_get_or_create('light', 'hue', '1234')
+
+ assert len(mock_call_later.mock_calls) == 1
+
+
+@asyncio.coroutine
+def test_save_timer_reset_on_subsequent_save(hass, registry):
+ """Test we reset the save timer on a new create."""
+ with patch.object(hass.loop, 'call_later') as mock_call_later:
+ registry.async_get_or_create('light', 'hue', '1234')
+
+ assert len(mock_call_later.mock_calls) == 1
+
+ with patch.object(hass.loop, 'call_later') as mock_call_later_2:
+ registry.async_get_or_create('light', 'hue', '5678')
+
+ assert len(mock_call_later().cancel.mock_calls) == 1
+ assert len(mock_call_later_2.mock_calls) == 1
+
+
+@asyncio.coroutine
+def test_loading_saving_data(hass, registry):
+ """Test that we load/save data correctly."""
+ yaml_path = 'homeassistant.util.yaml.open'
+ orig_entry1 = registry.async_get_or_create('light', 'hue', '1234')
+ orig_entry2 = registry.async_get_or_create('light', 'hue', '5678')
+
+ assert len(registry.entities) == 2
+
+ with patch(yaml_path, mock_open(), create=True) as mock_write:
+ yield from registry._async_save()
+
+ # 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 registry
+ registry2 = entity_registry.EntityRegistry(hass)
+
+ with patch('os.path.isfile', return_value=True), \
+ patch(yaml_path, mock_open(read_data=written), create=True):
+ yield from registry2._async_load()
+
+ # Ensure same order
+ assert list(registry.entities) == list(registry2.entities)
+ new_entry1 = registry.async_get_or_create('light', 'hue', '1234')
+ new_entry2 = registry.async_get_or_create('light', 'hue', '5678')
+
+ assert orig_entry1 == new_entry1
+ assert orig_entry2 == new_entry2
+
+
+@asyncio.coroutine
+def test_generate_entity_considers_registered_entities(registry):
+ """Test that we don't create entity id that are already registered."""
+ entry = registry.async_get_or_create('light', 'hue', '1234')
+ assert entry.entity_id == 'light.hue_1234'
+ assert registry.async_generate_entity_id('light', 'hue_1234') == \
+ 'light.hue_1234_2'
+
+
+@asyncio.coroutine
+def test_generate_entity_considers_existing_entities(hass, registry):
+ """Test that we don't create entity id that currently exists."""
+ hass.states.async_set('light.kitchen', 'on')
+ assert registry.async_generate_entity_id('light', 'kitchen') == \
+ 'light.kitchen_2'
+
+
+@asyncio.coroutine
+def test_is_registered(registry):
+ """Test that is_registered works."""
+ entry = registry.async_get_or_create('light', 'hue', '1234')
+ assert registry.async_is_registered(entry.entity_id)
+ assert not registry.async_is_registered('light.non_existing')
diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py
index 797cd257833..944224a34d1 100644
--- a/tests/helpers/test_entityfilter.py
+++ b/tests/helpers/test_entityfilter.py
@@ -1,4 +1,4 @@
-"""The tests for the EntityFitler component."""
+"""The tests for the EntityFilter component."""
from homeassistant.helpers.entityfilter import generate_filter
diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py
index 7d601c7a78d..73f2b9ff5a4 100644
--- a/tests/helpers/test_event.py
+++ b/tests/helpers/test_event.py
@@ -7,10 +7,12 @@ from datetime import datetime, timedelta
from astral import Astral
import pytest
+from homeassistant.core import callback
from homeassistant.setup import setup_component
import homeassistant.core as ha
from homeassistant.const import MATCH_ALL
from homeassistant.helpers.event import (
+ async_call_later,
track_point_in_utc_time,
track_point_in_time,
track_utc_time_change,
@@ -52,7 +54,7 @@ class TestEventHelpers(unittest.TestCase):
runs = []
track_point_in_utc_time(
- self.hass, lambda x: runs.append(1), birthday_paulus)
+ self.hass, callback(lambda x: runs.append(1)), birthday_paulus)
self._send_time_changed(before_birthday)
self.hass.block_till_done()
@@ -68,14 +70,14 @@ class TestEventHelpers(unittest.TestCase):
self.assertEqual(1, len(runs))
track_point_in_time(
- self.hass, lambda x: runs.append(1), birthday_paulus)
+ self.hass, callback(lambda x: runs.append(1)), birthday_paulus)
self._send_time_changed(after_birthday)
self.hass.block_till_done()
self.assertEqual(2, len(runs))
unsub = track_point_in_time(
- self.hass, lambda x: runs.append(1), birthday_paulus)
+ self.hass, callback(lambda x: runs.append(1)), birthday_paulus)
unsub()
self._send_time_changed(after_birthday)
@@ -642,3 +644,22 @@ class TestEventHelpers(unittest.TestCase):
self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0))
self.hass.block_till_done()
self.assertEqual(0, len(specific_runs))
+
+
+@asyncio.coroutine
+def test_async_call_later(hass):
+ """Test calling an action later."""
+ def action(): pass
+ now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC)
+
+ with patch('homeassistant.helpers.event'
+ '.async_track_point_in_utc_time') as mock, \
+ patch('homeassistant.util.dt.utcnow', return_value=now):
+ remove = async_call_later(hass, 3, action)
+
+ assert len(mock.mock_calls) == 1
+ p_hass, p_action, p_point = mock.mock_calls[0][1]
+ assert hass is hass
+ assert p_action is action
+ assert p_point == now + timedelta(seconds=3)
+ assert remove is mock()
diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py
index 31d98633ef8..a987f5130f1 100644
--- a/tests/helpers/test_service.py
+++ b/tests/helpers/test_service.py
@@ -28,7 +28,7 @@ class TestServiceHelpers(unittest.TestCase):
self.hass.stop()
def test_template_service_call(self):
- """Test service call with tempating."""
+ """Test service call with templating."""
config = {
'service_template': '{{ \'test_domain.test_service\' }}',
'entity_id': 'hello.world',
@@ -68,6 +68,24 @@ class TestServiceHelpers(unittest.TestCase):
self.assertEqual('goodbye', self.calls[0].data['hello'])
+ def test_bad_template(self):
+ """Test passing bad template."""
+ config = {
+ 'service_template': '{{ var_service }}',
+ 'entity_id': 'hello.world',
+ 'data_template': {
+ 'hello': '{{ states + unknown_var }}'
+ }
+ }
+
+ service.call_from_config(self.hass, config, variables={
+ 'var_service': 'test_domain.test_service',
+ 'var_data': 'goodbye',
+ })
+ self.hass.block_till_done()
+
+ self.assertEqual(len(self.calls), 0)
+
def test_split_entity_string(self):
"""Test splitting of entity string."""
service.call_from_config(self.hass, {
@@ -102,7 +120,7 @@ class TestServiceHelpers(unittest.TestCase):
@patch('homeassistant.helpers.service._LOGGER.error')
def test_fail_silently_if_no_service(self, mock_log):
- """Test failling if service is missing."""
+ """Test failing if service is missing."""
service.call_from_config(self.hass, None)
self.assertEqual(1, mock_log.call_count)
diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py
index 9047f26b2d1..c109ae30aad 100644
--- a/tests/test_bootstrap.py
+++ b/tests/test_bootstrap.py
@@ -17,7 +17,7 @@ VERSION_PATH = os.path.join(get_test_config_dir(), config_util.VERSION_FILE)
_LOGGER = logging.getLogger(__name__)
-# prevent .HA_VERISON file from being written
+# prevent .HA_VERSION file from being written
@patch(
'homeassistant.bootstrap.conf_util.process_ha_config_upgrade', Mock())
@patch('homeassistant.util.location.detect_location_info',
diff --git a/tests/test_config.py b/tests/test_config.py
index 377c650e91f..541eaf4f79e 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -255,18 +255,6 @@ class TestConfig(unittest.TestCase):
return self.hass.states.get('test.test')
- def test_entity_customization_false(self):
- """Test entity customization through configuration."""
- config = {CONF_LATITUDE: 50,
- CONF_LONGITUDE: 50,
- CONF_NAME: 'Test',
- CONF_CUSTOMIZE: {
- 'test.test': {'hidden': False}}}
-
- state = self._compute_state(config)
-
- assert 'hidden' not in state.attributes
-
def test_entity_customization(self):
"""Test entity customization through configuration."""
config = {CONF_LATITUDE: 50,
diff --git a/tests/test_core.py b/tests/test_core.py
index ea952a7c073..77a7872526f 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -135,7 +135,7 @@ class TestHomeAssistant(unittest.TestCase):
"""Test Coro."""
call_count.append('call')
- for i in range(3):
+ for _ in range(3):
self.hass.add_job(test_coro())
run_coroutine_threadsafe(
@@ -155,7 +155,7 @@ class TestHomeAssistant(unittest.TestCase):
"""Test Coro."""
call_count.append('call')
- for i in range(2):
+ for _ in range(2):
self.hass.add_job(test_coro())
@asyncio.coroutine
@@ -172,7 +172,7 @@ class TestHomeAssistant(unittest.TestCase):
assert len(call_count) == 2
def test_async_add_job_pending_tasks_executor(self):
- """Run a executor in pending tasks."""
+ """Run an executor in pending tasks."""
call_count = []
def test_executor():
@@ -185,7 +185,7 @@ class TestHomeAssistant(unittest.TestCase):
yield from asyncio.sleep(0, loop=self.hass.loop)
yield from asyncio.sleep(0, loop=self.hass.loop)
- for i in range(2):
+ for _ in range(2):
self.hass.add_job(test_executor)
run_coroutine_threadsafe(
@@ -210,7 +210,7 @@ class TestHomeAssistant(unittest.TestCase):
yield from asyncio.sleep(0, loop=self.hass.loop)
yield from asyncio.sleep(0, loop=self.hass.loop)
- for i in range(2):
+ for _ in range(2):
self.hass.add_job(test_callback)
run_coroutine_threadsafe(
diff --git a/tests/test_remote.py b/tests/test_remote.py
index 41011794914..9aa730d6eb6 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -28,7 +28,7 @@ def _url(path=''):
# pylint: disable=invalid-name
def setUpModule():
- """Initalization of a Home Assistant server instance."""
+ """Initialization of a Home Assistant server instance."""
global hass, master_api
hass = get_test_home_assistant()
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
new file mode 100644
index 00000000000..946e64af847
--- /dev/null
+++ b/tests/test_requirements.py
@@ -0,0 +1,61 @@
+"""Test requirements module."""
+import os
+from unittest import mock
+
+from homeassistant import loader, setup
+from homeassistant.requirements import CONSTRAINT_FILE
+
+from tests.common import get_test_home_assistant, MockModule
+
+
+class TestRequirements:
+ """Test the requirements module."""
+
+ hass = None
+ backup_cache = None
+
+ # pylint: disable=invalid-name, no-self-use
+ def setup_method(self, method):
+ """Setup the test."""
+ self.hass = get_test_home_assistant()
+
+ def teardown_method(self, method):
+ """Clean up."""
+ self.hass.stop()
+
+ @mock.patch('os.path.dirname')
+ @mock.patch('homeassistant.util.package.running_under_virtualenv',
+ return_value=True)
+ @mock.patch('homeassistant.util.package.install_package',
+ return_value=True)
+ def test_requirement_installed_in_venv(
+ self, mock_install, mock_venv, mock_dirname):
+ """Test requirement installed in virtual environment."""
+ mock_venv.return_value = True
+ mock_dirname.return_value = 'ha_package_path'
+ self.hass.config.skip_pip = False
+ loader.set_component(
+ 'comp', MockModule('comp', requirements=['package==0.0.1']))
+ assert setup.setup_component(self.hass, 'comp')
+ assert 'comp' in self.hass.config.components
+ assert mock_install.call_args == mock.call(
+ 'package==0.0.1',
+ constraints=os.path.join('ha_package_path', CONSTRAINT_FILE))
+
+ @mock.patch('os.path.dirname')
+ @mock.patch('homeassistant.util.package.running_under_virtualenv',
+ return_value=False)
+ @mock.patch('homeassistant.util.package.install_package',
+ return_value=True)
+ def test_requirement_installed_in_deps(
+ self, mock_install, mock_venv, mock_dirname):
+ """Test requirement installed in deps directory."""
+ mock_dirname.return_value = 'ha_package_path'
+ self.hass.config.skip_pip = False
+ loader.set_component(
+ 'comp', MockModule('comp', requirements=['package==0.0.1']))
+ assert setup.setup_component(self.hass, 'comp')
+ assert 'comp' in self.hass.config.components
+ assert mock_install.call_args == mock.call(
+ 'package==0.0.1', target=self.hass.config.path('deps'),
+ constraints=os.path.join('ha_package_path', CONSTRAINT_FILE))
diff --git a/tests/test_setup.py b/tests/test_setup.py
index afea30ddcd1..6a94310793c 100644
--- a/tests/test_setup.py
+++ b/tests/test_setup.py
@@ -9,7 +9,7 @@ import logging
import voluptuous as vol
from homeassistant.core import callback
-from homeassistant.const import EVENT_HOMEASSISTANT_START, CONSTRAINT_FILE
+from homeassistant.const import EVENT_HOMEASSISTANT_START
import homeassistant.config as config_util
from homeassistant import setup, loader
import homeassistant.util.dt as dt_util
@@ -41,9 +41,6 @@ class TestSetup:
"""Clean up."""
self.hass.stop()
- # if os.path.isfile(VERSION_PATH):
- # os.remove(VERSION_PATH)
-
def test_validate_component_config(self):
"""Test validating component configuration."""
config_schema = vol.Schema({
@@ -203,43 +200,6 @@ class TestSetup:
assert not setup.setup_component(self.hass, 'comp')
assert 'comp' not in self.hass.config.components
- @mock.patch('homeassistant.setup.os.path.dirname')
- @mock.patch('homeassistant.util.package.running_under_virtualenv',
- return_value=True)
- @mock.patch('homeassistant.util.package.install_package',
- return_value=True)
- def test_requirement_installed_in_venv(
- self, mock_install, mock_venv, mock_dirname):
- """Test requirement installed in virtual environment."""
- mock_venv.return_value = True
- mock_dirname.return_value = 'ha_package_path'
- self.hass.config.skip_pip = False
- loader.set_component(
- 'comp', MockModule('comp', requirements=['package==0.0.1']))
- assert setup.setup_component(self.hass, 'comp')
- assert 'comp' in self.hass.config.components
- assert mock_install.call_args == mock.call(
- 'package==0.0.1',
- constraints=os.path.join('ha_package_path', CONSTRAINT_FILE))
-
- @mock.patch('homeassistant.setup.os.path.dirname')
- @mock.patch('homeassistant.util.package.running_under_virtualenv',
- return_value=False)
- @mock.patch('homeassistant.util.package.install_package',
- return_value=True)
- def test_requirement_installed_in_deps(
- self, mock_install, mock_venv, mock_dirname):
- """Test requirement installed in deps directory."""
- mock_dirname.return_value = 'ha_package_path'
- self.hass.config.skip_pip = False
- loader.set_component(
- 'comp', MockModule('comp', requirements=['package==0.0.1']))
- assert setup.setup_component(self.hass, 'comp')
- assert 'comp' in self.hass.config.components
- assert mock_install.call_args == mock.call(
- 'package==0.0.1', target=self.hass.config.path('deps'),
- constraints=os.path.join('ha_package_path', CONSTRAINT_FILE))
-
def test_component_not_setup_twice_if_loaded_during_other_setup(self):
"""Test component setup while waiting for lock is not setup twice."""
result = []
diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py
index d11a71d541f..d7033775a14 100644
--- a/tests/test_util/aiohttp.py
+++ b/tests/test_util/aiohttp.py
@@ -97,7 +97,7 @@ class AiohttpClientMockResponse:
"""Mock Aiohttp client response."""
def __init__(self, method, url, status, response, cookies=None, exc=None,
- headers={}):
+ headers=None):
"""Initialize a fake response."""
self.method = method
self._url = url
@@ -107,7 +107,7 @@ class AiohttpClientMockResponse:
self.response = response
self.exc = exc
- self._headers = headers
+ self._headers = headers or {}
self._cookies = {}
if cookies:
diff --git a/tests/util/test_async.py b/tests/util/test_async.py
index b7a18d00fae..b6ae58a484f 100644
--- a/tests/util/test_async.py
+++ b/tests/util/test_async.py
@@ -1,7 +1,7 @@
"""Tests for async util methods from Python source."""
import asyncio
-from asyncio import test_utils
from unittest.mock import MagicMock, patch
+from unittest import TestCase
import pytest
@@ -104,14 +104,32 @@ def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _):
assert len(loop.call_soon_threadsafe.mock_calls) == 2
-class RunThreadsafeTests(test_utils.TestCase):
- """Test case for asyncio.run_coroutine_threadsafe."""
+class RunThreadsafeTests(TestCase):
+ """Test case for hasync.run_coroutine_threadsafe."""
def setUp(self):
"""Test setup method."""
- super().setUp()
self.loop = asyncio.new_event_loop()
- self.set_event_loop(self.loop) # Will cleanup properly
+
+ def tearDown(self):
+ """Test teardown method."""
+ executor = self.loop._default_executor
+ if executor is not None:
+ executor.shutdown(wait=True)
+ self.loop.close()
+
+ @staticmethod
+ def run_briefly(loop):
+ """Momentarily run a coroutine on the given loop."""
+ @asyncio.coroutine
+ def once():
+ pass
+ gen = once()
+ t = loop.create_task(gen)
+ try:
+ loop.run_until_complete(t)
+ finally:
+ gen.close()
def add_callback(self, a, b, fail, invalid):
"""Return a + b."""
@@ -185,7 +203,7 @@ class RunThreadsafeTests(test_utils.TestCase):
future = self.loop.run_in_executor(None, callback)
with self.assertRaises(asyncio.TimeoutError):
self.loop.run_until_complete(future)
- test_utils.run_briefly(self.loop)
+ self.run_briefly(self.loop)
# Check that there's no pending task (add has been cancelled)
for task in asyncio.Task.all_tasks(self.loop):
self.assertTrue(task.done())
diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py
index 7f04f6f0569..2ad3b42fdb8 100644
--- a/tests/util/test_distance.py
+++ b/tests/util/test_distance.py
@@ -1,4 +1,4 @@
-"""Test homeasssitant distance utility functions."""
+"""Test homeassistant distance utility functions."""
import unittest
import homeassistant.util.distance as distance_util
diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py
index 38b957ad102..734f4b548b9 100644
--- a/tests/util/test_yaml.py
+++ b/tests/util/test_yaml.py
@@ -48,7 +48,7 @@ class TestYaml(unittest.TestCase):
load_yaml_config_file(YAML_CONFIG_FILE)
def test_no_key(self):
- """Test item without an key."""
+ """Test item without a key."""
files = {YAML_CONFIG_FILE: 'a: a\nnokeyhere'}
with self.assertRaises(HomeAssistantError), \
patch_yaml_files(files):