diff --git a/.coveragerc b/.coveragerc
index a18ec476010..d8041b9fe6c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -8,6 +8,9 @@ omit =
homeassistant/helpers/signal.py
# omit pieces of code that rely on external devices being present
+ homeassistant/components/abode.py
+ homeassistant/components/*/abode.py
+
homeassistant/components/alarmdecoder.py
homeassistant/components/*/alarmdecoder.py
@@ -176,6 +179,9 @@ omit =
homeassistant/components/notify/twilio_sms.py
homeassistant/components/notify/twilio_call.py
+ homeassistant/components/usps.py
+ homeassistant/components/*/usps.py
+
homeassistant/components/velbus.py
homeassistant/components/*/velbus.py
@@ -326,6 +332,7 @@ omit =
homeassistant/components/light/yeelightsunflower.py
homeassistant/components/light/zengge.py
homeassistant/components/lirc.py
+ homeassistant/components/lock/nello.py
homeassistant/components/lock/nuki.py
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/sesame.py
@@ -383,6 +390,7 @@ omit =
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/gntp.py
homeassistant/components/notify/group.py
+ homeassistant/components/notify/hipchat.py
homeassistant/components/notify/instapush.py
homeassistant/components/notify/kodi.py
homeassistant/components/notify/lannouncer.py
@@ -391,6 +399,7 @@ omit =
homeassistant/components/notify/message_bird.py
homeassistant/components/notify/nfandroidtv.py
homeassistant/components/notify/nma.py
+ homeassistant/components/notify/prowl.py
homeassistant/components/notify/pushbullet.py
homeassistant/components/notify/pushetta.py
homeassistant/components/notify/pushover.py
@@ -517,9 +526,9 @@ omit =
homeassistant/components/sensor/uber.py
homeassistant/components/sensor/upnp.py
homeassistant/components/sensor/ups.py
- homeassistant/components/sensor/usps.py
homeassistant/components/sensor/vasttrafik.py
homeassistant/components/sensor/waqi.py
+ homeassistant/components/sensor/worldtidesinfo.py
homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/zamg.py
diff --git a/.gitignore b/.gitignore
index 26efcc25b85..87bc6990ce4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,3 +94,6 @@ docs/build
# Windows Explorer
desktop.ini
+/home-assistant.pyproj
+/home-assistant.sln
+/.vs/home-assistant/v14
diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py
new file mode 100644
index 00000000000..677fcab4f5d
--- /dev/null
+++ b/homeassistant/components/abode.py
@@ -0,0 +1,75 @@
+"""
+This component provides basic support for Abode Home Security system.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/abode/
+"""
+import logging
+
+import voluptuous as vol
+from requests.exceptions import HTTPError, ConnectTimeout
+from homeassistant.helpers import discovery
+from homeassistant.helpers import config_validation as cv
+from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME
+
+REQUIREMENTS = ['abodepy==0.7.1']
+
+_LOGGER = logging.getLogger(__name__)
+
+CONF_ATTRIBUTION = "Data provided by goabode.com"
+
+DOMAIN = 'abode'
+DEFAULT_NAME = 'Abode'
+DATA_ABODE = 'data_abode'
+DEFAULT_ENTITY_NAMESPACE = 'abode'
+
+NOTIFICATION_ID = 'abode_notification'
+NOTIFICATION_TITLE = 'Abode Security Setup'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.Schema({
+ vol.Required(CONF_USERNAME): cv.string,
+ vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ }),
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up Abode component."""
+ conf = config[DOMAIN]
+ username = conf.get(CONF_USERNAME)
+ password = conf.get(CONF_PASSWORD)
+
+ try:
+ data = AbodeData(username, password)
+ hass.data[DATA_ABODE] = data
+
+ for component in ['binary_sensor', 'alarm_control_panel']:
+ discovery.load_platform(hass, component, DOMAIN, {}, config)
+
+ except (ConnectTimeout, HTTPError) as ex:
+ _LOGGER.error("Unable to connect to Abode: %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
+
+ return True
+
+
+class AbodeData:
+ """Shared Abode data."""
+
+ def __init__(self, username, password):
+ """Initialize Abode oject."""
+ import abodepy
+
+ self.abode = abodepy.Abode(username, password)
+ self.devices = self.abode.get_devices()
+
+ _LOGGER.debug("Abode Security set up with %s devices",
+ len(self.devices))
diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py
index 39c86f3215f..005048ba8c1 100644
--- a/homeassistant/components/alarm_control_panel/__init__.py
+++ b/homeassistant/components/alarm_control_panel/__init__.py
@@ -13,7 +13,8 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
- SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY)
+ SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
+ SERVICE_ALARM_ARM_NIGHT)
from homeassistant.config import load_yaml_config_file
from homeassistant.loader import bind_hass
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
@@ -31,6 +32,7 @@ SERVICE_TO_METHOD = {
SERVICE_ALARM_DISARM: 'alarm_disarm',
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
+ SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night',
SERVICE_ALARM_TRIGGER: 'alarm_trigger'
}
@@ -81,6 +83,18 @@ def alarm_arm_away(hass, code=None, entity_id=None):
hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)
+@bind_hass
+def alarm_arm_night(hass, code=None, entity_id=None):
+ """Send the alarm the command for arm night."""
+ data = {}
+ if code:
+ data[ATTR_CODE] = code
+ if entity_id:
+ data[ATTR_ENTITY_ID] = entity_id
+
+ hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data)
+
+
@bind_hass
def alarm_trigger(hass, code=None, entity_id=None):
"""Send the alarm the command for trigger."""
@@ -187,6 +201,17 @@ class AlarmControlPanel(Entity):
"""
return self.hass.async_add_job(self.alarm_arm_away, code)
+ def alarm_arm_night(self, code=None):
+ """Send arm night command."""
+ raise NotImplementedError()
+
+ def async_alarm_arm_night(self, code=None):
+ """Send arm night command.
+
+ This method must be run in the event loop and returns a coroutine.
+ """
+ return self.hass.async_add_job(self.alarm_arm_night, code)
+
def alarm_trigger(self, code=None):
"""Send alarm trigger command."""
raise NotImplementedError()
diff --git a/homeassistant/components/alarm_control_panel/abode.py b/homeassistant/components/alarm_control_panel/abode.py
new file mode 100644
index 00000000000..7d7ce931c20
--- /dev/null
+++ b/homeassistant/components/alarm_control_panel/abode.py
@@ -0,0 +1,82 @@
+"""
+This component provides HA alarm_control_panel support for Abode System.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/alarm_control_panel.abode/
+"""
+import logging
+
+from homeassistant.components.abode import (DATA_ABODE, DEFAULT_NAME)
+from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
+import homeassistant.components.alarm_control_panel as alarm
+
+DEPENDENCIES = ['abode']
+
+_LOGGER = logging.getLogger(__name__)
+
+ICON = 'mdi:security'
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up a sensor for an Abode device."""
+ data = hass.data.get(DATA_ABODE)
+
+ add_devices([AbodeAlarm(hass, data, data.abode.get_alarm())])
+
+
+class AbodeAlarm(alarm.AlarmControlPanel):
+ """An alarm_control_panel implementation for Abode."""
+
+ def __init__(self, hass, data, device):
+ """Initialize the alarm control panel."""
+ super(AbodeAlarm, self).__init__()
+ self._device = device
+ self._name = "{0}".format(DEFAULT_NAME)
+
+ @property
+ def should_poll(self):
+ """Return the polling state."""
+ return True
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def icon(self):
+ """Return icon."""
+ return ICON
+
+ @property
+ def state(self):
+ """Return the state of the device."""
+ if self._device.mode == "standby":
+ state = STATE_ALARM_DISARMED
+ elif self._device.mode == "away":
+ state = STATE_ALARM_ARMED_AWAY
+ elif self._device.mode == "home":
+ state = STATE_ALARM_ARMED_HOME
+ else:
+ state = None
+ return state
+
+ def alarm_disarm(self, code=None):
+ """Send disarm command."""
+ self._device.set_standby()
+ self.schedule_update_ha_state()
+
+ def alarm_arm_home(self, code=None):
+ """Send arm home command."""
+ self._device.set_home()
+ self.schedule_update_ha_state()
+
+ def alarm_arm_away(self, code=None):
+ """Send arm away command."""
+ self._device.set_away()
+ self.schedule_update_ha_state()
+
+ def update(self):
+ """Update the device state."""
+ self._device.refresh()
diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py
index 8ea472a7b19..fe7db95651b 100644
--- a/homeassistant/components/alarm_control_panel/egardia.py
+++ b/homeassistant/components/alarm_control_panel/egardia.py
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
-REQUIREMENTS = ['pythonegardia==1.0.17']
+REQUIREMENTS = ['pythonegardia==1.0.18']
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py
index c87aea862d5..97820ab4b2b 100644
--- a/homeassistant/components/alarm_control_panel/manual.py
+++ b/homeassistant/components/alarm_control_panel/manual.py
@@ -12,9 +12,10 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
import homeassistant.util.dt as dt_util
from homeassistant.const import (
- STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
- STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME,
- CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
+ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
+ STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
+ CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME,
+ CONF_DISARM_AFTER_TRIGGER)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_point_in_time
@@ -87,7 +88,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
def state(self):
"""Return the state of the device."""
if self._state in (STATE_ALARM_ARMED_HOME,
- STATE_ALARM_ARMED_AWAY) and \
+ STATE_ALARM_ARMED_AWAY,
+ STATE_ALARM_ARMED_NIGHT) and \
self._pending_time and self._state_ts + self._pending_time > \
dt_util.utcnow():
return STATE_ALARM_PENDING
@@ -145,6 +147,20 @@ class ManualAlarm(alarm.AlarmControlPanel):
self._hass, self.async_update_ha_state,
self._state_ts + self._pending_time)
+ def alarm_arm_night(self, code=None):
+ """Send arm night command."""
+ if not self._validate_code(code, STATE_ALARM_ARMED_NIGHT):
+ return
+
+ self._state = STATE_ALARM_ARMED_NIGHT
+ self._state_ts = dt_util.utcnow()
+ self.schedule_update_ha_state()
+
+ if self._pending_time:
+ track_point_in_time(
+ self._hass, self.async_update_ha_state,
+ self._state_ts + self._pending_time)
+
def alarm_trigger(self, code=None):
"""Send alarm trigger command. No code needed."""
self._pre_trigger_state = self._state
diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml
index 6cc3946ca66..19c3ca0233d 100644
--- a/homeassistant/components/alarm_control_panel/services.yaml
+++ b/homeassistant/components/alarm_control_panel/services.yaml
@@ -31,6 +31,17 @@ alarm_arm_away:
description: An optional code to arm away the alarm control panel with
example: 1234
+alarm_arm_night:
+ description: Send the alarm the command for arm night
+
+ fields:
+ entity_id:
+ description: Name of alarm control panel to arm night
+ example: 'alarm_control_panel.downstairs'
+ code:
+ description: An optional code to arm night the alarm control panel with
+ example: 1234
+
alarm_trigger:
description: Send the alarm the command for trigger
diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py
index 5eb2e9fe7d3..7f4e4dfa756 100644
--- a/homeassistant/components/alarm_control_panel/simplisafe.py
+++ b/homeassistant/components/alarm_control_panel/simplisafe.py
@@ -16,7 +16,7 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
-REQUIREMENTS = ['simplisafe-python==1.0.4']
+REQUIREMENTS = ['simplisafe-python==1.0.5']
_LOGGER = logging.getLogger(__name__)
@@ -89,11 +89,11 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
def state(self):
"""Return the state of the device."""
status = self.simplisafe.state()
- if status == 'Off':
+ if status == 'off':
state = STATE_ALARM_DISARMED
- elif status == 'Home':
+ elif status == 'home':
state = STATE_ALARM_ARMED_HOME
- elif status == 'Away':
+ elif status == 'away':
state = STATE_ALARM_ARMED_AWAY
else:
state = STATE_UNKNOWN
diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py
index 9c0b5108fee..05dc8aeef20 100644
--- a/homeassistant/components/alarm_control_panel/totalconnect.py
+++ b/homeassistant/components/alarm_control_panel/totalconnect.py
@@ -13,8 +13,8 @@ import homeassistant.components.alarm_control_panel as alarm
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_DISARMED, STATE_UNKNOWN,
- CONF_NAME)
+ STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
+ STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME)
REQUIREMENTS = ['total_connect_client==0.11']
@@ -74,6 +74,12 @@ class TotalConnect(alarm.AlarmControlPanel):
state = STATE_ALARM_ARMED_HOME
elif status == self._client.ARMED_AWAY:
state = STATE_ALARM_ARMED_AWAY
+ elif status == self._client.ARMED_STAY_NIGHT:
+ state = STATE_ALARM_ARMED_NIGHT
+ elif status == self._client.ARMING:
+ state = STATE_ALARM_ARMING
+ elif status == self._client.DISARMING:
+ state = STATE_ALARM_DISARMING
else:
state = STATE_UNKNOWN
@@ -90,3 +96,7 @@ class TotalConnect(alarm.AlarmControlPanel):
def alarm_arm_away(self, code=None):
"""Send arm away command."""
self._client.arm_away()
+
+ def alarm_arm_night(self, code=None):
+ """Send arm night command."""
+ self._client.arm_stay_night()
diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py
index c5f40ca5db8..7a2ff7610f7 100644
--- a/homeassistant/components/apple_tv.py
+++ b/homeassistant/components/apple_tv.py
@@ -91,7 +91,7 @@ def request_configuration(hass, config, atv, credentials):
hass.async_add_job(configurator.request_done, instance)
instance = configurator.request_config(
- hass, 'Apple TV Authentication', configuration_callback,
+ 'Apple TV Authentication', configuration_callback,
description='Please enter PIN code shown on screen.',
submit_caption='Confirm',
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
diff --git a/homeassistant/components/axis.py b/homeassistant/components/axis.py
index d83e07989e6..eaf85937658 100644
--- a/homeassistant/components/axis.py
+++ b/homeassistant/components/axis.py
@@ -110,7 +110,7 @@ def request_configuration(hass, name, host, serialnumber):
title = '{} ({})'.format(name, host)
request_id = configurator.request_config(
- hass, title, configuration_callback,
+ title, configuration_callback,
description='Functionality: ' + str(AXIS_INCLUDE),
entity_picture="/static/images/logo_axis.png",
link_name='Axis platform documentation',
diff --git a/homeassistant/components/binary_sensor/abode.py b/homeassistant/components/binary_sensor/abode.py
new file mode 100644
index 00000000000..9abff53026d
--- /dev/null
+++ b/homeassistant/components/binary_sensor/abode.py
@@ -0,0 +1,81 @@
+"""
+This component provides HA binary_sensor support for Abode Security System.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/binary_sensor.abode/
+"""
+import logging
+
+from homeassistant.components.abode import (CONF_ATTRIBUTION, DATA_ABODE)
+from homeassistant.const import (ATTR_ATTRIBUTION)
+from homeassistant.components.binary_sensor import (BinarySensorDevice)
+
+DEPENDENCIES = ['abode']
+
+_LOGGER = logging.getLogger(__name__)
+
+# Sensor types: Name, device_class
+SENSOR_TYPES = {
+ 'Door Contact': 'opening',
+ 'Motion Camera': 'motion',
+}
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up a sensor for an Abode device."""
+ data = hass.data.get(DATA_ABODE)
+
+ sensors = []
+ for sensor in data.devices:
+ _LOGGER.debug('Sensor type %s', sensor.type)
+ if sensor.type in ['Door Contact', 'Motion Camera']:
+ sensors.append(AbodeBinarySensor(hass, data, sensor))
+
+ _LOGGER.debug('Adding %d sensors', len(sensors))
+ add_devices(sensors)
+
+
+class AbodeBinarySensor(BinarySensorDevice):
+ """A binary sensor implementation for Abode device."""
+
+ def __init__(self, hass, data, device):
+ """Initialize a sensor for Abode device."""
+ super(AbodeBinarySensor, self).__init__()
+ self._device = device
+
+ @property
+ def should_poll(self):
+ """Return the polling state."""
+ return True
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return "{0} {1}".format(self._device.type, self._device.name)
+
+ @property
+ def is_on(self):
+ """Return True if the binary sensor is on."""
+ if self._device.type == 'Door Contact':
+ return self._device.status != 'Closed'
+ elif self._device.type == 'Motion Camera':
+ return self._device.get_value('motion_event') == '1'
+
+ @property
+ def device_class(self):
+ """Return the class of the binary sensor."""
+ return SENSOR_TYPES.get(self._device.type)
+
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes."""
+ attrs = {}
+ attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
+ attrs['device_id'] = self._device.device_id
+ attrs['battery_low'] = self._device.battery_low
+
+ return attrs
+
+ def update(self):
+ """Update the device state."""
+ self._device.refresh()
diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py
index 767ed858ec7..4b83f0c8f2d 100644
--- a/homeassistant/components/binary_sensor/mysensors.py
+++ b/homeassistant/components/binary_sensor/mysensors.py
@@ -4,62 +4,27 @@ Support for MySensors binary sensors.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.mysensors/
"""
-import logging
-
from homeassistant.components import mysensors
-from homeassistant.components.binary_sensor import (DEVICE_CLASSES,
+from homeassistant.components.binary_sensor import (DEVICE_CLASSES, DOMAIN,
BinarySensorDevice)
from homeassistant.const import STATE_ON
-_LOGGER = logging.getLogger(__name__)
-DEPENDENCIES = []
-
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the MySensors platform for sensors."""
- # Only act if loaded via mysensors by discovery event.
- # Otherwise gateway is not setup.
- if discovery_info is None:
- return
-
- gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
- if not gateways:
- return
-
- for gateway in gateways:
- # Define the S_TYPES and V_TYPES that the platform should handle as
- # states. Map them in a dict of lists.
- pres = gateway.const.Presentation
- set_req = gateway.const.SetReq
- map_sv_types = {
- pres.S_DOOR: [set_req.V_TRIPPED],
- pres.S_MOTION: [set_req.V_TRIPPED],
- pres.S_SMOKE: [set_req.V_TRIPPED],
- }
- if float(gateway.protocol_version) >= 1.5:
- map_sv_types.update({
- pres.S_SPRINKLER: [set_req.V_TRIPPED],
- pres.S_WATER_LEAK: [set_req.V_TRIPPED],
- pres.S_SOUND: [set_req.V_TRIPPED],
- pres.S_VIBRATION: [set_req.V_TRIPPED],
- pres.S_MOISTURE: [set_req.V_TRIPPED],
- })
-
- devices = {}
- gateway.platform_callbacks.append(mysensors.pf_callback_factory(
- map_sv_types, devices, MySensorsBinarySensor, add_devices))
+ """Setup the mysensors platform for binary sensors."""
+ mysensors.setup_mysensors_platform(
+ hass, DOMAIN, discovery_info, MySensorsBinarySensor,
+ add_devices=add_devices)
class MySensorsBinarySensor(
- mysensors.MySensorsDeviceEntity, BinarySensorDevice):
+ mysensors.MySensorsEntity, BinarySensorDevice):
"""Represent the value of a MySensors Binary Sensor child node."""
@property
def is_on(self):
"""Return True if the binary sensor is on."""
- if self.value_type in self._values:
- return self._values[self.value_type] == STATE_ON
- return False
+ return self._values.get(self.value_type) == STATE_ON
@property
def device_class(self):
diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py
index 81cc8fd8798..f48525d41a8 100644
--- a/homeassistant/components/binary_sensor/workday.py
+++ b/homeassistant/components/binary_sensor/workday.py
@@ -6,13 +6,12 @@ https://home-assistant.io/components/binary_sensor.workday/
"""
import asyncio
import logging
-import datetime
+from datetime import datetime, timedelta
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, WEEKDAYS
-import homeassistant.util.dt as dt_util
from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv
@@ -39,11 +38,14 @@ CONF_EXCLUDES = 'excludes'
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
DEFAULT_NAME = 'Workday Sensor'
ALLOWED_DAYS = WEEKDAYS + ['holiday']
+CONF_OFFSET = 'days_offset'
+DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES),
vol.Optional(CONF_PROVINCE, default=None): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int),
vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS):
vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]),
vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES):
@@ -60,8 +62,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
province = config.get(CONF_PROVINCE)
workdays = config.get(CONF_WORKDAYS)
excludes = config.get(CONF_EXCLUDES)
+ days_offset = config.get(CONF_OFFSET)
- year = datetime.datetime.now().year
+ year = (datetime.now() + timedelta(days=days_offset)).year
obj_holidays = getattr(holidays, country)(years=year)
if province:
@@ -85,7 +88,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
_LOGGER.debug("%s %s", date, name)
add_devices([IsWorkdaySensor(
- obj_holidays, workdays, excludes, sensor_name)], True)
+ obj_holidays, workdays, excludes, days_offset, sensor_name)], True)
def day_to_string(day):
@@ -99,12 +102,13 @@ def day_to_string(day):
class IsWorkdaySensor(BinarySensorDevice):
"""Implementation of a Workday sensor."""
- def __init__(self, obj_holidays, workdays, excludes, name):
+ def __init__(self, obj_holidays, workdays, excludes, days_offset, name):
"""Initialize the Workday sensor."""
self._name = name
self._obj_holidays = obj_holidays
self._workdays = workdays
self._excludes = excludes
+ self._days_offset = days_offset
self._state = None
@property
@@ -135,6 +139,16 @@ class IsWorkdaySensor(BinarySensorDevice):
return False
+ @property
+ def state_attributes(self):
+ """Return the attributes of the entity."""
+ # return self._attributes
+ return {
+ CONF_WORKDAYS: self._workdays,
+ CONF_EXCLUDES: self._excludes,
+ CONF_OFFSET: self._days_offset
+ }
+
@asyncio.coroutine
def async_update(self):
"""Get date and look whether it is a holiday."""
@@ -142,11 +156,12 @@ class IsWorkdaySensor(BinarySensorDevice):
self._state = False
# Get iso day of the week (1 = Monday, 7 = Sunday)
- day = datetime.datetime.today().isoweekday() - 1
+ date = datetime.today() + timedelta(days=self._days_offset)
+ day = date.isoweekday() - 1
day_of_week = day_to_string(day)
- if self.is_include(day_of_week, dt_util.now()):
+ if self.is_include(day_of_week, date):
self._state = True
- if self.is_exclude(day_of_week, dt_util.now()):
+ if self.is_exclude(day_of_week, date):
self._state = False
diff --git a/homeassistant/components/camera/usps.py b/homeassistant/components/camera/usps.py
new file mode 100644
index 00000000000..545ea9798de
--- /dev/null
+++ b/homeassistant/components/camera/usps.py
@@ -0,0 +1,94 @@
+"""
+Support for a camera made up of usps mail images.
+
+For more details about this component, please refer to the documentation at
+https://home-assistant.io/components/camera.usps/
+"""
+from datetime import timedelta
+import logging
+
+from homeassistant.components.camera import Camera
+from homeassistant.components.usps import DATA_USPS
+
+_LOGGER = logging.getLogger(__name__)
+
+DEPENDENCIES = ['usps']
+
+SCAN_INTERVAL = timedelta(seconds=10)
+
+
+def setup_platform(hass, config, add_devices, discovery_info=None):
+ """Set up USPS mail camera."""
+ if discovery_info is None:
+ return
+
+ usps = hass.data[DATA_USPS]
+ add_devices([USPSCamera(usps)])
+
+
+class USPSCamera(Camera):
+ """Representation of the images available from USPS."""
+
+ def __init__(self, usps):
+ """Initialize the USPS camera images."""
+ super().__init__()
+
+ self._usps = usps
+ self._name = self._usps.name
+ self._session = self._usps.session
+
+ self._mail_img = []
+ self._last_mail = None
+ self._mail_index = 0
+ self._mail_count = 0
+
+ self._timer = None
+
+ def camera_image(self):
+ """Update the camera's image if it has changed."""
+ self._usps.update()
+ try:
+ self._mail_count = len(self._usps.mail)
+ except TypeError:
+ # No mail
+ return None
+
+ if self._usps.mail != self._last_mail:
+ # Mail items must have changed
+ self._mail_img = []
+ if len(self._usps.mail) >= 1:
+ self._last_mail = self._usps.mail
+ for article in self._usps.mail:
+ _LOGGER.debug("Fetching article image: %s", article)
+ img = self._session.get(article['image']).content
+ self._mail_img.append(img)
+
+ try:
+ return self._mail_img[self._mail_index]
+ except IndexError:
+ return None
+
+ @property
+ def name(self):
+ """Return the name of this camera."""
+ return '{} mail'.format(self._name)
+
+ @property
+ def model(self):
+ """Return date of mail as model."""
+ try:
+ return 'Date: {}'.format(self._usps.mail[0]['date'])
+ except IndexError:
+ return None
+
+ @property
+ def should_poll(self):
+ """Update the mail image index periodically."""
+ return True
+
+ def update(self):
+ """Update mail image index."""
+ if self._mail_index < (self._mail_count - 1):
+ self._mail_index += 1
+ else:
+ self._mail_index = 0
diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py
index 82ed8a94e2b..d4316c2cfba 100755
--- a/homeassistant/components/climate/mysensors.py
+++ b/homeassistant/components/climate/mysensors.py
@@ -4,15 +4,11 @@ MySensors platform that offers a Climate (MySensors-HVAC) component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/climate.mysensors/
"""
-import logging
-
from homeassistant.components import mysensors
from homeassistant.components.climate import (
- STATE_COOL, STATE_HEAT, STATE_OFF, STATE_AUTO, ClimateDevice,
- ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW)
-from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE
-
-_LOGGER = logging.getLogger(__name__)
+ ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN, STATE_AUTO,
+ STATE_COOL, STATE_HEAT, STATE_OFF, ClimateDevice)
+from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
DICT_HA_TO_MYS = {
STATE_AUTO: 'AutoChangeOver',
@@ -29,28 +25,12 @@ DICT_MYS_TO_HA = {
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the mysensors climate."""
- if discovery_info is None:
- return
-
- gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
- if not gateways:
- return
-
- for gateway in gateways:
- if float(gateway.protocol_version) < 1.5:
- continue
- pres = gateway.const.Presentation
- set_req = gateway.const.SetReq
- map_sv_types = {
- pres.S_HVAC: [set_req.V_HVAC_FLOW_STATE],
- }
- devices = {}
- gateway.platform_callbacks.append(mysensors.pf_callback_factory(
- map_sv_types, devices, MySensorsHVAC, add_devices))
+ """Setup the mysensors climate."""
+ mysensors.setup_mysensors_platform(
+ hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices)
-class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
+class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice):
"""Representation of a MySensors HVAC."""
@property
@@ -84,26 +64,28 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
if temp is None:
temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
- return float(temp)
+ return float(temp) if temp is not None else None
@property
def target_temperature_high(self):
"""Return the highbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_HEAT in self._values:
- return float(self._values.get(set_req.V_HVAC_SETPOINT_COOL))
+ temp = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
+ return float(temp) if temp is not None else None
@property
def target_temperature_low(self):
"""Return the lowbound target temperature we try to reach."""
set_req = self.gateway.const.SetReq
if set_req.V_HVAC_SETPOINT_COOL in self._values:
- return float(self._values.get(set_req.V_HVAC_SETPOINT_HEAT))
+ temp = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
+ return float(temp) if temp is not None else None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
- return self._values.get(self.gateway.const.SetReq.V_HVAC_FLOW_STATE)
+ return self._values.get(self.value_type)
@property
def operation_list(self):
@@ -128,7 +110,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
high = kwargs.get(ATTR_TARGET_TEMP_HIGH)
heat = self._values.get(set_req.V_HVAC_SETPOINT_HEAT)
cool = self._values.get(set_req.V_HVAC_SETPOINT_COOL)
- updates = ()
+ updates = []
if temp is not None:
if heat is not None:
# Set HEAT Target temperature
@@ -146,7 +128,7 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
self.gateway.set_child_value(
self.node_id, self.child_id, value_type, value)
if self.gateway.optimistic:
- # optimistically assume that switch has changed state
+ # optimistically assume that device has changed state
self._values[value_type] = value
self.schedule_update_ha_state()
@@ -156,54 +138,22 @@ class MySensorsHVAC(mysensors.MySensorsDeviceEntity, ClimateDevice):
self.gateway.set_child_value(
self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan)
if self.gateway.optimistic:
- # optimistically assume that switch has changed state
+ # optimistically assume that device has changed state
self._values[set_req.V_HVAC_SPEED] = fan
self.schedule_update_ha_state()
def set_operation_mode(self, operation_mode):
"""Set new target temperature."""
- set_req = self.gateway.const.SetReq
self.gateway.set_child_value(
- self.node_id, self.child_id, set_req.V_HVAC_FLOW_STATE,
+ self.node_id, self.child_id, self.value_type,
DICT_HA_TO_MYS[operation_mode])
if self.gateway.optimistic:
- # optimistically assume that switch has changed state
- self._values[set_req.V_HVAC_FLOW_STATE] = operation_mode
+ # optimistically assume that device has changed state
+ self._values[self.value_type] = operation_mode
self.schedule_update_ha_state()
def update(self):
"""Update the controller with the latest value from a sensor."""
- set_req = self.gateway.const.SetReq
- node = self.gateway.sensors[self.node_id]
- child = node.children[self.child_id]
- for value_type, value in child.values.items():
- _LOGGER.debug(
- "%s: value_type %s, value = %s", self._name, value_type, value)
- if value_type == set_req.V_HVAC_FLOW_STATE:
- self._values[value_type] = DICT_MYS_TO_HA[value]
- else:
- self._values[value_type] = value
-
- def set_humidity(self, humidity):
- """Set new target humidity."""
- _LOGGER.error("Service Not Implemented yet")
-
- def set_swing_mode(self, swing_mode):
- """Set new target swing operation."""
- _LOGGER.error("Service Not Implemented yet")
-
- def turn_away_mode_on(self):
- """Turn away mode on."""
- _LOGGER.error("Service Not Implemented yet")
-
- def turn_away_mode_off(self):
- """Turn away mode off."""
- _LOGGER.error("Service Not Implemented yet")
-
- def turn_aux_heat_on(self):
- """Turn auxillary heater on."""
- _LOGGER.error("Service Not Implemented yet")
-
- def turn_aux_heat_off(self):
- """Turn auxillary heater off."""
- _LOGGER.error("Service Not Implemented yet")
+ super().update()
+ self._values[self.value_type] = DICT_MYS_TO_HA[
+ self._values[self.value_type]]
diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py
index 0bc44501e28..9e447c8936a 100644
--- a/homeassistant/components/config/__init__.py
+++ b/homeassistant/components/config/__init__.py
@@ -14,7 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
-SECTIONS = ('core', 'group', 'hassbian', 'automation')
+SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script')
ON_DEMAND = ('zwave')
diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py
new file mode 100644
index 00000000000..345c8e4a849
--- /dev/null
+++ b/homeassistant/components/config/script.py
@@ -0,0 +1,19 @@
+"""Provide configuration end points for scripts."""
+import asyncio
+
+from homeassistant.components.config import EditKeyBasedConfigView
+from homeassistant.components.script import SCRIPT_ENTRY_SCHEMA, async_reload
+import homeassistant.helpers.config_validation as cv
+
+
+CONFIG_PATH = 'scripts.yaml'
+
+
+@asyncio.coroutine
+def async_setup(hass):
+ """Set up the script config API."""
+ hass.http.register_view(EditKeyBasedConfigView(
+ 'script', 'config', CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA,
+ post_write_hook=async_reload
+ ))
+ return True
diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py
index 660a62a5b89..2da8967bddf 100644
--- a/homeassistant/components/configurator.py
+++ b/homeassistant/components/configurator.py
@@ -7,19 +7,21 @@ A callback has to be provided to `request_config` which will be called when
the user has submitted configuration information.
"""
import asyncio
+import functools as ft
import logging
from homeassistant.core import callback as async_callback
from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME, \
ATTR_ENTITY_PICTURE
from homeassistant.loader import bind_hass
-from homeassistant.helpers.entity import generate_entity_id
+from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util.async import run_callback_threadsafe
_LOGGER = logging.getLogger(__name__)
-_REQUESTS = {}
_KEY_INSTANCE = 'configurator'
+DATA_REQUESTS = 'configurator_requests'
+
ATTR_CONFIGURE_ID = 'configure_id'
ATTR_DESCRIPTION = 'description'
ATTR_DESCRIPTION_IMAGE = 'description_image'
@@ -39,63 +41,89 @@ STATE_CONFIGURED = 'configured'
@bind_hass
-def request_config(
- hass, name, callback, description=None, description_image=None,
+@async_callback
+def async_request_config(
+ hass, name, callback=None, description=None, description_image=None,
submit_caption=None, fields=None, link_name=None, link_url=None,
entity_picture=None):
"""Create a new request for configuration.
Will return an ID to be used for sequent calls.
"""
- instance = run_callback_threadsafe(hass.loop,
- _async_get_instance,
- hass).result()
+ instance = hass.data.get(_KEY_INSTANCE)
- request_id = instance.request_config(
+ if instance is None:
+ instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
+
+ request_id = instance.async_request_config(
name, callback,
description, description_image, submit_caption,
fields, link_name, link_url, entity_picture)
- _REQUESTS[request_id] = instance
+ if DATA_REQUESTS not in hass.data:
+ hass.data[DATA_REQUESTS] = {}
+
+ hass.data[DATA_REQUESTS][request_id] = instance
return request_id
-def notify_errors(request_id, error):
+@bind_hass
+def request_config(hass, *args, **kwargs):
+ """Create a new request for configuration.
+
+ Will return an ID to be used for sequent calls.
+ """
+ return run_callback_threadsafe(
+ hass.loop, ft.partial(async_request_config, hass, *args, **kwargs)
+ ).result()
+
+
+@bind_hass
+@async_callback
+def async_notify_errors(hass, request_id, error):
"""Add errors to a config request."""
try:
- _REQUESTS[request_id].notify_errors(request_id, error)
+ hass.data[DATA_REQUESTS][request_id].async_notify_errors(
+ request_id, error)
except KeyError:
# If request_id does not exist
pass
-def request_done(request_id):
+@bind_hass
+def notify_errors(hass, request_id, error):
+ """Add errors to a config request."""
+ return run_callback_threadsafe(
+ hass.loop, async_notify_errors, hass, request_id, error
+ ).result()
+
+
+@bind_hass
+@async_callback
+def async_request_done(hass, request_id):
"""Mark a configuration request as done."""
try:
- _REQUESTS.pop(request_id).request_done(request_id)
+ hass.data[DATA_REQUESTS].pop(request_id).async_request_done(request_id)
except KeyError:
# If request_id does not exist
pass
+@bind_hass
+def request_done(hass, request_id):
+ """Mark a configuration request as done."""
+ return run_callback_threadsafe(
+ hass.loop, async_request_done, hass, request_id
+ ).result()
+
+
@asyncio.coroutine
def async_setup(hass, config):
"""Set up the configurator component."""
return True
-@async_callback
-def _async_get_instance(hass):
- """Get an instance per hass object."""
- instance = hass.data.get(_KEY_INSTANCE)
-
- if instance is None:
- instance = hass.data[_KEY_INSTANCE] = Configurator(hass)
-
- return instance
-
-
class Configurator(object):
"""The class to keep track of current configuration requests."""
@@ -105,14 +133,16 @@ class Configurator(object):
self._cur_id = 0
self._requests = {}
hass.services.async_register(
- DOMAIN, SERVICE_CONFIGURE, self.handle_service_call)
+ DOMAIN, SERVICE_CONFIGURE, self.async_handle_service_call)
- def request_config(
+ @async_callback
+ def async_request_config(
self, name, callback,
description, description_image, submit_caption,
fields, link_name, link_url, entity_picture):
"""Set up a request for configuration."""
- entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, hass=self.hass)
+ entity_id = async_generate_entity_id(
+ ENTITY_ID_FORMAT, name, hass=self.hass)
if fields is None:
fields = []
@@ -138,11 +168,12 @@ class Configurator(object):
] if value is not None
})
- self.hass.states.set(entity_id, STATE_CONFIGURE, data)
+ self.hass.states.async_set(entity_id, STATE_CONFIGURE, data)
return request_id
- def notify_errors(self, request_id, error):
+ @async_callback
+ def async_notify_errors(self, request_id, error):
"""Update the state with errors."""
if not self._validate_request_id(request_id):
return
@@ -154,9 +185,10 @@ class Configurator(object):
new_data = dict(state.attributes)
new_data[ATTR_ERRORS] = error
- self.hass.states.set(entity_id, STATE_CONFIGURE, new_data)
+ self.hass.states.async_set(entity_id, STATE_CONFIGURE, new_data)
- def request_done(self, request_id):
+ @async_callback
+ def async_request_done(self, request_id):
"""Remove the configuration request."""
if not self._validate_request_id(request_id):
return
@@ -167,15 +199,16 @@ class Configurator(object):
# the result fo the service call (current design limitation).
# Instead, we will set it to configured to give as feedback but delete
# it shortly after so that it is deleted when the client updates.
- self.hass.states.set(entity_id, STATE_CONFIGURED)
+ self.hass.states.async_set(entity_id, STATE_CONFIGURED)
def deferred_remove(event):
"""Remove the request state."""
- self.hass.states.remove(entity_id)
+ self.hass.states.async_remove(entity_id)
- self.hass.bus.listen_once(EVENT_TIME_CHANGED, deferred_remove)
+ self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove)
- def handle_service_call(self, call):
+ @async_callback
+ def async_handle_service_call(self, call):
"""Handle a configure service call."""
request_id = call.data.get(ATTR_CONFIGURE_ID)
@@ -186,8 +219,8 @@ class Configurator(object):
entity_id, fields, callback = self._requests[request_id]
# field validation goes here?
-
- self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
+ if callback:
+ self.hass.async_add_job(callback, call.data.get(ATTR_FIELDS, {}))
def _generate_unique_id(self):
"""Generate a unique configurator ID."""
diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py
index f48a2110eca..cd4ff62b3e9 100644
--- a/homeassistant/components/cover/mysensors.py
+++ b/homeassistant/components/cover/mysensors.py
@@ -4,42 +4,18 @@ Support for MySensors covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.mysensors/
"""
-import logging
-
from homeassistant.components import mysensors
-from homeassistant.components.cover import CoverDevice, ATTR_POSITION
+from homeassistant.components.cover import CoverDevice, ATTR_POSITION, DOMAIN
from homeassistant.const import STATE_ON, STATE_OFF
-_LOGGER = logging.getLogger(__name__)
-
-DEPENDENCIES = []
-
def setup_platform(hass, config, add_devices, discovery_info=None):
- """Set up the MySensors platform for covers."""
- if discovery_info is None:
- return
-
- gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
- if not gateways:
- return
-
- for gateway in gateways:
- pres = gateway.const.Presentation
- set_req = gateway.const.SetReq
- map_sv_types = {
- pres.S_COVER: [set_req.V_DIMMER, set_req.V_LIGHT],
- }
- if float(gateway.protocol_version) >= 1.5:
- map_sv_types.update({
- pres.S_COVER: [set_req.V_PERCENTAGE, set_req.V_STATUS],
- })
- devices = {}
- gateway.platform_callbacks.append(mysensors.pf_callback_factory(
- map_sv_types, devices, MySensorsCover, add_devices))
+ """Setup the mysensors platform for covers."""
+ mysensors.setup_mysensors_platform(
+ hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices)
-class MySensorsCover(mysensors.MySensorsDeviceEntity, CoverDevice):
+class MySensorsCover(mysensors.MySensorsEntity, CoverDevice):
"""Representation of the value of a MySensors Cover child node."""
@property
diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py
index 769c2fc4ed6..f9e059d3927 100644
--- a/homeassistant/components/cover/template.py
+++ b/homeassistant/components/cover/template.py
@@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_FRIENDLY_NAME, CONF_ENTITY_ID,
EVENT_HOMEASSISTANT_START, MATCH_ALL,
CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE,
- STATE_OPEN, STATE_CLOSED)
+ CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED)
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
@@ -39,6 +39,8 @@ CLOSE_ACTION = 'close_cover'
STOP_ACTION = 'stop_cover'
POSITION_ACTION = 'set_cover_position'
TILT_ACTION = 'set_cover_tilt_position'
+CONF_TILT_OPTIMISTIC = 'tilt_optimistic'
+
CONF_VALUE_OR_POSITION_TEMPLATE = 'value_or_position'
CONF_OPEN_OR_CLOSE = 'open_or_close'
@@ -56,6 +58,8 @@ COVER_SCHEMA = vol.Schema({
vol.Optional(CONF_POSITION_TEMPLATE): cv.template,
vol.Optional(CONF_TILT_TEMPLATE): cv.template,
vol.Optional(CONF_ICON_TEMPLATE): cv.template,
+ vol.Optional(CONF_OPTIMISTIC): cv.boolean,
+ vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean,
vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(TILT_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_FRIENDLY_NAME, default=None): cv.string,
@@ -83,11 +87,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
stop_action = device_config.get(STOP_ACTION)
position_action = device_config.get(POSITION_ACTION)
tilt_action = device_config.get(TILT_ACTION)
-
- if position_template is None and state_template is None:
- _LOGGER.error('Must specify either %s' or '%s',
- CONF_VALUE_TEMPLATE, CONF_VALUE_TEMPLATE)
- continue
+ optimistic = device_config.get(CONF_OPTIMISTIC)
+ tilt_optimistic = device_config.get(CONF_TILT_OPTIMISTIC)
if position_action is None and open_action is None:
_LOGGER.error('Must specify at least one of %s' or '%s',
@@ -125,7 +126,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
device, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
- position_action, tilt_action, entity_ids
+ position_action, tilt_action,
+ optimistic, tilt_optimistic, entity_ids
)
)
if not covers:
@@ -142,7 +144,8 @@ class CoverTemplate(CoverDevice):
def __init__(self, hass, device_id, friendly_name, state_template,
position_template, tilt_template, icon_template,
open_action, close_action, stop_action,
- position_action, tilt_action, entity_ids):
+ position_action, tilt_action,
+ optimistic, tilt_optimistic, entity_ids):
"""Initialize the Template cover."""
self.hass = hass
self.entity_id = async_generate_entity_id(
@@ -167,6 +170,9 @@ class CoverTemplate(CoverDevice):
self._tilt_script = None
if tilt_action is not None:
self._tilt_script = Script(hass, tilt_action)
+ self._optimistic = (optimistic or
+ (not state_template and not position_template))
+ self._tilt_optimistic = tilt_optimistic or not tilt_template
self._icon = None
self._position = None
self._tilt_value = None
@@ -260,19 +266,23 @@ class CoverTemplate(CoverDevice):
def async_open_cover(self, **kwargs):
"""Move the cover up."""
if self._open_script:
- self.hass.async_add_job(self._open_script.async_run())
+ yield from self._open_script.async_run()
elif self._position_script:
- self.hass.async_add_job(self._position_script.async_run(
- {"position": 100}))
+ yield from self._position_script.async_run({"position": 100})
+ if self._optimistic:
+ self._position = 100
+ self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_close_cover(self, **kwargs):
"""Move the cover down."""
if self._close_script:
- self.hass.async_add_job(self._close_script.async_run())
+ yield from self._close_script.async_run()
elif self._position_script:
- self.hass.async_add_job(self._position_script.async_run(
- {"position": 0}))
+ yield from self._position_script.async_run({"position": 0})
+ if self._optimistic:
+ self._position = 0
+ self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_stop_cover(self, **kwargs):
@@ -284,29 +294,35 @@ class CoverTemplate(CoverDevice):
def async_set_cover_position(self, **kwargs):
"""Set cover position."""
self._position = kwargs[ATTR_POSITION]
- self.hass.async_add_job(self._position_script.async_run(
- {"position": self._position}))
+ yield from self._position_script.async_run(
+ {"position": self._position})
+ if self._optimistic:
+ self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_open_cover_tilt(self, **kwargs):
"""Tilt the cover open."""
self._tilt_value = 100
- self.hass.async_add_job(self._tilt_script.async_run(
- {"tilt": self._tilt_value}))
+ yield from self._tilt_script.async_run({"tilt": self._tilt_value})
+ if self._tilt_optimistic:
+ self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_close_cover_tilt(self, **kwargs):
"""Tilt the cover closed."""
self._tilt_value = 0
- self.hass.async_add_job(self._tilt_script.async_run(
- {"tilt": self._tilt_value}))
+ yield from self._tilt_script.async_run(
+ {"tilt": self._tilt_value})
+ if self._tilt_optimistic:
+ self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
self._tilt_value = kwargs[ATTR_TILT_POSITION]
- self.hass.async_add_job(self._tilt_script.async_run(
- {"tilt": self._tilt_value}))
+ yield from self._tilt_script.async_run({"tilt": self._tilt_value})
+ if self._tilt_optimistic:
+ self.hass.async_add_job(self.async_update_ha_state())
@asyncio.coroutine
def async_update(self):
diff --git a/homeassistant/components/device_tracker/automatic.py b/homeassistant/components/device_tracker/automatic.py
index 891f1b22775..071edf42642 100644
--- a/homeassistant/components/device_tracker/automatic.py
+++ b/homeassistant/components/device_tracker/automatic.py
@@ -6,28 +6,32 @@ https://home-assistant.io/components/device_tracker.automatic/
"""
import asyncio
from datetime import timedelta
+import json
import logging
+import os
+from aiohttp import web
import voluptuous as vol
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, ATTR_ATTRIBUTES, ATTR_DEV_ID, ATTR_HOST_NAME, ATTR_MAC,
ATTR_GPS, ATTR_GPS_ACCURACY)
-from homeassistant.const import (
- CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP,
- EVENT_HOMEASSISTANT_START)
+from homeassistant.components.http import HomeAssistantView
+from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_time_interval
-REQUIREMENTS = ['aioautomatic==0.4.0']
+REQUIREMENTS = ['aioautomatic==0.6.0']
+DEPENDENCIES = ['http']
_LOGGER = logging.getLogger(__name__)
CONF_CLIENT_ID = 'client_id'
CONF_SECRET = 'secret'
CONF_DEVICES = 'devices'
+CONF_CURRENT_LOCATION = 'current_location'
DEFAULT_TIMEOUT = 5
@@ -38,38 +42,74 @@ ATTR_FUEL_LEVEL = 'fuel_level'
EVENT_AUTOMATIC_UPDATE = 'automatic_update'
+AUTOMATIC_CONFIG_FILE = '.automatic/session-{}.json'
+
+DATA_CONFIGURING = 'automatic_configurator_clients'
+DATA_REFRESH_TOKEN = 'refresh_token'
+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_SECRET): cv.string,
- vol.Required(CONF_USERNAME): cv.string,
- vol.Required(CONF_PASSWORD): cv.string,
+ vol.Optional(CONF_CURRENT_LOCATION, default=False): cv.boolean,
vol.Optional(CONF_DEVICES, default=None): vol.All(
cv.ensure_list, [cv.string])
})
+def _get_refresh_token_from_file(hass, filename):
+ """Attempt to load session data from file."""
+ path = hass.config.path(filename)
+
+ if not os.path.isfile(path):
+ return None
+
+ try:
+ with open(path) as data_file:
+ data = json.load(data_file)
+ if data is None:
+ return None
+
+ return data.get(DATA_REFRESH_TOKEN)
+ except ValueError:
+ return None
+
+
+def _write_refresh_token_to_file(hass, filename, refresh_token):
+ """Attempt to store session data to file."""
+ path = hass.config.path(filename)
+
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, 'w+') as data_file:
+ json.dump({
+ DATA_REFRESH_TOKEN: refresh_token
+ }, data_file)
+
+
@asyncio.coroutine
def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Validate the configuration and return an Automatic scanner."""
import aioautomatic
+ hass.http.register_view(AutomaticAuthCallbackView())
+
+ scope = FULL_SCOPE if config.get(CONF_CURRENT_LOCATION) else DEFAULT_SCOPE
+
client = aioautomatic.Client(
client_id=config[CONF_CLIENT_ID],
client_secret=config[CONF_SECRET],
client_session=async_get_clientsession(hass),
request_kwargs={'timeout': DEFAULT_TIMEOUT})
- try:
- try:
- session = yield from client.create_session_from_password(
- FULL_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
- except aioautomatic.exceptions.ForbiddenError as exc:
- if not str(exc).startswith("invalid_scope"):
- raise exc
- _LOGGER.info("Client not authorized for current_location scope. "
- "location:updated events will not be received.")
- session = yield from client.create_session_from_password(
- DEFAULT_SCOPE, config[CONF_USERNAME], config[CONF_PASSWORD])
+ filename = AUTOMATIC_CONFIG_FILE.format(config[CONF_CLIENT_ID])
+ refresh_token = yield from hass.async_add_job(
+ _get_refresh_token_from_file, hass, filename)
+
+ @asyncio.coroutine
+ def initialize_data(session):
+ """Initialize the AutomaticData object from the created session."""
+ hass.async_add_job(
+ _write_refresh_token_to_file, hass, filename,
+ session.refresh_token)
data = AutomaticData(
hass, client, session, config[CONF_DEVICES], async_see)
@@ -77,26 +117,86 @@ def async_setup_scanner(hass, config, async_see, discovery_info=None):
vehicles = yield from session.get_vehicles()
for vehicle in vehicles:
hass.async_add_job(data.load_vehicle(vehicle))
- except aioautomatic.exceptions.AutomaticError as err:
- _LOGGER.error(str(err))
- return False
- @callback
- def ws_connect(event):
- """Open the websocket connection."""
- hass.async_add_job(data.ws_connect())
+ # Create a task instead of adding a tracking job, since this task will
+ # run until the websocket connection is closed.
+ hass.loop.create_task(data.ws_connect())
- @callback
- def ws_close(event):
- """Close the websocket connection."""
- hass.async_add_job(data.ws_close())
+ if refresh_token is not None:
+ try:
+ session = yield from client.create_session_from_refresh_token(
+ refresh_token)
+ yield from initialize_data(session)
+ return True
+ except aioautomatic.exceptions.AutomaticError as err:
+ _LOGGER.error(str(err))
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, ws_connect)
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, ws_close)
+ configurator = hass.components.configurator
+ request_id = configurator.async_request_config(
+ "Automatic", description=(
+ "Authorization required for Automatic device tracker."),
+ link_name="Click here to authorize Home Assistant.",
+ link_url=client.generate_oauth_url(scope),
+ entity_picture="/static/images/logo_automatic.png",
+ )
+ @asyncio.coroutine
+ def initialize_callback(code, state):
+ """Callback after OAuth2 response is returned."""
+ try:
+ session = yield from client.create_session_from_oauth_code(
+ code, state)
+ yield from initialize_data(session)
+ configurator.async_request_done(request_id)
+ except aioautomatic.exceptions.AutomaticError as err:
+ _LOGGER.error(str(err))
+ configurator.async_notify_errors(request_id, str(err))
+ return False
+
+ if DATA_CONFIGURING not in hass.data:
+ hass.data[DATA_CONFIGURING] = {}
+
+ hass.data[DATA_CONFIGURING][client.state] = initialize_callback
return True
+class AutomaticAuthCallbackView(HomeAssistantView):
+ """Handle OAuth finish callback requests."""
+
+ requires_auth = False
+ url = '/api/automatic/callback'
+ name = 'api:automatic:callback'
+
+ @callback
+ def get(self, request): # pylint: disable=no-self-use
+ """Finish OAuth callback request."""
+ hass = request.app['hass']
+ params = request.query
+ response = web.HTTPFound('/states')
+
+ if 'state' not in params or 'code' not in params:
+ if 'error' in params:
+ _LOGGER.error(
+ "Error authorizing Automatic: %s", params['error'])
+ return response
+ else:
+ _LOGGER.error(
+ "Error authorizing Automatic. Invalid response returned.")
+ return response
+
+ if DATA_CONFIGURING not in hass.data or \
+ params['state'] not in hass.data[DATA_CONFIGURING]:
+ _LOGGER.error("Automatic configuration request not found.")
+ return response
+
+ code = params['code']
+ state = params['state']
+ initialize_callback = hass.data[DATA_CONFIGURING][state]
+ hass.async_add_job(initialize_callback(code, state))
+
+ return response
+
+
class AutomaticData(object):
"""A class representing an Automatic cloud service connection."""
@@ -115,6 +215,8 @@ class AutomaticData(object):
lambda name, event: self.hass.async_add_job(
self.handle_event(name, event)))
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.ws_close())
+
@asyncio.coroutine
def handle_event(self, name, event):
"""Coroutine to update state for a realtime event."""
diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py
index 194a2f4bfac..f20dad1fceb 100644
--- a/homeassistant/components/device_tracker/icloud.py
+++ b/homeassistant/components/device_tracker/icloud.py
@@ -19,7 +19,6 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify
import homeassistant.util.dt as dt_util
from homeassistant.util.location import distance
-from homeassistant.loader import get_component
_LOGGER = logging.getLogger(__name__)
@@ -209,7 +208,7 @@ class Icloud(DeviceScanner):
if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname)
- configurator = get_component('configurator')
+ configurator = self.hass.components.configurator
configurator.request_done(request_id)
# Trigger the next step immediately
@@ -217,7 +216,7 @@ class Icloud(DeviceScanner):
def icloud_need_trusted_device(self):
"""We need a trusted device."""
- configurator = get_component('configurator')
+ configurator = self.hass.components.configurator
if self.accountname in _CONFIGURING:
return
@@ -229,7 +228,7 @@ class Icloud(DeviceScanner):
devicesstring += "{}: {};".format(i, devicename)
_CONFIGURING[self.accountname] = configurator.request_config(
- self.hass, 'iCloud {}'.format(self.accountname),
+ 'iCloud {}'.format(self.accountname),
self.icloud_trusted_device_callback,
description=(
'Please choose your trusted device by entering'
@@ -259,17 +258,17 @@ class Icloud(DeviceScanner):
if self.accountname in _CONFIGURING:
request_id = _CONFIGURING.pop(self.accountname)
- configurator = get_component('configurator')
+ configurator = self.hass.components.configurator
configurator.request_done(request_id)
def icloud_need_verification_code(self):
"""Return the verification code."""
- configurator = get_component('configurator')
+ configurator = self.hass.components.configurator
if self.accountname in _CONFIGURING:
return
_CONFIGURING[self.accountname] = configurator.request_config(
- self.hass, 'iCloud {}'.format(self.accountname),
+ 'iCloud {}'.format(self.accountname),
self.icloud_verification_callback,
description=('Please enter the validation code:'),
entity_picture="/static/images/config_icloud.png",
diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py
index 4503c4d1b26..f68eb361ca0 100644
--- a/homeassistant/components/device_tracker/mysensors.py
+++ b/homeassistant/components/device_tracker/mysensors.py
@@ -4,61 +4,51 @@ Support for tracking MySensors devices.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.mysensors/
"""
-import logging
-
from homeassistant.components import mysensors
+from homeassistant.components.device_tracker import DOMAIN
+from homeassistant.helpers.dispatcher import dispatcher_connect
from homeassistant.util import slugify
-DEPENDENCIES = ['mysensors']
-
-_LOGGER = logging.getLogger(__name__)
-
def setup_scanner(hass, config, see, discovery_info=None):
- """Set up the MySensors tracker."""
- def mysensors_callback(gateway, msg):
- """Set up callback for mysensors platform."""
- node = gateway.sensors[msg.node_id]
- if node.sketch_name is None:
- _LOGGER.debug("No sketch_name: node %s", msg.node_id)
- return
+ """Set up the MySensors device scanner."""
+ new_devices = mysensors.setup_mysensors_platform(
+ hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
+ device_args=(see, ))
+ if not new_devices:
+ return False
- pres = gateway.const.Presentation
- set_req = gateway.const.SetReq
-
- child = node.children.get(msg.child_id)
- if child is None:
- return
- position = child.values.get(set_req.V_POSITION)
- if child.type != pres.S_GPS or position is None:
- return
- try:
- latitude, longitude, _ = position.split(',')
- except ValueError:
- _LOGGER.error("Payload for V_POSITION %s is not of format "
- "latitude, longitude, altitude", position)
- return
- name = '{} {} {}'.format(
- node.sketch_name, msg.node_id, child.id)
- attr = {
- mysensors.ATTR_CHILD_ID: child.id,
- mysensors.ATTR_DESCRIPTION: child.description,
- mysensors.ATTR_DEVICE: gateway.device,
- mysensors.ATTR_NODE_ID: msg.node_id,
- }
- see(
- dev_id=slugify(name),
- host_name=name,
- gps=(latitude, longitude),
- battery=node.battery_level,
- attributes=attr
- )
-
- gateways = hass.data.get(mysensors.MYSENSORS_GATEWAYS)
-
- for gateway in gateways:
- if float(gateway.protocol_version) < 2.0:
- continue
- gateway.platform_callbacks.append(mysensors_callback)
+ for device in new_devices:
+ dev_id = (
+ id(device.gateway), device.node_id, device.child_id,
+ device.value_type)
+ dispatcher_connect(
+ hass, mysensors.SIGNAL_CALLBACK.format(*dev_id),
+ device.update_callback)
return True
+
+
+class MySensorsDeviceScanner(mysensors.MySensorsDevice):
+ """Represent a MySensors scanner."""
+
+ def __init__(self, see, *args):
+ """Set up instance."""
+ super().__init__(*args)
+ self.see = see
+
+ def update_callback(self):
+ """Update the device."""
+ self.update()
+ node = self.gateway.sensors[self.node_id]
+ child = node.children[self.child_id]
+ position = child.values[self.value_type]
+ latitude, longitude, _ = position.split(',')
+
+ self.see(
+ dev_id=slugify(self.name),
+ host_name=self.name,
+ gps=(latitude, longitude),
+ battery=node.battery_level,
+ attributes=self.device_state_attributes
+ )
diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py
index f0c95f7de3d..9e74299e6bc 100644
--- a/homeassistant/components/ecobee.py
+++ b/homeassistant/components/ecobee.py
@@ -13,10 +13,9 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.const import CONF_API_KEY
-from homeassistant.loader import get_component
from homeassistant.util import Throttle
-REQUIREMENTS = ['python-ecobee-api==0.0.7']
+REQUIREMENTS = ['python-ecobee-api==0.0.8']
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
@@ -41,7 +40,7 @@ CONFIG_SCHEMA = vol.Schema({
def request_configuration(network, hass, config):
"""Request configuration steps from the user."""
- configurator = get_component('configurator')
+ configurator = hass.components.configurator
if 'ecobee' in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING['ecobee'], "Failed to register, please try again.")
@@ -56,7 +55,7 @@ def request_configuration(network, hass, config):
setup_ecobee(hass, network, config)
_CONFIGURING['ecobee'] = configurator.request_config(
- hass, "Ecobee", ecobee_configuration_callback,
+ "Ecobee", ecobee_configuration_callback,
description=(
'Please authorize this app at https://www.ecobee.com/consumer'
'portal/index.html with pin code: ' + network.pin),
@@ -73,7 +72,7 @@ def setup_ecobee(hass, network, config):
return
if 'ecobee' in _CONFIGURING:
- configurator = get_component('configurator')
+ configurator = hass.components.configurator
configurator.request_done(_CONFIGURING.pop('ecobee'))
hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP)
diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py
index 31d8ab60e30..f8d41424064 100644
--- a/homeassistant/components/emulated_hue/upnp.py
+++ b/homeassistant/components/emulated_hue/upnp.py
@@ -2,7 +2,6 @@
import threading
import socket
import logging
-import os
import select
from aiohttp import web
@@ -86,18 +85,6 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
advertise_ip, advertise_port).replace("\n", "\r\n") \
.encode('utf-8')
- # Set up a pipe for signaling to the receiver that it's time to
- # shutdown. Essentially, we place the SSDP socket into nonblocking
- # mode and use select() to wait for data to arrive on either the SSDP
- # socket or the pipe. If data arrives on either one, select() returns
- # and tells us which filenos have data ready to read.
- #
- # When we want to stop the responder, we write data to the pipe, which
- # causes the select() to return and indicate that said pipe has data
- # ready to be read, which indicates to us that the responder needs to
- # be shutdown.
- self._interrupted_read_pipe, self._interrupted_write_pipe = os.pipe()
-
def run(self):
"""Run the server."""
# Listen for UDP port 1900 packets sent to SSDP multicast address
@@ -119,7 +106,7 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
socket.inet_aton(self.host_ip_addr))
if self.upnp_bind_multicast:
- ssdp_socket.bind(("239.255.255.250", 1900))
+ ssdp_socket.bind(("", 1900))
else:
ssdp_socket.bind((self.host_ip_addr, 1900))
@@ -130,16 +117,13 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
try:
read, _, _ = select.select(
- [self._interrupted_read_pipe, ssdp_socket], [],
- [ssdp_socket])
+ [ssdp_socket], [],
+ [ssdp_socket], 2)
- if self._interrupted_read_pipe in read:
- # Implies self._interrupted is True
- clean_socket_close(ssdp_socket)
- return
- elif ssdp_socket in read:
+ if ssdp_socket in read:
data, addr = ssdp_socket.recvfrom(1024)
else:
+ # most likely the timeout, so check for interupt
continue
except socket.error as ex:
if self._interrupted:
@@ -148,6 +132,9 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
_LOGGER.error("UPNP Responder socket exception occured: %s",
ex.__str__)
+ # without the following continue, a second exception occurs
+ # because the data object has not been initialized
+ continue
if "M-SEARCH" in data.decode('utf-8'):
# SSDP M-SEARCH method received, respond to it with our info
@@ -161,7 +148,6 @@ USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1
"""Stop the server."""
# Request for server
self._interrupted = True
- os.write(self._interrupted_write_pipe, bytes([0]))
self.join()
diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py
index 87b6163282a..5ffd97ef0e3 100644
--- a/homeassistant/components/envisalink.py
+++ b/homeassistant/components/envisalink.py
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send
-REQUIREMENTS = ['pyenvisalink==2.1']
+REQUIREMENTS = ['pyenvisalink==2.2']
_LOGGER = logging.getLogger(__name__)
@@ -74,9 +74,9 @@ CONFIG_SCHEMA = vol.Schema({
vol.All(vol.Coerce(int), vol.Range(min=3, max=4)),
vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE):
vol.All(vol.Coerce(int), vol.Range(min=15)),
- vol.Optional(CONF_ZONEDUMP_INTERVAL,
- default=DEFAULT_ZONEDUMP_INTERVAL):
- vol.All(vol.Coerce(int), vol.Range(min=15)),
+ vol.Optional(
+ CONF_ZONEDUMP_INTERVAL,
+ default=DEFAULT_ZONEDUMP_INTERVAL): vol.Coerce(int),
}),
}, extra=vol.ALLOW_EXTRA)
diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py
index a18c173ecca..5bdfec08427 100644
--- a/homeassistant/components/fan/insteon_local.py
+++ b/homeassistant/components/fan/insteon_local.py
@@ -13,7 +13,6 @@ from homeassistant.components.fan import (
ATTR_SPEED, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED, FanEntity)
from homeassistant.helpers.entity import ToggleEntity
-from homeassistant.loader import get_component
import homeassistant.util as util
_CONFIGURING = {}
@@ -57,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback):
"""Request configuration steps from the user."""
- configurator = get_component('configurator')
+ configurator = hass.components.configurator
# We got an error if this method is called while we are configuring
if device_id in _CONFIGURING:
@@ -72,7 +71,7 @@ def request_configuration(device_id, insteonhub, model, hass,
add_devices_callback)
_CONFIGURING[device_id] = configurator.request_config(
- hass, 'Insteon ' + model + ' addr: ' + device_id,
+ 'Insteon ' + model + ' addr: ' + device_id,
insteon_fan_config_callback,
description=('Enter a name for ' + model + ' Fan addr: ' + device_id),
entity_picture='/static/images/config_insteon.png',
@@ -85,7 +84,7 @@ def setup_fan(device_id, name, insteonhub, hass, add_devices_callback):
"""Set up the fan."""
if device_id in _CONFIGURING:
request_id = _CONFIGURING.pop(device_id)
- configurator = get_component('configurator')
+ configurator = hass.components.configurator
configurator.request_done(request_id)
_LOGGER.info("Device configuration done!")
diff --git a/homeassistant/components/fan/isy994.py b/homeassistant/components/fan/isy994.py
index 8b9236fdb32..90cd161fa20 100644
--- a/homeassistant/components/fan/isy994.py
+++ b/homeassistant/components/fan/isy994.py
@@ -16,6 +16,11 @@ from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
+# Define term used for medium speed. This must be set as the fan component uses
+# 'medium' which the ISY does not understand
+ISY_SPEED_MEDIUM = 'med'
+
+
VALUE_TO_STATE = {
0: SPEED_OFF,
63: SPEED_LOW,
@@ -29,7 +34,7 @@ STATE_TO_VALUE = {}
for key in VALUE_TO_STATE:
STATE_TO_VALUE[VALUE_TO_STATE[key]] = key
-STATES = [SPEED_OFF, SPEED_LOW, 'med', SPEED_HIGH]
+STATES = [SPEED_OFF, SPEED_LOW, ISY_SPEED_MEDIUM, SPEED_HIGH]
# pylint: disable=unused-argument
@@ -93,6 +98,11 @@ class ISYFanDevice(isy.ISYDevice, FanEntity):
else:
self.speed = self.state
+ @property
+ def speed_list(self) -> list:
+ """Get the list of available speeds."""
+ return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH]
+
class ISYFanProgram(ISYFanDevice):
"""Representation of an ISY994 fan program."""
diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py
index 45bd651ad95..887d07e5855 100644
--- a/homeassistant/components/ffmpeg.py
+++ b/homeassistant/components/ffmpeg.py
@@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import (
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
-REQUIREMENTS = ['ha-ffmpeg==1.5']
+REQUIREMENTS = ['ha-ffmpeg==1.7']
DOMAIN = 'ffmpeg'
diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py
index 07cd39ca581..bd1b511d332 100644
--- a/homeassistant/components/frontend/version.py
+++ b/homeassistant/components/frontend/version.py
@@ -3,10 +3,10 @@
FINGERPRINTS = {
"compatibility.js": "1686167ff210e001f063f5c606b2e74b",
"core.js": "2a7d01e45187c7d4635da05065b5e54e",
- "frontend.html": "5a2a3d6181cc820f5b3e94d1a50def74",
+ "frontend.html": "6c8192a4393c9e83516dc8177b75c23d",
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
- "panels/ha-panel-config.html": "ec48185c79000d0cfe5bbf38c7974944",
+ "panels/ha-panel-config.html": "bd20a3b11b46522e3c705a0b6a72b9dc",
"panels/ha-panel-dev-event.html": "d409e7ab537d9fe629126d122345279c",
"panels/ha-panel-dev-info.html": "b0e55eb657fd75f21aba2426ac0cedc0",
"panels/ha-panel-dev-mqtt.html": "94b222b013a98583842de3e72d5888c6",
diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html
index 71c4381108a..eda4a4821f0 100644
--- a/homeassistant/components/frontend/www_static/frontend.html
+++ b/homeassistant/components/frontend/www_static/frontend.html
@@ -10,7 +10,7 @@
.flex-1{-ms-flex:1 1 0.000000001px;-webkit-flex:1;flex:1;-webkit-flex-basis:0.000000001px;flex-basis:0.000000001px;}.flex-2{-ms-flex:2;-webkit-flex:2;flex:2;}.flex-3{-ms-flex:3;-webkit-flex:3;flex:3;}.flex-4{-ms-flex:4;-webkit-flex:4;flex:4;}.flex-5{-ms-flex:5;-webkit-flex:5;flex:5;}.flex-6{-ms-flex:6;-webkit-flex:6;flex:6;}.flex-7{-ms-flex:7;-webkit-flex:7;flex:7;}.flex-8{-ms-flex:8;-webkit-flex:8;flex:8;}.flex-9{-ms-flex:9;-webkit-flex:9;flex:9;}.flex-10{-ms-flex:10;-webkit-flex:10;flex:10;}.flex-11{-ms-flex:11;-webkit-flex:11;flex:11;}.flex-12{-ms-flex:12;-webkit-flex:12;flex:12;}
Configure Home AssistantHere it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.[[_computeCaption(item)]]
[[_computeDescription(item)]]
Configuration
Server ManagementChanging your configuration can be a tiresome process. We know. This section will try to make your life a little bit easier.
Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid.
[[validateResult]]
check config
Configuration invalid.check config
[[validateLog]]
Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current configuration and load the new one.
Control your Home Assistant server… from Home Assistant.
RestartStop
Bring Hassbian to the next levelDiscover exciting add-ons to enhance your Home Assistant installation. Add an MQTT server or control a connected TV via HDMI-CEC.
Set a themeChoose 'default' to use whatever theme the backend chooses or pick a theme for this device.
[[theme]]
Core
{{label}}
Automations
Automation editor
The automation editor allows you to create and edit automations. Please read the instructions to make sure that you have configured Home Assistant correctly.
We couldn't find any editable automations.
[[computeName(automation)]]
[[computeDescription(automation)]]
Configure Home AssistantHere it is possible to configure your components and Home Assistant. Not everything is possible to configure from the UI yet, but we're working on it.[[_computeCaption(item)]]
[[_computeDescription(item)]]
Configuration
Server ManagementChanging your configuration can be a tiresome process. We know. This section will try to make your life a little bit easier.
Validate your configuration if you recently made some changes to your configuration and want to make sure that it is all valid.
[[validateResult]]
check config
Configuration invalid.check config
[[validateLog]]
Some parts of Home Assistant can reload without requiring a restart. Hitting reload will unload their current configuration and load the new one.
Control your Home Assistant server… from Home Assistant.
RestartStop
Bring Hassbian to the next levelDiscover exciting add-ons to enhance your Home Assistant installation. Add an MQTT server or control a connected TV via HDMI-CEC.
Set a themeChoose 'default' to use whatever theme the backend chooses or pick a theme for this device.
[[theme]]
Core
{{label}}
Automations
Automation editor
The automation editor allows you to create and edit automations. Please read the instructions to make sure that you have configured Home Assistant correctly.