diff --git a/.coveragerc b/.coveragerc
index e97d197ca94..4751ddce219 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -88,7 +88,7 @@ omit =
homeassistant/components/hive.py
homeassistant/components/*/hive.py
- homeassistant/components/homematic.py
+ homeassistant/components/homematic/__init__.py
homeassistant/components/*/homematic.py
homeassistant/components/insteon_local.py
@@ -264,6 +264,7 @@ omit =
homeassistant/components/*/zoneminder.py
homeassistant/components/alarm_control_panel/alarmdotcom.py
+ homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py
homeassistant/components/alarm_control_panel/egardia.py
homeassistant/components/alarm_control_panel/ialarm.py
@@ -283,8 +284,10 @@ omit =
homeassistant/components/binary_sensor/rest.py
homeassistant/components/binary_sensor/tapsaff.py
homeassistant/components/browser.py
+ homeassistant/components/calendar/caldav.py
homeassistant/components/calendar/todoist.py
homeassistant/components/camera/bloomsky.py
+ homeassistant/components/camera/canary.py
homeassistant/components/camera/ffmpeg.py
homeassistant/components/camera/foscam.py
homeassistant/components/camera/mjpeg.py
@@ -362,6 +365,7 @@ omit =
homeassistant/components/light/decora.py
homeassistant/components/light/decora_wifi.py
homeassistant/components/light/flux_led.py
+ homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py
homeassistant/components/light/lifx.py
@@ -425,6 +429,7 @@ omit =
homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/spotify.py
homeassistant/components/media_player/squeezebox.py
+ homeassistant/components/media_player/ue_smart_radio.py
homeassistant/components/media_player/vizio.py
homeassistant/components/media_player/vlc.py
homeassistant/components/media_player/volumio.py
@@ -500,6 +505,7 @@ omit =
homeassistant/components/sensor/deluge.py
homeassistant/components/sensor/deutsche_bahn.py
homeassistant/components/sensor/dht.py
+ homeassistant/components/sensor/discogs.py
homeassistant/components/sensor/dnsip.py
homeassistant/components/sensor/dovado.py
homeassistant/components/sensor/dte_energy_bridge.py
@@ -528,7 +534,6 @@ omit =
homeassistant/components/sensor/haveibeenpwned.py
homeassistant/components/sensor/hp_ilo.py
homeassistant/components/sensor/htu21d.py
- homeassistant/components/sensor/hydroquebec.py
homeassistant/components/sensor/imap.py
homeassistant/components/sensor/imap_email_content.py
homeassistant/components/sensor/influxdb.py
diff --git a/CODEOWNERS b/CODEOWNERS
index ac0f794482a..37a2494c182 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -53,10 +53,11 @@ homeassistant/components/light/yeelight.py @rytilahti
homeassistant/components/media_player/kodi.py @armills
homeassistant/components/media_player/monoprice.py @etsinko
homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth
+homeassistant/components/plant.py @ChristianKuehnel
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
+homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/sytadin.py @gautric
homeassistant/components/sensor/tibber.py @danielhiversen
homeassistant/components/sensor/waqi.py @andrey-git
diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py
index a8852b910c2..b7301e13bea 100644
--- a/homeassistant/__main__.py
+++ b/homeassistant/__main__.py
@@ -230,7 +230,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance."""
- if sys.argv[0].endswith(os.path.sep + '__main__.py'):
+ if os.path.basename(sys.argv[0]) == '__main__.py':
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if
diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py
index 3b58eb0b71d..d5fbbec5998 100644
--- a/homeassistant/components/alarm_control_panel/alarmdecoder.py
+++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py
@@ -7,30 +7,21 @@ https://home-assistant.io/components/alarm_control_panel.alarmdecoder/
import asyncio
import logging
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.components.alarm_control_panel as alarm
-
-from homeassistant.components.alarmdecoder import (DATA_AD,
- SIGNAL_PANEL_MESSAGE)
-
+from homeassistant.components.alarmdecoder import (
+ DATA_AD, SIGNAL_PANEL_MESSAGE)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
- STATE_UNKNOWN, STATE_ALARM_TRIGGERED)
+ STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder']
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up for AlarmDecoder alarm panels."""
- _LOGGER.debug("AlarmDecoderAlarmPanel: setup")
-
- device = AlarmDecoderAlarmPanel("Alarm Panel", hass)
-
- async_add_devices([device])
+ add_devices([AlarmDecoderAlarmPanel()])
return True
@@ -38,38 +29,35 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Representation of an AlarmDecoder-based alarm panel."""
- def __init__(self, name, hass):
+ def __init__(self):
"""Initialize the alarm panel."""
self._display = ""
- self._name = name
- self._state = STATE_UNKNOWN
-
- _LOGGER.debug("Setting up panel")
+ self._name = "Alarm Panel"
+ self._state = None
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
- async_dispatcher_connect(
- self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ SIGNAL_PANEL_MESSAGE, self._message_callback)
- @callback
def _message_callback(self, message):
if message.alarm_sounding or message.fire_alarm:
if self._state != STATE_ALARM_TRIGGERED:
self._state = STATE_ALARM_TRIGGERED
- self.async_schedule_update_ha_state()
+ self.schedule_update_ha_state()
elif message.armed_away:
if self._state != STATE_ALARM_ARMED_AWAY:
self._state = STATE_ALARM_ARMED_AWAY
- self.async_schedule_update_ha_state()
+ self.schedule_update_ha_state()
elif message.armed_home:
if self._state != STATE_ALARM_ARMED_HOME:
self._state = STATE_ALARM_ARMED_HOME
- self.async_schedule_update_ha_state()
+ self.schedule_update_ha_state()
else:
if self._state != STATE_ALARM_DISARMED:
self._state = STATE_ALARM_DISARMED
- self.async_schedule_update_ha_state()
+ self.schedule_update_ha_state()
@property
def name(self):
@@ -91,26 +79,20 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
"""Return the state of the device."""
return self._state
- @asyncio.coroutine
- def async_alarm_disarm(self, code=None):
+ def alarm_disarm(self, code=None):
"""Send disarm command."""
- _LOGGER.debug("alarm_disarm: %s", code)
if code:
_LOGGER.debug("alarm_disarm: sending %s1", str(code))
self.hass.data[DATA_AD].send("{!s}1".format(code))
- @asyncio.coroutine
- def async_alarm_arm_away(self, code=None):
+ def alarm_arm_away(self, code=None):
"""Send arm away command."""
- _LOGGER.debug("alarm_arm_away: %s", code)
if code:
_LOGGER.debug("alarm_arm_away: sending %s2", str(code))
self.hass.data[DATA_AD].send("{!s}2".format(code))
- @asyncio.coroutine
- def async_alarm_arm_home(self, code=None):
+ def alarm_arm_home(self, code=None):
"""Send arm home command."""
- _LOGGER.debug("alarm_arm_home: %s", code)
if code:
_LOGGER.debug("alarm_arm_home: sending %s3", str(code))
self.hass.data[DATA_AD].send("{!s}3".format(code))
diff --git a/homeassistant/components/alarm_control_panel/canary.py b/homeassistant/components/alarm_control_panel/canary.py
new file mode 100644
index 00000000000..fb5c4c37e8d
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/canary.py
@@ -0,0 +1,92 @@
+"""
+Support for Canary alarm.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/alarm_control_panel.canary/
+"""
+import logging
+
+from homeassistant.components.alarm_control_panel import AlarmControlPanel
+from homeassistant.components.canary import DATA_CANARY
+from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
+ STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
+
+DEPENDENCIES = ['canary']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Canary alarms."""
+ data = hass.data[DATA_CANARY]
+ devices = []
+
+ for location in data.locations:
+ devices.append(CanaryAlarm(data, location.location_id))
+
+ add_devices(devices, True)
+
+
+class CanaryAlarm(AlarmControlPanel):
+ """Representation of a Canary alarm control panel."""
+
+ def __init__(self, data, location_id):
+ """Initialize a Canary security camera."""
+ self._data = data
+ self._location_id = location_id
+
+ @property
+ def name(self):
+ """Return the name of the alarm."""
+ location = self._data.get_location(self._location_id)
+ return location.name
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
+ LOCATION_MODE_NIGHT
+
+ location = self._data.get_location(self._location_id)
+
+ if location.is_private:
+ return STATE_ALARM_DISARMED
+
+ mode = location.mode
+ if mode.name == LOCATION_MODE_AWAY:
+ return STATE_ALARM_ARMED_AWAY
+ elif mode.name == LOCATION_MODE_HOME:
+ return STATE_ALARM_ARMED_HOME
+ elif mode.name == LOCATION_MODE_NIGHT:
+ return STATE_ALARM_ARMED_NIGHT
+ else:
+ return None
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ location = self._data.get_location(self._location_id)
+ return {
+ 'private': location.is_private
+ }
+
+ def alarm_disarm(self, code=None):
+ """Send disarm command."""
+ location = self._data.get_location(self._location_id)
+ self._data.set_location_mode(self._location_id, location.mode.name,
+ True)
+
+ def alarm_arm_home(self, code=None):
+ """Send arm home command."""
+ from canary.api import LOCATION_MODE_HOME
+ self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
+
+ def alarm_arm_away(self, code=None):
+ """Send arm away command."""
+ from canary.api import LOCATION_MODE_AWAY
+ self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
+
+ def alarm_arm_night(self, code=None):
+ """Send arm night command."""
+ from canary.api import LOCATION_MODE_NIGHT
+ self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py
index 7719ab884bc..82c26c98104 100644
--- a/homeassistant/components/alarm_control_panel/egardia.py
+++ b/homeassistant/components/alarm_control_panel/egardia.py
@@ -116,12 +116,20 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
"""Return the state of the device."""
return self._status
+ @property
+ def should_poll(self):
+ """Poll if no report server is enabled."""
+ if not self._rs_enabled:
+ return True
+ return False
+
def handle_system_status_event(self, event):
"""Handle egardia_system_status_event."""
if event.data.get('status') is not None:
statuscode = event.data.get('status')
status = self.lookupstatusfromcode(statuscode)
self.parsestatus(status)
+ self.schedule_update_ha_state()
def listen_to_system_status(self):
"""Subscribe to egardia_system_status event."""
@@ -161,9 +169,8 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
def update(self):
"""Update the alarm status."""
- if not self._rs_enabled:
- status = self._egardiasystem.getstate()
- self.parsestatus(status)
+ status = self._egardiasystem.getstate()
+ self.parsestatus(status)
def alarm_disarm(self, code=None):
"""Send disarm command."""
diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py
index 6f22d6a358c..5c1323989d4 100644
--- a/homeassistant/components/alarm_control_panel/totalconnect.py
+++ b/homeassistant/components/alarm_control_panel/totalconnect.py
@@ -14,7 +14,9 @@ from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
- STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
+ STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
+ STATE_ALARM_ARMED_CUSTOM_BYPASS)
+
REQUIREMENTS = ['total_connect_client==0.16']
@@ -76,6 +78,8 @@ class TotalConnect(alarm.AlarmControlPanel):
state = STATE_ALARM_ARMED_AWAY
elif status == self._client.ARMED_STAY_NIGHT:
state = STATE_ALARM_ARMED_NIGHT
+ elif status == self._client.ARMED_CUSTOM_BYPASS:
+ state = STATE_ALARM_ARMED_CUSTOM_BYPASS
elif status == self._client.ARMING:
state = STATE_ALARM_ARMING
elif status == self._client.DISARMING:
diff --git a/homeassistant/components/alarmdecoder.py b/homeassistant/components/alarmdecoder.py
index 011cc3ad21d..6e30a83d96a 100644
--- a/homeassistant/components/alarmdecoder.py
+++ b/homeassistant/components/alarmdecoder.py
@@ -4,16 +4,13 @@ Support for AlarmDecoder devices.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/alarmdecoder/
"""
-import asyncio
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
-from homeassistant.core import callback
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
-from homeassistant.helpers.discovery import async_load_platform
-from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.discovery import load_platform
REQUIREMENTS = ['alarmdecoder==0.12.3']
@@ -71,9 +68,9 @@ ZONE_SCHEMA = vol.Schema({
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
- vol.Required(CONF_DEVICE): vol.Any(DEVICE_SOCKET_SCHEMA,
- DEVICE_SERIAL_SCHEMA,
- DEVICE_USB_SCHEMA),
+ vol.Required(CONF_DEVICE): vol.Any(
+ DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
+ DEVICE_USB_SCHEMA),
vol.Optional(CONF_PANEL_DISPLAY,
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
@@ -81,8 +78,7 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
-@asyncio.coroutine
-def async_setup(hass, config):
+def setup(hass, config):
"""Set up for the AlarmDecoder devices."""
from alarmdecoder import AlarmDecoder
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
@@ -99,32 +95,25 @@ def async_setup(hass, config):
path = DEFAULT_DEVICE_PATH
baud = DEFAULT_DEVICE_BAUD
- sync_connect = asyncio.Future(loop=hass.loop)
-
- def handle_open(device):
- """Handle the successful connection."""
- _LOGGER.info("Established a connection with the alarmdecoder")
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
- sync_connect.set_result(True)
-
- @callback
def stop_alarmdecoder(event):
"""Handle the shutdown of AlarmDecoder."""
_LOGGER.debug("Shutting down alarmdecoder")
controller.close()
- @callback
def handle_message(sender, message):
"""Handle message from AlarmDecoder."""
- async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message)
+ hass.helpers.dispatcher.dispatcher_send(
+ SIGNAL_PANEL_MESSAGE, message)
def zone_fault_callback(sender, zone):
"""Handle zone fault from AlarmDecoder."""
- async_dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone)
+ hass.helpers.dispatcher.dispatcher_send(
+ SIGNAL_ZONE_FAULT, zone)
def zone_restore_callback(sender, zone):
"""Handle zone restore from AlarmDecoder."""
- async_dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone)
+ hass.helpers.dispatcher.dispatcher_send(
+ SIGNAL_ZONE_RESTORE, zone)
controller = False
if device_type == 'socket':
@@ -139,7 +128,6 @@ def async_setup(hass, config):
AlarmDecoder(USBDevice.find())
return False
- controller.on_open += handle_open
controller.on_message += handle_message
controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback
@@ -148,21 +136,16 @@ def async_setup(hass, config):
controller.open(baud)
- result = yield from sync_connect
+ _LOGGER.debug("Established a connection with the alarmdecoder")
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
- if not result:
- return False
-
- hass.async_add_job(
- async_load_platform(hass, 'alarm_control_panel', DOMAIN, conf,
- config))
+ load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
if zones:
- hass.async_add_job(async_load_platform(
- hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config))
+ load_platform(
+ hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
if display:
- hass.async_add_job(async_load_platform(
- hass, 'sensor', DOMAIN, conf, config))
+ load_platform(hass, 'sensor', DOMAIN, conf, config)
return True
diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py
index c8eb1841c0d..bb6bfa0e9db 100644
--- a/homeassistant/components/apple_tv.py
+++ b/homeassistant/components/apple_tv.py
@@ -18,7 +18,7 @@ from homeassistant.helpers import discovery
from homeassistant.components.discovery import SERVICE_APPLE_TV
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyatv==0.3.8']
+REQUIREMENTS = ['pyatv==0.3.9']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py
index 9e48a30d04a..a0c141914ed 100644
--- a/homeassistant/components/binary_sensor/__init__.py
+++ b/homeassistant/components/binary_sensor/__init__.py
@@ -34,6 +34,7 @@ DEVICE_CLASSES = [
'plug', # On means plugged in, Off means unplugged
'power', # Power, over-current, etc
'presence', # On means home, Off means away
+ 'problem', # On means there is a problem, Off means the status is OK
'safety', # Generic on=unsafe, off=safe
'smoke', # Smoke detector
'sound', # On means sound detected, Off means no sound
diff --git a/homeassistant/components/binary_sensor/alarmdecoder.py b/homeassistant/components/binary_sensor/alarmdecoder.py
index bc05e4d84d8..f42d0de4bb0 100644
--- a/homeassistant/components/binary_sensor/alarmdecoder.py
+++ b/homeassistant/components/binary_sensor/alarmdecoder.py
@@ -7,39 +7,29 @@ https://home-assistant.io/components/binary_sensor.alarmdecoder/
import asyncio
import logging
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.binary_sensor import BinarySensorDevice
-
-from homeassistant.components.alarmdecoder import (ZONE_SCHEMA,
- CONF_ZONES,
- CONF_ZONE_NAME,
- CONF_ZONE_TYPE,
- SIGNAL_ZONE_FAULT,
- SIGNAL_ZONE_RESTORE)
-
+from homeassistant.components.alarmdecoder import (
+ ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
+ SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE)
DEPENDENCIES = ['alarmdecoder']
_LOGGER = logging.getLogger(__name__)
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the AlarmDecoder binary sensor devices."""
configured_zones = discovery_info[CONF_ZONES]
devices = []
-
for zone_num in configured_zones:
device_config_data = ZONE_SCHEMA(configured_zones[zone_num])
zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME]
- device = AlarmDecoderBinarySensor(
- hass, zone_num, zone_name, zone_type)
+ device = AlarmDecoderBinarySensor(zone_num, zone_name, zone_type)
devices.append(device)
- async_add_devices(devices)
+ add_devices(devices)
return True
@@ -47,7 +37,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor."""
- def __init__(self, hass, zone_number, zone_name, zone_type):
+ def __init__(self, zone_number, zone_name, zone_type):
"""Initialize the binary_sensor."""
self._zone_number = zone_number
self._zone_type = zone_type
@@ -55,16 +45,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self._name = zone_name
self._type = zone_type
- _LOGGER.debug("Setup up zone: %s", self._name)
-
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
- async_dispatcher_connect(
- self.hass, SIGNAL_ZONE_FAULT, self._fault_callback)
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ SIGNAL_ZONE_FAULT, self._fault_callback)
- async_dispatcher_connect(
- self.hass, SIGNAL_ZONE_RESTORE, self._restore_callback)
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ SIGNAL_ZONE_RESTORE, self._restore_callback)
@property
def name(self):
@@ -97,16 +85,14 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Return the class of this sensor, from DEVICE_CLASSES."""
return self._zone_type
- @callback
def _fault_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self._state = 1
- self.async_schedule_update_ha_state()
+ self.schedule_update_ha_state()
- @callback
def _restore_callback(self, zone):
"""Update the zone's state, if needed."""
if zone is None or int(zone) == self._zone_number:
self._state = 0
- self.async_schedule_update_ha_state()
+ self.schedule_update_ha_state()
diff --git a/homeassistant/components/binary_sensor/concord232.py b/homeassistant/components/binary_sensor/concord232.py
index 7ba88f76611..d689f030d8a 100755
--- a/homeassistant/components/binary_sensor/concord232.py
+++ b/homeassistant/components/binary_sensor/concord232.py
@@ -118,7 +118,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
def is_on(self):
"""Return true if the binary sensor is on."""
# True means "faulted" or "open" or "abnormal state"
- return bool(self._zone['state'] == 'Normal')
+ return bool(self._zone['state'] != 'Normal')
def update(self):
"""Get updated stats from API."""
diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py
index fd6269e3630..247ea0b231a 100644
--- a/homeassistant/components/binary_sensor/isy994.py
+++ b/homeassistant/components/binary_sensor/isy994.py
@@ -4,24 +4,31 @@ Support for ISY994 binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.isy994/
"""
+
+import asyncio
import logging
+from datetime import timedelta
from typing import Callable # noqa
+from homeassistant.core import callback
from homeassistant.components.binary_sensor import BinarySensorDevice, DOMAIN
import homeassistant.components.isy994 as isy
from homeassistant.const import STATE_ON, STATE_OFF
from homeassistant.helpers.typing import ConfigType
+from homeassistant.helpers.event import async_track_point_in_utc_time
+from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
-VALUE_TO_STATE = {
- False: STATE_OFF,
- True: STATE_ON,
-}
-
UOM = ['2', '78']
STATES = [STATE_OFF, STATE_ON, 'true', 'false']
+ISY_DEVICE_TYPES = {
+ 'moisture': ['16.8', '16.13', '16.14'],
+ 'opening': ['16.9', '16.6', '16.7', '16.2', '16.17', '16.20', '16.21'],
+ 'motion': ['16.1', '16.4', '16.5', '16.3']
+}
+
# pylint: disable=unused-argument
def setup_platform(hass, config: ConfigType,
@@ -32,10 +39,46 @@ def setup_platform(hass, config: ConfigType,
return False
devices = []
+ devices_by_nid = {}
+ child_nodes = []
for node in isy.filter_nodes(isy.SENSOR_NODES, units=UOM,
states=STATES):
- devices.append(ISYBinarySensorDevice(node))
+ if node.parent_node is None:
+ device = ISYBinarySensorDevice(node)
+ devices.append(device)
+ devices_by_nid[node.nid] = device
+ else:
+ # We'll process the child nodes last, to ensure all parent nodes
+ # have been processed
+ child_nodes.append(node)
+
+ for node in child_nodes:
+ try:
+ parent_device = devices_by_nid[node.parent_node.nid]
+ except KeyError:
+ _LOGGER.error("Node %s has a parent node %s, but no device "
+ "was created for the parent. Skipping.",
+ node.nid, node.parent_nid)
+ else:
+ device_type = _detect_device_type(node)
+ if device_type in ['moisture', 'opening']:
+ subnode_id = int(node.nid[-1])
+ # Leak and door/window sensors work the same way with negative
+ # nodes and heartbeat nodes
+ if subnode_id == 4:
+ # Subnode 4 is the heartbeat node, which we will represent
+ # as a separate binary_sensor
+ device = ISYBinarySensorHeartbeat(node, parent_device)
+ parent_device.add_heartbeat_device(device)
+ devices.append(device)
+ elif subnode_id == 2:
+ parent_device.add_negative_node(node)
+ else:
+ # We don't yet have any special logic for other sensor types,
+ # so add the nodes as individual devices
+ device = ISYBinarySensorDevice(node)
+ devices.append(device)
for program in isy.PROGRAMS.get(DOMAIN, []):
try:
@@ -48,23 +91,282 @@ def setup_platform(hass, config: ConfigType,
add_devices(devices)
+def _detect_device_type(node) -> str:
+ try:
+ device_type = node.type
+ except AttributeError:
+ # The type attribute didn't exist in the ISY's API response
+ return None
+
+ split_type = device_type.split('.')
+ for device_class, ids in ISY_DEVICE_TYPES.items():
+ if '{}.{}'.format(split_type[0], split_type[1]) in ids:
+ return device_class
+
+ return None
+
+
+def _is_val_unknown(val):
+ """Determine if a number value represents UNKNOWN from PyISY."""
+ return val == -1*float('inf')
+
+
class ISYBinarySensorDevice(isy.ISYDevice, BinarySensorDevice):
- """Representation of an ISY994 binary sensor device."""
+ """Representation of an ISY994 binary sensor device.
+
+ Often times, a single device is represented by multiple nodes in the ISY,
+ allowing for different nuances in how those devices report their on and
+ off events. This class turns those multiple nodes in to a single Hass
+ entity and handles both ways that ISY binary sensors can work.
+ """
def __init__(self, node) -> None:
"""Initialize the ISY994 binary sensor device."""
- isy.ISYDevice.__init__(self, node)
+ super().__init__(node)
+ self._negative_node = None
+ self._heartbeat_device = None
+ self._device_class_from_type = _detect_device_type(self._node)
+ # pylint: disable=protected-access
+ if _is_val_unknown(self._node.status._val):
+ self._computed_state = None
+ else:
+ self._computed_state = bool(self._node.status._val)
+
+ @asyncio.coroutine
+ def async_added_to_hass(self) -> None:
+ """Subscribe to the node and subnode event emitters."""
+ yield from super().async_added_to_hass()
+
+ self._node.controlEvents.subscribe(self._positive_node_control_handler)
+
+ if self._negative_node is not None:
+ self._negative_node.controlEvents.subscribe(
+ self._negative_node_control_handler)
+
+ def add_heartbeat_device(self, device) -> None:
+ """Register a heartbeat device for this sensor.
+
+ The heartbeat node beats on its own, but we can gain a little
+ reliability by considering any node activity for this sensor
+ to be a heartbeat as well.
+ """
+ self._heartbeat_device = device
+
+ def _heartbeat(self) -> None:
+ """Send a heartbeat to our heartbeat device, if we have one."""
+ if self._heartbeat_device is not None:
+ self._heartbeat_device.heartbeat()
+
+ def add_negative_node(self, child) -> None:
+ """Add a negative node to this binary sensor device.
+
+ The negative node is a node that can receive the 'off' events
+ for the sensor, depending on device configuration and type.
+ """
+ self._negative_node = child
+
+ # pylint: disable=protected-access
+ if not _is_val_unknown(self._negative_node.status._val):
+ # If the negative node has a value, it means the negative node is
+ # in use for this device. Therefore, we cannot determine the state
+ # of the sensor until we receive our first ON event.
+ self._computed_state = None
+
+ def _negative_node_control_handler(self, event: object) -> None:
+ """Handle an "On" control event from the "negative" node."""
+ if event == 'DON':
+ _LOGGER.debug("Sensor %s turning Off via the Negative node "
+ "sending a DON command", self.name)
+ self._computed_state = False
+ self.schedule_update_ha_state()
+ self._heartbeat()
+
+ def _positive_node_control_handler(self, event: object) -> None:
+ """Handle On and Off control event coming from the primary node.
+
+ Depending on device configuration, sometimes only On events
+ will come to this node, with the negative node representing Off
+ events
+ """
+ if event == 'DON':
+ _LOGGER.debug("Sensor %s turning On via the Primary node "
+ "sending a DON command", self.name)
+ self._computed_state = True
+ self.schedule_update_ha_state()
+ self._heartbeat()
+ if event == 'DOF':
+ _LOGGER.debug("Sensor %s turning Off via the Primary node "
+ "sending a DOF command", self.name)
+ self._computed_state = False
+ self.schedule_update_ha_state()
+ self._heartbeat()
+
+ # pylint: disable=unused-argument
+ def on_update(self, event: object) -> None:
+ """Ignore primary node status updates.
+
+ We listen directly to the Control events on all nodes for this
+ device.
+ """
+ pass
+
+ @property
+ def value(self) -> object:
+ """Get the current value of the device.
+
+ Insteon leak sensors set their primary node to On when the state is
+ DRY, not WET, so we invert the binary state if the user indicates
+ that it is a moisture sensor.
+ """
+ if self._computed_state is None:
+ # Do this first so we don't invert None on moisture sensors
+ return None
+
+ if self.device_class == 'moisture':
+ return not self._computed_state
+
+ return self._computed_state
+
+ @property
+ def is_on(self) -> bool:
+ """Get whether the ISY994 binary sensor device is on.
+
+ Note: This method will return false if the current state is UNKNOWN
+ """
+ return bool(self.value)
+
+ @property
+ def state(self):
+ """Return the state of the binary sensor."""
+ if self._computed_state is None:
+ return None
+ return STATE_ON if self.is_on else STATE_OFF
+
+ @property
+ def device_class(self) -> str:
+ """Return the class of this device.
+
+ This was discovered by parsing the device type code during init
+ """
+ return self._device_class_from_type
+
+
+class ISYBinarySensorHeartbeat(isy.ISYDevice, BinarySensorDevice):
+ """Representation of the battery state of an ISY994 sensor."""
+
+ def __init__(self, node, parent_device) -> None:
+ """Initialize the ISY994 binary sensor device."""
+ super().__init__(node)
+ self._computed_state = None
+ self._parent_device = parent_device
+ self._heartbeat_timer = None
+
+ @asyncio.coroutine
+ def async_added_to_hass(self) -> None:
+ """Subscribe to the node and subnode event emitters."""
+ yield from super().async_added_to_hass()
+
+ self._node.controlEvents.subscribe(
+ self._heartbeat_node_control_handler)
+
+ # Start the timer on bootup, so we can change from UNKNOWN to ON
+ self._restart_timer()
+
+ def _heartbeat_node_control_handler(self, event: object) -> None:
+ """Update the heartbeat timestamp when an On event is sent."""
+ if event == 'DON':
+ self.heartbeat()
+
+ def heartbeat(self):
+ """Mark the device as online, and restart the 25 hour timer.
+
+ This gets called when the heartbeat node beats, but also when the
+ parent sensor sends any events, as we can trust that to mean the device
+ is online. This mitigates the risk of false positives due to a single
+ missed heartbeat event.
+ """
+ self._computed_state = False
+ self._restart_timer()
+ self.schedule_update_ha_state()
+
+ def _restart_timer(self):
+ """Restart the 25 hour timer."""
+ try:
+ self._heartbeat_timer()
+ self._heartbeat_timer = None
+ except TypeError:
+ # No heartbeat timer is active
+ pass
+
+ # pylint: disable=unused-argument
+ @callback
+ def timer_elapsed(now) -> None:
+ """Heartbeat missed; set state to indicate dead battery."""
+ self._computed_state = True
+ self._heartbeat_timer = None
+ self.schedule_update_ha_state()
+
+ point_in_time = dt_util.utcnow() + timedelta(hours=25)
+ _LOGGER.debug("Timer starting. Now: %s Then: %s",
+ dt_util.utcnow(), point_in_time)
+
+ self._heartbeat_timer = async_track_point_in_utc_time(
+ self.hass, timer_elapsed, point_in_time)
+
+ # pylint: disable=unused-argument
+ def on_update(self, event: object) -> None:
+ """Ignore node status updates.
+
+ We listen directly to the Control events for this device.
+ """
+ pass
+
+ @property
+ def value(self) -> object:
+ """Get the current value of this sensor."""
+ return self._computed_state
+
+ @property
+ def is_on(self) -> bool:
+ """Get whether the ISY994 binary sensor device is on.
+
+ Note: This method will return false if the current state is UNKNOWN
+ """
+ return bool(self.value)
+
+ @property
+ def state(self):
+ """Return the state of the binary sensor."""
+ if self._computed_state is None:
+ return None
+ return STATE_ON if self.is_on else STATE_OFF
+
+ @property
+ def device_class(self) -> str:
+ """Get the class of this device."""
+ return 'battery'
+
+ @property
+ def device_state_attributes(self):
+ """Get the state attributes for the device."""
+ attr = super().device_state_attributes
+ attr['parent_entity_id'] = self._parent_device.entity_id
+ return attr
+
+
+class ISYBinarySensorProgram(isy.ISYDevice, BinarySensorDevice):
+ """Representation of an ISY994 binary sensor program.
+
+ This does not need all of the subnode logic in the device version of binary
+ sensors.
+ """
+
+ def __init__(self, name, node) -> None:
+ """Initialize the ISY994 binary sensor program."""
+ super().__init__(node)
+ self._name = name
@property
def is_on(self) -> bool:
"""Get whether the ISY994 binary sensor device is on."""
return bool(self.value)
-
-
-class ISYBinarySensorProgram(ISYBinarySensorDevice):
- """Representation of an ISY994 binary sensor program."""
-
- def __init__(self, name, node) -> None:
- """Initialize the ISY994 binary sensor program."""
- ISYBinarySensorDevice.__init__(self, node)
- self._name = name
diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py
index 5ca037767f2..36e8868661d 100644
--- a/homeassistant/components/binary_sensor/threshold.py
+++ b/homeassistant/components/binary_sensor/threshold.py
@@ -9,40 +9,48 @@ import logging
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
from homeassistant.components.binary_sensor import (
- BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
+ DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
from homeassistant.const import (
- CONF_NAME, CONF_ENTITY_ID, CONF_TYPE, STATE_UNKNOWN,
- ATTR_ENTITY_ID, CONF_DEVICE_CLASS)
+ ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
+ STATE_UNKNOWN)
from homeassistant.core import callback
+import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__)
ATTR_HYSTERESIS = 'hysteresis'
+ATTR_LOWER = 'lower'
+ATTR_POSITION = 'position'
ATTR_SENSOR_VALUE = 'sensor_value'
-ATTR_THRESHOLD = 'threshold'
ATTR_TYPE = 'type'
+ATTR_UPPER = 'upper'
CONF_HYSTERESIS = 'hysteresis'
CONF_LOWER = 'lower'
-CONF_THRESHOLD = 'threshold'
CONF_UPPER = 'upper'
DEFAULT_NAME = 'Threshold'
DEFAULT_HYSTERESIS = 0.0
-SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
+POSITION_ABOVE = 'above'
+POSITION_BELOW = 'below'
+POSITION_IN_RANGE = 'in_range'
+POSITION_UNKNOWN = 'unknown'
+
+TYPE_LOWER = 'lower'
+TYPE_RANGE = 'range'
+TYPE_UPPER = 'upper'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id,
- vol.Required(CONF_THRESHOLD): vol.Coerce(float),
- vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
- vol.Optional(
- CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
- vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
+ vol.Optional(CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS):
+ vol.Coerce(float),
+ vol.Optional(CONF_LOWER): vol.Coerce(float),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_UPPER): vol.Coerce(float),
})
@@ -51,47 +59,44 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Threshold sensor."""
entity_id = config.get(CONF_ENTITY_ID)
name = config.get(CONF_NAME)
- threshold = config.get(CONF_THRESHOLD)
+ lower = config.get(CONF_LOWER)
+ upper = config.get(CONF_UPPER)
hysteresis = config.get(CONF_HYSTERESIS)
- limit_type = config.get(CONF_TYPE)
device_class = config.get(CONF_DEVICE_CLASS)
async_add_devices([ThresholdSensor(
- hass, entity_id, name, threshold,
- hysteresis, limit_type, device_class)
- ], True)
-
- return True
+ hass, entity_id, name, lower, upper, hysteresis, device_class)], True)
class ThresholdSensor(BinarySensorDevice):
"""Representation of a Threshold sensor."""
- def __init__(self, hass, entity_id, name, threshold,
- hysteresis, limit_type, device_class):
+ def __init__(self, hass, entity_id, name, lower, upper, hysteresis,
+ device_class):
"""Initialize the Threshold sensor."""
self._hass = hass
self._entity_id = entity_id
- self.is_upper = limit_type == 'upper'
self._name = name
- self._threshold = threshold
+ self._threshold_lower = lower
+ self._threshold_upper = upper
self._hysteresis = hysteresis
self._device_class = device_class
- self._state = False
- self.sensor_value = 0
- @callback
+ self._state_position = None
+ self._state = False
+ self.sensor_value = None
+
# pylint: disable=invalid-name
+ @callback
def async_threshold_sensor_state_listener(
entity, old_state, new_state):
"""Handle sensor state changes."""
- if new_state.state == STATE_UNKNOWN:
- return
-
try:
- self.sensor_value = float(new_state.state)
- except ValueError:
- _LOGGER.error("State is not numerical")
+ self.sensor_value = None if new_state.state == STATE_UNKNOWN \
+ else float(new_state.state)
+ except (ValueError, TypeError):
+ self.sensor_value = None
+ _LOGGER.warning("State is not numerical")
hass.async_add_job(self.async_update_ha_state, True)
@@ -118,23 +123,67 @@ class ThresholdSensor(BinarySensorDevice):
"""Return the sensor class of the sensor."""
return self._device_class
+ @property
+ def threshold_type(self):
+ """Return the type of threshold this sensor represents."""
+ if self._threshold_lower and self._threshold_upper:
+ return TYPE_RANGE
+ elif self._threshold_lower:
+ return TYPE_LOWER
+ elif self._threshold_upper:
+ return TYPE_UPPER
+
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
return {
ATTR_ENTITY_ID: self._entity_id,
- ATTR_SENSOR_VALUE: self.sensor_value,
- ATTR_THRESHOLD: self._threshold,
ATTR_HYSTERESIS: self._hysteresis,
- ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
+ ATTR_LOWER: self._threshold_lower,
+ ATTR_POSITION: self._state_position,
+ ATTR_SENSOR_VALUE: self.sensor_value,
+ ATTR_TYPE: self.threshold_type,
+ ATTR_UPPER: self._threshold_upper,
}
@asyncio.coroutine
def async_update(self):
"""Get the latest data and updates the states."""
- if self._hysteresis == 0 and self.sensor_value == self._threshold:
+ def below(threshold):
+ """Determine if the sensor value is below a threshold."""
+ return self.sensor_value < (threshold - self._hysteresis)
+
+ def above(threshold):
+ """Determine if the sensor value is above a threshold."""
+ return self.sensor_value > (threshold + self._hysteresis)
+
+ if self.sensor_value is None:
+ self._state_position = POSITION_UNKNOWN
self._state = False
- elif self.sensor_value > (self._threshold + self._hysteresis):
- self._state = self.is_upper
- elif self.sensor_value < (self._threshold - self._hysteresis):
- self._state = not self.is_upper
+
+ elif self.threshold_type == TYPE_LOWER:
+ if below(self._threshold_lower):
+ self._state_position = POSITION_BELOW
+ self._state = True
+ elif above(self._threshold_lower):
+ self._state_position = POSITION_ABOVE
+ self._state = False
+
+ elif self.threshold_type == TYPE_UPPER:
+ if above(self._threshold_upper):
+ self._state_position = POSITION_ABOVE
+ self._state = True
+ elif below(self._threshold_upper):
+ self._state_position = POSITION_BELOW
+ self._state = False
+
+ elif self.threshold_type == TYPE_RANGE:
+ if below(self._threshold_lower):
+ self._state_position = POSITION_BELOW
+ self._state = False
+ if above(self._threshold_upper):
+ self._state_position = POSITION_ABOVE
+ self._state = False
+ elif above(self._threshold_lower) and below(self._threshold_upper):
+ self._state_position = POSITION_IN_RANGE
+ self._state = True
diff --git a/homeassistant/components/binary_sensor/vera.py b/homeassistant/components/binary_sensor/vera.py
index e16f4e17fa0..e87886376bc 100644
--- a/homeassistant/components/binary_sensor/vera.py
+++ b/homeassistant/components/binary_sensor/vera.py
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Perform the setup for Vera controller devices."""
add_devices(
- VeraBinarySensor(device, VERA_CONTROLLER)
- for device in VERA_DEVICES['binary_sensor'])
+ VeraBinarySensor(device, hass.data[VERA_CONTROLLER])
+ for device in hass.data[VERA_DEVICES]['binary_sensor'])
class VeraBinarySensor(VeraDevice, BinarySensorDevice):
diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py
new file mode 100644
index 00000000000..36894dcab61
--- /dev/null
+++ b/homeassistant/components/calendar/caldav.py
@@ -0,0 +1,230 @@
+"""
+Support for WebDav Calendar.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/calendar.caldav/
+"""
+import logging
+import re
+from datetime import datetime, timedelta
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.calendar import (
+ CalendarEventDevice, PLATFORM_SCHEMA)
+from homeassistant.const import (
+ CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME)
+from homeassistant.util import dt, Throttle
+
+REQUIREMENTS = ['caldav==0.5.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_DEVICE_ID = 'device_id'
+CONF_CALENDARS = 'calendars'
+CONF_CUSTOM_CALENDARS = 'custom_calendars'
+CONF_CALENDAR = 'calendar'
+CONF_SEARCH = 'search'
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ # pylint: disable=no-value-for-parameter
+ vol.Required(CONF_URL): vol.Url(),
+ vol.Optional(CONF_CALENDARS, default=[]):
+ vol.All(cv.ensure_list, vol.Schema([
+ cv.string
+ ])),
+ vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
+ vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
+ vol.Optional(CONF_CUSTOM_CALENDARS, default=[]):
+ vol.All(cv.ensure_list, vol.Schema([
+ vol.Schema({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_CALENDAR): cv.string,
+ vol.Required(CONF_SEARCH): cv.string
+ })
+ ]))
+})
+
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
+
+
+def setup_platform(hass, config, add_devices, disc_info=None):
+ """Set up the WebDav Calendar platform."""
+ import caldav
+
+ client = caldav.DAVClient(config.get(CONF_URL),
+ None,
+ config.get(CONF_USERNAME),
+ config.get(CONF_PASSWORD))
+
+ # Retrieve all the remote calendars
+ calendars = client.principal().calendars()
+
+ calendar_devices = []
+ for calendar in list(calendars):
+ # If a calendar name was given in the configuration,
+ # ignore all the others
+ if (config.get(CONF_CALENDARS)
+ and calendar.name not in config.get(CONF_CALENDARS)):
+ _LOGGER.debug("Ignoring calendar '%s'", calendar.name)
+ continue
+
+ # Create additional calendars based on custom filtering
+ # rules
+ for cust_calendar in config.get(CONF_CUSTOM_CALENDARS):
+ # Check that the base calendar matches
+ if cust_calendar.get(CONF_CALENDAR) != calendar.name:
+ continue
+
+ device_data = {
+ CONF_NAME: cust_calendar.get(CONF_NAME),
+ CONF_DEVICE_ID: "{} {}".format(
+ cust_calendar.get(CONF_CALENDAR),
+ cust_calendar.get(CONF_NAME)),
+ }
+
+ calendar_devices.append(
+ WebDavCalendarEventDevice(hass,
+ device_data,
+ calendar,
+ True,
+ cust_calendar.get(CONF_SEARCH))
+ )
+
+ # Create a default calendar if there was no custom one
+ if not config.get(CONF_CUSTOM_CALENDARS):
+ device_data = {
+ CONF_NAME: calendar.name,
+ CONF_DEVICE_ID: calendar.name
+ }
+ calendar_devices.append(
+ WebDavCalendarEventDevice(hass, device_data, calendar)
+ )
+
+ # Finally add all the calendars we've created
+ add_devices(calendar_devices)
+
+
+class WebDavCalendarEventDevice(CalendarEventDevice):
+ """A device for getting the next Task from a WebDav Calendar."""
+
+ def __init__(self,
+ hass,
+ device_data,
+ calendar,
+ all_day=False,
+ search=None):
+ """Create the WebDav Calendar Event Device."""
+ self.data = WebDavCalendarData(calendar, all_day, search)
+ super().__init__(hass, device_data)
+
+ @property
+ def device_state_attributes(self):
+ """Return the device state attributes."""
+ if self.data.event is None:
+ # No tasks, we don't REALLY need to show anything.
+ return {}
+
+ attributes = super().device_state_attributes
+ return attributes
+
+
+class WebDavCalendarData(object):
+ """Class to utilize the calendar dav client object to get next event."""
+
+ def __init__(self, calendar, include_all_day, search):
+ """Set up how we are going to search the WebDav calendar."""
+ self.calendar = calendar
+ self.include_all_day = include_all_day
+ self.search = search
+ self.event = None
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self):
+ """Get the latest data."""
+ # We have to retrieve the results for the whole day as the server
+ # won't return events that have already started
+ results = self.calendar.date_search(
+ dt.start_of_local_day(),
+ dt.start_of_local_day() + timedelta(days=1)
+ )
+
+ # dtstart can be a date or datetime depending if the event lasts a
+ # whole day. Convert everything to datetime to be able to sort it
+ results.sort(key=lambda x: self.to_datetime(
+ x.instance.vevent.dtstart.value
+ ))
+
+ vevent = next((
+ event.instance.vevent for event in results
+ if (self.is_matching(event.instance.vevent, self.search)
+ and (not self.is_all_day(event.instance.vevent)
+ or self.include_all_day)
+ and not self.is_over(event.instance.vevent))), None)
+
+ # If no matching event could be found
+ if vevent is None:
+ _LOGGER.debug(
+ "No matching event found in the %d results for %s",
+ len(results),
+ self.calendar.name,
+ )
+ self.event = None
+ return True
+
+ # Populate the entity attributes with the event values
+ self.event = {
+ "summary": vevent.summary.value,
+ "start": self.get_hass_date(vevent.dtstart.value),
+ "end": self.get_hass_date(vevent.dtend.value),
+ "location": self.get_attr_value(vevent, "location"),
+ "description": self.get_attr_value(vevent, "description")
+ }
+ return True
+
+ @staticmethod
+ def is_matching(vevent, search):
+ """Return if the event matches the filter critera."""
+ if search is None:
+ return True
+
+ pattern = re.compile(search)
+ return (hasattr(vevent, "summary")
+ and pattern.match(vevent.summary.value)
+ or hasattr(vevent, "location")
+ and pattern.match(vevent.location.value)
+ or hasattr(vevent, "description")
+ and pattern.match(vevent.description.value))
+
+ @staticmethod
+ def is_all_day(vevent):
+ """Return if the event last the whole day."""
+ return not isinstance(vevent.dtstart.value, datetime)
+
+ @staticmethod
+ def is_over(vevent):
+ """Return if the event is over."""
+ return dt.now() > WebDavCalendarData.to_datetime(vevent.dtend.value)
+
+ @staticmethod
+ def get_hass_date(obj):
+ """Return if the event matches."""
+ if isinstance(obj, datetime):
+ return {"dateTime": obj.isoformat()}
+
+ return {"date": obj.isoformat()}
+
+ @staticmethod
+ def to_datetime(obj):
+ """Return a datetime."""
+ if isinstance(obj, datetime):
+ return obj
+ return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min))
+
+ @staticmethod
+ def get_attr_value(obj, attribute):
+ """Return the value of the attribute if defined."""
+ if hasattr(obj, attribute):
+ return getattr(obj, attribute).value
+ return None
diff --git a/homeassistant/components/camera/canary.py b/homeassistant/components/camera/canary.py
new file mode 100644
index 00000000000..302758eee94
--- /dev/null
+++ b/homeassistant/components/camera/canary.py
@@ -0,0 +1,95 @@
+"""
+Support for Canary camera.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/camera.canary/
+"""
+import logging
+
+import requests
+
+from homeassistant.components.camera import Camera
+from homeassistant.components.canary import DATA_CANARY, DEFAULT_TIMEOUT
+
+DEPENDENCIES = ['canary']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_MOTION_START_TIME = "motion_start_time"
+ATTR_MOTION_END_TIME = "motion_end_time"
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Canary sensors."""
+ data = hass.data[DATA_CANARY]
+ 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))
+
+ add_devices(devices, True)
+
+
+class CanaryCamera(Camera):
+ """An implementation of a Canary security camera."""
+
+ def __init__(self, data, location_id, timeout):
+ """Initialize a Canary security camera."""
+ super().__init__()
+ self._data = data
+ self._location_id = location_id
+ 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
+
+ @property
+ def name(self):
+ """Return the name of this device."""
+ return self._location.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
diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py
new file mode 100644
index 00000000000..8ab7218e201
--- /dev/null
+++ b/homeassistant/components/canary.py
@@ -0,0 +1,117 @@
+"""
+Support for Canary.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/canary/
+"""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+from requests import ConnectTimeout, HTTPError
+
+import homeassistant.helpers.config_validation as cv
+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']
+
+_LOGGER = logging.getLogger(__name__)
+
+NOTIFICATION_ID = 'canary_notification'
+NOTIFICATION_TITLE = 'Canary Setup'
+
+DOMAIN = 'canary'
+DATA_CANARY = 'canary'
+MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
+DEFAULT_TIMEOUT = 10
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+CANARY_COMPONENTS = [
+ 'alarm_control_panel', 'camera', 'sensor'
+]
+
+
+def setup(hass, config):
+ """Set up the Canary component."""
+ conf = config[DOMAIN]
+ username = conf.get(CONF_USERNAME)
+ password = conf.get(CONF_PASSWORD)
+ timeout = conf.get(CONF_TIMEOUT)
+
+ try:
+ hass.data[DATA_CANARY] = CanaryData(username, password, timeout)
+ except (ConnectTimeout, HTTPError) as ex:
+ _LOGGER.error("Unable to connect to Canary service: %s", str(ex))
+ hass.components.persistent_notification.create(
+ 'Error: {}
'
+ 'You will need to restart hass after fixing.'
+ ''.format(ex),
+ title=NOTIFICATION_TITLE,
+ notification_id=NOTIFICATION_ID)
+ return False
+
+ for component in CANARY_COMPONENTS:
+ discovery.load_platform(hass, component, DOMAIN, {}, config)
+
+ return True
+
+
+class CanaryData(object):
+ """Get the latest data and update the states."""
+
+ def __init__(self, username, password, timeout):
+ """Init the Canary data object."""
+ from canary.api import Api
+ self._api = Api(username, password, timeout)
+
+ self._locations_by_id = {}
+ self._readings_by_device_id = {}
+ self._entries_by_location_id = {}
+
+ self.update()
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ def update(self, **kwargs):
+ """Get the latest data from py-canary."""
+ for location in self._api.get_locations():
+ location_id = location.location_id
+
+ self._locations_by_id[location_id] = location
+ self._entries_by_location_id[location_id] = self._api.get_entries(
+ location_id, entry_type="motion", limit=1)
+
+ for device in location.devices:
+ if device.is_online:
+ self._readings_by_device_id[device.device_id] = \
+ self._api.get_latest_readings(device.device_id)
+
+ @property
+ def locations(self):
+ """Return a list of locations."""
+ return self._locations_by_id.values()
+
+ def get_motion_entries(self, location_id):
+ """Return a list of motion entries based on location_id."""
+ return self._entries_by_location_id.get(location_id, [])
+
+ def get_location(self, location_id):
+ """Return a location based on location_id."""
+ return self._locations_by_id.get(location_id, [])
+
+ def get_readings(self, device_id):
+ """Return a list of readings based on device_id."""
+ return self._readings_by_device_id.get(device_id, [])
+
+ 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)
diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py
index 267657d56ce..8305e772869 100644
--- a/homeassistant/components/climate/hive.py
+++ b/homeassistant/components/climate/hive.py
@@ -6,7 +6,7 @@ https://home-assistant.io/components/climate.hive/
"""
from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
- SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
+ SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE
@@ -16,7 +16,9 @@ HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
HASS_TO_HIVE_STATE = {STATE_AUTO: 'SCHEDULE', STATE_HEAT: 'MANUAL',
STATE_ON: 'ON', STATE_OFF: 'OFF'}
-SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE
+SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE |
+ SUPPORT_OPERATION_MODE |
+ SUPPORT_AUX_HEAT)
def setup_platform(hass, config, add_devices, discovery_info=None):
@@ -134,6 +136,43 @@ class HiveClimateEntity(ClimateDevice):
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
+ @property
+ def is_aux_heat_on(self):
+ """Return true if auxiliary heater is on."""
+ boost_status = None
+ if self.device_type == "Heating":
+ boost_status = self.session.heating.get_boost(self.node_id)
+ elif self.device_type == "HotWater":
+ boost_status = self.session.hotwater.get_boost(self.node_id)
+ return boost_status == "ON"
+
+ def turn_aux_heat_on(self):
+ """Turn auxiliary heater on."""
+ target_boost_time = 30
+ if self.device_type == "Heating":
+ curtemp = self.session.heating.current_temperature(self.node_id)
+ curtemp = round(curtemp * 2) / 2
+ target_boost_temperature = curtemp + 0.5
+ self.session.heating.turn_boost_on(self.node_id,
+ target_boost_time,
+ target_boost_temperature)
+ elif self.device_type == "HotWater":
+ self.session.hotwater.turn_boost_on(self.node_id,
+ target_boost_time)
+
+ for entity in self.session.entities:
+ entity.handle_update(self.data_updatesource)
+
+ def turn_aux_heat_off(self):
+ """Turn auxiliary heater off."""
+ if self.device_type == "Heating":
+ self.session.heating.turn_boost_off(self.node_id)
+ elif self.device_type == "HotWater":
+ self.session.hotwater.turn_boost_off(self.node_id)
+
+ for entity in self.session.entities:
+ entity.handle_update(self.data_updatesource)
+
def update(self):
"""Update all Node data frome Hive."""
self.session.core.update_data(self.node_id)
diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py
index 624729249aa..ed23d91587c 100644
--- a/homeassistant/components/climate/sensibo.py
+++ b/homeassistant/components/climate/sensibo.py
@@ -13,11 +13,12 @@ import async_timeout
import voluptuous as vol
from homeassistant.const import (
- ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, TEMP_CELSIUS, TEMP_FAHRENHEIT)
+ ATTR_TEMPERATURE, CONF_API_KEY, CONF_ID, STATE_OFF, TEMP_CELSIUS,
+ TEMP_FAHRENHEIT)
from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
- SUPPORT_FAN_MODE, SUPPORT_AWAY_MODE, SUPPORT_SWING_MODE,
+ SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
SUPPORT_AUX_HEAT)
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv
@@ -41,9 +42,13 @@ _FETCH_FIELDS = ','.join([
'acState', 'connectionStatus{isAlive}', 'temperatureUnit'])
_INITIAL_FETCH_FIELDS = 'id,' + _FETCH_FIELDS
-SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
- SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | SUPPORT_SWING_MODE |
- SUPPORT_AUX_HEAT)
+FIELD_TO_FLAG = {
+ 'fanLevel': SUPPORT_FAN_MODE,
+ 'mode': SUPPORT_OPERATION_MODE,
+ 'swing': SUPPORT_SWING_MODE,
+ 'targetTemperature': SUPPORT_TARGET_TEMPERATURE,
+ 'on': SUPPORT_AUX_HEAT,
+}
@asyncio.coroutine
@@ -85,7 +90,14 @@ class SensiboClimate(ClimateDevice):
@property
def supported_features(self):
"""Return the list of supported features."""
- return SUPPORT_FLAGS
+ return self._supported_features
+
+ @property
+ def state(self):
+ """Return the current state."""
+ if not self.is_aux_heat_on:
+ return STATE_OFF
+ return super().state
def _do_update(self, data):
self._name = data['room']['name']
@@ -106,6 +118,10 @@ class SensiboClimate(ClimateDevice):
else:
self._temperature_unit = self.unit_of_measurement
self._temperatures_list = []
+ self._supported_features = 0
+ for key in self._ac_states:
+ if key in FIELD_TO_FLAG:
+ self._supported_features |= FIELD_TO_FLAG[key]
@property
def device_state_attributes(self):
@@ -196,13 +212,13 @@ class SensiboClimate(ClimateDevice):
def min_temp(self):
"""Return the minimum temperature."""
return self._temperatures_list[0] \
- if len(self._temperatures_list) else super.min_temp()
+ if len(self._temperatures_list) else super().min_temp()
@property
def max_temp(self):
"""Return the maximum temperature."""
return self._temperatures_list[-1] \
- if len(self._temperatures_list) else super.max_temp()
+ if len(self._temperatures_list) else super().max_temp()
@asyncio.coroutine
def async_set_temperature(self, **kwargs):
diff --git a/homeassistant/components/climate/vera.py b/homeassistant/components/climate/vera.py
index 4644f86cba2..c9d22e41d81 100644
--- a/homeassistant/components/climate/vera.py
+++ b/homeassistant/components/climate/vera.py
@@ -32,8 +32,8 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE |
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Set up of Vera thermostats."""
add_devices_callback(
- VeraThermostat(device, VERA_CONTROLLER) for
- device in VERA_DEVICES['climate'])
+ VeraThermostat(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['climate'])
class VeraThermostat(VeraDevice, ClimateDevice):
diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py
index 9bd91d22beb..58a2152f898 100644
--- a/homeassistant/components/cloud/__init__.py
+++ b/homeassistant/components/cloud/__init__.py
@@ -16,7 +16,7 @@ from homeassistant.components.alexa import smart_home
from . import http_api, iot
from .const import CONFIG_DIR, DOMAIN, SERVERS
-REQUIREMENTS = ['warrant==0.5.0']
+REQUIREMENTS = ['warrant==0.6.1']
_LOGGER = logging.getLogger(__name__)
@@ -27,7 +27,7 @@ CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
MODE_DEV = 'development'
-DEFAULT_MODE = MODE_DEV
+DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
ALEXA_SCHEMA = vol.Schema({
@@ -42,10 +42,10 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
vol.In([MODE_DEV] + list(SERVERS)),
# Change to optional when we include real servers
- vol.Required(CONF_COGNITO_CLIENT_ID): str,
- vol.Required(CONF_USER_POOL_ID): str,
- vol.Required(CONF_REGION): str,
- vol.Required(CONF_RELAYER): str,
+ vol.Optional(CONF_COGNITO_CLIENT_ID): str,
+ vol.Optional(CONF_USER_POOL_ID): str,
+ vol.Optional(CONF_REGION): str,
+ vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA
}),
}, extra=vol.ALLOW_EXTRA)
@@ -117,10 +117,6 @@ class Cloud:
@property
def subscription_expired(self):
"""Return a boolen if the subscription has expired."""
- # For now, don't enforce subscriptions to exist
- if 'custom:sub-exp' not in self.claims:
- return False
-
return dt_util.utcnow() > self.expiration_date
@property
diff --git a/homeassistant/components/cloud/auth_api.py b/homeassistant/components/cloud/auth_api.py
index 95bf5596835..9cad3ec77f3 100644
--- a/homeassistant/components/cloud/auth_api.py
+++ b/homeassistant/components/cloud/auth_api.py
@@ -68,11 +68,14 @@ def register(cloud, email, password):
from botocore.exceptions import ClientError
cognito = _cognito(cloud)
+ # Workaround for bug in Warrant. PR with fix:
+ # https://github.com/capless/warrant/pull/82
+ cognito.add_base_attributes()
try:
if cloud.cognito_email_based:
- cognito.register(email, password, email=email)
+ cognito.register(email, password)
else:
- cognito.register(_generate_username(email), password, email=email)
+ cognito.register(_generate_username(email), password)
except ClientError as err:
raise _map_aws_exception(err)
diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py
index 440e4179eea..b13ec6d1e45 100644
--- a/homeassistant/components/cloud/const.py
+++ b/homeassistant/components/cloud/const.py
@@ -4,13 +4,12 @@ CONFIG_DIR = '.cloud'
REQUEST_TIMEOUT = 10
SERVERS = {
- # Example entry:
- # 'production': {
- # 'cognito_client_id': '',
- # 'user_pool_id': '',
- # 'region': '',
- # 'relayer': ''
- # }
+ 'production': {
+ 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u',
+ 'user_pool_id': 'us-east-1_87ll5WOP8',
+ 'region': 'us-east-1',
+ 'relayer': 'wss://cloud.hass.io:8000/websocket'
+ }
}
MESSAGE_EXPIRATION = """
diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py
index 64eccfaa2b8..6ede91e9b66 100644
--- a/homeassistant/components/config/automation.py
+++ b/homeassistant/components/config/automation.py
@@ -1,4 +1,4 @@
-"""Provide configuration end points for Z-Wave."""
+"""Provide configuration end points for Automations."""
import asyncio
from homeassistant.components.config import EditIdBasedConfigView
diff --git a/homeassistant/components/cover/isy994.py b/homeassistant/components/cover/isy994.py
index 1e83038278c..4dd1c9be364 100644
--- a/homeassistant/components/cover/isy994.py
+++ b/homeassistant/components/cover/isy994.py
@@ -69,7 +69,10 @@ class ISYCoverDevice(isy.ISYDevice, CoverDevice):
@property
def state(self) -> str:
"""Get the state of the ISY994 cover device."""
- return VALUE_TO_STATE.get(self.value, STATE_OPEN)
+ if self.is_unknown():
+ return None
+ else:
+ return VALUE_TO_STATE.get(self.value, STATE_OPEN)
def open_cover(self, **kwargs) -> None:
"""Send the open cover command to the ISY994 cover device."""
diff --git a/homeassistant/components/cover/tellstick.py b/homeassistant/components/cover/tellstick.py
new file mode 100755
index 00000000000..56a5a24b409
--- /dev/null
+++ b/homeassistant/components/cover/tellstick.py
@@ -0,0 +1,65 @@
+"""
+Support for Tellstick covers.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/cover.tellstick/
+"""
+
+
+from homeassistant.components.cover import CoverDevice
+from homeassistant.components.tellstick import (
+ DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG,
+ DATA_TELLSTICK, TellstickDevice)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Tellstick covers."""
+ if (discovery_info is None or
+ discovery_info[ATTR_DISCOVER_DEVICES] is None):
+ return
+
+ signal_repetitions = discovery_info.get(
+ ATTR_DISCOVER_CONFIG, DEFAULT_SIGNAL_REPETITIONS)
+
+ add_devices([TellstickCover(hass.data[DATA_TELLSTICK][tellcore_id],
+ signal_repetitions)
+ for tellcore_id in discovery_info[ATTR_DISCOVER_DEVICES]],
+ True)
+
+
+class TellstickCover(TellstickDevice, CoverDevice):
+ """Representation of a Tellstick cover."""
+
+ @property
+ def is_closed(self):
+ """Return the current position of the cover is not possible."""
+ return None
+
+ @property
+ def assumed_state(self):
+ """Return True if unable to access real state of the entity."""
+ return True
+
+ def close_cover(self, **kwargs):
+ """Close the cover."""
+ self._tellcore_device.down()
+
+ def open_cover(self, **kwargs):
+ """Open the cover."""
+ self._tellcore_device.up()
+
+ def stop_cover(self, **kwargs):
+ """Stop the cover."""
+ self._tellcore_device.stop()
+
+ def _parse_tellcore_data(self, tellcore_data):
+ """Turn the value received from tellcore into something useful."""
+ pass
+
+ def _parse_ha_data(self, kwargs):
+ """Turn the value from HA into something useful."""
+ pass
+
+ def _update_model(self, new_state, data):
+ """Update the device entity state to match the arguments."""
+ pass
diff --git a/homeassistant/components/cover/vera.py b/homeassistant/components/cover/vera.py
index 05be125ec6f..6cf269b75b3 100644
--- a/homeassistant/components/cover/vera.py
+++ b/homeassistant/components/cover/vera.py
@@ -18,8 +18,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera covers."""
add_devices(
- VeraCover(device, VERA_CONTROLLER) for
- device in VERA_DEVICES['cover'])
+ VeraCover(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['cover'])
class VeraCover(VeraDevice, CoverDevice):
diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py
index f2d2a4c74b5..495e377077f 100644
--- a/homeassistant/components/device_tracker/asuswrt.py
+++ b/homeassistant/components/device_tracker/asuswrt.py
@@ -67,6 +67,15 @@ _IP_NEIGH_REGEX = re.compile(
r'\s?(router)?'
r'(?P(\w+))')
+_ARP_CMD = 'arp -n'
+_ARP_REGEX = re.compile(
+ r'.+\s' +
+ r'\((?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
+ r'.+\s' +
+ r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
+ r'\s' +
+ r'.*')
+
# pylint: disable=unused-argument
def get_scanner(hass, config):
@@ -76,7 +85,22 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
-AsusWrtResult = namedtuple('AsusWrtResult', 'neighbors leases')
+def _parse_lines(lines, regex):
+ """Parse the lines using the given regular expression.
+
+ If a line can't be parsed it is logged and skipped in the output.
+ """
+ results = []
+ for line in lines:
+ match = regex.search(line)
+ if not match:
+ _LOGGER.debug("Could not parse row: %s", line)
+ continue
+ results.append(match.groupdict())
+ return results
+
+
+Device = namedtuple('Device', ['mac', 'ip', 'name'])
class AsusWrtDeviceScanner(DeviceScanner):
@@ -121,16 +145,13 @@ class AsusWrtDeviceScanner(DeviceScanner):
def scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
- return [client['mac'] for client in self.last_results]
+ return list(self.last_results.keys())
def get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
- if not self.last_results:
+ if device not in self.last_results:
return None
- for client in self.last_results:
- if client['mac'] == device:
- return client['host']
- return None
+ return self.last_results[device].name
def _update_info(self):
"""Ensure the information from the ASUSWRT router is up to date.
@@ -145,72 +166,71 @@ class AsusWrtDeviceScanner(DeviceScanner):
if not data:
return False
- active_clients = [client for client in data.values() if
- client['status'] == 'REACHABLE' or
- client['status'] == 'DELAY' or
- client['status'] == 'STALE' or
- client['status'] == 'IN_ASSOCLIST']
- self.last_results = active_clients
+ self.last_results = data
return True
def get_asuswrt_data(self):
- """Retrieve data from ASUSWRT and return parsed result."""
- result = self.connection.get_result()
-
- if not result:
- return {}
+ """Retrieve data from ASUSWRT.
+ Calls various commands on the router and returns the superset of all
+ responses. Some commands will not work on some routers.
+ """
devices = {}
- if self.mode == 'ap':
- for lease in result.leases:
- match = _WL_REGEX.search(lease.decode('utf-8'))
+ devices.update(self._get_wl())
+ devices.update(self._get_arp())
+ devices.update(self._get_neigh())
+ if not self.mode == 'ap':
+ devices.update(self._get_leases())
+ return devices
- if not match:
- _LOGGER.warning("Could not parse wl row: %s", lease)
- continue
+ def _get_wl(self):
+ lines = self.connection.run_command(_WL_CMD)
+ if not lines:
+ return {}
+ result = _parse_lines(lines, _WL_REGEX)
+ devices = {}
+ for device in result:
+ mac = device['mac'].upper()
+ devices[mac] = Device(mac, None, None)
+ return devices
+ def _get_leases(self):
+ lines = self.connection.run_command(_LEASES_CMD)
+ if not lines:
+ return {}
+ lines = [line for line in lines if not line.startswith('duid ')]
+ result = _parse_lines(lines, _LEASES_REGEX)
+ devices = {}
+ for device in result:
+ # For leases where the client doesn't set a hostname, ensure it
+ # is blank and not '*', which breaks entity_id down the line.
+ host = device['host']
+ if host == '*':
host = ''
+ mac = device['mac'].upper()
+ devices[mac] = Device(mac, device['ip'], host)
+ return devices
- devices[match.group('mac').upper()] = {
- 'host': host,
- 'status': 'IN_ASSOCLIST',
- 'ip': '',
- 'mac': match.group('mac').upper(),
- }
-
- else:
- for lease in result.leases:
- if lease.startswith(b'duid '):
- continue
- match = _LEASES_REGEX.search(lease.decode('utf-8'))
-
- if not match:
- _LOGGER.warning("Could not parse lease row: %s", lease)
- continue
-
- # For leases where the client doesn't set a hostname, ensure it
- # is blank and not '*', which breaks entity_id down the line.
- host = match.group('host')
- if host == '*':
- host = ''
-
- devices[match.group('mac')] = {
- 'host': host,
- 'status': '',
- 'ip': match.group('ip'),
- 'mac': match.group('mac').upper(),
- }
-
- for neighbor in result.neighbors:
- match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8'))
- if not match:
- _LOGGER.warning("Could not parse neighbor row: %s",
- neighbor)
- continue
- if match.group('mac') in devices:
- devices[match.group('mac')]['status'] = (
- match.group('status'))
+ def _get_neigh(self):
+ lines = self.connection.run_command(_IP_NEIGH_CMD)
+ if not lines:
+ return {}
+ result = _parse_lines(lines, _IP_NEIGH_REGEX)
+ devices = {}
+ for device in result:
+ mac = device['mac'].upper()
+ devices[mac] = Device(mac, None, None)
+ return devices
+ def _get_arp(self):
+ lines = self.connection.run_command(_ARP_CMD)
+ if not lines:
+ return {}
+ result = _parse_lines(lines, _ARP_REGEX)
+ devices = {}
+ for device in result:
+ mac = device['mac'].upper()
+ devices[mac] = Device(mac, device['ip'], None)
return devices
@@ -247,8 +267,8 @@ class SshConnection(_Connection):
self._ssh_key = ssh_key
self._ap = ap
- def get_result(self):
- """Retrieve a single AsusWrtResult through an SSH connection.
+ def run_command(self, command):
+ """Run commands through an SSH connection.
Connect to the SSH server if not currently connected, otherwise
use the existing connection.
@@ -258,19 +278,10 @@ class SshConnection(_Connection):
try:
if not self.connected:
self.connect()
- if self._ap:
- neighbors = ['']
- self._ssh.sendline(_WL_CMD)
- self._ssh.prompt()
- leases_result = self._ssh.before.split(b'\n')[1:-1]
- else:
- self._ssh.sendline(_IP_NEIGH_CMD)
- self._ssh.prompt()
- neighbors = self._ssh.before.split(b'\n')[1:-1]
- self._ssh.sendline(_LEASES_CMD)
- self._ssh.prompt()
- leases_result = self._ssh.before.split(b'\n')[1:-1]
- return AsusWrtResult(neighbors, leases_result)
+ self._ssh.sendline(command)
+ self._ssh.prompt()
+ lines = self._ssh.before.split(b'\n')[1:-1]
+ return [line.decode('utf-8') for line in lines]
except exceptions.EOF as err:
_LOGGER.error("Connection refused. SSH enabled?")
self.disconnect()
@@ -326,8 +337,8 @@ class TelnetConnection(_Connection):
self._ap = ap
self._prompt_string = None
- def get_result(self):
- """Retrieve a single AsusWrtResult through a Telnet connection.
+ def run_command(self, command):
+ """Run a command through a Telnet connection.
Connect to the Telnet server if not currently connected, otherwise
use the existing connection.
@@ -336,18 +347,9 @@ class TelnetConnection(_Connection):
if not self.connected:
self.connect()
- self._telnet.write('{}\n'.format(_IP_NEIGH_CMD).encode('ascii'))
- neighbors = (self._telnet.read_until(self._prompt_string).
- split(b'\n')[1:-1])
- if self._ap:
- self._telnet.write('{}\n'.format(_WL_CMD).encode('ascii'))
- leases_result = (self._telnet.read_until(self._prompt_string).
- split(b'\n')[1:-1])
- else:
- self._telnet.write('{}\n'.format(_LEASES_CMD).encode('ascii'))
- leases_result = (self._telnet.read_until(self._prompt_string).
- split(b'\n')[1:-1])
- return AsusWrtResult(neighbors, leases_result)
+ self._telnet.write('{}\n'.format(command).encode('ascii'))
+ return (self._telnet.read_until(self._prompt_string).
+ split(b'\n')[1:-1])
except EOFError:
_LOGGER.error("Unexpected response from router")
self.disconnect()
diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py
index b88245ac9a5..1952e6d676d 100644
--- a/homeassistant/components/device_tracker/gpslogger.py
+++ b/homeassistant/components/device_tracker/gpslogger.py
@@ -5,23 +5,37 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/
"""
import asyncio
-from functools import partial
import logging
+from hmac import compare_digest
-from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY
-from homeassistant.components.http import HomeAssistantView
+from aiohttp.web import Request, HTTPUnauthorized # NOQA
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.const import (
+ CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY
+)
+from homeassistant.components.http import (
+ CONF_API_PASSWORD, HomeAssistantView
+)
# pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA
- DOMAIN, PLATFORM_SCHEMA)
+ DOMAIN, PLATFORM_SCHEMA
+)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Optional(CONF_PASSWORD): cv.string,
+})
-def setup_scanner(hass, config, see, discovery_info=None):
+
+@asyncio.coroutine
+def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application."""
- hass.http.register_view(GPSLoggerView(see))
+ hass.http.register_view(GPSLoggerView(async_see, config))
return True
@@ -32,26 +46,36 @@ class GPSLoggerView(HomeAssistantView):
url = '/api/gpslogger'
name = 'api:gpslogger'
- def __init__(self, see):
+ def __init__(self, async_see, config):
"""Initialize GPSLogger url endpoints."""
- self.see = see
+ self.async_see = async_see
+ self._password = config.get(CONF_PASSWORD)
+ # this component does not require external authentication if
+ # password is set
+ self.requires_auth = self._password is None
@asyncio.coroutine
- def get(self, request):
+ def get(self, request: Request):
"""Handle for GPSLogger message received as GET."""
- res = yield from self._handle(request.app['hass'], request.query)
- return res
+ hass = request.app['hass']
+ data = request.query
+
+ if self._password is not None:
+ authenticated = CONF_API_PASSWORD in data and compare_digest(
+ self._password,
+ data[CONF_API_PASSWORD]
+ )
+ if not authenticated:
+ raise HTTPUnauthorized()
- @asyncio.coroutine
- def _handle(self, hass, data):
- """Handle GPSLogger requests."""
if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data:
_LOGGER.error("Device id not specified")
- return ('Device id not specified.', HTTP_UNPROCESSABLE_ENTITY)
+ return ('Device id not specified.',
+ HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '')
gps_location = (data['latitude'], data['longitude'])
@@ -75,10 +99,11 @@ class GPSLoggerView(HomeAssistantView):
if 'activity' in data:
attrs['activity'] = data['activity']
- yield from hass.async_add_job(
- partial(self.see, dev_id=device,
- gps=gps_location, battery=battery,
- gps_accuracy=accuracy,
- attributes=attrs))
+ hass.async_add_job(self.async_see(
+ dev_id=device,
+ gps=gps_location, battery=battery,
+ gps_accuracy=accuracy,
+ attributes=attrs
+ ))
return 'Setting location for {}'.format(device)
diff --git a/homeassistant/components/device_tracker/meraki.py b/homeassistant/components/device_tracker/meraki.py
index 319c19d7b73..9437486a0aa 100644
--- a/homeassistant/components/device_tracker/meraki.py
+++ b/homeassistant/components/device_tracker/meraki.py
@@ -94,8 +94,26 @@ class MerakiView(HomeAssistantView):
def _handle(self, hass, data):
for i in data["data"]["observations"]:
data["data"]["secret"] = "hidden"
+
+ lat = i["location"]["lat"]
+ lng = i["location"]["lng"]
+ try:
+ accuracy = int(float(i["location"]["unc"]))
+ except ValueError:
+ accuracy = 0
+
mac = i["clientMac"]
_LOGGER.debug("clientMac: %s", mac)
+
+ if lat == "NaN" or lng == "NaN":
+ _LOGGER.debug(
+ "No coordinates received, skipping location for: " + mac
+ )
+ gps_location = None
+ accuracy = None
+ else:
+ gps_location = (lat, lng)
+
attrs = {}
if i.get('os', False):
attrs['os'] = i['os']
@@ -110,7 +128,9 @@ class MerakiView(HomeAssistantView):
if i.get('ssid', False):
attrs['ssid'] = i['ssid']
hass.async_add_job(self.async_see(
+ gps=gps_location,
mac=mac,
source_type=SOURCE_TYPE_ROUTER,
+ gps_accuracy=accuracy,
attributes=attrs
))
diff --git a/homeassistant/components/device_tracker/tile.py b/homeassistant/components/device_tracker/tile.py
index f27a950a49f..377686b6905 100644
--- a/homeassistant/components/device_tracker/tile.py
+++ b/homeassistant/components/device_tracker/tile.py
@@ -19,7 +19,7 @@ from homeassistant.util.json import load_json, save_json
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['pytile==1.0.0']
+REQUIREMENTS = ['pytile==1.1.0']
CLIENT_UUID_CONFIG_FILE = '.tile.conf'
DEFAULT_ICON = 'mdi:bluetooth'
@@ -29,14 +29,15 @@ ATTR_ALTITUDE = 'altitude'
ATTR_CONNECTION_STATE = 'connection_state'
ATTR_IS_DEAD = 'is_dead'
ATTR_IS_LOST = 'is_lost'
-ATTR_LAST_SEEN = 'last_seen'
-ATTR_LAST_UPDATED = 'last_updated'
ATTR_RING_STATE = 'ring_state'
ATTR_VOIP_STATE = 'voip_state'
+CONF_SHOW_INACTIVE = 'show_inactive'
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_SHOW_INACTIVE, default=False): cv.boolean,
vol.Optional(CONF_MONITORED_VARIABLES):
vol.All(cv.ensure_list, [vol.In(DEVICE_TYPES)]),
})
@@ -79,6 +80,7 @@ class TileDeviceScanner(DeviceScanner):
_LOGGER.debug('Client UUID: %s', self._client.client_uuid)
_LOGGER.debug('User UUID: %s', self._client.user_uuid)
+ self._show_inactive = config.get(CONF_SHOW_INACTIVE)
self._types = config.get(CONF_MONITORED_VARIABLES)
self.devices = {}
@@ -91,29 +93,25 @@ class TileDeviceScanner(DeviceScanner):
def _update_info(self, now=None) -> None:
"""Update the device info."""
- device_data = self._client.get_tiles(type_whitelist=self._types)
+ self.devices = self._client.get_tiles(
+ type_whitelist=self._types, show_inactive=self._show_inactive)
- try:
- self.devices = device_data['result']
- except KeyError:
+ if not self.devices:
_LOGGER.warning('No Tiles found')
- _LOGGER.debug(device_data)
return
- for info in self.devices.values():
- dev_id = 'tile_{0}'.format(slugify(info['name']))
- lat = info['tileState']['latitude']
- lon = info['tileState']['longitude']
+ for dev in self.devices:
+ dev_id = 'tile_{0}'.format(slugify(dev['name']))
+ lat = dev['tileState']['latitude']
+ lon = dev['tileState']['longitude']
attrs = {
- ATTR_ALTITUDE: info['tileState']['altitude'],
- ATTR_CONNECTION_STATE: info['tileState']['connection_state'],
- ATTR_IS_DEAD: info['is_dead'],
- ATTR_IS_LOST: info['tileState']['is_lost'],
- ATTR_LAST_SEEN: info['tileState']['timestamp'],
- ATTR_LAST_UPDATED: device_data['timestamp_ms'],
- ATTR_RING_STATE: info['tileState']['ring_state'],
- ATTR_VOIP_STATE: info['tileState']['voip_state'],
+ ATTR_ALTITUDE: dev['tileState']['altitude'],
+ ATTR_CONNECTION_STATE: dev['tileState']['connection_state'],
+ ATTR_IS_DEAD: dev['is_dead'],
+ ATTR_IS_LOST: dev['tileState']['is_lost'],
+ ATTR_RING_STATE: dev['tileState']['ring_state'],
+ ATTR_VOIP_STATE: dev['tileState']['voip_state'],
}
self.see(
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index 5d362f21cef..dde33aa10a2 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -36,6 +36,7 @@ SERVICE_APPLE_TV = 'apple_tv'
SERVICE_WINK = 'wink'
SERVICE_XIAOMI_GW = 'xiaomi_gw'
SERVICE_TELLDUSLIVE = 'tellstick'
+SERVICE_HUE = 'philips_hue'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@@ -48,7 +49,7 @@ SERVICE_HANDLERS = {
SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None),
- 'philips_hue': ('light', 'hue'),
+ SERVICE_HUE: ('hue', None),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'),
diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py
index e5430555910..7101f4a9527 100644
--- a/homeassistant/components/fan/xiaomi_miio.py
+++ b/homeassistant/components/fan/xiaomi_miio.py
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.3.2']
+REQUIREMENTS = ['python-miio==0.3.3']
ATTR_TEMPERATURE = 'temperature'
ATTR_HUMIDITY = 'humidity'
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 3d669ddc4d1..21900e2265f 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==20171206.0', 'user-agents==1.1.0']
+REQUIREMENTS = ['home-assistant-frontend==20171223.0', 'user-agents==1.1.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log']
@@ -35,7 +35,7 @@ CONF_EXTRA_HTML_URL = 'extra_html_url'
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version'
-JS_DEFAULT_OPTION = 'es5'
+JS_DEFAULT_OPTION = 'auto'
JS_OPTIONS = ['es5', 'latest', 'auto']
DEFAULT_THEME_COLOR = '#03A9F4'
@@ -49,7 +49,7 @@ MANIFEST_JSON = {
'lang': 'en-US',
'name': 'Home Assistant',
'short_name': 'Assistant',
- 'start_url': '/',
+ 'start_url': '/states',
'theme_color': DEFAULT_THEME_COLOR
}
@@ -299,8 +299,13 @@ def async_setup(hass, config):
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
if is_dev:
- hass.http.register_static_path(
- "/home-assistant-polymer", repo_path, False)
+ for subpath in ["src", "build-translations", "build-temp", "build",
+ "hass_frontend", "bower_components", "panels"]:
+ hass.http.register_static_path(
+ "/home-assistant-polymer/{}".format(subpath),
+ os.path.join(repo_path, subpath),
+ False)
+
hass.http.register_static_path(
"/static/translations",
os.path.join(repo_path, "build-translations/output"), False)
diff --git a/homeassistant/components/hive.py b/homeassistant/components/hive.py
index 277800502c1..bf5196d6582 100644
--- a/homeassistant/components/hive.py
+++ b/homeassistant/components/hive.py
@@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL,
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
-REQUIREMENTS = ['pyhiveapi==0.2.5']
+REQUIREMENTS = ['pyhiveapi==0.2.10']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'hive'
diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic/__init__.py
similarity index 79%
rename from homeassistant/components/homematic.py
rename to homeassistant/components/homematic/__init__.py
index 5e8cd3dc58e..08e8455b302 100644
--- a/homeassistant/components/homematic.py
+++ b/homeassistant/components/homematic/__init__.py
@@ -5,25 +5,26 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematic/
"""
import asyncio
-import os
-import logging
from datetime import timedelta
from functools import partial
+import logging
+import os
+import socket
import voluptuous as vol
-import homeassistant.helpers.config_validation as cv
+from homeassistant.config import load_yaml_config_file
from homeassistant.const import (
- EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD,
- CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
+ EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM,
+ CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN)
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
-from homeassistant.helpers.event import track_time_interval
-from homeassistant.config import load_yaml_config_file
-
-REQUIREMENTS = ['pyhomematic==0.1.35']
+import homeassistant.helpers.config_validation as cv
+from homeassistant.loader import bind_hass
+REQUIREMENTS = ['pyhomematic==0.1.36']
DOMAIN = 'homematic'
+_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL_HUB = timedelta(seconds=300)
SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
@@ -41,9 +42,11 @@ ATTR_CHANNEL = 'channel'
ATTR_NAME = 'name'
ATTR_ADDRESS = 'address'
ATTR_VALUE = 'value'
-ATTR_PROXY = 'proxy'
+ATTR_INTERFACE = 'interface'
ATTR_ERRORCODE = 'error'
ATTR_MESSAGE = 'message'
+ATTR_MODE = 'mode'
+ATTR_TIME = 'time'
EVENT_KEYPRESS = 'homematic.keypress'
EVENT_IMPULSE = 'homematic.impulse'
@@ -51,8 +54,9 @@ EVENT_ERROR = 'homematic.error'
SERVICE_VIRTUALKEY = 'virtualkey'
SERVICE_RECONNECT = 'reconnect'
-SERVICE_SET_VAR_VALUE = 'set_var_value'
-SERVICE_SET_DEV_VALUE = 'set_dev_value'
+SERVICE_SET_VARIABLE_VALUE = 'set_variable_value'
+SERVICE_SET_DEVICE_VALUE = 'set_device_value'
+SERVICE_SET_INSTALL_MODE = 'set_install_mode'
HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: [
@@ -73,9 +77,9 @@ HM_DEVICE_TYPES = {
'ThermostatGroup'],
DISCOVER_BINARY_SENSORS: [
'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2',
- 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact',
- 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor',
- 'PresenceIP'],
+ 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor',
+ 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain',
+ 'WiredSensor', 'PresenceIP'],
DISCOVER_COVER: ['Blind', 'KeyBlind']
}
@@ -90,12 +94,14 @@ HM_ATTRIBUTE_SUPPORT = {
'RSSI_DEVICE': ['rssi', {}],
'VALVE_STATE': ['valve', {}],
'BATTERY_STATE': ['battery', {}],
- 'CONTROL_MODE': ['mode', {0: 'Auto',
- 1: 'Manual',
- 2: 'Away',
- 3: 'Boost',
- 4: 'Comfort',
- 5: 'Lowering'}],
+ 'CONTROL_MODE': ['mode', {
+ 0: 'Auto',
+ 1: 'Manual',
+ 2: 'Away',
+ 3: 'Boost',
+ 4: 'Comfort',
+ 5: 'Lowering'
+ }],
'POWER': ['power', {}],
'CURRENT': ['current', {}],
'VOLTAGE': ['voltage', {}],
@@ -114,8 +120,6 @@ HM_IMPULSE_EVENTS = [
'SEQUENCE_OK',
]
-_LOGGER = logging.getLogger(__name__)
-
CONF_RESOLVENAMES_OPTIONS = [
'metadata',
'json',
@@ -124,12 +128,12 @@ CONF_RESOLVENAMES_OPTIONS = [
]
DATA_HOMEMATIC = 'homematic'
-DATA_DEVINIT = 'homematic_devinit'
DATA_STORE = 'homematic_store'
+DATA_CONF = 'homematic_conf'
+CONF_INTERFACES = 'interfaces'
CONF_LOCAL_IP = 'local_ip'
CONF_LOCAL_PORT = 'local_port'
-CONF_IP = 'ip'
CONF_PORT = 'port'
CONF_PATH = 'path'
CONF_CALLBACK_IP = 'callback_ip'
@@ -146,37 +150,35 @@ DEFAULT_PORT = 2001
DEFAULT_PATH = ''
DEFAULT_USERNAME = 'Admin'
DEFAULT_PASSWORD = ''
-DEFAULT_VARIABLES = False
-DEFAULT_DEVICES = True
-DEFAULT_PRIMARY = False
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'homematic',
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_ADDRESS): cv.string,
- vol.Required(ATTR_PROXY): cv.string,
+ vol.Required(ATTR_INTERFACE): cv.string,
vol.Optional(ATTR_CHANNEL, default=1): vol.Coerce(int),
vol.Optional(ATTR_PARAM): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
- vol.Required(CONF_HOSTS): {cv.match_all: {
- vol.Required(CONF_IP): cv.string,
+ vol.Optional(CONF_INTERFACES, default={}): {cv.match_all: {
+ vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string,
- vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
- vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
- vol.Optional(CONF_VARIABLES, default=DEFAULT_VARIABLES):
- cv.boolean,
vol.Optional(CONF_RESOLVENAMES, default=DEFAULT_RESOLVENAMES):
vol.In(CONF_RESOLVENAMES_OPTIONS),
- vol.Optional(CONF_DEVICES, default=DEFAULT_DEVICES): cv.boolean,
- vol.Optional(CONF_PRIMARY, default=DEFAULT_PRIMARY): cv.boolean,
+ vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
+ vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_CALLBACK_IP): cv.string,
vol.Optional(CONF_CALLBACK_PORT): cv.port,
}},
+ vol.Optional(CONF_HOSTS, default={}): {cv.match_all: {
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string,
+ vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
+ }},
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
}),
@@ -186,61 +188,88 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): cv.string,
- vol.Optional(ATTR_PROXY): cv.string,
+ vol.Optional(ATTR_INTERFACE): cv.string,
})
-SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({
+SCHEMA_SERVICE_SET_VARIABLE_VALUE = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.match_all,
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
})
-SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({
+SCHEMA_SERVICE_SET_DEVICE_VALUE = vol.Schema({
vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_CHANNEL): vol.Coerce(int),
vol.Required(ATTR_PARAM): vol.All(cv.string, vol.Upper),
vol.Required(ATTR_VALUE): cv.match_all,
- vol.Optional(ATTR_PROXY): cv.string,
+ vol.Optional(ATTR_INTERFACE): cv.string,
})
SCHEMA_SERVICE_RECONNECT = vol.Schema({})
+SCHEMA_SERVICE_SET_INSTALL_MODE = vol.Schema({
+ vol.Required(ATTR_INTERFACE): cv.string,
+ vol.Optional(ATTR_TIME, default=60): cv.positive_int,
+ vol.Optional(ATTR_MODE, default=1):
+ vol.All(vol.Coerce(int), vol.In([1, 2])),
+ vol.Optional(ATTR_ADDRESS): vol.All(cv.string, vol.Upper),
+})
-def virtualkey(hass, address, channel, param, proxy=None):
+
+@bind_hass
+def virtualkey(hass, address, channel, param, interface=None):
"""Send virtual keypress to homematic controlller."""
data = {
ATTR_ADDRESS: address,
ATTR_CHANNEL: channel,
ATTR_PARAM: param,
- ATTR_PROXY: proxy,
+ ATTR_INTERFACE: interface,
}
hass.services.call(DOMAIN, SERVICE_VIRTUALKEY, data)
-def set_var_value(hass, entity_id, value):
+@bind_hass
+def set_variable_value(hass, entity_id, value):
"""Change value of a Homematic system variable."""
data = {
ATTR_ENTITY_ID: entity_id,
ATTR_VALUE: value,
}
- hass.services.call(DOMAIN, SERVICE_SET_VAR_VALUE, data)
+ hass.services.call(DOMAIN, SERVICE_SET_VARIABLE_VALUE, data)
-def set_dev_value(hass, address, channel, param, value, proxy=None):
- """Call setValue XML-RPC method of supplied proxy."""
+@bind_hass
+def set_device_value(hass, address, channel, param, value, interface=None):
+ """Call setValue XML-RPC method of supplied interface."""
data = {
ATTR_ADDRESS: address,
ATTR_CHANNEL: channel,
ATTR_PARAM: param,
ATTR_VALUE: value,
- ATTR_PROXY: proxy,
+ ATTR_INTERFACE: interface,
}
- hass.services.call(DOMAIN, SERVICE_SET_DEV_VALUE, data)
+ hass.services.call(DOMAIN, SERVICE_SET_DEVICE_VALUE, data)
+@bind_hass
+def set_install_mode(hass, interface, mode=None, time=None, address=None):
+ """Call setInstallMode XML-RPC method of supplied inteface."""
+ data = {
+ key: value for key, value in (
+ (ATTR_INTERFACE, interface),
+ (ATTR_MODE, mode),
+ (ATTR_TIME, time),
+ (ATTR_ADDRESS, address)
+ ) if value
+ }
+
+ hass.services.call(DOMAIN, SERVICE_SET_INSTALL_MODE, data)
+
+
+@bind_hass
def reconnect(hass):
"""Reconnect to CCU/Homegear."""
hass.services.call(DOMAIN, SERVICE_RECONNECT, {})
@@ -250,31 +279,32 @@ def setup(hass, config):
"""Set up the Homematic component."""
from pyhomematic import HMConnection
- hass.data[DATA_DEVINIT] = {}
+ conf = config[DOMAIN]
+ hass.data[DATA_CONF] = remotes = {}
hass.data[DATA_STORE] = set()
# Create hosts-dictionary for pyhomematic
- remotes = {}
- hosts = {}
- for rname, rconfig in config[DOMAIN][CONF_HOSTS].items():
- server = rconfig.get(CONF_IP)
+ for rname, rconfig in conf[CONF_INTERFACES].items():
+ remotes[rname] = {
+ 'ip': socket.gethostbyname(rconfig.get(CONF_HOST)),
+ 'port': rconfig.get(CONF_PORT),
+ 'path': rconfig.get(CONF_PATH),
+ 'resolvenames': rconfig.get(CONF_RESOLVENAMES),
+ 'username': rconfig.get(CONF_USERNAME),
+ 'password': rconfig.get(CONF_PASSWORD),
+ 'callbackip': rconfig.get(CONF_CALLBACK_IP),
+ 'callbackport': rconfig.get(CONF_CALLBACK_PORT),
+ 'connect': True,
+ }
- remotes[rname] = {}
- remotes[rname][CONF_IP] = server
- remotes[rname][CONF_PORT] = rconfig.get(CONF_PORT)
- remotes[rname][CONF_PATH] = rconfig.get(CONF_PATH)
- remotes[rname][CONF_RESOLVENAMES] = rconfig.get(CONF_RESOLVENAMES)
- remotes[rname][CONF_USERNAME] = rconfig.get(CONF_USERNAME)
- remotes[rname][CONF_PASSWORD] = rconfig.get(CONF_PASSWORD)
- remotes[rname]['callbackip'] = rconfig.get(CONF_CALLBACK_IP)
- remotes[rname]['callbackport'] = rconfig.get(CONF_CALLBACK_PORT)
-
- if server not in hosts or rconfig.get(CONF_PRIMARY):
- hosts[server] = {
- CONF_VARIABLES: rconfig.get(CONF_VARIABLES),
- CONF_NAME: rname,
- }
- hass.data[DATA_DEVINIT][rname] = rconfig.get(CONF_DEVICES)
+ for sname, sconfig in conf[CONF_HOSTS].items():
+ remotes[sname] = {
+ 'ip': socket.gethostbyname(sconfig.get(CONF_HOST)),
+ 'port': DEFAULT_PORT,
+ 'username': sconfig.get(CONF_USERNAME),
+ 'password': sconfig.get(CONF_PASSWORD),
+ 'connect': False,
+ }
# Create server thread
bound_system_callback = partial(_system_callback_handler, hass, config)
@@ -295,9 +325,8 @@ def setup(hass, config):
# Init homematic hubs
entity_hubs = []
- for _, hub_data in hosts.items():
- entity_hubs.append(HMHub(
- hass, homematic, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
+ for hub_name in conf[CONF_HOSTS].keys():
+ entity_hubs.append(HMHub(hass, homematic, hub_name))
# Register HomeMatic services
descriptions = load_yaml_config_file(
@@ -331,8 +360,7 @@ def setup(hass, config):
hass.services.register(
DOMAIN, SERVICE_VIRTUALKEY, _hm_service_virtualkey,
- descriptions[DOMAIN][SERVICE_VIRTUALKEY],
- schema=SCHEMA_SERVICE_VIRTUALKEY)
+ descriptions[SERVICE_VIRTUALKEY], schema=SCHEMA_SERVICE_VIRTUALKEY)
def _service_handle_value(service):
"""Service to call setValue method for HomeMatic system variable."""
@@ -354,9 +382,9 @@ def setup(hass, config):
hub.hm_set_variable(name, value)
hass.services.register(
- DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value,
- descriptions[DOMAIN][SERVICE_SET_VAR_VALUE],
- schema=SCHEMA_SERVICE_SET_VAR_VALUE)
+ DOMAIN, SERVICE_SET_VARIABLE_VALUE, _service_handle_value,
+ descriptions[SERVICE_SET_VARIABLE_VALUE],
+ schema=SCHEMA_SERVICE_SET_VARIABLE_VALUE)
def _service_handle_reconnect(service):
"""Service to reconnect all HomeMatic hubs."""
@@ -364,8 +392,7 @@ def setup(hass, config):
hass.services.register(
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
- descriptions[DOMAIN][SERVICE_RECONNECT],
- schema=SCHEMA_SERVICE_RECONNECT)
+ descriptions[SERVICE_RECONNECT], schema=SCHEMA_SERVICE_RECONNECT)
def _service_handle_device(service):
"""Service to call setValue method for HomeMatic devices."""
@@ -383,9 +410,23 @@ def setup(hass, config):
hmdevice.setValue(param, value, channel)
hass.services.register(
- DOMAIN, SERVICE_SET_DEV_VALUE, _service_handle_device,
- descriptions[DOMAIN][SERVICE_SET_DEV_VALUE],
- schema=SCHEMA_SERVICE_SET_DEV_VALUE)
+ DOMAIN, SERVICE_SET_DEVICE_VALUE, _service_handle_device,
+ descriptions[SERVICE_SET_DEVICE_VALUE],
+ schema=SCHEMA_SERVICE_SET_DEVICE_VALUE)
+
+ def _service_handle_install_mode(service):
+ """Service to set interface into install mode."""
+ interface = service.data.get(ATTR_INTERFACE)
+ mode = service.data.get(ATTR_MODE)
+ time = service.data.get(ATTR_TIME)
+ address = service.data.get(ATTR_ADDRESS)
+
+ homematic.setInstallMode(interface, t=time, mode=mode, address=address)
+
+ hass.services.register(
+ DOMAIN, SERVICE_SET_INSTALL_MODE, _service_handle_install_mode,
+ descriptions[SERVICE_SET_INSTALL_MODE],
+ schema=SCHEMA_SERVICE_SET_INSTALL_MODE)
return True
@@ -395,10 +436,10 @@ def _system_callback_handler(hass, config, src, *args):
# New devices available at hub
if src == 'newDevices':
(interface_id, dev_descriptions) = args
- proxy = interface_id.split('-')[-1]
+ interface = interface_id.split('-')[-1]
# Device support active?
- if not hass.data[DATA_DEVINIT][proxy]:
+ if not hass.data[DATA_CONF][interface]['connect']:
return
addresses = []
@@ -410,9 +451,9 @@ def _system_callback_handler(hass, config, src, *args):
# Register EVENTS
# Search all devices with an EVENTNODE that includes data
- bound_event_callback = partial(_hm_event_handler, hass, proxy)
+ bound_event_callback = partial(_hm_event_handler, hass, interface)
for dev in addresses:
- hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(dev)
+ hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(dev)
if hmdevice.EVENTNODE:
hmdevice.setEventCallback(
@@ -429,7 +470,7 @@ def _system_callback_handler(hass, config, src, *args):
('climate', DISCOVER_CLIMATE)):
# Get all devices of a specific type
found_devices = _get_devices(
- hass, discovery_type, addresses, proxy)
+ hass, discovery_type, addresses, interface)
# When devices of this type are found
# they are setup in HASS and an discovery event is fired
@@ -448,12 +489,12 @@ def _system_callback_handler(hass, config, src, *args):
})
-def _get_devices(hass, discovery_type, keys, proxy):
+def _get_devices(hass, discovery_type, keys, interface):
"""Get the HomeMatic devices for given discovery_type."""
device_arr = []
for key in keys:
- device = hass.data[DATA_HOMEMATIC].devices[proxy][key]
+ device = hass.data[DATA_HOMEMATIC].devices[interface][key]
class_name = device.__class__.__name__
metadata = {}
@@ -485,7 +526,7 @@ def _get_devices(hass, discovery_type, keys, proxy):
device_dict = {
CONF_PLATFORM: "homematic",
ATTR_ADDRESS: key,
- ATTR_PROXY: proxy,
+ ATTR_INTERFACE: interface,
ATTR_NAME: name,
ATTR_CHANNEL: channel
}
@@ -521,12 +562,12 @@ def _create_ha_name(name, channel, param, count):
return "{} {} {}".format(name, channel, param)
-def _hm_event_handler(hass, proxy, device, caller, attribute, value):
+def _hm_event_handler(hass, interface, device, caller, attribute, value):
"""Handle all pyhomematic device events."""
try:
channel = int(device.split(":")[1])
address = device.split(":")[0]
- hmdevice = hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
+ hmdevice = hass.data[DATA_HOMEMATIC].devices[interface].get(address)
except (TypeError, ValueError):
_LOGGER.error("Event handling channel convert error!")
return
@@ -561,14 +602,14 @@ def _hm_event_handler(hass, proxy, device, caller, attribute, value):
def _device_from_servicecall(hass, service):
"""Extract HomeMatic device from service call."""
address = service.data.get(ATTR_ADDRESS)
- proxy = service.data.get(ATTR_PROXY)
+ interface = service.data.get(ATTR_INTERFACE)
if address == 'BIDCOS-RF':
address = 'BidCoS-RF'
- if proxy:
- return hass.data[DATA_HOMEMATIC].devices[proxy].get(address)
+ if interface:
+ return hass.data[DATA_HOMEMATIC].devices[interface].get(address)
- for _, devices in hass.data[DATA_HOMEMATIC].devices.items():
+ for devices in hass.data[DATA_HOMEMATIC].devices.values():
if address in devices:
return devices[address]
@@ -576,25 +617,23 @@ def _device_from_servicecall(hass, service):
class HMHub(Entity):
"""The HomeMatic hub. (CCU2/HomeGear)."""
- def __init__(self, hass, homematic, name, use_variables):
+ def __init__(self, hass, homematic, name):
"""Initialize HomeMatic hub."""
self.hass = hass
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
self._homematic = homematic
self._variables = {}
self._name = name
- self._state = STATE_UNKNOWN
- self._use_variables = use_variables
+ self._state = None
# Load data
- track_time_interval(
- self.hass, self._update_hub, SCAN_INTERVAL_HUB)
+ self.hass.helpers.event.track_time_interval(
+ self._update_hub, SCAN_INTERVAL_HUB)
self.hass.add_job(self._update_hub, None)
- if self._use_variables:
- track_time_interval(
- self.hass, self._update_variables, SCAN_INTERVAL_VARIABLES)
- self.hass.add_job(self._update_variables, None)
+ self.hass.helpers.event.track_time_interval(
+ self._update_variables, SCAN_INTERVAL_VARIABLES)
+ self.hass.add_job(self._update_variables, None)
@property
def name(self):
@@ -672,7 +711,7 @@ class HMDevice(Entity):
"""Initialize a generic HomeMatic device."""
self._name = config.get(ATTR_NAME)
self._address = config.get(ATTR_ADDRESS)
- self._proxy = config.get(ATTR_PROXY)
+ self._interface = config.get(ATTR_INTERFACE)
self._channel = config.get(ATTR_CHANNEL)
self._state = config.get(ATTR_PARAM)
self._data = {}
@@ -700,11 +739,6 @@ class HMDevice(Entity):
"""Return the name of the device."""
return self._name
- @property
- def assumed_state(self):
- """Return true if unable to access real state of the device."""
- return not self._available
-
@property
def available(self):
"""Return true if device is available."""
@@ -715,10 +749,6 @@ class HMDevice(Entity):
"""Return device specific state attributes."""
attr = {}
- # No data available
- if not self.available:
- return attr
-
# Generate a dictionary with attributes
for node, data in HM_ATTRIBUTE_SUPPORT.items():
# Is an attribute and exists for this object
@@ -728,7 +758,7 @@ class HMDevice(Entity):
# Static attributes
attr['id'] = self._hmdevice.ADDRESS
- attr['proxy'] = self._proxy
+ attr['interface'] = self._interface
return attr
@@ -739,7 +769,8 @@ class HMDevice(Entity):
# Initialize
self._homematic = self.hass.data[DATA_HOMEMATIC]
- self._hmdevice = self._homematic.devices[self._proxy][self._address]
+ self._hmdevice = \
+ self._homematic.devices[self._interface][self._address]
self._connected = True
try:
@@ -773,6 +804,9 @@ class HMDevice(Entity):
if attribute == 'UNREACH':
self._available = bool(value)
has_changed = True
+ elif not self.available:
+ self._available = False
+ has_changed = True
# If it has changed data point, update HASS
if has_changed:
diff --git a/homeassistant/components/homematic/services.yaml b/homeassistant/components/homematic/services.yaml
new file mode 100644
index 00000000000..bf4d99af9e7
--- /dev/null
+++ b/homeassistant/components/homematic/services.yaml
@@ -0,0 +1,68 @@
+# Describes the format for available component services
+
+virtualkey:
+ description: Press a virtual key from CCU/Homegear or simulate keypress.
+ fields:
+ address:
+ description: Address of homematic device or BidCoS-RF for virtual remote.
+ example: BidCoS-RF
+ channel:
+ description: Channel for calling a keypress.
+ example: 1
+ param:
+ description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
+ example: PRESS_LONG
+ interface:
+ description: (Optional) for set a interface value.
+ example: Interfaces name from config
+
+set_variable_value:
+ description: Set the name of a node.
+ fields:
+ entity_id:
+ description: Name(s) of homematic central to set value.
+ example: 'homematic.ccu2'
+ name:
+ description: Name of the variable to set.
+ example: 'testvariable'
+ value:
+ description: New value
+ example: 1
+
+set_device_value:
+ description: Set a device property on RPC XML interface.
+ fields:
+ address:
+ description: Address of homematic device or BidCoS-RF for virtual remote
+ example: BidCoS-RF
+ channel:
+ description: Channel for calling a keypress
+ example: 1
+ param:
+ description: Event to send i.e. PRESS_LONG, PRESS_SHORT
+ example: PRESS_LONG
+ interface:
+ description: (Optional) for set a interface value
+ example: Interfaces name from config
+ value:
+ description: New value
+ example: 1
+
+reconnect:
+ description: Reconnect to all Homematic Hubs.
+
+set_install_mode:
+ description: Set a RPC XML interface into installation mode.
+ fields:
+ interface:
+ description: Select the given interface into install mode
+ example: Interfaces name from config
+ mode:
+ description: (Default 1) 1= Normal mode / 2= Remove exists old links
+ example: 1
+ time:
+ description: (Default 60) Time in seconds to run in install mode
+ example: 1
+ address:
+ description: (Optional) Address of homematic device or BidCoS-RF to learn
+ example: LEQ3948571
diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py
new file mode 100644
index 00000000000..6147f706658
--- /dev/null
+++ b/homeassistant/components/hue.py
@@ -0,0 +1,246 @@
+"""
+This component provides basic support for the Philips Hue system.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/hue/
+"""
+import json
+import logging
+import os
+import socket
+
+import voluptuous as vol
+
+from homeassistant.components.discovery import SERVICE_HUE
+from homeassistant.config import load_yaml_config_file
+from homeassistant.const import CONF_FILENAME, CONF_HOST
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers import discovery
+
+REQUIREMENTS = ['phue==1.0']
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = "hue"
+SERVICE_HUE_SCENE = "hue_activate_scene"
+
+CONF_BRIDGES = "bridges"
+
+CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
+DEFAULT_ALLOW_UNREACHABLE = False
+
+PHUE_CONFIG_FILE = 'phue.conf'
+
+CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
+DEFAULT_ALLOW_IN_EMULATED_HUE = True
+
+CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
+DEFAULT_ALLOW_HUE_GROUPS = True
+
+BRIDGE_CONFIG_SCHEMA = vol.Schema([{
+ vol.Optional(CONF_HOST): cv.string,
+ vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string,
+ vol.Optional(CONF_ALLOW_UNREACHABLE,
+ default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean,
+ vol.Optional(CONF_ALLOW_IN_EMULATED_HUE,
+ default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean,
+ vol.Optional(CONF_ALLOW_HUE_GROUPS,
+ default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
+}])
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Optional(CONF_BRIDGES, default=[]): BRIDGE_CONFIG_SCHEMA,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+ATTR_GROUP_NAME = "group_name"
+ATTR_SCENE_NAME = "scene_name"
+SCENE_SCHEMA = vol.Schema({
+ vol.Required(ATTR_GROUP_NAME): cv.string,
+ vol.Required(ATTR_SCENE_NAME): cv.string,
+})
+
+CONFIG_INSTRUCTIONS = """
+Press the button on the bridge to register Philips Hue with Home Assistant.
+
+
+"""
+
+
+def setup(hass, config):
+ """Set up the Hue platform."""
+ config = config.get(DOMAIN)
+ if config is None:
+ config = {}
+
+ if DOMAIN not in hass.data:
+ hass.data[DOMAIN] = {}
+
+ discovery.listen(
+ hass,
+ SERVICE_HUE,
+ lambda service, discovery_info:
+ bridge_discovered(hass, service, discovery_info))
+
+ bridges = config.get(CONF_BRIDGES, [])
+ for bridge in bridges:
+ filename = bridge.get(CONF_FILENAME)
+ allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE)
+ allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE)
+ allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS)
+
+ host = bridge.get(CONF_HOST)
+
+ if host is None:
+ host = _find_host_from_config(hass, filename)
+
+ if host is None:
+ _LOGGER.error("No host found in configuration")
+ return False
+
+ setup_bridge(host, hass, filename, allow_unreachable,
+ allow_in_emulated_hue, allow_hue_groups)
+
+ return True
+
+
+def bridge_discovered(hass, service, discovery_info):
+ """Dispatcher for Hue discovery events."""
+ if "HASS Bridge" in discovery_info.get('name', ''):
+ return
+
+ host = discovery_info.get('host')
+ serial = discovery_info.get('serial')
+
+ filename = 'phue-{}.conf'.format(serial)
+ setup_bridge(host, hass, filename)
+
+
+def setup_bridge(host, hass, filename=None, allow_unreachable=False,
+ allow_in_emulated_hue=True, allow_hue_groups=True):
+ """Set up a given Hue bridge."""
+ # Only register a device once
+ if socket.gethostbyname(host) in hass.data[DOMAIN]:
+ return
+
+ bridge = HueBridge(host, hass, filename, allow_unreachable,
+ allow_in_emulated_hue, allow_hue_groups)
+ bridge.setup()
+
+
+def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
+ """Attempt to detect host based on existing configuration."""
+ path = hass.config.path(filename)
+
+ if not os.path.isfile(path):
+ return None
+
+ try:
+ with open(path) as inp:
+ return next(iter(json.load(inp).keys()))
+ except (ValueError, AttributeError, StopIteration):
+ # ValueError if can't parse as JSON
+ # AttributeError if JSON value is not a dict
+ # StopIteration if no keys
+ return None
+
+
+class HueBridge(object):
+ """Manages a single Hue bridge."""
+
+ def __init__(self, host, hass, filename, allow_unreachable=False,
+ allow_in_emulated_hue=True, allow_hue_groups=True):
+ """Initialize the system."""
+ self.host = host
+ self.hass = hass
+ self.filename = filename
+ self.allow_unreachable = allow_unreachable
+ self.allow_in_emulated_hue = allow_in_emulated_hue
+ self.allow_hue_groups = allow_hue_groups
+
+ self.bridge = None
+ self.lights = {}
+ self.lightgroups = {}
+
+ self.configured = False
+ self.config_request_id = None
+
+ hass.data[DOMAIN][socket.gethostbyname(host)] = self
+
+ def setup(self):
+ """Set up a phue bridge based on host parameter."""
+ import phue
+
+ try:
+ self.bridge = phue.Bridge(
+ self.host,
+ config_file_path=self.hass.config.path(self.filename))
+ except ConnectionRefusedError: # Wrong host was given
+ _LOGGER.error("Error connecting to the Hue bridge at %s",
+ self.host)
+ return
+ except phue.PhueRegistrationException:
+ _LOGGER.warning("Connected to Hue at %s but not registered.",
+ self.host)
+ self.request_configuration()
+ return
+
+ # If we came here and configuring this host, mark as done
+ if self.config_request_id:
+ request_id = self.config_request_id
+ self.config_request_id = None
+ configurator = self.hass.components.configurator
+ configurator.request_done(request_id)
+
+ self.configured = True
+
+ discovery.load_platform(
+ self.hass, 'light', DOMAIN,
+ {'bridge_id': socket.gethostbyname(self.host)})
+
+ # create a service for calling run_scene directly on the bridge,
+ # used to simplify automation rules.
+ def hue_activate_scene(call):
+ """Service to call directly into bridge to set scenes."""
+ group_name = call.data[ATTR_GROUP_NAME]
+ scene_name = call.data[ATTR_SCENE_NAME]
+ self.bridge.run_scene(group_name, scene_name)
+
+ descriptions = load_yaml_config_file(
+ os.path.join(os.path.dirname(__file__), 'services.yaml'))
+ self.hass.services.register(
+ DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
+ descriptions.get(SERVICE_HUE_SCENE),
+ schema=SCENE_SCHEMA)
+
+ def request_configuration(self):
+ """Request configuration steps from the user."""
+ configurator = self.hass.components.configurator
+
+ # We got an error if this method is called while we are configuring
+ if self.config_request_id:
+ configurator.notify_errors(
+ self.config_request_id,
+ "Failed to register, please try again.")
+ return
+
+ self.config_request_id = configurator.request_config(
+ "Philips Hue",
+ lambda data: self.setup(),
+ description=CONFIG_INSTRUCTIONS,
+ entity_picture="/static/images/logo_philips_hue.png",
+ submit_caption="I have pressed the button"
+ )
+
+ def get_api(self):
+ """Return the full api dictionary from phue."""
+ return self.bridge.get_api()
+
+ def set_light(self, light_id, command):
+ """Adjust properties of one or more lights. See phue for details."""
+ return self.bridge.set_light(light_id, command)
+
+ def set_group(self, light_id, command):
+ """Change light settings for a group. See phue for detail."""
+ return self.bridge.set_group(light_id, command)
diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py
index 7686eb7dc7d..af1846c7bf8 100644
--- a/homeassistant/components/isy994.py
+++ b/homeassistant/components/isy994.py
@@ -4,6 +4,7 @@ Support the ISY-994 controllers.
For configuration details please visit the documentation for this component at
https://home-assistant.io/components/isy994/
"""
+import asyncio
from collections import namedtuple
import logging
from urllib.parse import urlparse
@@ -17,7 +18,7 @@ from homeassistant.helpers import discovery, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType, Dict # noqa
-REQUIREMENTS = ['PyISY==1.0.8']
+REQUIREMENTS = ['PyISY==1.1.0']
_LOGGER = logging.getLogger(__name__)
@@ -91,6 +92,34 @@ def filter_nodes(nodes: list, units: list=None, states: list=None) -> list:
return filtered_nodes
+def _is_node_a_sensor(node, path: str, sensor_identifier: str) -> bool:
+ """Determine if the given node is a sensor."""
+ if not isinstance(node, PYISY.Nodes.Node):
+ return False
+
+ if sensor_identifier in path or sensor_identifier in node.name:
+ return True
+
+ # This method is most reliable but only works on 5.x firmware
+ try:
+ if node.node_def_id == 'BinaryAlarm':
+ return True
+ except AttributeError:
+ pass
+
+ # This method works on all firmwares, but only for Insteon devices
+ try:
+ device_type = node.type
+ except AttributeError:
+ # Node has no type; most likely not an Insteon device
+ pass
+ else:
+ split_type = device_type.split('.')
+ return split_type[0] == '16' # 16 represents Insteon binary sensors
+
+ return False
+
+
def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
"""Categorize the ISY994 nodes."""
global SENSOR_NODES
@@ -106,7 +135,7 @@ def _categorize_nodes(hidden_identifier: str, sensor_identifier: str) -> None:
hidden = hidden_identifier in path or hidden_identifier in node.name
if hidden:
node.name += hidden_identifier
- if sensor_identifier in path or sensor_identifier in node.name:
+ if _is_node_a_sensor(node, path, sensor_identifier):
SENSOR_NODES.append(node)
elif isinstance(node, PYISY.Nodes.Node):
NODES.append(node)
@@ -227,15 +256,31 @@ class ISYDevice(Entity):
def __init__(self, node) -> None:
"""Initialize the insteon device."""
self._node = node
+ self._change_handler = None
+ self._control_handler = None
+ @asyncio.coroutine
+ def async_added_to_hass(self) -> None:
+ """Subscribe to the node change events."""
self._change_handler = self._node.status.subscribe(
'changed', self.on_update)
+ if hasattr(self._node, 'controlEvents'):
+ self._control_handler = self._node.controlEvents.subscribe(
+ self.on_control)
+
# pylint: disable=unused-argument
def on_update(self, event: object) -> None:
"""Handle the update event from the ISY994 Node."""
self.schedule_update_ha_state()
+ def on_control(self, event: object) -> None:
+ """Handle a control event from the ISY994 Node."""
+ self.hass.bus.fire('isy994_control', {
+ 'entity_id': self.entity_id,
+ 'control': event
+ })
+
@property
def domain(self) -> str:
"""Get the domain of the device."""
@@ -270,6 +315,21 @@ class ISYDevice(Entity):
# pylint: disable=protected-access
return self._node.status._val
+ def is_unknown(self) -> bool:
+ """Get whether or not the value of this Entity's node is unknown.
+
+ PyISY reports unknown values as -inf
+ """
+ return self.value == -1 * float('inf')
+
+ @property
+ def state(self):
+ """Return the state of the ISY device."""
+ if self.is_unknown():
+ return None
+ else:
+ return super().state
+
@property
def device_state_attributes(self) -> Dict:
"""Get the state attributes for the device."""
diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py
index 5a81f6d2a9e..d737c555873 100644
--- a/homeassistant/components/keyboard_remote.py
+++ b/homeassistant/components/keyboard_remote.py
@@ -21,7 +21,7 @@ REQUIREMENTS = ['evdev==0.6.1']
_LOGGER = logging.getLogger(__name__)
DEVICE_DESCRIPTOR = 'device_descriptor'
-DEVICE_ID_GROUP = 'Device descriptor or name'
+DEVICE_ID_GROUP = 'Device description'
DEVICE_NAME = 'device_name'
DOMAIN = 'keyboard_remote'
@@ -36,12 +36,13 @@ KEYBOARD_REMOTE_DISCONNECTED = 'keyboard_remote_disconnected'
TYPE = 'type'
CONFIG_SCHEMA = vol.Schema({
- DOMAIN: vol.Schema({
- vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string,
- vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string,
- vol.Optional(TYPE, default='key_up'):
- vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold')),
- }),
+ DOMAIN:
+ vol.All(cv.ensure_list, [vol.Schema({
+ vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string,
+ vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string,
+ vol.Optional(TYPE, default='key_up'):
+ vol.All(cv.string, vol.Any('key_up', 'key_down', 'key_hold'))
+ })])
}, extra=vol.ALLOW_EXTRA)
@@ -49,11 +50,6 @@ def setup(hass, config):
"""Set up the keyboard_remote."""
config = config.get(DOMAIN)
- if not config.get(DEVICE_DESCRIPTOR) and\
- not config.get(DEVICE_NAME):
- _LOGGER.error("No device_descriptor or device_name found")
- return
-
keyboard_remote = KeyboardRemote(
hass,
config
@@ -63,7 +59,7 @@ def setup(hass, config):
keyboard_remote.run()
def _stop_keyboard_remote(_event):
- keyboard_remote.stopped.set()
+ keyboard_remote.stop()
hass.bus.listen_once(
EVENT_HOMEASSISTANT_START,
@@ -77,19 +73,21 @@ def setup(hass, config):
return True
-class KeyboardRemote(threading.Thread):
+class KeyboardRemoteThread(threading.Thread):
"""This interfaces with the inputdevice using evdev."""
- def __init__(self, hass, config):
- """Construct a KeyboardRemote interface object."""
- from evdev import InputDevice, list_devices
+ def __init__(self, hass, device_name, device_descriptor, key_value):
+ """Construct a thread listening for events on one device."""
+ self.hass = hass
+ self.device_name = device_name
+ self.device_descriptor = device_descriptor
+ self.key_value = key_value
- self.device_descriptor = config.get(DEVICE_DESCRIPTOR)
- self.device_name = config.get(DEVICE_NAME)
if self.device_descriptor:
self.device_id = self.device_descriptor
else:
self.device_id = self.device_name
+
self.dev = self._get_keyboard_device()
if self.dev is not None:
_LOGGER.debug("Keyboard connected, %s", self.device_id)
@@ -103,6 +101,7 @@ class KeyboardRemote(threading.Thread):
id_folder = '/dev/input/by-id/'
if os.path.isdir(id_folder):
+ from evdev import InputDevice, list_devices
device_names = [InputDevice(file_name).name
for file_name in list_devices()]
_LOGGER.debug(
@@ -116,7 +115,6 @@ class KeyboardRemote(threading.Thread):
threading.Thread.__init__(self)
self.stopped = threading.Event()
self.hass = hass
- self.key_value = KEY_VALUE.get(config.get(TYPE, 'key_up'))
def _get_keyboard_device(self):
"""Get the keyboard device."""
@@ -145,7 +143,7 @@ class KeyboardRemote(threading.Thread):
while not self.stopped.isSet():
# Sleeps to ease load on processor
- time.sleep(.1)
+ time.sleep(.05)
if self.dev is None:
self.dev = self._get_keyboard_device()
@@ -178,3 +176,32 @@ class KeyboardRemote(threading.Thread):
KEYBOARD_REMOTE_COMMAND_RECEIVED,
{KEY_CODE: event.code}
)
+
+
+class KeyboardRemote(object):
+ """Sets up one thread per device."""
+
+ def __init__(self, hass, config):
+ """Construct a KeyboardRemote interface object."""
+ self.threads = []
+ for dev_block in config:
+ device_descriptor = dev_block.get(DEVICE_DESCRIPTOR)
+ device_name = dev_block.get(DEVICE_NAME)
+ key_value = KEY_VALUE.get(dev_block.get(TYPE, 'key_up'))
+
+ if device_descriptor is not None\
+ or device_name is not None:
+ thread = KeyboardRemoteThread(hass, device_name,
+ device_descriptor,
+ key_value)
+ self.threads.append(thread)
+
+ def run(self):
+ """Run all event listener threads."""
+ for thread in self.threads:
+ thread.start()
+
+ def stop(self):
+ """Stop all event listener threads."""
+ for thread in self.threads:
+ thread.stopped.set()
diff --git a/homeassistant/components/light/greenwave.py b/homeassistant/components/light/greenwave.py
new file mode 100644
index 00000000000..0e99a49eaa9
--- /dev/null
+++ b/homeassistant/components/light/greenwave.py
@@ -0,0 +1,112 @@
+"""
+Support for Greenwave Reality (TCP Connected) lights.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/light.greenwave/
+"""
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.light import (
+ ATTR_BRIGHTNESS, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS)
+from homeassistant.const import CONF_HOST
+import homeassistant.helpers.config_validation as cv
+
+SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS)
+
+REQUIREMENTS = ['greenwavereality==0.2.9']
+_LOGGER = logging.getLogger(__name__)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required("version"): cv.positive_int,
+})
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Setup Greenwave Reality Platform."""
+ import greenwavereality as greenwave
+ import os
+ host = config.get(CONF_HOST)
+ tokenfile = hass.config.path('.greenwave')
+ if config.get("version") == 3:
+ if os.path.exists(tokenfile):
+ tokenfile = open(tokenfile)
+ token = tokenfile.read()
+ tokenfile.close()
+ else:
+ token = greenwave.grab_token(host, 'hass', 'homeassistant')
+ tokenfile = open(tokenfile, "w+")
+ tokenfile.write(token)
+ tokenfile.close()
+ else:
+ token = None
+ doc = greenwave.grab_xml(host, token)
+ add_devices(GreenwaveLight(device, host, token) for device in doc)
+
+
+class GreenwaveLight(Light):
+ """Representation of an Greenwave Reality Light."""
+
+ def __init__(self, light, host, token):
+ """Initialize a Greenwave Reality Light."""
+ import greenwavereality as greenwave
+ self._did = light['did']
+ self._name = light['name']
+ self._state = int(light['state'])
+ self._brightness = greenwave.hass_brightness(light)
+ self._host = host
+ self._online = greenwave.check_online(light)
+ self.token = token
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return SUPPORTED_FEATURES
+
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._online
+
+ @property
+ def name(self):
+ """Return the display name of this light."""
+ return self._name
+
+ @property
+ def brightness(self):
+ """Return the brightness of the light."""
+ return self._brightness
+
+ @property
+ def is_on(self):
+ """Return true if light is on."""
+ return self._state
+
+ def turn_on(self, **kwargs):
+ """Instruct the light to turn on."""
+ import greenwavereality as greenwave
+ temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255)
+ / 255) * 100)
+ greenwave.set_brightness(self._host, self._did,
+ temp_brightness, self.token)
+ greenwave.turn_on(self._host, self._did, self.token)
+
+ def turn_off(self, **kwargs):
+ """Instruct the light to turn off."""
+ import greenwavereality as greenwave
+ greenwave.turn_off(self._host, self._did, self.token)
+
+ def update(self):
+ """Fetch new state data for this light."""
+ import greenwavereality as greenwave
+ doc = greenwave.grab_xml(self._host, self.token)
+
+ for device in doc:
+ if device['did'] == self._did:
+ self._state = int(device['state'])
+ self._brightness = greenwave.hass_brightness(device)
+ self._online = greenwave.check_online(device)
+ self._name = device['name']
diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py
index 95bd0b6988d..3356d637be8 100644
--- a/homeassistant/components/light/hive.py
+++ b/homeassistant/components/light/hive.py
@@ -4,8 +4,10 @@ Support for the Hive devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hive/
"""
+import colorsys
from homeassistant.components.hive import DATA_HIVE
from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP,
+ ATTR_RGB_COLOR,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR_TEMP,
SUPPORT_RGB_COLOR, Light)
@@ -46,19 +48,24 @@ class HiveDeviceLight(Light):
"""Return the display name of this light."""
return self.node_name
+ @property
+ def brightness(self):
+ """Brightness of the light (an integer in the range 1-255)."""
+ return self.session.light.get_brightness(self.node_id)
+
@property
def min_mireds(self):
"""Return the coldest color_temp that this light supports."""
if self.light_device_type == "tuneablelight" \
or self.light_device_type == "colourtuneablelight":
- return self.session.light.get_min_colour_temp(self.node_id)
+ return self.session.light.get_min_color_temp(self.node_id)
@property
def max_mireds(self):
"""Return the warmest color_temp that this light supports."""
if self.light_device_type == "tuneablelight" \
or self.light_device_type == "colourtuneablelight":
- return self.session.light.get_max_colour_temp(self.node_id)
+ return self.session.light.get_max_color_temp(self.node_id)
@property
def color_temp(self):
@@ -68,9 +75,10 @@ class HiveDeviceLight(Light):
return self.session.light.get_color_temp(self.node_id)
@property
- def brightness(self):
- """Brightness of the light (an integer in the range 1-255)."""
- return self.session.light.get_brightness(self.node_id)
+ def rgb_color(self) -> tuple:
+ """Return the RBG color value."""
+ if self.light_device_type == "colourtuneablelight":
+ return self.session.light.get_color(self.node_id)
@property
def is_on(self):
@@ -81,6 +89,7 @@ class HiveDeviceLight(Light):
"""Instruct the light to turn on."""
new_brightness = None
new_color_temp = None
+ new_color = None
if ATTR_BRIGHTNESS in kwargs:
tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS)
percentage_brightness = ((tmp_new_brightness / 255) * 100)
@@ -90,13 +99,19 @@ class HiveDeviceLight(Light):
if ATTR_COLOR_TEMP in kwargs:
tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP)
new_color_temp = round(1000000 / tmp_new_color_temp)
+ if ATTR_RGB_COLOR in kwargs:
+ get_new_color = kwargs.get(ATTR_RGB_COLOR)
+ tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0],
+ get_new_color[1],
+ get_new_color[2])
+ hue = int(round(tmp_new_color[0] * 360))
+ saturation = int(round(tmp_new_color[1] * 100))
+ value = int(round((tmp_new_color[2] / 255) * 100))
+ new_color = (hue, saturation, value)
- if new_brightness is not None:
- self.session.light.set_brightness(self.node_id, new_brightness)
- elif new_color_temp is not None:
- self.session.light.set_colour_temp(self.node_id, new_color_temp)
- else:
- self.session.light.turn_on(self.node_id)
+ self.session.light.turn_on(self.node_id, self.light_device_type,
+ new_brightness, new_color_temp,
+ new_color)
for entity in self.session.entities:
entity.handle_update(self.data_updatesource)
diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py
index fe7dd765d01..f5c910ea116 100644
--- a/homeassistant/components/light/hue.py
+++ b/homeassistant/components/light/hue.py
@@ -1,19 +1,21 @@
"""
-Support for Hue lights.
+This component provides light support for the Philips Hue system.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.hue/
"""
-import json
-import logging
-import os
-import random
-import socket
from datetime import timedelta
+import logging
+import random
+import re
+import socket
import voluptuous as vol
+import homeassistant.components.hue as hue
+
import homeassistant.util as util
+from homeassistant.util import yaml
import homeassistant.util.color as color_util
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR,
@@ -21,30 +23,17 @@ from homeassistant.components.light import (
FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP,
SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION,
SUPPORT_XY_COLOR, Light, PLATFORM_SCHEMA)
-from homeassistant.config import load_yaml_config_file
-from homeassistant.const import (CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME)
+from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME
from homeassistant.components.emulated_hue import ATTR_EMULATED_HUE_HIDDEN
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['phue==1.0']
+DEPENDENCIES = ['hue']
-# Track previously setup bridges
-_CONFIGURED_BRIDGES = {}
-# Map ip to request id for configuring
-_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
-CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
-
-DEFAULT_ALLOW_UNREACHABLE = False
-DOMAIN = "light"
-SERVICE_HUE_SCENE = "hue_activate_scene"
-
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
-PHUE_CONFIG_FILE = 'phue.conf'
-
SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION)
SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS)
SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP)
@@ -60,10 +49,14 @@ SUPPORT_HUE = {
'Color temperature light': SUPPORT_HUE_COLOR_TEMP
}
-CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue"
-DEFAULT_ALLOW_IN_EMULATED_HUE = True
+ATTR_IS_HUE_GROUP = 'is_hue_group'
-CONF_ALLOW_HUE_GROUPS = "allow_hue_groups"
+# Legacy configuration, will be removed in 0.60
+CONF_ALLOW_UNREACHABLE = 'allow_unreachable'
+DEFAULT_ALLOW_UNREACHABLE = False
+CONF_ALLOW_IN_EMULATED_HUE = 'allow_in_emulated_hue'
+DEFAULT_ALLOW_IN_EMULATED_HUE = True
+CONF_ALLOW_HUE_GROUPS = 'allow_hue_groups'
DEFAULT_ALLOW_HUE_GROUPS = True
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@@ -75,236 +68,158 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean,
})
-ATTR_GROUP_NAME = "group_name"
-ATTR_SCENE_NAME = "scene_name"
-SCENE_SCHEMA = vol.Schema({
- vol.Required(ATTR_GROUP_NAME): cv.string,
- vol.Required(ATTR_SCENE_NAME): cv.string,
-})
+MIGRATION_ID = 'light_hue_config_migration'
+MIGRATION_TITLE = 'Philips Hue Configuration Migration'
+MIGRATION_INSTRUCTIONS = """
+Configuration for the Philips Hue component has changed; action required.
-ATTR_IS_HUE_GROUP = "is_hue_group"
+You have configured at least one bridge:
-CONFIG_INSTRUCTIONS = """
-Press the button on the bridge to register Philips Hue with Home Assistant.
+ hue:
+{config}
-
+This configuration is deprecated, please check the
+[Hue component](https://home-assistant.io/components/hue/) page for more
+information.
"""
-def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
- """Attempt to detect host based on existing configuration."""
- path = hass.config.path(filename)
-
- if not os.path.isfile(path):
- return None
-
- try:
- with open(path) as inp:
- return next(json.loads(''.join(inp)).keys().__iter__())
- except (ValueError, AttributeError, StopIteration):
- # ValueError if can't parse as JSON
- # AttributeError if JSON value is not a dict
- # StopIteration if no keys
- return None
-
-
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Hue lights."""
- # Default needed in case of discovery
- filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
- allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE,
- DEFAULT_ALLOW_UNREACHABLE)
- allow_in_emulated_hue = config.get(CONF_ALLOW_IN_EMULATED_HUE,
- DEFAULT_ALLOW_IN_EMULATED_HUE)
- allow_hue_groups = config.get(CONF_ALLOW_HUE_GROUPS)
-
- if discovery_info is not None:
- if "HASS Bridge" in discovery_info.get('name', ''):
- _LOGGER.info("Emulated hue found, will not add")
- return False
-
- host = discovery_info.get('host')
- else:
- host = config.get(CONF_HOST, None)
-
- if host is None:
- host = _find_host_from_config(hass, filename)
-
- if host is None:
- _LOGGER.error("No host found in configuration")
- return False
-
- # Only act if we are not already configuring this host
- if host in _CONFIGURING or \
- socket.gethostbyname(host) in _CONFIGURED_BRIDGES:
+ if discovery_info is None or 'bridge_id' not in discovery_info:
return
- setup_bridge(host, hass, add_devices, filename, allow_unreachable,
- allow_in_emulated_hue, allow_hue_groups)
+ if config is not None and len(config) > 0:
+ # Legacy configuration, will be removed in 0.60
+ config_str = yaml.dump([config])
+ # Indent so it renders in a fixed-width font
+ config_str = re.sub('(?m)^', ' ', config_str)
+ hass.components.persistent_notification.async_create(
+ MIGRATION_INSTRUCTIONS.format(config=config_str),
+ title=MIGRATION_TITLE,
+ notification_id=MIGRATION_ID)
+
+ bridge_id = discovery_info['bridge_id']
+ bridge = hass.data[hue.DOMAIN][bridge_id]
+ unthrottled_update_lights(hass, bridge, add_devices)
-def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
- allow_in_emulated_hue, allow_hue_groups):
- """Set up a phue bridge based on host parameter."""
+@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
+def update_lights(hass, bridge, add_devices):
+ """Update the Hue light objects with latest info from the bridge."""
+ return unthrottled_update_lights(hass, bridge, add_devices)
+
+
+def unthrottled_update_lights(hass, bridge, add_devices):
+ """Internal version of update_lights."""
import phue
+ if not bridge.configured:
+ return
+
try:
- bridge = phue.Bridge(
- host,
- config_file_path=hass.config.path(filename))
- except ConnectionRefusedError: # Wrong host was given
- _LOGGER.error("Error connecting to the Hue bridge at %s", host)
-
+ api = bridge.get_api()
+ except phue.PhueRequestTimeout:
+ _LOGGER.warning('Timeout trying to reach the bridge')
+ return
+ except ConnectionRefusedError:
+ _LOGGER.error('The bridge refused the connection')
+ return
+ except socket.error:
+ # socket.error when we cannot reach Hue
+ _LOGGER.exception('Cannot reach the bridge')
return
- except phue.PhueRegistrationException:
- _LOGGER.warning("Connected to Hue at %s but not registered.", host)
+ bridge_type = get_bridge_type(api)
- request_configuration(host, hass, add_devices, filename,
- allow_unreachable, allow_in_emulated_hue,
- allow_hue_groups)
+ new_lights = process_lights(
+ hass, api, bridge, bridge_type,
+ lambda **kw: update_lights(hass, bridge, add_devices, **kw))
+ if bridge.allow_hue_groups:
+ new_lightgroups = process_groups(
+ hass, api, bridge, bridge_type,
+ lambda **kw: update_lights(hass, bridge, add_devices, **kw))
+ new_lights.extend(new_lightgroups)
- return
+ if new_lights:
+ add_devices(new_lights)
- # If we came here and configuring this host, mark as done
- if host in _CONFIGURING:
- request_id = _CONFIGURING.pop(host)
- configurator = hass.components.configurator
- configurator.request_done(request_id)
- lights = {}
- lightgroups = {}
- skip_groups = not allow_hue_groups
+def get_bridge_type(api):
+ """Return the bridge type."""
+ api_name = api.get('config').get('name')
+ if api_name in ('RaspBee-GW', 'deCONZ-GW'):
+ return 'deconz'
+ else:
+ return 'hue'
- @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
- def update_lights():
- """Update the Hue light objects with latest info from the bridge."""
- nonlocal skip_groups
- try:
- api = bridge.get_api()
- except phue.PhueRequestTimeout:
- _LOGGER.warning("Timeout trying to reach the bridge")
- return
- except ConnectionRefusedError:
- _LOGGER.error("The bridge refused the connection")
- return
- except socket.error:
- # socket.error when we cannot reach Hue
- _LOGGER.exception("Cannot reach the bridge")
- return
+def process_lights(hass, api, bridge, bridge_type, update_lights_cb):
+ """Set up HueLight objects for all lights."""
+ api_lights = api.get('lights')
- api_lights = api.get('lights')
+ if not isinstance(api_lights, dict):
+ _LOGGER.error('Got unexpected result from Hue API')
+ return []
- if not isinstance(api_lights, dict):
- _LOGGER.error("Got unexpected result from Hue API")
- return
+ new_lights = []
- if skip_groups:
- api_groups = {}
+ for light_id, info in api_lights.items():
+ if light_id not in bridge.lights:
+ bridge.lights[light_id] = HueLight(
+ int(light_id), info, bridge,
+ update_lights_cb,
+ bridge_type, bridge.allow_unreachable,
+ bridge.allow_in_emulated_hue)
+ new_lights.append(bridge.lights[light_id])
else:
- api_groups = api.get('groups')
+ bridge.lights[light_id].info = info
+ bridge.lights[light_id].schedule_update_ha_state()
- if not isinstance(api_groups, dict):
- _LOGGER.error("Got unexpected result from Hue API")
- return
+ return new_lights
- new_lights = []
- api_name = api.get('config').get('name')
- if api_name in ('RaspBee-GW', 'deCONZ-GW'):
- bridge_type = 'deconz'
+def process_groups(hass, api, bridge, bridge_type, update_lights_cb):
+ """Set up HueLight objects for all groups."""
+ api_groups = api.get('groups')
+
+ if not isinstance(api_groups, dict):
+ _LOGGER.error('Got unexpected result from Hue API')
+ return []
+
+ new_lights = []
+
+ for lightgroup_id, info in api_groups.items():
+ if 'state' not in info:
+ _LOGGER.warning('Group info does not contain state. '
+ 'Please update your hub.')
+ return []
+
+ if lightgroup_id not in bridge.lightgroups:
+ bridge.lightgroups[lightgroup_id] = HueLight(
+ int(lightgroup_id), info, bridge,
+ update_lights_cb,
+ bridge_type, bridge.allow_unreachable,
+ bridge.allow_in_emulated_hue, True)
+ new_lights.append(bridge.lightgroups[lightgroup_id])
else:
- bridge_type = 'hue'
+ bridge.lightgroups[lightgroup_id].info = info
+ bridge.lightgroups[lightgroup_id].schedule_update_ha_state()
- for light_id, info in api_lights.items():
- if light_id not in lights:
- lights[light_id] = HueLight(int(light_id), info,
- bridge, update_lights,
- bridge_type, allow_unreachable,
- allow_in_emulated_hue)
- new_lights.append(lights[light_id])
- else:
- lights[light_id].info = info
- lights[light_id].schedule_update_ha_state()
-
- for lightgroup_id, info in api_groups.items():
- if 'state' not in info:
- _LOGGER.warning("Group info does not contain state. "
- "Please update your hub.")
- skip_groups = True
- break
-
- if lightgroup_id not in lightgroups:
- lightgroups[lightgroup_id] = HueLight(
- int(lightgroup_id), info, bridge, update_lights,
- bridge_type, allow_unreachable, allow_in_emulated_hue,
- True)
- new_lights.append(lightgroups[lightgroup_id])
- else:
- lightgroups[lightgroup_id].info = info
- lightgroups[lightgroup_id].schedule_update_ha_state()
-
- if new_lights:
- add_devices(new_lights)
-
- _CONFIGURED_BRIDGES[socket.gethostbyname(host)] = True
-
- # create a service for calling run_scene directly on the bridge,
- # used to simplify automation rules.
- def hue_activate_scene(call):
- """Service to call directly into bridge to set scenes."""
- group_name = call.data[ATTR_GROUP_NAME]
- scene_name = call.data[ATTR_SCENE_NAME]
- bridge.run_scene(group_name, scene_name)
-
- descriptions = load_yaml_config_file(
- os.path.join(os.path.dirname(__file__), 'services.yaml'))
- hass.services.register(DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene,
- descriptions.get(SERVICE_HUE_SCENE),
- schema=SCENE_SCHEMA)
-
- update_lights()
-
-
-def request_configuration(host, hass, add_devices, filename,
- allow_unreachable, allow_in_emulated_hue,
- allow_hue_groups):
- """Request configuration steps from the user."""
- configurator = hass.components.configurator
-
- # We got an error if this method is called while we are configuring
- if host in _CONFIGURING:
- configurator.notify_errors(
- _CONFIGURING[host], "Failed to register, please try again.")
-
- return
-
- # pylint: disable=unused-argument
- def hue_configuration_callback(data):
- """Set up actions to do when our configuration callback is called."""
- setup_bridge(host, hass, add_devices, filename, allow_unreachable,
- allow_in_emulated_hue, allow_hue_groups)
-
- _CONFIGURING[host] = configurator.request_config(
- "Philips Hue", hue_configuration_callback,
- description=CONFIG_INSTRUCTIONS,
- entity_picture="/static/images/logo_philips_hue.png",
- submit_caption="I have pressed the button"
- )
+ return new_lights
class HueLight(Light):
"""Representation of a Hue light."""
- def __init__(self, light_id, info, bridge, update_lights,
+ def __init__(self, light_id, info, bridge, update_lights_cb,
bridge_type, allow_unreachable, allow_in_emulated_hue,
is_group=False):
"""Initialize the light."""
self.light_id = light_id
self.info = info
self.bridge = bridge
- self.update_lights = update_lights
+ self.update_lights = update_lights_cb
self.bridge_type = bridge_type
self.allow_unreachable = allow_unreachable
self.is_group = is_group
@@ -381,14 +296,15 @@ class HueLight(Light):
command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10)
if ATTR_XY_COLOR in kwargs:
- if self.info.get('manufacturername') == "OSRAM":
- hue, sat = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR])
- command['hue'] = hue
+ if self.info.get('manufacturername') == 'OSRAM':
+ color_hue, sat = color_util.color_xy_to_hs(
+ *kwargs[ATTR_XY_COLOR])
+ command['hue'] = color_hue
command['sat'] = sat
else:
command['xy'] = kwargs[ATTR_XY_COLOR]
elif ATTR_RGB_COLOR in kwargs:
- if self.info.get('manufacturername') == "OSRAM":
+ if self.info.get('manufacturername') == 'OSRAM':
hsv = color_util.color_RGB_to_hsv(
*(int(val) for val in kwargs[ATTR_RGB_COLOR]))
command['hue'] = hsv[0]
diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py
index ad2cf204463..06a00954d3b 100644
--- a/homeassistant/components/light/lifx.py
+++ b/homeassistant/components/light/lifx.py
@@ -33,7 +33,7 @@ import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
-REQUIREMENTS = ['aiolifx==0.6.0', 'aiolifx_effects==0.1.2']
+REQUIREMENTS = ['aiolifx==0.6.1', 'aiolifx_effects==0.1.2']
UDP_BROADCAST_PORT = 56700
@@ -157,20 +157,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
return True
-def lifxwhite(device):
- """Return whether this is a white-only bulb."""
- features = aiolifx().products.features_map.get(device.product, None)
- if features:
- return not features["color"]
- return False
-
-
-def lifxmultizone(device):
- """Return whether this is a multizone bulb/strip."""
- features = aiolifx().products.features_map.get(device.product, None)
- if features:
- return features["multizone"]
- return False
+def lifx_features(device):
+ """Return a feature map for this device, or a default map if unknown."""
+ return aiolifx().products.features_map.get(device.product) or \
+ aiolifx().products.features_map.get(1)
def find_hsbk(**kwargs):
@@ -342,12 +332,12 @@ class LIFXManager(object):
device.retry_count = MESSAGE_RETRIES
device.unregister_timeout = UNAVAILABLE_GRACE
- if lifxwhite(device):
- entity = LIFXWhite(device, self.effects_conductor)
- elif lifxmultizone(device):
+ if lifx_features(device)["multizone"]:
entity = LIFXStrip(device, self.effects_conductor)
- else:
+ elif lifx_features(device)["color"]:
entity = LIFXColor(device, self.effects_conductor)
+ else:
+ entity = LIFXWhite(device, self.effects_conductor)
_LOGGER.debug("%s register READY", entity.who)
self.entities[device.mac_addr] = entity
@@ -427,6 +417,29 @@ class LIFXLight(Light):
"""Return a string identifying the device."""
return "%s (%s)" % (self.device.ip_addr, self.name)
+ @property
+ def min_mireds(self):
+ """Return the coldest color_temp that this light supports."""
+ kelvin = lifx_features(self.device)['max_kelvin']
+ return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin))
+
+ @property
+ def max_mireds(self):
+ """Return the warmest color_temp that this light supports."""
+ kelvin = lifx_features(self.device)['min_kelvin']
+ return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin))
+
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ support = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_EFFECT
+
+ device_features = lifx_features(self.device)
+ if device_features['min_kelvin'] != device_features['max_kelvin']:
+ support |= SUPPORT_COLOR_TEMP
+
+ return support
+
@property
def brightness(self):
"""Return the brightness of this light between 0..255."""
@@ -571,22 +584,6 @@ class LIFXLight(Light):
class LIFXWhite(LIFXLight):
"""Representation of a white-only LIFX light."""
- @property
- def min_mireds(self):
- """Return the coldest color_temp that this light supports."""
- return math.floor(color_util.color_temperature_kelvin_to_mired(6500))
-
- @property
- def max_mireds(self):
- """Return the warmest color_temp that this light supports."""
- return math.ceil(color_util.color_temperature_kelvin_to_mired(2700))
-
- @property
- def supported_features(self):
- """Flag supported features."""
- return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
- SUPPORT_EFFECT)
-
@property
def effect_list(self):
"""Return the list of supported effects for this light."""
@@ -599,21 +596,12 @@ class LIFXWhite(LIFXLight):
class LIFXColor(LIFXLight):
"""Representation of a color LIFX light."""
- @property
- def min_mireds(self):
- """Return the coldest color_temp that this light supports."""
- return math.floor(color_util.color_temperature_kelvin_to_mired(9000))
-
- @property
- def max_mireds(self):
- """Return the warmest color_temp that this light supports."""
- return math.ceil(color_util.color_temperature_kelvin_to_mired(2500))
-
@property
def supported_features(self):
"""Flag supported features."""
- return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION |
- SUPPORT_EFFECT | SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR)
+ support = super().supported_features
+ support |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR
+ return support
@property
def effect_list(self):
diff --git a/homeassistant/components/light/mochad.py b/homeassistant/components/light/mochad.py
index fffaa293188..efc62b05434 100644
--- a/homeassistant/components/light/mochad.py
+++ b/homeassistant/components/light/mochad.py
@@ -12,13 +12,15 @@ import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, PLATFORM_SCHEMA)
from homeassistant.components import mochad
-from homeassistant.const import (CONF_NAME, CONF_PLATFORM, CONF_DEVICES,
- CONF_ADDRESS)
+from homeassistant.const import (
+ CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS)
from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['mochad']
_LOGGER = logging.getLogger(__name__)
+CONF_BRIGHTNESS_LEVELS = 'brightness_levels'
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PLATFORM): mochad.DOMAIN,
@@ -26,6 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_ADDRESS): cv.x10_address,
vol.Optional(mochad.CONF_COMM_TYPE): cv.string,
+ vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32):
+ vol.All(vol.Coerce(int), vol.In([32, 64, 256])),
}]
})
@@ -54,6 +58,7 @@ class MochadLight(Light):
comm_type=self._comm_type)
self._brightness = 0
self._state = self._get_device_status()
+ self._brightness_levels = dev.get(CONF_BRIGHTNESS_LEVELS) - 1
@property
def brightness(self):
@@ -62,7 +67,8 @@ class MochadLight(Light):
def _get_device_status(self):
"""Get the status of the light from mochad."""
- status = self.device.get_status().rstrip()
+ with mochad.REQ_LOCK:
+ status = self.device.get_status().rstrip()
return status == 'on'
@property
@@ -85,15 +91,47 @@ class MochadLight(Light):
"""X10 devices are normally 1-way so we have to assume the state."""
return True
+ def _calculate_brightness_value(self, value):
+ return int(value * (float(self._brightness_levels) / 255.0))
+
+ def _adjust_brightness(self, brightness):
+ if self._brightness > brightness:
+ bdelta = self._brightness - brightness
+ mochad_brightness = self._calculate_brightness_value(bdelta)
+ self.device.send_cmd("dim {}".format(mochad_brightness))
+ self._controller.read_data()
+ elif self._brightness < brightness:
+ bdelta = brightness - self._brightness
+ mochad_brightness = self._calculate_brightness_value(bdelta)
+ self.device.send_cmd("bright {}".format(mochad_brightness))
+ self._controller.read_data()
+
def turn_on(self, **kwargs):
"""Send the command to turn the light on."""
- self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
- self.device.send_cmd("xdim {}".format(self._brightness))
- self._controller.read_data()
+ brightness = kwargs.get(ATTR_BRIGHTNESS, 255)
+ with mochad.REQ_LOCK:
+ if self._brightness_levels > 32:
+ out_brightness = self._calculate_brightness_value(brightness)
+ self.device.send_cmd('xdim {}'.format(out_brightness))
+ self._controller.read_data()
+ else:
+ self.device.send_cmd("on")
+ self._controller.read_data()
+ # There is no persistence for X10 modules so a fresh on command
+ # will be full brightness
+ if self._brightness == 0:
+ self._brightness = 255
+ self._adjust_brightness(brightness)
+ self._brightness = brightness
self._state = True
def turn_off(self, **kwargs):
"""Send the command to turn the light on."""
- self.device.send_cmd('off')
- self._controller.read_data()
+ with mochad.REQ_LOCK:
+ self.device.send_cmd('off')
+ self._controller.read_data()
+ # There is no persistence for X10 modules so we need to prepare
+ # to track a fresh on command will full brightness
+ if self._brightness_levels == 31:
+ self._brightness = 0
self._state = False
diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py
index f6a544950c0..692a5fb86ec 100644
--- a/homeassistant/components/light/tplink.py
+++ b/homeassistant/components/light/tplink.py
@@ -72,6 +72,7 @@ class TPLinkSmartBulb(Light):
if name is not None:
self._name = name
self._state = None
+ self._available = True
self._color_temp = None
self._brightness = None
self._rgb = None
@@ -83,6 +84,11 @@ class TPLinkSmartBulb(Light):
"""Return the name of the Smart Bulb, if any."""
return self._name
+ @property
+ def available(self) -> bool:
+ """Return if bulb is available."""
+ return self._available
+
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
@@ -132,6 +138,7 @@ class TPLinkSmartBulb(Light):
"""Update the TP-Link Bulb's state."""
from pyHS100 import SmartDeviceException
try:
+ self._available = True
if self._supported_features == 0:
self.get_features()
self._state = (
@@ -163,8 +170,10 @@ class TPLinkSmartBulb(Light):
except KeyError:
# device returned no daily/monthly history
pass
+
except (SmartDeviceException, OSError) as ex:
- _LOGGER.warning('Could not read state for %s: %s', self._name, ex)
+ _LOGGER.warning("Could not read state for %s: %s", self._name, ex)
+ self._available = False
@property
def supported_features(self):
diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py
index dc8e7f4c996..bb2fa44c15c 100644
--- a/homeassistant/components/light/tradfri.py
+++ b/homeassistant/components/light/tradfri.py
@@ -160,6 +160,7 @@ class TradfriLight(Light):
self._rgb_color = None
self._features = SUPPORTED_FEATURES
self._temp_supported = False
+ self._available = True
self._refresh(light)
@@ -196,6 +197,11 @@ class TradfriLight(Light):
"""Start thread when added to hass."""
self._async_start_observe()
+ @property
+ def available(self):
+ """Return True if entity is available."""
+ return self._available
+
@property
def should_poll(self):
"""No polling needed for tradfri light."""
@@ -299,6 +305,7 @@ class TradfriLight(Light):
self._light = light
# Caching of LightControl and light object
+ self._available = light.reachable
self._light_control = light.light_control
self._light_data = light.light_control.lights[0]
self._name = light.name
diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py
index b3be93d82e2..102ca814882 100644
--- a/homeassistant/components/light/vera.py
+++ b/homeassistant/components/light/vera.py
@@ -21,7 +21,8 @@ DEPENDENCIES = ['vera']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera lights."""
add_devices(
- VeraLight(device, VERA_CONTROLLER) for device in VERA_DEVICES['light'])
+ VeraLight(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['light'])
class VeraLight(VeraDevice, Light):
diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py
index ddffed52271..b35b5a3740e 100644
--- a/homeassistant/components/light/xiaomi_miio.py
+++ b/homeassistant/components/light/xiaomi_miio.py
@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
-REQUIREMENTS = ['python-miio==0.3.2']
+REQUIREMENTS = ['python-miio==0.3.3']
# The light does not accept cct values < 1
CCT_MIN = 1
diff --git a/homeassistant/components/lock/isy994.py b/homeassistant/components/lock/isy994.py
index edbb8a34f24..63272b90b1f 100644
--- a/homeassistant/components/lock/isy994.py
+++ b/homeassistant/components/lock/isy994.py
@@ -66,7 +66,10 @@ class ISYLockDevice(isy.ISYDevice, LockDevice):
@property
def state(self) -> str:
"""Get the state of the lock."""
- return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
+ if self.is_unknown():
+ return None
+ else:
+ return VALUE_TO_STATE.get(self.value, STATE_UNKNOWN)
def lock(self, **kwargs) -> None:
"""Send the lock command to the ISY994 device."""
diff --git a/homeassistant/components/lock/sesame.py b/homeassistant/components/lock/sesame.py
index 02b049618d2..5bc40435486 100644
--- a/homeassistant/components/lock/sesame.py
+++ b/homeassistant/components/lock/sesame.py
@@ -25,46 +25,53 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
-def setup_platform(hass, config: ConfigType,
- add_devices: Callable[[list], None], discovery_info=None):
+def setup_platform(
+ hass, config: ConfigType,
+ add_devices: Callable[[list], None], discovery_info=None):
"""Set up the Sesame platform."""
import pysesame
email = config.get(CONF_EMAIL)
password = config.get(CONF_PASSWORD)
- add_devices([SesameDevice(sesame) for
- sesame in pysesame.get_sesames(email, password)])
+ add_devices([SesameDevice(sesame) for sesame in
+ pysesame.get_sesames(email, password)],
+ update_before_add=True)
class SesameDevice(LockDevice):
"""Representation of a Sesame device."""
- _sesame = None
-
def __init__(self, sesame: object) -> None:
"""Initialize the Sesame device."""
self._sesame = sesame
+ # Cached properties from pysesame object.
+ self._device_id = None
+ self._nickname = None
+ self._is_unlocked = False
+ self._api_enabled = False
+ self._battery = -1
+
@property
def name(self) -> str:
"""Return the name of the device."""
- return self._sesame.nickname
+ return self._nickname
@property
def available(self) -> bool:
"""Return True if entity is available."""
- return self._sesame.api_enabled
+ return self._api_enabled
@property
def is_locked(self) -> bool:
"""Return True if the device is currently locked, else False."""
- return not self._sesame.is_unlocked
+ return not self._is_unlocked
@property
def state(self) -> str:
"""Get the state of the device."""
- if self._sesame.is_unlocked:
+ if self._is_unlocked:
return STATE_UNLOCKED
return STATE_LOCKED
@@ -79,11 +86,16 @@ class SesameDevice(LockDevice):
def update(self) -> None:
"""Update the internal state of the device."""
self._sesame.update_state()
+ self._nickname = self._sesame.nickname
+ self._api_enabled = self._sesame.api_enabled
+ self._is_unlocked = self._sesame.is_unlocked
+ self._device_id = self._sesame.device_id
+ self._battery = self._sesame.battery
@property
def device_state_attributes(self) -> dict:
"""Return the state attributes."""
attributes = {}
- attributes[ATTR_DEVICE_ID] = self._sesame.device_id
- attributes[ATTR_BATTERY_LEVEL] = self._sesame.battery
+ attributes[ATTR_DEVICE_ID] = self._device_id
+ attributes[ATTR_BATTERY_LEVEL] = self._battery
return attributes
diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py
index 04962566821..b3aae5e159f 100644
--- a/homeassistant/components/lock/vera.py
+++ b/homeassistant/components/lock/vera.py
@@ -19,8 +19,8 @@ DEPENDENCIES = ['vera']
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Find and return Vera locks."""
add_devices(
- VeraLock(device, VERA_CONTROLLER) for
- device in VERA_DEVICES['lock'])
+ VeraLock(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['lock'])
class VeraLock(VeraDevice, LockDevice):
diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py
index 63a271acdd5..1dc0861d737 100644
--- a/homeassistant/components/logbook.py
+++ b/homeassistant/components/logbook.py
@@ -135,9 +135,8 @@ class LogbookView(HomeAssistantView):
hass = request.app['hass']
events = yield from hass.async_add_job(
- _get_events, hass, start_day, end_day)
- events = _exclude_events(events, self.config)
- return self.json(humanify(events))
+ _get_events, hass, self.config, start_day, end_day)
+ return self.json(events)
class Entry(object):
@@ -274,7 +273,7 @@ def humanify(events):
entity_id)
-def _get_events(hass, start_day, end_day):
+def _get_events(hass, config, start_day, end_day):
"""Get events for a period of time."""
from homeassistant.components.recorder.models import Events
from homeassistant.components.recorder.util import (
@@ -285,7 +284,8 @@ def _get_events(hass, start_day, end_day):
Events.time_fired).filter(
(Events.time_fired > start_day) &
(Events.time_fired < end_day))
- return execute(query)
+ events = execute(query)
+ return humanify(_exclude_events(events, config))
def _exclude_events(events, config):
diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py
index 9d5e88282ae..669390b3b90 100644
--- a/homeassistant/components/media_extractor.py
+++ b/homeassistant/components/media_extractor.py
@@ -16,7 +16,7 @@ from homeassistant.components.media_player import (
from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_validation as cv
-REQUIREMENTS = ['youtube_dl==2017.11.26']
+REQUIREMENTS = ['youtube_dl==2017.12.10']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py
index 15698ec5022..8093f0d3dbe 100644
--- a/homeassistant/components/media_player/liveboxplaytv.py
+++ b/homeassistant/components/media_player/liveboxplaytv.py
@@ -20,8 +20,9 @@ from homeassistant.const import (
CONF_HOST, CONF_PORT, STATE_ON, STATE_OFF, STATE_PLAYING,
STATE_PAUSED, CONF_NAME)
import homeassistant.helpers.config_validation as cv
+import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['liveboxplaytv==2.0.0']
+REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3']
_LOGGER = logging.getLogger(__name__)
@@ -76,19 +77,32 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
self._channel_list = {}
self._current_channel = None
self._current_program = None
+ self._media_duration = None
+ self._media_remaining_time = None
self._media_image_url = None
+ self._media_last_updated = None
@asyncio.coroutine
def async_update(self):
"""Retrieve the latest data."""
+ import pyteleloisirs
try:
self._state = self.refresh_state()
# Update current channel
channel = self._client.channel
if channel is not None:
- self._current_program = yield from \
- self._client.async_get_current_program_name()
self._current_channel = channel
+ program = yield from \
+ self._client.async_get_current_program()
+ if program and self._current_program != program.get('name'):
+ self._current_program = program.get('name')
+ # Media progress info
+ self._media_duration = \
+ pyteleloisirs.get_program_duration(program)
+ rtime = pyteleloisirs.get_remaining_time(program)
+ if rtime != self._media_remaining_time:
+ self._media_remaining_time = rtime
+ self._media_last_updated = dt_util.utcnow()
# Set media image to current program if a thumbnail is
# available. Otherwise we'll use the channel's image.
img_size = 800
@@ -100,7 +114,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
chan_img_url = \
self._client.get_current_channel_image(img_size)
self._media_image_url = chan_img_url
- self.refresh_channel_list()
except requests.ConnectionError:
self._state = None
@@ -149,8 +162,25 @@ class LiveboxPlayTvDevice(MediaPlayerDevice):
if self._current_program:
return '{}: {}'.format(self._current_channel,
self._current_program)
- else:
- return self._current_channel
+ return self._current_channel
+
+ @property
+ def media_duration(self):
+ """Duration of current playing media in seconds."""
+ return self._media_duration
+
+ @property
+ def media_position(self):
+ """Position of current playing media in seconds."""
+ return self._media_remaining_time
+
+ @property
+ def media_position_updated_at(self):
+ """When was the position of the current playing media valid.
+
+ Returns value from homeassistant.util.dt.utcnow().
+ """
+ return self._media_last_updated
@property
def supported_features(self):
diff --git a/homeassistant/components/media_player/monoprice.py b/homeassistant/components/media_player/monoprice.py
index 10b4b8414d8..a2b5d91945a 100644
--- a/homeassistant/components/media_player/monoprice.py
+++ b/homeassistant/components/media_player/monoprice.py
@@ -5,18 +5,21 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.monoprice/
"""
import logging
+from os import path
import voluptuous as vol
-from homeassistant.const import (CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON)
+from homeassistant.const import (ATTR_ENTITY_ID, CONF_NAME, CONF_PORT,
+ STATE_OFF, STATE_ON)
+from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import (
- MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_VOLUME_MUTE,
- SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
- SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP)
+ DOMAIN, MediaPlayerDevice, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA,
+ SUPPORT_VOLUME_MUTE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_ON,
+ SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP)
-REQUIREMENTS = ['pymonoprice==0.2']
+REQUIREMENTS = ['pymonoprice==0.3']
_LOGGER = logging.getLogger(__name__)
@@ -35,6 +38,11 @@ SOURCE_SCHEMA = vol.Schema({
CONF_ZONES = 'zones'
CONF_SOURCES = 'sources'
+DATA_MONOPRICE = 'monoprice'
+
+SERVICE_SNAPSHOT = 'snapshot'
+SERVICE_RESTORE = 'restore'
+
# Valid zone ids: 11-16 or 21-26 or 31-36
ZONE_IDS = vol.All(vol.Coerce(int), vol.Any(vol.Range(min=11, max=16),
vol.Range(min=21, max=26),
@@ -56,9 +64,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
port = config.get(CONF_PORT)
from serial import SerialException
- from pymonoprice import Monoprice
+ from pymonoprice import get_monoprice
try:
- monoprice = Monoprice(port)
+ monoprice = get_monoprice(port)
except SerialException:
_LOGGER.error('Error connecting to Monoprice controller.')
return
@@ -66,10 +74,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sources = {source_id: extra[CONF_NAME] for source_id, extra
in config[CONF_SOURCES].items()}
+ hass.data[DATA_MONOPRICE] = []
for zone_id, extra in config[CONF_ZONES].items():
_LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME])
- add_devices([MonopriceZone(monoprice, sources,
- zone_id, extra[CONF_NAME])], True)
+ hass.data[DATA_MONOPRICE].append(MonopriceZone(monoprice, sources,
+ zone_id,
+ extra[CONF_NAME]))
+
+ add_devices(hass.data[DATA_MONOPRICE], True)
+
+ descriptions = load_yaml_config_file(
+ path.join(path.dirname(__file__), 'services.yaml'))
+
+ def service_handle(service):
+ """Handle for services."""
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+
+ if entity_ids:
+ devices = [device for device in hass.data[DATA_MONOPRICE]
+ if device.entity_id in entity_ids]
+ else:
+ devices = hass.data[DATA_MONOPRICE]
+
+ for device in devices:
+ if service.service == SERVICE_SNAPSHOT:
+ device.snapshot()
+ elif service.service == SERVICE_RESTORE:
+ device.restore()
+
+ hass.services.register(
+ DOMAIN, SERVICE_SNAPSHOT, service_handle,
+ descriptions.get(SERVICE_SNAPSHOT), schema=MEDIA_PLAYER_SCHEMA)
+
+ hass.services.register(
+ DOMAIN, SERVICE_RESTORE, service_handle,
+ descriptions.get(SERVICE_RESTORE), schema=MEDIA_PLAYER_SCHEMA)
class MonopriceZone(MediaPlayerDevice):
@@ -90,6 +129,7 @@ class MonopriceZone(MediaPlayerDevice):
self._zone_id = zone_id
self._name = zone_name
+ self._snapshot = None
self._state = None
self._volume = None
self._source = None
@@ -152,6 +192,16 @@ class MonopriceZone(MediaPlayerDevice):
"""List of available input sources."""
return self._source_names
+ def snapshot(self):
+ """Save zone's current state."""
+ self._snapshot = self._monoprice.zone_status(self._zone_id)
+
+ def restore(self):
+ """Restore saved state."""
+ if self._snapshot:
+ self._monoprice.restore_zone(self._snapshot)
+ self.schedule_update_ha_state(True)
+
def select_source(self, source):
"""Set input source."""
if source not in self._source_name_id:
diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py
index 9b984813ff6..c6f3042f2ba 100644
--- a/homeassistant/components/media_player/plex.py
+++ b/homeassistant/components/media_player/plex.py
@@ -227,7 +227,7 @@ def request_configuration(host, hass, config, add_devices_callback):
_CONFIGURING[host] = configurator.request_config(
'Plex Media Server',
plex_configuration_callback,
- description=('Enter the X-Plex-Token'),
+ description='Enter the X-Plex-Token',
entity_picture='/static/images/logo_plex_mediaserver.png',
submit_caption='Confirm',
fields=[{
@@ -273,8 +273,23 @@ class PlexClient(MediaPlayerDevice):
self.plex_sessions = plex_sessions
self.update_devices = update_devices
self.update_sessions = update_sessions
-
- self._clear_media()
+ # General
+ self._media_content_id = None
+ self._media_content_rating = None
+ self._media_content_type = None
+ self._media_duration = None
+ self._media_image_url = None
+ self._media_title = None
+ self._media_position = None
+ # Music
+ self._media_album_artist = None
+ self._media_album_name = None
+ self._media_artist = None
+ self._media_track = None
+ # TV Show
+ self._media_episode = None
+ self._media_season = None
+ self._media_series_title = None
self.refresh(device, session)
@@ -296,7 +311,7 @@ class PlexClient(MediaPlayerDevice):
'media_player', prefix,
self.name.lower().replace('-', '_'))
- def _clear_media(self):
+ def _clear_media_details(self):
"""Set all Media Items to None."""
# General
self._media_content_id = None
@@ -316,10 +331,13 @@ class PlexClient(MediaPlayerDevice):
self._media_season = None
self._media_series_title = None
+ # Clear library Name
+ self._app_name = ''
+
def refresh(self, device, session):
"""Refresh key device data."""
# new data refresh
- self._clear_media()
+ self._clear_media_details()
if session: # Not being triggered by Chrome or FireTablet Plex App
self._session = session
@@ -355,6 +373,36 @@ class PlexClient(MediaPlayerDevice):
self._media_content_id = self._session.ratingKey
self._media_content_rating = self._session.contentRating
+ self._set_player_state()
+
+ if self._is_player_active and self._session is not None:
+ self._session_type = self._session.type
+ self._media_duration = self._session.duration
+ # title (movie name, tv episode name, music song name)
+ self._media_title = self._session.title
+ # media type
+ self._set_media_type()
+ self._app_name = self._session.section().title \
+ if self._session.section() is not None else ''
+ self._set_media_image()
+ else:
+ self._session_type = None
+
+ def _set_media_image(self):
+ thumb_url = self._session.thumbUrl
+ if (self.media_content_type is MEDIA_TYPE_TVSHOW
+ and not self.config.get(CONF_USE_EPISODE_ART)):
+ thumb_url = self._server.url(
+ self._session.grandparentThumb)
+
+ if thumb_url is None:
+ _LOGGER.debug("Using media art because media thumb "
+ "was not found: %s", self.entity_id)
+ thumb_url = self._server.url(self._session.art)
+
+ self._media_image_url = thumb_url
+
+ def _set_player_state(self):
if self._player_state == 'playing':
self._is_player_active = True
self._state = STATE_PLAYING
@@ -368,35 +416,10 @@ class PlexClient(MediaPlayerDevice):
self._is_player_active = False
self._state = STATE_OFF
- if self._is_player_active and self._session is not None:
- self._session_type = self._session.type
- self._media_duration = self._session.duration
- else:
- self._session_type = None
-
- # media type
- if self._session_type == 'clip':
- _LOGGER.debug("Clip content type detected, compatibility may "
- "vary: %s", self.entity_id)
+ def _set_media_type(self):
+ if self._session_type in ['clip', 'episode']:
self._media_content_type = MEDIA_TYPE_TVSHOW
- elif self._session_type == 'episode':
- self._media_content_type = MEDIA_TYPE_TVSHOW
- elif self._session_type == 'movie':
- self._media_content_type = MEDIA_TYPE_VIDEO
- elif self._session_type == 'track':
- self._media_content_type = MEDIA_TYPE_MUSIC
- # title (movie name, tv episode name, music song name)
- if self._session and self._is_player_active:
- self._media_title = self._session.title
-
- # Movies
- if (self.media_content_type == MEDIA_TYPE_VIDEO and
- self._session.year is not None):
- self._media_title += ' (' + str(self._session.year) + ')'
-
- # TV Show
- if self._media_content_type is MEDIA_TYPE_TVSHOW:
# season number (00)
if callable(self._session.seasons):
self._media_season = self._session.seasons()[0].index.zfill(2)
@@ -410,8 +433,14 @@ class PlexClient(MediaPlayerDevice):
if self._session.index is not None:
self._media_episode = str(self._session.index).zfill(2)
- # Music
- if self._media_content_type == MEDIA_TYPE_MUSIC:
+ elif self._session_type == 'movie':
+ self._media_content_type = MEDIA_TYPE_VIDEO
+ if self._session.year is not None and \
+ self._media_title is not None:
+ self._media_title += ' (' + str(self._session.year) + ')'
+
+ elif self._session_type == 'track':
+ self._media_content_type = MEDIA_TYPE_MUSIC
self._media_album_name = self._session.parentTitle
self._media_album_artist = self._session.grandparentTitle
self._media_track = self._session.index
@@ -422,33 +451,11 @@ class PlexClient(MediaPlayerDevice):
"was not found: %s", self.entity_id)
self._media_artist = self._media_album_artist
- # set app name to library name
- if (self._session is not None
- and self._session.section() is not None):
- self._app_name = self._session.section().title
- else:
- self._app_name = ''
-
- # media image url
- if self._session is not None:
- thumb_url = self._session.thumbUrl
- if (self.media_content_type is MEDIA_TYPE_TVSHOW
- and not self.config.get(CONF_USE_EPISODE_ART)):
- thumb_url = self._server.url(
- self._session.grandparentThumb)
-
- if thumb_url is None:
- _LOGGER.debug("Using media art because media thumb "
- "was not found: %s", self.entity_id)
- thumb_url = self._server.url(self._session.art)
-
- self._media_image_url = thumb_url
-
def force_idle(self):
"""Force client to idle."""
self._state = STATE_IDLE
self._session = None
- self._clear_media()
+ self._clear_media_details()
@property
def unique_id(self):
@@ -792,9 +799,10 @@ class PlexClient(MediaPlayerDevice):
@property
def device_state_attributes(self):
"""Return the scene state attributes."""
- attr = {}
- attr['media_content_rating'] = self._media_content_rating
- attr['session_username'] = self._session_username
- attr['media_library_name'] = self._app_name
+ attr = {
+ 'media_content_rating': self._media_content_rating,
+ 'session_username': self._session_username,
+ 'media_library_name': self._app_name
+ }
return attr
diff --git a/homeassistant/components/media_player/samsungtv.py b/homeassistant/components/media_player/samsungtv.py
index 721b095c083..d42bd9ea012 100644
--- a/homeassistant/components/media_player/samsungtv.py
+++ b/homeassistant/components/media_player/samsungtv.py
@@ -190,7 +190,10 @@ class SamsungTVDevice(MediaPlayerDevice):
else:
self.send_key('KEY_POWEROFF')
# Force closing of remote session to provide instant UI feedback
- self.get_remote().close()
+ try:
+ self.get_remote().close()
+ except OSError:
+ _LOGGER.debug("Could not establish connection.")
def volume_up(self):
"""Volume up the media player."""
diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml
index f2d7b8e07dd..0ed5f9d2732 100644
--- a/homeassistant/components/media_player/services.yaml
+++ b/homeassistant/components/media_player/services.yaml
@@ -107,6 +107,20 @@ media_seek:
description: Position to seek to. The format is platform dependent.
example: 100
+monoprice_snapshot:
+ description: Take a snapshot of the media player zone.
+ fields:
+ entity_id:
+ description: Name(s) of entities that will be snapshot. Platform dependent.
+ example: 'media_player.living_room'
+
+monoprice_restore:
+ description: Restore a snapshot of the media player zone.
+ fields:
+ entity_id:
+ description: Name(s) of entities that will be restored. Platform dependent.
+ example: 'media_player.living_room'
+
play_media:
description: Send the media player the command for playing media.
fields:
@@ -215,6 +229,18 @@ sonos_clear_sleep_timer:
description: Name(s) of entities that will have the timer cleared.
example: 'media_player.living_room_sonos'
+sonos_set_option:
+ description: Set Sonos sound options.
+ fields:
+ entity_id:
+ description: Name(s) of entities that will have options set.
+ example: 'media_player.living_room_sonos'
+ night_sound:
+ description: Enable Night Sound mode
+ example: 'true'
+ speech_enhance:
+ description: Enable Speech Enhancement mode
+ example: 'true'
soundtouch_play_everywhere:
description: Play on all Bose Soundtouch devices.
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index 47786e793ca..3bd3a722b46 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -19,7 +19,7 @@ from homeassistant.components.media_player import (
SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_CLEAR_PLAYLIST,
SUPPORT_SELECT_SOURCE, MediaPlayerDevice, PLATFORM_SCHEMA, SUPPORT_STOP,
- SUPPORT_PLAY)
+ SUPPORT_PLAY, SUPPORT_SHUFFLE_SET)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID,
CONF_HOSTS, ATTR_TIME)
@@ -27,7 +27,7 @@ from homeassistant.config import load_yaml_config_file
import homeassistant.helpers.config_validation as cv
from homeassistant.util.dt import utcnow
-REQUIREMENTS = ['SoCo==0.12']
+REQUIREMENTS = ['SoCo==0.13']
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ _REQUESTS_LOGGER.setLevel(logging.ERROR)
SUPPORT_SONOS = SUPPORT_STOP | SUPPORT_PAUSE | SUPPORT_VOLUME_SET |\
SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |\
SUPPORT_PLAY_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST |\
- SUPPORT_SELECT_SOURCE | SUPPORT_PLAY
+ SUPPORT_SELECT_SOURCE | SUPPORT_PLAY | SUPPORT_SHUFFLE_SET
SERVICE_JOIN = 'sonos_join'
SERVICE_UNJOIN = 'sonos_unjoin'
@@ -52,6 +52,7 @@ SERVICE_RESTORE = 'sonos_restore'
SERVICE_SET_TIMER = 'sonos_set_sleep_timer'
SERVICE_CLEAR_TIMER = 'sonos_clear_sleep_timer'
SERVICE_UPDATE_ALARM = 'sonos_update_alarm'
+SERVICE_SET_OPTION = 'sonos_set_option'
DATA_SONOS = 'sonos'
@@ -69,6 +70,8 @@ ATTR_ENABLED = 'enabled'
ATTR_INCLUDE_LINKED_ZONES = 'include_linked_zones'
ATTR_MASTER = 'master'
ATTR_WITH_GROUP = 'with_group'
+ATTR_NIGHT_SOUND = 'night_sound'
+ATTR_SPEECH_ENHANCE = 'speech_enhance'
ATTR_IS_COORDINATOR = 'is_coordinator'
@@ -105,6 +108,11 @@ SONOS_UPDATE_ALARM_SCHEMA = SONOS_SCHEMA.extend({
vol.Optional(ATTR_INCLUDE_LINKED_ZONES): cv.boolean,
})
+SONOS_SET_OPTION_SCHEMA = SONOS_SCHEMA.extend({
+ vol.Optional(ATTR_NIGHT_SOUND): cv.boolean,
+ vol.Optional(ATTR_SPEECH_ENHANCE): cv.boolean,
+})
+
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Sonos platform."""
@@ -140,7 +148,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
hosts = hosts.split(',') if isinstance(hosts, str) else hosts
players = []
for host in hosts:
- players.append(soco.SoCo(socket.gethostbyname(host)))
+ try:
+ players.append(soco.SoCo(socket.gethostbyname(host)))
+ except OSError:
+ _LOGGER.warning("Failed to initialize '%s'", host)
if not players:
players = soco.discover(
@@ -189,6 +200,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device.clear_sleep_timer()
elif service.service == SERVICE_UPDATE_ALARM:
device.update_alarm(**service.data)
+ elif service.service == SERVICE_SET_OPTION:
+ device.update_option(**service.data)
device.schedule_update_ha_state(True)
@@ -221,6 +234,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
descriptions.get(SERVICE_UPDATE_ALARM),
schema=SONOS_UPDATE_ALARM_SCHEMA)
+ hass.services.register(
+ DOMAIN, SERVICE_SET_OPTION, service_handle,
+ descriptions.get(SERVICE_SET_OPTION),
+ schema=SONOS_SET_OPTION_SCHEMA)
+
def _parse_timespan(timespan):
"""Parse a time-span into number of seconds."""
@@ -331,8 +349,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
+ self._support_shuffle_set = True
self._support_stop = False
self._support_pause = False
+ self._night_sound = None
+ self._speech_enhance = None
self._current_track_uri = None
self._current_track_is_radio_stream = False
self._queue = None
@@ -450,8 +471,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = False
self._support_next_track = False
self._support_play = False
+ self._support_shuffle_set = False
self._support_stop = False
self._support_pause = False
+ self._night_sound = None
+ self._speech_enhance = None
self._is_playing_tv = False
self._is_playing_line_in = False
self._source_name = None
@@ -524,6 +548,9 @@ class SonosDevice(MediaPlayerDevice):
media_position_updated_at = None
source_name = None
+ night_sound = self._player.night_mode
+ speech_enhance = self._player.dialog_mode
+
is_radio_stream = \
current_media_uri.startswith('x-sonosapi-stream:') or \
current_media_uri.startswith('x-rincon-mp3radio:')
@@ -536,6 +563,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = False
support_stop = True
support_pause = False
+ support_shuffle_set = False
if is_playing_tv:
media_artist = SUPPORT_SOURCE_TV
@@ -558,6 +586,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = True
support_stop = True
support_pause = False
+ support_shuffle_set = False
source_name = 'Radio'
# Check if currently playing radio station is in favorites
@@ -622,6 +651,7 @@ class SonosDevice(MediaPlayerDevice):
support_play = True
support_stop = True
support_pause = True
+ support_shuffle_set = True
position_info = self._player.avTransport.GetPositionInfo(
[('InstanceID', 0),
@@ -694,8 +724,11 @@ class SonosDevice(MediaPlayerDevice):
self._support_previous_track = support_previous_track
self._support_next_track = support_next_track
self._support_play = support_play
+ self._support_shuffle_set = support_shuffle_set
self._support_stop = support_stop
self._support_pause = support_pause
+ self._night_sound = night_sound
+ self._speech_enhance = speech_enhance
self._is_playing_tv = is_playing_tv
self._is_playing_line_in = is_playing_line_in
self._source_name = source_name
@@ -762,6 +795,11 @@ class SonosDevice(MediaPlayerDevice):
"""Return true if volume is muted."""
return self._player_volume_muted
+ @property
+ def shuffle(self):
+ """Shuffling state."""
+ return True if self._player.play_mode == 'SHUFFLE' else False
+
@property
def media_content_id(self):
"""Content ID of current playing media."""
@@ -834,6 +872,16 @@ class SonosDevice(MediaPlayerDevice):
return self._media_title
+ @property
+ def night_sound(self):
+ """Get status of Night Sound."""
+ return self._night_sound
+
+ @property
+ def speech_enhance(self):
+ """Get status of Speech Enhancement."""
+ return self._speech_enhance
+
@property
def supported_features(self):
"""Flag media player features that are supported."""
@@ -850,7 +898,8 @@ class SonosDevice(MediaPlayerDevice):
if not self._support_play:
supported = supported ^ SUPPORT_PLAY
-
+ if not self._support_shuffle_set:
+ supported = supported ^ SUPPORT_SHUFFLE_SET
if not self._support_stop:
supported = supported ^ SUPPORT_STOP
@@ -874,6 +923,11 @@ class SonosDevice(MediaPlayerDevice):
"""Set volume level, range 0..1."""
self._player.volume = str(int(volume * 100))
+ @soco_error
+ def set_shuffle(self, shuffle):
+ """Enable/Disable shuffle mode."""
+ self._player.play_mode = 'SHUFFLE' if shuffle else 'NORMAL'
+
@soco_error
def mute_volume(self, mute):
"""Mute (true) or unmute (false) media player."""
@@ -932,7 +986,6 @@ class SonosDevice(MediaPlayerDevice):
self._player.stop()
self._player.clear_queue()
- self._player.play_mode = 'NORMAL'
self._player.add_to_queue(didl)
@property
@@ -1160,7 +1213,24 @@ class SonosDevice(MediaPlayerDevice):
a.include_linked_zones = data[ATTR_INCLUDE_LINKED_ZONES]
a.save()
+ @soco_error
+ def update_option(self, **data):
+ """Modify playback options."""
+ if ATTR_NIGHT_SOUND in data and self.night_sound is not None:
+ self.soco.night_mode = data[ATTR_NIGHT_SOUND]
+
+ if ATTR_SPEECH_ENHANCE in data and self.speech_enhance is not None:
+ self.soco.dialog_mode = data[ATTR_SPEECH_ENHANCE]
+
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
- return {ATTR_IS_COORDINATOR: self.is_coordinator}
+ attributes = {ATTR_IS_COORDINATOR: self.is_coordinator}
+
+ if self.night_sound is not None:
+ attributes[ATTR_NIGHT_SOUND] = self.night_sound
+
+ if self.speech_enhance is not None:
+ attributes[ATTR_SPEECH_ENHANCE] = self.speech_enhance
+
+ return attributes
diff --git a/homeassistant/components/media_player/ue_smart_radio.py b/homeassistant/components/media_player/ue_smart_radio.py
new file mode 100644
index 00000000000..2684a819417
--- /dev/null
+++ b/homeassistant/components/media_player/ue_smart_radio.py
@@ -0,0 +1,207 @@
+"""
+Support for Logitech UE Smart Radios.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/media_player.ue_smart_radio/
+"""
+
+import logging
+import voluptuous as vol
+import requests
+
+from homeassistant.components.media_player import (
+ MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
+ SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
+ SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_SET,
+ SUPPORT_VOLUME_MUTE)
+from homeassistant.const import (
+ CONF_USERNAME, CONF_PASSWORD, STATE_OFF, STATE_IDLE, STATE_PLAYING,
+ STATE_PAUSED)
+import homeassistant.helpers.config_validation as cv
+
+_LOGGER = logging.getLogger(__name__)
+
+ICON = "mdi:radio"
+URL = "http://decibel.logitechmusic.com/jsonrpc.js"
+
+SUPPORT_UE_SMART_RADIO = SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP | \
+ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_TURN_ON | \
+ SUPPORT_TURN_OFF | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE
+
+PLAYBACK_DICT = {"play": STATE_PLAYING,
+ "pause": STATE_PAUSED,
+ "stop": STATE_IDLE}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+})
+
+
+def send_request(payload, session):
+ """Send request to radio."""
+ try:
+ request = requests.post(URL,
+ cookies={"sdi_squeezenetwork_session":
+ session},
+ json=payload, timeout=5)
+ except requests.exceptions.Timeout:
+ _LOGGER.error("Timed out when sending request")
+ except requests.exceptions.ConnectionError:
+ _LOGGER.error("An error occurred while connecting")
+ else:
+ return request.json()
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Logitech UE Smart Radio platform."""
+ email = config.get(CONF_USERNAME)
+ password = config.get(CONF_PASSWORD)
+
+ session_request = requests.post("https://www.uesmartradio.com/user/login",
+ data={"email": email, "password":
+ password})
+ session = session_request.cookies["sdi_squeezenetwork_session"]
+
+ player_request = send_request({"params": ["", ["serverstatus"]]}, session)
+ player_id = player_request["result"]["players_loop"][0]["playerid"]
+ player_name = player_request["result"]["players_loop"][0]["name"]
+
+ add_devices([UERadioDevice(session, player_id, player_name)])
+
+
+class UERadioDevice(MediaPlayerDevice):
+ """Representation of a Logitech UE Smart Radio device."""
+
+ def __init__(self, session, player_id, player_name):
+ """Initialize the Logitech UE Smart Radio device."""
+ self._session = session
+ self._player_id = player_id
+ self._name = player_name
+ self._state = None
+ self._volume = 0
+ self._last_volume = 0
+ self._media_title = None
+ self._media_artist = None
+ self._media_artwork_url = None
+
+ def send_command(self, command):
+ """Send command to radio."""
+ send_request({"method": "slim.request", "params":
+ [self._player_id, command]}, self._session)
+
+ def update(self):
+ """Get the latest details from the device."""
+ request = send_request({
+ "method": "slim.request", "params":
+ [self._player_id, ["status", "-", 1,
+ "tags:cgABbehldiqtyrSuoKLN"]]}, self._session)
+
+ if request["error"] is not None:
+ self._state = None
+ return
+
+ if request["result"]["power"] == 0:
+ self._state = STATE_OFF
+ else:
+ self._state = PLAYBACK_DICT[request["result"]["mode"]]
+
+ media_info = request["result"]["playlist_loop"][0]
+
+ self._volume = request["result"]["mixer volume"] / 100
+ self._media_artwork_url = media_info["artwork_url"]
+ self._media_title = media_info["title"]
+ if "artist" in media_info:
+ self._media_artist = media_info["artist"]
+ else:
+ self._media_artist = media_info.get("remote_title")
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ return self._state
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return ICON
+
+ @property
+ def is_volume_muted(self):
+ """Boolean if volume is currently muted."""
+ return True if self._volume <= 0 else False
+
+ @property
+ def volume_level(self):
+ """Volume level of the media player (0..1)."""
+ return self._volume
+
+ @property
+ def supported_features(self):
+ """Flag of features that are supported."""
+ return SUPPORT_UE_SMART_RADIO
+
+ @property
+ def media_content_type(self):
+ """Return the media content type."""
+ return MEDIA_TYPE_MUSIC
+
+ @property
+ def media_image_url(self):
+ """Image URL of current playing media."""
+ return self._media_artwork_url
+
+ @property
+ def media_artist(self):
+ """Artist of current playing media, music track only."""
+ return self._media_artist
+
+ @property
+ def media_title(self):
+ """Title of current playing media."""
+ return self._media_title
+
+ def turn_on(self):
+ """Turn on specified media player or all."""
+ self.send_command(["power", 1])
+
+ def turn_off(self):
+ """Turn off specified media player or all."""
+ self.send_command(["power", 0])
+
+ def media_play(self):
+ """Send the media player the command for play/pause."""
+ self.send_command(["play"])
+
+ def media_pause(self):
+ """Send the media player the command for pause."""
+ self.send_command(["pause"])
+
+ def media_stop(self):
+ """Send the media player the stop command."""
+ self.send_command(["stop"])
+
+ def media_previous_track(self):
+ """Send the media player the command for prev track."""
+ self.send_command(["button", "rew"])
+
+ def media_next_track(self):
+ """Send the media player the command for next track."""
+ self.send_command(["button", "fwd"])
+
+ def mute_volume(self, mute):
+ """Send mute command."""
+ if mute:
+ self._last_volume = self._volume
+ self.send_command(["mixer", "volume", 0])
+ else:
+ self.send_command(["mixer", "volume", self._last_volume * 100])
+
+ def set_volume_level(self, volume):
+ """Set volume level, range 0..1."""
+ self.send_command(["mixer", "volume", volume * 100])
diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py
index 0abdb90e67a..9d3e0b90fa4 100644
--- a/homeassistant/components/media_player/webostv.py
+++ b/homeassistant/components/media_player/webostv.py
@@ -322,17 +322,17 @@ class LgWebOSDevice(MediaPlayerDevice):
def select_source(self, source):
"""Select input source."""
- source = self._source_list.get(source)
- if source is None:
+ source_dict = self._source_list.get(source)
+ if source_dict is None:
_LOGGER.warning("Source %s not found for %s", source, self.name)
return
- self._current_source_id = self._source_list[source]['id']
- if source.get('title'):
- self._current_source = self._source_list[source]['title']
- self._client.launch_app(self._source_list[source]['id'])
- elif source.get('label'):
- self._current_source = self._source_list[source]['label']
- self._client.set_input(self._source_list[source]['id'])
+ self._current_source_id = source_dict['id']
+ if source_dict.get('title'):
+ self._current_source = source_dict['title']
+ self._client.launch_app(source_dict['id'])
+ elif source_dict.get('label'):
+ self._current_source = source_dict['label']
+ self._client.set_input(source_dict['id'])
def media_play(self):
"""Send play command."""
diff --git a/homeassistant/components/media_player/yamaha_musiccast.py b/homeassistant/components/media_player/yamaha_musiccast.py
index bfcffff6bb4..b42a5ae474c 100644
--- a/homeassistant/components/media_player/yamaha_musiccast.py
+++ b/homeassistant/components/media_player/yamaha_musiccast.py
@@ -36,7 +36,7 @@ SUPPORTED_FEATURES = (
KNOWN_HOSTS_KEY = 'data_yamaha_musiccast'
INTERVAL_SECONDS = 'interval_seconds'
-REQUIREMENTS = ['pymusiccast==0.1.5']
+REQUIREMENTS = ['pymusiccast==0.1.6']
DEFAULT_PORT = 5005
DEFAULT_INTERVAL = 480
diff --git a/homeassistant/components/mochad.py b/homeassistant/components/mochad.py
index 165c43f488f..3cc4eda7675 100644
--- a/homeassistant/components/mochad.py
+++ b/homeassistant/components/mochad.py
@@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mochad/
"""
import logging
+import threading
import voluptuous as vol
@@ -23,6 +24,8 @@ CONF_COMM_TYPE = 'comm_type'
DOMAIN = 'mochad'
+REQ_LOCK = threading.Lock()
+
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_HOST, default='localhost'): cv.string,
diff --git a/homeassistant/components/scene/vera.py b/homeassistant/components/scene/vera.py
new file mode 100644
index 00000000000..3dbb68d214f
--- /dev/null
+++ b/homeassistant/components/scene/vera.py
@@ -0,0 +1,60 @@
+"""
+Support for Vera scenes.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/scene.vera/
+"""
+import logging
+
+from homeassistant.util import slugify
+from homeassistant.components.scene import Scene
+from homeassistant.components.vera import (
+ VERA_CONTROLLER, VERA_SCENES, VERA_ID_FORMAT)
+
+DEPENDENCIES = ['vera']
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Vera scenes."""
+ add_devices(
+ [VeraScene(scene, hass.data[VERA_CONTROLLER])
+ for scene in hass.data[VERA_SCENES]], True)
+
+
+class VeraScene(Scene):
+ """Representation of a Vera scene entity."""
+
+ def __init__(self, vera_scene, controller):
+ """Initialize the scene."""
+ self.vera_scene = vera_scene
+ self.controller = controller
+
+ self._name = self.vera_scene.name
+ # Append device id to prevent name clashes in HA.
+ self.vera_id = VERA_ID_FORMAT.format(
+ slugify(vera_scene.name), vera_scene.scene_id)
+
+ def update(self):
+ """Update the scene status."""
+ self.vera_scene.refresh()
+
+ def activate(self, **kwargs):
+ """Activate the scene."""
+ self.vera_scene.activate()
+
+ @property
+ def name(self):
+ """Return the name of the scene."""
+ return self._name
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the scene."""
+ return {'vera_scene_id': self.vera_scene.vera_scene_id}
+
+ @property
+ def should_poll(self):
+ """Return that polling is not necessary."""
+ return False
diff --git a/homeassistant/components/sensor/alarmdecoder.py b/homeassistant/components/sensor/alarmdecoder.py
index 6b026298db0..ce709eee94c 100644
--- a/homeassistant/components/sensor/alarmdecoder.py
+++ b/homeassistant/components/sensor/alarmdecoder.py
@@ -7,25 +7,21 @@ https://home-assistant.io/components/sensor.alarmdecoder/
import asyncio
import logging
-from homeassistant.core import callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.components.alarmdecoder import (SIGNAL_PANEL_MESSAGE)
-from homeassistant.const import (STATE_UNKNOWN)
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['alarmdecoder']
-@asyncio.coroutine
-def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up for AlarmDecoder sensor devices."""
- _LOGGER.debug("AlarmDecoderSensor: async_setup_platform")
+ _LOGGER.debug("AlarmDecoderSensor: setup_platform")
device = AlarmDecoderSensor(hass)
- async_add_devices([device])
+ add_devices([device])
class AlarmDecoderSensor(Entity):
@@ -34,23 +30,20 @@ class AlarmDecoderSensor(Entity):
def __init__(self, hass):
"""Initialize the alarm panel."""
self._display = ""
- self._state = STATE_UNKNOWN
+ self._state = None
self._icon = 'mdi:alarm-check'
self._name = 'Alarm Panel Display'
- _LOGGER.debug("Setting up panel")
-
@asyncio.coroutine
def async_added_to_hass(self):
"""Register callbacks."""
- async_dispatcher_connect(
- self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ SIGNAL_PANEL_MESSAGE, self._message_callback)
- @callback
def _message_callback(self, message):
if self._display != message.text:
self._display = message.text
- self.async_schedule_update_ha_state()
+ self.schedule_update_ha_state()
@property
def icon(self):
diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py
new file mode 100644
index 00000000000..b0d2c27ae5d
--- /dev/null
+++ b/homeassistant/components/sensor/canary.py
@@ -0,0 +1,85 @@
+"""
+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.helpers.entity import Entity
+
+DEPENDENCIES = ['canary']
+
+SENSOR_VALUE_PRECISION = 1
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up the Canary sensors."""
+ 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:
+ devices.append(CanarySensor(data, sensor_type, location,
+ device))
+
+ add_devices(devices, True)
+
+
+class CanarySensor(Entity):
+ """Representation of a Canary sensor."""
+
+ def __init__(self, data, sensor_type, location, device):
+ """Initialize the sensor."""
+ 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()
+ self._name = '{} {} {}'.format(location.name,
+ device.name,
+ sensor_type_name)
+
+ @property
+ def name(self):
+ """Return the name of the Canary sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._sensor_value
+
+ @property
+ def unique_id(self):
+ """Return the unique ID of this sensor."""
+ return "sensor_canary_{}_{}".format(self._device_id,
+ self._sensor_type.value)
+
+ @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 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)
+ if value is not None:
+ self._sensor_value = round(float(value), SENSOR_VALUE_PRECISION)
diff --git a/homeassistant/components/sensor/discogs.py b/homeassistant/components/sensor/discogs.py
new file mode 100644
index 00000000000..2920dc025d7
--- /dev/null
+++ b/homeassistant/components/sensor/discogs.py
@@ -0,0 +1,97 @@
+"""
+Show the amount of records in a user's Discogs collection.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.discogs/
+"""
+import asyncio
+from datetime import timedelta
+import logging
+
+import voluptuous as vol
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TOKEN
+from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+
+REQUIREMENTS = ['discogs_client==2.2.1']
+
+_LOGGER = logging.getLogger(__name__)
+
+ATTR_IDENTITY = 'identity'
+
+CONF_ATTRIBUTION = "Data provided by Discogs"
+
+DEFAULT_NAME = 'Discogs'
+
+ICON = 'mdi:album'
+
+SCAN_INTERVAL = timedelta(hours=2)
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_TOKEN): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+ """Set up the Discogs sensor."""
+ import discogs_client
+
+ name = config.get(CONF_NAME)
+ token = config.get(CONF_TOKEN)
+
+ try:
+ discogs = discogs_client.Client(SERVER_SOFTWARE, user_token=token)
+ identity = discogs.identity()
+ except discogs_client.exceptions.HTTPError:
+ _LOGGER.error("API token is not valid")
+ return
+
+ async_add_devices([DiscogsSensor(identity, name)], True)
+
+
+class DiscogsSensor(Entity):
+ """Get a user's number of records in collection."""
+
+ def __init__(self, identity, name):
+ """Initialize the Discogs sensor."""
+ self._identity = identity
+ self._name = name
+ self._state = None
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def icon(self):
+ """Return the icon to use in the frontend, if any."""
+ return ICON
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit this state is expressed in."""
+ return 'records'
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the sensor."""
+ return {
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
+ ATTR_IDENTITY: self._identity.name,
+ }
+
+ @asyncio.coroutine
+ def async_update(self):
+ """Set state to the amount of records in user's collection."""
+ self._state = self._identity.num_collection
diff --git a/homeassistant/components/sensor/efergy.py b/homeassistant/components/sensor/efergy.py
index 0f24905c5f5..c14a33dce01 100644
--- a/homeassistant/components/sensor/efergy.py
+++ b/homeassistant/components/sensor/efergy.py
@@ -31,6 +31,7 @@ CONF_COST = 'cost'
CONF_CURRENT_VALUES = 'current_values'
DEFAULT_PERIOD = 'year'
+DEFAULT_UTC_OFFSET = '0'
SENSOR_TYPES = {
CONF_INSTANT: ['Energy Usage', 'W'],
@@ -50,7 +51,7 @@ SENSORS_SCHEMA = vol.Schema({
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_APPTOKEN): cv.string,
- vol.Optional(CONF_UTC_OFFSET): cv.string,
+ vol.Optional(CONF_UTC_OFFSET, default=DEFAULT_UTC_OFFSET): cv.string,
vol.Required(CONF_MONITORED_VARIABLES): [SENSORS_SCHEMA]
})
diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py
index d857ce57fce..d4dea54514a 100644
--- a/homeassistant/components/sensor/hydroquebec.py
+++ b/homeassistant/components/sensor/hydroquebec.py
@@ -7,10 +7,10 @@ https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.hydroquebec/
"""
+import asyncio
import logging
from datetime import timedelta
-import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
@@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['pyhydroquebec==1.3.1']
+REQUIREMENTS = ['pyhydroquebec==2.0.2']
_LOGGER = logging.getLogger(__name__)
@@ -93,7 +93,8 @@ DAILY_MAP = (('yesterday_total_consumption', 'consoTotalQuot'),
('yesterday_higher_price_consumption', 'consoHautQuot'))
-def setup_platform(hass, config, add_devices, discovery_info=None):
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the HydroQuebec sensor."""
# Create a data fetcher to support all of the configured sensors. Then make
# the first call to init the data.
@@ -102,13 +103,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
password = config.get(CONF_PASSWORD)
contract = config.get(CONF_CONTRACT)
- try:
- hydroquebec_data = HydroquebecData(username, password, contract)
- _LOGGER.info("Contract list: %s",
- ", ".join(hydroquebec_data.get_contract_list()))
- except requests.exceptions.HTTPError as error:
- _LOGGER.error("Failt login: %s", error)
- return False
+ hydroquebec_data = HydroquebecData(username, password, contract)
+ contracts = yield from hydroquebec_data.get_contract_list()
+ _LOGGER.info("Contract list: %s",
+ ", ".join(contracts))
name = config.get(CONF_NAME)
@@ -116,7 +114,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
for variable in config[CONF_MONITORED_VARIABLES]:
sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name))
- add_devices(sensors)
+ async_add_devices(sensors, True)
class HydroQuebecSensor(Entity):
@@ -152,10 +150,11 @@ class HydroQuebecSensor(Entity):
"""Icon to use in the frontend, if any."""
return self._icon
- def update(self):
+ @asyncio.coroutine
+ def async_update(self):
"""Get the latest data from Hydroquebec and update the state."""
- self.hydroquebec_data.update()
- if self.type in self.hydroquebec_data.data:
+ yield from self.hydroquebec_data.async_update()
+ if self.hydroquebec_data.data.get(self.type) is not None:
self._state = round(self.hydroquebec_data.data[self.type], 2)
@@ -170,23 +169,24 @@ class HydroquebecData(object):
self._contract = contract
self.data = {}
+ @asyncio.coroutine
def get_contract_list(self):
"""Return the contract list."""
# Fetch data
- self._fetch_data()
+ yield from self._fetch_data()
return self.client.get_contracts()
+ @asyncio.coroutine
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
def _fetch_data(self):
"""Fetch latest data from HydroQuebec."""
- from pyhydroquebec.client import PyHydroQuebecError
try:
- self.client.fetch_data()
- except PyHydroQuebecError as exp:
+ yield from self.client.fetch_data()
+ except BaseException as exp:
_LOGGER.error("Error on receive last Hydroquebec data: %s", exp)
- return
- @Throttle(MIN_TIME_BETWEEN_UPDATES)
- def update(self):
+ @asyncio.coroutine
+ def async_update(self):
"""Return the latest collected data from HydroQuebec."""
- self._fetch_data()
+ yield from self._fetch_data()
self.data = self.client.get_data(self._contract)[self._contract]
diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py
index f64fa6191e2..e961c63a1b5 100644
--- a/homeassistant/components/sensor/isy994.py
+++ b/homeassistant/components/sensor/isy994.py
@@ -282,6 +282,9 @@ class ISYSensorDevice(isy.ISYDevice):
@property
def state(self) -> str:
"""Get the state of the ISY994 sensor device."""
+ if self.is_unknown():
+ return None
+
if len(self._node.uom) == 1:
if self._node.uom[0] in UOM_TO_STATES:
states = UOM_TO_STATES.get(self._node.uom[0])
diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py
index e317e89030f..8c5fcc15ec2 100644
--- a/homeassistant/components/sensor/luftdaten.py
+++ b/homeassistant/components/sensor/luftdaten.py
@@ -5,85 +5,94 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.luftdaten/
"""
import asyncio
-import json
-import logging
from datetime import timedelta
+import logging
-import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_NAME, CONF_RESOURCE, CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS,
- TEMP_CELSIUS)
-from homeassistant.helpers.entity import Entity
+ ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS)
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['luftdaten==0.1.1']
_LOGGER = logging.getLogger(__name__)
+ATTR_SENSOR_ID = 'sensor_id'
+
+CONF_ATTRIBUTION = "Data provided by luftdaten.info"
+
+
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
SENSOR_TEMPERATURE = 'temperature'
SENSOR_HUMIDITY = 'humidity'
SENSOR_PM10 = 'P1'
SENSOR_PM2_5 = 'P2'
+SENSOR_PRESSURE = 'pressure'
SENSOR_TYPES = {
SENSOR_TEMPERATURE: ['Temperature', TEMP_CELSIUS],
SENSOR_HUMIDITY: ['Humidity', '%'],
+ SENSOR_PRESSURE: ['Pressure', 'Pa'],
SENSOR_PM10: ['PM10', VOLUME_MICROGRAMS_PER_CUBIC_METER],
SENSOR_PM2_5: ['PM2.5', VOLUME_MICROGRAMS_PER_CUBIC_METER]
}
-DEFAULT_NAME = 'Luftdaten Sensor'
-DEFAULT_RESOURCE = 'https://api.luftdaten.info/v1/sensor/'
-DEFAULT_VERIFY_SSL = True
+DEFAULT_NAME = 'Luftdaten'
CONF_SENSORID = 'sensorid'
-SCAN_INTERVAL = timedelta(minutes=3)
+MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORID): cv.positive_int,
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
- vol.Optional(CONF_RESOURCE, default=DEFAULT_RESOURCE): cv.string,
- vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Luftdaten sensor."""
+ from luftdaten import Luftdaten
+
name = config.get(CONF_NAME)
- sensorid = config.get(CONF_SENSORID)
- verify_ssl = config.get(CONF_VERIFY_SSL)
+ sensor_id = config.get(CONF_SENSORID)
- resource = '{}{}/'.format(config.get(CONF_RESOURCE), sensorid)
+ session = async_get_clientsession(hass)
+ luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session))
- rest_client = LuftdatenData(resource, verify_ssl)
- rest_client.update()
+ yield from luftdaten.async_update()
- if rest_client.data is None:
- _LOGGER.error("Unable to fetch Luftdaten data")
- return False
+ if luftdaten.data is None:
+ _LOGGER.error("Sensor is not available: %s", sensor_id)
+ return
devices = []
for variable in config[CONF_MONITORED_CONDITIONS]:
- devices.append(LuftdatenSensor(rest_client, name, variable))
+ if luftdaten.data.values[variable] is None:
+ _LOGGER.warning("It might be that sensor %s is not providing "
+ "measurements for %s", sensor_id, variable)
+ devices.append(LuftdatenSensor(luftdaten, name, variable, sensor_id))
- async_add_devices(devices, True)
+ async_add_devices(devices)
class LuftdatenSensor(Entity):
- """Implementation of a LuftdatenSensor sensor."""
+ """Implementation of a Luftdaten sensor."""
- def __init__(self, rest_client, name, sensor_type):
- """Initialize the LuftdatenSensor sensor."""
- self.rest_client = rest_client
+ def __init__(self, luftdaten, name, sensor_type, sensor_id):
+ """Initialize the Luftdaten sensor."""
+ self.luftdaten = luftdaten
self._name = name
self._state = None
+ self._sensor_id = sensor_id
self.sensor_type = sensor_type
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
@@ -95,48 +104,50 @@ class LuftdatenSensor(Entity):
@property
def state(self):
"""Return the state of the device."""
- return self._state
+ return self.luftdaten.data.values[self.sensor_type]
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
- def update(self):
- """Get the latest data from REST API and update the state."""
- self.rest_client.update()
- value = self.rest_client.data
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ if self.luftdaten.data.meta is None:
+ return
- if value is None:
- self._state = None
- else:
- parsed_json = json.loads(value)
+ attr = {
+ ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
+ ATTR_SENSOR_ID: self._sensor_id,
+ 'lat': self.luftdaten.data.meta['latitude'],
+ 'long': self.luftdaten.data.meta['longitude'],
+ }
+ return attr
- log_entries_count = len(parsed_json) - 1
- latest_log_entry = parsed_json[log_entries_count]
- sensordata_values = latest_log_entry['sensordatavalues']
- for sensordata_value in sensordata_values:
- if sensordata_value['value_type'] == self.sensor_type:
- self._state = sensordata_value['value']
+ @asyncio.coroutine
+ def async_update(self):
+ """Get the latest data from luftdaten.info and update the state."""
+ try:
+ yield from self.luftdaten.async_update()
+ except TypeError:
+ pass
class LuftdatenData(object):
"""Class for handling the data retrieval."""
- def __init__(self, resource, verify_ssl):
+ def __init__(self, data):
"""Initialize the data object."""
- self._request = requests.Request('GET', resource).prepare()
- self._verify_ssl = verify_ssl
- self.data = None
+ self.data = data
+
+ @Throttle(MIN_TIME_BETWEEN_UPDATES)
+ @asyncio.coroutine
+ def async_update(self):
+ """Get the latest data from luftdaten.info."""
+ from luftdaten.exceptions import LuftdatenError
- def update(self):
- """Get the latest data from Luftdaten service."""
try:
- with requests.Session() as sess:
- response = sess.send(
- self._request, timeout=10, verify=self._verify_ssl)
-
- self.data = response.text
- except requests.exceptions.RequestException:
- _LOGGER.error("Error fetching data: %s", self._request)
- self.data = None
+ yield from self.data.async_get_data()
+ except LuftdatenError:
+ _LOGGER.error("Unable to retrieve data from luftdaten.info")
diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py
index 063c4e8068e..77d77949ebd 100644
--- a/homeassistant/components/sensor/miflora.py
+++ b/homeassistant/components/sensor/miflora.py
@@ -12,15 +12,15 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
- CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC)
+ CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC
+)
-REQUIREMENTS = ['miflora==0.1.16']
+REQUIREMENTS = ['miflora==0.2.0']
_LOGGER = logging.getLogger(__name__)
CONF_ADAPTER = 'adapter'
CONF_CACHE = 'cache_value'
-CONF_FORCE_UPDATE = 'force_update'
CONF_MEDIAN = 'median'
CONF_RETRIES = 'retries'
CONF_TIMEOUT = 'timeout'
@@ -60,11 +60,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the MiFlora sensor."""
from miflora import miflora_poller
+ from miflora.backends.gatttool import GatttoolBackend
cache = config.get(CONF_CACHE)
poller = miflora_poller.MiFloraPoller(
config.get(CONF_MAC), cache_timeout=cache,
- adapter=config.get(CONF_ADAPTER))
+ adapter=config.get(CONF_ADAPTER), backend=GatttoolBackend)
force_update = config.get(CONF_FORCE_UPDATE)
median = config.get(CONF_MEDIAN)
poller.ble_timeout = config.get(CONF_TIMEOUT)
diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py
index 70b1294c13f..bf7de94b5d7 100644
--- a/homeassistant/components/sensor/mqtt.py
+++ b/homeassistant/components/sensor/mqtt.py
@@ -13,7 +13,8 @@ import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS
from homeassistant.const import (
- CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT)
+ CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN,
+ CONF_UNIT_OF_MEASUREMENT)
from homeassistant.helpers.entity import Entity
import homeassistant.components.mqtt as mqtt
import homeassistant.helpers.config_validation as cv
@@ -22,7 +23,6 @@ from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
-CONF_FORCE_UPDATE = 'force_update'
CONF_EXPIRE_AFTER = 'expire_after'
DEFAULT_NAME = 'MQTT Sensor'
diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py
index 85b388a1919..71b72b0a671 100644
--- a/homeassistant/components/sensor/octoprint.py
+++ b/homeassistant/components/sensor/octoprint.py
@@ -45,7 +45,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
name = config.get(CONF_NAME)
monitored_conditions = config.get(CONF_MONITORED_CONDITIONS)
tools = octoprint_api.get_tools()
- _LOGGER.error(str(tools))
if "Temperatures" in monitored_conditions:
if not tools:
diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py
index 0a75d0395ec..a40aeee55e5 100644
--- a/homeassistant/components/sensor/plex.py
+++ b/homeassistant/components/sensor/plex.py
@@ -10,7 +10,8 @@ import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN)
+ CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN,
+ CONF_SSL)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv
@@ -24,6 +25,7 @@ CONF_SERVER = 'server'
DEFAULT_HOST = 'localhost'
DEFAULT_NAME = 'Plex'
DEFAULT_PORT = 32400
+DEFAULT_SSL = False
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
@@ -35,6 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SERVER): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
+ vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
})
@@ -48,11 +51,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
plex_host = config.get(CONF_HOST)
plex_port = config.get(CONF_PORT)
plex_token = config.get(CONF_TOKEN)
- plex_url = 'http://{}:{}'.format(plex_host, plex_port)
- add_devices([PlexSensor(
- name, plex_url, plex_user, plex_password, plex_server,
- plex_token)], True)
+ plex_url = '{}://{}:{}'.format('https' if config.get(CONF_SSL) else 'http',
+ plex_host, plex_port)
+
+ import plexapi.exceptions
+
+ try:
+ add_devices([PlexSensor(
+ name, plex_url, plex_user, plex_password, plex_server,
+ plex_token)], True)
+ except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized,
+ plexapi.exceptions.NotFound) as error:
+ _LOGGER.error(error)
+ return
class PlexSensor(Entity):
diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py
index 86362e8f2d9..19f5a1c271e 100644
--- a/homeassistant/components/sensor/rest.py
+++ b/homeassistant/components/sensor/rest.py
@@ -13,10 +13,11 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
- CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE,
- CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_VERIFY_SSL, CONF_USERNAME,
- CONF_PASSWORD, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
- HTTP_DIGEST_AUTHENTICATION, CONF_HEADERS)
+ CONF_AUTHENTICATION, CONF_FORCE_UPDATE, CONF_HEADERS, CONF_NAME,
+ CONF_METHOD, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE,
+ CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME,
+ CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL,
+ HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, STATE_UNKNOWN)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
@@ -25,6 +26,7 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_METHOD = 'GET'
DEFAULT_NAME = 'REST Sensor'
DEFAULT_VERIFY_SSL = True
+DEFAULT_FORCE_UPDATE = False
CONF_JSON_ATTRS = 'json_attributes'
METHODS = ['POST', 'GET']
@@ -43,6 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
+ vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
})
@@ -59,6 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
unit = config.get(CONF_UNIT_OF_MEASUREMENT)
value_template = config.get(CONF_VALUE_TEMPLATE)
json_attrs = config.get(CONF_JSON_ATTRS)
+ force_update = config.get(CONF_FORCE_UPDATE)
if value_template is not None:
value_template.hass = hass
@@ -74,14 +78,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
rest.update()
add_devices([RestSensor(
- hass, rest, name, unit, value_template, json_attrs)], True)
+ hass, rest, name, unit, value_template, json_attrs, force_update
+ )], True)
class RestSensor(Entity):
"""Implementation of a REST sensor."""
- def __init__(self, hass, rest, name,
- unit_of_measurement, value_template, json_attrs):
+ def __init__(self, hass, rest, name, unit_of_measurement,
+ value_template, json_attrs, force_update):
"""Initialize the REST sensor."""
self._hass = hass
self.rest = rest
@@ -91,6 +96,7 @@ class RestSensor(Entity):
self._value_template = value_template
self._json_attrs = json_attrs
self._attributes = None
+ self._force_update = force_update
@property
def name(self):
@@ -112,6 +118,11 @@ class RestSensor(Entity):
"""Return the state of the device."""
return self._state
+ @property
+ def force_update(self):
+ """Force update."""
+ return self._force_update
+
def update(self):
"""Get the latest data from REST API and update the state."""
self.rest.update()
diff --git a/homeassistant/components/sensor/ripple.py b/homeassistant/components/sensor/ripple.py
index 6f92a1a3390..d516706fdc0 100644
--- a/homeassistant/components/sensor/ripple.py
+++ b/homeassistant/components/sensor/ripple.py
@@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['python-ripple-api==0.0.2']
+REQUIREMENTS = ['python-ripple-api==0.0.3']
CONF_ADDRESS = 'address'
CONF_ATTRIBUTION = "Data provided by ripple.com"
@@ -71,4 +71,6 @@ class RippleSensor(Entity):
def update(self):
"""Get the latest state of the sensor."""
from pyripple import get_balance
- self._state = get_balance(self.address)
+ balance = get_balance(self.address)
+ if balance is not None:
+ self._state = balance
diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py
index 3d86d940f4d..720158e1029 100644
--- a/homeassistant/components/sensor/shodan.py
+++ b/homeassistant/components/sensor/shodan.py
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['shodan==1.7.5']
+REQUIREMENTS = ['shodan==1.7.7']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py
index a6932e2aebb..19281d36d88 100644
--- a/homeassistant/components/sensor/statistics.py
+++ b/homeassistant/components/sensor/statistics.py
@@ -175,15 +175,20 @@ class StatisticsSensor(Entity):
self._purge_old()
if not self.is_binary:
- try:
+ try: # require only one data point
self.mean = round(statistics.mean(self.states), 2)
self.median = round(statistics.median(self.states), 2)
+ except statistics.StatisticsError as err:
+ _LOGGER.error(err)
+ self.mean = self.median = STATE_UNKNOWN
+
+ try: # require at least two data points
self.stdev = round(statistics.stdev(self.states), 2)
self.variance = round(statistics.variance(self.states), 2)
except statistics.StatisticsError as err:
_LOGGER.error(err)
- self.mean = self.median = STATE_UNKNOWN
self.stdev = self.variance = STATE_UNKNOWN
+
if self.states:
self.total = round(sum(self.states), 2)
self.min = min(self.states)
diff --git a/homeassistant/components/sensor/steam_online.py b/homeassistant/components/sensor/steam_online.py
index 8645d4ee7c6..88cb786e66d 100644
--- a/homeassistant/components/sensor/steam_online.py
+++ b/homeassistant/components/sensor/steam_online.py
@@ -21,12 +21,13 @@ CONF_ACCOUNTS = 'accounts'
ICON = 'mdi:steam'
-STATE_ONLINE = 'Online'
-STATE_BUSY = 'Busy'
-STATE_AWAY = 'Away'
-STATE_SNOOZE = 'Snooze'
-STATE_TRADE = 'Trade'
-STATE_PLAY = 'Play'
+STATE_OFFLINE = 'offline'
+STATE_ONLINE = 'online'
+STATE_BUSY = 'busy'
+STATE_AWAY = 'away'
+STATE_SNOOZE = 'snooze'
+STATE_LOOKING_TO_TRADE = 'looking_to_trade'
+STATE_LOOKING_TO_PLAY = 'looking_to_play'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
@@ -40,17 +41,23 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Steam platform."""
import steam as steamod
steamod.api.key.set(config.get(CONF_API_KEY))
+ # Initialize steammods app list before creating sensors
+ # to benefit from internal caching of the list.
+ steam_app_list = steamod.apps.app_list()
add_devices(
[SteamSensor(account,
- steamod) for account in config.get(CONF_ACCOUNTS)], True)
+ steamod,
+ steam_app_list)
+ for account in config.get(CONF_ACCOUNTS)], True)
class SteamSensor(Entity):
"""A class for the Steam account."""
- def __init__(self, account, steamod):
+ def __init__(self, account, steamod, steam_app_list):
"""Initialize the sensor."""
self._steamod = steamod
+ self._steam_app_list = steam_app_list
self._account = account
self._profile = None
self._game = self._state = self._name = self._avatar = None
@@ -75,28 +82,39 @@ class SteamSensor(Entity):
"""Update device state."""
try:
self._profile = self._steamod.user.profile(self._account)
- if self._profile.current_game[2] is None:
- self._game = 'None'
- else:
- self._game = self._profile.current_game[2]
+ self._game = self._get_current_game()
self._state = {
1: STATE_ONLINE,
2: STATE_BUSY,
3: STATE_AWAY,
4: STATE_SNOOZE,
- 5: STATE_TRADE,
- 6: STATE_PLAY,
- }.get(self._profile.status, 'Offline')
+ 5: STATE_LOOKING_TO_TRADE,
+ 6: STATE_LOOKING_TO_PLAY,
+ }.get(self._profile.status, STATE_OFFLINE)
self._name = self._profile.persona
self._avatar = self._profile.avatar_medium
except self._steamod.api.HTTPTimeoutError as error:
_LOGGER.warning(error)
self._game = self._state = self._name = self._avatar = None
+ def _get_current_game(self):
+ game_id = self._profile.current_game[0]
+ game_extra_info = self._profile.current_game[2]
+
+ if game_extra_info:
+ return game_extra_info
+
+ if game_id and game_id in self._steam_app_list:
+ # The app list always returns a tuple
+ # with the game id and the game name
+ return self._steam_app_list[game_id][1]
+
+ return None
+
@property
def device_state_attributes(self):
"""Return the state attributes."""
- return {'game': self._game}
+ return {'game': self._game} if self._game else None
@property
def entity_picture(self):
diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py
index 324d3029c99..8e6f7b404fd 100755
--- a/homeassistant/components/sensor/systemmonitor.py
+++ b/homeassistant/components/sensor/systemmonitor.py
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['psutil==5.4.1']
+REQUIREMENTS = ['psutil==5.4.2']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py
index f901bd27dca..c81c208e33e 100644
--- a/homeassistant/components/sensor/vera.py
+++ b/homeassistant/components/sensor/vera.py
@@ -25,8 +25,8 @@ SCAN_INTERVAL = timedelta(seconds=5)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera controller devices."""
add_devices(
- VeraSensor(device, VERA_CONTROLLER)
- for device in VERA_DEVICES['sensor'])
+ VeraSensor(device, hass.data[VERA_CONTROLLER])
+ for device in hass.data[VERA_DEVICES]['sensor'])
class VeraSensor(VeraDevice, Entity):
diff --git a/homeassistant/components/sensor/volvooncall.py b/homeassistant/components/sensor/volvooncall.py
index 622261941d6..32b228ca1f9 100644
--- a/homeassistant/components/sensor/volvooncall.py
+++ b/homeassistant/components/sensor/volvooncall.py
@@ -6,8 +6,10 @@ https://home-assistant.io/components/sensor.volvooncall/
"""
import logging
+from math import floor
-from homeassistant.components.volvooncall import VolvoEntity, RESOURCES
+from homeassistant.components.volvooncall import (
+ VolvoEntity, RESOURCES, CONF_SCANDINAVIAN_MILES)
_LOGGER = logging.getLogger(__name__)
@@ -26,14 +28,37 @@ class VolvoSensor(VolvoEntity):
def state(self):
"""Return the state of the sensor."""
val = getattr(self.vehicle, self._attribute)
+
+ if val is None:
+ return val
+
if self._attribute == 'odometer':
- return round(val / 1000) # km
- return val
+ val /= 1000 # m -> km
+
+ if 'mil' in self.unit_of_measurement:
+ val /= 10 # km -> mil
+
+ if self._attribute == 'average_fuel_consumption':
+ val /= 10 # L/1000km -> L/100km
+ if 'mil' in self.unit_of_measurement:
+ return round(val, 2)
+ else:
+ return round(val, 1)
+ elif self._attribute == 'distance_to_empty':
+ return int(floor(val))
+ else:
+ return int(round(val))
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
- return RESOURCES[self._attribute][3]
+ unit = RESOURCES[self._attribute][3]
+ if self._state.config[CONF_SCANDINAVIAN_MILES] and 'km' in unit:
+ if self._attribute == 'average_fuel_consumption':
+ return 'L/mil'
+ else:
+ return unit.replace('km', 'mil')
+ return unit
@property
def icon(self):
diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml
index c532c0dfd20..90a1bbbc613 100644
--- a/homeassistant/components/services.yaml
+++ b/homeassistant/components/services.yaml
@@ -32,55 +32,6 @@ foursquare:
description: Vertical accuracy of the user's location, in meters.
example: 1
-homematic:
- virtualkey:
- description: Press a virtual key from CCU/Homegear or simulate keypress.
- fields:
- address:
- description: Address of homematic device or BidCoS-RF for virtual remote.
- example: BidCoS-RF
- channel:
- description: Channel for calling a keypress.
- example: 1
- param:
- description: Event to send i.e. PRESS_LONG, PRESS_SHORT.
- example: PRESS_LONG
- proxy:
- description: (Optional) for set a hosts value.
- example: Hosts name from config
- set_var_value:
- description: Set the name of a node.
- fields:
- entity_id:
- description: Name(s) of homematic central to set value.
- example: 'homematic.ccu2'
- name:
- description: Name of the variable to set.
- example: 'testvariable'
- value:
- description: New value
- example: 1
- set_dev_value:
- description: Set a device property on RPC XML interface.
- fields:
- address:
- description: Address of homematic device or BidCoS-RF for virtual remote
- example: BidCoS-RF
- channel:
- description: Channel for calling a keypress
- example: 1
- param:
- description: Event to send i.e. PRESS_LONG, PRESS_SHORT
- example: PRESS_LONG
- proxy:
- description: (Optional) for set a hosts value
- example: Hosts name from config
- value:
- description: New value
- example: 1
- reconnect:
- description: Reconnect to all Homematic Hubs.
-
microsoft_face:
create_group:
description: Create a new person group.
diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py
index 1f64f78e9c8..a302f25bd00 100644
--- a/homeassistant/components/snips.py
+++ b/homeassistant/components/snips.py
@@ -15,7 +15,7 @@ DEPENDENCIES = ['mqtt']
CONF_INTENTS = 'intents'
CONF_ACTION = 'action'
-INTENT_TOPIC = 'hermes/nlu/intentParsed'
+INTENT_TOPIC = 'hermes/intent/#'
_LOGGER = logging.getLogger(__name__)
@@ -32,7 +32,8 @@ INTENT_SCHEMA = vol.Schema({
vol.Required('slotName'): str,
vol.Required('value'): {
vol.Required('kind'): str,
- vol.Required('value'): cv.match_all
+ vol.Optional('value'): cv.match_all,
+ vol.Optional('rawValue'): cv.match_all
}
}]
}, extra=vol.ALLOW_EXTRA)
@@ -59,8 +60,12 @@ def async_setup(hass, config):
return
intent_type = request['intent']['intentName'].split('__')[-1]
- slots = {slot['slotName']: {'value': slot['value']['value']}
- for slot in request.get('slots', [])}
+ slots = {}
+ for slot in request.get('slots', []):
+ if 'value' in slot['value']:
+ slots[slot['slotName']] = {'value': slot['value']['value']}
+ else:
+ slots[slot['slotName']] = {'value': slot['rawValue']}
try:
yield from intent.async_handle(
diff --git a/homeassistant/components/switch/isy994.py b/homeassistant/components/switch/isy994.py
index b930bedc2c7..0f1ec62eaee 100644
--- a/homeassistant/components/switch/isy994.py
+++ b/homeassistant/components/switch/isy994.py
@@ -69,7 +69,10 @@ class ISYSwitchDevice(isy.ISYDevice, SwitchDevice):
@property
def state(self) -> str:
"""Get the state of the ISY994 device."""
- return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
+ if self.is_unknown():
+ return None
+ else:
+ return VALUE_TO_STATE.get(bool(self.value), STATE_UNKNOWN)
def turn_off(self, **kwargs) -> None:
"""Send the turn on command to the ISY994 switch."""
diff --git a/homeassistant/components/switch/mochad.py b/homeassistant/components/switch/mochad.py
index a67b27a6a91..da8f96dc1f0 100644
--- a/homeassistant/components/switch/mochad.py
+++ b/homeassistant/components/switch/mochad.py
@@ -60,18 +60,21 @@ class MochadSwitch(SwitchDevice):
def turn_on(self, **kwargs):
"""Turn the switch on."""
self._state = True
- self.device.send_cmd('on')
- self._controller.read_data()
+ with mochad.REQ_LOCK:
+ self.device.send_cmd('on')
+ self._controller.read_data()
def turn_off(self, **kwargs):
"""Turn the switch off."""
self._state = False
- self.device.send_cmd('off')
- self._controller.read_data()
+ with mochad.REQ_LOCK:
+ self.device.send_cmd('off')
+ self._controller.read_data()
def _get_device_status(self):
"""Get the status of the switch from mochad."""
- status = self.device.get_status().rstrip()
+ with mochad.REQ_LOCK:
+ status = self.device.get_status().rstrip()
return status == 'on'
@property
diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py
index c731b336dfb..211ff54d5a4 100644
--- a/homeassistant/components/switch/modbus.py
+++ b/homeassistant/components/switch/modbus.py
@@ -141,10 +141,17 @@ class ModbusRegisterSwitch(ModbusCoilSwitch):
self._verify_register = (
verify_register if verify_register else self._register)
self._register_type = register_type
- self._state_on = (
- state_on if state_on else self._command_on)
- self._state_off = (
- state_off if state_off else self._command_off)
+
+ if state_on is not None:
+ self._state_on = state_on
+ else:
+ self._state_on = self._command_on
+
+ if state_off is not None:
+ self._state_off = state_off
+ else:
+ self._state_off = self._command_off
+
self._is_on = None
def turn_on(self, **kwargs):
diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py
index 1e92612b9a9..d7c284e4ccf 100644
--- a/homeassistant/components/switch/vera.py
+++ b/homeassistant/components/switch/vera.py
@@ -19,8 +19,8 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Vera switches."""
add_devices(
- VeraSwitch(device, VERA_CONTROLLER) for
- device in VERA_DEVICES['switch'])
+ VeraSwitch(device, hass.data[VERA_CONTROLLER]) for
+ device in hass.data[VERA_DEVICES]['switch'])
class VeraSwitch(VeraDevice, SwitchDevice):
diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py
index 534c4ac0a32..49a400f4a23 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.2']
+REQUIREMENTS = ['python-miio==0.3.3']
ATTR_POWER = 'power'
ATTR_TEMPERATURE = 'temperature'
diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py
index bcef0d3fb85..0eef2c4ece1 100644
--- a/homeassistant/components/tellstick.py
+++ b/homeassistant/components/tellstick.py
@@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['tellcore-py==1.1.2', 'tellcore-net==0.3']
+REQUIREMENTS = ['tellcore-py==1.1.2', 'tellcore-net==0.4']
_LOGGER = logging.getLogger(__name__)
@@ -67,7 +67,7 @@ def _discover(hass, config, component_name, found_tellcore_devices):
def setup(hass, config):
"""Set up the Tellstick component."""
- from tellcore.constants import TELLSTICK_DIM
+ from tellcore.constants import (TELLSTICK_DIM, TELLSTICK_UP)
from tellcore.telldus import AsyncioCallbackDispatcher
from tellcore.telldus import TelldusCore
from tellcorenet import TellCoreClient
@@ -102,16 +102,22 @@ def setup(hass, config):
hass.data[DATA_TELLSTICK] = {device.id: device for
device in tellcore_devices}
- # Discover the switches
- _discover(hass, config, 'switch',
- [device.id for device in tellcore_devices
- if not device.methods(TELLSTICK_DIM)])
-
# Discover the lights
_discover(hass, config, 'light',
[device.id for device in tellcore_devices
if device.methods(TELLSTICK_DIM)])
+ # Discover the cover
+ _discover(hass, config, 'cover',
+ [device.id for device in tellcore_devices
+ if device.methods(TELLSTICK_UP)])
+
+ # Discover the switches
+ _discover(hass, config, 'switch',
+ [device.id for device in tellcore_devices
+ if (not device.methods(TELLSTICK_UP) and
+ not device.methods(TELLSTICK_DIM))])
+
@callback
def async_handle_callback(tellcore_id, tellcore_command,
tellcore_data, cid):
diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py
index a2265706d87..3fc000f8027 100644
--- a/homeassistant/components/vacuum/xiaomi_miio.py
+++ b/homeassistant/components/vacuum/xiaomi_miio.py
@@ -21,7 +21,7 @@ from homeassistant.const import (
ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['python-miio==0.3.2']
+REQUIREMENTS = ['python-miio==0.3.3']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py
index 7418ca812a1..b15c4ddabfd 100644
--- a/homeassistant/components/vera.py
+++ b/homeassistant/components/vera.py
@@ -19,13 +19,13 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE)
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['pyvera==0.2.38']
+REQUIREMENTS = ['pyvera==0.2.39']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'vera'
-VERA_CONTROLLER = None
+VERA_CONTROLLER = 'vera_controller'
CONF_CONTROLLER = 'vera_controller_url'
@@ -34,7 +34,8 @@ VERA_ID_FORMAT = '{}_{}'
ATTR_CURRENT_POWER_W = "current_power_w"
ATTR_CURRENT_ENERGY_KWH = "current_energy_kwh"
-VERA_DEVICES = defaultdict(list)
+VERA_DEVICES = 'vera_devices'
+VERA_SCENES = 'vera_scenes'
VERA_ID_LIST_SCHEMA = vol.Schema([int])
@@ -47,20 +48,20 @@ CONFIG_SCHEMA = vol.Schema({
}, extra=vol.ALLOW_EXTRA)
VERA_COMPONENTS = [
- 'binary_sensor', 'sensor', 'light', 'switch', 'lock', 'climate', 'cover'
+ 'binary_sensor', 'sensor', 'light', 'switch',
+ 'lock', 'climate', 'cover', 'scene'
]
# pylint: disable=unused-argument, too-many-function-args
def setup(hass, base_config):
"""Set up for Vera devices."""
- global VERA_CONTROLLER
import pyvera as veraApi
def stop_subscription(event):
"""Shutdown Vera subscriptions and subscription thread on exit."""
_LOGGER.info("Shutting down subscriptions")
- VERA_CONTROLLER.stop()
+ hass.data[VERA_CONTROLLER].stop()
config = base_config.get(DOMAIN)
@@ -70,11 +71,14 @@ def setup(hass, base_config):
exclude_ids = config.get(CONF_EXCLUDE)
# Initialize the Vera controller.
- VERA_CONTROLLER, _ = veraApi.init_controller(base_url)
+ controller, _ = veraApi.init_controller(base_url)
+ hass.data[VERA_CONTROLLER] = controller
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
try:
- all_devices = VERA_CONTROLLER.get_devices()
+ all_devices = controller.get_devices()
+
+ all_scenes = controller.get_scenes()
except RequestException:
# There was a network related error connecting to the Vera controller.
_LOGGER.exception("Error communicating with Vera API")
@@ -84,12 +88,19 @@ def setup(hass, base_config):
devices = [device for device in all_devices
if device.device_id not in exclude_ids]
+ vera_devices = defaultdict(list)
for device in devices:
device_type = map_vera_device(device, light_ids)
if device_type is None:
continue
- VERA_DEVICES[device_type].append(device)
+ vera_devices[device_type].append(device)
+ hass.data[VERA_DEVICES] = vera_devices
+
+ vera_scenes = []
+ for scene in all_scenes:
+ vera_scenes.append(scene)
+ hass.data[VERA_SCENES] = vera_scenes
for component in VERA_COMPONENTS:
discovery.load_platform(hass, component, DOMAIN, {}, base_config)
diff --git a/homeassistant/components/volvooncall.py b/homeassistant/components/volvooncall.py
index 4cee6ea2139..dcd4ed518d0 100644
--- a/homeassistant/components/volvooncall.py
+++ b/homeassistant/components/volvooncall.py
@@ -26,11 +26,13 @@ REQUIREMENTS = ['volvooncall==0.4.0']
_LOGGER = logging.getLogger(__name__)
-CONF_UPDATE_INTERVAL = 'update_interval'
MIN_UPDATE_INTERVAL = timedelta(minutes=1)
DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1)
+
+CONF_UPDATE_INTERVAL = 'update_interval'
CONF_REGION = 'region'
CONF_SERVICE_URL = 'service_url'
+CONF_SCANDINAVIAN_MILES = 'scandinavian_miles'
SIGNAL_VEHICLE_SEEN = '{}.vehicle_seen'.format(DOMAIN)
@@ -41,6 +43,8 @@ RESOURCES = {'position': ('device_tracker',),
'fuel_amount': ('sensor', 'Fuel amount', 'mdi:gas-station', 'L'),
'fuel_amount_level': (
'sensor', 'Fuel level', 'mdi:water-percent', '%'),
+ 'average_fuel_consumption': (
+ 'sensor', 'Fuel consumption', 'mdi:gas-station', 'L/100 km'),
'distance_to_empty': ('sensor', 'Range', 'mdi:ruler', 'km'),
'washer_fluid_level': ('binary_sensor', 'Washer fluid'),
'brake_fluid': ('binary_sensor', 'Brake Fluid'),
@@ -61,6 +65,7 @@ CONFIG_SCHEMA = vol.Schema({
cv.ensure_list, [vol.In(RESOURCES)]),
vol.Optional(CONF_REGION): cv.string,
vol.Optional(CONF_SERVICE_URL): cv.string,
+ vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -123,7 +128,8 @@ class VolvoData:
"""Initialize the component state."""
self.entities = {}
self.vehicles = {}
- self.names = config[DOMAIN].get(CONF_NAME)
+ self.config = config[DOMAIN]
+ self.names = self.config.get(CONF_NAME)
def vehicle_name(self, vehicle):
"""Provide a friendly name for a vehicle."""
diff --git a/homeassistant/config.py b/homeassistant/config.py
index c4c96804fca..fee7572a2c2 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -110,6 +110,9 @@ sensor:
tts:
- platform: google
+# Cloud
+cloud:
+
group: !include groups.yaml
automation: !include automations.yaml
script: !include scripts.yaml
diff --git a/homeassistant/const.py b/homeassistant/const.py
index 85047f0482e..be085bd75f1 100644
--- a/homeassistant/const.py
+++ b/homeassistant/const.py
@@ -1,7 +1,7 @@
# coding: utf-8
"""Constants used by Home Assistant components."""
MAJOR_VERSION = 0
-MINOR_VERSION = 60
+MINOR_VERSION = 61
PATCH_VERSION = '0.dev0'
__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION)
__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION)
@@ -75,6 +75,7 @@ CONF_EXCLUDE = 'exclude'
CONF_FILE_PATH = 'file_path'
CONF_FILENAME = 'filename'
CONF_FOR = 'for'
+CONF_FORCE_UPDATE = 'force_update'
CONF_FRIENDLY_NAME = 'friendly_name'
CONF_HEADERS = 'headers'
CONF_HOST = 'host'
diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py
index 46eeef45f14..6a527021c77 100644
--- a/homeassistant/helpers/discovery.py
+++ b/homeassistant/helpers/discovery.py
@@ -149,7 +149,7 @@ def async_load_platform(hass, component, platform, discovered=None,
Use `listen_platform` to register a callback for these events.
Warning: Do not yield from this inside a setup method to avoid a dead lock.
- Use `hass.loop.async_add_job(async_load_platform(..))` instead.
+ Use `hass.async_add_job(async_load_platform(..))` instead.
This method is a coroutine.
"""
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index 2e7acb212e2..3080160dfce 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -5,8 +5,8 @@ pip>=8.0.3
jinja2>=2.9.6
voluptuous==0.10.5
typing>=3,<4
-aiohttp==2.3.5
-yarl==0.15.0
+aiohttp==2.3.6
+yarl==0.16.0
async_timeout==2.0.0
chardet==3.0.4
astral==1.4
diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py
index 646edcf1c35..cb3ebeb7ee6 100644
--- a/homeassistant/util/__init__.py
+++ b/homeassistant/util/__init__.py
@@ -299,7 +299,7 @@ class Throttle(object):
return None
# Check if method is never called or no_throttle is given
- force = not throttle[1] or kwargs.pop('no_throttle', False)
+ force = kwargs.pop('no_throttle', False) or not throttle[1]
try:
if force or utcnow() - throttle[1] > self.min_time:
diff --git a/requirements_all.txt b/requirements_all.txt
index 1a00666d4ba..66f5abffd09 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -6,8 +6,8 @@ pip>=8.0.3
jinja2>=2.9.6
voluptuous==0.10.5
typing>=3,<4
-aiohttp==2.3.5
-yarl==0.15.0
+aiohttp==2.3.6
+yarl==0.16.0
async_timeout==2.0.0
chardet==3.0.4
astral==1.4
@@ -23,7 +23,7 @@ certifi>=2017.4.17
DoorBirdPy==0.1.0
# homeassistant.components.isy994
-PyISY==1.0.8
+PyISY==1.1.0
# homeassistant.components.notify.html5
PyJWT==1.5.3
@@ -44,7 +44,7 @@ PyXiaomiGateway==0.6.0
RtmAPI==0.7.0
# homeassistant.components.media_player.sonos
-SoCo==0.12
+SoCo==0.13
# homeassistant.components.sensor.travisci
TravisPy==0.3.5
@@ -72,7 +72,7 @@ aiohttp_cors==0.5.3
aioimaplib==0.7.13
# homeassistant.components.light.lifx
-aiolifx==0.6.0
+aiolifx==0.6.1
# homeassistant.components.light.lifx
aiolifx_effects==0.1.2
@@ -160,6 +160,9 @@ broadlink==0.5
# homeassistant.components.weather.buienradar
buienradar==0.9
+# homeassistant.components.calendar.caldav
+caldav==0.5.0
+
# homeassistant.components.notify.ciscospark
ciscosparkapi==0.4.2
@@ -205,6 +208,9 @@ denonavr==0.5.5
# homeassistant.components.media_player.directv
directpy==0.2
+# homeassistant.components.sensor.discogs
+discogs_client==2.2.1
+
# homeassistant.components.notify.discord
discord.py==0.16.12
@@ -309,6 +315,9 @@ googlemaps==2.5.1
# homeassistant.components.sensor.gpsd
gps3==0.33.3
+# homeassistant.components.light.greenwave
+greenwavereality==0.2.9
+
# homeassistant.components.media_player.gstreamer
gstreamer-player==1.1.0
@@ -337,7 +346,7 @@ hipnotify==1.0.8
holidays==0.8.1
# homeassistant.components.frontend
-home-assistant-frontend==20171206.0
+home-assistant-frontend==20171223.0
# homeassistant.components.camera.onvif
http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a
@@ -435,12 +444,15 @@ limitlessled==1.0.8
linode-api==4.1.4b2
# homeassistant.components.media_player.liveboxplaytv
-liveboxplaytv==2.0.0
+liveboxplaytv==2.0.2
# homeassistant.components.lametric
# homeassistant.components.notify.lametric
lmnotify==0.0.4
+# homeassistant.components.sensor.luftdaten
+luftdaten==0.1.1
+
# homeassistant.components.sensor.lyft
lyft_rides==0.2
@@ -458,7 +470,7 @@ messagebird==1.2.0
mficlient==0.3.0
# homeassistant.components.sensor.miflora
-miflora==0.1.16
+miflora==0.2.0
# homeassistant.components.upnp
miniupnpc==2.0.2
@@ -533,7 +545,7 @@ pdunehd==1.3
# homeassistant.components.media_player.pandora
pexpect==4.0.1
-# homeassistant.components.light.hue
+# homeassistant.components.hue
phue==1.0
# homeassistant.components.rpi_pfio
@@ -569,7 +581,7 @@ proliphix==0.4.1
prometheus_client==0.0.21
# homeassistant.components.sensor.systemmonitor
-psutil==5.4.1
+psutil==5.4.2
# homeassistant.components.wink
pubnubsub-handler==1.0.2
@@ -584,6 +596,9 @@ pushetta==1.0.15
# homeassistant.components.light.rpi_gpio_pwm
pwmled==1.2.1
+# homeassistant.components.canary
+py-canary==0.2.3
+
# homeassistant.components.sensor.cpuspeed
py-cpuinfo==3.3.0
@@ -625,7 +640,7 @@ pyasn1-modules==0.1.5
pyasn1==0.3.7
# homeassistant.components.apple_tv
-pyatv==0.3.8
+pyatv==0.3.9
# homeassistant.components.device_tracker.bbox
# homeassistant.components.sensor.bbox
@@ -686,13 +701,13 @@ pyharmony==1.0.18
pyhik==0.1.4
# homeassistant.components.hive
-pyhiveapi==0.2.5
+pyhiveapi==0.2.10
# homeassistant.components.homematic
-pyhomematic==0.1.35
+pyhomematic==0.1.36
# homeassistant.components.sensor.hydroquebec
-pyhydroquebec==1.3.1
+pyhydroquebec==2.0.2
# homeassistant.components.alarm_control_panel.ialarm
pyialarm==0.2
@@ -747,10 +762,10 @@ pymochad==0.1.1
pymodbus==1.3.1
# homeassistant.components.media_player.monoprice
-pymonoprice==0.2
+pymonoprice==0.3
# homeassistant.components.media_player.yamaha_musiccast
-pymusiccast==0.1.5
+pymusiccast==0.1.6
# homeassistant.components.cover.myq
pymyq==0.0.8
@@ -810,6 +825,9 @@ pysma==0.1.3
# homeassistant.components.switch.snmp
pysnmp==4.4.2
+# homeassistant.components.media_player.liveboxplaytv
+pyteleloisirs==3.3
+
# homeassistant.components.sensor.thinkingcleaner
# homeassistant.components.switch.thinkingcleaner
pythinkingcleaner==0.0.3
@@ -855,7 +873,7 @@ python-juicenet==0.0.5
# homeassistant.components.light.xiaomi_miio
# homeassistant.components.switch.xiaomi_miio
# homeassistant.components.vacuum.xiaomi_miio
-python-miio==0.3.2
+python-miio==0.3.3
# homeassistant.components.media_player.mpd
python-mpd2==0.5.5
@@ -874,7 +892,7 @@ python-nmap==0.6.1
python-pushover==0.3
# homeassistant.components.sensor.ripple
-python-ripple-api==0.0.2
+python-ripple-api==0.0.3
# homeassistant.components.media_player.roku
python-roku==3.1.3
@@ -913,7 +931,7 @@ pythonegardia==1.0.22
pythonwhois==2.4.3
# homeassistant.components.device_tracker.tile
-pytile==1.0.0
+pytile==1.1.0
# homeassistant.components.device_tracker.trackr
pytrackr==0.0.5
@@ -928,7 +946,7 @@ pyunifi==2.13
# pyuserinput==0.1.11
# homeassistant.components.vera
-pyvera==0.2.38
+pyvera==0.2.39
# homeassistant.components.media_player.vizio
pyvizio==0.0.2
@@ -1013,7 +1031,7 @@ sense-hat==2.2.0
sharp_aquos_rc==0.3.2
# homeassistant.components.sensor.shodan
-shodan==1.7.5
+shodan==1.7.7
# homeassistant.components.notify.simplepush
simplepush==1.1.4
@@ -1072,7 +1090,7 @@ tank_utility==1.4.0
tapsaff==0.1.3
# homeassistant.components.tellstick
-tellcore-net==0.3
+tellcore-net==0.4
# homeassistant.components.tellstick
tellcore-py==1.1.2
@@ -1143,7 +1161,7 @@ wakeonlan==0.2.2
waqiasync==1.0.0
# homeassistant.components.cloud
-warrant==0.5.0
+warrant==0.6.1
# homeassistant.components.media_player.gpmdp
websocket-client==0.37.0
@@ -1181,7 +1199,7 @@ yeelight==0.3.3
yeelightsunflower==0.0.8
# homeassistant.components.media_extractor
-youtube_dl==2017.11.26
+youtube_dl==2017.12.10
# homeassistant.components.light.zengge
zengge==0.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 72325d6305b..ad9fae671cc 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -24,7 +24,7 @@ freezegun>=0.3.8
PyJWT==1.5.3
# homeassistant.components.media_player.sonos
-SoCo==0.12
+SoCo==0.13
# homeassistant.components.device_tracker.automatic
aioautomatic==0.6.4
@@ -36,6 +36,9 @@ aiohttp_cors==0.5.3
# homeassistant.components.notify.apns
apns2==0.3.0
+# homeassistant.components.calendar.caldav
+caldav==0.5.0
+
# homeassistant.components.sensor.coinmarketcap
coinmarketcap==4.1.1
@@ -74,7 +77,7 @@ hbmqtt==0.9.1
holidays==0.8.1
# homeassistant.components.frontend
-home-assistant-frontend==20171206.0
+home-assistant-frontend==20171223.0
# homeassistant.components.influxdb
# homeassistant.components.sensor.influxdb
@@ -115,12 +118,18 @@ pmsensor==0.4
# homeassistant.components.prometheus
prometheus_client==0.0.21
+# homeassistant.components.canary
+py-canary==0.2.3
+
# homeassistant.components.zwave
pydispatcher==2.0.5
# homeassistant.components.litejet
pylitejet==0.1
+# homeassistant.components.media_player.monoprice
+pymonoprice==0.3
+
# homeassistant.components.alarm_control_panel.nx584
# homeassistant.components.binary_sensor.nx584
pynx584==0.4
@@ -176,7 +185,7 @@ vultr==0.1.2
wakeonlan==0.2.2
# homeassistant.components.cloud
-warrant==0.5.0
+warrant==0.6.1
# homeassistant.components.sensor.yahoo_finance
yahoo-finance==1.4.0
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index fbd60ffdadc..5f4d789fa77 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -38,6 +38,7 @@ TEST_REQUIREMENTS = (
'aioautomatic',
'aiohttp_cors',
'apns2',
+ 'caldav',
'coinmarketcap',
'defusedxml',
'dsmr_parser',
@@ -61,9 +62,11 @@ TEST_REQUIREMENTS = (
'pilight',
'pmsensor',
'prometheus_client',
+ 'py-canary',
'pydispatcher',
'PyJWT',
'pylitejet',
+ 'pymonoprice',
'pynx584',
'python-forecastio',
'pyunifi',
diff --git a/setup.py b/setup.py
index d79f11732ad..fe60a15e32e 100755
--- a/setup.py
+++ b/setup.py
@@ -53,8 +53,8 @@ REQUIRES = [
'jinja2>=2.9.6',
'voluptuous==0.10.5',
'typing>=3,<4',
- 'aiohttp==2.3.5', # If updated, check if yarl also needs an update!
- 'yarl==0.15.0',
+ 'aiohttp==2.3.6', # If updated, check if yarl also needs an update!
+ 'yarl==0.16.0',
'async_timeout==2.0.0',
'chardet==3.0.4',
'astral==1.4',
diff --git a/tests/components/binary_sensor/test_threshold.py b/tests/components/binary_sensor/test_threshold.py
index d8c49de1cc0..38573b295d3 100644
--- a/tests/components/binary_sensor/test_threshold.py
+++ b/tests/components/binary_sensor/test_threshold.py
@@ -2,7 +2,8 @@
import unittest
from homeassistant.setup import setup_component
-from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS)
from tests.common import get_test_home_assistant
@@ -23,8 +24,7 @@ class TestThresholdSensor(unittest.TestCase):
config = {
'binary_sensor': {
'platform': 'threshold',
- 'threshold': '15',
- 'type': 'upper',
+ 'upper': '15',
'entity_id': 'sensor.test_monitored',
}
}
@@ -37,12 +37,14 @@ class TestThresholdSensor(unittest.TestCase):
state = self.hass.states.get('binary_sensor.threshold')
- self.assertEqual('upper', state.attributes.get('type'))
self.assertEqual('sensor.test_monitored',
state.attributes.get('entity_id'))
self.assertEqual(16, state.attributes.get('sensor_value'))
- self.assertEqual(float(config['binary_sensor']['threshold']),
- state.attributes.get('threshold'))
+ self.assertEqual('above', state.attributes.get('position'))
+ self.assertEqual(float(config['binary_sensor']['upper']),
+ state.attributes.get('upper'))
+ self.assertEqual(0.0, state.attributes.get('hysteresis'))
+ self.assertEqual('upper', state.attributes.get('type'))
assert state.state == 'on'
@@ -65,9 +67,7 @@ class TestThresholdSensor(unittest.TestCase):
config = {
'binary_sensor': {
'platform': 'threshold',
- 'threshold': '15',
- 'name': 'Test_threshold',
- 'type': 'lower',
+ 'lower': '15',
'entity_id': 'sensor.test_monitored',
}
}
@@ -77,8 +77,12 @@ class TestThresholdSensor(unittest.TestCase):
self.hass.states.set('sensor.test_monitored', 16)
self.hass.block_till_done()
- state = self.hass.states.get('binary_sensor.test_threshold')
+ state = self.hass.states.get('binary_sensor.threshold')
+ self.assertEqual('above', state.attributes.get('position'))
+ self.assertEqual(float(config['binary_sensor']['lower']),
+ state.attributes.get('lower'))
+ self.assertEqual(0.0, state.attributes.get('hysteresis'))
self.assertEqual('lower', state.attributes.get('type'))
assert state.state == 'off'
@@ -86,26 +90,17 @@ class TestThresholdSensor(unittest.TestCase):
self.hass.states.set('sensor.test_monitored', 14)
self.hass.block_till_done()
- state = self.hass.states.get('binary_sensor.test_threshold')
+ state = self.hass.states.get('binary_sensor.threshold')
assert state.state == 'on'
- self.hass.states.set('sensor.test_monitored', 15)
- self.hass.block_till_done()
-
- state = self.hass.states.get('binary_sensor.test_threshold')
-
- assert state.state == 'off'
-
def test_sensor_hysteresis(self):
"""Test if source is above threshold using hysteresis."""
config = {
'binary_sensor': {
'platform': 'threshold',
- 'threshold': '15',
+ 'upper': '15',
'hysteresis': '2.5',
- 'name': 'Test_threshold',
- 'type': 'upper',
'entity_id': 'sensor.test_monitored',
}
}
@@ -115,34 +110,226 @@ class TestThresholdSensor(unittest.TestCase):
self.hass.states.set('sensor.test_monitored', 20)
self.hass.block_till_done()
- state = self.hass.states.get('binary_sensor.test_threshold')
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('above', state.attributes.get('position'))
+ self.assertEqual(float(config['binary_sensor']['upper']),
+ state.attributes.get('upper'))
+ self.assertEqual(2.5, state.attributes.get('hysteresis'))
+ self.assertEqual('upper', state.attributes.get('type'))
assert state.state == 'on'
self.hass.states.set('sensor.test_monitored', 13)
self.hass.block_till_done()
- state = self.hass.states.get('binary_sensor.test_threshold')
+ state = self.hass.states.get('binary_sensor.threshold')
assert state.state == 'on'
self.hass.states.set('sensor.test_monitored', 12)
self.hass.block_till_done()
- state = self.hass.states.get('binary_sensor.test_threshold')
+ state = self.hass.states.get('binary_sensor.threshold')
assert state.state == 'off'
self.hass.states.set('sensor.test_monitored', 17)
self.hass.block_till_done()
- state = self.hass.states.get('binary_sensor.test_threshold')
+ state = self.hass.states.get('binary_sensor.threshold')
assert state.state == 'off'
self.hass.states.set('sensor.test_monitored', 18)
self.hass.block_till_done()
- state = self.hass.states.get('binary_sensor.test_threshold')
+ state = self.hass.states.get('binary_sensor.threshold')
assert state.state == 'on'
+
+ def test_sensor_in_range_no_hysteresis(self):
+ """Test if source is within the range."""
+ config = {
+ 'binary_sensor': {
+ 'platform': 'threshold',
+ 'lower': '10',
+ 'upper': '20',
+ 'entity_id': 'sensor.test_monitored',
+ }
+ }
+
+ assert setup_component(self.hass, 'binary_sensor', config)
+
+ self.hass.states.set('sensor.test_monitored', 16,
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('sensor.test_monitored',
+ state.attributes.get('entity_id'))
+ self.assertEqual(16, state.attributes.get('sensor_value'))
+ self.assertEqual('in_range', state.attributes.get('position'))
+ self.assertEqual(float(config['binary_sensor']['lower']),
+ state.attributes.get('lower'))
+ self.assertEqual(float(config['binary_sensor']['upper']),
+ state.attributes.get('upper'))
+ self.assertEqual(0.0, state.attributes.get('hysteresis'))
+ self.assertEqual('range', state.attributes.get('type'))
+
+ assert state.state == 'on'
+
+ self.hass.states.set('sensor.test_monitored', 9)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('below', state.attributes.get('position'))
+ assert state.state == 'off'
+
+ self.hass.states.set('sensor.test_monitored', 21)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('above', state.attributes.get('position'))
+ assert state.state == 'off'
+
+ def test_sensor_in_range_with_hysteresis(self):
+ """Test if source is within the range."""
+ config = {
+ 'binary_sensor': {
+ 'platform': 'threshold',
+ 'lower': '10',
+ 'upper': '20',
+ 'hysteresis': '2',
+ 'entity_id': 'sensor.test_monitored',
+ }
+ }
+
+ assert setup_component(self.hass, 'binary_sensor', config)
+
+ self.hass.states.set('sensor.test_monitored', 16,
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('sensor.test_monitored',
+ state.attributes.get('entity_id'))
+ self.assertEqual(16, state.attributes.get('sensor_value'))
+ self.assertEqual('in_range', state.attributes.get('position'))
+ self.assertEqual(float(config['binary_sensor']['lower']),
+ state.attributes.get('lower'))
+ self.assertEqual(float(config['binary_sensor']['upper']),
+ state.attributes.get('upper'))
+ self.assertEqual(float(config['binary_sensor']['hysteresis']),
+ state.attributes.get('hysteresis'))
+ self.assertEqual('range', state.attributes.get('type'))
+
+ assert state.state == 'on'
+
+ self.hass.states.set('sensor.test_monitored', 8)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('in_range', state.attributes.get('position'))
+ assert state.state == 'on'
+
+ self.hass.states.set('sensor.test_monitored', 7)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('below', state.attributes.get('position'))
+ assert state.state == 'off'
+
+ self.hass.states.set('sensor.test_monitored', 12)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('below', state.attributes.get('position'))
+ assert state.state == 'off'
+
+ self.hass.states.set('sensor.test_monitored', 13)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('in_range', state.attributes.get('position'))
+ assert state.state == 'on'
+
+ self.hass.states.set('sensor.test_monitored', 22)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('in_range', state.attributes.get('position'))
+ assert state.state == 'on'
+
+ self.hass.states.set('sensor.test_monitored', 23)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('above', state.attributes.get('position'))
+ assert state.state == 'off'
+
+ self.hass.states.set('sensor.test_monitored', 18)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('above', state.attributes.get('position'))
+ assert state.state == 'off'
+
+ self.hass.states.set('sensor.test_monitored', 17)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('in_range', state.attributes.get('position'))
+ assert state.state == 'on'
+
+ def test_sensor_in_range_unknown_state(self):
+ """Test if source is within the range."""
+ config = {
+ 'binary_sensor': {
+ 'platform': 'threshold',
+ 'lower': '10',
+ 'upper': '20',
+ 'entity_id': 'sensor.test_monitored',
+ }
+ }
+
+ assert setup_component(self.hass, 'binary_sensor', config)
+
+ self.hass.states.set('sensor.test_monitored', 16,
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('sensor.test_monitored',
+ state.attributes.get('entity_id'))
+ self.assertEqual(16, state.attributes.get('sensor_value'))
+ self.assertEqual('in_range', state.attributes.get('position'))
+ self.assertEqual(float(config['binary_sensor']['lower']),
+ state.attributes.get('lower'))
+ self.assertEqual(float(config['binary_sensor']['upper']),
+ state.attributes.get('upper'))
+ self.assertEqual(0.0, state.attributes.get('hysteresis'))
+ self.assertEqual('range', state.attributes.get('type'))
+
+ assert state.state == 'on'
+
+ self.hass.states.set('sensor.test_monitored', STATE_UNKNOWN)
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('binary_sensor.threshold')
+
+ self.assertEqual('unknown', state.attributes.get('position'))
+ assert state.state == 'off'
diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py
new file mode 100644
index 00000000000..7234d40c410
--- /dev/null
+++ b/tests/components/calendar/test_caldav.py
@@ -0,0 +1,305 @@
+"""The tests for the webdav calendar component."""
+# pylint: disable=protected-access
+import datetime
+import logging
+import unittest
+from unittest.mock import (patch, Mock, MagicMock)
+
+import homeassistant.components.calendar as calendar_base
+import homeassistant.components.calendar.caldav as caldav
+from caldav.objects import Event
+from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON
+from homeassistant.util import dt
+from tests.common import get_test_home_assistant
+
+TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}}
+
+_LOGGER = logging.getLogger(__name__)
+
+
+DEVICE_DATA = {
+ "name": "Private Calendar",
+ "device_id": "Private Calendar"
+}
+
+EVENTS = [
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//E-Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:1
+DTSTAMP:20171125T000000Z
+DTSTART:20171127T170000Z
+DTEND:20171127T180000Z
+SUMMARY:This is a normal event
+LOCATION:Hamburg
+DESCRIPTION:Surprisingly rainy
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Dynamics.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:2
+DTSTAMP:20171125T000000Z
+DTSTART:20171127T100000Z
+DTEND:20171127T110000Z
+SUMMARY:This is an offset event !!-02:00
+LOCATION:Hamburg
+DESCRIPTION:Surprisingly shiny
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Global Corp.//CalDAV Client//EN
+BEGIN:VEVENT
+UID:3
+DTSTAMP:20171125T000000Z
+DTSTART:20171127
+DTEND:20171128
+SUMMARY:This is an all day event
+LOCATION:Hamburg
+DESCRIPTION:What a beautiful day
+END:VEVENT
+END:VCALENDAR
+"""
+]
+
+
+def _local_datetime(hours, minutes):
+ """Build a datetime object for testing in the correct timezone."""
+ return dt.as_local(datetime.datetime(2017, 11, 27, hours, minutes, 0))
+
+
+def _mocked_dav_client(*args, **kwargs):
+ """Mock requests.get invocations."""
+ calendars = [
+ _mock_calendar("First"),
+ _mock_calendar("Second")
+ ]
+ principal = Mock()
+ principal.calendars = MagicMock(return_value=calendars)
+
+ client = Mock()
+ client.principal = MagicMock(return_value=principal)
+ return client
+
+
+def _mock_calendar(name):
+ events = []
+ for idx, event in enumerate(EVENTS):
+ events.append(Event(None, "%d.ics" % idx, event, None, str(idx)))
+
+ calendar = Mock()
+ calendar.date_search = MagicMock(return_value=events)
+ calendar.name = name
+ return calendar
+
+
+class TestComponentsWebDavCalendar(unittest.TestCase):
+ """Test the WebDav calendar."""
+
+ hass = None # HomeAssistant
+
+ # pylint: disable=invalid-name
+ def setUp(self):
+ """Set up things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.calendar = _mock_calendar("Private")
+
+ # pylint: disable=invalid-name
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @patch('caldav.DAVClient', side_effect=_mocked_dav_client)
+ def test_setup_component(self, req_mock):
+ """Test setup component with calendars."""
+ def _add_device(devices):
+ assert len(devices) == 2
+ assert devices[0].name == "First"
+ assert devices[0].dev_id == "First"
+ self.assertFalse(devices[0].data.include_all_day)
+ assert devices[1].name == "Second"
+ assert devices[1].dev_id == "Second"
+ self.assertFalse(devices[1].data.include_all_day)
+
+ caldav.setup_platform(self.hass,
+ {
+ "url": "http://test.local",
+ "custom_calendars": []
+ },
+ _add_device)
+
+ @patch('caldav.DAVClient', side_effect=_mocked_dav_client)
+ def test_setup_component_with_no_calendar_matching(self, req_mock):
+ """Test setup component with wrong calendar."""
+ def _add_device(devices):
+ assert not devices
+
+ caldav.setup_platform(self.hass,
+ {
+ "url": "http://test.local",
+ "calendars": ["none"],
+ "custom_calendars": []
+ },
+ _add_device)
+
+ @patch('caldav.DAVClient', side_effect=_mocked_dav_client)
+ def test_setup_component_with_a_calendar_match(self, req_mock):
+ """Test setup component with right calendar."""
+ def _add_device(devices):
+ assert len(devices) == 1
+ assert devices[0].name == "Second"
+
+ caldav.setup_platform(self.hass,
+ {
+ "url": "http://test.local",
+ "calendars": ["Second"],
+ "custom_calendars": []
+ },
+ _add_device)
+
+ @patch('caldav.DAVClient', side_effect=_mocked_dav_client)
+ def test_setup_component_with_one_custom_calendar(self, req_mock):
+ """Test setup component with custom calendars."""
+ def _add_device(devices):
+ assert len(devices) == 1
+ assert devices[0].name == "HomeOffice"
+ assert devices[0].dev_id == "Second HomeOffice"
+ self.assertTrue(devices[0].data.include_all_day)
+
+ caldav.setup_platform(self.hass,
+ {
+ "url": "http://test.local",
+ "custom_calendars": [
+ {
+ "name": "HomeOffice",
+ "calendar": "Second",
+ "filter": "HomeOffice"
+ }]
+ },
+ _add_device)
+
+ @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30))
+ def test_ongoing_event(self, mock_now):
+ """Test that the ongoing event is returned."""
+ cal = caldav.WebDavCalendarEventDevice(self.hass,
+ DEVICE_DATA,
+ self.calendar)
+
+ self.assertEqual(cal.name, DEVICE_DATA["name"])
+ self.assertEqual(cal.state, STATE_ON)
+ self.assertEqual(cal.device_state_attributes, {
+ "message": "This is a normal event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 17:00:00",
+ "end_time": "2017-11-27 18:00:00",
+ "location": "Hamburg",
+ "description": "Surprisingly rainy"
+ })
+
+ @patch('homeassistant.util.dt.now', return_value=_local_datetime(8, 30))
+ def test_ongoing_event_with_offset(self, mock_now):
+ """Test that the offset is taken into account."""
+ cal = caldav.WebDavCalendarEventDevice(self.hass,
+ DEVICE_DATA,
+ self.calendar)
+
+ self.assertEqual(cal.state, STATE_OFF)
+ self.assertEqual(cal.device_state_attributes, {
+ "message": "This is an offset event",
+ "all_day": False,
+ "offset_reached": True,
+ "start_time": "2017-11-27 10:00:00",
+ "end_time": "2017-11-27 11:00:00",
+ "location": "Hamburg",
+ "description": "Surprisingly shiny"
+ })
+
+ @patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00))
+ def test_matching_filter(self, mock_now):
+ """Test that the matching event is returned."""
+ cal = caldav.WebDavCalendarEventDevice(self.hass,
+ DEVICE_DATA,
+ self.calendar,
+ False,
+ "This is a normal event")
+
+ self.assertEqual(cal.state, STATE_OFF)
+ self.assertFalse(cal.offset_reached())
+ self.assertEqual(cal.device_state_attributes, {
+ "message": "This is a normal event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 17:00:00",
+ "end_time": "2017-11-27 18:00:00",
+ "location": "Hamburg",
+ "description": "Surprisingly rainy"
+ })
+
+ @patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00))
+ def test_matching_filter_real_regexp(self, mock_now):
+ """Test that the event matching the regexp is returned."""
+ cal = caldav.WebDavCalendarEventDevice(self.hass,
+ DEVICE_DATA,
+ self.calendar,
+ False,
+ "^This.*event")
+
+ self.assertEqual(cal.state, STATE_OFF)
+ self.assertFalse(cal.offset_reached())
+ self.assertEqual(cal.device_state_attributes, {
+ "message": "This is a normal event",
+ "all_day": False,
+ "offset_reached": False,
+ "start_time": "2017-11-27 17:00:00",
+ "end_time": "2017-11-27 18:00:00",
+ "location": "Hamburg",
+ "description": "Surprisingly rainy"
+ })
+
+ @patch('homeassistant.util.dt.now', return_value=_local_datetime(20, 00))
+ def test_filter_matching_past_event(self, mock_now):
+ """Test that the matching past event is not returned."""
+ cal = caldav.WebDavCalendarEventDevice(self.hass,
+ DEVICE_DATA,
+ self.calendar,
+ False,
+ "This is a normal event")
+
+ self.assertEqual(cal.data.event, None)
+
+ @patch('homeassistant.util.dt.now', return_value=_local_datetime(12, 00))
+ def test_no_result_with_filtering(self, mock_now):
+ """Test that nothing is returned since nothing matches."""
+ cal = caldav.WebDavCalendarEventDevice(self.hass,
+ DEVICE_DATA,
+ self.calendar,
+ False,
+ "This is a non-existing event")
+
+ self.assertEqual(cal.data.event, None)
+
+ @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30))
+ def test_all_day_event_returned(self, mock_now):
+ """Test that the event lasting the whole day is returned."""
+ cal = caldav.WebDavCalendarEventDevice(self.hass,
+ DEVICE_DATA,
+ self.calendar,
+ True)
+
+ self.assertEqual(cal.name, DEVICE_DATA["name"])
+ self.assertEqual(cal.state, STATE_ON)
+ self.assertEqual(cal.device_state_attributes, {
+ "message": "This is an all day event",
+ "all_day": True,
+ "offset_reached": False,
+ "start_time": "2017-11-27 00:00:00",
+ "end_time": "2017-11-28 00:00:00",
+ "location": "Hamburg",
+ "description": "What a beautiful day"
+ })
diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py
index c05fdabf465..c5bb6f7fda7 100644
--- a/tests/components/cloud/test_init.py
+++ b/tests/components/cloud/test_init.py
@@ -132,15 +132,6 @@ def test_write_user_info():
}
-@asyncio.coroutine
-def test_subscription_not_expired_without_sub_in_claim():
- """Test that we do not enforce subscriptions yet."""
- cl = cloud.Cloud(None, cloud.MODE_DEV)
- cl.id_token = jwt.encode({}, 'test')
-
- assert not cl.subscription_expired
-
-
@asyncio.coroutine
def test_subscription_expired():
"""Test subscription being expired."""
diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py
index a6827d165cd..0159eec2eff 100644
--- a/tests/components/device_tracker/test_asuswrt.py
+++ b/tests/components/device_tracker/test_asuswrt.py
@@ -144,7 +144,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase):
update_mock.start()
self.addCleanup(update_mock.stop)
asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict)
- asuswrt.connection.get_result()
+ asuswrt.connection.run_command('ls')
self.assertEqual(ssh.login.call_count, 1)
self.assertEqual(
ssh.login.call_args,
@@ -170,7 +170,7 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase):
update_mock.start()
self.addCleanup(update_mock.stop)
asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict)
- asuswrt.connection.get_result()
+ asuswrt.connection.run_command('ls')
self.assertEqual(ssh.login.call_count, 1)
self.assertEqual(
ssh.login.call_args,
@@ -225,9 +225,9 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase):
update_mock.start()
self.addCleanup(update_mock.stop)
asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict)
- asuswrt.connection.get_result()
- self.assertEqual(telnet.read_until.call_count, 5)
- self.assertEqual(telnet.write.call_count, 4)
+ asuswrt.connection.run_command('ls')
+ self.assertEqual(telnet.read_until.call_count, 4)
+ self.assertEqual(telnet.write.call_count, 3)
self.assertEqual(
telnet.read_until.call_args_list[0],
mock.call(b'login: ')
diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py
new file mode 100644
index 00000000000..7955cecba04
--- /dev/null
+++ b/tests/components/light/test_hue.py
@@ -0,0 +1,541 @@
+"""Philips Hue lights platform tests."""
+
+import logging
+import unittest
+import unittest.mock as mock
+from unittest.mock import call, MagicMock, patch
+
+from homeassistant.components import hue
+import homeassistant.components.light.hue as hue_light
+
+from tests.common import get_test_home_assistant, MockDependency
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TestSetup(unittest.TestCase):
+ """Test the Hue light platform."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.skip_teardown_stop = False
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ if not self.skip_teardown_stop:
+ self.hass.stop()
+
+ def setup_mocks_for_update_lights(self):
+ """Set up all mocks for update_lights tests."""
+ self.mock_bridge = MagicMock()
+ self.mock_bridge.allow_hue_groups = False
+ self.mock_api = MagicMock()
+ self.mock_bridge.get_api.return_value = self.mock_api
+ self.mock_bridge_type = MagicMock()
+ self.mock_lights = []
+ self.mock_groups = []
+ self.mock_add_devices = MagicMock()
+
+ def setup_mocks_for_process_lights(self):
+ """Set up all mocks for process_lights tests."""
+ self.mock_bridge = self.create_mock_bridge('host')
+ self.mock_api = MagicMock()
+ self.mock_api.get.return_value = {}
+ self.mock_bridge.get_api.return_value = self.mock_api
+ self.mock_bridge_type = MagicMock()
+
+ def setup_mocks_for_process_groups(self):
+ """Set up all mocks for process_groups tests."""
+ self.mock_bridge = self.create_mock_bridge('host')
+ self.mock_bridge.get_group.return_value = {
+ 'name': 'Group 0', 'state': {'any_on': True}}
+
+ self.mock_api = MagicMock()
+ self.mock_api.get.return_value = {}
+ self.mock_bridge.get_api.return_value = self.mock_api
+
+ self.mock_bridge_type = MagicMock()
+
+ def create_mock_bridge(self, host, allow_hue_groups=True):
+ """Return a mock HueBridge with reasonable defaults."""
+ mock_bridge = MagicMock()
+ mock_bridge.host = host
+ mock_bridge.allow_hue_groups = allow_hue_groups
+ mock_bridge.lights = {}
+ mock_bridge.lightgroups = {}
+ return mock_bridge
+
+ def create_mock_lights(self, lights):
+ """Return a dict suitable for mocking api.get('lights')."""
+ mock_bridge_lights = lights
+
+ for light_id, info in mock_bridge_lights.items():
+ if 'state' not in info:
+ info['state'] = {'on': False}
+
+ return mock_bridge_lights
+
+ def test_setup_platform_no_discovery_info(self):
+ """Test setup_platform without discovery info."""
+ self.hass.data[hue.DOMAIN] = {}
+ mock_add_devices = MagicMock()
+
+ hue_light.setup_platform(self.hass, {}, mock_add_devices)
+
+ mock_add_devices.assert_not_called()
+
+ def test_setup_platform_no_bridge_id(self):
+ """Test setup_platform without a bridge."""
+ self.hass.data[hue.DOMAIN] = {}
+ mock_add_devices = MagicMock()
+
+ hue_light.setup_platform(self.hass, {}, mock_add_devices, {})
+
+ mock_add_devices.assert_not_called()
+
+ def test_setup_platform_one_bridge(self):
+ """Test setup_platform with one bridge."""
+ mock_bridge = MagicMock()
+ self.hass.data[hue.DOMAIN] = {'10.0.0.1': mock_bridge}
+ mock_add_devices = MagicMock()
+
+ with patch('homeassistant.components.light.hue.' +
+ 'unthrottled_update_lights') as mock_update_lights:
+ hue_light.setup_platform(
+ self.hass, {}, mock_add_devices,
+ {'bridge_id': '10.0.0.1'})
+ mock_update_lights.assert_called_once_with(
+ self.hass, mock_bridge, mock_add_devices)
+
+ def test_setup_platform_multiple_bridges(self):
+ """Test setup_platform wuth multiple bridges."""
+ mock_bridge = MagicMock()
+ mock_bridge2 = MagicMock()
+ self.hass.data[hue.DOMAIN] = {
+ '10.0.0.1': mock_bridge,
+ '192.168.0.10': mock_bridge2,
+ }
+ mock_add_devices = MagicMock()
+
+ with patch('homeassistant.components.light.hue.' +
+ 'unthrottled_update_lights') as mock_update_lights:
+ hue_light.setup_platform(
+ self.hass, {}, mock_add_devices,
+ {'bridge_id': '10.0.0.1'})
+ hue_light.setup_platform(
+ self.hass, {}, mock_add_devices,
+ {'bridge_id': '192.168.0.10'})
+
+ mock_update_lights.assert_has_calls([
+ call(self.hass, mock_bridge, mock_add_devices),
+ call(self.hass, mock_bridge2, mock_add_devices),
+ ])
+
+ @MockDependency('phue')
+ def test_update_lights_with_no_lights(self, mock_phue):
+ """Test the update_lights function when no lights are found."""
+ self.setup_mocks_for_update_lights()
+
+ with patch('homeassistant.components.light.hue.get_bridge_type',
+ return_value=self.mock_bridge_type):
+ with patch('homeassistant.components.light.hue.process_lights',
+ return_value=[]) as mock_process_lights:
+ with patch('homeassistant.components.light.hue.process_groups',
+ return_value=self.mock_groups) \
+ as mock_process_groups:
+ hue_light.unthrottled_update_lights(
+ self.hass, self.mock_bridge, self.mock_add_devices)
+
+ mock_process_lights.assert_called_once_with(
+ self.hass, self.mock_api, self.mock_bridge,
+ self.mock_bridge_type, mock.ANY)
+ mock_process_groups.assert_not_called()
+ self.mock_add_devices.assert_not_called()
+
+ @MockDependency('phue')
+ def test_update_lights_with_some_lights(self, mock_phue):
+ """Test the update_lights function with some lights."""
+ self.setup_mocks_for_update_lights()
+ self.mock_lights = ['some', 'light']
+
+ with patch('homeassistant.components.light.hue.get_bridge_type',
+ return_value=self.mock_bridge_type):
+ with patch('homeassistant.components.light.hue.process_lights',
+ return_value=self.mock_lights) as mock_process_lights:
+ with patch('homeassistant.components.light.hue.process_groups',
+ return_value=self.mock_groups) \
+ as mock_process_groups:
+ hue_light.unthrottled_update_lights(
+ self.hass, self.mock_bridge, self.mock_add_devices)
+
+ mock_process_lights.assert_called_once_with(
+ self.hass, self.mock_api, self.mock_bridge,
+ self.mock_bridge_type, mock.ANY)
+ mock_process_groups.assert_not_called()
+ self.mock_add_devices.assert_called_once_with(
+ self.mock_lights)
+
+ @MockDependency('phue')
+ def test_update_lights_no_groups(self, mock_phue):
+ """Test the update_lights function when no groups are found."""
+ self.setup_mocks_for_update_lights()
+ self.mock_bridge.allow_hue_groups = True
+ self.mock_lights = ['some', 'light']
+
+ with patch('homeassistant.components.light.hue.get_bridge_type',
+ return_value=self.mock_bridge_type):
+ with patch('homeassistant.components.light.hue.process_lights',
+ return_value=self.mock_lights) as mock_process_lights:
+ with patch('homeassistant.components.light.hue.process_groups',
+ return_value=self.mock_groups) \
+ as mock_process_groups:
+ hue_light.unthrottled_update_lights(
+ self.hass, self.mock_bridge, self.mock_add_devices)
+
+ mock_process_lights.assert_called_once_with(
+ self.hass, self.mock_api, self.mock_bridge,
+ self.mock_bridge_type, mock.ANY)
+ mock_process_groups.assert_called_once_with(
+ self.hass, self.mock_api, self.mock_bridge,
+ self.mock_bridge_type, mock.ANY)
+ self.mock_add_devices.assert_called_once_with(
+ self.mock_lights)
+
+ @MockDependency('phue')
+ def test_update_lights_with_lights_and_groups(self, mock_phue):
+ """Test the update_lights function with both lights and groups."""
+ self.setup_mocks_for_update_lights()
+ self.mock_bridge.allow_hue_groups = True
+ self.mock_lights = ['some', 'light']
+ self.mock_groups = ['and', 'groups']
+
+ with patch('homeassistant.components.light.hue.get_bridge_type',
+ return_value=self.mock_bridge_type):
+ with patch('homeassistant.components.light.hue.process_lights',
+ return_value=self.mock_lights) as mock_process_lights:
+ with patch('homeassistant.components.light.hue.process_groups',
+ return_value=self.mock_groups) \
+ as mock_process_groups:
+ hue_light.unthrottled_update_lights(
+ self.hass, self.mock_bridge, self.mock_add_devices)
+
+ mock_process_lights.assert_called_once_with(
+ self.hass, self.mock_api, self.mock_bridge,
+ self.mock_bridge_type, mock.ANY)
+ mock_process_groups.assert_called_once_with(
+ self.hass, self.mock_api, self.mock_bridge,
+ self.mock_bridge_type, mock.ANY)
+ self.mock_add_devices.assert_called_once_with(
+ self.mock_lights)
+
+ @MockDependency('phue')
+ def test_update_lights_with_two_bridges(self, mock_phue):
+ """Test the update_lights function with two bridges."""
+ self.setup_mocks_for_update_lights()
+
+ mock_bridge_one = self.create_mock_bridge('one', False)
+ mock_bridge_one_lights = self.create_mock_lights(
+ {1: {'name': 'b1l1'}, 2: {'name': 'b1l2'}})
+
+ mock_bridge_two = self.create_mock_bridge('two', False)
+ mock_bridge_two_lights = self.create_mock_lights(
+ {1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}})
+
+ with patch('homeassistant.components.light.hue.get_bridge_type',
+ return_value=self.mock_bridge_type):
+ with patch('homeassistant.components.light.hue.HueLight.'
+ 'schedule_update_ha_state'):
+ mock_api = MagicMock()
+ mock_api.get.return_value = mock_bridge_one_lights
+ with patch.object(mock_bridge_one, 'get_api',
+ return_value=mock_api):
+ hue_light.unthrottled_update_lights(
+ self.hass, mock_bridge_one, self.mock_add_devices)
+
+ mock_api = MagicMock()
+ mock_api.get.return_value = mock_bridge_two_lights
+ with patch.object(mock_bridge_two, 'get_api',
+ return_value=mock_api):
+ 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.assertEquals(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))
+
+ # 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))
+
+ def test_process_lights_api_error(self):
+ """Test the process_lights function when the bridge errors out."""
+ self.setup_mocks_for_process_lights()
+ self.mock_api.get.return_value = None
+
+ ret = hue_light.process_lights(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals([], ret)
+ self.assertEquals(self.mock_bridge.lights, {})
+
+ def test_process_lights_no_lights(self):
+ """Test the process_lights function when bridge returns no lights."""
+ self.setup_mocks_for_process_lights()
+
+ ret = hue_light.process_lights(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals([], ret)
+ self.assertEquals(self.mock_bridge.lights, {})
+
+ @patch('homeassistant.components.light.hue.HueLight')
+ def test_process_lights_some_lights(self, mock_hue_light):
+ """Test the process_lights function with multiple groups."""
+ self.setup_mocks_for_process_lights()
+ self.mock_api.get.return_value = {
+ 1: {'state': 'on'}, 2: {'state': 'off'}}
+
+ ret = hue_light.process_lights(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals(len(ret), 2)
+ mock_hue_light.assert_has_calls([
+ call(
+ 1, {'state': 'on'}, self.mock_bridge, mock.ANY,
+ self.mock_bridge_type, self.mock_bridge.allow_unreachable,
+ self.mock_bridge.allow_in_emulated_hue),
+ call(
+ 2, {'state': 'off'}, self.mock_bridge, mock.ANY,
+ self.mock_bridge_type, self.mock_bridge.allow_unreachable,
+ self.mock_bridge.allow_in_emulated_hue),
+ ])
+ self.assertEquals(len(self.mock_bridge.lights), 2)
+
+ @patch('homeassistant.components.light.hue.HueLight')
+ def test_process_lights_new_light(self, mock_hue_light):
+ """
+ Test the process_lights function with new groups.
+
+ Test what happens when we already have a light and a new one shows up.
+ """
+ self.setup_mocks_for_process_lights()
+ self.mock_api.get.return_value = {
+ 1: {'state': 'on'}, 2: {'state': 'off'}}
+ self.mock_bridge.lights = {1: MagicMock()}
+
+ ret = hue_light.process_lights(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals(len(ret), 1)
+ mock_hue_light.assert_has_calls([
+ call(
+ 2, {'state': 'off'}, self.mock_bridge, mock.ANY,
+ self.mock_bridge_type, self.mock_bridge.allow_unreachable,
+ self.mock_bridge.allow_in_emulated_hue),
+ ])
+ self.assertEquals(len(self.mock_bridge.lights), 2)
+ self.mock_bridge.lights[1]\
+ .schedule_update_ha_state.assert_called_once_with()
+
+ def test_process_groups_api_error(self):
+ """Test the process_groups function when the bridge errors out."""
+ self.setup_mocks_for_process_groups()
+ self.mock_api.get.return_value = None
+
+ ret = hue_light.process_groups(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals([], ret)
+ self.assertEquals(self.mock_bridge.lightgroups, {})
+
+ def test_process_groups_no_state(self):
+ """Test the process_groups function when bridge returns no status."""
+ self.setup_mocks_for_process_groups()
+ self.mock_bridge.get_group.return_value = {'name': 'Group 0'}
+
+ ret = hue_light.process_groups(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals([], ret)
+ self.assertEquals(self.mock_bridge.lightgroups, {})
+
+ @patch('homeassistant.components.light.hue.HueLight')
+ def test_process_groups_some_groups(self, mock_hue_light):
+ """Test the process_groups function with multiple groups."""
+ self.setup_mocks_for_process_groups()
+ self.mock_api.get.return_value = {
+ 1: {'state': 'on'}, 2: {'state': 'off'}}
+
+ ret = hue_light.process_groups(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals(len(ret), 2)
+ mock_hue_light.assert_has_calls([
+ call(
+ 1, {'state': 'on'}, self.mock_bridge, mock.ANY,
+ self.mock_bridge_type, self.mock_bridge.allow_unreachable,
+ self.mock_bridge.allow_in_emulated_hue, True),
+ call(
+ 2, {'state': 'off'}, self.mock_bridge, mock.ANY,
+ self.mock_bridge_type, self.mock_bridge.allow_unreachable,
+ self.mock_bridge.allow_in_emulated_hue, True),
+ ])
+ self.assertEquals(len(self.mock_bridge.lightgroups), 2)
+
+ @patch('homeassistant.components.light.hue.HueLight')
+ def test_process_groups_new_group(self, mock_hue_light):
+ """
+ Test the process_groups function with new groups.
+
+ Test what happens when we already have a light and a new one shows up.
+ """
+ self.setup_mocks_for_process_groups()
+ self.mock_api.get.return_value = {
+ 1: {'state': 'on'}, 2: {'state': 'off'}}
+ self.mock_bridge.lightgroups = {1: MagicMock()}
+
+ ret = hue_light.process_groups(
+ self.hass, self.mock_api, self.mock_bridge, self.mock_bridge_type,
+ None)
+
+ self.assertEquals(len(ret), 1)
+ mock_hue_light.assert_has_calls([
+ call(
+ 2, {'state': 'off'}, self.mock_bridge, mock.ANY,
+ self.mock_bridge_type, self.mock_bridge.allow_unreachable,
+ self.mock_bridge.allow_in_emulated_hue, True),
+ ])
+ self.assertEquals(len(self.mock_bridge.lightgroups), 2)
+ self.mock_bridge.lightgroups[1]\
+ .schedule_update_ha_state.assert_called_once_with()
+
+
+class TestHueLight(unittest.TestCase):
+ """Test the HueLight class."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.skip_teardown_stop = False
+
+ self.light_id = 42
+ self.mock_info = MagicMock()
+ self.mock_bridge = MagicMock()
+ self.mock_update_lights = MagicMock()
+ self.mock_bridge_type = MagicMock()
+ self.mock_allow_unreachable = MagicMock()
+ self.mock_is_group = MagicMock()
+ self.mock_allow_in_emulated_hue = MagicMock()
+ self.mock_is_group = False
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ if not self.skip_teardown_stop:
+ self.hass.stop()
+
+ def buildLight(
+ self, light_id=None, info=None, update_lights=None, is_group=None):
+ """Helper to build a HueLight object with minimal fuss."""
+ return hue_light.HueLight(
+ light_id if light_id is not None else self.light_id,
+ info if info is not None else self.mock_info,
+ self.mock_bridge,
+ (update_lights
+ if update_lights is not None
+ else self.mock_update_lights),
+ self.mock_bridge_type,
+ self.mock_allow_unreachable, self.mock_allow_in_emulated_hue,
+ is_group if is_group is not None else self.mock_is_group)
+
+ 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)
+
+ 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)
+
+ 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)
+
+ 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)
diff --git a/tests/components/light/test_mochad.py b/tests/components/light/test_mochad.py
index e69ebdb4aef..5c82ab06085 100644
--- a/tests/components/light/test_mochad.py
+++ b/tests/components/light/test_mochad.py
@@ -60,7 +60,8 @@ class TestMochadLight(unittest.TestCase):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
controller_mock = mock.MagicMock()
- dev_dict = {'address': 'a1', 'name': 'fake_light'}
+ dev_dict = {'address': 'a1', 'name': 'fake_light',
+ 'brightness_levels': 32}
self.light = mochad.MochadLight(self.hass, controller_mock,
dev_dict)
@@ -72,6 +73,39 @@ class TestMochadLight(unittest.TestCase):
"""Test the name."""
self.assertEqual('fake_light', self.light.name)
+ def test_turn_on_with_no_brightness(self):
+ """Test turn_on."""
+ self.light.turn_on()
+ self.light.device.send_cmd.assert_called_once_with('on')
+
+ def test_turn_on_with_brightness(self):
+ """Test turn_on."""
+ self.light.turn_on(brightness=45)
+ self.light.device.send_cmd.assert_has_calls(
+ [mock.call('on'), mock.call('dim 25')])
+
+ def test_turn_off(self):
+ """Test turn_off."""
+ self.light.turn_off()
+ self.light.device.send_cmd.assert_called_once_with('off')
+
+
+class TestMochadLight256Levels(unittest.TestCase):
+ """Test for mochad light platform."""
+
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ controller_mock = mock.MagicMock()
+ dev_dict = {'address': 'a1', 'name': 'fake_light',
+ 'brightness_levels': 256}
+ self.light = mochad.MochadLight(self.hass, controller_mock,
+ dev_dict)
+
+ def teardown_method(self, method):
+ """Stop everything that was started."""
+ self.hass.stop()
+
def test_turn_on_with_no_brightness(self):
"""Test turn_on."""
self.light.turn_on()
@@ -86,3 +120,35 @@ class TestMochadLight(unittest.TestCase):
"""Test turn_off."""
self.light.turn_off()
self.light.device.send_cmd.assert_called_once_with('off')
+
+
+class TestMochadLight64Levels(unittest.TestCase):
+ """Test for mochad light platform."""
+
+ def setUp(self):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ controller_mock = mock.MagicMock()
+ dev_dict = {'address': 'a1', 'name': 'fake_light',
+ 'brightness_levels': 64}
+ self.light = mochad.MochadLight(self.hass, controller_mock,
+ dev_dict)
+
+ def teardown_method(self, method):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ def test_turn_on_with_no_brightness(self):
+ """Test turn_on."""
+ self.light.turn_on()
+ self.light.device.send_cmd.assert_called_once_with('xdim 63')
+
+ def test_turn_on_with_brightness(self):
+ """Test turn_on."""
+ self.light.turn_on(brightness=45)
+ self.light.device.send_cmd.assert_called_once_with('xdim 11')
+
+ def test_turn_off(self):
+ """Test turn_off."""
+ self.light.turn_off()
+ self.light.device.send_cmd.assert_called_once_with('off')
diff --git a/tests/components/media_player/test_monoprice.py b/tests/components/media_player/test_monoprice.py
index 2bcd02e69aa..399cdc67ca6 100644
--- a/tests/components/media_player/test_monoprice.py
+++ b/tests/components/media_player/test_monoprice.py
@@ -1,27 +1,30 @@
"""The tests for Monoprice Media player platform."""
import unittest
+from unittest import mock
import voluptuous as vol
from collections import defaultdict
-
from homeassistant.components.media_player import (
- SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE,
+ DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE)
from homeassistant.const import STATE_ON, STATE_OFF
+import tests.common
from homeassistant.components.media_player.monoprice import (
- MonopriceZone, PLATFORM_SCHEMA)
+ DATA_MONOPRICE, PLATFORM_SCHEMA, SERVICE_SNAPSHOT,
+ SERVICE_RESTORE, setup_platform)
-class MockState(object):
- """Mock for zone state object."""
+class AttrDict(dict):
+ """Helper class for mocking attributes."""
- def __init__(self):
- """Init zone state."""
- self.power = True
- self.volume = 0
- self.mute = True
- self.source = 1
+ def __setattr__(self, name, value):
+ """Set attribute."""
+ self[name] = value
+
+ def __getattr__(self, item):
+ """Get attribute."""
+ return self[item]
class MockMonoprice(object):
@@ -29,11 +32,16 @@ class MockMonoprice(object):
def __init__(self):
"""Init mock object."""
- self.zones = defaultdict(lambda *a: MockState())
+ self.zones = defaultdict(lambda: AttrDict(power=True,
+ volume=0,
+ mute=True,
+ source=1))
def zone_status(self, zone_id):
"""Get zone status."""
- return self.zones[zone_id]
+ status = self.zones[zone_id]
+ status.zone = zone_id
+ return AttrDict(status)
def set_source(self, zone_id, source_idx):
"""Set source for zone."""
@@ -51,6 +59,10 @@ class MockMonoprice(object):
"""Set volume for zone."""
self.zones[zone_id].volume = volume
+ def restore_zone(self, zone):
+ """Restore zone status."""
+ self.zones[zone.zone] = AttrDict(zone)
+
class TestMonopriceSchema(unittest.TestCase):
"""Test Monoprice schema."""
@@ -147,11 +159,144 @@ class TestMonopriceMediaPlayer(unittest.TestCase):
def setUp(self):
"""Set up the test case."""
self.monoprice = MockMonoprice()
+ self.hass = tests.common.get_test_home_assistant()
+ self.hass.start()
# Note, source dictionary is unsorted!
- self.media_player = MonopriceZone(self.monoprice, {1: 'one',
- 3: 'three',
- 2: 'two'},
- 12, 'Zone name')
+ with mock.patch('pymonoprice.get_monoprice',
+ new=lambda *a: self.monoprice):
+ setup_platform(self.hass, {
+ 'platform': 'monoprice',
+ 'port': '/dev/ttyS0',
+ 'name': 'Name',
+ 'zones': {12: {'name': 'Zone name'}},
+ 'sources': {1: {'name': 'one'},
+ 3: {'name': 'three'},
+ 2: {'name': 'two'}},
+ }, lambda *args, **kwargs: None, {})
+ self.hass.block_till_done()
+ self.media_player = self.hass.data[DATA_MONOPRICE][0]
+ self.media_player.hass = self.hass
+ self.media_player.entity_id = 'media_player.zone_1'
+
+ def tearDown(self):
+ """Tear down the test case."""
+ self.hass.stop()
+
+ def test_setup_platform(self, *args):
+ """Test setting up platform."""
+ # Two services must be registered
+ self.assertTrue(self.hass.services.has_service(DOMAIN,
+ SERVICE_RESTORE))
+ self.assertTrue(self.hass.services.has_service(DOMAIN,
+ SERVICE_SNAPSHOT))
+ self.assertEqual(len(self.hass.data[DATA_MONOPRICE]), 1)
+ self.assertEqual(self.hass.data[DATA_MONOPRICE][0].name, 'Zone name')
+
+ def test_service_calls_with_entity_id(self):
+ """Test snapshot save/restore service calls."""
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual(0.0, self.media_player.volume_level, 0.0001)
+ self.assertTrue(self.media_player.is_volume_muted)
+ self.assertEqual('one', self.media_player.source)
+
+ # Saving default values
+ self.hass.services.call(DOMAIN, SERVICE_SNAPSHOT,
+ {'entity_id': 'media_player.zone_1'},
+ blocking=True)
+ # self.hass.block_till_done()
+
+ # Changing media player to new state
+ self.media_player.set_volume_level(1)
+ self.media_player.select_source('two')
+ self.media_player.mute_volume(False)
+ self.media_player.turn_off()
+
+ # Checking that values were indeed changed
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_OFF, self.media_player.state)
+ self.assertEqual(1.0, self.media_player.volume_level, 0.0001)
+ self.assertFalse(self.media_player.is_volume_muted)
+ self.assertEqual('two', self.media_player.source)
+
+ # Restoring wrong media player to its previous state
+ # Nothing should be done
+ self.hass.services.call(DOMAIN, SERVICE_RESTORE,
+ {'entity_id': 'not_existing'},
+ blocking=True)
+ # self.hass.block_till_done()
+
+ # Checking that values were not (!) restored
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_OFF, self.media_player.state)
+ self.assertEqual(1.0, self.media_player.volume_level, 0.0001)
+ self.assertFalse(self.media_player.is_volume_muted)
+ self.assertEqual('two', self.media_player.source)
+
+ # Restoring media player to its previous state
+ self.hass.services.call(DOMAIN, SERVICE_RESTORE,
+ {'entity_id': 'media_player.zone_1'},
+ blocking=True)
+ self.hass.block_till_done()
+
+ # Checking that values were restored
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual(0.0, self.media_player.volume_level, 0.0001)
+ self.assertTrue(self.media_player.is_volume_muted)
+ self.assertEqual('one', self.media_player.source)
+
+ def test_service_calls_without_entity_id(self):
+ """Test snapshot save/restore service calls."""
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual(0.0, self.media_player.volume_level, 0.0001)
+ self.assertTrue(self.media_player.is_volume_muted)
+ self.assertEqual('one', self.media_player.source)
+
+ # Restoring media player
+ # since there is no snapshot, nothing should be done
+ self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True)
+ self.hass.block_till_done()
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual(0.0, self.media_player.volume_level, 0.0001)
+ self.assertTrue(self.media_player.is_volume_muted)
+ self.assertEqual('one', self.media_player.source)
+
+ # Saving default values
+ self.hass.services.call(DOMAIN, SERVICE_SNAPSHOT, blocking=True)
+ self.hass.block_till_done()
+
+ # Changing media player to new state
+ self.media_player.set_volume_level(1)
+ self.media_player.select_source('two')
+ self.media_player.mute_volume(False)
+ self.media_player.turn_off()
+
+ # Checking that values were indeed changed
+ self.media_player.update()
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_OFF, self.media_player.state)
+ self.assertEqual(1.0, self.media_player.volume_level, 0.0001)
+ self.assertFalse(self.media_player.is_volume_muted)
+ self.assertEqual('two', self.media_player.source)
+
+ # Restoring media player to its previous state
+ self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True)
+ self.hass.block_till_done()
+
+ # Checking that values were restored
+ self.assertEqual('Zone name', self.media_player.name)
+ self.assertEqual(STATE_ON, self.media_player.state)
+ self.assertEqual(0.0, self.media_player.volume_level, 0.0001)
+ self.assertTrue(self.media_player.is_volume_muted)
+ self.assertEqual('one', self.media_player.source)
def test_update(self):
"""Test updating values from monoprice."""
diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py
index 8c62c6c84e9..815204e718a 100644
--- a/tests/components/media_player/test_sonos.py
+++ b/tests/components/media_player/test_sonos.py
@@ -281,6 +281,20 @@ class TestSonosMediaPlayer(unittest.TestCase):
self.assertEqual(unjoinMock.call_count, 1)
self.assertEqual(unjoinMock.call_args, mock.call())
+ @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('socket.create_connection', side_effect=socket.error())
+ def test_set_shuffle(self, shuffle_set_mock, *args):
+ """Ensuring soco methods called for sonos_snapshot service."""
+ sonos.setup_platform(self.hass, {}, fake_add_device, {
+ 'host': '192.0.2.1'
+ })
+ device = self.hass.data[sonos.DATA_SONOS][-1]
+ device.hass = self.hass
+
+ device.set_shuffle(True)
+ self.assertEqual(shuffle_set_mock.call_count, 1)
+ self.assertEqual(device._player.play_mode, 'SHUFFLE')
+
@mock.patch('soco.SoCo', new=SoCoMock)
@mock.patch('socket.create_connection', side_effect=socket.error())
@mock.patch.object(SoCoMock, 'set_sleep_timer')
@@ -375,3 +389,21 @@ class TestSonosMediaPlayer(unittest.TestCase):
device.restore()
self.assertEqual(restoreMock.call_count, 1)
self.assertEqual(restoreMock.call_args, mock.call(False))
+
+ @mock.patch('soco.SoCo', new=SoCoMock)
+ @mock.patch('socket.create_connection', side_effect=socket.error())
+ def test_sonos_set_option(self, option_mock, *args):
+ """Ensuring soco methods called for sonos_set_option service."""
+ sonos.setup_platform(self.hass, {}, fake_add_device, {
+ 'host': '192.0.2.1'
+ })
+ device = self.hass.data[sonos.DATA_SONOS][-1]
+ device.hass = self.hass
+
+ option_mock.return_value = True
+ device._snapshot_coordinator = mock.MagicMock()
+ device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17')
+
+ device.update_option(night_sound=True, speech_enhance=True)
+
+ self.assertEqual(option_mock.call_count, 1)
diff --git a/tests/components/media_player/test_webostv.py b/tests/components/media_player/test_webostv.py
new file mode 100644
index 00000000000..8017ad6cd54
--- /dev/null
+++ b/tests/components/media_player/test_webostv.py
@@ -0,0 +1,60 @@
+"""The tests for the LG webOS media player platform."""
+import unittest
+from unittest import mock
+
+from homeassistant.components.media_player import webostv
+
+
+class FakeLgWebOSDevice(webostv.LgWebOSDevice):
+ """A fake device without the client setup required for the real one."""
+
+ def __init__(self, *args, **kwargs):
+ """Initialise parameters needed for tests with fake values."""
+ self._source_list = {}
+ self._client = mock.MagicMock()
+ self._name = 'fake_device'
+ self._current_source = None
+
+
+class TestLgWebOSDevice(unittest.TestCase):
+ """Test the LgWebOSDevice class."""
+
+ def setUp(self):
+ """Configure a fake device for each test."""
+ self.device = FakeLgWebOSDevice()
+
+ def test_select_source_with_empty_source_list(self):
+ """Ensure we don't call client methods when we don't have sources."""
+ self.device.select_source('nonexistent')
+ assert 0 == self.device._client.launch_app.call_count
+ assert 0 == self.device._client.set_input.call_count
+
+ def test_select_source_with_titled_entry(self):
+ """Test that a titled source is treated as an app."""
+ self.device._source_list = {
+ 'existent': {
+ 'id': 'existent_id',
+ 'title': 'existent_title',
+ },
+ }
+
+ self.device.select_source('existent')
+
+ assert 'existent_title' == self.device._current_source
+ assert [mock.call('existent_id')] == (
+ self.device._client.launch_app.call_args_list)
+
+ def test_select_source_with_labelled_entry(self):
+ """Test that a labelled source is treated as an input source."""
+ self.device._source_list = {
+ 'existent': {
+ 'id': 'existent_id',
+ 'label': 'existent_label',
+ },
+ }
+
+ self.device.select_source('existent')
+
+ assert 'existent_label' == self.device._current_source
+ assert [mock.call('existent_id')] == (
+ self.device._client.set_input.call_args_list)
diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py
new file mode 100644
index 00000000000..99df05f36a4
--- /dev/null
+++ b/tests/components/sensor/test_canary.py
@@ -0,0 +1,125 @@
+"""The tests for the Canary sensor platform."""
+import copy
+import unittest
+from unittest.mock import patch, Mock
+
+from canary.api import SensorType
+from homeassistant.components import canary as base_canary
+from homeassistant.components.canary import DATA_CANARY
+from homeassistant.components.sensor import canary
+from homeassistant.components.sensor.canary import CanarySensor
+from tests.common import (get_test_home_assistant)
+from tests.components.test_canary import mock_device, mock_reading, \
+ mock_location
+
+VALID_CONFIG = {
+ "canary": {
+ "username": "foo@bar.org",
+ "password": "bar",
+ }
+}
+
+
+class TestCanarySensorSetup(unittest.TestCase):
+ """Test the Canary platform."""
+
+ DEVICES = []
+
+ def add_devices(self, devices, action):
+ """Mock add devices."""
+ for device in devices:
+ self.DEVICES.append(device)
+
+ def setUp(self):
+ """Initialize values for this testcase class."""
+ self.hass = get_test_home_assistant()
+ self.config = copy.deepcopy(VALID_CONFIG)
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ self.hass.stop()
+
+ @patch('homeassistant.components.canary.CanaryData')
+ def test_setup_sensors(self, mock_canary):
+ """Test the sensor setup."""
+ base_canary.setup(self.hass, self.config)
+
+ online_device_at_home = mock_device(20, "Dining Room", True)
+ offline_device_at_home = mock_device(21, "Front Yard", False)
+ online_device_at_work = mock_device(22, "Office", True)
+
+ self.hass.data[DATA_CANARY] = mock_canary()
+ self.hass.data[DATA_CANARY].locations = [
+ mock_location("Home", True, devices=[online_device_at_home,
+ offline_device_at_home]),
+ mock_location("Work", True, devices=[online_device_at_work]),
+ ]
+
+ canary.setup_platform(self.hass, self.config, self.add_devices, None)
+
+ 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):
+ """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)]
+
+ sensor = CanarySensor(data, SensorType.TEMPERATURE, 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)
+
+ def test_humidity_sensor(self):
+ """Test humidity sensor."""
+ device = mock_device(10, "Family Room")
+ location = mock_location("Home")
+
+ data = Mock()
+ data.get_readings.return_value = [
+ mock_reading(SensorType.HUMIDITY, 50.4567)]
+
+ sensor = CanarySensor(data, SensorType.HUMIDITY, location, device)
+ sensor.update()
+
+ self.assertEqual("Home Family Room Humidity", sensor.name)
+ self.assertEqual("%", sensor.unit_of_measurement)
+ self.assertEqual(50.5, sensor.state)
+
+ def test_air_quality_sensor(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)]
+
+ sensor = CanarySensor(data, SensorType.AIR_QUALITY, 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)
diff --git a/tests/components/sensor/test_hydroquebec.py b/tests/components/sensor/test_hydroquebec.py
new file mode 100644
index 00000000000..f2ca97313d3
--- /dev/null
+++ b/tests/components/sensor/test_hydroquebec.py
@@ -0,0 +1,89 @@
+"""The test for the hydroquebec sensor platform."""
+import asyncio
+import logging
+import sys
+from unittest.mock import MagicMock
+
+from homeassistant.bootstrap import async_setup_component
+from homeassistant.components.sensor import hydroquebec
+from tests.common import assert_setup_component
+
+
+CONTRACT = "123456789"
+
+
+class HydroQuebecClientMock():
+ """Fake Hydroquebec client."""
+
+ def __init__(self, username, password, contract=None):
+ """Fake Hydroquebec client init."""
+ pass
+
+ def get_data(self, contract):
+ """Return fake hydroquebec data."""
+ return {CONTRACT: {"balance": 160.12}}
+
+ def get_contracts(self):
+ """Return fake hydroquebec contracts."""
+ return [CONTRACT]
+
+ @asyncio.coroutine
+ def fetch_data(self):
+ """Return fake fetching data."""
+ pass
+
+
+class HydroQuebecClientMockError(HydroQuebecClientMock):
+ """Fake Hydroquebec client error."""
+
+ @asyncio.coroutine
+ def fetch_data(self):
+ """Return fake fetching data."""
+ raise hydroquebec.PyHydroQuebecError("Fake Error")
+
+
+class PyHydroQuebecErrorMock(BaseException):
+ """Fake PyHydroquebec Error."""
+
+
+@asyncio.coroutine
+def test_hydroquebec_sensor(loop, hass):
+ """Test the Hydroquebec number sensor."""
+ sys.modules['pyhydroquebec'] = MagicMock()
+ sys.modules['pyhydroquebec.client'] = MagicMock()
+ sys.modules['pyhydroquebec.client.PyHydroQuebecError'] = \
+ PyHydroQuebecErrorMock
+ import pyhydroquebec.client
+ pyhydroquebec.HydroQuebecClient = HydroQuebecClientMock
+ pyhydroquebec.client.PyHydroQuebecError = PyHydroQuebecErrorMock
+ config = {
+ 'sensor': {
+ 'platform': 'hydroquebec',
+ 'name': 'hydro',
+ 'contract': CONTRACT,
+ 'username': 'myusername',
+ 'password': 'password',
+ 'monitored_variables': [
+ 'balance',
+ ],
+ }
+ }
+ with assert_setup_component(1):
+ yield from async_setup_component(hass, 'sensor', config)
+ state = hass.states.get('sensor.hydro_balance')
+ assert state.state == "160.12"
+ assert state.attributes.get('unit_of_measurement') == "CAD"
+
+
+@asyncio.coroutine
+def test_error(hass, caplog):
+ """Test the Hydroquebec sensor errors."""
+ caplog.set_level(logging.ERROR)
+ sys.modules['pyhydroquebec'] = MagicMock()
+ sys.modules['pyhydroquebec.client'] = MagicMock()
+ import pyhydroquebec.client
+ pyhydroquebec.HydroQuebecClient = HydroQuebecClientMockError
+ pyhydroquebec.client.PyHydroQuebecError = BaseException
+ hydro_data = hydroquebec.HydroquebecData('username', 'password')
+ yield from hydro_data._fetch_data()
+ assert "Error on receive last Hydroquebec data: " in caplog.text
diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py
index 1bda8ab82f3..eddab8caf4d 100644
--- a/tests/components/sensor/test_rest.py
+++ b/tests/components/sensor/test_rest.py
@@ -132,10 +132,12 @@ class TestRestSensor(unittest.TestCase):
self.unit_of_measurement = 'MB'
self.value_template = template('{{ value_json.key }}')
self.value_template.hass = self.hass
+ self.force_update = False
- self.sensor = rest.RestSensor(self.hass, self.rest, self.name,
- self.unit_of_measurement,
- self.value_template, [])
+ self.sensor = rest.RestSensor(
+ self.hass, self.rest, self.name, self.unit_of_measurement,
+ self.value_template, [], self.force_update
+ )
def tearDown(self):
"""Stop everything that was started."""
@@ -154,6 +156,11 @@ class TestRestSensor(unittest.TestCase):
self.assertEqual(
self.unit_of_measurement, self.sensor.unit_of_measurement)
+ def test_force_update(self):
+ """Test the unit of measurement."""
+ self.assertEqual(
+ self.force_update, self.sensor.force_update)
+
def test_state(self):
"""Test the initial state."""
self.sensor.update()
@@ -182,7 +189,8 @@ class TestRestSensor(unittest.TestCase):
side_effect=self.update_side_effect(
'plain_state'))
self.sensor = rest.RestSensor(self.hass, self.rest, self.name,
- self.unit_of_measurement, None, [])
+ self.unit_of_measurement, None, [],
+ self.force_update)
self.sensor.update()
self.assertEqual('plain_state', self.sensor.state)
self.assertTrue(self.sensor.available)
@@ -193,7 +201,8 @@ class TestRestSensor(unittest.TestCase):
side_effect=self.update_side_effect(
'{ "key": "some_json_value" }'))
self.sensor = rest.RestSensor(self.hass, self.rest, self.name,
- self.unit_of_measurement, None, ['key'])
+ self.unit_of_measurement, None, ['key'],
+ self.force_update)
self.sensor.update()
self.assertEqual('some_json_value',
self.sensor.device_state_attributes['key'])
@@ -205,7 +214,8 @@ class TestRestSensor(unittest.TestCase):
side_effect=self.update_side_effect(
'["list", "of", "things"]'))
self.sensor = rest.RestSensor(self.hass, self.rest, self.name,
- self.unit_of_measurement, None, ['key'])
+ self.unit_of_measurement, None, ['key'],
+ self.force_update)
self.sensor.update()
self.assertEqual({}, self.sensor.device_state_attributes)
self.assertTrue(mock_logger.warning.called)
@@ -217,7 +227,8 @@ class TestRestSensor(unittest.TestCase):
side_effect=self.update_side_effect(
'This is text rather than JSON data.'))
self.sensor = rest.RestSensor(self.hass, self.rest, self.name,
- self.unit_of_measurement, None, ['key'])
+ self.unit_of_measurement, None, ['key'],
+ self.force_update)
self.sensor.update()
self.assertEqual({}, self.sensor.device_state_attributes)
self.assertTrue(mock_logger.warning.called)
@@ -230,12 +241,14 @@ class TestRestSensor(unittest.TestCase):
'{ "key": "json_state_updated_value" }'))
self.sensor = rest.RestSensor(self.hass, self.rest, self.name,
self.unit_of_measurement,
- self.value_template, ['key'])
+ self.value_template, ['key'],
+ self.force_update)
self.sensor.update()
self.assertEqual('json_state_updated_value', self.sensor.state)
self.assertEqual('json_state_updated_value',
- self.sensor.device_state_attributes['key'])
+ self.sensor.device_state_attributes['key'],
+ self.force_update)
class TestRestData(unittest.TestCase):
diff --git a/tests/components/sensor/test_statistics.py b/tests/components/sensor/test_statistics.py
index bfb8fb61f9b..48ebf720633 100644
--- a/tests/components/sensor/test_statistics.py
+++ b/tests/components/sensor/test_statistics.py
@@ -3,7 +3,8 @@ import unittest
import statistics
from homeassistant.setup import setup_component
-from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS)
+from homeassistant.const import (
+ ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN)
from homeassistant.util import dt as dt_util
from tests.common import get_test_home_assistant
from unittest.mock import patch
@@ -106,6 +107,38 @@ class TestStatisticsSensor(unittest.TestCase):
self.assertEqual(3.8, state.attributes.get('min_value'))
self.assertEqual(14, state.attributes.get('max_value'))
+ def test_sampling_size_1(self):
+ """Test validity of stats requiring only one sample."""
+ assert setup_component(self.hass, 'sensor', {
+ 'sensor': {
+ 'platform': 'statistics',
+ 'name': 'test',
+ 'entity_id': 'sensor.test_monitored',
+ 'sampling_size': 1,
+ }
+ })
+
+ for value in self.values[-3:]: # just the last 3 will do
+ self.hass.states.set('sensor.test_monitored', value,
+ {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS})
+ self.hass.block_till_done()
+
+ state = self.hass.states.get('sensor.test_mean')
+
+ # require only one data point
+ self.assertEqual(self.values[-1], state.attributes.get('min_value'))
+ self.assertEqual(self.values[-1], state.attributes.get('max_value'))
+ self.assertEqual(self.values[-1], state.attributes.get('mean'))
+ self.assertEqual(self.values[-1], state.attributes.get('median'))
+ self.assertEqual(self.values[-1], state.attributes.get('total'))
+ self.assertEqual(0, state.attributes.get('change'))
+ self.assertEqual(0, state.attributes.get('average_change'))
+
+ # require at least two data points
+ self.assertEqual(STATE_UNKNOWN, state.attributes.get('variance'))
+ self.assertEqual(STATE_UNKNOWN,
+ state.attributes.get('standard_deviation'))
+
def test_max_age(self):
"""Test value deprecation."""
mock_data = {
diff --git a/tests/components/test_canary.py b/tests/components/test_canary.py
new file mode 100644
index 00000000000..67122813fb7
--- /dev/null
+++ b/tests/components/test_canary.py
@@ -0,0 +1,85 @@
+"""The tests for the Canary component."""
+import unittest
+from unittest.mock import patch, MagicMock, PropertyMock
+
+import homeassistant.components.canary as canary
+from homeassistant import setup
+from tests.common import (
+ get_test_home_assistant)
+
+
+def mock_device(device_id, name, is_online=True):
+ """Mock Canary Device class."""
+ device = MagicMock()
+ type(device).device_id = PropertyMock(return_value=device_id)
+ type(device).name = PropertyMock(return_value=name)
+ type(device).is_online = PropertyMock(return_value=is_online)
+ return device
+
+
+def mock_location(name, is_celsius=True, devices=[]):
+ """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)
+ return location
+
+
+def mock_reading(sensor_type, sensor_value):
+ """Mock Canary Reading class."""
+ reading = MagicMock()
+ type(reading).sensor_type = PropertyMock(return_value=sensor_type)
+ type(reading).value = PropertyMock(return_value=sensor_value)
+ return reading
+
+
+class TestCanary(unittest.TestCase):
+ """Tests the Canary 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 was started."""
+ self.hass.stop()
+
+ @patch('homeassistant.components.canary.CanaryData.update')
+ @patch('canary.api.Api.login')
+ def test_setup_with_valid_config(self, mock_login, mock_update):
+ """Test setup component."""
+ config = {
+ "canary": {
+ "username": "foo@bar.org",
+ "password": "bar",
+ }
+ }
+
+ self.assertTrue(
+ setup.setup_component(self.hass, canary.DOMAIN, config))
+
+ mock_update.assert_called_once_with()
+ mock_login.assert_called_once_with()
+
+ def test_setup_with_missing_password(self):
+ """Test setup component."""
+ config = {
+ "canary": {
+ "username": "foo@bar.org",
+ }
+ }
+
+ self.assertFalse(
+ setup.setup_component(self.hass, canary.DOMAIN, config))
+
+ def test_setup_with_missing_username(self):
+ """Test setup component."""
+ config = {
+ "canary": {
+ "password": "bar",
+ }
+ }
+
+ self.assertFalse(
+ setup.setup_component(self.hass, canary.DOMAIN, config))
diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py
new file mode 100644
index 00000000000..227295594db
--- /dev/null
+++ b/tests/components/test_hue.py
@@ -0,0 +1,402 @@
+"""Generic Philips Hue component tests."""
+
+import logging
+import unittest
+from unittest.mock import call, MagicMock, patch
+
+from homeassistant.components import configurator, hue
+from homeassistant.const import CONF_FILENAME, CONF_HOST
+from homeassistant.setup import setup_component
+
+from tests.common import (
+ assert_setup_component, get_test_home_assistant, get_test_config_dir,
+ MockDependency
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class TestSetup(unittest.TestCase):
+ """Test the Hue component."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.skip_teardown_stop = False
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ if not self.skip_teardown_stop:
+ self.hass.stop()
+
+ @MockDependency('phue')
+ def test_setup_no_domain(self, mock_phue):
+ """If it's not in the config we won't even try."""
+ with assert_setup_component(0):
+ self.assertTrue(setup_component(
+ self.hass, hue.DOMAIN, {}))
+ mock_phue.Bridge.assert_not_called()
+ self.assertEquals({}, self.hass.data[hue.DOMAIN])
+
+ @MockDependency('phue')
+ def test_setup_no_host(self, mock_phue):
+ """No host specified in any way."""
+ with assert_setup_component(1):
+ self.assertTrue(setup_component(
+ self.hass, hue.DOMAIN, {hue.DOMAIN: {}}))
+ mock_phue.Bridge.assert_not_called()
+ self.assertEquals({}, self.hass.data[hue.DOMAIN])
+
+ @MockDependency('phue')
+ def test_setup_with_host(self, mock_phue):
+ """Host specified in the config file."""
+ mock_bridge = mock_phue.Bridge
+
+ with assert_setup_component(1):
+ with patch('homeassistant.helpers.discovery.load_platform') \
+ as mock_load:
+ self.assertTrue(setup_component(
+ self.hass, hue.DOMAIN,
+ {hue.DOMAIN: {hue.CONF_BRIDGES: [
+ {CONF_HOST: 'localhost'}]}}))
+
+ mock_bridge.assert_called_once_with(
+ 'localhost',
+ config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
+ mock_load.assert_called_once_with(
+ self.hass, 'light', hue.DOMAIN,
+ {'bridge_id': '127.0.0.1'})
+
+ self.assertTrue(hue.DOMAIN in self.hass.data)
+ self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+
+ @MockDependency('phue')
+ def test_setup_with_phue_conf(self, mock_phue):
+ """No host in the config file, but one is cached in phue.conf."""
+ mock_bridge = mock_phue.Bridge
+
+ with assert_setup_component(1):
+ with patch(
+ 'homeassistant.components.hue._find_host_from_config',
+ return_value='localhost'):
+ with patch('homeassistant.helpers.discovery.load_platform') \
+ as mock_load:
+ self.assertTrue(setup_component(
+ self.hass, hue.DOMAIN,
+ {hue.DOMAIN: {hue.CONF_BRIDGES: [
+ {CONF_FILENAME: 'phue.conf'}]}}))
+
+ mock_bridge.assert_called_once_with(
+ 'localhost',
+ config_file_path=get_test_config_dir(
+ hue.PHUE_CONFIG_FILE))
+ mock_load.assert_called_once_with(
+ self.hass, 'light', hue.DOMAIN,
+ {'bridge_id': '127.0.0.1'})
+
+ self.assertTrue(hue.DOMAIN in self.hass.data)
+ self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+
+ @MockDependency('phue')
+ def test_setup_with_multiple_hosts(self, mock_phue):
+ """Multiple hosts specified in the config file."""
+ mock_bridge = mock_phue.Bridge
+
+ with assert_setup_component(1):
+ with patch('homeassistant.helpers.discovery.load_platform') \
+ as mock_load:
+ self.assertTrue(setup_component(
+ self.hass, hue.DOMAIN,
+ {hue.DOMAIN: {hue.CONF_BRIDGES: [
+ {CONF_HOST: 'localhost'},
+ {CONF_HOST: '192.168.0.1'}]}}))
+
+ mock_bridge.assert_has_calls([
+ call(
+ 'localhost',
+ config_file_path=get_test_config_dir(
+ hue.PHUE_CONFIG_FILE)),
+ call(
+ '192.168.0.1',
+ config_file_path=get_test_config_dir(
+ hue.PHUE_CONFIG_FILE))])
+ mock_load.mock_bridge.assert_not_called()
+ mock_load.assert_has_calls([
+ call(
+ self.hass, 'light', hue.DOMAIN,
+ {'bridge_id': '127.0.0.1'}),
+ call(
+ self.hass, 'light', hue.DOMAIN,
+ {'bridge_id': '192.168.0.1'}),
+ ], any_order=True)
+
+ self.assertTrue(hue.DOMAIN in self.hass.data)
+ self.assertEquals(2, len(self.hass.data[hue.DOMAIN]))
+
+ @MockDependency('phue')
+ def test_bridge_discovered(self, mock_phue):
+ """Bridge discovery."""
+ mock_bridge = mock_phue.Bridge
+ mock_service = MagicMock()
+ discovery_info = {'host': '192.168.0.10', 'serial': 'foobar'}
+
+ with patch('homeassistant.helpers.discovery.load_platform') \
+ as mock_load:
+ self.assertTrue(setup_component(
+ self.hass, hue.DOMAIN, {}))
+ hue.bridge_discovered(self.hass, mock_service, discovery_info)
+
+ mock_bridge.assert_called_once_with(
+ '192.168.0.10',
+ config_file_path=get_test_config_dir('phue-foobar.conf'))
+ mock_load.assert_called_once_with(
+ self.hass, 'light', hue.DOMAIN,
+ {'bridge_id': '192.168.0.10'})
+
+ self.assertTrue(hue.DOMAIN in self.hass.data)
+ self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+
+ @MockDependency('phue')
+ def test_bridge_configure_and_discovered(self, mock_phue):
+ """Bridge is in the config file, then we discover it."""
+ mock_bridge = mock_phue.Bridge
+ mock_service = MagicMock()
+ discovery_info = {'host': '192.168.1.10', 'serial': 'foobar'}
+
+ with assert_setup_component(1):
+ with patch('homeassistant.helpers.discovery.load_platform') \
+ as mock_load:
+ # First we set up the component from config
+ self.assertTrue(setup_component(
+ self.hass, hue.DOMAIN,
+ {hue.DOMAIN: {hue.CONF_BRIDGES: [
+ {CONF_HOST: '192.168.1.10'}]}}))
+
+ mock_bridge.assert_called_once_with(
+ '192.168.1.10',
+ config_file_path=get_test_config_dir(
+ hue.PHUE_CONFIG_FILE))
+ calls_to_mock_load = [
+ call(
+ self.hass, 'light', hue.DOMAIN,
+ {'bridge_id': '192.168.1.10'}),
+ ]
+ 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]))
+
+ # Then we discover the same bridge
+ hue.bridge_discovered(self.hass, mock_service, discovery_info)
+
+ # No additional calls
+ mock_bridge.assert_called_once_with(
+ '192.168.1.10',
+ config_file_path=get_test_config_dir(
+ hue.PHUE_CONFIG_FILE))
+ mock_load.assert_has_calls(calls_to_mock_load)
+
+ # Still only one
+ self.assertTrue(hue.DOMAIN in self.hass.data)
+ self.assertEquals(1, len(self.hass.data[hue.DOMAIN]))
+
+
+class TestHueBridge(unittest.TestCase):
+ """Test the HueBridge class."""
+
+ def setUp(self): # pylint: disable=invalid-name
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+ self.hass.data[hue.DOMAIN] = {}
+ self.skip_teardown_stop = False
+
+ def tearDown(self):
+ """Stop everything that was started."""
+ if not self.skip_teardown_stop:
+ self.hass.stop()
+
+ @MockDependency('phue')
+ def test_setup_bridge_connection_refused(self, mock_phue):
+ """Test a registration failed with a connection refused exception."""
+ mock_bridge = mock_phue.Bridge
+ mock_bridge.side_effect = ConnectionRefusedError()
+
+ bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
+ bridge.setup()
+ self.assertFalse(bridge.configured)
+ self.assertTrue(bridge.config_request_id is None)
+
+ mock_bridge.assert_called_once_with(
+ 'localhost',
+ config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
+
+ @MockDependency('phue')
+ def test_setup_bridge_registration_exception(self, mock_phue):
+ """Test a registration failed with an exception."""
+ mock_bridge = mock_phue.Bridge
+ mock_phue.PhueRegistrationException = Exception
+ mock_bridge.side_effect = mock_phue.PhueRegistrationException(1, 2)
+
+ bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
+ bridge.setup()
+ self.assertFalse(bridge.configured)
+ self.assertFalse(bridge.config_request_id is None)
+ self.assertTrue(isinstance(bridge.config_request_id, str))
+
+ mock_bridge.assert_called_once_with(
+ 'localhost',
+ config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
+
+ @MockDependency('phue')
+ def test_setup_bridge_registration_succeeds(self, mock_phue):
+ """Test a registration success sequence."""
+ mock_bridge = mock_phue.Bridge
+ mock_phue.PhueRegistrationException = Exception
+ mock_bridge.side_effect = [
+ # First call, raise because not registered
+ mock_phue.PhueRegistrationException(1, 2),
+ # Second call, registration is done
+ None,
+ ]
+
+ bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
+ bridge.setup()
+ self.assertFalse(bridge.configured)
+ self.assertFalse(bridge.config_request_id is None)
+
+ # Simulate the user confirming the registration
+ self.hass.services.call(
+ configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
+ {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})
+
+ self.hass.block_till_done()
+ self.assertTrue(bridge.configured)
+ self.assertTrue(bridge.config_request_id is None)
+
+ # We should see a total of two identical calls
+ args = call(
+ 'localhost',
+ config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
+ mock_bridge.assert_has_calls([args, args])
+
+ # Make sure the request is done
+ self.assertEqual(1, len(self.hass.states.all()))
+ self.assertEqual('configured', self.hass.states.all()[0].state)
+
+ @MockDependency('phue')
+ def test_setup_bridge_registration_fails(self, mock_phue):
+ """
+ Test a registration failure sequence.
+
+ This may happen when we start the registration process, the user
+ responds to the request but the bridge has become unreachable.
+ """
+ mock_bridge = mock_phue.Bridge
+ mock_phue.PhueRegistrationException = Exception
+ mock_bridge.side_effect = [
+ # First call, raise because not registered
+ mock_phue.PhueRegistrationException(1, 2),
+ # Second call, the bridge has gone away
+ ConnectionRefusedError(),
+ ]
+
+ bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
+ bridge.setup()
+ self.assertFalse(bridge.configured)
+ self.assertFalse(bridge.config_request_id is None)
+
+ # Simulate the user confirming the registration
+ self.hass.services.call(
+ configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
+ {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})
+
+ self.hass.block_till_done()
+ self.assertFalse(bridge.configured)
+ self.assertFalse(bridge.config_request_id is None)
+
+ # We should see a total of two identical calls
+ args = call(
+ 'localhost',
+ config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
+ mock_bridge.assert_has_calls([args, args])
+
+ # The request should still be pending
+ self.assertEqual(1, len(self.hass.states.all()))
+ self.assertEqual('configure', self.hass.states.all()[0].state)
+
+ @MockDependency('phue')
+ def test_setup_bridge_registration_retry(self, mock_phue):
+ """
+ Test a registration retry sequence.
+
+ This may happen when we start the registration process, the user
+ responds to the request but we fail to confirm it with the bridge.
+ """
+ mock_bridge = mock_phue.Bridge
+ mock_phue.PhueRegistrationException = Exception
+ mock_bridge.side_effect = [
+ # First call, raise because not registered
+ mock_phue.PhueRegistrationException(1, 2),
+ # Second call, for whatever reason authentication fails
+ mock_phue.PhueRegistrationException(1, 2),
+ ]
+
+ bridge = hue.HueBridge('localhost', self.hass, hue.PHUE_CONFIG_FILE)
+ bridge.setup()
+ self.assertFalse(bridge.configured)
+ self.assertFalse(bridge.config_request_id is None)
+
+ # Simulate the user confirming the registration
+ self.hass.services.call(
+ configurator.DOMAIN, configurator.SERVICE_CONFIGURE,
+ {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id})
+
+ self.hass.block_till_done()
+ self.assertFalse(bridge.configured)
+ self.assertFalse(bridge.config_request_id is None)
+
+ # We should see a total of two identical calls
+ args = call(
+ 'localhost',
+ config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE))
+ mock_bridge.assert_has_calls([args, args])
+
+ # Make sure the request is done
+ self.assertEqual(1, len(self.hass.states.all()))
+ self.assertEqual('configure', self.hass.states.all()[0].state)
+ self.assertEqual(
+ 'Failed to register, please try again.',
+ self.hass.states.all()[0].attributes.get(configurator.ATTR_ERRORS))
+
+ @MockDependency('phue')
+ def test_hue_activate_scene(self, mock_phue):
+ """Test the hue_activate_scene service."""
+ with patch('homeassistant.helpers.discovery.load_platform'):
+ bridge = hue.HueBridge('localhost', self.hass,
+ hue.PHUE_CONFIG_FILE)
+ bridge.setup()
+
+ # No args
+ self.hass.services.call(hue.DOMAIN, hue.SERVICE_HUE_SCENE,
+ blocking=True)
+ bridge.bridge.run_scene.assert_not_called()
+
+ # Only one arg
+ self.hass.services.call(
+ hue.DOMAIN, hue.SERVICE_HUE_SCENE,
+ {hue.ATTR_GROUP_NAME: 'group'},
+ blocking=True)
+ bridge.bridge.run_scene.assert_not_called()
+
+ self.hass.services.call(
+ hue.DOMAIN, hue.SERVICE_HUE_SCENE,
+ {hue.ATTR_SCENE_NAME: 'scene'},
+ blocking=True)
+ bridge.bridge.run_scene.assert_not_called()
+
+ # Both required args
+ self.hass.services.call(
+ hue.DOMAIN, hue.SERVICE_HUE_SCENE,
+ {hue.ATTR_GROUP_NAME: 'group', hue.ATTR_SCENE_NAME: 'scene'},
+ blocking=True)
+ bridge.bridge.run_scene.assert_called_once_with('group', 'scene')
diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py
index 5e49bbd0382..a3e6fac0295 100644
--- a/tests/components/test_snips.py
+++ b/tests/components/test_snips.py
@@ -34,7 +34,7 @@ def test_snips_call_action(hass, mqtt_mock):
intents = async_mock_intent(hass, 'Lights')
- async_fire_mqtt_message(hass, 'hermes/nlu/intentParsed',
+ async_fire_mqtt_message(hass, 'hermes/intent/activateLights',
EXAMPLE_MSG)
yield from hass.async_block_till_done()
assert len(intents) == 1
diff --git a/tox.ini b/tox.ini
index f3e58ce8889..32f80b95dc1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,7 +18,7 @@ deps =
-c{toxinidir}/homeassistant/package_constraints.txt
[testenv:pylint]
-basepython = python3
+basepython = {env:PYTHON3_PATH:python3}
ignore_errors = True
deps =
-r{toxinidir}/requirements_all.txt
@@ -28,7 +28,7 @@ commands =
pylint homeassistant
[testenv:lint]
-basepython = python3
+basepython = {env:PYTHON3_PATH:python3}
deps =
-r{toxinidir}/requirements_test.txt
commands =
@@ -37,7 +37,7 @@ commands =
pydocstyle homeassistant tests
[testenv:typing]
-basepython = python3
+basepython = {env:PYTHON3_PATH:python3}
deps =
-r{toxinidir}/requirements_test.txt
commands =