From 44177a7fde7546836cfee3e6afea96be434287a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 18 Jun 2016 13:21:04 -0700 Subject: [PATCH 01/79] Version bump to 0.23.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1b6ae128c18fc8..92909f354354d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = "0.22.0" +__version__ = "0.23.0.dev0" REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}' From cb6f50b7ffe80bcfbeda2efdd45cebaa4a22c883 Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Sun, 19 Jun 2016 12:45:07 -0500 Subject: [PATCH 02/79] Envisalink support (#2304) * Created a new platform for envisalink-based alarm panels (Honeywell/DSC) * Added a sensor component and cleanup * Completed initial development. * Fixing pylint issues. * Fix more pylint issues * Fixed more validation issues. * Final pylint issues * Final tweaks prior to PR. * Fixed final pylint issue * Resolved a few minor issues, and used volumptous for validation. * Fixing final lint issues * Fixes to validation schema and refactoring. --- .coveragerc | 3 + .../alarm_control_panel/envisalink.py | 105 +++++++++ .../components/binary_sensor/envisalink.py | 71 ++++++ homeassistant/components/envisalink.py | 210 ++++++++++++++++++ homeassistant/components/sensor/envisalink.py | 68 ++++++ requirements_all.txt | 4 + 6 files changed, 461 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/envisalink.py create mode 100644 homeassistant/components/binary_sensor/envisalink.py create mode 100644 homeassistant/components/envisalink.py create mode 100644 homeassistant/components/sensor/envisalink.py diff --git a/.coveragerc b/.coveragerc index e8666da60f4466..265c653d636c2e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,6 +20,9 @@ omit = homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py + homeassistant/components/envisalink.py + homeassistant/components/*/envisalink.py + homeassistant/components/insteon_hub.py homeassistant/components/*/insteon_hub.py diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py new file mode 100644 index 00000000000000..ebd54da15587c9 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -0,0 +1,105 @@ +""" +Support for Envisalink-based alarm control panels (Honeywell/DSC). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.envisalink/ +""" +import logging +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.envisalink import (EVL_CONTROLLER, + EnvisalinkDevice, + PARTITION_SCHEMA, + CONF_CODE, + CONF_PARTITIONNAME, + SIGNAL_PARTITION_UPDATE, + SIGNAL_KEYPAD_UPDATE) +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, + STATE_UNKNOWN, STATE_ALARM_TRIGGERED) + +DEPENDENCIES = ['envisalink'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Perform the setup for Envisalink alarm panels.""" + _configured_partitions = discovery_info['partitions'] + _code = discovery_info[CONF_CODE] + for part_num in _configured_partitions: + _device_config_data = PARTITION_SCHEMA( + _configured_partitions[part_num]) + _device = EnvisalinkAlarm( + part_num, + _device_config_data[CONF_PARTITIONNAME], + _code, + EVL_CONTROLLER.alarm_state['partition'][part_num], + EVL_CONTROLLER) + add_devices_callback([_device]) + + return True + + +class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): + """Represents the Envisalink-based alarm panel.""" + + # pylint: disable=too-many-arguments + def __init__(self, partition_number, alarm_name, code, info, controller): + """Initialize the alarm panel.""" + from pydispatch import dispatcher + self._partition_number = partition_number + self._code = code + _LOGGER.debug('Setting up alarm: ' + alarm_name) + EnvisalinkDevice.__init__(self, alarm_name, info, controller) + dispatcher.connect(self._update_callback, + signal=SIGNAL_PARTITION_UPDATE, + sender=dispatcher.Any) + dispatcher.connect(self._update_callback, + signal=SIGNAL_KEYPAD_UPDATE, + sender=dispatcher.Any) + + def _update_callback(self, partition): + """Update HA state, if needed.""" + if partition is None or int(partition) == self._partition_number: + self.update_ha_state() + + @property + def code_format(self): + """The characters if code is defined.""" + return self._code + + @property + def state(self): + """Return the state of the device.""" + if self._info['status']['alarm']: + return STATE_ALARM_TRIGGERED + elif self._info['status']['armed_away']: + return STATE_ALARM_ARMED_AWAY + elif self._info['status']['armed_stay']: + return STATE_ALARM_ARMED_HOME + elif self._info['status']['alpha']: + return STATE_ALARM_DISARMED + else: + return STATE_UNKNOWN + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if self._code: + EVL_CONTROLLER.disarm_partition(str(code), + self._partition_number) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if self._code: + EVL_CONTROLLER.arm_stay_partition(str(code), + self._partition_number) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if self._code: + EVL_CONTROLLER.arm_away_partition(str(code), + self._partition_number) + + def alarm_trigger(self, code=None): + """Alarm trigger command. Not possible for us.""" + raise NotImplementedError() diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py new file mode 100644 index 00000000000000..144de83aa53f70 --- /dev/null +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -0,0 +1,71 @@ +""" +Support for Envisalink zone states- represented as binary sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.envisalink/ +""" +import logging +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.envisalink import (EVL_CONTROLLER, + ZONE_SCHEMA, + CONF_ZONENAME, + CONF_ZONETYPE, + EnvisalinkDevice, + SIGNAL_ZONE_UPDATE) +from homeassistant.const import ATTR_LAST_TRIP_TIME + +DEPENDENCIES = ['envisalink'] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Perform the setup for Envisalink sensor devices.""" + _configured_zones = discovery_info['zones'] + for zone_num in _configured_zones: + _device_config_data = ZONE_SCHEMA(_configured_zones[zone_num]) + _device = EnvisalinkBinarySensor( + zone_num, + _device_config_data[CONF_ZONENAME], + _device_config_data[CONF_ZONETYPE], + EVL_CONTROLLER.alarm_state['zone'][zone_num], + EVL_CONTROLLER) + add_devices_callback([_device]) + + +class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): + """Representation of an envisalink Binary Sensor.""" + + # pylint: disable=too-many-arguments + def __init__(self, zone_number, zone_name, zone_type, info, controller): + """Initialize the binary_sensor.""" + from pydispatch import dispatcher + self._zone_type = zone_type + self._zone_number = zone_number + + _LOGGER.debug('Setting up zone: ' + zone_name) + EnvisalinkDevice.__init__(self, zone_name, info, controller) + dispatcher.connect(self._update_callback, + signal=SIGNAL_ZONE_UPDATE, + sender=dispatcher.Any) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault'] + return attr + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._info['status']['open'] + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return self._zone_type + + def _update_callback(self, zone): + """Update the zone's state, if needed.""" + if zone is None or int(zone) == self._zone_number: + self.update_ha_state() diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py new file mode 100644 index 00000000000000..f1a7009e059517 --- /dev/null +++ b/homeassistant/components/envisalink.py @@ -0,0 +1,210 @@ +""" +Support for Envisalink devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/envisalink/ +""" +import logging +import time +import voluptuous as vol +import homeassistant.helpers.config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.entity import Entity +from homeassistant.components.discovery import load_platform + +REQUIREMENTS = ['pyenvisalink==0.9', 'pydispatcher==2.0.5'] + +_LOGGER = logging.getLogger(__name__) +DOMAIN = 'envisalink' + +EVL_CONTROLLER = None + +CONF_EVL_HOST = 'host' +CONF_EVL_PORT = 'port' +CONF_PANEL_TYPE = 'panel_type' +CONF_EVL_VERSION = 'evl_version' +CONF_CODE = 'code' +CONF_USERNAME = 'user_name' +CONF_PASS = 'password' +CONF_EVL_KEEPALIVE = 'keepalive_interval' +CONF_ZONEDUMP_INTERVAL = 'zonedump_interval' +CONF_ZONES = 'zones' +CONF_PARTITIONS = 'partitions' + +CONF_ZONENAME = 'name' +CONF_ZONETYPE = 'type' +CONF_PARTITIONNAME = 'name' + +DEFAULT_PORT = 4025 +DEFAULT_EVL_VERSION = 3 +DEFAULT_KEEPALIVE = 60 +DEFAULT_ZONEDUMP_INTERVAL = 30 +DEFAULT_ZONETYPE = 'opening' + +SIGNAL_ZONE_UPDATE = 'zones_updated' +SIGNAL_PARTITION_UPDATE = 'partition_updated' +SIGNAL_KEYPAD_UPDATE = 'keypad_updated' + +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONENAME): cv.string, + vol.Optional(CONF_ZONETYPE, default=DEFAULT_ZONETYPE): cv.string}) + +PARTITION_SCHEMA = vol.Schema({ + vol.Required(CONF_PARTITIONNAME): cv.string}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_EVL_HOST): cv.string, + vol.Required(CONF_PANEL_TYPE): + vol.All(cv.string, vol.In(['HONEYWELL', 'DSC'])), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASS): cv.string, + vol.Required(CONF_CODE): cv.string, + vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, + vol.Optional(CONF_PARTITIONS): {vol.Coerce(int): PARTITION_SCHEMA}, + vol.Optional(CONF_EVL_PORT, default=DEFAULT_PORT): + vol.All(vol.Coerce(int), vol.Range(min=1, max=65535)), + vol.Optional(CONF_EVL_VERSION, default=DEFAULT_EVL_VERSION): + 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)), + }), +}, extra=vol.ALLOW_EXTRA) + + +# pylint: disable=unused-argument, too-many-function-args, too-many-locals +# pylint: disable=too-many-return-statements +def setup(hass, base_config): + """Common setup for Envisalink devices.""" + from pyenvisalink import EnvisalinkAlarmPanel + from pydispatch import dispatcher + + global EVL_CONTROLLER + + config = base_config.get(DOMAIN) + + _host = config.get(CONF_EVL_HOST) + _port = config.get(CONF_EVL_PORT) + _code = config.get(CONF_CODE) + _panel_type = config.get(CONF_PANEL_TYPE) + _version = config.get(CONF_EVL_VERSION) + _user = config.get(CONF_USERNAME) + _pass = config.get(CONF_PASS) + _keep_alive = config.get(CONF_EVL_KEEPALIVE) + _zone_dump = config.get(CONF_ZONEDUMP_INTERVAL) + _zones = config.get(CONF_ZONES) + _partitions = config.get(CONF_PARTITIONS) + _connect_status = {} + EVL_CONTROLLER = EnvisalinkAlarmPanel(_host, + _port, + _panel_type, + _version, + _user, + _pass, + _zone_dump, + _keep_alive) + + def login_fail_callback(data): + """Callback for when the evl rejects our login.""" + _LOGGER.error("The envisalink rejected your credentials.") + _connect_status['fail'] = 1 + + def connection_fail_callback(data): + """Network failure callback.""" + _LOGGER.error("Could not establish a connection with the envisalink.") + _connect_status['fail'] = 1 + + def connection_success_callback(data): + """Callback for a successful connection.""" + _LOGGER.info("Established a connection with the envisalink.") + _connect_status['success'] = 1 + + def zones_updated_callback(data): + """Handle zone timer updates.""" + _LOGGER.info("Envisalink sent a zone update event. Updating zones...") + dispatcher.send(signal=SIGNAL_ZONE_UPDATE, + sender=None, + zone=data) + + def alarm_data_updated_callback(data): + """Handle non-alarm based info updates.""" + _LOGGER.info("Envisalink sent new alarm info. Updating alarms...") + dispatcher.send(signal=SIGNAL_KEYPAD_UPDATE, + sender=None, + partition=data) + + def partition_updated_callback(data): + """Handle partition changes thrown by evl (including alarms).""" + _LOGGER.info("The envisalink sent a partition update event.") + dispatcher.send(signal=SIGNAL_PARTITION_UPDATE, + sender=None, + partition=data) + + def stop_envisalink(event): + """Shutdown envisalink connection and thread on exit.""" + _LOGGER.info("Shutting down envisalink.") + EVL_CONTROLLER.stop() + + def start_envisalink(event): + """Startup process for the envisalink.""" + EVL_CONTROLLER.start() + for _ in range(10): + if 'success' in _connect_status: + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) + return True + elif 'fail' in _connect_status: + return False + else: + time.sleep(1) + + _LOGGER.error("Timeout occurred while establishing evl connection.") + return False + + EVL_CONTROLLER.callback_zone_timer_dump = zones_updated_callback + EVL_CONTROLLER.callback_zone_state_change = zones_updated_callback + EVL_CONTROLLER.callback_partition_state_change = partition_updated_callback + EVL_CONTROLLER.callback_keypad_update = alarm_data_updated_callback + EVL_CONTROLLER.callback_login_failure = login_fail_callback + EVL_CONTROLLER.callback_login_timeout = connection_fail_callback + EVL_CONTROLLER.callback_login_success = connection_success_callback + + _result = start_envisalink(None) + if not _result: + return False + + # Load sub-components for envisalink + if _partitions: + load_platform(hass, 'alarm_control_panel', 'envisalink', + {'partitions': _partitions, + 'code': _code}, config) + load_platform(hass, 'sensor', 'envisalink', + {'partitions': _partitions, + 'code': _code}, config) + if _zones: + load_platform(hass, 'binary_sensor', 'envisalink', + {'zones': _zones}, config) + + return True + + +class EnvisalinkDevice(Entity): + """Representation of an envisalink devicetity.""" + + def __init__(self, name, info, controller): + """Initialize the device.""" + self._controller = controller + self._info = info + self._name = name + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/sensor/envisalink.py b/homeassistant/components/sensor/envisalink.py new file mode 100644 index 00000000000000..cd71673b99f47e --- /dev/null +++ b/homeassistant/components/sensor/envisalink.py @@ -0,0 +1,68 @@ +""" +Support for Envisalink sensors (shows panel info). + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.envisalink/ +""" +import logging +from homeassistant.components.envisalink import (EVL_CONTROLLER, + PARTITION_SCHEMA, + CONF_PARTITIONNAME, + EnvisalinkDevice, + SIGNAL_KEYPAD_UPDATE) + +DEPENDENCIES = ['envisalink'] +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Perform the setup for Envisalink sensor devices.""" + _configured_partitions = discovery_info['partitions'] + for part_num in _configured_partitions: + _device_config_data = PARTITION_SCHEMA( + _configured_partitions[part_num]) + _device = EnvisalinkSensor( + _device_config_data[CONF_PARTITIONNAME], + part_num, + EVL_CONTROLLER.alarm_state['partition'][part_num], + EVL_CONTROLLER) + add_devices_callback([_device]) + + +class EnvisalinkSensor(EnvisalinkDevice): + """Representation of an envisalink keypad.""" + + def __init__(self, partition_name, partition_number, info, controller): + """Initialize the sensor.""" + from pydispatch import dispatcher + self._icon = 'mdi:alarm' + self._partition_number = partition_number + _LOGGER.debug('Setting up sensor for partition: ' + partition_name) + EnvisalinkDevice.__init__(self, + partition_name + ' Keypad', + info, + controller) + + dispatcher.connect(self._update_callback, + signal=SIGNAL_KEYPAD_UPDATE, + sender=dispatcher.Any) + + @property + def icon(self): + """Return the icon if any.""" + return self._icon + + @property + def state(self): + """Return the overall state.""" + return self._info['status']['alpha'] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._info['status'] + + def _update_callback(self, partition): + """Update the partition state in HA, if needed.""" + if partition is None or int(partition) == self._partition_number: + self.update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 368ea27649caec..bafb405b956842 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,9 +241,13 @@ pyasn1==0.1.9 # homeassistant.components.media_player.cast pychromecast==0.7.2 +# homeassistant.components.envisalink # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.envisalink +pyenvisalink==0.9 + # homeassistant.components.ifttt pyfttt==0.3 From f59e242c63bf4cda2dd0abe060d8856aec7e366c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jun 2016 20:53:45 -0700 Subject: [PATCH 03/79] fix insteon hub discovery --- homeassistant/components/insteon_hub.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_hub.py b/homeassistant/components/insteon_hub.py index 5cfed006b81975..306acab5361e54 100644 --- a/homeassistant/components/insteon_hub.py +++ b/homeassistant/components/insteon_hub.py @@ -39,7 +39,6 @@ def setup(hass, config): _LOGGER.error("Could not connect to Insteon service.") return - for component in 'light': - discovery.load_platform(hass, component, DOMAIN, {}, config) + discovery.load_platform(hass, 'light', DOMAIN, {}, config) return True From 2e620536290d73d0a08f375e97e274ee882d5ccd Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Mon, 20 Jun 2016 07:30:57 +0200 Subject: [PATCH 04/79] Basic implementation of Zwave Rollershutters (#2313) * Basic implementation of Zwave Rollershutters * Better filtering, by @wokar * Fix typo * Remove polling from component, and loop fix * linter fix * Filter to channel devices to correct component * Remove overwriting of parent node name --- homeassistant/components/light/zwave.py | 1 - .../components/rollershutter/zwave.py | 86 +++++++++++++++++++ homeassistant/components/zwave.py | 49 +++++++++-- 3 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/rollershutter/zwave.py diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index c91a2ddd489cbe..b4aaf5e2b4f25b 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -7,7 +7,6 @@ # Because we do not compile openzwave on CI # pylint: disable=import-error from threading import Timer - from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light from homeassistant.components import zwave from homeassistant.const import STATE_OFF, STATE_ON diff --git a/homeassistant/components/rollershutter/zwave.py b/homeassistant/components/rollershutter/zwave.py new file mode 100644 index 00000000000000..45928d1bfb41d7 --- /dev/null +++ b/homeassistant/components/rollershutter/zwave.py @@ -0,0 +1,86 @@ +""" +Support for Zwave roller shutter components. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/rollershutter.zwave/ +""" +# Because we do not compile openzwave on CI +# pylint: disable=import-error +import logging +from homeassistant.components.rollershutter import DOMAIN +from homeassistant.components.zwave import ZWaveDeviceEntity +from homeassistant.components import zwave +from homeassistant.components.rollershutter import RollershutterDevice + +COMMAND_CLASS_SWITCH_MULTILEVEL = 0x26 # 38 +COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37 + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return Z-Wave roller shutters.""" + if discovery_info is None or zwave.NETWORK is None: + return + + node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] + value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] + + if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL: + return + if value.index != 1: + return + + value.set_change_verified(False) + add_devices([ZwaveRollershutter(value)]) + + +class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): + """Representation of an Zwave roller shutter.""" + + def __init__(self, value): + """Initialize the zwave rollershutter.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + ZWaveDeviceEntity.__init__(self, value, DOMAIN) + self._node = value.node + dispatcher.connect( + self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.node == value.node: + self.update_ha_state(True) + _LOGGER.debug("Value changed on network %s", value) + + @property + def current_position(self): + """Return the current position of Zwave roller shutter.""" + for value in self._node.get_values( + class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): + if value.command_class == 38 and value.index == 0: + return value.data + + def move_up(self, **kwargs): + """Move the roller shutter up.""" + for value in self._node.get_values( + class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): + if value.command_class == 38 and value.index == 0: + value.data = 255 + break + + def move_down(self, **kwargs): + """Move the roller shutter down.""" + for value in self._node.get_values( + class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): + if value.command_class == 38 and value.index == 0: + value.data = 0 + break + + def stop(self, **kwargs): + """Stop the roller shutter.""" + for value in self._node.get_values( + class_id=COMMAND_CLASS_SWITCH_BINARY).values(): + # Rollershutter will toggle between UP (True), DOWN (False). + # It also stops the shutter if the same value is sent while moving. + value.data = value.data diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index b2dd036074c172..36bf01634243d0 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -50,6 +50,14 @@ COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43 COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 # 0x44 +SPECIFIC_DEVICE_CLASS_WHATEVER = None +SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH = 1 +SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR = 3 +SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE = 4 +SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A = 5 +SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B = 6 +SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C = 7 + GENRE_WHATEVER = None GENRE_USER = "User" @@ -60,38 +68,54 @@ # List of tuple (DOMAIN, discovered service, supported command classes, -# value type). +# value type, genre type, specific device class). DISCOVERY_COMPONENTS = [ ('sensor', [COMMAND_CLASS_SENSOR_MULTILEVEL, COMMAND_CLASS_METER, COMMAND_CLASS_ALARM], TYPE_WHATEVER, - GENRE_USER), + GENRE_USER, + SPECIFIC_DEVICE_CLASS_WHATEVER), ('light', [COMMAND_CLASS_SWITCH_MULTILEVEL], TYPE_BYTE, - GENRE_USER), + GENRE_USER, + [SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH, + SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE]), ('switch', [COMMAND_CLASS_SWITCH_BINARY], TYPE_BOOL, - GENRE_USER), + GENRE_USER, + SPECIFIC_DEVICE_CLASS_WHATEVER), ('binary_sensor', [COMMAND_CLASS_SENSOR_BINARY], TYPE_BOOL, - GENRE_USER), + GENRE_USER, + SPECIFIC_DEVICE_CLASS_WHATEVER), ('thermostat', [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, - GENRE_WHATEVER), + GENRE_WHATEVER, + SPECIFIC_DEVICE_CLASS_WHATEVER), ('hvac', [COMMAND_CLASS_THERMOSTAT_FAN_MODE], TYPE_WHATEVER, - GENRE_WHATEVER), + GENRE_WHATEVER, + SPECIFIC_DEVICE_CLASS_WHATEVER), ('lock', [COMMAND_CLASS_DOOR_LOCK], TYPE_BOOL, - GENRE_USER), + GENRE_USER, + SPECIFIC_DEVICE_CLASS_WHATEVER), + ('rollershutter', + [COMMAND_CLASS_SWITCH_MULTILEVEL], + TYPE_WHATEVER, + GENRE_USER, + [SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A, + SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B, + SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C, + SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR]), ] @@ -222,7 +246,8 @@ def value_added(node, value): for (component, command_ids, value_type, - value_genre) in DISCOVERY_COMPONENTS: + value_genre, + specific_device_class) in DISCOVERY_COMPONENTS: if value.command_class not in command_ids: continue @@ -230,8 +255,14 @@ def value_added(node, value): continue if value_genre is not None and value_genre != value.genre: continue + if specific_device_class is not None and \ + specific_device_class != node.specific: + continue # Configure node + _LOGGER.debug("Node_id=%s Value type=%s Genre=%s \ + Specific Device_class=%s", node.node_id, + value.type, value.genre, specific_device_class) name = "{}.{}".format(component, _object_id(value)) node_config = customize.get(name, {}) From cbc0833360d596cb2d58710295b4559d9c046e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20P=C3=A1rraga=20Navarro?= Date: Mon, 20 Jun 2016 07:35:26 +0200 Subject: [PATCH 05/79] Support for Sony Bravia TV (#2243) * Added Sony Bravia support to HA * Improvements to make it work on my poor raspberry 1 * Just a typo * A few fixes in order to pass pylint * - Remove noqa: was due to the 80 characters max per line restriction - Move communication logic to a separate library at https://github.com/aparraga/braviarc.git - Added dependency and adapt the code according to that * A few improvements * Just a typo in a comment * Rebase from HM/dev * Update requirements by executing the script/gen_requirements_all.py * More isolation level for braviarc lib * Remove unnecessary StringIO usage * Revert submodule polymer commit * Small refactorization and clean up of unused functions * Executed script/gen_requirements_all.py * Added a missing condition to ensure that a map is not null * Fix missing parameter detected by pylint * A few improvements, also added an empty line to avoid the lint error * A typo --- .coveragerc | 1 + .../frontend/www_static/images/smart-tv.png | Bin 0 -> 3250 bytes .../components/media_player/braviatv.py | 371 ++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 375 insertions(+) create mode 100644 homeassistant/components/frontend/www_static/images/smart-tv.png create mode 100644 homeassistant/components/media_player/braviatv.py diff --git a/.coveragerc b/.coveragerc index 265c653d636c2e..a7464feb57119c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,6 +123,7 @@ omit = homeassistant/components/light/limitlessled.py homeassistant/components/light/osramlightify.py homeassistant/components/lirc.py + homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/denon.py homeassistant/components/media_player/firetv.py diff --git a/homeassistant/components/frontend/www_static/images/smart-tv.png b/homeassistant/components/frontend/www_static/images/smart-tv.png new file mode 100644 index 0000000000000000000000000000000000000000..5ecda68b40290303ef4c360f7e001aa5a1708d16 GIT binary patch literal 3250 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 KLJtFSi~TPZy8vte literal 0 HcmV?d00001 diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py new file mode 100644 index 00000000000000..ea316f5742532a --- /dev/null +++ b/homeassistant/components/media_player/braviatv.py @@ -0,0 +1,371 @@ +""" +Support for interface with a Sony Bravia TV. + +By Antonio Parraga Navarro + +dedicated to Isabel + +""" +import logging +import os +import json +import re +from homeassistant.loader import get_component +from homeassistant.components.media_player import ( + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, + SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) + +REQUIREMENTS = [ + 'https://github.com/aparraga/braviarc/archive/0.3.2.zip' + '#braviarc==0.3.2'] + +BRAVIA_CONFIG_FILE = 'bravia.conf' +CLIENTID_PREFIX = 'HomeAssistant' +NICKNAME = 'Home Assistant' + +# Map ip to request id for configuring +_CONFIGURING = {} + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ + SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + + +def _get_mac_address(ip_address): + from subprocess import Popen, PIPE + + pid = Popen(["arp", "-n", ip_address], stdout=PIPE) + pid_component = pid.communicate()[0] + mac = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})".encode('UTF-8'), + pid_component).groups()[0] + return mac + + +def _config_from_file(filename, config=None): + """Small configuration file management function.""" + if config: + # We're writing configuration + bravia_config = _config_from_file(filename) + if bravia_config is None: + bravia_config = {} + new_config = bravia_config.copy() + new_config.update(config) + try: + with open(filename, 'w') as fdesc: + fdesc.write(json.dumps(new_config)) + except IOError as error: + _LOGGER.error('Saving config file failed: %s', error) + return False + return True + else: + # We're reading config + if os.path.isfile(filename): + try: + with open(filename, 'r') as fdesc: + return json.loads(fdesc.read()) + except ValueError as error: + return {} + except IOError as error: + _LOGGER.error('Reading config file failed: %s', error) + # This won't work yet + return False + else: + return {} + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup the Sony Bravia TV platform.""" + host = config.get(CONF_HOST) + + if host is None: + return # if no host configured, do not continue + + pin = None + bravia_config = _config_from_file(hass.config.path(BRAVIA_CONFIG_FILE)) + while len(bravia_config): + # Setup a configured TV + host_ip, host_config = bravia_config.popitem() + if host_ip == host: + pin = host_config['pin'] + mac = host_config['mac'] + name = config.get(CONF_NAME) + add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + return + + setup_bravia(config, pin, hass, add_devices_callback) + + +# pylint: disable=too-many-branches +def setup_bravia(config, pin, hass, add_devices_callback): + """Setup a sony bravia based on host parameter.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + if name is None: + name = "Sony Bravia TV" + + if pin is None: + request_configuration(config, hass, add_devices_callback) + return + else: + mac = _get_mac_address(host) + if mac is not None: + mac = mac.decode('utf8') + # If we came here and configuring this host, mark as done + if host in _CONFIGURING: + request_id = _CONFIGURING.pop(host) + configurator = get_component('configurator') + configurator.request_done(request_id) + _LOGGER.info('Discovery configuration done!') + + # Save config + if not _config_from_file( + hass.config.path(BRAVIA_CONFIG_FILE), + {host: {'pin': pin, 'host': host, 'mac': mac}}): + _LOGGER.error('failed to save config file') + + add_devices_callback([BraviaTVDevice(host, mac, name, pin)]) + + +def request_configuration(config, hass, add_devices_callback): + """Request configuration steps from the user.""" + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + if name is None: + name = "Sony Bravia" + + configurator = get_component('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 + + def bravia_configuration_callback(data): + """Callback after user enter PIN.""" + from braviarc import braviarc + + pin = data.get('pin') + braviarc = braviarc.BraviaRC(host) + braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if braviarc.is_connected(): + setup_bravia(config, pin, hass, add_devices_callback) + else: + request_configuration(config, hass, add_devices_callback) + + _CONFIGURING[host] = configurator.request_config( + hass, name, bravia_configuration_callback, + description='Enter the Pin shown on your Sony Bravia TV.' + + 'If no Pin is shown, enter 0000 to let TV show you a Pin.', + description_image="/static/images/smart-tv.png", + submit_caption="Confirm", + fields=[{'id': 'pin', 'name': 'Enter the pin', 'type': ''}] + ) + + +# pylint: disable=abstract-method, too-many-public-methods, +# pylint: disable=too-many-instance-attributes, too-many-arguments +class BraviaTVDevice(MediaPlayerDevice): + """Representation of a Sony Bravia TV.""" + + def __init__(self, host, mac, name, pin): + """Initialize the sony bravia device.""" + from braviarc import braviarc + + self._pin = pin + self._braviarc = braviarc.BraviaRC(host, mac) + self._name = name + self._state = STATE_OFF + self._muted = False + self._program_name = None + self._channel_name = None + self._channel_number = None + self._source = None + self._source_list = [] + self._original_content_list = [] + self._content_mapping = {} + self._duration = None + self._content_uri = None + self._id = None + self._playing = False + self._start_date_time = None + self._program_media_type = None + self._min_volume = None + self._max_volume = None + self._volume = None + + self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if self._braviarc.is_connected(): + self.update() + else: + self._state = STATE_OFF + + def update(self): + """Update TV info.""" + if not self._braviarc.is_connected(): + self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME) + if not self._braviarc.is_connected(): + return + + # Retrieve the latest data. + try: + if self._state == STATE_ON: + # refresh volume info: + self._refresh_volume() + self._refresh_channels() + + playing_info = self._braviarc.get_playing_info() + if playing_info is None or len(playing_info) == 0: + self._state = STATE_OFF + else: + self._state = STATE_ON + self._program_name = playing_info.get('programTitle') + self._channel_name = playing_info.get('title') + self._program_media_type = playing_info.get( + 'programMediaType') + self._channel_number = playing_info.get('dispNum') + self._source = playing_info.get('source') + self._content_uri = playing_info.get('uri') + self._duration = playing_info.get('durationSec') + self._start_date_time = playing_info.get('startDateTime') + + except Exception as exception_instance: # pylint: disable=broad-except + _LOGGER.error(exception_instance) + self._state = STATE_OFF + + def _refresh_volume(self): + """Refresh volume information.""" + volume_info = self._braviarc.get_volume_info() + if volume_info is not None: + self._volume = volume_info.get('volume') + self._min_volume = volume_info.get('minVolume') + self._max_volume = volume_info.get('maxVolume') + self._muted = volume_info.get('mute') + + def _refresh_channels(self): + if len(self._source_list) == 0: + self._content_mapping = self._braviarc. \ + load_source_list() + self._source_list = [] + for key in self._content_mapping: + self._source_list.append(key) + + @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 source(self): + """Return the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_list + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + if self._volume is not None: + return self._volume / 100 + else: + return None + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_BRAVIA + + @property + def media_title(self): + """Title of current playing media.""" + return_value = None + if self._channel_name is not None: + return_value = self._channel_name + if self._program_name is not None: + return_value = return_value + ': ' + self._program_name + return return_value + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self._channel_name + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self._duration + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._braviarc.set_volume_level(volume) + + def turn_on(self): + """Turn the media player on.""" + self._braviarc.turn_on() + + def turn_off(self): + """Turn off media player.""" + self._braviarc.turn_off() + + def volume_up(self): + """Volume up the media player.""" + self._braviarc.volume_up() + + def volume_down(self): + """Volume down media player.""" + self._braviarc.volume_down() + + def mute_volume(self, mute): + """Send mute command.""" + self._braviarc.mute_volume(mute) + + def select_source(self, source): + """Set the input source.""" + if source in self._content_mapping: + uri = self._content_mapping[source] + self._braviarc.play_content(uri) + + def media_play_pause(self): + """Simulate play pause media player.""" + if self._playing: + self.media_pause() + else: + self.media_play() + + def media_play(self): + """Send play command.""" + self._playing = True + self._braviarc.media_play() + + def media_pause(self): + """Send media pause command to media player.""" + self._playing = False + self._braviarc.media_pause() + + def media_next_track(self): + """Send next track command.""" + self._braviarc.media_next_track() + + def media_previous_track(self): + """Send the previous track command.""" + self._braviarc.media_previous_track() diff --git a/requirements_all.txt b/requirements_all.txt index bafb405b956842..36036655c2b7d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -111,6 +111,9 @@ https://github.com/TheRealLink/pythinkingcleaner/archive/v0.0.2.zip#pythinkingcl # homeassistant.components.alarm_control_panel.alarmdotcom https://github.com/Xorso/pyalarmdotcom/archive/0.1.1.zip#pyalarmdotcom==0.1.1 +# homeassistant.components.media_player.braviatv +https://github.com/aparraga/braviarc/archive/0.3.2.zip#braviarc==0.3.2 + # homeassistant.components.media_player.roku https://github.com/bah2830/python-roku/archive/3.1.1.zip#python-roku==3.1.1 From 5efa0760809fa35ef2c189cb67d7c91f83e1b469 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Mon, 20 Jun 2016 07:42:23 +0200 Subject: [PATCH 06/79] Make sure we exit loop when value is set (#2326) --- homeassistant/components/hvac/zwave.py | 4 ++++ homeassistant/components/thermostat/zwave.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py index e1b1614a60f0ea..5becb53b98a648 100755 --- a/homeassistant/components/hvac/zwave.py +++ b/homeassistant/components/hvac/zwave.py @@ -211,6 +211,7 @@ def set_temperature(self, temperature): value.data = int(round(temperature, 0)) else: value.data = int(temperature) + break def set_fan_mode(self, fan): """Set new target fan mode.""" @@ -218,6 +219,7 @@ def set_fan_mode(self, fan): class_id=COMMAND_CLASS_THERMOSTAT_FAN_MODE).values(): if value.command_class == 68 and value.index == 0: value.data = bytes(fan, 'utf-8') + break def set_operation_mode(self, operation_mode): """Set new target operation mode.""" @@ -225,6 +227,7 @@ def set_operation_mode(self, operation_mode): class_id=COMMAND_CLASS_THERMOSTAT_MODE).values(): if value.command_class == 64 and value.index == 0: value.data = bytes(operation_mode, 'utf-8') + break def set_swing_mode(self, swing_mode): """Set new target swing mode.""" @@ -233,3 +236,4 @@ def set_swing_mode(self, swing_mode): class_id=COMMAND_CLASS_CONFIGURATION).values(): if value.command_class == 112 and value.index == 33: value.data = int(swing_mode) + break diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py index 4eb18664a24c30..57287605e6738d 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -156,3 +156,4 @@ def set_temperature(self, temperature): COMMAND_CLASS_THERMOSTAT_SETPOINT).items(): if int(value.data) != 0 and value.index == self._index: value.data = temperature + break From 6fa095f4a7c0d005381e9f3f6b621f9919cb779e Mon Sep 17 00:00:00 2001 From: dale3h Date: Mon, 20 Jun 2016 01:08:30 -0500 Subject: [PATCH 07/79] Add additional Pushover parameters (#2309) * Add additional Pushover parameters Add support for more Pushover parameters: target (device), sound, url, url_title, priority, timestamp * Remove data dictionary reference https://github.com/home-assistant/home-assistant/pull/2309#discussion_r67603127 --- homeassistant/components/notify/pushover.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index b202f38fa7ce01..304d771f92a159 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -7,7 +7,7 @@ import logging from homeassistant.components.notify import ( - ATTR_TITLE, DOMAIN, BaseNotificationService) + ATTR_TITLE, ATTR_TARGET, ATTR_DATA, DOMAIN, BaseNotificationService) from homeassistant.const import CONF_API_KEY from homeassistant.helpers import validate_config @@ -51,7 +51,17 @@ def send_message(self, message="", **kwargs): """Send a message to a user.""" from pushover import RequestError + # Make a copy and use empty dict as default value (thanks @balloob) + data = dict(kwargs.get(ATTR_DATA, {})) + + data['title'] = kwargs.get(ATTR_TITLE) + target = kwargs.get(ATTR_TARGET) + if target is not None: + data['device'] = target + try: - self.pushover.send_message(message, title=kwargs.get(ATTR_TITLE)) + self.pushover.send_message(message, **data) + except ValueError as val_err: + _LOGGER.error(str(val_err)) except RequestError: _LOGGER.exception("Could not send pushover notification") From ba417a730b17e74ebe1cafb2c6dd631b7fc1bdc8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 20 Jun 2016 17:55:57 +0200 Subject: [PATCH 08/79] Upgrade slacker to 0.9.17 (#2340) --- homeassistant/components/notify/slack.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 49b6f8acc93a07..5257c965cd6707 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.helpers import validate_config -REQUIREMENTS = ['slacker==0.9.16'] +REQUIREMENTS = ['slacker==0.9.17'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 36036655c2b7d6..e876ccacbb3693 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -351,7 +351,7 @@ scsgate==0.1.0 sendgrid>=1.6.0,<1.7.0 # homeassistant.components.notify.slack -slacker==0.9.16 +slacker==0.9.17 # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 From caa096ebd5f2978963a638ba9c97e4f8f96ac2e7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 21 Jun 2016 06:51:07 +0200 Subject: [PATCH 09/79] Upgrade psutil to 4.3.0 (#2342) * Upgrade psutil to 4.3.0 * Remove period --- homeassistant/components/sensor/systemmonitor.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 20af7e71a59a90..c9767428aaadd6 100755 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -1,5 +1,5 @@ """ -Support for monitoring the local system.. +Support for monitoring the local system. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.systemmonitor/ @@ -10,7 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['psutil==4.2.0'] +REQUIREMENTS = ['psutil==4.3.0'] SENSOR_TYPES = { 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], diff --git a/requirements_all.txt b/requirements_all.txt index e876ccacbb3693..db0f7db3964743 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -218,7 +218,7 @@ plexapi==1.1.0 proliphix==0.1.0 # homeassistant.components.sensor.systemmonitor -psutil==4.2.0 +psutil==4.3.0 # homeassistant.components.notify.pushbullet pushbullet.py==0.10.0 From 38b03366941dc58f8d7c8496865c7b746a6ff2c9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 21 Jun 2016 06:51:50 +0200 Subject: [PATCH 10/79] Upgrade paho-mqtt to 1.2 (#2339) --- homeassistant/components/mqtt/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e9087d9c578731..6db231f6bd7fcb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -29,7 +29,7 @@ SERVICE_PUBLISH = 'publish' EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' -REQUIREMENTS = ['paho-mqtt==1.1'] +REQUIREMENTS = ['paho-mqtt==1.2'] CONF_EMBEDDED = 'embedded' CONF_BROKER = 'broker' diff --git a/requirements_all.txt b/requirements_all.txt index db0f7db3964743..4853c520429d9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -197,7 +197,7 @@ neurio==0.2.10 orvibo==1.1.1 # homeassistant.components.mqtt -paho-mqtt==1.1 +paho-mqtt==1.2 # homeassistant.components.media_player.panasonic_viera panasonic_viera==0.2 From 278514b994b21b1a89ebdd28b359cc08432a239f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 21 Jun 2016 16:43:02 +0200 Subject: [PATCH 11/79] Add support for Fixer.io (#2336) * Add support for Fixer.io * Add unit of measurment and set throttle to one day --- .coveragerc | 1 + homeassistant/components/sensor/fixer.py | 125 +++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 129 insertions(+) create mode 100644 homeassistant/components/sensor/fixer.py diff --git a/.coveragerc b/.coveragerc index a7464feb57119c..a1b63cf05595e8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -174,6 +174,7 @@ omit = homeassistant/components/sensor/efergy.py homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/fitbit.py + homeassistant/components/sensor/fixer.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/google_travel_time.py diff --git a/homeassistant/components/sensor/fixer.py b/homeassistant/components/sensor/fixer.py new file mode 100644 index 00000000000000..05f6003039e774 --- /dev/null +++ b/homeassistant/components/sensor/fixer.py @@ -0,0 +1,125 @@ +""" +Currency exchange rate support that comes from fixer.io. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.fixer/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import (CONF_PLATFORM, CONF_NAME) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['fixerio==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Exchange rate" +ICON = 'mdi:currency' + +CONF_BASE = 'base' +CONF_TARGET = 'target' + +STATE_ATTR_BASE = 'Base currency' +STATE_ATTR_TARGET = 'Target currency' +STATE_ATTR_EXCHANGE_RATE = 'Exchange rate' + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'fixer', + vol.Optional(CONF_BASE): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_TARGET): cv.string, +}) + +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_UPDATES = timedelta(days=1) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Fixer.io sensor.""" + from fixerio import (Fixerio, exceptions) + + name = config.get(CONF_NAME, DEFAULT_NAME) + base = config.get(CONF_BASE, 'USD') + target = config.get(CONF_TARGET) + + try: + Fixerio(base=base, symbols=[target], secure=True).latest() + except exceptions.FixerioException: + _LOGGER.error('One of the given currencies is not supported') + return False + + data = ExchangeData(base, target) + add_devices([ExchangeRateSensor(data, name, target)]) + + +# pylint: disable=too-few-public-methods +class ExchangeRateSensor(Entity): + """Representation of a Exchange sensor.""" + + def __init__(self, data, name, target): + """Initialize the sensor.""" + self.data = data + self._target = target + self._name = name + self._state = None + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._target + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.data.rate is not None: + return { + STATE_ATTR_BASE: self.data.rate['base'], + STATE_ATTR_TARGET: self._target, + STATE_ATTR_EXCHANGE_RATE: self.data.rate['rates'][self._target] + } + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return ICON + + def update(self): + """Get the latest data and updates the states.""" + self.data.update() + self._state = round(self.data.rate['rates'][self._target], 3) + + +class ExchangeData(object): + """Get the latest data and update the states.""" + + def __init__(self, base_currency, target_currency): + """Initialize the data object.""" + from fixerio import Fixerio + + self.rate = None + self.base_currency = base_currency + self.target_currency = target_currency + self.exchange = Fixerio(base=self.base_currency, + symbols=[self.target_currency], + secure=True) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from Fixer.io.""" + self.rate = self.exchange.latest() diff --git a/requirements_all.txt b/requirements_all.txt index 4853c520429d9f..e8a24fc2024ef1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -73,6 +73,9 @@ feedparser==5.2.1 # homeassistant.components.sensor.fitbit fitbit==0.2.2 +# homeassistant.components.sensor.fixer +fixerio==0.1.1 + # homeassistant.components.notify.free_mobile freesms==0.1.0 From d87e96967192278b6d50a93c208bcbc495d7b4bd Mon Sep 17 00:00:00 2001 From: happyleavesaoc Date: Mon, 20 Jun 2016 01:22:29 +0000 Subject: [PATCH 12/79] add cec platform --- .coveragerc | 1 + homeassistant/components/cec.py | 120 ++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 homeassistant/components/cec.py diff --git a/.coveragerc b/.coveragerc index 265c653d636c2e..6416ad3e2b43c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,7 @@ omit = homeassistant/components/camera/generic.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/rpi_camera.py + homeassistant/components/cec.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py diff --git a/homeassistant/components/cec.py b/homeassistant/components/cec.py new file mode 100644 index 00000000000000..0034eb131b60de --- /dev/null +++ b/homeassistant/components/cec.py @@ -0,0 +1,120 @@ +""" +CEC platform. + +Requires libcec + Python bindings. +""" + +import logging +import voluptuous as vol +from homeassistant.const import EVENT_HOMEASSISTANT_START +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) +_CEC = None +DOMAIN = 'cec' +SERVICE_SELECT_DEVICE = 'select_device' +SERVICE_POWER_ON = 'power_on' +SERVICE_STANDBY = 'standby' +CONF_DEVICES = 'devices' +ATTR_DEVICE = 'device' +MAX_DEPTH = 4 + + +# pylint: disable=unnecessary-lambda +DEVICE_SCHEMA = vol.Schema({ + vol.All(cv.positive_int): vol.Any(lambda devices: DEVICE_SCHEMA(devices), + cv.string) +}) + + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_DEVICES): DEVICE_SCHEMA +}) + + +def parse_mapping(mapping, parents=None): + """Parse configuration device mapping.""" + if parents is None: + parents = [] + for addr, val in mapping.items(): + cur = parents + [str(addr)] + if isinstance(val, dict): + yield from parse_mapping(val, cur) + elif isinstance(val, str): + yield (val, cur) + + +def pad_physical_address(addr): + """Right-pad a physical address""" + return addr + ['0'] * (MAX_DEPTH - len(addr)) + + +def setup(hass, config): + """Setup CEC capability.""" + global _CEC + + # cec is only available if libcec is properly installed + # and the Python bindings are accessible. + try: + import cec + except ImportError: + _LOGGER.error("libcec must be installed") + return False + + # Parse configuration into a dict of device name + # to physical address represented as a list of + # four elements. + flat = {} + for pair in parse_mapping(config[DOMAIN][0].get(CONF_DEVICES, {})): + flat[pair[0]] = pad_physical_address(pair[1]) + + # Configure libcec. + cfg = cec.libcec_configuration() + cfg.strDeviceName = 'HASS' + cfg.bActivateSource = 0 + cfg.bMonitorOnly = 1 + cfg.clientVersion = cec.LIBCEC_VERSION_CURRENT + + # Set up CEC adapter. + _CEC = cec.ICECAdapter.Create(cfg) + + def _power_on(call): + """Power on all devices.""" + _CEC.PowerOnDevices() + + def _standby(call): + """Standby all devices.""" + _CEC.StandbyDevices() + + def _select_device(call): + """Select the active device.""" + path = flat.get(call.data[ATTR_DEVICE]) + if not path: + _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) + cmds = [] + for i in range(1, MAX_DEPTH - 1): + addr = pad_physical_address(path[:i]) + cmds.append('1f:82:{}{}:{}{}'.format(*addr)) + cmds.append('1f:86:{}{}:{}{}'.format(*addr)) + for cmd in cmds: + _CEC.Transmit(_CEC.CommandFromString(cmd)) + _LOGGER.info("Selected %s", call.data[ATTR_DEVICE]) + + def _start_cec(event): + """Open CEC adapter.""" + adapters = _CEC.DetectAdapters() + if len(adapters) == 0: + _LOGGER.error("No CEC adapter found") + return + + if _CEC.Open(adapters[0].strComName): + hass.services.register(DOMAIN, SERVICE_POWER_ON, _power_on) + hass.services.register(DOMAIN, SERVICE_STANDBY, _standby) + hass.services.register(DOMAIN, SERVICE_SELECT_DEVICE, + _select_device) + else: + _LOGGER.error("Failed to open adapter") + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_cec) + return True From 7fc9fa4b0c186633c2609cfef923407f97c831ca Mon Sep 17 00:00:00 2001 From: happyleaves Date: Tue, 21 Jun 2016 19:31:40 -0400 Subject: [PATCH 13/79] satisfy farcy --- homeassistant/components/cec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cec.py b/homeassistant/components/cec.py index 0034eb131b60de..0c7c40c1214bf8 100644 --- a/homeassistant/components/cec.py +++ b/homeassistant/components/cec.py @@ -46,7 +46,7 @@ def parse_mapping(mapping, parents=None): def pad_physical_address(addr): - """Right-pad a physical address""" + """Right-pad a physical address.""" return addr + ['0'] * (MAX_DEPTH - len(addr)) From a564fe828669ed11c964668a9e72958a1f07bb24 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 21 Jun 2016 22:26:40 -0700 Subject: [PATCH 14/79] Fix error log (#2349) --- homeassistant/components/http.py | 2 +- tests/components/test_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 3ccf92daea2d81..0baa7dd177cb1b 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -437,7 +437,7 @@ def file(self, request, fil, mimetype=None): mimetype = mimetypes.guess_type(fil)[0] try: - fil = open(fil) + fil = open(fil, mode='br') except IOError: raise NotFound() diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 66fb97dfd33392..60ff19d4a4322e 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -225,7 +225,7 @@ def test_api_get_components(self): def test_api_get_error_log(self): """Test the return of the error log.""" - test_content = 'Test String' + test_content = 'Test String°' with tempfile.NamedTemporaryFile() as log: log.write(test_content.encode('utf-8')) log.flush() From d7b006600e76e5f6edb0c23b06daf867533ffdec Mon Sep 17 00:00:00 2001 From: Dale Higgs Date: Wed, 22 Jun 2016 10:54:44 -0500 Subject: [PATCH 15/79] [notify.pushover] Fix 'NoneType' error on data retrieval (#2352) * Fix 'NoneType' error on data retrieval * Reduce code for empty dict as the default --- homeassistant/components/notify/pushover.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/pushover.py b/homeassistant/components/notify/pushover.py index 304d771f92a159..a8bc3cf51796d3 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -51,10 +51,11 @@ def send_message(self, message="", **kwargs): """Send a message to a user.""" from pushover import RequestError - # Make a copy and use empty dict as default value (thanks @balloob) - data = dict(kwargs.get(ATTR_DATA, {})) + # Make a copy and use empty dict if necessary + data = dict(kwargs.get(ATTR_DATA) or {}) data['title'] = kwargs.get(ATTR_TITLE) + target = kwargs.get(ATTR_TARGET) if target is not None: data['device'] = target From 9ce9b8debb0b177ca1c4edf7c82544a5c523346e Mon Sep 17 00:00:00 2001 From: Jean-Philippe Bouillot Date: Wed, 22 Jun 2016 18:01:53 +0200 Subject: [PATCH 16/79] Add support for wind, battery, radio signals for Netatmo sensor (#2351) * Add support for wind, battery, radio signals * Fix indentation error * second indentation fix * Fix for pylint too many statements error * Moving "pylint: disable=too-many-statements" --- homeassistant/components/sensor/netatmo.py | 101 +++++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 22caab1d1fbce0..05498d41496f86 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -11,19 +11,29 @@ from homeassistant.util import Throttle from homeassistant.loader import get_component + DEPENDENCIES = ["netatmo"] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'co2': ['CO2', 'ppm', 'mdi:cloud'], - 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], - 'noise': ['Noise', 'dB', 'mdi:volume-high'], - 'humidity': ['Humidity', '%', 'mdi:water-percent'], - 'rain': ['Rain', 'mm', 'mdi:weather-rainy'], - 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], - 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], + 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], + 'co2': ['CO2', 'ppm', 'mdi:cloud'], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], + 'noise': ['Noise', 'dB', 'mdi:volume-high'], + 'humidity': ['Humidity', '%', 'mdi:water-percent'], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy'], + 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], + 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], + 'battery_vp': ['Battery', '', 'mdi:battery'], + 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'], + 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'], + 'WindAngle': ['Angle', '', 'mdi:compass'], + 'WindStrength': ['Strength', 'km/h', 'mdi:weather-windy'], + 'GustAngle': ['Gust Angle', '', 'mdi:compass'], + 'GustStrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'], + 'rf_status': ['Radio', '', 'mdi:signal'], + 'wifi_status': ['Wifi', '', 'mdi:wifi'] } CONF_STATION = 'station' @@ -97,6 +107,8 @@ def unit_of_measurement(self): return self._unit_of_measurement # pylint: disable=too-many-branches + # Fix for pylint too many statements error + # pylint: disable=too-many-statements def update(self): """Get the latest data from NetAtmo API and updates the states.""" self.netatmo_data.update() @@ -118,6 +130,79 @@ def update(self): self._state = data['CO2'] elif self.type == 'pressure': self._state = round(data['Pressure'], 1) + elif self.type == 'battery_vp': + if data['battery_vp'] >= 5500: + self._state = "Full" + elif data['battery_vp'] >= 5100: + self._state = "High" + elif data['battery_vp'] >= 4600: + self._state = "Medium" + elif data['battery_vp'] >= 4100: + self._state = "Low" + elif data['battery_vp'] < 4100: + self._state = "Very Low" + elif self.type == 'min_temp': + self._state = data['min_temp'] + elif self.type == 'max_temp': + self._state = data['max_temp'] + elif self.type == 'WindAngle': + if data['WindAngle'] >= 330: + self._state = "North (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 300: + self._state = "North-West (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 240: + self._state = "West (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 210: + self._state = "South-West (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 150: + self._state = "South (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 120: + self._state = "South-East (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 60: + self._state = "East (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 30: + self._state = "North-East (%d\xb0)" % data['WindAngle'] + elif data['WindAngle'] >= 0: + self._state = "North (%d\xb0)" % data['WindAngle'] + elif self.type == 'WindStrength': + self._state = data['WindStrength'] + elif self.type == 'GustAngle': + if data['GustAngle'] >= 330: + self._state = "North (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 300: + self._state = "North-West (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 240: + self._state = "West (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 210: + self._state = "South-West (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 150: + self._state = "South (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 120: + self._state = "South-East (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 60: + self._state = "East (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 30: + self._state = "North-East (%d\xb0)" % data['GustAngle'] + elif data['GustAngle'] >= 0: + self._state = "North (%d\xb0)" % data['GustAngle'] + elif self.type == 'GustStrength': + self._state = data['GustStrength'] + elif self.type == 'rf_status': + if data['rf_status'] >= 90: + self._state = "Low" + elif data['rf_status'] >= 76: + self._state = "Medium" + elif data['rf_status'] >= 60: + self._state = "High" + elif data['rf_status'] <= 59: + self._state = "Full" + elif self.type == 'wifi_status': + if data['wifi_status'] >= 86: + self._state = "Bad" + elif data['wifi_status'] >= 71: + self._state = "Middle" + elif data['wifi_status'] <= 70: + self._state = "Good" class NetAtmoData(object): From a70f922a71d6942ac9b8e2f21da3243a9d148fdd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 22 Jun 2016 09:13:18 -0700 Subject: [PATCH 17/79] ps - add reload core config service (#2350) --- homeassistant/bootstrap.py | 7 +- homeassistant/components/__init__.py | 24 +++++++ homeassistant/config.py | 6 +- homeassistant/helpers/entity.py | 37 +++++----- tests/components/test_init.py | 103 +++++++++++++++++++++------ tests/helpers/test_entity.py | 33 +++++++-- 6 files changed, 156 insertions(+), 54 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 99382bebe74431..754d4f4f5aafd3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -25,8 +25,8 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( - event_decorators, service, config_per_platform, extract_domain_configs) -from homeassistant.helpers.entity import Entity + event_decorators, service, config_per_platform, extract_domain_configs, + entity) _LOGGER = logging.getLogger(__name__) _SETUP_LOCK = RLock() @@ -412,8 +412,7 @@ def set_time_zone(time_zone_str): if CONF_TIME_ZONE in config: set_time_zone(config.get(CONF_TIME_ZONE)) - for entity_id, attrs in config.get(CONF_CUSTOMIZE).items(): - Entity.overwrite_attribute(entity_id, attrs.keys(), attrs.values()) + entity.set_customize(config.get(CONF_CUSTOMIZE)) if CONF_TEMPERATURE_UNIT in config: hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index f2696bbbd1ac9e..d625f9cd3cdf4d 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -19,6 +19,8 @@ _LOGGER = logging.getLogger(__name__) +SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' + def is_on(hass, entity_id=None): """Load up the module to call the is_on method. @@ -73,6 +75,11 @@ def toggle(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) +def reload_core_config(hass): + """Reload the core config.""" + hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG) + + def setup(hass, config): """Setup general services related to Home Assistant.""" def handle_turn_service(service): @@ -111,4 +118,21 @@ def handle_turn_service(service): hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) + def handle_reload_config(call): + """Service handler for reloading core config.""" + from homeassistant.exceptions import HomeAssistantError + from homeassistant import config, bootstrap + + try: + path = config.find_config_file(hass.config.config_dir) + conf = config.load_yaml_config_file(path) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + bootstrap.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) + + hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, + handle_reload_config) + return True diff --git a/homeassistant/config.py b/homeassistant/config.py index b89d358045dac8..e8981e520c8d8e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -149,9 +149,9 @@ def load_yaml_config_file(config_path): conf_dict = load_yaml(config_path) if not isinstance(conf_dict, dict): - _LOGGER.error( - 'The configuration file %s does not contain a dictionary', + msg = 'The configuration file {} does not contain a dictionary'.format( os.path.basename(config_path)) - raise HomeAssistantError() + _LOGGER.error(msg) + raise HomeAssistantError(msg) return conf_dict diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 423a276f11a495..ee1b786dce3f0b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,6 +1,6 @@ """An abstract class for entities.""" +import logging import re -from collections import defaultdict from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, @@ -10,8 +10,10 @@ from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify -# Dict mapping entity_id to a boolean that overwrites the hidden property -_OVERWRITE = defaultdict(dict) +# Entity attributes that we will overwrite +_OVERWRITE = {} + +_LOGGER = logging.getLogger(__name__) # Pattern for validating entity IDs (format: .) ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") @@ -22,7 +24,7 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): name = (name or DEVICE_DEFAULT_NAME).lower() if current_ids is None: if hass is None: - raise RuntimeError("Missing required parameter currentids or hass") + raise ValueError("Missing required parameter currentids or hass") current_ids = hass.states.entity_ids() @@ -30,6 +32,13 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): entity_id_format.format(slugify(name)), current_ids) +def set_customize(customize): + """Overwrite all current customize settings.""" + global _OVERWRITE + + _OVERWRITE = {key.lower(): val for key, val in customize.items()} + + def split_entity_id(entity_id): """Split a state entity_id into domain, object_id.""" return entity_id.split(".", 1) @@ -207,20 +216,6 @@ def __repr__(self): """Return the representation.""" return "".format(self.name, self.state) - @staticmethod - def overwrite_attribute(entity_id, attrs, vals): - """Overwrite any attribute of an entity. - - This function should receive a list of attributes and a - list of values. Set attribute to None to remove any overwritten - value in place. - """ - for attr, val in zip(attrs, vals): - if val is None: - _OVERWRITE[entity_id.lower()].pop(attr, None) - else: - _OVERWRITE[entity_id.lower()][attr] = val - class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" @@ -238,11 +233,13 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the entity on.""" - pass + _LOGGER.warning('Method turn_on not implemented for %s', + self.entity_id) def turn_off(self, **kwargs): """Turn the entity off.""" - pass + _LOGGER.warning('Method turn_off not implemented for %s', + self.entity_id) def toggle(self, **kwargs): """Toggle the entity off.""" diff --git a/tests/components/test_init.py b/tests/components/test_init.py index ff663a95a5378d..68b0ca3be35d78 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -2,13 +2,18 @@ # pylint: disable=protected-access,too-many-public-methods import unittest from unittest.mock import patch +from tempfile import TemporaryDirectory + +import yaml import homeassistant.core as ha +from homeassistant import config from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) import homeassistant.components as comps +from homeassistant.helpers import entity -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_service class TestComponentsCore(unittest.TestCase): @@ -31,47 +36,40 @@ def test_is_on(self): self.assertTrue(comps.is_on(self.hass, 'light.Bowl')) self.assertFalse(comps.is_on(self.hass, 'light.Ceiling')) self.assertTrue(comps.is_on(self.hass)) + self.assertFalse(comps.is_on(self.hass, 'non_existing.entity')) + + def test_turn_on_without_entities(self): + """Test turn_on method without entities.""" + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + comps.turn_on(self.hass) + self.hass.pool.block_till_done() + self.assertEqual(0, len(calls)) def test_turn_on(self): """Test turn_on method.""" - runs = [] - self.hass.services.register( - 'light', SERVICE_TURN_ON, lambda x: runs.append(1)) - + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) comps.turn_on(self.hass, 'light.Ceiling') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(runs)) + self.assertEqual(1, len(calls)) def test_turn_off(self): """Test turn_off method.""" - runs = [] - self.hass.services.register( - 'light', SERVICE_TURN_OFF, lambda x: runs.append(1)) - + calls = mock_service(self.hass, 'light', SERVICE_TURN_OFF) comps.turn_off(self.hass, 'light.Bowl') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(runs)) + self.assertEqual(1, len(calls)) def test_toggle(self): """Test toggle method.""" - runs = [] - self.hass.services.register( - 'light', SERVICE_TOGGLE, lambda x: runs.append(1)) - + calls = mock_service(self.hass, 'light', SERVICE_TOGGLE) comps.toggle(self.hass, 'light.Bowl') - self.hass.pool.block_till_done() - - self.assertEqual(1, len(runs)) + self.assertEqual(1, len(calls)) @patch('homeassistant.core.ServiceRegistry.call') def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): """Test if turn_on is blocking domain with no service.""" - self.hass.services.register('light', SERVICE_TURN_ON, lambda x: x) + mock_service(self.hass, 'light', SERVICE_TURN_ON) # We can't test if our service call results in services being called # because by mocking out the call service method, we mock out all @@ -89,3 +87,62 @@ def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): self.assertEqual( ('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False), mock_call.call_args_list[1][0]) + + def test_reload_core_conf(self): + """Test reload core conf service.""" + ent = entity.Entity() + ent.entity_id = 'test.entity' + ent.hass = self.hass + ent.update_ha_state() + + state = self.hass.states.get('test.entity') + assert state is not None + assert state.state == 'unknown' + assert state.attributes == {} + + with TemporaryDirectory() as conf_dir: + self.hass.config.config_dir = conf_dir + conf_yaml = self.hass.config.path(config.YAML_CONFIG_FILE) + + with open(conf_yaml, 'a') as fp: + fp.write(yaml.dump({ + ha.DOMAIN: { + 'latitude': 10, + 'longitude': 20, + 'customize': { + 'test.Entity': { + 'hello': 'world' + } + } + } + })) + + comps.reload_core_config(self.hass) + self.hass.pool.block_till_done() + + assert 10 == self.hass.config.latitude + assert 20 == self.hass.config.longitude + + ent.update_ha_state() + + state = self.hass.states.get('test.entity') + assert state is not None + assert state.state == 'unknown' + assert state.attributes.get('hello') == 'world' + + @patch('homeassistant.components._LOGGER.error') + @patch('homeassistant.bootstrap.process_ha_core_config') + def test_reload_core_with_wrong_conf(self, mock_process, mock_error): + """Test reload core conf service.""" + with TemporaryDirectory() as conf_dir: + self.hass.config.config_dir = conf_dir + conf_yaml = self.hass.config.path(config.YAML_CONFIG_FILE) + + with open(conf_yaml, 'a') as fp: + fp.write(yaml.dump(['invalid', 'config'])) + + comps.reload_core_config(self.hass) + self.hass.pool.block_till_done() + + assert mock_error.called + assert mock_process.called is False diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index a317702b29f0f8..a465c2f2c74b8a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -21,8 +21,7 @@ def setUp(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name """Stop everything that was started.""" self.hass.stop() - entity.Entity.overwrite_attribute(self.entity.entity_id, - [ATTR_HIDDEN], [None]) + entity.set_customize({}) def test_default_hidden_not_in_attributes(self): """Test that the default hidden property is set to False.""" @@ -32,8 +31,7 @@ def test_default_hidden_not_in_attributes(self): def test_overwriting_hidden_property_to_true(self): """Test we can overwrite hidden property to True.""" - entity.Entity.overwrite_attribute(self.entity.entity_id, - [ATTR_HIDDEN], [True]) + entity.set_customize({self.entity.entity_id: {ATTR_HIDDEN: True}}) self.entity.update_ha_state() state = self.hass.states.get(self.entity.entity_id) @@ -43,3 +41,30 @@ def test_split_entity_id(self): """Test split_entity_id.""" self.assertEqual(['domain', 'object_id'], entity.split_entity_id('domain.object_id')) + + def test_generate_entity_id_requires_hass_or_ids(self): + """Ensure we require at least hass or current ids.""" + fmt = 'test.{}' + with self.assertRaises(ValueError): + entity.generate_entity_id(fmt, 'hello world') + + def test_generate_entity_id_given_hass(self): + """Test generating an entity id given hass object.""" + fmt = 'test.{}' + self.assertEqual( + 'test.overwrite_hidden_true_2', + entity.generate_entity_id(fmt, 'overwrite hidden true', + hass=self.hass)) + + def test_generate_entity_id_given_keys(self): + """Test generating an entity id given current ids.""" + fmt = 'test.{}' + self.assertEqual( + 'test.overwrite_hidden_true_2', + entity.generate_entity_id( + fmt, 'overwrite hidden true', + current_ids=['test.overwrite_hidden_true'])) + self.assertEqual( + 'test.overwrite_hidden_true', + entity.generate_entity_id(fmt, 'overwrite hidden true', + current_ids=['test.another_entity'])) From 7b942243ab004afa98d5fea59fadf0c68761fe89 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 22 Jun 2016 20:12:36 +0200 Subject: [PATCH 18/79] Increase interval (#2353) --- homeassistant/components/sensor/swiss_hydrological_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index 6bfda3f55f512f..ddc31bb56ec9c8 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -47,7 +47,7 @@ 'temperature_max']) # Return cached results if last scan was less then this time ago. -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) def setup_platform(hass, config, add_devices, discovery_info=None): From 94b47d8bc361ce1485443b2d42bacc85e13cd876 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Wed, 22 Jun 2016 17:07:46 -0400 Subject: [PATCH 19/79] addressed review --- homeassistant/components/{cec.py => hdmi_cec.py} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename homeassistant/components/{cec.py => hdmi_cec.py} (95%) diff --git a/homeassistant/components/cec.py b/homeassistant/components/hdmi_cec.py similarity index 95% rename from homeassistant/components/cec.py rename to homeassistant/components/hdmi_cec.py index 0c7c40c1214bf8..f5a64b909af9e6 100644 --- a/homeassistant/components/cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -1,5 +1,5 @@ """ -CEC platform. +CEC component. Requires libcec + Python bindings. """ @@ -12,7 +12,7 @@ _LOGGER = logging.getLogger(__name__) _CEC = None -DOMAIN = 'cec' +DOMAIN = 'hdmi_cec' SERVICE_SELECT_DEVICE = 'select_device' SERVICE_POWER_ON = 'power_on' SERVICE_STANDBY = 'standby' @@ -28,8 +28,10 @@ }) -PLATFORM_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICES): DEVICE_SCHEMA +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICES): DEVICE_SCHEMA + }) }) From d0ee8abcb839ec13409fc66d0aca24a46eaac932 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Wed, 22 Jun 2016 17:29:22 -0400 Subject: [PATCH 20/79] couple fixes --- .coveragerc | 2 +- homeassistant/components/hdmi_cec.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6416ad3e2b43c4..0f9904d80a2611 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,7 +94,6 @@ omit = homeassistant/components/camera/generic.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/rpi_camera.py - homeassistant/components/cec.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py homeassistant/components/device_tracker/asuswrt.py @@ -115,6 +114,7 @@ omit = homeassistant/components/downloader.py homeassistant/components/feedreader.py homeassistant/components/garage_door/wink.py + homeassistant/components/hdmi_cec.py homeassistant/components/ifttt.py homeassistant/components/keyboard.py homeassistant/components/light/blinksticklight.py diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index f5a64b909af9e6..89cbe789c581e0 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -32,7 +32,7 @@ DOMAIN: vol.Schema({ vol.Required(CONF_DEVICES): DEVICE_SCHEMA }) -}) +}, extra=vol.ALLOW_EXTRA) def parse_mapping(mapping, parents=None): @@ -68,7 +68,7 @@ def setup(hass, config): # to physical address represented as a list of # four elements. flat = {} - for pair in parse_mapping(config[DOMAIN][0].get(CONF_DEVICES, {})): + for pair in parse_mapping(config[DOMAIN].get(CONF_DEVICES, {})): flat[pair[0]] = pad_physical_address(pair[1]) # Configure libcec. From aa3d0e10472e0d8c9dfcf6fb7f807afd62091007 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 22 Jun 2016 20:01:39 -0400 Subject: [PATCH 21/79] Fix incorrect check on presence of password and pub_key (#2355) This commit fixes an issue with the use of None in default values for the config get() calls in __init__() of AsusWrtDeviceScanner. These values are cast as strings and when a NoneType is cast it returns the string "None" this broke the check for the existence of these fields. This commit fixes the issue by changing the default value to be an empty string '' which will conform with the behavior expected by the ssh login code. Closes #2343 --- .../components/device_tracker/asuswrt.py | 4 +- .../components/device_tracker/test_asuswrt.py | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 282ae46ba8592e..725a49308be0c6 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -83,8 +83,8 @@ def __init__(self, config): """Initialize the scanner.""" self.host = config[CONF_HOST] self.username = str(config[CONF_USERNAME]) - self.password = str(config.get(CONF_PASSWORD)) - self.pub_key = str(config.get('pub_key')) + self.password = str(config.get(CONF_PASSWORD, "")) + self.pub_key = str(config.get('pub_key', "")) self.protocol = config.get('protocol') self.mode = config.get('mode') diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 210ce2c58faa4d..241e4a65a0f295 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -67,3 +67,68 @@ def test_get_scanner_with_pubkey_no_password(self, asuswrt_mock): self.assertIsNotNone(device_tracker.asuswrt.get_scanner( self.hass, conf_dict)) asuswrt_mock.assert_called_once_with(conf_dict[device_tracker.DOMAIN]) + + def test_ssh_login_with_pub_key(self): + """Test that login is done with pub_key when configured to.""" + ssh = mock.MagicMock() + ssh_mock = mock.patch('pexpect.pxssh.pxssh', return_value=ssh) + ssh_mock.start() + self.addCleanup(ssh_mock.stop) + conf_dict = { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + 'pub_key': '/fake_path' + } + update_mock = mock.patch( + 'homeassistant.components.device_tracker.asuswrt.' + 'AsusWrtDeviceScanner.get_asuswrt_data') + update_mock.start() + self.addCleanup(update_mock.stop) + asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) + asuswrt.ssh_connection() + ssh.login.assert_called_once_with('fake_host', 'fake_user', + ssh_key='/fake_path') + + def test_ssh_login_with_password(self): + """Test that login is done with password when configured to.""" + ssh = mock.MagicMock() + ssh_mock = mock.patch('pexpect.pxssh.pxssh', return_value=ssh) + ssh_mock.start() + self.addCleanup(ssh_mock.stop) + conf_dict = { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + CONF_PASSWORD: 'fake_pass' + } + update_mock = mock.patch( + 'homeassistant.components.device_tracker.asuswrt.' + 'AsusWrtDeviceScanner.get_asuswrt_data') + update_mock.start() + self.addCleanup(update_mock.stop) + asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) + asuswrt.ssh_connection() + ssh.login.assert_called_once_with('fake_host', 'fake_user', + 'fake_pass') + + def test_ssh_login_without_password_or_pubkey(self): + """Test that login is not called without password or pub_key.""" + ssh = mock.MagicMock() + ssh_mock = mock.patch('pexpect.pxssh.pxssh', return_value=ssh) + ssh_mock.start() + self.addCleanup(ssh_mock.stop) + conf_dict = { + CONF_PLATFORM: 'asuswrt', + CONF_HOST: 'fake_host', + CONF_USERNAME: 'fake_user', + } + update_mock = mock.patch( + 'homeassistant.components.device_tracker.asuswrt.' + 'AsusWrtDeviceScanner.get_asuswrt_data') + update_mock.start() + self.addCleanup(update_mock.stop) + asuswrt = device_tracker.asuswrt.AsusWrtDeviceScanner(conf_dict) + result = asuswrt.ssh_connection() + ssh.login.assert_not_called() + self.assertIsNone(result) From 12e26d25a57821f629a683ed5340850100ee25dc Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Thu, 23 Jun 2016 00:48:16 -0500 Subject: [PATCH 22/79] Bump to pyenvisalink 1.0 (#2358) --- homeassistant/components/envisalink.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index f1a7009e059517..23f9acef12f45e 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -12,7 +12,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.discovery import load_platform -REQUIREMENTS = ['pyenvisalink==0.9', 'pydispatcher==2.0.5'] +REQUIREMENTS = ['pyenvisalink==1.0', 'pydispatcher==2.0.5'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'envisalink' diff --git a/requirements_all.txt b/requirements_all.txt index e8a24fc2024ef1..a5ef32201c5c5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -252,7 +252,7 @@ pychromecast==0.7.2 pydispatcher==2.0.5 # homeassistant.components.envisalink -pyenvisalink==0.9 +pyenvisalink==1.0 # homeassistant.components.ifttt pyfttt==0.3 From 3349bdc2bda59d8589639292efa374c382a0d4b2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 23 Jun 2016 12:34:13 +0200 Subject: [PATCH 23/79] Log successful and failed login attempts (#2347) --- homeassistant/components/http.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 0baa7dd177cb1b..d7ce8e78013ea4 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -1,4 +1,9 @@ -"""This module provides WSGI application to serve the Home Assistant API.""" +""" +This module provides WSGI application to serve the Home Assistant API. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/http/ +""" import hmac import json import logging @@ -19,7 +24,7 @@ import homeassistant.helpers.config_validation as cv DOMAIN = "http" -REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5",) +REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5") CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" @@ -395,7 +400,12 @@ def handle_request(self, request, **values): self.hass.wsgi.api_password): authenticated = True - if self.requires_auth and not authenticated: + if authenticated: + _LOGGER.info('Successful login/request from %s', + request.remote_addr) + elif self.requires_auth and not authenticated: + _LOGGER.warning('Login attempt or request with an invalid' + 'password from %s', request.remote_addr) raise Unauthorized() request.authenticated = authenticated From 600a3e3965387be38211012fd7351121a17a433d Mon Sep 17 00:00:00 2001 From: Dale Higgs Date: Thu, 23 Jun 2016 10:47:56 -0500 Subject: [PATCH 24/79] Allow service data to be passed to shell_command (#2362) --- homeassistant/components/shell_command.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index dec518db6ea8b6..17ffad41f93f77 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -6,6 +6,7 @@ """ import logging import subprocess +import shlex import voluptuous as vol @@ -23,8 +24,6 @@ }), }, extra=vol.ALLOW_EXTRA) -SHELL_COMMAND_SCHEMA = vol.Schema({}) - def setup(hass, config): """Setup the shell_command component.""" @@ -44,8 +43,7 @@ def service_handler(call): _LOGGER.exception('Error running command: %s', cmd) for name in conf.keys(): - hass.services.register(DOMAIN, name, service_handler, - schema=SHELL_COMMAND_SCHEMA) + hass.services.register(DOMAIN, name, service_handler) return True @@ -64,6 +62,6 @@ def _parse_command(hass, cmd, variables): shell = True else: # template used. Must break into list and use shell=False for security - cmd = [prog] + rendered_args.split() + cmd = [prog] + shlex.split(rendered_args) shell = False return cmd, shell From 67a04c2a0eb1018024c89b728d962619cdbfa2c4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 24 Jun 2016 10:06:58 +0200 Subject: [PATCH 25/79] Initial clean import --- .coveragerc | 3 + .../components/binary_sensor/homematic.py | 164 ++++++ homeassistant/components/homematic.py | 528 ++++++++++++++++++ homeassistant/components/light/homematic.py | 112 ++++ .../components/rollershutter/homematic.py | 105 ++++ homeassistant/components/sensor/homematic.py | 119 ++++ homeassistant/components/switch/homematic.py | 111 ++++ .../components/thermostat/homematic.py | 209 ++----- requirements_all.txt | 3 + 9 files changed, 1201 insertions(+), 153 deletions(-) create mode 100644 homeassistant/components/binary_sensor/homematic.py create mode 100644 homeassistant/components/homematic.py create mode 100644 homeassistant/components/light/homematic.py create mode 100644 homeassistant/components/rollershutter/homematic.py create mode 100644 homeassistant/components/sensor/homematic.py create mode 100644 homeassistant/components/switch/homematic.py diff --git a/.coveragerc b/.coveragerc index a1b63cf05595e8..5932ed9a0d0017 100644 --- a/.coveragerc +++ b/.coveragerc @@ -84,6 +84,9 @@ omit = homeassistant/components/netatmo.py homeassistant/components/*/netatmo.py + homeassistant/components/homematic.py + homeassistant/components/*/homematic.py + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/binary_sensor/arest.py diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py new file mode 100644 index 00000000000000..fe50a5ef48c71a --- /dev/null +++ b/homeassistant/components/binary_sensor/homematic.py @@ -0,0 +1,164 @@ +""" +The homematic binary sensor platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration (single channel, simple device): + +binary_sensor: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + name: "" (optional) + + +Configuration (multiple channels, like motion detector with buttons): + +binary_sensor: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + param: (device-dependent) (optional) + button: n (integer of channel to map, device-dependent) (optional) + name: "" (optional) +binary_sensor: + - platform: homematic + ... +""" + +import logging +from homeassistant.const import STATE_UNKNOWN +from homeassistant.components.binary_sensor import BinarySensorDevice +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + +SENSOR_TYPES_CLASS = { + "Remote": None, + "ShutterContact": "opening", + "Smoke": "smoke", + "SmokeV2": "smoke", + "Motion": "motion", + "MotionV2": "motion", + "RemoteMotion": None +} + +SUPPORT_HM_EVENT_AS_BINMOD = [ + "PRESS_LONG", + "PRESS_SHORT" +] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMBinarySensor, + config, + add_callback_devices) + + +class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): + """Represents diverse binary Homematic units in Home Assistant.""" + + @property + def is_on(self): + """Return True if switch is on.""" + if not self.available: + return False + # no binary is defined, check all! + if self._state is None: + available_bin = self._create_binary_list_from_hm() + for binary in available_bin: + try: + if binary in self._data and self._data[binary] == 1: + return True + except (ValueError, TypeError): + _LOGGER.warning("%s datatype error!", self._name) + return False + + # single binary + return bool(self._hm_get_state()) + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + if not self.available: + return None + + # If state is MOTION (RemoteMotion works only) + if self._state in "MOTION": + return "motion" + return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.sensors import HMBinarySensor\ + as pyHMBinarySensor + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # check if the homematic device correct for this HA device + if not isinstance(self._hmdevice, pyHMBinarySensor): + _LOGGER.critical("This %s can't be use as binary!", self._name) + return False + + # load possible binary sensor + available_bin = self._create_binary_list_from_hm() + + # if exists user value? + if self._state and self._state not in available_bin: + _LOGGER.critical("This %s have no binary with %s!", self._name, + self._state) + return False + + # only check and give a warining to User + if self._state is None and len(available_bin) > 1: + _LOGGER.warning("%s have multible binary params. It use all " + + "binary nodes as one. Possible param values: %s", + self._name, str(available_bin)) + + return True + + def _init_data_struct(self): + """Generate a data struct (self._data) from hm metadata.""" + super()._init_data_struct() + + # load possible binary sensor + available_bin = self._create_binary_list_from_hm() + + # object have 1 binary + if self._state is None and len(available_bin) == 1: + for value in available_bin: + self._state = value + + # no binary is definit, use all binary for state + if self._state is None and len(available_bin) > 1: + for node in available_bin: + self._data.update({node: STATE_UNKNOWN}) + + # add state to data struct + if self._state: + _LOGGER.debug("%s init datastruct with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + + def _create_binary_list_from_hm(self): + """Generate a own metadata for binary_sensors.""" + bin_data = {} + if not self._hmdevice: + return bin_data + + # copy all data from BINARYNODE + bin_data.update(self._hmdevice.BINARYNODE) + + # copy all hm event they are supportet by this object + for event, channel in self._hmdevice.EVENTNODE.items(): + if event in SUPPORT_HM_EVENT_AS_BINMOD: + bin_data.update({event: channel}) + + return bin_data diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py new file mode 100644 index 00000000000000..751db436defb91 --- /dev/null +++ b/homeassistant/components/homematic.py @@ -0,0 +1,528 @@ +""" +Support for Homematic Devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematic/ + +Configuration: + +homematic: + local_ip: "" + local_port: + remote_ip: "" + remote_port: + autodetect: "" (optional, experimental, detect all devices) +""" +import time +import logging +from collections import OrderedDict +from homeassistant.const import EVENT_HOMEASSISTANT_STOP,\ + EVENT_PLATFORM_DISCOVERED,\ + ATTR_SERVICE,\ + ATTR_DISCOVERED,\ + STATE_UNKNOWN +from homeassistant.loader import get_component +from homeassistant.helpers.entity import Entity +import homeassistant.bootstrap + +DOMAIN = 'homematic' +REQUIREMENTS = ['pyhomematic==0.1.6'] + +HOMEMATIC = None +HOMEMATIC_DEVICES = {} +HOMEMATIC_AUTODETECT = False + +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_ROLLERSHUTTER = "homematic.rollershutter" +DISCOVER_THERMOSTATS = "homematic.thermostat" + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_DISCOVER_CONFIG = "config" + +HM_DEVICE_TYPES = { + DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"], + DISCOVER_LIGHTS: ["Dimmer"], + DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", + "RemoteMotion", "ThermostatWall", "AreaThermostat", + "RotaryHandleSensor", "GongSensor"], + DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], + DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", + "Motion", "MotionV2", "RemoteMotion"], + DISCOVER_ROLLERSHUTTER: ["Blind"] +} + +HM_IGNORE_DISCOVERY_NODE = [ + "ACTUAL_TEMPERATURE" +] + +HM_ATTRIBUTE_SUPPORT = { + "LOWBAT": ["Battery", {0: "High", 1: "Low"}], + "ERROR": ["Sabotage", {0: "No", 1: "Yes"}], + "RSSI_DEVICE": ["RSSI", {}], + "VALVE_STATE": ["Valve", {}], + "BATTERY_STATE": ["Battery", {}], + "CONTROL_MODE": ["Mode", {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost"}], + "POWER": ["Power", {}], + "CURRENT": ["Current", {}], + "VOLTAGE": ["Voltage", {}] +} + +_HM_DISCOVER_HASS = None +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup(hass, config): + """Setup the Homematic component.""" + global HOMEMATIC, HOMEMATIC_AUTODETECT, _HM_DISCOVER_HASS + + from pyhomematic import HMConnection + + local_ip = config[DOMAIN].get("local_ip", None) + local_port = config[DOMAIN].get("local_port", 8943) + remote_ip = config[DOMAIN].get("remote_ip", None) + remote_port = config[DOMAIN].get("remote_port", 2001) + autodetect = config[DOMAIN].get("autodetect", False) + + if remote_ip is None or local_ip is None: + _LOGGER.error("Missing remote CCU/Homegear or local address") + return False + + # Create server thread + HOMEMATIC_AUTODETECT = autodetect + _HM_DISCOVER_HASS = hass + HOMEMATIC = HMConnection(local=local_ip, + localport=local_port, + remote=remote_ip, + remoteport=remote_port, + systemcallback=system_callback_handler, + interface_id="homeassistant") + + # Start server thread, connect to peer, initialize to receive events + HOMEMATIC.start() + + # Stops server when Homeassistant is shutting down + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, HOMEMATIC.stop) + hass.config.components.append(DOMAIN) + + return True + + +# pylint: disable=too-many-branches +def system_callback_handler(src, *args): + """Callback handler.""" + if src == 'newDevices': + # pylint: disable=unused-variable + (interface_id, dev_descriptions) = args + key_dict = {} + # Get list of all keys of the devices (ignoring channels) + for dev in dev_descriptions: + key_dict[dev['ADDRESS'].split(':')[0]] = True + # Connect devices already created in HA to pyhomematic and + # add remaining devices to list + devices_not_created = [] + for dev in key_dict: + try: + if dev in HOMEMATIC_DEVICES: + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic() + else: + devices_not_created.append(dev) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed to setup device %s: %s", str(dev), + str(err)) + # If configuration allows autodetection of devices, + # all devices not configured are added. + if HOMEMATIC_AUTODETECT and devices_not_created: + for component_name, discovery_type in ( + ('switch', DISCOVER_SWITCHES), + ('light', DISCOVER_LIGHTS), + ('rollershutter', DISCOVER_ROLLERSHUTTER), + ('binary_sensor', DISCOVER_BINARY_SENSORS), + ('sensor', DISCOVER_SENSORS), + ('thermostat', DISCOVER_THERMOSTATS)): + # Get all devices of a specific type + try: + found_devices = _get_devices(discovery_type, + devices_not_created) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed generate opt %s with error '%s'", + component_name, str(err)) + + # When devices of this type are found + # they are setup in HA and an event is fired + if found_devices: + try: + component = get_component(component_name) + config = {component.DOMAIN: found_devices} + + # Ensure component is loaded + homeassistant.bootstrap.setup_component( + _HM_DISCOVER_HASS, + component.DOMAIN, + config) + + # Fire discovery event + _HM_DISCOVER_HASS.bus.fire( + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' + } + } + ) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed to autotetect %s with" + + "error '%s'", component_name, str(err)) + for dev in devices_not_created: + if dev in HOMEMATIC_DEVICES: + try: + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic() + # Need to wait, if you have a lot devices we don't + # to overload CCU/Homegear + time.sleep(1) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Failed link %s with" + + "error '%s'", dev, str(err)) + + +def _get_devices(device_type, keys): + """Get devices.""" + from homeassistant.components.binary_sensor.homematic import \ + SUPPORT_HM_EVENT_AS_BINMOD + + # run + device_arr = [] + if not keys: + keys = HOMEMATIC.devices + for key in keys: + device = HOMEMATIC.devices[key] + if device.__class__.__name__ in HM_DEVICE_TYPES[device_type]: + elements = device.ELEMENT + 1 + metadata = {} + + # Load metadata if needed to generate a param list + if device_type is DISCOVER_SENSORS: + metadata.update(device.SENSORNODE) + elif device_type is DISCOVER_BINARY_SENSORS: + metadata.update(device.BINARYNODE) + + # Also add supported events as binary type + for event, channel in device.EVENTNODE.items(): + if event in SUPPORT_HM_EVENT_AS_BINMOD: + metadata.update({event: channel}) + + params = _create_params_list(device, metadata) + + # Generate options for 1...n elements with 1...n params + for channel in range(1, elements): + for param in params[channel]: + name = _create_ha_name(name=device.NAME, + channel=channel, + param=param) + ordered_device_dict = OrderedDict() + ordered_device_dict["platform"] = "homematic" + ordered_device_dict["address"] = key + ordered_device_dict["name"] = name + ordered_device_dict["button"] = channel + if param is not None: + ordered_device_dict["param"] = param + + # Add new device + device_arr.append(ordered_device_dict) + _LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) + return device_arr + + +def _create_params_list(hmdevice, metadata): + """Create a list from HMDevice with all possible parameters in config.""" + params = {} + elements = hmdevice.ELEMENT + 1 + + # Search in sensor and binary metadata per elements + for channel in range(1, elements): + param_chan = [] + for node, meta_chan in metadata.items(): + # Is this attribute ignored? + if node in HM_IGNORE_DISCOVERY_NODE: + continue + if meta_chan == 'c' or meta_chan is None: + # Only channel linked data + param_chan.append(node) + elif channel == 1: + # First channel can have other data channel + param_chan.append(node) + + # Default parameter + if len(param_chan) == 0: + param_chan.append(None) + # Add to channel + params.update({channel: param_chan}) + + _LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS, + str(params)) + return params + + +def _create_ha_name(name, channel, param): + """Generate a unique object name.""" + # HMDevice is a simple device + if channel == 1 and param is None: + return name + + # Has multiple elements/channels + if channel > 1 and param is None: + return name + " " + str(channel) + + # With multiple param first elements + if channel == 1 and param is not None: + return name + " " + param + + # Multiple param on object with multiple elements + if channel > 1 and param is not None: + return name + " " + str(channel) + " " + param + + +def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): + """Helper to setup Homematic devices.""" + if HOMEMATIC is None: + _LOGGER.error('Error setting up HMDevice: Server not configured.') + return False + + address = config.get('address', None) + if address is None: + _LOGGER.error("Error setting up device '%s': " + + "'address' missing in configuration.", address) + return False + + # Create a new HA homematic object + new_device = hmdevicetype(config) + if address not in HOMEMATIC_DEVICES: + HOMEMATIC_DEVICES[address] = [] + HOMEMATIC_DEVICES[address].append(new_device) + + # Add to HA + add_callback_devices([new_device]) + return True + + +class HMDevice(Entity): + """Homematic device base object.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, config): + """Initialize generic HM device.""" + self._name = config.get("name", None) + self._address = config.get("address", None) + self._channel = config.get("button", 1) + self._state = config.get("param", None) + self._hidden = config.get("hidden", False) + self._data = {} + self._hmdevice = None + self._connected = False + self._available = False + + # Set param to uppercase + if self._state: + self._state = self._state.upper() + + # Generate name + if not self._name: + self._name = _create_ha_name(name=self._address, + channel=self._channel, + param=self._state) + + @property + def should_poll(self): + """Return False. Homematic states are pushed by the XML RPC Server.""" + return False + + @property + def name(self): + """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.""" + return self._available + + @property + def hidden(self): + """Return True if the entity should be hidden from UIs.""" + return self._hidden + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + # Generate an attributes list + for node, data in HM_ATTRIBUTE_SUPPORT.items(): + # Is an attributes and exists for this object + if node in self._data: + value = data[1].get(self._data[node], self._data[node]) + attr[data[0]] = value + + return attr + + def link_homematic(self): + """Connect to homematic.""" + # Does a HMDevice from pyhomematic exist? + if self._address in HOMEMATIC.devices: + # Init + self._hmdevice = HOMEMATIC.devices[self._address] + self._connected = True + + # Check if HM class is okay for HA class + _LOGGER.info("Start linking %s to %s", self._address, self._name) + if self._check_hm_to_ha_object(): + # Init datapoints of this object + self._init_data_struct() + self._load_init_data_from_hm() + _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) + + # Link events from pyhomatic + self._subscribe_homematic_events() + self._available = not self._hmdevice.UNREACH + else: + _LOGGER.critical("Delink %s object from HM!", self._name) + self._connected = False + self._available = False + + # Update HA + _LOGGER.debug("%s linking down, send update_ha_state", self._name) + self.update_ha_state() + + def _hm_event_callback(self, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + _LOGGER.debug("%s receive event '%s' value: %s", self._name, + attribute, value) + have_change = False + + # Is data needed for this instance? + if attribute in self._data: + # Did data change? + if self._data[attribute] != value: + self._data[attribute] = value + have_change = True + + # If available it has changed + if attribute is "UNREACH": + self._available = bool(value) + have_change = True + + # If it has changed, update HA + if have_change: + _LOGGER.debug("%s update_ha_state after '%s'", self._name, + attribute) + self.update_ha_state() + + # Reset events + if attribute in self._hmdevice.EVENTNODE: + _LOGGER.debug("%s reset event", self._name) + self._data[attribute] = False + self.update_ha_state() + + def _subscribe_homematic_events(self): + """Subscribe all required events to handle job.""" + channels_to_sub = {} + + # Push data to channels_to_sub from hmdevice metadata + for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE): + for node, channel in metadata.items(): + # Data is needed for this instance + if node in self._data: + # chan is current channel + if channel == 'c' or channel is None: + channel = self._channel + # Prepare for subscription + try: + if int(channel) > 0: + channels_to_sub.update({int(channel): True}) + except (ValueError, TypeError): + _LOGGER("Invalid channel in metadata from %s", + self._name) + + # Set callbacks + for channel in channels_to_sub: + _LOGGER.debug("Subscribe channel %s from %s", + str(channel), self._name) + self._hmdevice.setEventCallback(callback=self._hm_event_callback, + bequeath=False, + channel=channel) + + def _load_init_data_from_hm(self): + """Load first value from pyhomematic.""" + if not self._connected: + return False + + # Read data from pyhomematic + for metadata, funct in ( + (self._hmdevice.ATTRIBUTENODE, + self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData)): + for node in metadata: + if node in self._data: + self._data[node] = funct(name=node, channel=self._channel) + + # Set events to False + for node in self._hmdevice.EVENTNODE: + if node in self._data: + self._data[node] = False + + return True + + def _hm_set_state(self, value): + if self._state in self._data: + self._data[self._state] = value + + def _hm_get_state(self): + if self._state in self._data: + return self._data[self._state] + return None + + def _check_hm_to_ha_object(self): + """Check if it is possible to use the HM Object as this HA type. + + NEEDS overwrite by inherit! + """ + if not self._connected or self._hmdevice is None: + _LOGGER.error("HA object is not linked to homematic.") + return False + + # Check if button option is correctly set for this object + if self._channel > self._hmdevice.ELEMENT: + _LOGGER.critical("Button option is not correct for this object!") + return False + + return True + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata. + + NEEDS overwrite by inherit! + """ + # Add all attributes to data dict + for data_note in self._hmdevice.ATTRIBUTENODE: + self._data.update({data_note: STATE_UNKNOWN}) diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py new file mode 100644 index 00000000000000..94dabb0f00ab90 --- /dev/null +++ b/homeassistant/components/light/homematic.py @@ -0,0 +1,112 @@ +""" +The homematic light platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +light: + - platform: homematic + addresss: # e.g. "JEQ0XXXXXXX" + name: (optional) + button: n (integer of channel to map, device-dependent) +""" + +import logging +from homeassistant.components.light import (ATTR_BRIGHTNESS, Light) +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +# List of component names (string) your component depends upon. +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMLight, + config, + add_callback_devices) + + +class HMLight(homematic.HMDevice, Light): + """Represents a Homematic Light in Home Assistant.""" + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + if not self.available: + return None + # Is dimmer? + if self._state is "LEVEL": + return int(self._hm_get_state() * 255) + else: + return None + + @property + def is_on(self): + """Return True if light is on.""" + try: + return self._hm_get_state() > 0 + except TypeError: + return False + + def turn_on(self, **kwargs): + """Turn the light on.""" + if not self.available: + return + + if ATTR_BRIGHTNESS in kwargs and self._state is "LEVEL": + percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 + self._hmdevice.set_level(percent_bright, self._channel) + else: + self._hmdevice.on(self._channel) + + def turn_off(self, **kwargs): + """Turn the light off.""" + if self.available: + self._hmdevice.off(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if isinstance(self._hmdevice, Switch): + return True + if isinstance(self._hmdevice, Dimmer): + return True + + _LOGGER.critical("This %s can't be use as light!", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + super()._init_data_struct() + + # Use STATE + if isinstance(self._hmdevice, Switch): + self._state = "STATE" + + # Use LEVEL + if isinstance(self._hmdevice, Dimmer): + self._state = "LEVEL" + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init datadict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init light %s.", self._name) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py new file mode 100644 index 00000000000000..e0dd5e5469fc42 --- /dev/null +++ b/homeassistant/components/rollershutter/homematic.py @@ -0,0 +1,105 @@ +""" +The homematic rollershutter platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/rollershutter.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +rollershutter: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + name: "" (optional) +""" + +import logging +from homeassistant.const import (STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN) +from homeassistant.components.rollershutter import RollershutterDevice,\ + ATTR_CURRENT_POSITION +import homeassistant.components.homematic as homematic + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMRollershutter, + config, + add_callback_devices) + + +class HMRollershutter(homematic.HMDevice, RollershutterDevice): + """Represents a Homematic Rollershutter in Home Assistant.""" + + @property + def current_position(self): + """ + Return current position of rollershutter. + + None is unknown, 0 is closed, 100 is fully open. + """ + if self.available: + return int((1 - self._hm_get_state()) * 100) + return None + + def position(self, **kwargs): + """Move to a defined position: 0 (closed) and 100 (open).""" + if self.available: + if ATTR_CURRENT_POSITION in kwargs: + position = float(kwargs[ATTR_CURRENT_POSITION]) + position = min(100, max(0, position)) + level = (100 - position) / 100.0 + self._hmdevice.set_level(level, self._channel) + + @property + def state(self): + """Return the state of the rollershutter.""" + current = self.current_position + if current is None: + return STATE_UNKNOWN + + return STATE_CLOSED if current == 100 else STATE_OPEN + + def move_up(self, **kwargs): + """Move the rollershutter up.""" + if self.available: + self._hmdevice.move_up(self._channel) + + def move_down(self, **kwargs): + """Move the rollershutter down.""" + if self.available: + self._hmdevice.move_down(self._channel) + + def stop(self, **kwargs): + """Stop the device if in motion.""" + if self.available: + self._hmdevice.stop(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.actors import Blind + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if isinstance(self._hmdevice, Blind): + return True + + _LOGGER.critical("This %s can't be use as rollershutter!", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + # Add state to data dict + self._state = "LEVEL" + self._data.update({self._state: STATE_UNKNOWN}) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py new file mode 100644 index 00000000000000..52ece78f59e493 --- /dev/null +++ b/homeassistant/components/sensor/homematic.py @@ -0,0 +1,119 @@ +""" +The homematic sensor platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +sensor: + - platform: homematic + address: # e.g. "JEQ0XXXXXXX" + name: (optional) + param: (optional) +""" + +import logging +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + +HM_STATE_HA_CAST = { + "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"} +} + +HM_UNIT_HA_CAST = { + "HUMIDITY": "%", + "TEMPERATURE": "°C", + "BRIGHTNESS": "#", + "POWER": "W", + "CURRENT": "mA", + "VOLTAGE": "V", + "ENERGY_COUNTER": "Wh" +} + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMSensor, + config, + add_callback_devices) + + +class HMSensor(homematic.HMDevice): + """Represents various Homematic sensors in Home Assistant.""" + + @property + def state(self): + """Return the state of the sensor.""" + if not self.available: + return STATE_UNKNOWN + + # Does a cast exist for this class? + name = self._hmdevice.__class__.__name__ + if name in HM_STATE_HA_CAST: + return HM_STATE_HA_CAST[name].get(self._hm_get_state(), None) + + # No cast, return original value + return self._hm_get_state() + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + if not self.available: + return None + + return HM_UNIT_HA_CAST.get(self._state, None) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.sensors import HMSensor as pyHMSensor + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if not isinstance(self._hmdevice, pyHMSensor): + _LOGGER.critical("This %s can't be use as sensor!", self._name) + return False + + # Does user defined value exist? + if self._state and self._state not in self._hmdevice.SENSORNODE: + # pylint: disable=logging-too-many-args + _LOGGER.critical("This %s have no sensor with %s! Values are", + self._name, self._state, + str(self._hmdevice.SENSORNODE.keys())) + return False + + # No param is set and more than 1 sensor nodes are present + if self._state is None and len(self._hmdevice.SENSORNODE) > 1: + _LOGGER.critical("This %s has multiple sensor nodes. " + + "Please us param. Values are: %s", self._name, + str(self._hmdevice.SENSORNODE.keys())) + return False + + _LOGGER.debug("%s is okay for linking", self._name) + return True + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + if self._state is None and len(self._hmdevice.SENSORNODE) == 1: + for value in self._hmdevice.SENSORNODE: + self._state = value + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init datadict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init sensor %s.", self._name) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py new file mode 100644 index 00000000000000..5a630f43022f0a --- /dev/null +++ b/homeassistant/components/switch/homematic.py @@ -0,0 +1,111 @@ +""" +The homematic switch platform. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +switch: + - platform: homematic + address: # e.g. "JEQ0XXXXXXX" + name: (optional) + button: n (integer of channel to map, device-dependent) (optional) +""" + +import logging +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_UNKNOWN +import homeassistant.components.homematic as homematic + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMSwitch, + config, + add_callback_devices) + + +class HMSwitch(homematic.HMDevice, SwitchDevice): + """Represents a Homematic Switch in Home Assistant.""" + + @property + def is_on(self): + """Return True if switch is on.""" + try: + return self._hm_get_state() > 0 + except TypeError: + return False + + @property + def current_power_mwh(self): + """Return the current power usage in mWh.""" + if "ENERGY_COUNTER" in self._data: + try: + return self._data["ENERGY_COUNTER"] / 1000 + except ZeroDivisionError: + return 0 + + return None + + def turn_on(self, **kwargs): + """Turn the switch on.""" + if self.available: + self._hmdevice.on(self._channel) + + def turn_off(self, **kwargs): + """Turn the switch off.""" + if self.available: + self._hmdevice.off(self._channel) + + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.actors import Dimmer, Switch + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device is correct for this HA device + if isinstance(self._hmdevice, Switch): + return True + if isinstance(self._hmdevice, Dimmer): + return True + + _LOGGER.critical("This %s can't be use as switch!", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + from pyhomematic.devicetypes.actors import Dimmer,\ + Switch, SwitchPowermeter + + super()._init_data_struct() + + # Use STATE + if isinstance(self._hmdevice, Switch): + self._state = "STATE" + + # Use LEVEL + if isinstance(self._hmdevice, Dimmer): + self._state = "LEVEL" + + # Need sensor values for SwitchPowermeter + if isinstance(self._hmdevice, SwitchPowermeter): + for node in self._hmdevice.SENSORNODE: + self._data.update({node: STATE_UNKNOWN}) + + # Add state to data dict + if self._state: + _LOGGER.debug("%s init data dict with main node '%s'", self._name, + self._state) + self._data.update({self._state: STATE_UNKNOWN}) + else: + _LOGGER.critical("Can't correctly init light %s.", self._name) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index b4ecc6c166b922..a1ed06bc4bd300 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -1,121 +1,40 @@ """ -Support for Homematic (HM-TC-IT-WM-W-EU, HM-CC-RT-DN) thermostats. +The Homematic thermostat platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.homematic/ + +Important: For this platform to work the homematic component has to be +properly configured. + +Configuration: + +thermostat: + - platform: homematic + address: "" # e.g. "JEQ0XXXXXXX" + name: "" (optional) """ -import logging -import socket -from xmlrpc.client import ServerProxy -from xmlrpc.client import Error -from collections import namedtuple +import logging +import homeassistant.components.homematic as homematic from homeassistant.components.thermostat import ThermostatDevice -from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.temperature import convert +from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN -REQUIREMENTS = [] +DEPENDENCIES = ['homematic'] _LOGGER = logging.getLogger(__name__) -CONF_ADDRESS = 'address' -CONF_DEVICES = 'devices' -CONF_ID = 'id' -PROPERTY_SET_TEMPERATURE = 'SET_TEMPERATURE' -PROPERTY_VALVE_STATE = 'VALVE_STATE' -PROPERTY_ACTUAL_TEMPERATURE = 'ACTUAL_TEMPERATURE' -PROPERTY_BATTERY_STATE = 'BATTERY_STATE' -PROPERTY_LOWBAT = 'LOWBAT' -PROPERTY_CONTROL_MODE = 'CONTROL_MODE' -PROPERTY_BURST_MODE = 'BURST_RX' -TYPE_HM_THERMOSTAT = 'HOMEMATIC_THERMOSTAT' -TYPE_HM_WALLTHERMOSTAT = 'HOMEMATIC_WALLTHERMOSTAT' -TYPE_MAX_THERMOSTAT = 'MAX_THERMOSTAT' - -HomematicConfig = namedtuple('HomematicConfig', - ['device_type', - 'platform_type', - 'channel', - 'maint_channel']) - -HM_TYPE_MAPPING = { - 'HM-CC-RT-DN': HomematicConfig('HM-CC-RT-DN', - TYPE_HM_THERMOSTAT, - 4, 4), - 'HM-CC-RT-DN-BoM': HomematicConfig('HM-CC-RT-DN-BoM', - TYPE_HM_THERMOSTAT, - 4, 4), - 'HM-TC-IT-WM-W-EU': HomematicConfig('HM-TC-IT-WM-W-EU', - TYPE_HM_WALLTHERMOSTAT, - 2, 2), - 'BC-RT-TRX-CyG': HomematicConfig('BC-RT-TRX-CyG', - TYPE_MAX_THERMOSTAT, - 1, 0), - 'BC-RT-TRX-CyG-2': HomematicConfig('BC-RT-TRX-CyG-2', - TYPE_MAX_THERMOSTAT, - 1, 0), - 'BC-RT-TRX-CyG-3': HomematicConfig('BC-RT-TRX-CyG-3', - TYPE_MAX_THERMOSTAT, - 1, 0) -} - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Homematic thermostat.""" - devices = [] - try: - address = config[CONF_ADDRESS] - homegear = ServerProxy(address) - - for name, device_cfg in config[CONF_DEVICES].items(): - # get device description to detect the type - device_type = homegear.getDeviceDescription( - device_cfg[CONF_ID] + ':-1')['TYPE'] - - if device_type in HM_TYPE_MAPPING.keys(): - devices.append(HomematicThermostat( - HM_TYPE_MAPPING[device_type], - address, - device_cfg[CONF_ID], - name)) - else: - raise ValueError( - "Device Type '{}' currently not supported".format( - device_type)) - except socket.error: - _LOGGER.exception("Connection error to homematic web service") - return False - - add_devices(devices) - - return True - -# pylint: disable=too-many-instance-attributes -class HomematicThermostat(ThermostatDevice): - """Representation of a Homematic thermostat.""" +def setup_platform(hass, config, add_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMThermostat, + config, + add_callback_devices) - def __init__(self, hm_config, address, _id, name): - """Initialize the thermostat.""" - self._hm_config = hm_config - self.address = address - self._id = _id - self._name = name - self._full_device_name = '{}:{}'.format(self._id, - self._hm_config.channel) - self._maint_device_name = '{}:{}'.format(self._id, - self._hm_config.maint_channel) - self._current_temperature = None - self._target_temperature = None - self._valve = None - self._battery = None - self._mode = None - self.update() - @property - def name(self): - """Return the name of the Homematic device.""" - return self._name +class HMThermostat(homematic.HMDevice, ThermostatDevice): + """Represents a Homematic Thermostat in Home Assistant.""" @property def unit_of_measurement(self): @@ -125,26 +44,22 @@ def unit_of_measurement(self): @property def current_temperature(self): """Return the current temperature.""" - return self._current_temperature + if not self.available: + return None + return self._data["ACTUAL_TEMPERATURE"] @property def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature + """Return the target temperature.""" + if not self.available: + return None + return self._data["SET_TEMPERATURE"] def set_temperature(self, temperature): """Set new target temperature.""" - device = ServerProxy(self.address) - device.setValue(self._full_device_name, - PROPERTY_SET_TEMPERATURE, - temperature) - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return {"valve": self._valve, - "battery": self._battery, - "mode": self._mode} + if not self.available: + return None + self._hmdevice.set_temperature(temperature) @property def min_temp(self): @@ -156,39 +71,27 @@ def max_temp(self): """Return the maximum temperature - 30.5 means on.""" return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) - def update(self): - """Update the data from the thermostat.""" - try: - device = ServerProxy(self.address) - self._current_temperature = device.getValue( - self._full_device_name, - PROPERTY_ACTUAL_TEMPERATURE) - self._target_temperature = device.getValue( - self._full_device_name, - PROPERTY_SET_TEMPERATURE) - self._valve = device.getValue( - self._full_device_name, - PROPERTY_VALVE_STATE) - self._mode = device.getValue( - self._full_device_name, - PROPERTY_CONTROL_MODE) - - if self._hm_config.platform_type in [TYPE_HM_THERMOSTAT, - TYPE_HM_WALLTHERMOSTAT]: - self._battery = device.getValue(self._maint_device_name, - PROPERTY_BATTERY_STATE) - elif self._hm_config.platform_type == TYPE_MAX_THERMOSTAT: - # emulate homematic battery voltage, - # max reports lowbat if voltage < 2.2V - # while homematic battery_state should - # be between 1.5V and 4.6V - lowbat = device.getValue(self._maint_device_name, - PROPERTY_LOWBAT) - if lowbat: - self._battery = 1.5 - else: - self._battery = 4.6 - - except Error: - _LOGGER.exception("Did not receive any temperature data from the " - "homematic API.") + def _check_hm_to_ha_object(self): + """Check if possible to use the HM Object as this HA type.""" + from pyhomematic.devicetypes.thermostats import HMThermostat\ + as pyHMThermostat + + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False + + # Check if the homematic device correct for this HA device + if isinstance(self._hmdevice, pyHMThermostat): + return True + + _LOGGER.critical("This %s can't be use as thermostat", self._name) + return False + + def _init_data_struct(self): + """Generate a data dict (self._data) from hm metadata.""" + super()._init_data_struct() + + # Add state to data dict + self._data.update({"CONTROL_MODE": STATE_UNKNOWN, + "SET_TEMPERATURE": STATE_UNKNOWN, + "ACTUAL_TEMPERATURE": STATE_UNKNOWN}) diff --git a/requirements_all.txt b/requirements_all.txt index a5ef32201c5c5c..f3670ea35da6d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -257,6 +257,9 @@ pyenvisalink==1.0 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.homematic +pyhomematic==0.1.6 + # homeassistant.components.device_tracker.icloud pyicloud==0.8.3 From ec8dc25c9cb3c053662ad35de1b178dcda2fecbb Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Fri, 24 Jun 2016 17:44:24 +0200 Subject: [PATCH 26/79] Zwave garagedoor (#2361) * First go at zwave Garage door * Refactor of zwave discovery * Allaround fixes for rollershutter and garage door --- homeassistant/components/garage_door/zwave.py | 70 ++++++++++ .../components/rollershutter/zwave.py | 20 +-- homeassistant/components/zwave.py | 132 +++++++++++++----- 3 files changed, 169 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/garage_door/zwave.py diff --git a/homeassistant/components/garage_door/zwave.py b/homeassistant/components/garage_door/zwave.py new file mode 100644 index 00000000000000..18a2ea96b86db4 --- /dev/null +++ b/homeassistant/components/garage_door/zwave.py @@ -0,0 +1,70 @@ +""" +Support for Zwave garage door components. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/garagedoor.zwave/ +""" +# Because we do not compile openzwave on CI +# pylint: disable=import-error +import logging +from homeassistant.components.garage_door import DOMAIN +from homeassistant.components.zwave import ZWaveDeviceEntity +from homeassistant.components import zwave +from homeassistant.components.garage_door import GarageDoorDevice + +COMMAND_CLASS_SWITCH_BINARY = 0x25 # 37 + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return Z-Wave garage door device.""" + if discovery_info is None or zwave.NETWORK is None: + return + + node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] + value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] + + if value.command_class != zwave.COMMAND_CLASS_SWITCH_BINARY: + return + if value.type != zwave.TYPE_BOOL: + return + if value.genre != zwave.GENRE_USER: + return + + value.set_change_verified(False) + add_devices([ZwaveGarageDoor(value)]) + + +class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice): + """Representation of an Zwave garage door device.""" + + def __init__(self, value): + """Initialize the zwave garage door.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + ZWaveDeviceEntity.__init__(self, value, DOMAIN) + self._node = value.node + self._state = value.data + dispatcher.connect( + self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.node == value.node: + self._state = value.data + self.update_ha_state(True) + _LOGGER.debug("Value changed on network %s", value) + + @property + def is_closed(self): + """Return the current position of Zwave garage door.""" + return self._state + + def close_door(self): + """Close the garage door.""" + self._value.node.set_switch(self._value.value_id, False) + + def open_door(self): + """Open the garage door.""" + self._value.node.set_switch(self._value.value_id, True) diff --git a/homeassistant/components/rollershutter/zwave.py b/homeassistant/components/rollershutter/zwave.py index 45928d1bfb41d7..288488fe057edd 100644 --- a/homeassistant/components/rollershutter/zwave.py +++ b/homeassistant/components/rollershutter/zwave.py @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if value.command_class != zwave.COMMAND_CLASS_SWITCH_MULTILEVEL: return - if value.index != 1: + if value.index != 0: return value.set_change_verified(False) @@ -56,26 +56,15 @@ def value_changed(self, value): @property def current_position(self): """Return the current position of Zwave roller shutter.""" - for value in self._node.get_values( - class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == 38 and value.index == 0: - return value.data + return self._value.data def move_up(self, **kwargs): """Move the roller shutter up.""" - for value in self._node.get_values( - class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == 38 and value.index == 0: - value.data = 255 - break + self._node.set_dimmer(self._value.value_id, 0) def move_down(self, **kwargs): """Move the roller shutter down.""" - for value in self._node.get_values( - class_id=COMMAND_CLASS_SWITCH_MULTILEVEL).values(): - if value.command_class == 38 and value.index == 0: - value.data = 0 - break + self._node.set_dimmer(self._value.value_id, 100) def stop(self, **kwargs): """Stop the roller shutter.""" @@ -84,3 +73,4 @@ def stop(self, **kwargs): # Rollershutter will toggle between UP (True), DOWN (False). # It also stops the shutter if the same value is sent while moving. value.data = value.data + break diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 36bf01634243d0..98a24240a00eeb 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -39,23 +39,39 @@ EVENT_SCENE_ACTIVATED = "zwave.scene_activated" -COMMAND_CLASS_SWITCH_MULTILEVEL = 38 -COMMAND_CLASS_DOOR_LOCK = 98 -COMMAND_CLASS_SWITCH_BINARY = 37 -COMMAND_CLASS_SENSOR_BINARY = 48 +COMMAND_CLASS_WHATEVER = None COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_METER = 50 +COMMAND_CLASS_ALARM = 113 +COMMAND_CLASS_SWITCH_BINARY = 37 +COMMAND_CLASS_SENSOR_BINARY = 48 +COMMAND_CLASS_SWITCH_MULTILEVEL = 38 +COMMAND_CLASS_DOOR_LOCK = 98 +COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 +COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 COMMAND_CLASS_BATTERY = 128 -COMMAND_CLASS_ALARM = 113 # 0x71 -COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43 -COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 # 0x44 + +GENERIC_COMMAND_CLASS_WHATEVER = None +GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH = 17 +GENERIC_COMMAND_CLASS_BINARY_SWITCH = 16 +GENERIC_COMMAND_CLASS_ENTRY_CONTROL = 64 +GENERIC_COMMAND_CLASS_BINARY_SENSOR = 32 +GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR = 33 +GENERIC_COMMAND_CLASS_METER = 49 +GENERIC_COMMAND_CLASS_ALARM_SENSOR = 161 +GENERIC_COMMAND_CLASS_THERMOSTAT = 8 SPECIFIC_DEVICE_CLASS_WHATEVER = None +SPECIFIC_DEVICE_CLASS_NOT_USED = 0 SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH = 1 +SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK = 2 SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR = 3 +SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK = 3 SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE = 4 +SPECIFIC_DEVICE_CLASS_SECURE_DOOR = 5 SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A = 5 SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B = 6 +SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON = 7 SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C = 7 GENRE_WHATEVER = None @@ -71,51 +87,67 @@ # value type, genre type, specific device class). DISCOVERY_COMPONENTS = [ ('sensor', + [GENERIC_COMMAND_CLASS_WHATEVER], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SENSOR_MULTILEVEL, COMMAND_CLASS_METER, COMMAND_CLASS_ALARM], TYPE_WHATEVER, - GENRE_USER, - SPECIFIC_DEVICE_CLASS_WHATEVER), + GENRE_USER), ('light', + [GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH], + [SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH, + SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE], [COMMAND_CLASS_SWITCH_MULTILEVEL], TYPE_BYTE, - GENRE_USER, - [SPECIFIC_DEVICE_CLASS_MULTILEVEL_POWER_SWITCH, - SPECIFIC_DEVICE_CLASS_MULTILEVEL_SCENE]), + GENRE_USER), ('switch', + [GENERIC_COMMAND_CLASS_BINARY_SWITCH], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SWITCH_BINARY], TYPE_BOOL, - GENRE_USER, - SPECIFIC_DEVICE_CLASS_WHATEVER), + GENRE_USER), ('binary_sensor', + [GENERIC_COMMAND_CLASS_BINARY_SENSOR], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SENSOR_BINARY], TYPE_BOOL, - GENRE_USER, - SPECIFIC_DEVICE_CLASS_WHATEVER), + GENRE_USER), ('thermostat', + [GENERIC_COMMAND_CLASS_THERMOSTAT], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, - GENRE_WHATEVER, - SPECIFIC_DEVICE_CLASS_WHATEVER), + GENRE_WHATEVER), ('hvac', + [GENERIC_COMMAND_CLASS_THERMOSTAT], + [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_THERMOSTAT_FAN_MODE], TYPE_WHATEVER, - GENRE_WHATEVER, - SPECIFIC_DEVICE_CLASS_WHATEVER), + GENRE_WHATEVER), ('lock', + [GENERIC_COMMAND_CLASS_ENTRY_CONTROL], + [SPECIFIC_DEVICE_CLASS_ADVANCED_DOOR_LOCK, + SPECIFIC_DEVICE_CLASS_SECURE_KEYPAD_DOOR_LOCK], [COMMAND_CLASS_DOOR_LOCK], TYPE_BOOL, - GENRE_USER, - SPECIFIC_DEVICE_CLASS_WHATEVER), + GENRE_USER), ('rollershutter', - [COMMAND_CLASS_SWITCH_MULTILEVEL], - TYPE_WHATEVER, - GENRE_USER, + [GENERIC_COMMAND_CLASS_MULTILEVEL_SWITCH], [SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_A, SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_B, SPECIFIC_DEVICE_CLASS_MOTOR_CONTROL_CLASS_C, - SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR]), + SPECIFIC_DEVICE_CLASS_MULTIPOSITION_MOTOR], + [COMMAND_CLASS_WHATEVER], + TYPE_WHATEVER, + GENRE_USER), + ('garage_door', + [GENERIC_COMMAND_CLASS_ENTRY_CONTROL], + [SPECIFIC_DEVICE_CLASS_SECURE_BARRIER_ADD_ON, + SPECIFIC_DEVICE_CLASS_SECURE_DOOR], + [COMMAND_CLASS_SWITCH_BINARY], + TYPE_BOOL, + GENRE_USER) ] @@ -244,25 +276,49 @@ def log_all(signal, value=None): def value_added(node, value): """Called when a value is added to a node on the network.""" for (component, - command_ids, + generic_device_class, + specific_device_class, + command_class, value_type, - value_genre, - specific_device_class) in DISCOVERY_COMPONENTS: - - if value.command_class not in command_ids: + value_genre) in DISCOVERY_COMPONENTS: + + _LOGGER.debug("Component=%s Node_id=%s query start", + component, node.node_id) + if node.generic not in generic_device_class and \ + None not in generic_device_class: + _LOGGER.debug("node.generic %s not None and in \ + generic_device_class %s", + node.generic, generic_device_class) + continue + if node.specific not in specific_device_class and \ + None not in specific_device_class: + _LOGGER.debug("node.specific %s is not None and in \ + specific_device_class %s", node.specific, + specific_device_class) continue - if value_type is not None and value_type != value.type: + if value.command_class not in command_class and \ + None not in command_class: + _LOGGER.debug("value.command_class %s is not None \ + and in command_class %s", + value.command_class, command_class) continue - if value_genre is not None and value_genre != value.genre: + if value_type != value.type and value_type is not None: + _LOGGER.debug("value.type %s != value_type %s", + value.type, value_type) continue - if specific_device_class is not None and \ - specific_device_class != node.specific: + if value_genre != value.genre and value_genre is not None: + _LOGGER.debug("value.genre %s != value_genre %s", + value.genre, value_genre) continue # Configure node - _LOGGER.debug("Node_id=%s Value type=%s Genre=%s \ - Specific Device_class=%s", node.node_id, - value.type, value.genre, specific_device_class) + _LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, \ + Specific_command_class=%s, \ + Command_class=%s, Value type=%s, \ + Genre=%s", node.node_id, + node.generic, node.specific, + value.command_class, value.type, + value.genre) name = "{}.{}".format(component, _object_id(value)) node_config = customize.get(name, {}) From dfe1b8d9344fbed665896bf6c2b65de8fbb5f280 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Fri, 24 Jun 2016 19:46:42 +0200 Subject: [PATCH 27/79] Fixed minor feature-detection bug with incomplet configuration --- homeassistant/components/binary_sensor/homematic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index fe50a5ef48c71a..d2005f99ba54a9 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -89,7 +89,7 @@ def sensor_class(self): return None # If state is MOTION (RemoteMotion works only) - if self._state in "MOTION": + if self._state == "MOTION": return "motion" return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) From c6161154193b6dbeba65805fed30a0d1c5152715 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 25 Jun 2016 06:22:10 +0200 Subject: [PATCH 28/79] rpi_gpi garage_door controller (#2369) --- .coveragerc | 1 + .../components/garage_door/rpi_gpio.py | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 homeassistant/components/garage_door/rpi_gpio.py diff --git a/.coveragerc b/.coveragerc index a1b63cf05595e8..da463380733fe6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -114,6 +114,7 @@ omit = homeassistant/components/downloader.py homeassistant/components/feedreader.py homeassistant/components/garage_door/wink.py + homeassistant/components/garage_door/rpi_gpio.py homeassistant/components/ifttt.py homeassistant/components/keyboard.py homeassistant/components/light/blinksticklight.py diff --git a/homeassistant/components/garage_door/rpi_gpio.py b/homeassistant/components/garage_door/rpi_gpio.py new file mode 100644 index 00000000000000..6a50ffb408d7f5 --- /dev/null +++ b/homeassistant/components/garage_door/rpi_gpio.py @@ -0,0 +1,96 @@ +""" +Support for building a Raspberry Pi garage controller in HA. + +Instructions for building the controller can be found here +https://github.com/andrewshilliday/garage-door-controller + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/garage_door.rpi_gpio/ +""" + +import logging +from time import sleep +import voluptuous as vol +from homeassistant.components.garage_door import GarageDoorDevice +import homeassistant.components.rpi_gpio as rpi_gpio +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['rpi_gpio'] + +_LOGGER = logging.getLogger(__name__) + +_DOORS_SCHEMA = vol.All( + cv.ensure_list, + [ + vol.Schema({ + 'name': str, + 'relay_pin': int, + 'state_pin': int, + }) + ] +) +PLATFORM_SCHEMA = vol.Schema({ + 'platform': str, + vol.Required('doors'): _DOORS_SCHEMA, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the garage door platform.""" + doors = [] + doors_conf = config.get('doors') + + for door in doors_conf: + doors.append(RPiGPIOGarageDoor(door['name'], door['relay_pin'], + door['state_pin'])) + add_devices(doors) + + +class RPiGPIOGarageDoor(GarageDoorDevice): + """Representation of a Raspberry garage door.""" + + def __init__(self, name, relay_pin, state_pin): + """Initialize the garage door.""" + self._name = name + self._state = False + self._relay_pin = relay_pin + self._state_pin = state_pin + rpi_gpio.setup_output(self._relay_pin) + rpi_gpio.setup_input(self._state_pin, 'DOWN') + rpi_gpio.write_output(self._relay_pin, True) + + @property + def unique_id(self): + """Return the ID of this garage door.""" + return "{}.{}".format(self.__class__, self._name) + + @property + def name(self): + """Return the name of the garage door if any.""" + return self._name + + def update(self): + """Update the state of the garage door.""" + self._state = rpi_gpio.read_input(self._state_pin) is True + + @property + def is_closed(self): + """Return true if door is closed.""" + return self._state + + def _trigger(self): + """Trigger the door.""" + rpi_gpio.write_output(self._relay_pin, False) + sleep(0.2) + rpi_gpio.write_output(self._relay_pin, True) + + def close_door(self): + """Close the door.""" + if not self.is_closed: + self._trigger() + + def open_door(self): + """Open the door.""" + if self.is_closed: + self._trigger() From 68df3deee038daefdc3114fbaf3abaee4893fce7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 24 Jun 2016 21:27:40 -0700 Subject: [PATCH 29/79] ABC consistent not implemented behavior (#2359) --- homeassistant/components/hvac/__init__.py | 18 ++++++++-------- homeassistant/components/hvac/zwave.py | 2 +- homeassistant/components/light/__init__.py | 3 ++- .../components/light/limitlessled.py | 1 + homeassistant/components/light/mysensors.py | 1 + homeassistant/components/switch/__init__.py | 2 +- .../components/switch/acer_projector.py | 21 ++++++++++++------- homeassistant/components/switch/arest.py | 1 + .../components/switch/wake_on_lan.py | 4 ++++ .../components/thermostat/__init__.py | 14 ++++++------- homeassistant/components/thermostat/demo.py | 2 +- homeassistant/components/thermostat/ecobee.py | 2 +- .../components/thermostat/eq3btsmart.py | 2 +- .../components/thermostat/heat_control.py | 3 +-- .../components/thermostat/heatmiser.py | 2 +- .../components/thermostat/homematic.py | 2 +- .../components/thermostat/honeywell.py | 5 ++--- homeassistant/components/thermostat/nest.py | 1 + .../components/thermostat/proliphix.py | 1 + .../components/thermostat/radiotherm.py | 1 + homeassistant/components/thermostat/zwave.py | 1 + homeassistant/helpers/entity.py | 8 +++---- 22 files changed, 56 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/hvac/__init__.py b/homeassistant/components/hvac/__init__.py index 85d10671a17cac..560f3d13fd6038 100644 --- a/homeassistant/components/hvac/__init__.py +++ b/homeassistant/components/hvac/__init__.py @@ -425,39 +425,39 @@ def swing_list(self): def set_temperature(self, temperature): """Set new target temperature.""" - pass + raise NotImplementedError() def set_humidity(self, humidity): """Set new target humidity.""" - pass + raise NotImplementedError() def set_fan_mode(self, fan): """Set new target fan mode.""" - pass + raise NotImplementedError() def set_operation_mode(self, operation_mode): """Set new target operation mode.""" - pass + raise NotImplementedError() def set_swing_mode(self, swing_mode): """Set new target swing operation.""" - pass + raise NotImplementedError() def turn_away_mode_on(self): """Turn away mode on.""" - pass + raise NotImplementedError() def turn_away_mode_off(self): """Turn away mode off.""" - pass + raise NotImplementedError() def turn_aux_heat_on(self): """Turn auxillary heater on.""" - pass + raise NotImplementedError() def turn_aux_heat_off(self): """Turn auxillary heater off.""" - pass + raise NotImplementedError() @property def min_temp(self): diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py index 5becb53b98a648..c950200932a5ce 100755 --- a/homeassistant/components/hvac/zwave.py +++ b/homeassistant/components/hvac/zwave.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): discovery_info, zwave.NETWORK) -# pylint: disable=too-many-arguments +# pylint: disable=too-many-arguments, abstract-method class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): """Represents a HeatControl hvac.""" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 27c68819909e09..2b0af395d02f4e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -248,7 +248,8 @@ def handle_light_service(service): class Light(ToggleEntity): """Representation of a light.""" - # pylint: disable=no-self-use + # pylint: disable=no-self-use, abstract-method + @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 5242746dc4256a..010088af824a04 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.limitlessled/ """ +# pylint: disable=abstract-method import logging from homeassistant.components.light import ( diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 04c0942e1f6a6f..d8d288afd0e6dd 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mysensors/ """ +# pylint: disable=abstract-method import logging from homeassistant.components import mysensors diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 1f92b458d53e9b..60b9c9fdcd8a49 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -108,7 +108,7 @@ def handle_switch_service(service): class SwitchDevice(ToggleEntity): """Representation of a switch.""" - # pylint: disable=no-self-use + # pylint: disable=no-self-use, abstract-method @property def current_power_mwh(self): """Return the current power usage in mWh.""" diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py index e6e1eb76b75641..7a1f3498f18ea9 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -4,7 +4,6 @@ This component allows to control almost all projectors from acer using their RS232 serial communication protocol. """ - import logging import re @@ -61,7 +60,8 @@ def __init__(self, serial_port, name='Projector', write_timeout=write_timeout, **kwargs) self._serial_port = serial_port self._name = name - self._state = STATE_UNKNOWN + self._state = False + self._available = False self._attributes = { LAMP_HOURS: STATE_UNKNOWN, INPUT_SOURCE: STATE_UNKNOWN, @@ -100,14 +100,19 @@ def _write_read_format(self, msg): return match.group(1) return STATE_UNKNOWN + @property + def available(self): + """Return if projector is available.""" + return self._available + @property def name(self): """Return name of the projector.""" return self._name @property - def state(self): - """Return the current state of the projector.""" + def is_on(self): + """Return if the projector is turned on.""" return self._state @property @@ -120,11 +125,13 @@ def update(self): msg = CMD_DICT[LAMP] awns = self._write_read_format(msg) if awns == 'Lamp 1': - self._state = STATE_ON + self._state = True + self._available = True elif awns == 'Lamp 0': - self._state = STATE_OFF + self._state = False + self._available = True else: - self._state = STATE_UNKNOWN + self._available = False for key in self._attributes.keys(): msg = CMD_DICT.get(key, None) diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index 1a166a9c2dc308..08138db9e70bfb 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -4,6 +4,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.arest/ """ +# pylint: disable=abstract-method import logging import requests diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index e77453fa6eb9ee..28a44249e122f6 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -65,6 +65,10 @@ def turn_on(self): self._wol.send_magic_packet(self._mac_address) self.update_ha_state() + def turn_off(self): + """Do nothing.""" + pass + def update(self): """Check if device is on and update the state.""" if platform.system().lower() == "windows": diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 4c57a23ff9cb27..8d811c3a5ccf22 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -273,29 +273,29 @@ def is_fan_on(self): """Return true if the fan is on.""" return None - def set_temperate(self, temperature): + def set_temperature(self, temperature): """Set new target temperature.""" - pass + raise NotImplementedError() def set_hvac_mode(self, hvac_mode): """Set hvac mode.""" - pass + raise NotImplementedError() def turn_away_mode_on(self): """Turn away mode on.""" - pass + raise NotImplementedError() def turn_away_mode_off(self): """Turn away mode off.""" - pass + raise NotImplementedError() def turn_fan_on(self): """Turn fan on.""" - pass + raise NotImplementedError() def turn_fan_off(self): """Turn fan off.""" - pass + raise NotImplementedError() @property def min_temp(self): diff --git a/homeassistant/components/thermostat/demo.py b/homeassistant/components/thermostat/demo.py index 5d47276c7bc72b..7718299ef6a35c 100644 --- a/homeassistant/components/thermostat/demo.py +++ b/homeassistant/components/thermostat/demo.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) -# pylint: disable=too-many-arguments +# pylint: disable=too-many-arguments, abstract-method class DemoThermostat(ThermostatDevice): """Representation of a demo thermostat.""" diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index f07ef47269d1cc..577a33c87f4997 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -68,7 +68,7 @@ def fan_min_on_time_set_service(service): schema=SET_FAN_MIN_ON_TIME_SCHEMA) -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods, abstract-method class Thermostat(ThermostatDevice): """A thermostat class for Ecobee.""" diff --git a/homeassistant/components/thermostat/eq3btsmart.py b/homeassistant/components/thermostat/eq3btsmart.py index 34c164f2c0dba2..c9bbdaeb0a4a85 100644 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ b/homeassistant/components/thermostat/eq3btsmart.py @@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return True -# pylint: disable=too-many-instance-attributes, import-error +# pylint: disable=too-many-instance-attributes, import-error, abstract-method class EQ3BTSmartThermostat(ThermostatDevice): """Representation of a EQ3 Bluetooth Smart thermostat.""" diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 64f95c2e517246..3d5190bcc2f1ed 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -41,7 +41,6 @@ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the heat control thermostat.""" name = config.get(CONF_NAME) @@ -55,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): min_temp, max_temp, target_temp)]) -# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-instance-attributes, abstract-method class HeatControl(ThermostatDevice): """Representation of a HeatControl device.""" diff --git a/homeassistant/components/thermostat/heatmiser.py b/homeassistant/components/thermostat/heatmiser.py index ec8bbeb19817f6..e7bbfd72f9b4e4 100644 --- a/homeassistant/components/thermostat/heatmiser.py +++ b/homeassistant/components/thermostat/heatmiser.py @@ -58,7 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class HeatmiserV3Thermostat(ThermostatDevice): """Representation of a HeatmiserV3 thermostat.""" - # pylint: disable=too-many-instance-attributes + # pylint: disable=too-many-instance-attributes, abstract-method def __init__(self, heatmiser, device, name, serport): """Initialize the thermostat.""" self.heatmiser = heatmiser diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index b4ecc6c166b922..6f1ff29c16c5c0 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -91,7 +91,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return True -# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-instance-attributes, abstract-method class HomematicThermostat(ThermostatDevice): """Representation of a Homematic thermostat.""" diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 633212e02b5b43..f45b07b9fd613e 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -49,7 +49,6 @@ def _setup_round(username, password, config, add_devices): # config will be used later -# pylint: disable=unused-argument def _setup_us(username, password, config, add_devices): """Setup user.""" import somecomfort @@ -74,7 +73,6 @@ def _setup_us(username, password, config, add_devices): return True -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the honeywel thermostat.""" username = config.get(CONF_USERNAME) @@ -98,7 +96,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """Representation of a Honeywell Round Connected thermostat.""" - # pylint: disable=too-many-instance-attributes + # pylint: disable=too-many-instance-attributes, abstract-method def __init__(self, device, zone_id, master, away_temp): """Initialize the thermostat.""" self.device = device @@ -182,6 +180,7 @@ def update(self): self._is_dhw = False +# pylint: disable=abstract-method class HoneywellUSThermostat(ThermostatDevice): """Representation of a Honeywell US Thermostat.""" diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 881eb821865bca..00a1acf07b4b45 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -26,6 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for structure, device in nest.devices()]) +# pylint: disable=abstract-method class NestThermostat(ThermostatDevice): """Representation of a Nest thermostat.""" diff --git a/homeassistant/components/thermostat/proliphix.py b/homeassistant/components/thermostat/proliphix.py index 4b86f556352add..bf5c61d2be60a2 100644 --- a/homeassistant/components/thermostat/proliphix.py +++ b/homeassistant/components/thermostat/proliphix.py @@ -27,6 +27,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ]) +# pylint: disable=abstract-method class ProliphixThermostat(ThermostatDevice): """Representation a Proliphix thermostat.""" diff --git a/homeassistant/components/thermostat/radiotherm.py b/homeassistant/components/thermostat/radiotherm.py index a6ae39434e77c1..963ef1a9a7ff2f 100644 --- a/homeassistant/components/thermostat/radiotherm.py +++ b/homeassistant/components/thermostat/radiotherm.py @@ -45,6 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(tstats) +# pylint: disable=abstract-method class RadioThermostat(ThermostatDevice): """Representation of a Radio Thermostat.""" diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py index 57287605e6738d..8d7e36cc2aabd5 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -58,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice): """Represents a HeatControl thermostat.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ee1b786dce3f0b..e4ccf11e1683c5 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -229,17 +229,15 @@ def state(self): @property def is_on(self): """Return True if entity is on.""" - return False + raise NotImplementedError() def turn_on(self, **kwargs): """Turn the entity on.""" - _LOGGER.warning('Method turn_on not implemented for %s', - self.entity_id) + raise NotImplementedError() def turn_off(self, **kwargs): """Turn the entity off.""" - _LOGGER.warning('Method turn_off not implemented for %s', - self.entity_id) + raise NotImplementedError() def toggle(self, **kwargs): """Toggle the entity off.""" From 7a8c5a07094857d9e8258f6291182e8a161d45f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 25 Jun 2016 06:40:02 +0200 Subject: [PATCH 30/79] Add frontend to the example config (#2367) --- config/configuration.yaml.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/configuration.yaml.example b/config/configuration.yaml.example index 0f6318a063834d..e7483740bc34ba 100644 --- a/config/configuration.yaml.example +++ b/config/configuration.yaml.example @@ -22,6 +22,9 @@ http: # Set to 1 to enable development mode # development: 1 +frontend: +# enable the frontend + light: # platform: hue From e4b67c95742ec7a048b310d3cff9f0006dfa2f85 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 25 Jun 2016 06:43:44 +0200 Subject: [PATCH 31/79] Add persistent notification component (#1844) --- .../components/persistent_notification.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 homeassistant/components/persistent_notification.py diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py new file mode 100644 index 00000000000000..6c784eaf5ca771 --- /dev/null +++ b/homeassistant/components/persistent_notification.py @@ -0,0 +1,18 @@ +""" +A component which is collecting configuration errors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/persistent_notification/ +""" + +DOMAIN = "persistent_notification" + + +def create(hass, entity, msg): + """Create a state for an error.""" + hass.states.set('{}.{}'.format(DOMAIN, entity), msg) + + +def setup(hass, config): + """Setup the persistent notification component.""" + return True From cbb897b2cfeb40020415971473ae256272442972 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 24 Jun 2016 22:34:55 -0700 Subject: [PATCH 32/79] Update frontend --- homeassistant/components/frontend/version.py | 4 ++-- .../components/frontend/www_static/core.js | 2 +- .../components/frontend/www_static/core.js.gz | Bin 32090 -> 32109 bytes .../frontend/www_static/frontend.html | 8 ++++---- .../frontend/www_static/frontend.html.gz | Bin 193782 -> 193927 bytes .../www_static/home-assistant-polymer | 2 +- .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 3785 -> 3786 bytes 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 50c61ad3af7c2b..ac957e0661f6ed 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,3 +1,3 @@ """DO NOT MODIFY. Auto-generated by build_frontend script.""" -CORE = "7962327e4a29e51d4a6f4ee6cca9acc3" -UI = "570e1b8744a58024fc4e256f5e024424" +CORE = "db0bb387f4d3bcace002d62b94baa348" +UI = "5b306b7e7d36799b7b67f592cbe94703" diff --git a/homeassistant/components/frontend/www_static/core.js b/homeassistant/components/frontend/www_static/core.js index 8bb155ea288130..4bc3619c443152 100644 --- a/homeassistant/components/frontend/www_static/core.js +++ b/homeassistant/components/frontend/www_static/core.js @@ -1,4 +1,4 @@ !function(t){function e(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,e,n){Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:n})},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=25)}({17:function(t,e,n){"use strict";(function(t){function n(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function r(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){return e={exports:{}},t(e,e.exports),e.exports}function u(t,e){var n=e.authToken,r=e.host;return Ne({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function a(){return Ue.getInitialState()}function s(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)})}function c(t,e){var n=e.authToken,r=e.host;return Pe({authToken:n,host:r})}function f(){return xe.getInitialState()}function h(t,e){var n=e.rememberAuth;return n}function l(t){return t.withMutations(function(t){t.set("isStreaming",!0).set("useStreaming",!0).set("hasError",!1)})}function p(t){return t.withMutations(function(t){t.set("isStreaming",!1).set("useStreaming",!1).set("hasError",!1)})}function _(t){return t.withMutations(function(t){t.set("isStreaming",!1).set("hasError",!0)})}function d(){return Be.getInitialState()}function v(t,e){var n=e.model,r=e.result,i=e.params,o=n.entity;if(!r)return t;var u=i.replace?t.set(o,un({})):t,a=Array.isArray(r)?r:[r],s=n.fromJSON||un;return u.withMutations(function(t){return a.forEach(function(e){var n=s(e);t.setIn([o,n.id],n)})})}function y(t,e){var n=e.model,r=e.params;return t.removeIn([n.entity,r.id])}function S(t){var e={};return e.incrementData=function(e,n){var r=arguments.length<=2||void 0===arguments[2]?{}:arguments[2];g(e,t,r,n)},e.replaceData=function(e,n){var r=arguments.length<=2||void 0===arguments[2]?{}:arguments[2];g(e,t,cn({},r,{replace:!0}),n)},e.removeData=function(e,n){I(e,t,{id:n})},t.fetch&&(e.fetch=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_FETCH_START,{model:t,params:n,method:"fetch"}),t.fetch(e,n).then(g.bind(null,e,t,n),m.bind(null,e,t,n))}),e.fetchAll=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_FETCH_START,{model:t,params:n,method:"fetchAll"}),t.fetchAll(e,n).then(g.bind(null,e,t,cn({},n,{replace:!0})),m.bind(null,e,t,n))},t.save&&(e.save=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_SAVE_START,{params:n}),t.save(e,n).then(E.bind(null,e,t,n),b.bind(null,e,t,n))}),t["delete"]&&(e["delete"]=function(e){var n=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return e.dispatch(rn.API_DELETE_START,{params:n}),t["delete"](e,n).then(I.bind(null,e,t,n),w.bind(null,e,t,n))}),e}function g(t,e,n,r){return t.dispatch(rn.API_FETCH_SUCCESS,{model:e,params:n,result:r}),r}function m(t,e,n,r){return t.dispatch(rn.API_FETCH_FAIL,{model:e,params:n,reason:r}),Promise.reject(r)}function E(t,e,n,r){return t.dispatch(rn.API_SAVE_SUCCESS,{model:e,params:n,result:r}),r}function b(t,e,n,r){return t.dispatch(rn.API_SAVE_FAIL,{model:e,params:n,reason:r}),Promise.reject(r)}function I(t,e,n,r){return t.dispatch(rn.API_DELETE_SUCCESS,{model:e,params:n,result:r}),r}function w(t,e,n,r){return t.dispatch(rn.API_DELETE_FAIL,{model:e,params:n,reason:r}),Promise.reject(r)}function O(t){t.registerStores({restApiCache:an})}function T(t){return[["restApiCache",t.entity],function(t){return!!t}]}function A(t){return[["restApiCache",t.entity],function(t){return t||fn({})}]}function D(t){return function(e){return["restApiCache",t.entity,e]}}function C(t){return new Date(t)}function z(t,e,n){var r=arguments.length<=3||void 0===arguments[3]?null:arguments[3],i=t.evaluate(si.authInfo),o=i.host+"/api/"+n;return new Promise(function(t,n){var u=new XMLHttpRequest;u.open(e,o,!0),u.setRequestHeader("X-HA-access",i.authToken),u.onload=function(){var e=void 0;try{e="application/json"===u.getResponseHeader("content-type")?JSON.parse(u.responseText):u.responseText}catch(r){e=u.responseText}u.status>199&&u.status<300?t(e):n(e)},u.onerror=function(){return n({})},r?u.send(JSON.stringify(r)):u.send()})}function R(t,e){var n=e.message;return t.set(t.size,n)}function M(){return jn.getInitialState()}function j(t,e){t.dispatch(zn.NOTIFICATION_CREATED,{message:e})}function k(t){t.registerStores({notifications:jn})}function L(t,e){if("lock"===t)return!0;if("garage_door"===t)return!0;var n=e.get(t);return!!n&&n.services.has("turn_on")}function N(t,e){return t?"group"===t.domain?"on"===t.state||"off"===t.state:L(t.domain,e):!1}function U(t,e){return[ur(t),function(t){return!!t&&t.services.has(e)}]}function H(t){return[Dn.byId(t),or,N]}function P(t,e){var n=e.component;return t.push(n)}function x(t,e){var n=e.components;return Sr(n)}function V(){return gr.getInitialState()}function q(t,e){var n=e.latitude,r=e.longitude,i=e.location_name,o=e.temperature_unit,u=e.time_zone,a=e.version;return Er({latitude:n,longitude:r,location_name:i,temperature_unit:o,time_zone:u,serverVersion:a})}function F(){return br.getInitialState()}function G(t,e){t.dispatch(vr.SERVER_CONFIG_LOADED,e)}function K(t){dn(t,"GET","config").then(function(e){return G(t,e)})}function Y(t,e){t.dispatch(vr.COMPONENT_LOADED,{component:e})}function B(t){return[["serverComponent"],function(e){return e.contains(t)}]}function J(t){t.registerStores({serverComponent:gr,serverConfig:br})}function W(t){return t.evaluate(_r)}function X(t){W(t)&&(t.hassId in Mr||(Mr[t.hassId]=Qe(Z.bind(null,t),Rr)),Mr[t.hassId]())}function Q(t){var e=Mr[t.hassId];e&&e.cancel()}function Z(t){return t.dispatch(Ze.API_FETCH_ALL_START,{}),dn(t,"GET","bootstrap").then(function(e){t.batch(function(){An.replaceData(t,e.states),cr.replaceData(t,e.services),Xn.replaceData(t,e.events),Dr.configLoaded(t,e.config),t.dispatch(Ze.API_FETCH_ALL_SUCCESS,{})}),X(t)},function(e){return t.dispatch(Ze.API_FETCH_ALL_FAIL,{message:e}),X(t),Promise.reject(e)})}function $(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=e.skipInitialSync,r=void 0===n?!1:n;t.dispatch(Ze.SYNC_SCHEDULED),r?X(t):Z(t)}function tt(t){t.dispatch(Ze.SYNC_SCHEDULE_CANCELLED),Q(t)}function et(t){t.registerStores({isFetchingData:tn,isSyncScheduled:nn})}function nt(t,e){switch(e.event_type){case"state_changed":e.data.new_state?An.incrementData(t,e.data.new_state):An.removeData(t,e.data.entity_id);break;case"component_loaded":Dr.componentLoaded(t,e.data.component);break;case"service_registered":cr.serviceRegistered(t,e.data.domain,e.data.service)}}function rt(t){var e=Hr[t.hassId];e&&(e.scheduleHealthCheck.cancel(),e.source.close(),Hr[t.hassId]=!1)}function it(t){var e=arguments.length<=1||void 0===arguments[1]?{}:arguments[1],n=e.syncOnInitialConnect,r=void 0===n?!0:n;rt(t);var i=Qe(it.bind(null,t),Ur),o=t.evaluate(si.authToken),u=new EventSource("/api/stream?api_password="+o+"&restrict="+Pr),a=r;Hr[t.hassId]={source:u,scheduleHealthCheck:i},u.addEventListener("open",function(){i(),t.batch(function(){t.dispatch(Fe.STREAM_START),kr.stop(t),a?kr.fetchAll(t):a=!0})},!1),u.addEventListener("message",function(e){i(),"ping"!==e.data&&nt(t,JSON.parse(e.data))},!1),u.addEventListener("error",function(){i(),u.readyState!==EventSource.CLOSED&&t.dispatch(Fe.STREAM_ERROR)},!1)}function ot(t){rt(t),t.batch(function(){t.dispatch(Fe.STREAM_STOP),kr.start(t)})}function ut(t){t.registerStores({streamStatus:Be})}function at(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2],r=n.useStreaming,i=void 0===r?t.evaluate(Br.isSupported):r,o=n.rememberAuth,u=void 0===o?!1:o,a=n.host,s=void 0===a?"":a;t.dispatch(ke.VALIDATING_AUTH_TOKEN,{authToken:e,host:s}),kr.fetchAll(t).then(function(){t.dispatch(ke.VALID_AUTH_TOKEN,{authToken:e,host:s,rememberAuth:u}),i?Yr.start(t,{syncOnInitialConnect:!1}):kr.start(t,{skipInitialSync:!0})},function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],n=e.message,r=void 0===n?Wr:n;t.dispatch(ke.INVALID_AUTH_TOKEN,{errorMessage:r})})}function st(t){t.dispatch(ke.LOG_OUT,{})}function ct(t){t.registerStores({authAttempt:Ue,authCurrent:xe,rememberAuth:qe})}function ft(t,e){var n=e.pane;return n}function ht(){return li.getInitialState()}function lt(t,e){var n=e.show;return!!n}function pt(){return _i.getInitialState()}function _t(t,e){t.dispatch(fi.SHOW_SIDEBAR,{show:e})}function dt(t,e){t.dispatch(fi.NAVIGATE,{pane:e})}function vt(t){return[vi,function(e){return e===t}]}function yt(t,e){var n=e.entityId;return n}function St(){return Ei.getInitialState()}function gt(t,e){t.dispatch(gi.SELECT_ENTITY,{entityId:e})}function mt(t){t.dispatch(gi.SELECT_ENTITY,{entityId:null})}function Et(t){return!t||(new Date).getTime()-t>6e4}function bt(t){return t.getUTCFullYear()+"-"+(t.getUTCMonth()+1)+"-"+t.getUTCDate()}function It(t,e){var n=e.date;return bt(n)}function wt(){return Oi.getInitialState()}function Ot(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,Ai({})):t.withMutations(function(t){r.forEach(function(e){return t.setIn([n,e[0].entity_id],Ai(e.map(mn.fromJSON)))})})}function Tt(){return Di.getInitialState()}function At(t,e){var n=e.stateHistory;return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,Mi(e.map(mn.fromJSON)))})})}function Dt(){return ji.getInitialState()}function Ct(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,r)}),history.length>1&&t.set(Ni,r)})}function zt(){return Ui.getInitialState()}function Rt(t,e){t.dispatch(Ii.ENTITY_HISTORY_DATE_SELECTED,{date:e})}function Mt(t){var e=arguments.length<=1||void 0===arguments[1]?null:arguments[1];t.dispatch(Ii.RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),dn(t,"GET",n).then(function(e){return t.dispatch(Ii.RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})},function(){return t.dispatch(Ii.RECENT_ENTITY_HISTORY_FETCH_ERROR,{})})}function jt(t,e){return t.dispatch(Ii.ENTITY_HISTORY_FETCH_START,{date:e}),dn(t,"GET","history/period/"+e).then(function(n){return t.dispatch(Ii.ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})},function(){return t.dispatch(Ii.ENTITY_HISTORY_FETCH_ERROR,{})})}function kt(t){var e=t.evaluate(xi);return jt(t,e)}function Lt(t){t.registerStores({currentEntityHistoryDate:Oi,entityHistory:Di,isLoadingEntityHistory:zi,recentEntityHistory:ji,recentEntityHistoryUpdated:Ui})}function Nt(t){t.registerStores({moreInfoEntityId:Ei})}function Ut(t,e){var n=e.model,r=e.result,i=e.params;if(null===t||"entity"!==n.entity||!i.replace)return t;for(var o=0;onu}function ce(t){t.registerStores({currentLogbookDate:qo,isLoadingLogbookEntries:Go,logbookEntries:Xo,logbookEntriesUpdated:$o})}function fe(t,e){return dn(t,"POST","template",{template:e})}function he(t){return t.set("isListening",!0)}function le(t,e){var n=e.interimTranscript,r=e.finalTranscript;return t.withMutations(function(t){return t.set("isListening",!0).set("isTransmitting",!1).set("interimTranscript",n).set("finalTranscript",r)})}function pe(t,e){var n=e.finalTranscript;return t.withMutations(function(t){return t.set("isListening",!1).set("isTransmitting",!0).set("interimTranscript","").set("finalTranscript",n)})}function _e(){return gu.getInitialState()}function de(){return gu.getInitialState()}function ve(){return gu.getInitialState()}function ye(t){return mu[t.hassId]}function Se(t){var e=ye(t);if(e){var n=e.finalTranscript||e.interimTranscript;t.dispatch(vu.VOICE_TRANSMITTING,{finalTranscript:n}),cr.callService(t,"conversation","process",{text:n}).then(function(){t.dispatch(vu.VOICE_DONE)},function(){t.dispatch(vu.VOICE_ERROR)})}}function ge(t){var e=ye(t);e&&(e.recognition.stop(),mu[t.hassId]=!1)}function me(t){Se(t),ge(t)}function Ee(t){var e=me.bind(null,t);e();var n=new webkitSpeechRecognition;mu[t.hassId]={recognition:n,interimTranscript:"",finalTranscript:""},n.interimResults=!0,n.onstart=function(){return t.dispatch(vu.VOICE_START)},n.onerror=function(){return t.dispatch(vu.VOICE_ERROR)},n.onend=e,n.onresult=function(e){var n=ye(t);if(n){for(var r="",i="",o=e.resultIndex;oi;i++)r[i]=t[i+e];return r}function o(t){return void 0===t.size&&(t.size=t.__iterate(a)),t.size}function u(t,e){if("number"!=typeof e){var n=+e;if(""+n!==e)return NaN;e=n}return 0>e?o(t)+e:e}function a(){return!0}function s(t,e,n){return(0===t||void 0!==n&&-n>=t)&&(void 0===e||void 0!==n&&e>=n)}function c(t,e){return h(t,e,0)}function f(t,e){return h(t,e,e)}function h(t,e,n){return void 0===t?n:0>t?Math.max(0,e+t):void 0===e?t:Math.min(e,t)}function l(t){return v(t)?t:C(t)}function p(t){return y(t)?t:z(t)}function _(t){return S(t)?t:R(t)}function d(t){return v(t)&&!g(t)?t:M(t)}function v(t){return!(!t||!t[vn])}function y(t){return!(!t||!t[yn])}function S(t){return!(!t||!t[Sn])}function g(t){return y(t)||S(t)}function m(t){return!(!t||!t[gn])}function E(t){this.next=t}function b(t,e,n,r){var i=0===t?e:1===t?n:[e,n];return r?r.value=i:r={value:i,done:!1},r}function I(){return{value:void 0,done:!0}}function w(t){return!!A(t)}function O(t){return t&&"function"==typeof t.next}function T(t){var e=A(t);return e&&e.call(t)}function A(t){var e=t&&(In&&t[In]||t[wn]);return"function"==typeof e?e:void 0}function D(t){return t&&"number"==typeof t.length}function C(t){return null===t||void 0===t?H():v(t)?t.toSeq():V(t)}function z(t){return null===t||void 0===t?H().toKeyedSeq():v(t)?y(t)?t.toSeq():t.fromEntrySeq():P(t)}function R(t){return null===t||void 0===t?H():v(t)?y(t)?t.entrySeq():t.toIndexedSeq():x(t)}function M(t){return(null===t||void 0===t?H():v(t)?y(t)?t.entrySeq():t:x(t)).toSetSeq()}function j(t){this._array=t,this.size=t.length}function k(t){var e=Object.keys(t);this._object=t,this._keys=e,this.size=e.length}function L(t){this._iterable=t,this.size=t.length||t.size}function N(t){this._iterator=t,this._iteratorCache=[]}function U(t){return!(!t||!t[Tn])}function H(){return An||(An=new j([]))}function P(t){var e=Array.isArray(t)?new j(t).fromEntrySeq():O(t)?new N(t).fromEntrySeq():w(t)?new L(t).fromEntrySeq():"object"===("undefined"==typeof t?"undefined":De(t))?new k(t):void 0;if(!e)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+t);return e}function x(t){var e=q(t);if(!e)throw new TypeError("Expected Array or iterable object of values: "+t);return e}function V(t){var e=q(t)||"object"===("undefined"==typeof t?"undefined":De(t))&&new k(t);if(!e)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+t);return e}function q(t){return D(t)?new j(t):O(t)?new N(t):w(t)?new L(t):void 0}function F(t,e,n,r){var i=t._cache;if(i){for(var o=i.length-1,u=0;o>=u;u++){var a=i[n?o-u:u];if(e(a[1],r?a[0]:u,t)===!1)return u+1}return u}return t.__iterateUncached(e,n)}function G(t,e,n,r){var i=t._cache;if(i){var o=i.length-1,u=0;return new E(function(){var t=i[n?o-u:u];return u++>o?I():b(e,r?t[0]:u-1,t[1])})}return t.__iteratorUncached(e,n)}function K(){throw TypeError("Abstract")}function Y(){}function B(){}function J(){}function W(t,e){if(t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1;if("function"==typeof t.valueOf&&"function"==typeof e.valueOf){if(t=t.valueOf(),e=e.valueOf(),t===e||t!==t&&e!==e)return!0;if(!t||!e)return!1}return!("function"!=typeof t.equals||"function"!=typeof e.equals||!t.equals(e))}function X(t,e){return e?Q(e,t,"",{"":t}):Z(t)}function Q(t,e,n,r){return Array.isArray(e)?t.call(r,n,R(e).map(function(n,r){return Q(t,n,r,e)})):$(e)?t.call(r,n,z(e).map(function(n,r){return Q(t,n,r,e)})):e}function Z(t){return Array.isArray(t)?R(t).map(Z).toList():$(t)?z(t).map(Z).toMap():t}function $(t){return t&&(t.constructor===Object||void 0===t.constructor)}function tt(t){return t>>>1&1073741824|3221225471&t}function et(t){if(t===!1||null===t||void 0===t)return 0;if("function"==typeof t.valueOf&&(t=t.valueOf(),t===!1||null===t||void 0===t))return 0;if(t===!0)return 1;var e="undefined"==typeof t?"undefined":De(t);if("number"===e){var n=0|t;for(n!==t&&(n^=4294967295*t);t>4294967295;)t/=4294967295,n^=t;return tt(n)}return"string"===e?t.length>Ln?nt(t):rt(t):"function"==typeof t.hashCode?t.hashCode():it(t)}function nt(t){var e=Hn[t];return void 0===e&&(e=rt(t),Un===Nn&&(Un=0,Hn={}),Un++,Hn[t]=e),e}function rt(t){for(var e=0,n=0;n0)switch(t.nodeType){case 1:return t.uniqueID;case 9:return t.documentElement&&t.documentElement.uniqueID}}function ut(t,e){if(!t)throw new Error(e)}function at(t){ut(t!==1/0,"Cannot perform this action with an infinite size.")}function st(t,e){this._iter=t,this._useKeys=e,this.size=t.size}function ct(t){this._iter=t,this.size=t.size}function ft(t){this._iter=t,this.size=t.size}function ht(t){this._iter=t,this.size=t.size}function lt(t){var e=jt(t);return e._iter=t,e.size=t.size,e.flip=function(){return t},e.reverse=function(){var e=t.reverse.apply(this);return e.flip=function(){return t.reverse()},e},e.has=function(e){return t.includes(e)},e.includes=function(e){return t.has(e)},e.cacheResult=kt,e.__iterateUncached=function(e,n){var r=this;return t.__iterate(function(t,n){return e(n,t,r)!==!1},n)},e.__iteratorUncached=function(e,n){if(e===bn){var r=t.__iterator(e,n);return new E(function(){var t=r.next();if(!t.done){var e=t.value[0];t.value[0]=t.value[1],t.value[1]=e}return t})}return t.__iterator(e===En?mn:En,n)},e}function pt(t,e,n){var r=jt(t);return r.size=t.size,r.has=function(e){return t.has(e)},r.get=function(r,i){var o=t.get(r,pn);return o===pn?i:e.call(n,o,r,t)},r.__iterateUncached=function(r,i){var o=this;return t.__iterate(function(t,i,u){return r(e.call(n,t,i,u),i,o)!==!1},i)},r.__iteratorUncached=function(r,i){var o=t.__iterator(bn,i);return new E(function(){var i=o.next();if(i.done)return i;var u=i.value,a=u[0];return b(r,a,e.call(n,u[1],a,t),i)})},r}function _t(t,e){var n=jt(t);return n._iter=t,n.size=t.size,n.reverse=function(){return t},t.flip&&(n.flip=function(){var e=lt(t);return e.reverse=function(){return t.flip()},e}),n.get=function(n,r){return t.get(e?n:-1-n,r)},n.has=function(n){return t.has(e?n:-1-n)},n.includes=function(e){return t.includes(e)},n.cacheResult=kt,n.__iterate=function(e,n){var r=this;return t.__iterate(function(t,n){return e(t,n,r)},!n)},n.__iterator=function(e,n){return t.__iterator(e,!n)},n}function dt(t,e,n,r){var i=jt(t);return r&&(i.has=function(r){var i=t.get(r,pn);return i!==pn&&!!e.call(n,i,r,t)},i.get=function(r,i){var o=t.get(r,pn);return o!==pn&&e.call(n,o,r,t)?o:i}),i.__iterateUncached=function(i,o){var u=this,a=0;return t.__iterate(function(t,o,s){return e.call(n,t,o,s)?(a++,i(t,r?o:a-1,u)):void 0},o),a},i.__iteratorUncached=function(i,o){var u=t.__iterator(bn,o),a=0;return new E(function(){for(;;){var o=u.next();if(o.done)return o;var s=o.value,c=s[0],f=s[1];if(e.call(n,f,c,t))return b(i,r?c:a++,f,o)}})},i}function vt(t,e,n){var r=Ut().asMutable();return t.__iterate(function(i,o){r.update(e.call(n,i,o,t),0,function(t){return t+1})}),r.asImmutable()}function yt(t,e,n){var r=y(t),i=(m(t)?Ie():Ut()).asMutable();t.__iterate(function(o,u){i.update(e.call(n,o,u,t),function(t){return t=t||[],t.push(r?[u,o]:o),t})});var o=Mt(t);return i.map(function(e){return Ct(t,o(e))})}function St(t,e,n,r){var i=t.size;if(void 0!==e&&(e=0|e),void 0!==n&&(n=0|n),s(e,n,i))return t;var o=c(e,i),a=f(n,i);if(o!==o||a!==a)return St(t.toSeq().cacheResult(),e,n,r);var h,l=a-o;l===l&&(h=0>l?0:l);var p=jt(t);return p.size=0===h?h:t.size&&h||void 0,!r&&U(t)&&h>=0&&(p.get=function(e,n){return e=u(this,e),e>=0&&h>e?t.get(e+o,n):n}),p.__iterateUncached=function(e,n){var i=this;if(0===h)return 0;if(n)return this.cacheResult().__iterate(e,n);var u=0,a=!0,s=0;return t.__iterate(function(t,n){return a&&(a=u++h)return I();var t=i.next();return r||e===En?t:e===mn?b(e,a-1,void 0,t):b(e,a-1,t.value[1],t)})},p}function gt(t,e,n){var r=jt(t);return r.__iterateUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterate(r,i);var u=0;return t.__iterate(function(t,i,a){return e.call(n,t,i,a)&&++u&&r(t,i,o)}),u},r.__iteratorUncached=function(r,i){var o=this;if(i)return this.cacheResult().__iterator(r,i);var u=t.__iterator(bn,i),a=!0;return new E(function(){if(!a)return I();var t=u.next();if(t.done)return t;var i=t.value,s=i[0],c=i[1];return e.call(n,c,s,o)?r===bn?t:b(r,s,c,t):(a=!1,I())})},r}function mt(t,e,n,r){var i=jt(t);return i.__iterateUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterate(i,o);var a=!0,s=0;return t.__iterate(function(t,o,c){return a&&(a=e.call(n,t,o,c))?void 0:(s++,i(t,r?o:s-1,u))}),s},i.__iteratorUncached=function(i,o){var u=this;if(o)return this.cacheResult().__iterator(i,o);var a=t.__iterator(bn,o),s=!0,c=0;return new E(function(){var t,o,f;do{if(t=a.next(),t.done)return r||i===En?t:i===mn?b(i,c++,void 0,t):b(i,c++,t.value[1],t);var h=t.value;o=h[0],f=h[1],s&&(s=e.call(n,f,o,u))}while(s);return i===bn?t:b(i,o,f,t)})},i}function Et(t,e){var n=y(t),r=[t].concat(e).map(function(t){return v(t)?n&&(t=p(t)):t=n?P(t):x(Array.isArray(t)?t:[t]),t}).filter(function(t){return 0!==t.size});if(0===r.length)return t;if(1===r.length){var i=r[0];if(i===t||n&&y(i)||S(t)&&S(i))return i}var o=new j(r);return n?o=o.toKeyedSeq():S(t)||(o=o.toSetSeq()),o=o.flatten(!0),o.size=r.reduce(function(t,e){if(void 0!==t){var n=e.size;if(void 0!==n)return t+n}},0),o}function bt(t,e,n){var r=jt(t);return r.__iterateUncached=function(r,i){function o(t,s){var c=this;t.__iterate(function(t,i){return(!e||e>s)&&v(t)?o(t,s+1):r(t,n?i:u++,c)===!1&&(a=!0),!a},i)}var u=0,a=!1;return o(t,0),u},r.__iteratorUncached=function(r,i){var o=t.__iterator(r,i),u=[],a=0;return new E(function(){for(;o;){var t=o.next();if(t.done===!1){var s=t.value;if(r===bn&&(s=s[1]),e&&!(u.length0}function Dt(t,e,n){var r=jt(t); return r.size=new j(n).map(function(t){return t.size}).min(),r.__iterate=function(t,e){for(var n,r=this.__iterator(En,e),i=0;!(n=r.next()).done&&t(n.value,i++,this)!==!1;);return i},r.__iteratorUncached=function(t,r){var i=n.map(function(t){return t=l(t),T(r?t.reverse():t)}),o=0,u=!1;return new E(function(){var n;return u||(n=i.map(function(t){return t.next()}),u=n.some(function(t){return t.done})),u?I():b(t,o++,e.apply(null,n.map(function(t){return t.value})))})},r}function Ct(t,e){return U(t)?e:t.constructor(e)}function zt(t){if(t!==Object(t))throw new TypeError("Expected [K, V] tuple: "+t)}function Rt(t){return at(t.size),o(t)}function Mt(t){return y(t)?p:S(t)?_:d}function jt(t){return Object.create((y(t)?z:S(t)?R:M).prototype)}function kt(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):C.prototype.cacheResult.call(this)}function Lt(t,e){return t>e?1:e>t?-1:0}function Nt(t){var e=T(t);if(!e){if(!D(t))throw new TypeError("Expected iterable or array-like: "+t);e=T(l(t))}return e}function Ut(t){return null===t||void 0===t?Jt():Ht(t)&&!m(t)?t:Jt().withMutations(function(e){var n=p(t);at(n.size),n.forEach(function(t,n){return e.set(n,t)})})}function Ht(t){return!(!t||!t[Pn])}function Pt(t,e){this.ownerID=t,this.entries=e}function xt(t,e,n){this.ownerID=t,this.bitmap=e,this.nodes=n}function Vt(t,e,n){this.ownerID=t,this.count=e,this.nodes=n}function qt(t,e,n){this.ownerID=t,this.keyHash=e,this.entries=n}function Ft(t,e,n){this.ownerID=t,this.keyHash=e,this.entry=n}function Gt(t,e,n){this._type=e,this._reverse=n,this._stack=t._root&&Yt(t._root)}function Kt(t,e){return b(t,e[0],e[1])}function Yt(t,e){return{node:t,index:0,__prev:e}}function Bt(t,e,n,r){var i=Object.create(xn);return i.size=t,i._root=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Jt(){return Vn||(Vn=Bt(0))}function Wt(t,n,r){var i,o;if(t._root){var u=e(_n),a=e(dn);if(i=Xt(t._root,t.__ownerID,0,void 0,n,r,u,a),!a.value)return t;o=t.size+(u.value?r===pn?-1:1:0)}else{if(r===pn)return t;o=1,i=new Pt(t.__ownerID,[[n,r]])}return t.__ownerID?(t.size=o,t._root=i,t.__hash=void 0,t.__altered=!0,t):i?Bt(o,i):Jt()}function Xt(t,e,r,i,o,u,a,s){return t?t.update(e,r,i,o,u,a,s):u===pn?t:(n(s),n(a),new Ft(e,i,[o,u]))}function Qt(t){return t.constructor===Ft||t.constructor===qt}function Zt(t,e,n,r,i){if(t.keyHash===r)return new qt(e,r,[t.entry,i]);var o,u=(0===n?t.keyHash:t.keyHash>>>n)&ln,a=(0===n?r:r>>>n)&ln,s=u===a?[Zt(t,e,n+fn,r,i)]:(o=new Ft(e,r,i),a>u?[t,o]:[o,t]);return new xt(e,1<a;a++,s<<=1){var f=e[a];void 0!==f&&a!==r&&(i|=s,u[o++]=f)}return new xt(t,i,u)}function ee(t,e,n,r,i){for(var o=0,u=new Array(hn),a=0;0!==n;a++,n>>>=1)u[a]=1&n?e[o++]:void 0;return u[r]=i,new Vt(t,o+1,u)}function ne(t,e,n){for(var r=[],i=0;i>1&1431655765,t=(858993459&t)+(t>>2&858993459),t=t+(t>>4)&252645135,t+=t>>8,t+=t>>16,127&t}function ae(t,e,n,r){var o=r?t:i(t);return o[e]=n,o}function se(t,e,n,r){var i=t.length+1;if(r&&e+1===i)return t[e]=n,t;for(var o=new Array(i),u=0,a=0;i>a;a++)a===e?(o[a]=n,u=-1):o[a]=t[a+u];return o}function ce(t,e,n){var r=t.length-1;if(n&&e===r)return t.pop(),t;for(var i=new Array(r),o=0,u=0;r>u;u++)u===e&&(o=1),i[u]=t[u+o];return i}function fe(t){var e=de();if(null===t||void 0===t)return e;if(he(t))return t;var n=_(t),r=n.size;return 0===r?e:(at(r),r>0&&hn>r?_e(0,r,fn,null,new le(n.toArray())):e.withMutations(function(t){t.setSize(r),n.forEach(function(e,n){return t.set(n,e)})}))}function he(t){return!(!t||!t[Kn])}function le(t,e){this.array=t,this.ownerID=e}function pe(t,e){function n(t,e,n){return 0===e?r(t,n):i(t,e,n)}function r(t,n){var r=n===a?s&&s.array:t&&t.array,i=n>o?0:o-n,c=u-n;return c>hn&&(c=hn),function(){if(i===c)return Jn;var t=e?--c:i++;return r&&r[t]}}function i(t,r,i){var a,s=t&&t.array,c=i>o?0:o-i>>r,f=(u-i>>r)+1;return f>hn&&(f=hn),function(){for(;;){if(a){var t=a();if(t!==Jn)return t;a=null}if(c===f)return Jn;var o=e?--f:c++;a=n(s&&s[o],r-fn,i+(o<=t.size||0>n)return t.withMutations(function(t){0>n?me(t,n).set(0,r):me(t,0,n+1).set(n,r)});n+=t._origin;var i=t._tail,o=t._root,a=e(dn);return n>=be(t._capacity)?i=ye(i,t.__ownerID,0,n,r,a):o=ye(o,t.__ownerID,t._level,n,r,a),a.value?t.__ownerID?(t._root=o,t._tail=i,t.__hash=void 0,t.__altered=!0,t):_e(t._origin,t._capacity,t._level,o,i):t}function ye(t,e,r,i,o,u){var a=i>>>r&ln,s=t&&a0){var f=t&&t.array[a],h=ye(f,e,r-fn,i,o,u);return h===f?t:(c=Se(t,e),c.array[a]=h,c)}return s&&t.array[a]===o?t:(n(u),c=Se(t,e),void 0===o&&a===c.array.length-1?c.array.pop():c.array[a]=o,c)}function Se(t,e){return e&&t&&e===t.ownerID?t:new le(t?t.array.slice():[],e)}function ge(t,e){if(e>=be(t._capacity))return t._tail;if(e<1<0;)n=n.array[e>>>r&ln],r-=fn;return n}}function me(t,e,n){void 0!==e&&(e=0|e),void 0!==n&&(n=0|n);var i=t.__ownerID||new r,o=t._origin,u=t._capacity,a=o+e,s=void 0===n?u:0>n?u+n:o+n;if(a===o&&s===u)return t;if(a>=s)return t.clear();for(var c=t._level,f=t._root,h=0;0>a+h;)f=new le(f&&f.array.length?[void 0,f]:[],i),c+=fn,h+=1<=1<p?ge(t,s-1):p>l?new le([],i):_;if(_&&p>l&&u>a&&_.array.length){f=Se(f,i);for(var v=f,y=c;y>fn;y-=fn){var S=l>>>y&ln;v=v.array[S]=Se(v.array[S],i)}v.array[l>>>fn&ln]=_}if(u>s&&(d=d&&d.removeAfter(i,0,s)),a>=p)a-=p,s-=p,c=fn,f=null,d=d&&d.removeBefore(i,0,a);else if(a>o||l>p){for(h=0;f;){var g=a>>>c&ln;if(g!==p>>>c&ln)break;g&&(h+=(1<o&&(f=f.removeBefore(i,c,a-h)),f&&l>p&&(f=f.removeAfter(i,c,p-h)),h&&(a-=h,s-=h)}return t.__ownerID?(t.size=s-a,t._origin=a,t._capacity=s,t._level=c,t._root=f,t._tail=d,t.__hash=void 0,t.__altered=!0,t):_e(a,s,c,f,d)}function Ee(t,e,n){for(var r=[],i=0,o=0;oi&&(i=a.size),v(u)||(a=a.map(function(t){return X(t)})),r.push(a)}return i>t.size&&(t=t.setSize(i)),ie(t,e,r)}function be(t){return hn>t?0:t-1>>>fn<=hn&&u.size>=2*o.size?(i=u.filter(function(t,e){return void 0!==t&&a!==e}),r=i.toKeyedSeq().map(function(t){return t[0]}).flip().toMap(),t.__ownerID&&(r.__ownerID=i.__ownerID=t.__ownerID)):(r=o.remove(e),i=a===u.size-1?u.pop():u.set(a,void 0))}else if(s){if(n===u.get(a)[1])return t;r=o,i=u.set(a,[e,n])}else r=o.set(e,u.size),i=u.set(u.size,[e,n]);return t.__ownerID?(t.size=r.size,t._map=r,t._list=i,t.__hash=void 0,t):Oe(r,i)}function Ce(t){return null===t||void 0===t?Me():ze(t)?t:Me().unshiftAll(t)}function ze(t){return!(!t||!t[Xn])}function Re(t,e,n,r){var i=Object.create(Qn);return i.size=t,i._head=e,i.__ownerID=n,i.__hash=r,i.__altered=!1,i}function Me(){return Zn||(Zn=Re(0))}function je(t){return null===t||void 0===t?Ue():ke(t)&&!m(t)?t:Ue().withMutations(function(e){var n=d(t);at(n.size),n.forEach(function(t){return e.add(t)})})}function ke(t){return!(!t||!t[$n])}function Le(t,e){return t.__ownerID?(t.size=e.size,t._map=e,t):e===t._map?t:0===e.size?t.__empty():t.__make(e)}function Ne(t,e){var n=Object.create(tr);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function Ue(){return er||(er=Ne(Jt()))}function He(t){return null===t||void 0===t?Ve():Pe(t)?t:Ve().withMutations(function(e){var n=d(t);at(n.size),n.forEach(function(t){return e.add(t)})})}function Pe(t){return ke(t)&&m(t)}function xe(t,e){var n=Object.create(nr);return n.size=t?t.size:0,n._map=t,n.__ownerID=e,n}function Ve(){return rr||(rr=xe(Te()))}function qe(t,e){var n,r=function(o){if(o instanceof r)return o;if(!(this instanceof r))return new r(o);if(!n){n=!0;var u=Object.keys(t);Ke(i,u),i.size=u.length,i._name=e,i._keys=u,i._defaultValues=t}this._map=Ut(o)},i=r.prototype=Object.create(ir);return i.constructor=r,r}function Fe(t,e,n){var r=Object.create(Object.getPrototypeOf(t));return r._map=e,r.__ownerID=n,r}function Ge(t){return t._name||t.constructor.name||"Record"}function Ke(t,e){try{e.forEach(Ye.bind(void 0,t))}catch(n){}}function Ye(t,e){Object.defineProperty(t,e,{get:function(){return this.get(e)},set:function(t){ut(this.__ownerID,"Cannot set on an immutable record."),this.set(e,t)}})}function Be(t,e){if(t===e)return!0;if(!v(e)||void 0!==t.size&&void 0!==e.size&&t.size!==e.size||void 0!==t.__hash&&void 0!==e.__hash&&t.__hash!==e.__hash||y(t)!==y(e)||S(t)!==S(e)||m(t)!==m(e))return!1;if(0===t.size&&0===e.size)return!0;var n=!g(t);if(m(t)){var r=t.entries();return e.every(function(t,e){var i=r.next().value;return i&&W(i[1],t)&&(n||W(i[0],e))})&&r.next().done}var i=!1;if(void 0===t.size)if(void 0===e.size)"function"==typeof t.cacheResult&&t.cacheResult();else{i=!0;var o=t;t=e,e=o}var u=!0,a=e.__iterate(function(e,r){return(n?t.has(e):i?W(e,t.get(r,pn)):W(t.get(r,pn),e))?void 0:(u=!1,!1)});return u&&t.size===a}function Je(t,e,n){if(!(this instanceof Je))return new Je(t,e,n);if(ut(0!==n,"Cannot step a Range by 0"),t=t||0,void 0===e&&(e=1/0),n=void 0===n?1:Math.abs(n),t>e&&(n=-n),this._start=t,this._end=e,this._step=n,this.size=Math.max(0,Math.ceil((e-t)/n-1)+1),0===this.size){if(or)return or;or=this}}function We(t,e){if(!(this instanceof We))return new We(t,e);if(this._value=t,this.size=void 0===e?1/0:Math.max(0,e),0===this.size){if(ur)return ur;ur=this}}function Xe(t,e){var n=function(n){t.prototype[n]=e[n]};return Object.keys(e).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(e).forEach(n),t}function Qe(t,e){return e}function Ze(t,e){return[e,t]}function $e(t){return function(){return!t.apply(this,arguments)}}function tn(t){return function(){return-t.apply(this,arguments)}}function en(t){return"string"==typeof t?JSON.stringify(t):t}function nn(){return i(arguments)}function rn(t,e){return e>t?1:t>e?-1:0}function on(t){if(t.size===1/0)return 0;var e=m(t),n=y(t),r=e?1:0,i=t.__iterate(n?e?function(t,e){r=31*r+an(et(t),et(e))|0}:function(t,e){r=r+an(et(t),et(e))|0}:e?function(t){r=31*r+et(t)|0}:function(t){r=r+et(t)|0});return un(i,r)}function un(t,e){return e=Cn(e,3432918353),e=Cn(e<<15|e>>>-15,461845907),e=Cn(e<<13|e>>>-13,5),e=(e+3864292196|0)^t,e=Cn(e^e>>>16,2246822507),e=Cn(e^e>>>13,3266489909),e=tt(e^e>>>16)}function an(t,e){return t^e+2654435769+(t<<6)+(t>>2)|0}var sn=Array.prototype.slice,cn="delete",fn=5,hn=1<=i;i++)if(t(n[e?r-i:i],i,this)===!1)return i+1;return i},j.prototype.__iterator=function(t,e){var n=this._array,r=n.length-1,i=0;return new E(function(){return i>r?I():b(t,i,n[e?r-i++:i++])})},t(k,z),k.prototype.get=function(t,e){return void 0===e||this.has(t)?this._object[t]:e},k.prototype.has=function(t){return this._object.hasOwnProperty(t)},k.prototype.__iterate=function(t,e){for(var n=this._object,r=this._keys,i=r.length-1,o=0;i>=o;o++){var u=r[e?i-o:o];if(t(n[u],u,this)===!1)return o+1}return o},k.prototype.__iterator=function(t,e){var n=this._object,r=this._keys,i=r.length-1,o=0;return new E(function(){var u=r[e?i-o:o];return o++>i?I():b(t,u,n[u])})},k.prototype[gn]=!0,t(L,R),L.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);var n=this._iterable,r=T(n),i=0;if(O(r))for(var o;!(o=r.next()).done&&t(o.value,i++,this)!==!1;);return i},L.prototype.__iteratorUncached=function(t,e){if(e)return this.cacheResult().__iterator(t,e);var n=this._iterable,r=T(n);if(!O(r))return new E(I);var i=0;return new E(function(){var e=r.next();return e.done?e:b(t,i++,e.value)})},t(N,R),N.prototype.__iterateUncached=function(t,e){if(e)return this.cacheResult().__iterate(t,e);for(var n=this._iterator,r=this._iteratorCache,i=0;i=r.length){var e=n.next();if(e.done)return e;r[i]=e.value}return b(t,i,r[i++])})};var An;t(K,l),t(Y,K),t(B,K),t(J,K),K.Keyed=Y,K.Indexed=B,K.Set=J;var Dn,Cn="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(t,e){t=0|t,e=0|e;var n=65535&t,r=65535&e;return n*r+((t>>>16)*r+n*(e>>>16)<<16>>>0)|0},zn=Object.isExtensible,Rn=function(){try{return Object.defineProperty({},"@",{}),!0}catch(t){return!1}}(),Mn="function"==typeof WeakMap;Mn&&(Dn=new WeakMap);var jn=0,kn="__immutablehash__";"function"==typeof Symbol&&(kn=Symbol(kn));var Ln=16,Nn=255,Un=0,Hn={};t(st,z),st.prototype.get=function(t,e){return this._iter.get(t,e)},st.prototype.has=function(t){return this._iter.has(t)},st.prototype.valueSeq=function(){return this._iter.valueSeq()},st.prototype.reverse=function(){var t=this,e=_t(this,!0);return this._useKeys||(e.valueSeq=function(){return t._iter.toSeq().reverse()}),e},st.prototype.map=function(t,e){var n=this,r=pt(this,t,e);return this._useKeys||(r.valueSeq=function(){return n._iter.toSeq().map(t,e)}),r},st.prototype.__iterate=function(t,e){var n,r=this;return this._iter.__iterate(this._useKeys?function(e,n){return t(e,n,r)}:(n=e?Rt(this):0,function(i){return t(i,e?--n:n++,r)}),e)},st.prototype.__iterator=function(t,e){if(this._useKeys)return this._iter.__iterator(t,e);var n=this._iter.__iterator(En,e),r=e?Rt(this):0;return new E(function(){var i=n.next();return i.done?i:b(t,e?--r:r++,i.value,i)})},st.prototype[gn]=!0,t(ct,R),ct.prototype.includes=function(t){return this._iter.includes(t)},ct.prototype.__iterate=function(t,e){var n=this,r=0;return this._iter.__iterate(function(e){return t(e,r++,n)},e)},ct.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e),r=0;return new E(function(){var e=n.next();return e.done?e:b(t,r++,e.value,e)})},t(ft,M),ft.prototype.has=function(t){return this._iter.includes(t)},ft.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate(function(e){return t(e,e,n)},e)},ft.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){var e=n.next();return e.done?e:b(t,e.value,e.value,e)})},t(ht,z),ht.prototype.entrySeq=function(){return this._iter.toSeq()},ht.prototype.__iterate=function(t,e){var n=this;return this._iter.__iterate(function(e){if(e){zt(e);var r=v(e);return t(r?e.get(1):e[1],r?e.get(0):e[0],n)}},e)},ht.prototype.__iterator=function(t,e){var n=this._iter.__iterator(En,e);return new E(function(){for(;;){var e=n.next();if(e.done)return e;var r=e.value;if(r){zt(r);var i=v(r);return b(t,i?r.get(0):r[0],i?r.get(1):r[1],e)}}})},ct.prototype.cacheResult=st.prototype.cacheResult=ft.prototype.cacheResult=ht.prototype.cacheResult=kt,t(Ut,Y),Ut.prototype.toString=function(){return this.__toString("Map {","}")},Ut.prototype.get=function(t,e){return this._root?this._root.get(0,void 0,t,e):e},Ut.prototype.set=function(t,e){return Wt(this,t,e)},Ut.prototype.setIn=function(t,e){return this.updateIn(t,pn,function(){return e})},Ut.prototype.remove=function(t){return Wt(this,t,pn)},Ut.prototype.deleteIn=function(t){return this.updateIn(t,function(){return pn})},Ut.prototype.update=function(t,e,n){return 1===arguments.length?t(this):this.updateIn([t],e,n)},Ut.prototype.updateIn=function(t,e,n){n||(n=e,e=void 0);var r=oe(this,Nt(t),e,n);return r===pn?void 0:r},Ut.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):Jt()},Ut.prototype.merge=function(){return ne(this,void 0,arguments)},Ut.prototype.mergeWith=function(t){var e=sn.call(arguments,1);return ne(this,t,e)},Ut.prototype.mergeIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,Jt(),function(t){return"function"==typeof t.merge?t.merge.apply(t,e):e[e.length-1]})},Ut.prototype.mergeDeep=function(){return ne(this,re(void 0),arguments)},Ut.prototype.mergeDeepWith=function(t){var e=sn.call(arguments,1);return ne(this,re(t),e)},Ut.prototype.mergeDeepIn=function(t){var e=sn.call(arguments,1);return this.updateIn(t,Jt(),function(t){return"function"==typeof t.mergeDeep?t.mergeDeep.apply(t,e):e[e.length-1]})},Ut.prototype.sort=function(t){return Ie(Ot(this,t))},Ut.prototype.sortBy=function(t,e){return Ie(Ot(this,e,t))},Ut.prototype.withMutations=function(t){var e=this.asMutable();return t(e),e.wasAltered()?e.__ensureOwner(this.__ownerID):this},Ut.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new r)},Ut.prototype.asImmutable=function(){return this.__ensureOwner()},Ut.prototype.wasAltered=function(){return this.__altered},Ut.prototype.__iterator=function(t,e){return new Gt(this,t,e)},Ut.prototype.__iterate=function(t,e){var n=this,r=0;return this._root&&this._root.iterate(function(e){return r++,t(e[1],e[0],n)},e),r},Ut.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Bt(this.size,this._root,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Ut.isMap=Ht;var Pn="@@__IMMUTABLE_MAP__@@",xn=Ut.prototype;xn[Pn]=!0,xn[cn]=xn.remove,xn.removeIn=xn.deleteIn,Pt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;u>o;o++)if(W(n,i[o][0]))return i[o][1];return r},Pt.prototype.update=function(t,e,r,o,u,a,s){for(var c=u===pn,f=this.entries,h=0,l=f.length;l>h&&!W(o,f[h][0]);h++);var p=l>h;if(p?f[h][1]===u:c)return this;if(n(s),(c||!p)&&n(a),!c||1!==f.length){if(!p&&!c&&f.length>=qn)return $t(t,f,o,u);var _=t&&t===this.ownerID,d=_?f:i(f);return p?c?h===l-1?d.pop():d[h]=d.pop():d[h]=[o,u]:d.push([o,u]),_?(this.entries=d,this):new Pt(t,d)}},xt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=1<<((0===t?e:e>>>t)&ln),o=this.bitmap;return 0===(o&i)?r:this.nodes[ue(o&i-1)].get(t+fn,e,n,r)},xt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&ln,s=1<=Fn)return ee(t,l,c,a,_);if(f&&!_&&2===l.length&&Qt(l[1^h]))return l[1^h];if(f&&_&&1===l.length&&Qt(_))return _;var d=t&&t===this.ownerID,v=f?_?c:c^s:c|s,y=f?_?ae(l,h,_,d):ce(l,h,d):se(l,h,_,d);return d?(this.bitmap=v,this.nodes=y,this):new xt(t,v,y)},Vt.prototype.get=function(t,e,n,r){void 0===e&&(e=et(n));var i=(0===t?e:e>>>t)&ln,o=this.nodes[i];return o?o.get(t+fn,e,n,r):r},Vt.prototype.update=function(t,e,n,r,i,o,u){void 0===n&&(n=et(r));var a=(0===e?n:n>>>e)&ln,s=i===pn,c=this.nodes,f=c[a];if(s&&!f)return this;var h=Xt(f,t,e+fn,n,r,i,o,u);if(h===f)return this;var l=this.count;if(f){if(!h&&(l--,Gn>l))return te(t,c,l,a)}else l++;var p=t&&t===this.ownerID,_=ae(c,a,h,p);return p?(this.count=l,this.nodes=_,this):new Vt(t,l,_)},qt.prototype.get=function(t,e,n,r){for(var i=this.entries,o=0,u=i.length;u>o;o++)if(W(n,i[o][0]))return i[o][1];return r},qt.prototype.update=function(t,e,r,o,u,a,s){void 0===r&&(r=et(o));var c=u===pn;if(r!==this.keyHash)return c?this:(n(s),n(a),Zt(this,t,e,r,[o,u]));for(var f=this.entries,h=0,l=f.length;l>h&&!W(o,f[h][0]);h++);var p=l>h;if(p?f[h][1]===u:c)return this;if(n(s),(c||!p)&&n(a),c&&2===l)return new Ft(t,this.keyHash,f[1^h]);var _=t&&t===this.ownerID,d=_?f:i(f);return p?c?h===l-1?d.pop():d[h]=d.pop():d[h]=[o,u]:d.push([o,u]),_?(this.entries=d,this):new qt(t,this.keyHash,d)},Ft.prototype.get=function(t,e,n,r){return W(n,this.entry[0])?this.entry[1]:r},Ft.prototype.update=function(t,e,r,i,o,u,a){var s=o===pn,c=W(i,this.entry[0]);return(c?o===this.entry[1]:s)?this:(n(a),s?void n(u):c?t&&t===this.ownerID?(this.entry[1]=o,this):new Ft(t,this.keyHash,[i,o]):(n(u),Zt(this,t,e,et(i),[i,o])))},Pt.prototype.iterate=qt.prototype.iterate=function(t,e){for(var n=this.entries,r=0,i=n.length-1;i>=r;r++)if(t(n[e?i-r:r])===!1)return!1},xt.prototype.iterate=Vt.prototype.iterate=function(t,e){for(var n=this.nodes,r=0,i=n.length-1;i>=r;r++){var o=n[e?i-r:r];if(o&&o.iterate(t,e)===!1)return!1}},Ft.prototype.iterate=function(t,e){return t(this.entry)},t(Gt,E),Gt.prototype.next=function(){for(var t=this._type,e=this._stack;e;){var n,r=e.node,i=e.index++;if(r.entry){if(0===i)return Kt(t,r.entry)}else if(r.entries){if(n=r.entries.length-1,n>=i)return Kt(t,r.entries[this._reverse?n-i:i])}else if(n=r.nodes.length-1,n>=i){var o=r.nodes[this._reverse?n-i:i];if(o){if(o.entry)return Kt(t,o.entry);e=this._stack=Yt(o,e)}continue}e=this._stack=this._stack.__prev}return I()};var Vn,qn=hn/4,Fn=hn/2,Gn=hn/4;t(fe,B),fe.of=function(){return this(arguments)},fe.prototype.toString=function(){return this.__toString("List [","]")},fe.prototype.get=function(t,e){if(t=u(this,t),t>=0&&t>>e&ln;if(r>=this.array.length)return new le([],t);var i,o=0===r;if(e>0){var u=this.array[r];if(i=u&&u.removeBefore(t,e-fn,n),i===u&&o)return this}if(o&&!i)return this;var a=Se(this,t);if(!o)for(var s=0;r>s;s++)a.array[s]=void 0;return i&&(a.array[r]=i),a},le.prototype.removeAfter=function(t,e,n){if(n===(e?1<>>e&ln;if(r>=this.array.length)return this;var i;if(e>0){var o=this.array[r];if(i=o&&o.removeAfter(t,e-fn,n),i===o&&r===this.array.length-1)return this}var u=Se(this,t);return u.array.splice(r+1),i&&(u.array[r]=i),u};var Bn,Jn={};t(Ie,Ut),Ie.of=function(){return this(arguments)},Ie.prototype.toString=function(){return this.__toString("OrderedMap {","}")},Ie.prototype.get=function(t,e){var n=this._map.get(t);return void 0!==n?this._list.get(n)[1]:e},Ie.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._map.clear(),this._list.clear(),this):Te()},Ie.prototype.set=function(t,e){return Ae(this,t,e)},Ie.prototype.remove=function(t){return Ae(this,t,pn)},Ie.prototype.wasAltered=function(){return this._map.wasAltered()||this._list.wasAltered()},Ie.prototype.__iterate=function(t,e){var n=this;return this._list.__iterate(function(e){return e&&t(e[1],e[0],n)},e)},Ie.prototype.__iterator=function(t,e){return this._list.fromEntrySeq().__iterator(t,e)},Ie.prototype.__ensureOwner=function(t){if(t===this.__ownerID)return this;var e=this._map.__ensureOwner(t),n=this._list.__ensureOwner(t);return t?Oe(e,n,t,this.__hash):(this.__ownerID=t,this._map=e,this._list=n,this)},Ie.isOrderedMap=we,Ie.prototype[gn]=!0,Ie.prototype[cn]=Ie.prototype.remove;var Wn;t(Ce,B),Ce.of=function(){return this(arguments)},Ce.prototype.toString=function(){return this.__toString("Stack [","]")},Ce.prototype.get=function(t,e){var n=this._head;for(t=u(this,t);n&&t--;)n=n.next;return n?n.value:e},Ce.prototype.peek=function(){return this._head&&this._head.value},Ce.prototype.push=function(){if(0===arguments.length)return this;for(var t=this.size+arguments.length,e=this._head,n=arguments.length-1;n>=0;n--)e={value:arguments[n],next:e};return this.__ownerID?(this.size=t,this._head=e,this.__hash=void 0,this.__altered=!0,this):Re(t,e)},Ce.prototype.pushAll=function(t){if(t=_(t),0===t.size)return this;at(t.size);var e=this.size,n=this._head;return t.reverse().forEach(function(t){e++,n={value:t,next:n}}),this.__ownerID?(this.size=e,this._head=n,this.__hash=void 0,this.__altered=!0,this):Re(e,n)},Ce.prototype.pop=function(){return this.slice(1)},Ce.prototype.unshift=function(){return this.push.apply(this,arguments)},Ce.prototype.unshiftAll=function(t){return this.pushAll(t)},Ce.prototype.shift=function(){return this.pop.apply(this,arguments)},Ce.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Me()},Ce.prototype.slice=function(t,e){if(s(t,e,this.size))return this;var n=c(t,this.size),r=f(e,this.size);if(r!==this.size)return B.prototype.slice.call(this,t,e);for(var i=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=i,this._head=o,this.__hash=void 0,this.__altered=!0,this):Re(i,o)},Ce.prototype.__ensureOwner=function(t){return t===this.__ownerID?this:t?Re(this.size,this._head,t,this.__hash):(this.__ownerID=t,this.__altered=!1,this)},Ce.prototype.__iterate=function(t,e){if(e)return this.reverse().__iterate(t);for(var n=0,r=this._head;r&&t(r.value,n++,this)!==!1;)r=r.next;return n},Ce.prototype.__iterator=function(t,e){if(e)return this.reverse().__iterator(t);var n=0,r=this._head;return new E(function(){if(r){var e=r.value;return r=r.next,b(t,n++,e)}return I()})},Ce.isStack=ze;var Xn="@@__IMMUTABLE_STACK__@@",Qn=Ce.prototype;Qn[Xn]=!0,Qn.withMutations=xn.withMutations,Qn.asMutable=xn.asMutable,Qn.asImmutable=xn.asImmutable,Qn.wasAltered=xn.wasAltered;var Zn;t(je,J),je.of=function(){return this(arguments)},je.fromKeys=function(t){return this(p(t).keySeq())},je.prototype.toString=function(){return this.__toString("Set {","}")},je.prototype.has=function(t){return this._map.has(t)},je.prototype.add=function(t){return Le(this,this._map.set(t,!0))},je.prototype.remove=function(t){return Le(this,this._map.remove(t))},je.prototype.clear=function(){return Le(this,this._map.clear())},je.prototype.union=function(){var t=sn.call(arguments,0);return t=t.filter(function(t){return 0!==t.size}),0===t.length?this:0!==this.size||this.__ownerID||1!==t.length?this.withMutations(function(e){for(var n=0;n1?" by "+this._step:"")+" ]"},Je.prototype.get=function(t,e){return this.has(t)?this._start+u(this,t)*this._step:e},Je.prototype.includes=function(t){var e=(t-this._start)/this._step;return e>=0&&e=e?new Je(0,0):new Je(this.get(t,this._end),this.get(e,this._end),this._step))},Je.prototype.indexOf=function(t){var e=t-this._start;if(e%this._step===0){var n=e/this._step;if(n>=0&&n=o;o++){if(t(i,o,this)===!1)return o+1;i+=e?-r:r}return o},Je.prototype.__iterator=function(t,e){var n=this.size-1,r=this._step,i=e?this._start+n*r:this._start,o=0;return new E(function(){var u=i;return i+=e?-r:r,o>n?I():b(t,o++,u)})},Je.prototype.equals=function(t){return t instanceof Je?this._start===t._start&&this._end===t._end&&this._step===t._step:Be(this,t)};var or;t(We,R),We.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},We.prototype.get=function(t,e){return this.has(t)?this._value:e},We.prototype.includes=function(t){return W(this._value,t)},We.prototype.slice=function(t,e){var n=this.size;return s(t,e,n)?this:new We(this._value,f(e,n)-c(t,n))},We.prototype.reverse=function(){return this},We.prototype.indexOf=function(t){return W(this._value,t)?0:-1},We.prototype.lastIndexOf=function(t){return W(this._value,t)?this.size:-1},We.prototype.__iterate=function(t,e){for(var n=0;nt?this.count():this.size);var r=this.slice(0,t);return Ct(this,1===n?r:r.concat(i(arguments,2),this.slice(t+e)))},findLastIndex:function(t,e){var n=this.toKeyedSeq().findLastKey(t,e);return void 0===n?-1:n},first:function(){return this.get(0)},flatten:function(t){return Ct(this,bt(this,t,!1))},get:function(t,e){return t=u(this,t),0>t||this.size===1/0||void 0!==this.size&&t>this.size?e:this.find(function(e,n){return n===t},void 0,e)},has:function(t){return t=u(this,t),t>=0&&(void 0!==this.size?this.size===1/0||t-1&&t%1===0&&t<=Number.MAX_VALUE}var i=Function.prototype.bind;e.isString=function(t){return"string"==typeof t||"[object String]"===n(t)},e.isArray=Array.isArray||function(t){return"[object Array]"===n(t)},"function"!=typeof/./&&"object"!==("undefined"==typeof Int8Array?"undefined":De(Int8Array))?e.isFunction=function(t){return"function"==typeof t||!1}:e.isFunction=function(t){return"[object Function]"===toString.call(t)},e.isObject=function(t){var e="undefined"==typeof t?"undefined":De(t);return"function"===e||"object"===e&&!!t},e.extend=function(t){var e=arguments.length;if(!t||2>e)return t||{};for(var n=1;e>n;n++)for(var r=arguments[n],i=Object.keys(r),o=i.length,u=0;o>u;u++){var a=i[u];t[a]=r[a]}return t},e.clone=function(t){return e.isObject(t)?e.isArray(t)?t.slice():e.extend({},t):t},e.each=function(t,e,n){var i,o,u=t?t.length:0,a=-1;if(n&&(o=e,e=function(t,e,r){return o.call(n,t,e,r)}),r(u))for(;++ar;r++)n[r]=arguments[r];return new(i.apply(t,[null].concat(n)))};return e.__proto__=t,e.prototype=t.prototype,e}},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return c["default"].Iterable.isIterable(t)}function o(t){return i(t)||!(0,f.isObject)(t)}function u(t){return i(t)?t.toJS():t}function a(t){return i(t)?t:c["default"].fromJS(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.isImmutable=i,e.isImmutableValue=o,e.toJS=u,e.toImmutable=a;var s=n(3),c=r(s),f=n(4)},function(t,e,n){function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function u(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var a=function(){function t(t,e){for(var n=0;n0)){var e=this.reactorState.get("dirtyStores");if(0!==e.size){var n=c["default"].Set().withMutations(function(n){n.union(t.observerState.get("any")),e.forEach(function(e){var r=t.observerState.getIn(["stores",e]);r&&n.union(r)})});n.forEach(function(e){var n=t.observerState.getIn(["observersMap",e]);if(n){var r=n.get("getter"),i=n.get("handler"),o=p.evaluate(t.prevReactorState,r),u=p.evaluate(t.reactorState,r);t.prevReactorState=o.reactorState,t.reactorState=u.reactorState;var a=o.result,s=u.result;c["default"].is(a,s)||i.call(null,s)}});var r=p.resetDirtyStores(this.reactorState);this.prevReactorState=r,this.reactorState=r}}}},{key:"batchStart",value:function(){this.__batchDepth++}},{key:"batchEnd",value:function(){if(this.__batchDepth--,this.__batchDepth<=0){this.__isDispatching=!0;try{this.__notify()}catch(t){throw this.__isDispatching=!1,t}this.__isDispatching=!1}}}]),t}();e["default"]=(0,y.toFactory)(g),t.exports=e["default"]},function(t,e,n){function r(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function i(t,e){var n={};return(0,o.each)(e,function(e,r){n[r]=t.evaluate(e)}),n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(4);e["default"]=function(t){return{getInitialState:function(){return i(t,this.getDataBindings())},componentDidMount:function(){var e=this;this.__unwatchFns=[],(0,o.each)(this.getDataBindings(),function(n,i){var o=t.observe(n,function(t){e.setState(r({},i,t))});e.__unwatchFns.push(o)})},componentWillUnmount:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}},t.exports=e["default"]},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return new M({result:t,reactorState:e})}function o(t,e){return t.withMutations(function(t){(0,R.each)(e,function(e,n){t.getIn(["stores",n])&&console.warn("Store already defined for id = "+n);var r=e.getInitialState();if(void 0===r&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store getInitialState() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,D.isImmutableValue)(r))throw new Error("Store getInitialState() must return an immutable value, did you forget to call toImmutable");t.update("stores",function(t){return t.set(n,e)}).update("state",function(t){return t.set(n,r)}).update("dirtyStores",function(t){return t.add(n)}).update("storeStates",function(t){return I(t,[n])})}),b(t)})}function u(t,e){return t.withMutations(function(t){(0,R.each)(e,function(e,n){t.update("stores",function(t){return t.set(n,e)})})})}function a(t,e,n){if(void 0===e&&f(t,"throwOnUndefinedActionType"))throw new Error("`dispatch` cannot be called with an `undefined` action type.");var r=t.get("state"),i=t.get("dirtyStores"),o=r.withMutations(function(r){A["default"].dispatchStart(t,e,n),t.get("stores").forEach(function(o,u){var a=r.get(u),s=void 0;try{s=o.handle(a,e,n)}catch(c){throw A["default"].dispatchError(t,c.message),c}if(void 0===s&&f(t,"throwOnUndefinedStoreReturnValue")){var h="Store handler must return a value, did you forget a return statement";throw A["default"].dispatchError(t,h),new Error(h)}r.set(u,s),a!==s&&(i=i.add(u))}),A["default"].dispatchEnd(t,r,i)}),u=t.set("state",o).set("dirtyStores",i).update("storeStates",function(t){return I(t,i)});return b(u)}function s(t,e){var n=[],r=(0,D.toImmutable)({}).withMutations(function(r){(0,R.each)(e,function(e,i){var o=t.getIn(["stores",i]);if(o){var u=o.deserialize(e);void 0!==u&&(r.set(i,u),n.push(i))}})}),i=O["default"].Set(n);return t.update("state",function(t){return t.merge(r)}).update("dirtyStores",function(t){return t.union(i)}).update("storeStates",function(t){return I(t,n)})}function c(t,e,n){var r=e;(0,z.isKeyPath)(e)&&(e=(0,C.fromKeyPath)(e));var i=t.get("nextId"),o=(0,C.getStoreDeps)(e),u=O["default"].Map({id:i,storeDeps:o,getterKey:r,getter:e,handler:n}),a=void 0;return a=0===o.size?t.update("any",function(t){return t.add(i)}):t.withMutations(function(t){o.forEach(function(e){var n=["stores",e];t.hasIn(n)||t.setIn(n,O["default"].Set()),t.updateIn(["stores",e],function(t){return t.add(i)})})}),a=a.set("nextId",i+1).setIn(["observersMap",i],u),{observerState:a,entry:u}}function f(t,e){var n=t.getIn(["options",e]);if(void 0===n)throw new Error("Invalid option: "+e);return n}function h(t,e,n){var r=t.get("observersMap").filter(function(t){var r=t.get("getterKey"),i=!n||t.get("handler")===n;return i?(0,z.isKeyPath)(e)&&(0,z.isKeyPath)(r)?(0,z.isEqual)(e,r):e===r:!1});return t.withMutations(function(t){r.forEach(function(e){return l(t,e)})})}function l(t,e){return t.withMutations(function(t){var n=e.get("id"),r=e.get("storeDeps");0===r.size?t.update("any",function(t){return t.remove(n)}):r.forEach(function(e){t.updateIn(["stores",e],function(t){return t?t.remove(n):t})}),t.removeIn(["observersMap",n])})}function p(t){var e=t.get("state");return t.withMutations(function(t){var n=t.get("stores"),r=n.keySeq().toJS();n.forEach(function(n,r){var i=e.get(r),o=n.handleReset(i);if(void 0===o&&f(t,"throwOnUndefinedStoreReturnValue"))throw new Error("Store handleReset() must return a value, did you forget a return statement");if(f(t,"throwOnNonImmutableStore")&&!(0,D.isImmutableValue)(o))throw new Error("Store reset state must be an immutable value, did you forget to call toImmutable");t.setIn(["state",r],o)}),t.update("storeStates",function(t){return I(t,r)}),v(t)})}function _(t,e){var n=t.get("state");if((0,z.isKeyPath)(e))return i(n.getIn(e),t);if(!(0,C.isGetter)(e))throw new Error("evaluate must be passed a keyPath or Getter");if(g(t,e))return i(E(t,e),t);var r=(0,C.getDeps)(e).map(function(e){return _(t,e).result}),o=(0,C.getComputeFn)(e).apply(null,r);return i(o,m(t,e,o))}function d(t){var e={};return t.get("stores").forEach(function(n,r){var i=t.getIn(["state",r]),o=n.serialize(i);void 0!==o&&(e[r]=o)}),e}function v(t){return t.set("dirtyStores",O["default"].Set())}function y(t){return t}function S(t,e){var n=y(e);return t.getIn(["cache",n])}function g(t,e){var n=S(t,e);if(!n)return!1;var r=n.get("storeStates");return 0===r.size?!1:r.every(function(e,n){return t.getIn(["storeStates",n])===e})}function m(t,e,n){var r=y(e),i=t.get("dispatchId"),o=(0,C.getStoreDeps)(e),u=(0,D.toImmutable)({}).withMutations(function(e){o.forEach(function(n){var r=t.getIn(["storeStates",n]);e.set(n,r)})});return t.setIn(["cache",r],O["default"].Map({value:n,storeStates:u,dispatchId:i}))}function E(t,e){var n=y(e);return t.getIn(["cache",n,"value"])}function b(t){return t.update("dispatchId",function(t){return t+1})}function I(t,e){return t.withMutations(function(t){e.forEach(function(e){var n=t.has(e)?t.get(e)+1:1;t.set(e,n)})})}Object.defineProperty(e,"__esModule",{value:!0}),e.registerStores=o,e.replaceStores=u,e.dispatch=a,e.loadState=s,e.addObserver=c,e.getOption=f,e.removeObserver=h,e.removeObserverByEntry=l,e.reset=p,e.evaluate=_,e.serialize=d,e.resetDirtyStores=v;var w=n(3),O=r(w),T=n(9),A=r(T),D=n(5),C=n(10),z=n(11),R=n(4),M=O["default"].Record({result:null,reactorState:null})},function(t,e,n){var r=n(8);e.dispatchStart=function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.groupCollapsed("Dispatch: %s",e),console.group("payload"),console.debug(n),console.groupEnd())},e.dispatchError=function(t,e){(0,r.getOption)(t,"logDispatches")&&console.group&&(console.debug("Dispatch error: "+e),console.groupEnd())},e.dispatchEnd=function(t,e,n){(0,r.getOption)(t,"logDispatches")&&console.group&&((0,r.getOption)(t,"logDirtyStores")&&console.log("Stores updated:",n.toList().toJS()),(0,r.getOption)(t,"logAppState")&&console.debug("Dispatch done, new state: ",e.toJS()),console.groupEnd())}},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return(0,l.isArray)(t)&&(0,l.isFunction)(t[t.length-1])}function o(t){return t[t.length-1]}function u(t){return t.slice(0,t.length-1)}function a(t,e){e||(e=h["default"].Set());var n=h["default"].Set().withMutations(function(e){if(!i(t))throw new Error("getFlattenedDeps must be passed a Getter");u(t).forEach(function(t){if((0,p.isKeyPath)(t))e.add((0,f.List)(t));else{if(!i(t))throw new Error("Invalid getter, each dependency must be a KeyPath or Getter");e.union(a(t))}})});return e.union(n)}function s(t){if(!(0,p.isKeyPath)(t))throw new Error("Cannot create Getter from KeyPath: "+t);return[t,_]}function c(t){if(t.hasOwnProperty("__storeDeps"))return t.__storeDeps;var e=a(t).map(function(t){return t.first()}).filter(function(t){return!!t});return Object.defineProperty(t,"__storeDeps",{enumerable:!1,configurable:!1,writable:!1,value:e}),e}Object.defineProperty(e,"__esModule",{value:!0});var f=n(3),h=r(f),l=n(4),p=n(11),_=function(t){return t};e["default"]={isGetter:i,getComputeFn:o,getFlattenedDeps:a,getStoreDeps:c,getDeps:u,fromKeyPath:s},t.exports=e["default"]},function(t,e,n){function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return(0,s.isArray)(t)&&!(0,s.isFunction)(t[t.length-1])}function o(t,e){var n=a["default"].List(t),r=a["default"].List(e);return a["default"].is(n,r)}Object.defineProperty(e,"__esModule",{value:!0}),e.isKeyPath=i,e.isEqual=o;var u=n(3),a=r(u),s=n(4)},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0});var r=n(3),i=(0,r.Map)({logDispatches:!1,logAppState:!1,logDirtyStores:!1,throwOnUndefinedActionType:!1,throwOnUndefinedStoreReturnValue:!1,throwOnNonImmutableStore:!1,throwOnDispatchInDispatch:!1});e.PROD_OPTIONS=i;var o=(0,r.Map)({logDispatches:!0,logAppState:!0,logDirtyStores:!0,throwOnUndefinedActionType:!0,throwOnUndefinedStoreReturnValue:!0,throwOnNonImmutableStore:!0,throwOnDispatchInDispatch:!0});e.DEBUG_OPTIONS=o;var u=(0,r.Record)({dispatchId:0,state:(0,r.Map)(),stores:(0,r.Map)(),cache:(0,r.Map)(),storeStates:(0,r.Map)(),dirtyStores:(0,r.Set)(),debug:!1,options:i});e.ReactorState=u;var a=(0,r.Record)({any:(0,r.Set)(),stores:(0,r.Map)({}),observersMap:(0,r.Map)({}),nextId:1});e.ObserverState=a}])})}),Re=ze&&"object"===("undefined"==typeof ze?"undefined":De(ze))&&"default"in ze?ze["default"]:ze,Me=o(function(t){var e=function(t){var e,n={};if(!(t instanceof Object)||Array.isArray(t))throw new Error("keyMirror(...): Argument must be an object.");for(e in t)t.hasOwnProperty(e)&&(n[e]=e);return n};t.exports=e}),je=Me&&"object"===("undefined"==typeof Me?"undefined":De(Me))&&"default"in Me?Me["default"]:Me,ke=je({VALIDATING_AUTH_TOKEN:null,VALID_AUTH_TOKEN:null,INVALID_AUTH_TOKEN:null,LOG_OUT:null}),Le=Re.Store,Ne=Re.toImmutable,Ue=new Le({getInitialState:function(){return Ne({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(ke.VALIDATING_AUTH_TOKEN,u),this.on(ke.VALID_AUTH_TOKEN,a),this.on(ke.INVALID_AUTH_TOKEN,s)}}),He=Re.Store,Pe=Re.toImmutable,xe=new He({getInitialState:function(){return Pe({authToken:null,host:""})},initialize:function(){this.on(ke.VALID_AUTH_TOKEN,c),this.on(ke.LOG_OUT,f)}}),Ve=Re.Store,qe=new Ve({getInitialState:function(){return!0},initialize:function(){this.on(ke.VALID_AUTH_TOKEN,h)}}),Fe=je({STREAM_START:null,STREAM_STOP:null,STREAM_ERROR:null}),Ge="object"===("undefined"==typeof window?"undefined":De(window))&&"EventSource"in window,Ke=Re.Store,Ye=Re.toImmutable,Be=new Ke({getInitialState:function(){return Ye({isSupported:Ge,isStreaming:!1,useStreaming:!0,hasError:!1})},initialize:function(){this.on(Fe.STREAM_START,l),this.on(Fe.STREAM_STOP,p),this.on(Fe.STREAM_ERROR,_),this.on(Fe.LOG_OUT,d)}}),Je=o(function(t){function e(){return(new Date).getTime()}t.exports=Date.now||e}),We=Je&&"object"===("undefined"==typeof Je?"undefined":De(Je))&&"default"in Je?Je["default"]:Je,Xe=o(function(t){var e=We;t.exports=function(t,n,r){function i(){var f=e()-s;n>f&&f>0?o=setTimeout(i,n-f):(o=null,r||(c=t.apply(a,u),o||(a=u=null)))}var o,u,a,s,c;return null==n&&(n=100),function(){a=this,u=arguments,s=e();var f=r&&!o;return o||(o=setTimeout(i,n)),f&&(c=t.apply(a,u),a=u=null),c}}}),Qe=Xe&&"object"===("undefined"==typeof Xe?"undefined":De(Xe))&&"default"in Xe?Xe["default"]:Xe,Ze=je({API_FETCH_ALL_START:null,API_FETCH_ALL_SUCCESS:null,API_FETCH_ALL_FAIL:null,SYNC_SCHEDULED:null,SYNC_SCHEDULE_CANCELLED:null}),$e=Re.Store,tn=new $e({getInitialState:function(){return!0},initialize:function(){this.on(Ze.API_FETCH_ALL_START,function(){return!0}),this.on(Ze.API_FETCH_ALL_SUCCESS,function(){return!1}),this.on(Ze.API_FETCH_ALL_FAIL,function(){return!1}),this.on(Ze.LOG_OUT,function(){return!1})}}),en=Re.Store,nn=new en({getInitialState:function(){return!1},initialize:function(){this.on(Ze.SYNC_SCHEDULED,function(){return!0}),this.on(Ze.SYNC_SCHEDULE_CANCELLED,function(){return!1}),this.on(Ze.LOG_OUT,function(){return!1})}}),rn=je({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null}),on=Re.Store,un=Re.toImmutable,an=new on({getInitialState:function(){return un({})},initialize:function(){var t=this;this.on(rn.API_FETCH_SUCCESS,v),this.on(rn.API_SAVE_SUCCESS,v),this.on(rn.API_DELETE_SUCCESS,y),this.on(rn.LOG_OUT,function(){return t.getInitialState()})}}),sn=o(function(t){function e(t){if(null===t||void 0===t)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(t)}function n(){try{if(!Object.assign)return!1;var t=new String("abc");if(t[5]="de","5"===Object.getOwnPropertyNames(t)[0])return!1;for(var e={},n=0;10>n;n++)e["_"+String.fromCharCode(n)]=n;var r=Object.getOwnPropertyNames(e).map(function(t){return e[t]});if("0123456789"!==r.join(""))return!1;var i={};return"abcdefghijklmnopqrst".split("").forEach(function(t){i[t]=t}),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},i)).join("")}catch(o){return!1}}var r=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;t.exports=n()?Object.assign:function(t,n){for(var o,u,a=e(t),s=1;s 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 diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 34ef06d3ee9ad7..08d2cdea3a42d2 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2,7 +2,7 @@ },_distributeDirtyRoots:function(){for(var e,t=this.shadyRoot._dirtyRoots,o=0,i=t.length;i>o&&(e=t[o]);o++)e._distributeContent();this.shadyRoot._dirtyRoots=[]},_finishDistribute:function(){if(this._useContent){if(this.shadyRoot._distributionClean=!0,h.hasInsertionPoint(this.shadyRoot))this._composeTree(),d(this.shadyRoot);else if(this.shadyRoot._hasDistributed){var e=this._composeNode(this);this._updateChildNodes(this,e)}else u.Composed.clearChildNodes(this),this.appendChild(this.shadyRoot);this.shadyRoot._hasDistributed||a(this),this.shadyRoot._hasDistributed=!0}},elementMatches:function(e,t){return t=t||this,h.matchesSelector.call(t,e)},_resetDistribution:function(){for(var e=u.Logical.getChildNodes(this),o=0;os&&(i=n[s]);s++)this._distributeInsertionPoint(i,t),o(i,this)},_distributeInsertionPoint:function(t,o){for(var i,n=!1,s=0,r=o.length;r>s;s++)i=o[s],i&&this._matchesContentSelect(i,t)&&(e(i,t),o[s]=void 0,n=!0);if(!n)for(var d=u.Logical.getChildNodes(t),a=0;ai&&(e=o[i]);i++)t=u.Logical.getParentNode(e),t._useContent||t===this||t===this.shadyRoot||this._updateChildNodes(t,this._composeNode(t))},_composeNode:function(e){for(var t=[],o=u.Logical.getChildNodes(e.shadyRoot||e),s=0;s0?~setTimeout(e,t):(this._twiddle.textContent=this._twiddleContent++,this._callbacks.push(e),this._currVal++)},cancel:function(e){if(0>e)clearTimeout(~e);else{var t=e-this._lastVal;if(t>=0){if(!this._callbacks[t])throw"invalid async handle: "+e;this._callbacks[t]=null}}},_atEndOfMicrotask:function(){for(var e=this._callbacks.length,t=0;e>t;t++){var o=this._callbacks[t];if(o)try{o()}catch(i){throw t++,this._callbacks.splice(0,t),this._lastVal+=t,this._twiddle.textContent=this._twiddleContent++,i}}this._callbacks.splice(0,e),this._lastVal+=e}},new window.MutationObserver(function(){Polymer.Async._atEndOfMicrotask()}).observe(Polymer.Async._twiddle,{characterData:!0}),Polymer.Debounce=function(){function e(e,t,i){return e?e.stop():e=new o(this),e.go(t,i),e}var t=Polymer.Async,o=function(e){this.context=e;var t=this;this.boundComplete=function(){t.complete()}};return o.prototype={go:function(e,o){var i;this.finish=function(){t.cancel(i)},i=t.run(this.boundComplete,o),this.callback=e},stop:function(){this.finish&&(this.finish(),this.finish=null)},complete:function(){this.finish&&(this.stop(),this.callback.call(this.context))}},e}(),Polymer.Base._addFeature({_setupDebouncers:function(){this._debouncers={}},debounce:function(e,t,o){return this._debouncers[e]=Polymer.Debounce.call(this,this._debouncers[e],t,o)},isDebouncerActive:function(e){var t=this._debouncers[e];return!(!t||!t.finish)},flushDebouncer:function(e){var t=this._debouncers[e];t&&t.complete()},cancelDebouncer:function(e){var t=this._debouncers[e];t&&t.stop()}}),Polymer.DomModule=document.createElement("dom-module"),Polymer.Base._addFeature({_registerFeatures:function(){this._prepIs(),this._prepBehaviors(),this._prepConstructor(),this._prepTemplate(),this._prepShady(),this._prepPropertyInfo()},_prepBehavior:function(e){this._addHostAttributes(e.hostAttributes)},_initFeatures:function(){this._registerHost(),this._template&&(this._poolContent(),this._beginHosting(),this._stampTemplate(),this._endHosting()),this._marshalHostAttributes(),this._setupDebouncers(),this._marshalBehaviors(),this._tryReady()},_marshalBehavior:function(e){}})- \ No newline at end of file +return this._week.doy}function Kt(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),"d")}function en(e){var t=Oe(this,1,4).week;return null==e?t:this.add(7*(e-t),"d")}function tn(e,t){return"string"!=typeof e?e:isNaN(e)?(e=t.weekdaysParse(e),"number"==typeof e?e:null):parseInt(e,10)}function nn(e,t){return i(this._weekdays)?this._weekdays[e.day()]:this._weekdays[this._weekdays.isFormat.test(t)?"format":"standalone"][e.day()]}function rn(e){return this._weekdaysShort[e.day()]}function an(e){return this._weekdaysMin[e.day()]}function sn(e,t,n){var i,r,a,s=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],i=0;7>i;++i)a=u([2e3,1]).day(i),this._minWeekdaysParse[i]=this.weekdaysMin(a,"").toLocaleLowerCase(),this._shortWeekdaysParse[i]=this.weekdaysShort(a,"").toLocaleLowerCase(),this._weekdaysParse[i]=this.weekdays(a,"").toLocaleLowerCase();return n?"dddd"===t?(r=mi.call(this._weekdaysParse,s),-1!==r?r:null):"ddd"===t?(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:null):(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:null):"dddd"===t?(r=mi.call(this._weekdaysParse,s),-1!==r?r:(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:null))):"ddd"===t?(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:(r=mi.call(this._weekdaysParse,s),-1!==r?r:(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:null))):(r=mi.call(this._minWeekdaysParse,s),-1!==r?r:(r=mi.call(this._weekdaysParse,s),-1!==r?r:(r=mi.call(this._shortWeekdaysParse,s),-1!==r?r:null)))}function on(e,t,n){var i,r,a;if(this._weekdaysParseExact)return sn.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),i=0;7>i;i++){if(r=u([2e3,1]).day(i),n&&!this._fullWeekdaysParse[i]&&(this._fullWeekdaysParse[i]=new RegExp("^"+this.weekdays(r,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[i]=new RegExp("^"+this.weekdaysShort(r,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[i]=new RegExp("^"+this.weekdaysMin(r,"").replace(".",".?")+"$","i")),this._weekdaysParse[i]||(a="^"+this.weekdays(r,"")+"|^"+this.weekdaysShort(r,"")+"|^"+this.weekdaysMin(r,""),this._weekdaysParse[i]=new RegExp(a.replace(".",""),"i")),n&&"dddd"===t&&this._fullWeekdaysParse[i].test(e))return i;if(n&&"ddd"===t&&this._shortWeekdaysParse[i].test(e))return i;if(n&&"dd"===t&&this._minWeekdaysParse[i].test(e))return i;if(!n&&this._weekdaysParse[i].test(e))return i}}function un(e){if(!this.isValid())return null!=e?this:NaN;var t=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(e=tn(e,this.localeData()),this.add(e-t,"d")):t}function cn(e){if(!this.isValid())return null!=e?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==e?t:this.add(e-t,"d")}function ln(e){return this.isValid()?null==e?this.day()||7:this.day(this.day()%7?e:e-7):null!=e?this:NaN}function dn(e){return this._weekdaysParseExact?(s(this,"_weekdaysRegex")||mn.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex}function hn(e){return this._weekdaysParseExact?(s(this,"_weekdaysRegex")||mn.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex}function fn(e){return this._weekdaysParseExact?(s(this,"_weekdaysRegex")||mn.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex}function mn(){function e(e,t){return t.length-e.length}var t,n,i,r,a,s=[],o=[],c=[],l=[];for(t=0;7>t;t++)n=u([2e3,1]).day(t),i=this.weekdaysMin(n,""),r=this.weekdaysShort(n,""),a=this.weekdays(n,""),s.push(i),o.push(r),c.push(a),l.push(i),l.push(r),l.push(a);for(s.sort(e),o.sort(e),c.sort(e),l.sort(e),t=0;7>t;t++)o[t]=X(o[t]),c[t]=X(c[t]),l[t]=X(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+c.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+s.join("|")+")","i")}function pn(e){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?t:this.add(e-t,"d")}function yn(){return this.hours()%12||12}function gn(){return this.hours()||24}function vn(e,t){R(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function _n(e,t){return t._meridiemParse}function bn(e){return"p"===(e+"").toLowerCase().charAt(0)}function wn(e,t,n){return e>11?n?"pm":"PM":n?"am":"AM"}function Sn(e,t){t[zi]=v(1e3*("0."+e))}function On(){return this._isUTC?"UTC":""}function kn(){return this._isUTC?"Coordinated Universal Time":""}function Dn(e){return He(1e3*e)}function Cn(){return He.apply(null,arguments).parseZone()}function Mn(e,t,n){var i=this._calendar[e];return O(i)?i.call(t,n):i}function jn(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.replace(/MMMM|MM|DD|dddd/g,function(e){return e.slice(1)}),this._longDateFormat[e])}function Tn(){return this._invalidDate}function xn(e){return this._ordinal.replace("%d",e)}function Yn(e){return e}function Pn(e,t,n,i){var r=this._relativeTime[n];return O(r)?r(e,t,n,i):r.replace(/%d/i,e)}function In(e,t){var n=this._relativeTime[e>0?"future":"past"];return O(n)?n(t):n.replace(/%s/i,t)}function An(e,t,n,i){var r=A(),a=u().set(i,t);return r[n](a,e)}function Nn(e,t,n){if("number"==typeof e&&(t=e,e=void 0),e=e||"",null!=t)return An(e,t,n,"month");var i,r=[];for(i=0;12>i;i++)r[i]=An(e,i,n,"month");return r}function Bn(e,t,n,i){"boolean"==typeof e?("number"==typeof t&&(n=t,t=void 0),t=t||""):(t=e,n=t,e=!1,"number"==typeof t&&(n=t,t=void 0),t=t||"");var r=A(),a=e?r._week.dow:0;if(null!=n)return An(t,(n+a)%7,i,"day");var s,o=[];for(s=0;7>s;s++)o[s]=An(t,(s+a)%7,i,"day");return o}function Hn(e,t){return Nn(e,t,"months")}function Ln(e,t){return Nn(e,t,"monthsShort")}function Gn(e,t,n){return Bn(e,t,n,"weekdays")}function En(e,t,n){return Bn(e,t,n,"weekdaysShort")}function Wn(e,t,n){return Bn(e,t,n,"weekdaysMin")}function Vn(){var e=this._data;return this._milliseconds=Gr(this._milliseconds),this._days=Gr(this._days),this._months=Gr(this._months),e.milliseconds=Gr(e.milliseconds),e.seconds=Gr(e.seconds),e.minutes=Gr(e.minutes),e.hours=Gr(e.hours),e.months=Gr(e.months),e.years=Gr(e.years),this}function Fn(e,t,n,i){var r=rt(t,n);return e._milliseconds+=i*r._milliseconds,e._days+=i*r._days,e._months+=i*r._months,e._bubble()}function Rn(e,t){return Fn(this,e,t,1)}function Un(e,t){return Fn(this,e,t,-1)}function zn(e){return 0>e?Math.floor(e):Math.ceil(e)}function $n(){var e,t,n,i,r,a=this._milliseconds,s=this._days,o=this._months,u=this._data;return a>=0&&s>=0&&o>=0||0>=a&&0>=s&&0>=o||(a+=864e5*zn(Jn(o)+s),s=0,o=0),u.milliseconds=a%1e3,e=g(a/1e3),u.seconds=e%60,t=g(e/60),u.minutes=t%60,n=g(t/60),u.hours=n%24,s+=g(n/24),r=g(Zn(s)),o+=r,s-=zn(Jn(r)),i=g(o/12),o%=12,u.days=s,u.months=o,u.years=i,this}function Zn(e){return 4800*e/146097}function Jn(e){return 146097*e/4800}function qn(e){var t,n,i=this._milliseconds;if(e=H(e),"month"===e||"year"===e)return t=this._days+i/864e5,n=this._months+Zn(t),"month"===e?n:n/12;switch(t=this._days+Math.round(Jn(this._months)),e){case"week":return t/7+i/6048e5;case"day":return t+i/864e5;case"hour":return 24*t+i/36e5;case"minute":return 1440*t+i/6e4;case"second":return 86400*t+i/1e3;case"millisecond":return Math.floor(864e5*t)+i;default:throw new Error("Unknown unit "+e)}}function Qn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*v(this._months/12)}function Xn(e){return function(){return this.as(e)}}function Kn(e){return e=H(e),this[e+"s"]()}function ei(e){return function(){return this._data[e]}}function ti(){return g(this.days()/7)}function ni(e,t,n,i,r){return r.relativeTime(t||1,!!n,e,i)}function ii(e,t,n){var i=rt(e).abs(),r=ta(i.as("s")),a=ta(i.as("m")),s=ta(i.as("h")),o=ta(i.as("d")),u=ta(i.as("M")),c=ta(i.as("y")),l=r=a&&["m"]||a=s&&["h"]||s=o&&["d"]||o=u&&["M"]||u=c&&["y"]||["yy",c];return l[2]=t,l[3]=+e>0,l[4]=n,ni.apply(null,l)}function ri(e,t){return void 0===na[e]?!1:void 0===t?na[e]:(na[e]=t,!0)}function ai(e){var t=this.localeData(),n=ii(this,!e,t);return e&&(n=t.pastFuture(+this,n)),t.postformat(n)}function si(){var e,t,n,i=ia(this._milliseconds)/1e3,r=ia(this._days),a=ia(this._months);e=g(i/60),t=g(e/60),i%=60,e%=60,n=g(a/12),a%=12;var s=n,o=a,u=r,c=t,l=e,d=i,h=this.asSeconds();return h?(0>h?"-":"")+"P"+(s?s+"Y":"")+(o?o+"M":"")+(u?u+"D":"")+(c||l||d?"T":"")+(c?c+"H":"")+(l?l+"M":"")+(d?d+"S":""):"P0D"}var oi,ui;ui=Array.prototype.some?Array.prototype.some:function(e){for(var t=Object(this),n=t.length>>>0,i=0;n>i;i++)if(i in t&&e.call(this,t[i],i,t))return!0;return!1};var ci=t.momentProperties=[],li=!1,di={};t.suppressDeprecationWarnings=!1,t.deprecationHandler=null;var hi;hi=Object.keys?Object.keys:function(e){var t,n=[];for(t in e)s(e,t)&&n.push(t);return n};var fi,mi,pi={},yi={},gi=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,vi=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,_i={},bi={},wi=/\d/,Si=/\d\d/,Oi=/\d{3}/,ki=/\d{4}/,Di=/[+-]?\d{6}/,Ci=/\d\d?/,Mi=/\d\d\d\d?/,ji=/\d\d\d\d\d\d?/,Ti=/\d{1,3}/,xi=/\d{1,4}/,Yi=/[+-]?\d{1,6}/,Pi=/\d+/,Ii=/[+-]?\d+/,Ai=/Z|[+-]\d\d:?\d\d/gi,Ni=/Z|[+-]\d\d(?::?\d\d)?/gi,Bi=/[+-]?\d+(\.\d{1,3})?/,Hi=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Li={},Gi={},Ei=0,Wi=1,Vi=2,Fi=3,Ri=4,Ui=5,zi=6,$i=7,Zi=8;mi=Array.prototype.indexOf?Array.prototype.indexOf:function(e){var t;for(t=0;t=e?""+e:"+"+e}),R(0,["YY",2],0,function(){return this.year()%100}),R(0,["YYYY",4],0,"year"),R(0,["YYYYY",5],0,"year"),R(0,["YYYYYY",6,!0],0,"year"),B("year","y"),J("Y",Ii),J("YY",Ci,Si),J("YYYY",xi,ki),J("YYYYY",Yi,Di),J("YYYYYY",Yi,Di),K(["YYYYY","YYYYYY"],Ei),K("YYYY",function(e,n){n[Ei]=2===e.length?t.parseTwoDigitYear(e):v(e)}),K("YY",function(e,n){n[Ei]=t.parseTwoDigitYear(e)}),K("Y",function(e,t){t[Ei]=parseInt(e,10)}),t.parseTwoDigitYear=function(e){return v(e)+(v(e)>68?1900:2e3)};var sr=G("FullYear",!0);t.ISO_8601=function(){};var or=w("moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var e=He.apply(null,arguments);return this.isValid()&&e.isValid()?this>e?this:e:h()}),ur=w("moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var e=He.apply(null,arguments);return this.isValid()&&e.isValid()?e>this?this:e:h()}),cr=function(){return Date.now?Date.now():+new Date};Fe("Z",":"),Fe("ZZ",""),J("Z",Ni),J("ZZ",Ni),K(["Z","ZZ"],function(e,t,n){n._useUTC=!0,n._tzm=Re(Ni,e)});var lr=/([\+\-]|\d\d)/gi;t.updateOffset=function(){};var dr=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/,hr=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;rt.fn=We.prototype;var fr=ct(1,"add"),mr=ct(-1,"subtract");t.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",t.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var pr=w("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});R(0,["gg",2],0,function(){return this.weekYear()%100}),R(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Vt("gggg","weekYear"),Vt("ggggg","weekYear"),Vt("GGGG","isoWeekYear"),Vt("GGGGG","isoWeekYear"),B("weekYear","gg"),B("isoWeekYear","GG"),J("G",Ii),J("g",Ii),J("GG",Ci,Si),J("gg",Ci,Si),J("GGGG",xi,ki),J("gggg",xi,ki),J("GGGGG",Yi,Di),J("ggggg",Yi,Di),ee(["gggg","ggggg","GGGG","GGGGG"],function(e,t,n,i){t[i.substr(0,2)]=v(e)}),ee(["gg","GG"],function(e,n,i,r){n[r]=t.parseTwoDigitYear(e)}),R("Q",0,"Qo","quarter"),B("quarter","Q"),J("Q",wi),K("Q",function(e,t){t[Wi]=3*(v(e)-1)}),R("w",["ww",2],"wo","week"),R("W",["WW",2],"Wo","isoWeek"),B("week","w"),B("isoWeek","W"),J("w",Ci),J("ww",Ci,Si),J("W",Ci),J("WW",Ci,Si),ee(["w","ww","W","WW"],function(e,t,n,i){t[i.substr(0,1)]=v(e)});var yr={dow:0,doy:6};R("D",["DD",2],"Do","date"),B("date","D"),J("D",Ci),J("DD",Ci,Si),J("Do",function(e,t){return e?t._ordinalParse:t._ordinalParseLenient}),K(["D","DD"],Vi),K("Do",function(e,t){t[Vi]=v(e.match(Ci)[0],10)});var gr=G("Date",!0);R("d",0,"do","day"),R("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),R("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),R("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),R("e",0,0,"weekday"),R("E",0,0,"isoWeekday"),B("day","d"),B("weekday","e"),B("isoWeekday","E"),J("d",Ci),J("e",Ci),J("E",Ci),J("dd",function(e,t){return t.weekdaysMinRegex(e)}),J("ddd",function(e,t){return t.weekdaysShortRegex(e)}),J("dddd",function(e,t){return t.weekdaysRegex(e)}),ee(["dd","ddd","dddd"],function(e,t,n,i){var r=n._locale.weekdaysParse(e,i,n._strict);null!=r?t.d=r:l(n).invalidWeekday=e}),ee(["d","e","E"],function(e,t,n,i){t[i]=v(e)});var vr="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),_r="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),br="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),wr=Hi,Sr=Hi,Or=Hi;R("DDD",["DDDD",3],"DDDo","dayOfYear"),B("dayOfYear","DDD"),J("DDD",Ti),J("DDDD",Oi),K(["DDD","DDDD"],function(e,t,n){n._dayOfYear=v(e)}),R("H",["HH",2],0,"hour"),R("h",["hh",2],0,yn),R("k",["kk",2],0,gn),R("hmm",0,0,function(){return""+yn.apply(this)+F(this.minutes(),2)}),R("hmmss",0,0,function(){return""+yn.apply(this)+F(this.minutes(),2)+F(this.seconds(),2)}),R("Hmm",0,0,function(){return""+this.hours()+F(this.minutes(),2)}),R("Hmmss",0,0,function(){return""+this.hours()+F(this.minutes(),2)+F(this.seconds(),2)}),vn("a",!0),vn("A",!1),B("hour","h"),J("a",_n),J("A",_n),J("H",Ci),J("h",Ci),J("HH",Ci,Si),J("hh",Ci,Si),J("hmm",Mi),J("hmmss",ji),J("Hmm",Mi),J("Hmmss",ji),K(["H","HH"],Fi),K(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),K(["h","hh"],function(e,t,n){t[Fi]=v(e),l(n).bigHour=!0}),K("hmm",function(e,t,n){var i=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i)),l(n).bigHour=!0}),K("hmmss",function(e,t,n){var i=e.length-4,r=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i,2)),t[Ui]=v(e.substr(r)),l(n).bigHour=!0}),K("Hmm",function(e,t,n){var i=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i))}),K("Hmmss",function(e,t,n){var i=e.length-4,r=e.length-2;t[Fi]=v(e.substr(0,i)),t[Ri]=v(e.substr(i,2)),t[Ui]=v(e.substr(r))});var kr=/[ap]\.?m?\.?/i,Dr=G("Hours",!0);R("m",["mm",2],0,"minute"),B("minute","m"),J("m",Ci),J("mm",Ci,Si),K(["m","mm"],Ri);var Cr=G("Minutes",!1);R("s",["ss",2],0,"second"),B("second","s"),J("s",Ci),J("ss",Ci,Si),K(["s","ss"],Ui);var Mr=G("Seconds",!1);R("S",0,0,function(){return~~(this.millisecond()/100)}),R(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),R(0,["SSS",3],0,"millisecond"),R(0,["SSSS",4],0,function(){return 10*this.millisecond()}),R(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),R(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),R(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),R(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),R(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),B("millisecond","ms"),J("S",Ti,wi),J("SS",Ti,Si),J("SSS",Ti,Oi);var jr;for(jr="SSSS";jr.length<=9;jr+="S")J(jr,Pi);for(jr="S";jr.length<=9;jr+="S")K(jr,Sn);var Tr=G("Milliseconds",!1);R("z",0,0,"zoneAbbr"),R("zz",0,0,"zoneName");var xr=p.prototype;xr.add=fr,xr.calendar=dt,xr.clone=ht,xr.diff=_t,xr.endOf=Yt,xr.format=Ot,xr.from=kt,xr.fromNow=Dt,xr.to=Ct,xr.toNow=Mt,xr.get=V,xr.invalidAt=Et,xr.isAfter=ft,xr.isBefore=mt,xr.isBetween=pt,xr.isSame=yt,xr.isSameOrAfter=gt,xr.isSameOrBefore=vt,xr.isValid=Lt,xr.lang=pr,xr.locale=jt,xr.localeData=Tt,xr.max=ur,xr.min=or,xr.parsingFlags=Gt,xr.set=V,xr.startOf=xt,xr.subtract=mr,xr.toArray=Nt,xr.toObject=Bt,xr.toDate=At,xr.toISOString=St,xr.toJSON=Ht,xr.toString=wt,xr.unix=It,xr.valueOf=Pt,xr.creationData=Wt,xr.year=sr,xr.isLeapYear=be,xr.weekYear=Ft,xr.isoWeekYear=Rt,xr.quarter=xr.quarters=Jt,xr.month=ue,xr.daysInMonth=ce,xr.week=xr.weeks=Kt,xr.isoWeek=xr.isoWeeks=en,xr.weeksInYear=zt,xr.isoWeeksInYear=Ut,xr.date=gr,xr.day=xr.days=un,xr.weekday=cn,xr.isoWeekday=ln,xr.dayOfYear=pn,xr.hour=xr.hours=Dr,xr.minute=xr.minutes=Cr,xr.second=xr.seconds=Mr,xr.millisecond=xr.milliseconds=Tr,xr.utcOffset=$e,xr.utc=Je,xr.local=qe,xr.parseZone=Qe,xr.hasAlignedHourOffset=Xe,xr.isDST=Ke,xr.isDSTShifted=et,xr.isLocal=tt,xr.isUtcOffset=nt,xr.isUtc=it,xr.isUTC=it,xr.zoneAbbr=On,xr.zoneName=kn,xr.dates=w("dates accessor is deprecated. Use date instead.",gr),xr.months=w("months accessor is deprecated. Use month instead",ue),xr.years=w("years accessor is deprecated. Use year instead",sr),xr.zone=w("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ze);var Yr=xr,Pr={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Ir={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Ar="Invalid date",Nr="%d",Br=/\d{1,2}/,Hr={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Lr=M.prototype;Lr._calendar=Pr,Lr.calendar=Mn,Lr._longDateFormat=Ir,Lr.longDateFormat=jn,Lr._invalidDate=Ar,Lr.invalidDate=Tn,Lr._ordinal=Nr,Lr.ordinal=xn,Lr._ordinalParse=Br,Lr.preparse=Yn,Lr.postformat=Yn,Lr._relativeTime=Hr,Lr.relativeTime=Pn,Lr.pastFuture=In,Lr.set=D,Lr.months=ie,Lr._months=qi,Lr.monthsShort=re,Lr._monthsShort=Qi,Lr.monthsParse=se,Lr._monthsRegex=Ki,Lr.monthsRegex=de,Lr._monthsShortRegex=Xi,Lr.monthsShortRegex=le,Lr.week=qt,Lr._week=yr,Lr.firstDayOfYear=Xt,Lr.firstDayOfWeek=Qt,Lr.weekdays=nn,Lr._weekdays=vr,Lr.weekdaysMin=an,Lr._weekdaysMin=br,Lr.weekdaysShort=rn,Lr._weekdaysShort=_r,Lr.weekdaysParse=on,Lr._weekdaysRegex=wr,Lr.weekdaysRegex=dn,Lr._weekdaysShortRegex=Sr,Lr.weekdaysShortRegex=hn,Lr._weekdaysMinRegex=Or,Lr.weekdaysMinRegex=fn,Lr.isPM=bn,Lr._meridiemParse=kr,Lr.meridiem=wn,Y("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10,n=1===v(e%100/10)?"th":1===t?"st":2===t?"nd":3===t?"rd":"th";return e+n}}),t.lang=w("moment.lang is deprecated. Use moment.locale instead.",Y),t.langData=w("moment.langData is deprecated. Use moment.localeData instead.",A);var Gr=Math.abs,Er=Xn("ms"),Wr=Xn("s"),Vr=Xn("m"),Fr=Xn("h"),Rr=Xn("d"),Ur=Xn("w"),zr=Xn("M"),$r=Xn("y"),Zr=ei("milliseconds"),Jr=ei("seconds"),qr=ei("minutes"),Qr=ei("hours"),Xr=ei("days"),Kr=ei("months"),ea=ei("years"),ta=Math.round,na={s:45,m:45,h:22,d:26,M:11},ia=Math.abs,ra=We.prototype;ra.abs=Vn,ra.add=Rn,ra.subtract=Un,ra.as=qn,ra.asMilliseconds=Er,ra.asSeconds=Wr,ra.asMinutes=Vr,ra.asHours=Fr,ra.asDays=Rr,ra.asWeeks=Ur,ra.asMonths=zr,ra.asYears=$r,ra.valueOf=Qn,ra._bubble=$n,ra.get=Kn,ra.milliseconds=Zr,ra.seconds=Jr,ra.minutes=qr,ra.hours=Qr,ra.days=Xr,ra.weeks=ti,ra.months=Kr,ra.years=ea,ra.humanize=ai,ra.toISOString=si,ra.toString=si,ra.toJSON=si,ra.locale=jt,ra.localeData=Tt,ra.toIsoString=w("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",si),ra.lang=pr,R("X",0,0,"unix"),R("x",0,0,"valueOf"),J("x",Ii),J("X",Bi),K("X",function(e,t,n){n._d=new Date(1e3*parseFloat(e,10))}),K("x",function(e,t,n){n._d=new Date(v(e))}),t.version="2.13.0",n(He),t.fn=Yr,t.min=Ge,t.max=Ee,t.now=cr,t.utc=u,t.unix=Dn,t.months=Hn,t.isDate=r,t.locale=Y,t.invalid=h,t.duration=rt,t.isMoment=y,t.weekdays=Gn,t.parseZone=Cn,t.localeData=A,t.isDuration=Ve,t.monthsShort=Ln,t.weekdaysMin=Wn,t.defineLocale=P,t.updateLocale=I,t.locales=N,t.weekdaysShort=En,t.normalizeUnits=H,t.relativeTimeThreshold=ri,t.prototype=Yr;var aa=t;return aa})}).call(t,n(73)(e))},,,,,,,function(e,t,n){"use strict";var i=n(0);n(35),new i.a({is:"ha-badges-card",properties:{hass:{type:Object},states:{type:Array}}})},function(e,t,n){"use strict";var i=n(0),r=1e4;new i.a({is:"ha-camera-card",properties:{hass:{type:Object},stateObj:{type:Object,observer:"updateCameraFeedSrc"},cameraFeedSrc:{type:String},imageLoaded:{type:Boolean,value:!0},elevation:{type:Number,value:1,reflectToAttribute:!0}},listeners:{tap:"cardTapped"},attached:function(){var e=this;this.timer=setInterval(function(){return e.updateCameraFeedSrc(e.stateObj)},r)},detached:function(){clearInterval(this.timer)},cardTapped:function(){var e=this;this.async(function(){return e.hass.moreInfoActions.selectEntity(e.stateObj.entityId)},1)},updateCameraFeedSrc:function(e){var t=e.attributes,n=(new Date).getTime();this.cameraFeedSrc=t.entity_picture+"&time="+n},imageLoadSuccess:function(){this.imageLoaded=!0},imageLoadFail:function(){this.imageLoaded=!1}})},function(e,t,n){"use strict";var i=n(0),r=n(3);n(27),n(29),n(30),new i.a({is:"ha-card-chooser",properties:{cardData:{type:Object,observer:"cardDataChanged"}},cardDataChanged:function(e){e&&n.i(r.a)(this,"HA-"+e.cardType.toUpperCase()+"-CARD",e)}})},function(e,t,n){"use strict";var i=n(0),r=n(9);n(33),n(4),new i.a({is:"ha-entities-card",properties:{hass:{type:Object},states:{type:Array},groupEntity:{type:Object}},computeTitle:function(e,t){return t?t.entityDisplay:e[0].domain.replace(/_/g," ")},computeTitleClass:function(e){var t="header horizontal layout center ";return e&&(t+="header-more-info"),t},entityTapped:function(e){var t=this;if(!e.target.classList.contains("paper-toggle-button")&&!e.target.classList.contains("paper-icon-button")&&(e.model||this.groupEntity)){e.stopPropagation();var n=void 0;n=e.model?e.model.item.entityId:this.groupEntity.entityId,this.async(function(){return t.hass.moreInfoActions.selectEntity(n)},1)}},showGroupToggle:function(e,t){var i=this;return!e||!t||"on"!==e.state&&"off"!==e.state?!1:t.reduce(function(e,t){return e+n.i(r.a)(i.hass,t.entityId)},0)>1}})},function(e,t,n){"use strict";var i=n(70),r=i&&i.__esModule?function(){return i["default"]}:function(){return i};n.d(r,"a",r);var a=n(0);new a.a({is:"ha-media_player-card",properties:{hass:{type:Object},stateObj:{type:Object},playerObj:{type:Object,computed:"computePlayerObj(stateObj)",observer:"playerObjChanged"},playbackControlIcon:{type:String,computed:"computePlaybackControlIcon(playerObj)"},elevation:{type:Number,value:1,reflectToAttribute:!0}},playerObjChanged:function(e){e.isOff||e.isIdle||(this.$.cover.style.backgroundImage=e.stateObj.attributes.entity_picture?"url("+e.stateObj.attributes.entity_picture+")":"")},computeBannerClasses:function(e){return r()({banner:!0,"is-off":e.isOff||e.isIdle,"no-cover":!e.stateObj.attributes.entity_picture})},computeHidePowerOnButton:function(e){return!e.isOff||!e.supportsTurnOn},computePlayerObj:function(e){return e.domainModel(this.hass)},computePlaybackControlIcon:function(e){return e.isPlaying?e.supportsPause?"mdi:pause":"mdi:stop":e.isPaused||e.isOff?"mdi:play":""},computeShowControls:function(e){return!e.isOff},handleNext:function(e){e.stopPropagation(),this.playerObj.nextTrack()},handleOpenMoreInfo:function(e){var t=this;e.stopPropagation(),this.async(function(){return t.hass.moreInfoActions.selectEntity(t.stateObj.entityId)},1)},handlePlaybackControl:function(e){e.stopPropagation(),this.playerObj.mediaPlayPause()},handlePrevious:function(e){e.stopPropagation(),this.playerObj.previousTrack()},handleTogglePower:function(e){e.stopPropagation(),this.playerObj.togglePower()}})},function(e,t,n){"use strict";var i=n(0),r=n(11);new i.a({is:"display-time",properties:{dateObj:{type:Object}},computeTime:function(e){return e?n.i(r.a)(e):""}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"ha-entity-marker",properties:{hass:{type:Object},entityId:{type:String,value:"",reflectToAttribute:!0},state:{type:Object,computed:"computeState(entityId)"},icon:{type:Object,computed:"computeIcon(state)"},image:{type:Object,computed:"computeImage(state)"},value:{type:String,computed:"computeValue(state)"}},listeners:{tap:"badgeTap"},badgeTap:function(e){var t=this;e.stopPropagation(),this.entityId&&this.async(function(){return window.hass.moreInfoActions.selectEntity(t.entityId)},1)},computeState:function(e){return e&&window.hass.reactor.evaluate(window.hass.entityGetters.byId(e))},computeIcon:function(e){return!e&&"home"},computeImage:function(e){return e&&e.attributes.entity_picture},computeValue:function(e){return e&&e.entityDisplay.split(" ").map(function(e){return e.substr(0,1)}).join("")}})},function(e,t,n){"use strict";var i=n(0),r=n(68);new i.a({is:"ha-entity-toggle",properties:{hass:{type:Object},stateObj:{type:Object},toggleChecked:{type:Boolean,value:!1},isOn:{type:Boolean,computed:"computeIsOn(stateObj)",observer:"isOnChanged"}},listeners:{tap:"onTap"},onTap:function(e){e.stopPropagation()},ready:function(){this.forceStateChange()},toggleChanged:function(e){var t=e.target.checked;t&&!this.isOn?this.callService(!0):!t&&this.isOn&&this.callService(!1)},isOnChanged:function(e){this.toggleChecked=e},forceStateChange:function(){this.toggleChecked===this.isOn&&(this.toggleChecked=!this.toggleChecked),this.toggleChecked=this.isOn},turnOn:function(){this.callService(!0)},turnOff:function(){this.callService(!1)},computeIsOn:function(e){return e&&-1===r.a.indexOf(e.state)},callService:function(e){var t=this,n=void 0,i=void 0;"lock"===this.stateObj.domain?(n="lock",i=e?"lock":"unlock"):"garage_door"===this.stateObj.domain?(n="garage_door",i=e?"open":"close"):(n="homeassistant",i=e?"turn_on":"turn_off");var r=this.hass.serviceActions.callService(n,i,{entity_id:this.stateObj.entityId});this.stateObj.attributes.assumed_state||r.then(function(){return t.forceStateChange()})}})},function(e,t,n){"use strict";var i=n(0),r=n(12);new i.a({is:"ha-state-icon",properties:{stateObj:{type:Object}},computeIcon:function(e){return n.i(r.a)(e)}})},function(e,t,n){"use strict";var i=n(0),r=n(5),a=n(12);new i.a({is:"ha-state-label-badge",properties:{hass:{type:Object},state:{type:Object,observer:"stateChanged"}},listeners:{tap:"badgeTap"},badgeTap:function(e){var t=this;e.stopPropagation(),this.async(function(){return t.hass.moreInfoActions.selectEntity(t.state.entityId)},1)},computeClasses:function(e){switch(e.domain){case"binary_sensor":case"updater":return"blue";default:return""}},computeValue:function(e){switch(e.domain){case"binary_sensor":case"device_tracker":case"updater":case"sun":case"alarm_control_panel":return null;case"sensor":default:return"unknown"===e.state?"-":e.state}},computeIcon:function(e){if("unavailable"===e.state)return null;switch(e.domain){case"alarm_control_panel":return"pending"===e.state?"mdi:clock-fast":"armed_away"===e.state?"mdi:nature":"armed_home"===e.state?"mdi:home-variant":n.i(r.a)(e.domain,e.state);case"binary_sensor":case"device_tracker":case"updater":return n.i(a.a)(e);case"sun":return"above_horizon"===e.state?n.i(r.a)(e.domain):"mdi:brightness-3";default:return null}},computeImage:function(e){return e.attributes.entity_picture||null},computeLabel:function(e){if("unavailable"===e.state)return"unavai";switch(e.domain){case"device_tracker":return"not_home"===e.state?"Away":e.state;case"alarm_control_panel":return"pending"===e.state?"pend":"armed_away"===e.state||"armed_home"===e.state?"armed":"disarm";default:return e.attributes.unit_of_measurement||null}},computeDescription:function(e){return e.entityDisplay},stateChanged:function(){this.updateStyles()}})},function(e,t,n){"use strict";var i=n(0);n(34),new i.a({is:"state-badge",properties:{stateObj:{type:Object,observer:"updateIconColor"}},updateIconColor:function(e){return e.attributes.entity_picture?(this.style.backgroundImage="url("+e.attributes.entity_picture+")",void(this.$.icon.style.display="none")):(this.style.backgroundImage="",this.$.icon.style.display="inline",void("light"===e.domain&&"on"===e.state&&e.attributes.rgb_color&&e.attributes.rgb_color.reduce(function(e,t){return e+t},0)<730?this.$.icon.style.color="rgb("+e.attributes.rgb_color.join(",")+")":this.$.icon.style.color=null))}})},function(e,t,n){"use strict";function i(e){return e in o?o[e]:30}function r(e){return"group"===e.domain?e.attributes.order:e.entityDisplay.toLowerCase()}var a=n(0),s=(n(26),n(28),{camera:4,media_player:3,persistent_notification:0}),o={configurator:-20,persistent_notification:-15,group:-10,a:-1,updater:0,sun:1,device_tracker:2,alarm_control_panel:3,sensor:5,binary_sensor:6};new a.a({is:"ha-cards",properties:{hass:{type:Object},showIntroduction:{type:Boolean,value:!1},columns:{type:Number,value:2},states:{type:Object},cards:{type:Object}},observers:["updateCards(columns, states, showIntroduction)"],updateCards:function(e,t,n){var i=this;this.debounce("updateCards",function(){i.cards=i.computeCards(e,t,n)},0)},computeCards:function(e,t,n){function a(e){return e.filter(function(e){return!(e.entityId in d)})}function o(e){for(var t=0,n=t;n1;var u=o(a);r.length>0&&h.columns[u].push({hass:c,cardType:"entities",states:r,groupEntity:n}),i.forEach(function(e){h.columns[u].push({hass:c,cardType:e.domain,stateObj:e})})}}for(var c=this.hass,l=t.groupBy(function(e){return e.domain}),d={},h={ +demo:!1,badges:[],columns:[]},f=[],m=0;e>m;m++)h.columns.push([]),f.push(0);n&&h.columns[o(5)].push({hass:c,cardType:"introduction",showHideInstruction:t.size>0&&!c.demo});var p=this.hass.util.expandGroup;return l.keySeq().sortBy(function(e){return i(e)}).forEach(function(e){if("a"===e)return void(h.demo=!0);var n=i(e);n>=0&&10>n?h.badges.push.apply(h.badges,a(l.get(e)).sortBy(r).toArray()):"group"===e?l.get(e).sortBy(r).forEach(function(e){var n=p(e,t);n.forEach(function(e){d[e.entityId]=!0}),u(e.entityId,n.toArray(),e)}):u(e,a(l.get(e)).sortBy(r).toArray())}),h.columns=h.columns.filter(function(e){return e.length>0}),h}})},function(e,t,n){"use strict";var i=n(0);n(7),n(31),new i.a({is:"logbook-entry",properties:{hass:{type:Object}},entityClicked:function(e){e.preventDefault(),this.hass.moreInfoActions.selectEntity(this.entryObj.entityId)}})},function(e,t,n){"use strict";var i=n(0);n(7),new i.a({is:"services-list",behaviors:[window.hassBehavior],properties:{hass:{type:Object},serviceDomains:{type:Array,bindNuclear:function(e){return e.serviceGetters.entityMap}}},computeDomains:function(e){return e.valueSeq().map(function(e){return e.domain}).sort().toJS()},computeServices:function(e,t){return e.get(t).get("services").keySeq().toArray()},serviceClicked:function(e){e.preventDefault(),this.fire("service-selected",{domain:e.model.domain,service:e.model.service})}})},function(e,t,n){"use strict";function i(e,t){for(var n=[],i=e;t>i;i++)n.push(i);return n}function r(e){var t=parseFloat(e);return!isNaN(t)&&isFinite(t)?t:null}var a=n(0);new a.a({is:"state-history-chart-line",properties:{data:{type:Object,observer:"dataChanged"},unit:{type:String},isSingleDevice:{type:Boolean,value:!1},isAttached:{type:Boolean,value:!1,observer:"dataChanged"},chartEngine:{type:Object}},created:function(){this.style.display="block"},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){if(this.isAttached){this.chartEngine||(this.chartEngine=new window.google.visualization.LineChart(this));var e=this.unit,t=this.data;if(0!==t.length){var n={legend:{position:"top"},interpolateNulls:!0,titlePosition:"none",vAxes:{0:{title:e}},hAxis:{format:"H:mm"},chartArea:{left:"60",width:"95%"},explorer:{actions:["dragToZoom","rightClickToReset","dragToPan"],keepInBounds:!0,axis:"horizontal",maxZoomIn:.1}};this.isSingleDevice&&(n.legend.position="none",n.vAxes[0].title=null,n.chartArea.left=40,n.chartArea.height="80%",n.chartArea.top=5,n.enableInteractivity=!1);var a=new Date(Math.min.apply(null,t.map(function(e){return e[0].lastChangedAsDate}))),s=new Date(a);s.setDate(s.getDate()+1),s>new Date&&(s=new Date);var o=t.map(function(e){function t(e,t){c&&t&&u.push([e[0]].concat(c.slice(1).map(function(e,n){return t[n]?e:null}))),u.push(e),c=e}var n=e[e.length-1],i=n.domain,a=n.entityDisplay,o=new window.google.visualization.DataTable;o.addColumn({type:"datetime",id:"Time"});var u=[],c=void 0;if("thermostat"===i){var l=e.reduce(function(e,t){return e||t.attributes.target_temp_high!==t.attributes.target_temp_low},!1);o.addColumn("number",a+" current temperature");var d=void 0;l?!function(){o.addColumn("number",a+" target temperature high"),o.addColumn("number",a+" target temperature low");var e=[!1,!0,!0];d=function(n){var i=r(n.attributes.current_temperature),a=r(n.attributes.target_temp_high),s=r(n.attributes.target_temp_low);t([n.lastUpdatedAsDate,i,a,s],e)}}():!function(){o.addColumn("number",a+" target temperature");var e=[!1,!0];d=function(n){var i=r(n.attributes.current_temperature),a=r(n.attributes.temperature);t([n.lastUpdatedAsDate,i,a],e)}}(),e.forEach(d)}else!function(){o.addColumn("number",a);var n="sensor"!==i&&[!0];e.forEach(function(e){var i=r(e.state);t([e.lastChangedAsDate,i],n)})}();return t([s].concat(c.slice(1)),!1),o.addRows(u),o}),u=void 0;u=1===o.length?o[0]:o.slice(1).reduce(function(e,t){return window.google.visualization.data.join(e,t,"full",[[0,0]],i(1,e.getNumberOfColumns()),i(1,t.getNumberOfColumns()))},o[0]),this.chartEngine.draw(u,n)}}}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"state-history-chart-timeline",properties:{data:{type:Object,observer:"dataChanged"},isAttached:{type:Boolean,value:!1,observer:"dataChanged"}},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){function e(e,t,n,i){var r=t.replace(/_/g," ");a.addRow([e,r,n,i])}if(this.isAttached){for(var t=i.a.dom(this),n=this.data;t.node.lastChild;)t.node.removeChild(t.node.lastChild);if(n&&0!==n.length){var r=new window.google.visualization.Timeline(this),a=new window.google.visualization.DataTable;a.addColumn({type:"string",id:"Entity"}),a.addColumn({type:"string",id:"State"}),a.addColumn({type:"date",id:"Start"}),a.addColumn({type:"date",id:"End"});var s=new Date(n.reduce(function(e,t){return Math.min(e,t[0].lastChangedAsDate)},new Date)),o=new Date(s);o.setDate(o.getDate()+1),o>new Date&&(o=new Date);var u=0;n.forEach(function(t){if(0!==t.length){var n=t[0].entityDisplay,i=void 0,r=null,a=null;t.forEach(function(t){null!==r&&t.state!==r?(i=t.lastChangedAsDate,e(n,r,a,i),r=t.state,a=i):null===r&&(r=t.state,a=t.lastChangedAsDate)}),e(n,r,a,o),u++}}),r.draw(a,{height:55+42*u,timeline:{showRowLabels:n.length>1},hAxis:{format:"H:mm"}})}}}})},function(e,t,n){"use strict";var i=n(0);n(36),new i.a({is:"state-info",properties:{detailed:{type:Boolean,value:!1},stateObj:{type:Object}}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"ha-voice-command-dialog",behaviors:[window.hassBehavior],properties:{hass:{type:Object},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},finalTranscript:{type:String,bindNuclear:function(e){return e.voiceGetters.finalTranscript}},interimTranscript:{type:String,bindNuclear:function(e){return e.voiceGetters.extraInterimTranscript}},isTransmitting:{type:Boolean,bindNuclear:function(e){return e.voiceGetters.isTransmitting}},isListening:{type:Boolean,bindNuclear:function(e){return e.voiceGetters.isListening}},showListenInterface:{type:Boolean,computed:"computeShowListenInterface(isListening, isTransmitting)",observer:"showListenInterfaceChanged"}},computeShowListenInterface:function(e,t){return e||t},dialogOpenChanged:function(e){!e&&this.isListening&&this.hass.voiceActions.stop()},showListenInterfaceChanged:function(e){!e&&this.dialogOpen?this.dialogOpen=!1:e&&(this.dialogOpen=!0)}})},function(e,t,n){"use strict";var i=n(0),r=(n(4),n(8),n(58),["camera","configurator","scene"]);new i.a({is:"more-info-dialog",behaviors:[window.hassBehavior],properties:{stateObj:{type:Object,bindNuclear:function(e){return e.moreInfoGetters.currentEntity},observer:"stateObjChanged"},stateHistory:{type:Object,bindNuclear:function(e){return[e.moreInfoGetters.currentEntityHistory,function(e){return e?[e]:!1}]}},isLoadingHistoryData:{type:Boolean,computed:"computeIsLoadingHistoryData(delayedDialogOpen, isLoadingEntityHistoryData)"},isLoadingEntityHistoryData:{type:Boolean,bindNuclear:function(e){return e.entityHistoryGetters.isLoadingEntityHistory}},hasHistoryComponent:{type:Boolean,bindNuclear:function(e){return e.configGetters.isComponentLoaded("history")},observer:"fetchHistoryData"},shouldFetchHistory:{type:Boolean,bindNuclear:function(e){return e.moreInfoGetters.isCurrentEntityHistoryStale},observer:"fetchHistoryData"},showHistoryComponent:{type:Boolean,value:!1,computed:"computeShowHistoryComponent(hasHistoryComponent, stateObj)"},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},delayedDialogOpen:{type:Boolean,value:!1}},ready:function(){this.$.scrollable.dialogElement=this.$.dialog},computeIsLoadingHistoryData:function(e,t){return!e||t},computeShowHistoryComponent:function(e,t){return this.hasHistoryComponent&&t&&-1===r.indexOf(t.domain)},fetchHistoryData:function(){this.stateObj&&this.hasHistoryComponent&&this.shouldFetchHistory&&this.hass.entityHistoryActions.fetchRecent(this.stateObj.entityId)},stateObjChanged:function(e){var t=this;return e?void this.async(function(){t.fetchHistoryData(),t.dialogOpen=!0},10):void(this.dialogOpen=!1)},dialogOpenChanged:function(e){var t=this;e?this.async(function(){t.delayedDialogOpen=!0},10):!e&&this.stateObj&&(this.async(function(){return t.hass.moreInfoActions.deselectEntity()},10),this.delayedDialogOpen=!1)}})},function(e,t,n){"use strict";var i=n(19),r=i&&i.__esModule?function(){return i["default"]}:function(){return i};n.d(r,"a",r),n(18),window.moment=r.a},function(e,t,n){"use strict";var i=n(0);n(1),n(37),new i.a({is:"partial-cards",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},isFetching:{type:Boolean,bindNuclear:function(e){return e.syncGetters.isFetching}},isStreaming:{type:Boolean,bindNuclear:function(e){return e.streamGetters.isStreamingEvents}},canListen:{type:Boolean,bindNuclear:function(e){return[e.voiceGetters.isVoiceSupported,e.configGetters.isComponentLoaded("conversation"),function(e,t){return e&&t}]}},introductionLoaded:{type:Boolean,bindNuclear:function(e){return e.configGetters.isComponentLoaded("introduction")}},locationName:{type:String,bindNuclear:function(e){return e.configGetters.locationName}},showMenu:{type:Boolean,value:!1,observer:"windowChange"},currentView:{type:String,bindNuclear:function(e){return[e.viewGetters.currentView,function(e){return e||""}]}},hasViews:{type:Boolean,bindNuclear:function(e){return[e.viewGetters.views,function(e){return e.size>0}]}},states:{type:Object,bindNuclear:function(e){return e.viewGetters.currentViewEntities}},columns:{type:Number,value:1}},created:function(){var e=this;this.windowChange=this.windowChange.bind(this);for(var t=[],n=0;5>n;n++)t.push(300+300*n);this.mqls=t.map(function(t){var n=window.matchMedia("(min-width: "+t+"px)");return n.addListener(e.windowChange),n})},detached:function(){var e=this;this.mqls.forEach(function(t){return t.removeListener(e.windowChange)})},windowChange:function(){var e=this.mqls.reduce(function(e,t){return e+t.matches},0);this.columns=Math.max(1,e-(!this.narrow&&this.showMenu))},scrollToTop:function(){this.$.panel.scrollToTop(!0)},handleRefresh:function(){this.hass.syncActions.fetchAll()},handleListenClick:function(){this.hass.voiceActions.listen()},contentScroll:function(){var e=this;this.debouncedContentScroll||(this.debouncedContentScroll=this.async(function(){e.checkRaised(),e.debouncedContentScroll=!1},100))},checkRaised:function(){this.toggleClass("raised",this.$.panel.scroller.scrollTop>(this.hasViews?56:0),this.$.panel)},headerScrollAdjust:function(e){this.hasViews&&this.translate3d("0","-"+e.detail.y+"px","0",this.$.menu)},computeHeaderHeight:function(e,t){return e?104:t?56:64},computeCondensedHeaderHeight:function(e,t){return e?48:t?56:64},computeMenuButtonClass:function(e,t){return!e&&t?"menu-icon invisible":"menu-icon"},computeRefreshButtonClass:function(e){return e?"ha-spin":""},computeTitle:function(e,t){return e?"Home Assistant":t},computeShowIntroduction:function(e,t,n){return""===e&&(t||0===n.size)},computeHasViews:function(e){return e.length>0},toggleMenu:function(){this.fire("open-menu")}})},function(e,t,n){"use strict";var i=n(0);n(1),n(39),new i.a({is:"partial-dev-call-service",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},domain:{type:String,value:""},service:{type:String,value:""},serviceData:{type:String,value:""},description:{type:String,computed:"computeDescription(hass, domain, service)"}},computeDescription:function(e,t,n){return e.reactor.evaluate([e.serviceGetters.entityMap,function(e){return e.has(t)&&e.get(t).get("services").has(n)?JSON.stringify(e.get(t).get("services").get(n).toJS(),null,2):"No description available"}])},serviceSelected:function(e){this.domain=e.detail.domain,this.service=e.detail.service},callService:function(){var e=void 0;try{e=this.serviceData?JSON.parse(this.serviceData):{}}catch(t){return void alert("Error parsing JSON: "+t)}this.hass.serviceActions.callService(this.domain,this.service,e)},computeFormClasses:function(e){return"content fit "+(e?"":"layout horizontal")}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-fire-event",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(e){this.eventType=e.detail.eventType},fireEvent:function(){var e=void 0;try{e=this.eventData?JSON.parse(this.eventData):{}}catch(t){return void alert("Error parsing JSON: "+t)}this.hass.eventActions.fireEvent(this.eventType,e)},computeFormClasses:function(e){return"content fit "+(e?"":"layout horizontal")}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-info",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:function(e){return e.configGetters.serverVersion}},polymerVersion:{type:String,value:i.a.version},nuclearVersion:{type:String,value:"1.3.0"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(e){var t=this;e&&e.preventDefault(),this.errorLog="Loading error log…",this.hass.errorLogActions.fetchErrorLog().then(function(e){t.errorLog=e||"No errors have been reported."})}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-set-state",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(e){var t=e?JSON.stringify(e,null," "):"";this.$.inputData.value=t,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(e){var t=this.hass.reactor.evaluate(this.hass.entityGetters.byId(e.detail.entityId));this.entityId=t.entityId,this.state=t.state,this.stateAttributes=JSON.stringify(t.attributes,null," ")},handleSetState:function(){var e=void 0;try{e=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(t){return void alert("Error parsing JSON: "+t)}this.hass.entityActions.save({entityId:this.entityId,state:this.state,attributes:e})},computeFormClasses:function(e){return"content fit "+(e?"":"layout horizontal")}})},function(e,t,n){"use strict";var i=n(0);n(1),new i.a({is:"partial-dev-template",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'{%- if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(e){return"content fit "+(e?"":"layout horizontal")},computeRenderedClasses:function(e){return e?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate,500)},renderTemplate:function(){var e=this;this.rendering=!0,this.hass.templateActions.render(this.template).then(function(t){e.processed=t,e.rendering=!1},function(t){e.processed=t.message,e.error=!0,e.rendering=!1})}})},function(e,t,n){"use strict";var i=n(0);n(1),n(8),new i.a({is:"partial-history",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},isDataLoaded:{type:Boolean,bindNuclear:function(e){return e.entityHistoryGetters.hasDataForCurrentDate},observer:"isDataLoadedChanged"},stateHistory:{type:Object,bindNuclear:function(e){return e.entityHistoryGetters.entityHistoryForCurrentDate}},isLoadingData:{type:Boolean,bindNuclear:function(e){return e.entityHistoryGetters.isLoadingEntityHistory}},selectedDate:{type:String,value:null,bindNuclear:function(e){return e.entityHistoryGetters.currentDate}}},isDataLoadedChanged:function(e){var t=this;e||this.async(function(){return t.hass.entityHistoryActions.fetchSelectedDate()},1)},handleRefreshClick:function(){this.hass.entityHistoryActions.fetchSelectedDate()},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:this.hass.entityHistoryActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()},computeContentClasses:function(e){return"flex content "+(e?"narrow":"wide")}})},function(e,t,n){"use strict";var i=n(0);n(1),n(38),new i.a({is:"partial-logbook",behaviors:[window.hassBehavior],properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},selectedDate:{type:String,bindNuclear:function(e){return e.logbookGetters.currentDate}},isLoading:{type:Boolean,bindNuclear:function(e){return e.logbookGetters.isLoadingEntries}},isStale:{type:Boolean,bindNuclear:function(e){return e.logbookGetters.isCurrentStale},observer:"isStaleChanged"},entries:{type:Array,bindNuclear:function(e){return[e.logbookGetters.currentEntries,function(e){return e.reverse().toArray()}]}},datePicker:{type:Object}},isStaleChanged:function(e){var t=this;e&&this.async(function(){return t.hass.logbookActions.fetchDate(t.selectedDate)},1)},handleRefresh:function(){this.hass.logbookActions.fetchDate(this.selectedDate)},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:this.hass.logbookActions.changeCurrentDate})},detached:function(){this.datePicker.destroy()}})},function(e,t,n){"use strict";var i=n(0);n(32),window.L.Icon.Default.imagePath="/static/images/leaflet",new i.a({is:"partial-map",behaviors:[window.hassBehavior],properties:{hass:{type:Object},locationGPS:{type:Number,bindNuclear:function(e){return e.configGetters.locationGPS}},locationName:{type:String,bindNuclear:function(e){return e.configGetters.locationName}},locationEntities:{type:Array,bindNuclear:function(e){return[e.entityGetters.visibleEntityMap,function(e){return e.valueSeq().filter(function(e){return e.attributes.latitude&&"home"!==e.state}).toArray()}]}},zoneEntities:{type:Array,bindNuclear:function(e){return[e.entityGetters.entityMap,function(e){return e.valueSeq().filter(function(e){return"zone"===e.domain&&!e.attributes.passive}).toArray()}]}},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1}},attached:function(){var e=this;(window.L.Browser.mobileWebkit||window.L.Browser.webkit)&&this.async(function(){var t=e.$.map,n=t.style.display;t.style.display="none",e.async(function(){t.style.display=n},1)},1)},computeMenuButtonClass:function(e,t){return!e&&t?"menu-icon invisible":"menu-icon"},toggleMenu:function(){this.fire("open-menu")}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"notification-manager",behaviors:[window.hassBehavior],properties:{hass:{type:Object},neg:{type:Boolean,value:!1},text:{type:String,bindNuclear:function(e){return e.notificationGetters.lastNotificationMessage},observer:"showNotification"}},showNotification:function(e){e&&this.$.toast.show()}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-alarm_control_panel",properties:{hass:{type:Object},stateObj:{type:Object,observer:"stateObjChanged"},enteredCode:{type:String,value:""},disarmButtonVisible:{type:Boolean,value:!1},armHomeButtonVisible:{type:Boolean,value:!1},armAwayButtonVisible:{type:Boolean,value:!1},codeInputVisible:{type:Boolean,value:!1},codeInputEnabled:{type:Boolean,value:!1},codeFormat:{type:String,value:""},codeValid:{type:Boolean,computed:"validateCode(enteredCode, codeFormat)"}},validateCode:function(e,t){var n=new RegExp(t);return null===t?!0:n.test(e)},stateObjChanged:function(e){var t=this;e&&(this.codeFormat=e.attributes.code_format,this.codeInputVisible=null!==this.codeFormat,this.codeInputEnabled="armed_home"===e.state||"armed_away"===e.state||"disarmed"===e.state||"pending"===e.state||"triggered"===e.state,this.disarmButtonVisible="armed_home"===e.state||"armed_away"===e.state||"pending"===e.state||"triggered"===e.state,this.armHomeButtonVisible="disarmed"===e.state,this.armAwayButtonVisible="disarmed"===e.state),this.async(function(){return t.fire("iron-resize")},500)},handleDisarmTap:function(){this.callService("alarm_disarm",{code:this.enteredCode})},handleHomeTap:function(){this.callService("alarm_arm_home",{code:this.enteredCode})},handleAwayTap:function(){this.callService("alarm_arm_away",{code:this.enteredCode})},callService:function(e,t){var n=t||{};n.entity_id=this.stateObj.entityId,this.hass.serviceActions.callService("alarm_control_panel",e,n)}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-configurator",behaviors:[window.hassBehavior],properties:{stateObj:{type:Object},action:{type:String,value:"display"},isStreaming:{type:Boolean,bindNuclear:function(e){return e.streamGetters.isStreamingEvents}},isConfigurable:{type:Boolean,computed:"computeIsConfigurable(stateObj)"},isConfiguring:{type:Boolean,value:!1},submitCaption:{type:String,computed:"computeSubmitCaption(stateObj)"},fieldInput:{type:Object,value:{}}},computeIsConfigurable:function(e){return"configure"===e.state},computeSubmitCaption:function(e){return e.attributes.submit_caption||"Set configuration"},fieldChanged:function(e){var t=e.target;this.fieldInput[t.id]=t.value},submitClicked:function(){var e=this;this.isConfiguring=!0;var t={configure_id:this.stateObj.attributes.configure_id,fields:this.fieldInput};this.hass.serviceActions.callService("configurator","configure",t).then(function(){e.isConfiguring=!1,e.isStreaming||e.hass.syncActions.fetchAll()},function(){e.isConfiguring=!1})}})},function(e,t,n){"use strict";var i=n(0),r=n(3),a=n(13);n(59),n(65),n(57),n(66),n(64),n(61),n(63),n(67),n(56),n(62),n(60),new i.a({is:"more-info-content",properties:{hass:{type:Object},stateObj:{type:Object,observer:"stateObjChanged"}},stateObjChanged:function(e){e&&n.i(r.a)(this,"MORE-INFO-"+n.i(a.a)(e).toUpperCase(),{hass:this.hass,stateObj:e})}})},function(e,t,n){"use strict";var i=n(0),r=n(3),a=n(13);n(4),new i.a({is:"more-info-group",behaviors:[window.hassBehavior],properties:{hass:{type:Object},stateObj:{type:Object},states:{type:Array,bindNuclear:function(e){return[e.moreInfoGetters.currentEntity,e.entityGetters.entityMap,function(e,t){return e?e.attributes.entity_id.map(t.get.bind(t)):[]}]}}},observers:["statesChanged(stateObj, states)"],statesChanged:function(e,t){var s=!1;if(t&&t.length>0){var o=t[0];s=o.set("entityId",e.entityId).set("attributes",Object.assign({},o.attributes));for(var u=0;ue||e>=this.stateObj.attributes.source_list.length)){var t=this.stateObj.attributes.source_list[e];t!==this.stateObj.attributes.source&&this.callService("select_source",{source:t})}},handleVolumeTap:function(){this.supportsVolumeMute&&this.callService("volume_mute",{is_volume_muted:!this.isMuted})},handleVolumeUp:function(){var e=this.$.volumeUp;this.handleVolumeWorker("volume_up",e,!0)},handleVolumeDown:function(){var e=this.$.volumeDown;this.handleVolumeWorker("volume_down",e,!0)},handleVolumeWorker:function(e,t,n){var i=this;(n||void 0!==t&&t.pointerDown)&&(this.callService(e),this.async(function(){return i.handleVolumeWorker(e,t,!1)},500))},volumeSliderChanged:function(e){var t=parseFloat(e.target.value),n=t>0?t/100:0;this.callService("volume_set",{volume_level:n})},callService:function(e,t){var n=t||{};n.entity_id=this.stateObj.entityId,this.hass.serviceActions.callService("media_player",e,n)}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-script",properties:{stateObj:{type:Object}}})},function(e,t,n){"use strict";var i=n(0),r=n(11);new i.a({is:"more-info-sun",properties:{stateObj:{type:Object},risingDate:{type:Object,computed:"computeRising(stateObj)"},settingDate:{type:Object, +computed:"computeSetting(stateObj)"}},computeRising:function(e){return new Date(e.attributes.next_rising)},computeSetting:function(e){return new Date(e.attributes.next_setting)},computeOrder:function(e,t){return e>t?["set","ris"]:["ris","set"]},itemCaption:function(e){return"ris"===e?"Rising ":"Setting "},itemDate:function(e){return"ris"===e?this.risingDate:this.settingDate},itemValue:function(e){return n.i(r.a)(this.itemDate(e))}})},function(e,t,n){"use strict";var i=n(0),r=n(2),a=["away_mode"];new i.a({is:"more-info-thermostat",properties:{hass:{type:Object},stateObj:{type:Object,observer:"stateObjChanged"},tempMin:{type:Number},tempMax:{type:Number},targetTemperatureSliderValue:{type:Number},awayToggleChecked:{type:Boolean}},stateObjChanged:function(e){this.targetTemperatureSliderValue=e.attributes.temperature,this.awayToggleChecked="on"===e.attributes.away_mode,this.tempMin=e.attributes.min_temp,this.tempMax=e.attributes.max_temp},computeClassNames:function(e){return n.i(r.a)(e,a)},targetTemperatureSliderChanged:function(e){this.hass.serviceActions.callService("thermostat","set_temperature",{entity_id:this.stateObj.entityId,temperature:e.target.value})},toggleChanged:function(e){var t=e.target.checked;t&&"off"===this.stateObj.attributes.away_mode?this.service_set_away(!0):t||"on"!==this.stateObj.attributes.away_mode||this.service_set_away(!1)},service_set_away:function(e){var t=this;this.hass.serviceActions.callService("thermostat","set_away_mode",{away_mode:e,entity_id:this.stateObj.entityId}).then(function(){return t.stateObjChanged(t.stateObj)})}})},function(e,t,n){"use strict";var i=n(0);new i.a({is:"more-info-updater",properties:{}})},function(e,t,n){"use strict";t.a=["off","closed","unlocked"]},function(e,t,n){"use strict";function i(e,t){return"unavailable"===t.state?"display":-1!==a.indexOf(t.domain)?t.domain:n.i(r.a)(e,t.entityId)?"toggle":"display"}var r=n(9);t.a=i;var a=["configurator","hvac","input_select","input_slider","media_player","rollershutter","scene","script","thermostat","weblink"]},function(e,t,n){var i,r;!function(){"use strict";function n(){for(var e=[],t=0;t \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/frontend.html.gz b/homeassistant/components/frontend/www_static/frontend.html.gz index 58501c46c30810078f39b22ffce530797bc0ec75..fa2fdc437c5eb49a893aa8891b4565f53259aa1c 100644 GIT binary patch delta 82789 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 Dl}}SS delta 82688 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Ν z@21vIKdK@EO}@D)KDy^n#thk(2C{X|Fuj)5)<^jq21#jDZKyF$Y>mJ&unp<~ zDyUD%+7!v$npk1j>bMci4|KL|e|?B|B=LpqAj(N2CbAi@JA9@TM4HeSb6b{!0Iar+ z3!eO%IY_ilKH@#l1`22dl}8&yqw=7CMSu6Xzc=m+mU5@jD1?cI&d7j|e1Zc0vT3(G z7CAJ^PDb67y8Ecz%Sl*V{HBn0)dq9pfkI57llw#uzJ&cmoD@-L#5j$te?zN{Ay)f6 zWN9F3l~OQCr%@ctULiH`Jd7h4e!@<#!;?p!&+KZZZU#r+1Qy29YHxj*UiRJX)s~3V z8v4*AXcPN#12@r~(=h#kVNg%x zX$lysHwX#>o-?FA)G`Xjjx-+!NN(<@62Zls-tLc)VvC2dmXG>9$RG3uMD0cXXFKIR zXv66sHy&hkl!O~;ff!Zq#DRu*_|Q(Fg|H7G)ag4%X&8JQPu(2jh$XNhZ+A(~Ha4KaRSG!O>CbZd5L16@sw;<6#iTN#4SX?Upc8 zvASD)GF$(F>g&MRl5Ym|`nC6R_s#Q1hc8|p96o;e=EbfB+ygMl?_j&HCTQx7`rNWl z)Z(I3@3ciaDDFq$e_%SsjkJR&-7CuD!U)>Oksl-dAbL^2d1dN~&>E1~H1)I!Nllf`V>O5)i?>nLm?-HTQ-EmD?6fI$mqe?|;GF4KoLbweT&=(pO8 zLNS)B!(o)YHdDq!0i_hJf%IUE^t3l>g}D~Jq>T4!Ej>po%38Q;M84KVn4?n$YAY?S zJ*2kZAs0k(j%N#OK|z`!4G%n1`WvEea+PQ^2|cVG4&1b|!r15{my?a6*6>}G2~`Uj z#fMz5n&27bfAxMTG)SSMe>>_N8L(=JqJp(gUgpR``K36=EiMj^3Bzol?1cs_2b>ft z^pg)lA8QuD0|VQ>CPSehCi`E(qo0Z`;Z&`pU;TKlD^Pa5EN4_zE=* zFPJ32KPKR@$IMj$=(%>@!ZCKa(huE?C=6N#Pm#Jne{PdoLT6OX3G{?1898zi2}UK{ z5e@CPf@g4n>#Cvpk+LD`-+LsP3r&FVsH9;bxKrC9CSzm)mjN`H~N3iDXzSGub{&gY8;@jG2P+6HsbEkflh^PKVFf}X3MZ9`MXdvYU z2Z4Ol8;@(sJw}zqI`SHbs6J9s+hYV7Gn9DTKu~p;cRp(WK<@-C*a$jA!!OnCH^v16 zxn8liK8!{mvJd;?EnJV1rrP??52NLW@ef-ku7#9Uk?`RI#}!b&n3tWK0#qM(k7bR~ zcicm2F8wZ-U+{>bZ^XEsnO6$YNY0=Q-g}o2oB|meC$_J7hS~`*6cKC}V47BUDBwYqVO~tcuos@dPy-Gb`x?>Yne4Pc{%)>td4T{Nw$dH)m` zDzkcuYH9-~eqBR?uq2Li12+@Y815pSCPNxfua|@cbgoxoebj)aNW?aKbqN`^x|D)~ zy#DrsXwYs)PT()>(RO&-jmAzx85GJ=iZ&2-Ks&gme*^U`D#2(juEhI8Yj1CljZomM zn9KW?+7`NSUa~&eFeMruxRP?2QiE!Wd~Jm{uj+=)`-=W+`!B05KGG#Duck+zgGsKm z7{j7Imx~W-*Whybz1F2uEKC$JBMX%3i<7)5Hmoh=9h#P%c0gJGDXj9vDol-I$m-Iu z)V6*=f9*PJ-F9e($-84cVCc(oSs68wquW3q)itthEgS1`>py2UQ!f15$fL>M4#3&Bo5`tzEJ>GbxMq-f8Vi8=@4}B@a$fM0qMu^W%D|I&!WMz z=p;68@5AbK^3aAcQ5Rx0tnEgx#M=t+`YnH`U2!lp)m*jKY#p1-^p@1)kfEK4`a}{e z!|<~O6!58rRiJ;>!*`#^OUhq8d@0)T))fMd`fGq{*Z==o_Zk{e*QquASqDYCG{{V(>uT{%B2vH3LP4gktczqN@e+CL~ZEkk90gdAYnbJKWVO%_V8;yVI0ds{P z5BmM#+sKPYe?nv*BjMu#V2yb6C>pzz#~J>KIUdVm@bO^#&T_eZ2P&W-0LDA)c=Q^N zJN|rAe11U~j}gXe2?IVJ-2Hj@8YADx$TuAM1|z@7Nced0^UpsIzeHZA?^1sbe`SYu zgNv8P??O~dv>!zefMd)b{28&(5&o8Pqee{zb3 zhS^3RDD70Vr(A!qrv4Sl$fe#zaAiyHjM+XXPGv*x6avq>QkiePw^pQzOzzYqMQ7PS%1R4Iv(4BWgF~D9* zP~NO!pa+Sj22(73zam7a*HOKq{#*zgj3(sR1U(YZ1x>L=Ki>~AFuZ;5o_G7+T}^jJ z&lCQLs0*qMe}q=R!~v#(f4Ckhj%oKBM;ak@Xb@7mzKO|+`S;s-Cx#Q(zTd{taeL^f zMG8?aY^ReuKi(H(&`H}-M_Xt6p)Lm_yDGvY04wf9P!7-u)raZs#P7H7-?H(Jx}CHa zN}K3`KN$kc5dyl*d>EU+X-^z)@_>ekQreg@=}-hLOo+j-<6dI#f4sn*O1$c28nyni z%5zd{6mg04ZRTs*Ped~dP|))LTDo=LX)6I||3QXqxr6`>j{L0sF$5L}fjM6G;A5$R zp$%C&((V%@u96`;bnG1Px)-_Bzf|kf$I|Sn*#wCA4I@Qtvbgi05$i4 z;jftoS89rU%v;!Xs*l0z@$NWazaKdSisX5NQRUarhbL>jEhR3BvF3 zp!s&OQcgne0!B>7yN;`r8S^i5zMK@;bT5k0E>&qOYf(-o6uPEG<=hgkd;5-lqxyF} z0pui=KWSMv07N3t0DIrR_1GBn|Tj9wQS1k}gJ5Q1A?z?v!+SItG zDc4)KkNcFqAK|y%e~>Ph>4W>Ycow)!7`qks^N0t!)8w2awdL3gS^ddLwR$`{&Fasw zN~L#)hz$O?c8}X|PiuqB6mq7p9k@_FL;@H77+F&we}CNmgJ`*o5ckDY@34lDh3o?E zVpvPVVg@_$_YsIZ`&GC6g!*D^9*8S8lBT;AN*^0YKB0>Dv;CKcKi%u~jWf#llaf2?U@FyXK^a?CTM);Iyjz|Kh1Ar4PZhapdYs|{bGZc2e7%zD*DHaaqV`+l z1Np+4fB&%q7tS^;;1F>PBb-5P;`+JgCd!030vwQOUN@-eniT{yurPKBmM3j8y>L5H zO0|==Uj&Y#Q80$nYN!uI-_xKA9eM%}lMdJ@aPj5_?<$psK9a?aWd@XDsOkHQ&cscw z>`o$B8lkO^RA`To1MTl${Qhe~6N;1Ys_zbtke&^8-wi-vuK392RQk zD%RDqIxZbeF0s3s)}48F4-BJ_7>`_m`6L`{`_K%MP)=oxvR8XZVDJ}et-$D`@B#V2 zHVg_xESLT54Km3`MhPW=o<@qktD-_YKm|rWma9WFl{r7%oC7g@4G)3(?vA=)Pk+&1 zfAL+rkF4XI^+*eh&&L8bUVay`C|-p9jF3j@7?~j8YNxDw9<<3Cz+71_#P)l)*dU{7 z1maRchR$DnKyN$?gvJ7gE=&X1xjFO#?Wa&wuFd4DPPJ6u|D+7b7x z#OR#auo!oUelGNC^rJHG2GgnCcde;KBP~yGxplNPvlHr$PXH~FDCbxs1ceEP`dTh` zXv-+Ko3I*{E7P+Q^s__z_lR)kLe?VFar5HJbvKRx*UqJRs_(#|wMeZ}?8#SH5aILAK zZejCWztmxCE0U!tW3k9aoh+8p(U-fCmk=XTT*Al`a{!SCJmW8UVe6SXIqDE;T6Y^5 zfeH^Xt~xB27Ai{`^YtWDYSp80t%Zi$Q{|v9R+Rl3LQ_J^(4cs9b1*x{f8;Afhw@h2 z`p?-2k+Ixz*+b=&F8V5hQW!+akuV*T-V9y(URZ&#m=mhGwwh4h=`x zYFmY@gcW@{Xpcpv266kb{a^oOhgs zt2d!$@#Gda0P-dhkpvpl$Hc|>_60Q$WoW~&z@8rNa7V+You~+)?h$c~WPV2hmDUa_ z7nOt%08B>CtTk%Sp$`-Ke*(#GPt$+>rGJms2h2%`Xo{+#Vcpzh{QHIFg7S$&X=o`eh#22) z^aM>7i92$n4I^yUH?ZaYRo4Y&ixrDV$x?l?+R|!YEQ1ma0(+ zHS~+9T~u0>fu*_=$+lIh{j1VNO>q{e$RTe_Z6Lj#TkgFBa{)*X1>%mU--Y~M?vz&M z0YwO0BlHY^QFnmVOfEs_R&7635?g~=S8 zFZ2B!DW42<0-VgJkP&j&reS*j_$(H}InSkv&#?WW1p z@sTk{adVlbBGI*q2qyp81iHS~h6LAU@Cw-bLVZ|WRa_jJ5N+Y>lihn>FHho)U#z`~z; zpX>w$!w%3YrxbY1fjI?Ub0DL@80aa~n;l>o4tbqvf0I=RK$&pe?ohotdIjuOtG ziy0i3KJx@sH*knyWHUcCh8N=YFp713k;(y9=PmLJE=?|~AcwAXDDC3#oUHP&P*BsQ z%?*nt!H_?mC74hsy1DRg*HRp}&E7VEx9fSae{)M?sWSuKrQUj&a&EcTTNSjDpj&gP z9Mw@8zevQ|xxAs0*Gh;qB+kOaTtaZIJqs`MiPk4!N~fdJVsn+Y^G$X(xH_F^Y9UztCrX4V;D_n@ZQ7ne|j>w z3y{3N%uJze3<*h`<=vMdE?B@sjv9vXrJbW8`VfE7(HaJf6I-}C&ID0osG8y@K5l6< zXN+hyHncWQbK9`LI1|s?7s_46s9g}h#vD~WdqWS%=s4i1O@`W*wvH?h)n4@o5)=|3 zwIUv9LN!nO{>7lw&&n)O2B=)f|Xj*m%MU28O>@@S%rLLa)T zM-rQ>jinvyLHEk_@Cyo~<6rzM=Abn2CnoL}-P#Z4%uI^iRt&g)lkn-9e`VFo=!FmS z8jGF=S%OXIOL7v%P^Ui1%Yf$5xv4VSgL(%~NeLYZ3hCsT@VN{K*`Kgp$^0X6fB@Dt z)3Po9wpYNxtIjRFtfI?rEYgVprCItk2V4CR%CLM;MnSXD;qM}q5x4J!cRfPM+=if& zI$J;8yMwVPebl4x_3r!xfAM%24m0n>&K!2rWC{e1${d1UPhPd{8d40=zezBmFocdk z%PB;9c_%;;yp*%kFY@9|G30W#py{Y)RL$M4%^kz9<8~k!NeoR>?7Yl9Nnm^?*j5-@ zg=_rjblTe6+iUIaw%)-%t*1}D`P|F0*67LJ!T9}qUB!1lH$|Bee=vFWQ+@tL4jdl! z{Pm0fGpfzLWOY2nl6ZS(I_dRe^W>`S6ZYDftI_>6OSEs0=wjeD>cVjLm+GSbTJR&=u2pC>K2MwGD#qAPxyl%9zi84Qi)3z zTm-(vEp8Je`j!&Sb5#cYI5A;3jE4>(>|MyRY7ja%x_(O$Z4 z&|Twbh07zMfZ*AymD#CO$dHG{4Y*NU%HcI3;S2MW7|17qq!O$;LS1FW7SdwlG+Ipb z$)hb|)#w{)qQBPGMXJLn(GxWac|cJyR_m$SX`y?ee^~I4o!7S)s+A5rsSqxNYJp=u zP3jPui(A*%czT66(>&@-Z(h%}vePnG_C$BXjQTdhGEBc3*0P9HWjdl={T8_{>D32$ zyW;ZMC4$E;<0?-_TrSIRHNmXQ7xyBH1KM!>nagX^o;yM%E54K*yFG&deys1vA9B-L zSYioDfA~v`A%ez%Q0Q4Ur(NimsaI7jwy~^CLr1I_s$GX_qyN#gel#I8Eb7l7sJAw6h)d0rb@ZaBKH-oXw-Px!BemYdV83YW#e8JV8I~bgHNflRa zfF)GvE2LZ`XfCoB#We-8_-lNF7{jf32QlJH?iSwGr@<*&NkLBx_sbqa`eTP$A2|&r zm6H#x$5%QEsWz$o5@B8Gv&gyv$dwQQMhITiSNeO~x0qkjU6j6Tt0%@;U60@d;! zb`~&Sb9*4MwxedDYB#S1RJ#t3zsi4);2rQOCj6O@b*1jg=YH9fk35NUJgqAS0Dn2& zkqQ>(xxq<6iN@wwVRMMOApC_62$b#OOr_3L>P%TFVoNjS&?iw}6ZJJS-2KQNCG68D zAqPJb4ooO8vnQVFvh-^py7kk0jjZe3`%JFctaUJwt6`WcwE`hq z>tyImlR;?OT6XR2KMUhK^=WjM;(v%6WX%o4wa$`@vg2hTI{vZ0jV76z`anZQbXcS* z3dD_uBaIh}(s0z)!Meu%4BCV0Rw9RSX`d^jRzhpq#kht5tuv%?@O>G<+!l{)phf0R zZtDV;^SI=FB1ET7!1$icEzf$jXL;});P=j+rCltGmgT6%>VLA)y2rM9U4MK{N58Ty zk4VYCop!(4l=ET2Zn5)l+Ika5pTaZ?W-U}jGXd=CXTZ~tT4daLY85;U>%GgcHS&8} zc-lpz$%2VZD^;{cq4GZc5>VJsa(k$hqv@#r9hZcBho(DddMKeG1!nQh*_raBKPX%r zZ=j*F-2l&z3kS#O({Ty^?SJkrk^VM(Cu>5RX&S#eX|;{ z;e+1LI$i)^TzO|fmRnjyG0>!NW_sF~|Cm7)`US~OrS&^xRJA}fM}L-2g`0hJNkCo* zv}-!G4$-A$MV3m^|IiJUL1#$Upq9MpmSntYg}8QR-H3?X{Ha&Hl+V=^IXfy6=|yBK zwTnHib5(OwwA!ijgbgDNhqf|n_Sq25qn=<|h;}nKC^^u_H5{e|_u=&h>dBE<8;jhv z<92}Fkyuq!z{rSTVt?X;h>C2G5mAxRa6qZ7R7zg!Y1fD*`WEx2G=sD|!*vvSA_m z#B@7cx^Sj+Gp4wm7KvKCIDKUy3dkF75lf%yB3x!wb<^!NM}N|1`6BCaW#gy0t$BuO zl}uUMesn7>sc;M17Li0|F3QM7=4bW)u+AX%wYkKgMM^ZNn_s_a&eqZMeTnOyFqE$r zdisNg+9ju2BZW&jzq$%gfm%&G8YOEpjdsbE)i^s`giX2e%5@A{ePzC;Y7%VnYC-t*n9${IzfZt z#h60p6wex|M5-Pz%C(tnM@+gaGDoc4_c1B57J!Da7sD6~ghG`8Lt`O+i%lGWdSREE z)|70*)_tg%^-BkZ?vzZGtXSH=-zg3QEUJOCvupswP-G9CBZfwN#PLyWp7@5of$sNevo5Ui4 z9bvDnjJy{YVY2Vdits6hlOl|=Sj25IyocdQ5x&6i%uj!bgD&8-5mex&V`w5Fx)H#K zK!B-p3eGJz;AF~uipd!!pHlGDax+d&Fg(TN1e2!}1Zq6tx5s`PNN4!EgAcRC#^dw)yKre_96g0Y7o6hZM&B!* zi5^m%XVV|3#dvizh}Ni6pwU$%%+^DV+~%hV@zuO6e@ryK-4>65p@V`3rQj?w4>Ugt z>Zlsr{;)VyA`>)$mRlNP0s4>V^e7IP=zm8a&XtgUV?L1JcnI)#U_uoJHSq&jJSi?1 zSR|o4g~Q?07h7;&D!9qMo(}EagVUka5gz7WS#3KTW^L;oN7%_QX~U#*U@=^@tz8kA zESIz8ayqmQWW;dNww{W>Y&a{*O^4IAwNC-hdez&tNGO>^?jjm2BERz3Z88ssb$^kb z&Zw$KK*htZLSP#!|1b|8Jm}FaV=VX=bbU;ZqM|-?aV1JmOe4P~PAc`(??!(`!#at` zoD<~&uhImV?>q`gj!I8LyHk3wZvAV;kge|RT()P%I!>d(X(XSh{TNw{+4i z1-Hc`c{v$Av6pnsVGY4Qj@)_Vo_}Fe-3$6XiTo}5!)WxM_>vMntYm-q;0*22)6?;M zZfBV@T<#^yy}cp+yWCBdySwx=#D7t8Fr31lWVk#ZCCl^iaQPPgzJ^g_roPU_4KK}<(}kzKcLV%K(CXr-ALe z2>2Bxx3}D9MG}N|AoPBTVSj|_4XLdsk^91kw}&1lIYUhRRRdvv=!zyln)?(}dYzxg zw?_XFbiRC8^m;qJ4*a?M_%Zz6?_pr~J_a8DOo7L{4>7R&m;#RQhWCx z>(&@*$!-nJ=yBWO*NP+eISu&}`h5gD(R~~FefQ7E-*z8I{%!Ylbd~A?F{K( zd-S3Gp@N-YAaA|L^es1E(5T0 zmXV3NQ_27*-jmMgf9?3~+s-4EMGkx^hnc?RZuC_ZyG8|gZfUg}RqMsxz1!QAv;oB-sFBFZMoA`MQ$X$xr0);TaC z`1E)h&H?>Qk!W0;O_Di2K0XirP5`Hdd4868TR>psr;DR56x!kj z$v;t+Ey8T8e}AWUpI$YeCn>twJ5IWmaeA5FEYUwwTXQ*Xwd6xr;s|_at#QQ178H`m zMFZ6bXqIKQL(gi%FM;UMdZZJ)@m`hz;p+`r5iE2E3yGU%Edf&QHl#q(J>Wm})L+G2 zwToOk9-NTp8K1kDy*P!r_~!^{P!)!VcM(vcMMM#Ue+Q+Akx65qu`ofvDnBcVzPh)` zdoWW%!(h@Z5w|d-1}+R z2h!BrMw2&YcT@j~ZAnWLa_ccDe2VMOEOY)OEjAODWRGIC!hl0V#g>-&*NgdY1ckchvSL*LXgEVi47`3Tw!r& za)XPoXi;&9iY|$WFtkUq@3OO^4_va2{me&E@%F>Equ%Nkz_>T#ns>P4QM%^n1bXKW zq<5@8605L(il?!X0Z8Lvf{f>U(*I%AveUP3+00Q3$mb}^I6r4m#(5&ik;I%7b?wu&p_^Oxbbaft0@Bd7PqA$U>>3h3cPw%}5A#m5;NmSkPBLUUjOlAfeLYn; z8~;$6!x=V`u&rrL>f%MGCGbfQLZG?qd-FErB-FuB|A0HP^5JJwZhN zSr3c%lf;3A7X;9{Dwv#JRQg0V6^I&tT!>6xjMBFq@;`Yz%zpd98_ zfwbeWstl{b5#g$ok4dOXe7mSrgB4_|sw^|A$}mp`RlN~a?NL=i0YX)^-+)K{>%Z)> z&nxt{)ayBl*+ThnJ+swRSTDOzM@`QyxzSX~`$7O&<>dOkTUF>fWGs8PsqEE-q4d3m z(&6o^OGEMdO~vnAqd1iRsj2*b-RqPG06#YYxK|H=)^ITm?8plj)jl5X#B5=JL;Ets z!Ixrv{hiW<^WR8@VA|ks`LN(G{?8C-L%iku1{vH}kyGSt$oM;E+>aUdf!HR69+6tW zUj%iGFP!Wg9SM5jiyr+Vn1@1>{d4M{X)>J8DVj+6$Egcn(ul>M<`hJKn=Aj6zR`QK ze@LI?bJc%G->e1YzvMug%>9q*^F?y*?@}mF{DUjYlKD?4TPAxZjMeRwYi7wWE7X$`*k7qGowNDga zU;NGd-*xDI@tOHyEOP?@s`rbYS4O_!^@+oEl8PFAQL(>JFfeu!YvnOw7K;Lc4nLzX zl6$`RoB6wP!r_4O)mP>}OnI(xGy3W)^Y=u4wNrgFnE3~kL;3IZe+Wg`|1DJO&i^Nr z%Zp6V{68ps8e}_vv*;uar+79C0DpztuigCv|M%isz6}zJVI#*jel9|9RQ+)g<45xQ z;Gy`IXvKdS%OIi?|6?r2JwrJxeYAqfB+RlTEsJ4MfQmWPHH77!q>frE8B22UYb^-M zB4JqWB6Q>`CJ6Mg769fb%4ggJ?2;i$#)#(Y6kB^x<}0p$Rqo&a*>c~9jE&#nLUUiG z{-y9M?^&aR1Qui~$j7bc2bL>5%v1b4Nm2V-Pr{YVZhl%L87~~xLy>nOv@X@bN#FL6 z_#8gREwiX}Tkl!Vt(E%>+V%Wk-?L7IPhntqo*!_KzCCMiZ)Qy6^_yDb@0 zifXB%l!TRkyREtmD|ZKu#%Fvqv{-`o0%FiK@YhtR)1k`2Q)nZb;d%HB$&YX@qSmfw z{Rk`J1eo~Dvx3%fc-~Uy<{V=r#R?q*pdtu~r;r%8nBzM=rLV zDnUW&I-25G0ip1xR8Izm$PdgM74wd+uHuf_RArfZ1AR`7)j^)2Xw6<%&F2g-}2Iv5$K5_=YHO!g&9MOxe&v zf6Mf+=+Me?%iHEpw92{7pDBD|E@dImj#s3^K*XauJSbRya&a<|l-eoyPQoCn%!Gqc zSHj{<3f>N%D^F5izC7qYg7p~N%+S2$Z~XjIMB^Pgf1-TrHGd)_`i4L8-scN{q8jOc z5B!M|`1h$FMh4$RJQK$j5&y_+jA0%34~}58>bQS#kPTJc-$iceFFy<@%!m^PE71H# zxN!gnr+4Qrpmg{-^|rSGOK;x;oZRoP+$bmpAq~vC>omY<|IgUPFW`V))0d*r-*5t8 zAn~EIUjCz$BZVx%f1AooOdI|w!X!U`@gIxOE^hPJBEU568(EOi-4_vjM-z zAJhA>^Tcr!RxRd`|^s=(m(JaMS*w5R1RJhV>wjGcqbkz_n+WH<;bEw!jBjj z?Fl9!U$MZy$QSO$^2zhee~RCKz$hS5fEmTqyu(7Z{jG2{z8;aksO?+VRo9#rhXb>^CfP85hWZN1ipHIL?8%OFcZp` zhkgIHLzCn3KjnF}a8>dp8|$h>evBUVWBc}9$HfVbx`BNe0d0C3bdG}QNtkse0UR>! znKCc(E`iyDUu=`d(t(wm<;Z7=ND?5d;2c_Aw*~k{N_b~bZove|{xrBj3m{-byU9@& zrm##si)ZN}L3|vB)BSXR0tD|w4~wF_V*yXkgAZ(E%CGD&!lnI(ug$yREc`^P zN91_HkEn5mzMF=}Gib&^vXcYQBfxZ6YUKh>%)=OV4-^Z|JPUhx5S*QbQ)p9==fUI@ zDy`KoWmix+ER+=J$^2OioD@`NSA~jUx2Df1XBQ&{)04vg)3929-2}RsD$qrZ4vv^? z>bmv?*+o35Z4*vi7xi$T#mC7GCF6}34%4GJkMawxh24;1o=u_NKKx(PpE_cy8_3bl zfT*%1;&SAh%eL^RWlX-Gi`ov)U~vK?(6%7d%DxH@YzRPD*PUg<{7AMbsp}#-X&K$H|_Y(gsX02m+w@2 zO!F6g1IltwfPOpUWoYJx5lH4|u%Zs4e5PA_>IxD=xrBCqqwH)JTzKKAH||c8c@Sw1 zcMi8sT&ra{Cdh}gAgi&a^sQ5%8Nw8p{WSUlXHzh10pw(nw-kCkcT?w~e)mM3Ru&>EZsyluqGNUX6qz^AOJ_)z@qMtL|_ z*upErR4MLSAXl@ zP=d9_E3YPfHHf?PR9Rrbp)wHfdlGE{n>5fi={%f9!69xBSQTG?vaehQQ91F&rkVm8 zK7!WvGK*|Ui{-kCJ0ucK7LR)8JrtLcPt<4n8@#OurB ztDs}XqiS__QRIJ+LO*Rsf_Zrs!muzVZ8~roA%6>o&Kt=`ZdhqbZrW9U z!e0o32EXX=5S5NEk zvPRaF++rxbWp#js4zAN1v~W;M@?CW1LHe;w<<^`eia;>1IU`}qYQVyL{#dE@OW`z`#> zH|^f@D4r(g*V@9RovV*>?V8%$)PbjAFv*j&8{z=r#OR3(`bn6hWF-r|bp;3^m|j$v z_v0j;@KWTOFsJNWl}eK`3CSUG_XoLrKLlGm6yk{4Y#;i8&TTh(_*k}jnDV83+QG%89QH>KQ!OW+?z(^5!RjIhtd6DnL)mYS>C zN?$>-Y*!H$RZT(%6I<1zrmK*DOQTqB+`PN!L~UIwXTec8V`=;KP-~OiE-MCd`fnZe z-+?l3INa+PM)_K?*cZZobLLSLql23)jI%^OPRycVtT7}B^oAzH)}NO&##29u$)%`WYN0pTa{S zb=6x~X^z8O`B563oaS+uWu4pQg-3(Ex(4CcOf@c-1Ve&&jyU6HGcHoChDk3EnJ5$| zd3BO@aAxHy9DLP(n!&iHm9ktmtqzK|h$n^5$}k(_hn{EQ@H@l<5(W@&hs7YOQQQr) zNgADzT&{LH3?f`$-wL%Q2oqjj`zUkEZpL@xoksaOgO7%qL*aV&oQMz}CNtO&D|v0E zqHehIhQcVVR+y;dglolv^yImeP2!;}pb61}534K=@tv=KwN+W}8VFH5ivR}KWX*6d z^2*^wj)Hc58<#k=^yCQEE{si6{1qbk90l*c+`ru$R+~uWeG5R92daS1%$mz@(|AoV z_}Rs)S61N$Sf62ZtdtBBbV++#bEKVWZ?e#pi9T#YqrFKwg%wt2O^wG}R)yr>&3rqy zx9`y{B*4dM;pTukV`Z z%G-8p5RiA<l&v78t0eqCGaXDyPEzwP0pWT6;pP0)yT%ej4kGIRwwS?F5Nv! z*wlhDVJbIv=8cpJ17x!We7D7*;BTpAjx~0*0$#m;4PS9%KMjvyui-N%S}DWPPDB^F znICl}#YQb*c(?%yQm(ef!uPR(c#fkPOl*xvW5X^;MO?g7`Z7-L~ zhl)yn9c_~zXg!QKtGx|h`Tv!pKN{0}Ccq!`2E+_K3gBQi#S**`vH#i7j@XB82rNb0 zr+1_&b&PMrYDsNW&w?D%eCt7CZ7xtRV7--KF{kop|m(y^b zz;^kSLD*9L7F;Fdxie)Kr7HwfGk7|2pPxJf-#FN`S9x9I;5-hZf-s zyp4KR_{j5s6^g!uI0GA#F1Avbyq;++`XY~J-S9J@vMF6OE2rh?d3itl2jAmK(!73u zFd{PDubmd86a(V{1UfWNsU1EV)$)2r5QCWBtLXP0#KY4r4KBYzEcU5NJ63-d1R=G`+0Tb43q9h6R3?ooIee2Nl0-!vipuOfDQ<$27|c4^}q z$~<`DPhTuZldYcO6=mg30C#XNI9q89f`D6Fo=$V#hmA6MiIMp2M~9S-ZS6vTLkvc} zLQzPoG$*|^TMeA$6#HMTSv3+jSpvsV8Y;LQo_k=g+)Hk?CzWd?iWYKG87;!A8&ag$ zL5bbbS3C+W2f1*BAInEJ3n%d=rOKzhuDUFaLnJ zQkc(z(Ez&hqJvKl<{joFN;jQS`Rn#uhOpJT!+E6$65E1ue*SD9)_ zve>_zg(qPQQ*f4K5gpYQs$0PHptjvvGK1lK0ShWa&6ONA_g<-t#KpRwc0S{h>A_Tx zt{{r(PIo@TV!LF^G|wUzz+&EZV1_)X^%&ys_0WIXG(YvMpYQ&NYD1sTfboMFzYG{d z0~Y{8dUBAwPZIQ5K|&{g+Vcm=>o5y*^bSNBUIj6503XBf>{B}zEXEl&yjjdJSxPhu640z{+*r?cN$`| zee0**kCu@DL+#&%m@q~e7AYWO$3FpL_R+0?*u~Nkfy`*;QLMXv{>&SZF*XwIOvS?8 zArpg@Lr>l)kU$^fF_=>zsAuitQ_JhAiH1#Zc>QoT13x32|+?FA^nV*K~JV9KG1fXyS zHuJ;vVwL4G*ToNfgL;^U^RvTKSU|LU8`5XV`HCD-=na=IGy-d6wI+)+Mc37s9ab4h zrMdZ2+49+NL&I8)khoqQPz#%9;WNS6r;_}5q2YNWM!~B>god86;`p48;wzx;;ae>;*c?` z!4eKR0i)J`y-vKpWo~s!anj0h=d?QtiMeQo>%~h^)R|ORSEe7+pMAikGbvU4`~-Q@OOG z*>vED820Z^adtVAL$BU$ZX!h%8x&c3PBSDIg$>rp(F=cq=O#A|n~q=PWBmdb%;qNo)|9Q;h-;0Ys|}nwyp+ys!Lmt>MWd+1 zlENt!(Q}8KAD^;wh=02hnVewq1ih%(tlI!kkXR&Lo-7 zgLvARM!_sO`DQr}gF&70Yam>ew?Np(=y>5E4Pw6VHFajMloU~yq@1V(Y9(01X#Rb` zhoAE_cvgmpwPX~WM|loVOT%!pI!sUm`#I?feLG+UN0>-)nEG=ZRHPqOJ?d%^TLbEU zw#=p*x}P=G=@yKQ6?l!@hraIWlq^vxSR9h7i6Vx?t28!BwhGKG3F;b~Sr5Bt!mtzu zme>oWVx}J5yQ!9%&GwY`Pxx~een%GH)>^Km-^`-x%1H=x)3~(nm9^nlZTNbfhAWqz zlwQl#C)fi%wpV4vIK#w?VxBThc#~RxM_1G;K)SVd4|w$sNO#N|E(uU6MikJ@ao1=) zSIgMb5U$ilQ1`#88oKebmxaU01X4wfJYHnkcq%U=uQ$t>6=!_E3v3a z0b)yP+Y&@R+M*Sce;nqMQ>~j8ZNbHC`dCZ2X;aFJ02))X41nUBg;%%b{EC5pQDq<)YXTTLD)RQc-= z?ZEANC2gl5@S05%G$UoeQACk{*K7Lb-Ew#FBFQ{>R_I8^E7ZM=!^*kpJk=GDhfR&B ztUgt(Ix>qV%Oz!*Uwlhk?z&w1%dQQtQs<+Yeqt z;RU@TpbI1npi9;pazkciIv~MP?1k~->Vu2dCu14BYvbej&nP^F?!y!F9XWYs3GyVqtSM|7Vt!Z`+iXo;!RRRaXj7di3V(EQKHHE5)GkjE2TV2>0KXh z^z9)A{_KHrJ3$C%c=w(si4{=@hn9Q-abtF-e=oAUYAh-th?S6|m<6>RZUcVr0pm|D zomimQy}&&LaXr<%IWC&Q`3BYZlqIAMub1_Ecf1^%d+$zvp*WlXjU7X`uLI!DPnGhx zblKZS*`JcmB0vv^7B-j+1Y1%3Dasp6 zpcx<8q~6>Tni?%pjbmM9siZH3R#SzXH(TkEgUKUdr2=w-dx z+ma;?#@2C^L&Y{eMebR`hn%Je_BH#uX-6Z@=#ZD>;{q#x_gGH^Oi@^}fwtM3|Jf^u zuQLB#al@5s3qk(LE{?y6hh>&jKtiu>$?RgTBI?#9s#gf-%Fqq2X-#&k-$nP!u#|Ce zJWoy<*LtJ&Ny=2tBLRTDpBPb%k~%yt>J`job9_sPZ(9`8nk6Uy@BjY4EX@!><}~<% zqA?{861X*g3*g9g!-+!StOaW$Y#oJR+)6_hsC2E>_gtD;n6u-Ce;>E4bysCNgQ!nb zj;ylA6#?BL^_7xHT{*6)ob`N+Tdfv4GqDEJSTzPjf-u-D!q44u!rK&`kEX&-+Ab$M zvcI*MxL9uu9-hjwa?`()Lfld?F8E>@_Vbd9P#Qgdr5{p66$9#*#w9nr4yz%OZ0)`n z=4+82W=&P{!%Y7!M+Ws?T~)&#vM)_{s%m_A)fEkKi++2$1IGV(hYBi&&S>NQHyi@X>vD1owqERD+)p0y1W$j%0(gn%maO0zpoTL9nS`+iBGSEo zi_fmQX5>2AquSa&S`R7gD=U%sm$M0nYa*>o1LbVjCuRxG+fawz*KlSzXLk-C!3l5x zviH4m;ja?%{M3MYrB=qjfpxEeZ0RYH;Oj2FYE@iL1A+((U+8?09g*RLu&D9dRgRjP zVMLZ1qpA7)`*}0;=zTJ$@+$oS;aUEQtN&)jihZqDw&6fn2!z*M zQCuG?lEPVZw4$DRz;Ie zz%S5i$W`CK1;D6jbVM)eMdT>xKu%0IzQ=p`Vr7?Yu+65f5u;oOVTl$SNQIC|-qlx0 z9m1-|Rm}id+-$)7{}s9__3S%;AgUW$X>2Q%tmj?yzSb4uF@FB%RgjGNWc*ZLp?MyN*R z)XW((Zq;1Zepj`FfA20{M=AIS<#EKD3-C$|*eCsVIwxEwv-*5fyC}Vl3O@u{{z8l2 zV_tSuvIEkySVC`5k1IcaU?xtNJOS{e1Z982*cQexbzV`w^&!KmF$+BehAKzX#@hgP zn96JPPhNJ7GX)t@G3DBs(HrdbGEWlpf(AS0+8G~_BarFZjQq-YQ_nHSpZIEMqj?Rz zDT?wU$Z2b1>9z{!q>G_vtax*$Ufc!Diav|3t(dRu zyNb3L4;)hbAzPHXDx*HgzObAvK`CVx1MSy)-c=rXtDbs!Wx+Z$VyiBHArjhrazaA7 zB3g*us>}1{ExBQxb<5nZ>yFA^S?IOduXL2oSUgGr@d8;MeL=2OND`S|-zA`iclMgc z4}{BQ;R@Nk#Hmz&S2c1CuM|-1>oq~*|1|7Z0>u%z0Z^K=H9+fMV<`3)kO7xy`Jf&r z@ayef+-vF9)cUM;LvlCq?Wo^0oBU>LNWip{yy^O1P$t&DZ0~@I|Dsb>Gt)!MqR``5{feqRPlC_=IqaZY;eP62Bqr=sZiN8ntK(~ zz@0msaCSgc`(fTH)(nv?-1e&Yuxx~Il2G{pU0C|sIw7o~EcR6*Ne#_7L8t5gzj(**Z zsmb+}N%gJYy_c^ab)LO={IX-UwHFo4H-BAjTH(sqaQFL7<9nxZh}hxmH+vYZ$Hvvy zgTK*o+`SHuZ>W#gIo*$@WEMl7mV}9o!D%4yGLm_lN zv-m|;I#{5qZ{aL@v=|R2o0}6Q)R5}4Kmiw#cE)y_(E7|V7Bnz{v~Jjf1L%TX(j019 z=vifdp{wVDZ=>vS=Xrs(;|MOg!%Mu+UK*E$Z=X9nBpgahLyD(Li6F!>iaUe&B; zXp>!Mq}I`G0o9A7TnPKGxrPaD1m$s1Do?|Ia1#joSippV({j-KJRW3Nd|sD+ZFZ7b zlX7jodK^Q4#Y-*~!Otu{<6^l+IkBuoeDjzb1cu-9;X)1tp;6b`;x#+fb|%zad|J-JGt~s{w*KznGk{Mw${r*X|Qlju-yedDwv;Zl8jWcR9J zov&`4#0Fp4ICVx{%1>@u9Wt)GhecO^G3dVo{~AifbxoD%XB3XtPrAfJ>zgifLDS_Y zO}h3#cI7zw#vYic?8<(aSZ#f8%)HPiGs5Uu`GEVzUKI%P${rQCa(!=#e9)63AbL?L zR4-bw`r(Qba?IOmxxf6a&4%aq`AJKBvd!ojppssqn${WRh-PDb{ zC-DE#c*RdZrA&DSOnGKNc^2>Eq1$p>?dLSyHR1X2NdGr~+!f1H{0+TW zedXNtD@)E0d^Irn_iltVj?rDGy40TuIc^{-&66Z-1K5f7yEod|1aU;Ec_JC znuRs`yNv(B(u*>D&%Ieq{}oic?mj5W_UO*1@xCZoq-%UPP->%`rdJiyP=TWPJcd`( z2^l-`%3cL9FKaHh{=eRTj;^Xt0)fbXj= zRx4sR-;T(udFHgqbGUcJh-Jqx!n%Bv?l4B@uJINv_pLR8($p&>FSUwUyH0oC)cvkT zffg?i2Q4}Pv=GhKSnA3_LAu(DrdlnGFk9HiY$2M zG#ccXeafbP09S88isCIlKXLGw9v6`1}-$s)JsD9E5S|vT{HfmO7x~-Z7 z+q7t+EfdLY!2oE=WU@1jvAn2Vnpbo0bX8$U22epwRERuNs;HDbM)_&m62$UHDVcoQ zDu1A=&_7vzk7im~4SH}Sp%Ge=q27bc)qxyAI?=CxPE$popEg^7|iO-9r59Hhl)blE!LluPof zDM9kV%m1wV+B)g?a?a$~bAAuE%cC{!@wmO#UD~J0Fc$@1+_uxXHoYGIB0$UC5vHx| zSNJ1;Lz<VeOt-&-La97Q>>Lp5dc-lU!e zM#C0^L((8~QduMBCV#$+hJ~D@O zdXA-Wpf+1cFkA@2(|u;-NZ2qNk&@-z09n>ll+E&yKu%LAj&RB7$Ae93akyJ_o;H~N z81`;N&KVsZ22HmCJUnBisM{%GYDUdmD1QfxRUN@jg>&fjK_ky%ns~-lz*7D8p!3y# z@uTf_&`83rao@KdC6|q(){xSxk@d{St4)5I;$_KYpKDZ**|INxd8c?g$qrk4S+kv2z&@z7}X4Zjq zC^PHguOQ*5|AO6T2LMd|)tHHtt^q&+zy2YMK!I3^zu~nm;AgZ=F+SD&0>gNTU`fku ze<;ENgmf`0<`lmY(G@`XT1%%Sx0oGu&lC7{k4yK%Lwd~w{d5U*wMuS93WVvYzELpD z@uK3dU*l0Ak#N&VHS(pKb+(+sD)3ZJyjQQVbRq9wCNv@~ks-6RIAYp+3f&)aWiT?L~dxtkkJyMM=ioo8!Zg2P71qMI- zaKL_GucP=b@t3QV4NYsxeu<)GAlO;x80<6%n7SodBBV%eg==65t$OD>B9y{qS?G#{ zr7OWsWA5N1ztdy?6p8eskPQu!b&23Gzjb(YOf2rAjIH?{3!{`}Qh zWq!zSa<~mu1HNHV!uT=~xR*wx z^d|9BKV0^hjJ$Ni6G3cTU#5v~XKBM{g^IAeP5`g*kR6_0va`!W_S5O**=3KId^)@O z`}xN!_UZKE;`II1+1pEYe!*U!zkhRfb$0$9-+pGN@BhXAarXXAj|q4Ta>TbdAq$|U zPu|p<$S#FwsNh9fB^e9H_k0JYh0BaD1zSe9A_-v;i$${bCGDz&f-bQM{I#F*R9)J# ziv$f04h92uMVq@6p-4CxNWtXOrbfvPmi)?Z`K6Qi*z}E$po7AW3MRxz!la*yjtW71 zbA&WBLxIh91q0yuX2$4qOX0?*UBWyr7%KiL=hHZBRTd@kO8Y`qt1_m%Z2v(07>meGHZoj zAiw2_A7!%bbNC`m;md=;4@}6w57WK~Z}Ypof;uP0E>>iSR2BO^wmVFFIKcfZB)9Y- zN~-VcEJ%Im+866Mz3XF{z>PKk*2fsqtLgdbk_KEGw&U`T&i-l#6 zW{z1fUhwIBJY9??mNj+U;bJ&jI15WaoXLk%*HnxPtvO;0C(Bw4zu~_Thf8o6TZ@G` zvghOBa5A6rnHbK^p=HAW)3U7Dq`?7Z%Rv&^Q5p>{)JOlPC~%E5^}xy<4Hu3xw@1)> z=2_O5FD78iId?7FoQ@u2^^TxM&TD2*JbOO(tf4KOxiubnrYWG8Ga3$u)^Pk7uiu30 zXX~7qC#;beTQhTJEu5j}z|e_liP>ysy8^r)V|F2yS-_K=8+S$?Hrt)pHuomhV(M|* zns9sWiurhCIdyLSno7vP;fecY4VCy8oWVPhe6g3g{}F=2x#e1pH6Kmf(P+-6;~|{C z7@1;fabD-P5jFv$D5T7dq?!t2?T0#mZPObKxr^AEjTg??bjS1Q)Es$GXyMLn1n~87 zBh^2iH(yiIe9zYc3DonyA#rwU^{jtu_4^(6BkM{Lct*W!u~%&Cnjrzr1bfLkog*yj z^i?t^F=xvV=k#L|eBcR$p_0kpcKhbm7eFG%sP;xp$2_nUc zvcb9$0IMJJ0Q`EYLVY`8e;g3A_kc14o zed~ywoWTA;r{Km|osRu@qJ?QT_v&{d(kuxTT^e1Yf`!Yjl5>BoEm?|G`S_=dYT`$w zTV+W`7R2O}LZZ=G75m1;XKW! z@D*UwnQjA0YC_oxkM!s|H5Z}u3D^OPkZ%eUxv9h&WmJ?GjX;D;v?3G_W)%&&iD{CF zA_fXQ&IyKZd?{2r16;!dJisV+2)rSHA(u2&SMufs`rx~i3eOtl<|-2GPO+Wa_8*l& zd0h8>rN0C)?H11L){G^WUywdqRuM;IO^0$feQt4M3*#ux2yeX35+J#>OeV6im2m@` z^%NhcnRj~3=8_DoALN?dS5e6utpB(_E{UAB=&R_oT9*^+lhshe02FSZ#Ve&i;6i*~>lGdrz|9{2 zseQ?-3W^G{!`p98kh#=N`KiwX)_K**onc6H>uDFcRuNv(H;^G&pzGsF!qudsb0h9f zZh4T2jxsk~%3?hbb``=mw^ihpyLEG02s!B-9amCRg4*VsOJ|>Pr^nQ-YpqGn`2MJn zjouq>dqn&Lem2mbCU@0(P3pGtQ;pYmbqWKhCWTC#K5f=Tm7bB#b&7=%%#DN$J1JAi z4Sp?$M=CMXtf3HdUmn@Y$R|1YYvhNOe}Ba{?cQxC7Vh14{FY?R^hKR1Y5h=C6x{Qz zRP!{d<5+5obR&e;>)>(hqK+19j}_aK{i(j&Y`uGJNZHdkF^C+65g~v$pYF@;g-By} z649$)KbG`ME)ORY*jZ)>R|E*o(;!f^va2^b_mMJC8gMvn7=$cZbwlwkVk2MlA&J&N z1O}i-x5#}ZZ8NUQ>pQ|Lm#y`yQMN)=?;L&ulqin_L#v_2J1XQM&S}RF(PRWcr&s31 zu9^^Oq3Ta#!~OJQnEJWOG=<|g2s##^@25eVitGFU=>cA-D*lFMx-lSoK4k#+N!#XVcstyCF=+~!YYD^WEnI{d_5R!3{1&Z zR|_-hTz-!75)Xj6f|m#Z@*ol1y(^E7tA~7S>BG~7bCl3Jr(y)E04%u3Rg?uTVOC3N zb!Y4hklnFK7n_;1#LT21{+orV{W*e?Dlt_{+CVzmS$tvL3hSWzUK!JkP9K~<+2$(Q*;mj)jj2a6<@nrKJ0AI@a zm}{rx*?zhm&^eF+bb?ty*4i{tEGKAZ^q)H*cokdn#u1NDeGr4ueV3a zzLc4e1IsiUoJvedP1c9JX)FW3sQCR3T7@XCplG#tcMG}Iu67#>*(0g9hCh2P^$NUH z*MT0mpor%H>q39q)s^;ITKwSv1wajeDTy9cLSSEGv(A0X*8Grof;yXAVEdRLcw;n* zq8%wZpo+UjbhAy+I)!nTcAxH|EndVeSLL{M^ZuZ9fm+)r5C%?Dgyf!kPvw~JWv&&@ z3@=$S8Zl*(eed>Ir)=u0pN@>wgF2q3)Iw80;K1o4;ipDtTisndRf;pTxGh8SoSXP= zaq3jVUI_B&v^$&t8!8xKnapv7lwKB8me51E)Hv9`5f9YMHq@(zIc*47bU*EI*;jma zreK~UiUva1gAy9FTkv`(NNDPy&b}H_oqOFD5Yyu=lf8J+aKv{NU#fPGl;eiej-CKw zp1Cn4`pp4Nb0hZd10B=!E*1zVg?;3WAAjP$PHHsNE7o*c)gN`+(WT(>lN!;W=ty4y zLiLO=FJHu`|2M|L<5r)es&KUhR>}7Mpfrha8D&6HR4s4c2W5!f`7Z=P3!RE27DUT# z=hTj}ly*TADlnD@jbU_hw{GF|y+P67t*P_XZpp3iN*pDrL??+qWcr)7uvm_))L;D+;NPEsfM3>Ui6?iK-kvx`lbUG=OAQQND@4 zyuIqQ1|(DiuygqgrZIroJDh1r$cnKHd+1p~eFR<~sO|11KR=+_bu~eB+YcH#DuJrU zZ=LFq!FQEFoZ050#sh-~ys`CIqx%4TuPx-b-9-LBmfu&N4Ee3UMB?~4e_l^6$PK4% z!41!g`cepTKj4LvkK1v*7Vqj#qA&O8dSV#R?AqEHVG z?uu8A^gI#_DyaI`<`jRe*Ke)T4|j-+My7wM1$vy=BeC452>(>%iP^1 zx~DNgULG|HEBou-HnW7&Q#bOrex=Y?FVjfdaML+vJ|)=T#Nd1uEN$EAikjlQ@+-O%%(0mkoZ$l~DOe*q6|3tlP!00M7h ABme*a literal 3785 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 From a1ef1c996cb86242e8ec837ecb0a9832c9318e96 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sat, 25 Jun 2016 08:22:14 +0200 Subject: [PATCH 33/79] Fix physical manual update of state of device (#2372) --- homeassistant/components/garage_door/zwave.py | 4 ++-- homeassistant/components/hvac/zwave.py | 2 +- homeassistant/components/rollershutter/zwave.py | 2 +- homeassistant/components/thermostat/zwave.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/garage_door/zwave.py b/homeassistant/components/garage_door/zwave.py index 18a2ea96b86db4..b527fc0052c941 100644 --- a/homeassistant/components/garage_door/zwave.py +++ b/homeassistant/components/garage_door/zwave.py @@ -51,7 +51,7 @@ def __init__(self, value): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.node == value.node: + if self._value.value_id == value.value_id: self._state = value.data self.update_ha_state(True) _LOGGER.debug("Value changed on network %s", value) @@ -59,7 +59,7 @@ def value_changed(self, value): @property def is_closed(self): """Return the current position of Zwave garage door.""" - return self._state + return not self._state def close_door(self): """Close the garage door.""" diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py index c950200932a5ce..2a9c0726f92f1a 100755 --- a/homeassistant/components/hvac/zwave.py +++ b/homeassistant/components/hvac/zwave.py @@ -98,7 +98,7 @@ def __init__(self, value): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.node == value.node: + if self._value.value_id == value.value_id: self.update_properties() self.update_ha_state(True) _LOGGER.debug("Value changed on network %s", value) diff --git a/homeassistant/components/rollershutter/zwave.py b/homeassistant/components/rollershutter/zwave.py index 288488fe057edd..ea0be0ddf74f7c 100644 --- a/homeassistant/components/rollershutter/zwave.py +++ b/homeassistant/components/rollershutter/zwave.py @@ -49,7 +49,7 @@ def __init__(self, value): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.node == value.node: + if self._value.value_id == value.value_id: self.update_ha_state(True) _LOGGER.debug("Value changed on network %s", value) diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py index 8d7e36cc2aabd5..ed653874af2925 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -81,7 +81,7 @@ def __init__(self, value): def value_changed(self, value): """Called when a value has changed on the network.""" - if self._value.node == value.node: + if self._value.value_id == value.value_id: self.update_properties() self.update_ha_state() From 2ac752d67afde42df59ab28eb6e6e8085a4840cd Mon Sep 17 00:00:00 2001 From: arsaboo Date: Sat, 25 Jun 2016 03:02:28 -0400 Subject: [PATCH 34/79] Add OpenExchangeRates sensor (#2356) * Create openexchangerates.py * Create OpenExchangeRates Sensor * Add openexchangerate sensor * Update openexchangerates.py * Added params dict * Update openexchangerates.py * Update openexchangerates.py * Update openexchangerates.py * Update openexchangerates.py * Added API key validation * Update openexchangerates.py --- .coveragerc | 1 + .../components/sensor/openexchangerates.py | 100 ++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 homeassistant/components/sensor/openexchangerates.py diff --git a/.coveragerc b/.coveragerc index 7d030d8640cdb2..6a9d1fc11fa579 100644 --- a/.coveragerc +++ b/.coveragerc @@ -187,6 +187,7 @@ omit = homeassistant/components/sensor/nzbget.py homeassistant/components/sensor/onewire.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/openexchangerates.py homeassistant/components/sensor/plex.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/sabnzbd.py diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py new file mode 100644 index 00000000000000..f95e5c3623356e --- /dev/null +++ b/homeassistant/components/sensor/openexchangerates.py @@ -0,0 +1,100 @@ +"""Support for openexchangerates.org exchange rates service.""" +from datetime import timedelta +import logging +import requests +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.const import CONF_API_KEY + +_RESOURCE = 'https://openexchangerates.org/api/latest.json' +_LOGGER = logging.getLogger(__name__) +# Return cached results if last scan was less then this time ago. +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=100) +CONF_BASE = 'base' +CONF_QUOTE = 'quote' +CONF_NAME = 'name' +DEFAULT_NAME = 'Exchange Rate Sensor' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Openexchangerates sensor.""" + payload = config.get('payload', None) + rest = OpenexchangeratesData( + _RESOURCE, + config.get(CONF_API_KEY), + config.get(CONF_BASE, 'USD'), + config.get(CONF_QUOTE), + payload + ) + response = requests.get(_RESOURCE, params={'base': config.get(CONF_BASE, + 'USD'), + 'app_id': + config.get(CONF_API_KEY)}, + timeout=10) + if response.status_code != 200: + _LOGGER.error("Check your OpenExchangeRates API") + return False + rest.update() + add_devices([OpenexchangeratesSensor(rest, config.get(CONF_NAME, + DEFAULT_NAME), + config.get(CONF_QUOTE))]) + + +class OpenexchangeratesSensor(Entity): + """Implementing the Openexchangerates sensor.""" + + def __init__(self, rest, name, quote): + """Initialize the sensor.""" + self.rest = rest + self._name = name + self._quote = quote + self.update() + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return other attributes of the sensor.""" + return self.rest.data + + def update(self): + """Update current conditions.""" + self.rest.update() + value = self.rest.data + self._state = round(value[str(self._quote)], 4) + + +# pylint: disable=too-few-public-methods +class OpenexchangeratesData(object): + """Get data from Openexchangerates.org.""" + + # pylint: disable=too-many-arguments + def __init__(self, resource, api_key, base, quote, data): + """Initialize the data object.""" + self._resource = resource + self._api_key = api_key + self._base = base + self._quote = quote + self.data = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from openexchangerates.""" + try: + result = requests.get(self._resource, params={'base': self._base, + 'app_id': + self._api_key}, + timeout=10) + self.data = result.json()['rates'] + except requests.exceptions.HTTPError: + _LOGGER.error("Check Openexchangerates API Key") + self.data = None + return False From 1c1d18053b19dd4749569a6759c179bccf2008e1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 25 Jun 2016 03:06:36 -0400 Subject: [PATCH 35/79] Add cmus media device (#2321) This commit adds support for the cmus console music player as a media device. --- .coveragerc | 1 + homeassistant/components/media_player/cmus.py | 214 ++++++++++++++++++ requirements_all.txt | 3 + tests/components/media_player/test_cmus.py | 31 +++ 4 files changed, 249 insertions(+) create mode 100644 homeassistant/components/media_player/cmus.py create mode 100644 tests/components/media_player/test_cmus.py diff --git a/.coveragerc b/.coveragerc index 6a9d1fc11fa579..6526b2b1e3d252 100644 --- a/.coveragerc +++ b/.coveragerc @@ -127,6 +127,7 @@ omit = homeassistant/components/lirc.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py homeassistant/components/media_player/firetv.py homeassistant/components/media_player/gpmdp.py diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py new file mode 100644 index 00000000000000..43ddee3ba027a3 --- /dev/null +++ b/homeassistant/components/media_player/cmus.py @@ -0,0 +1,214 @@ +""" +Support for interacting with and controlling the cmus music player. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.mpd/ +""" + +import logging + +from homeassistant.components.media_player import ( + MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, + SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, + SUPPORT_VOLUME_SET, SUPPORT_PLAY_MEDIA, SUPPORT_SEEK, + MediaPlayerDevice) +from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING, + CONF_HOST, CONF_NAME, CONF_PASSWORD, + CONF_PORT) + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pycmus>=0.1.0'] + +SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ + SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_PLAY_MEDIA | SUPPORT_SEEK + + +def setup_platform(hass, config, add_devices, discover_info=None): + """Setup the Cmus platform.""" + from pycmus import exceptions + + host = config.get(CONF_HOST, None) + password = config.get(CONF_PASSWORD, None) + port = config.get(CONF_PORT, None) + name = config.get(CONF_NAME, None) + if host and not password: + _LOGGER.error("A password must be set if using a remote cmus server") + return False + try: + cmus_remote = CmusDevice(host, password, port, name) + except exceptions.InvalidPassword: + _LOGGER.error("The provided password was rejected by cmus") + return False + add_devices([cmus_remote]) + + +class CmusDevice(MediaPlayerDevice): + """Representation of a running cmus.""" + + # pylint: disable=no-member, too-many-public-methods, abstract-method + def __init__(self, server, password, port, name): + """Initialize the CMUS device.""" + from pycmus import remote + + if server: + port = port or 3000 + self.cmus = remote.PyCmus(server=server, password=password, + port=port) + auto_name = "cmus-%s" % server + else: + self.cmus = remote.PyCmus() + auto_name = "cmus-local" + self._name = name or auto_name + self.status = {} + self.update() + + def update(self): + """Get the latest data and update the state.""" + status = self.cmus.get_status_dict() + if not status: + _LOGGER.warning("Recieved no status from cmus") + else: + self.status = status + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the media state.""" + if 'status' not in self.status: + self.update() + if self.status['status'] == 'playing': + return STATE_PLAYING + elif self.status['status'] == 'paused': + return STATE_PAUSED + else: + return STATE_OFF + + @property + def media_content_id(self): + """Content ID of current playing media.""" + return self.status.get('file') + + @property + def content_type(self): + """Content type of the current playing media.""" + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """Duration of current playing media in seconds.""" + return self.status.get('duration') + + @property + def media_title(self): + """Title of current playing media.""" + return self.status['tag'].get('title') + + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + return self.status['tag'].get('artist') + + @property + def media_track(self): + """Track number of current playing media, music track only.""" + return self.status['tag'].get('tracknumber') + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + return self.status['tag'].get('album') + + @property + def media_album_artist(self): + """Album artist of current playing media, music track only.""" + return self.status['tag'].get('albumartist') + + @property + def volume_level(self): + """Return the volume level.""" + left = self.status['set'].get('vol_left')[0] + right = self.status['set'].get('vol_right')[0] + if left != right: + volume = float(left + right) / 2 + else: + volume = left + return int(volume)/100 + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_CMUS + + def turn_off(self): + """Service to send the CMUS the command to stop playing.""" + self.cmus.player_stop() + + def turn_on(self): + """Service to send the CMUS the command to start playing.""" + self.cmus.player_play() + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self.cmus.set_volume(int(volume * 100)) + + def volume_up(self): + """Function to send CMUS the command for volume up.""" + left = self.status['set'].get('vol_left') + right = self.status['set'].get('vol_right') + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self.cmus.set_volume(int(current_volume) + 5) + + def volume_down(self): + """Function to send CMUS the command for volume down.""" + left = self.status['set'].get('vol_left') + right = self.status['set'].get('vol_right') + if left != right: + current_volume = float(left + right) / 2 + else: + current_volume = left + + if current_volume <= 100: + self.cmus.set_volume(int(current_volume) - 5) + + def play_media(self, media_type, media_id, **kwargs): + """Send the play command.""" + if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: + self.cmus.player_play_file(media_id) + else: + _LOGGER.error( + "Invalid media type %s. Only %s and %s are supported", + media_type, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST) + + def media_pause(self): + """Send the pause command.""" + self.cmus.player_pause() + + def media_next_track(self): + """Send next track command.""" + self.cmus.player_next() + + def media_previous_track(self): + """Send next track command.""" + self.cmus.player_prev() + + def media_seek(self, position): + """Send seek command.""" + self.cmus.seek(position) + + def media_play(self): + """Send the play command.""" + self.cmus.player_play() + + def media_stop(self): + """Send the stop command.""" + self.cmus.stop() diff --git a/requirements_all.txt b/requirements_all.txt index a5ef32201c5c5c..7360074d8ab60e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,6 +247,9 @@ pyasn1==0.1.9 # homeassistant.components.media_player.cast pychromecast==0.7.2 +# homeassistant.components.media_player.cmus +pycmus>=0.1.0 + # homeassistant.components.envisalink # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/media_player/test_cmus.py b/tests/components/media_player/test_cmus.py new file mode 100644 index 00000000000000..24322b5bce0b24 --- /dev/null +++ b/tests/components/media_player/test_cmus.py @@ -0,0 +1,31 @@ +"""The tests for the Demo Media player platform.""" +import unittest +from unittest import mock + +from homeassistant.components.media_player import cmus +from homeassistant import const + +from tests.common import get_test_home_assistant + +entity_id = 'media_player.cmus' + + +class TestCmusMediaPlayer(unittest.TestCase): + """Test the media_player module.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch('homeassistant.components.media_player.cmus.CmusDevice') + def test_password_required_with_host(self, cmus_mock): + """Test that a password is required when specifying a remote host.""" + fake_config = { + const.CONF_HOST: 'a_real_hostname', + } + self.assertFalse( + cmus.setup_platform(self.hass, fake_config, mock.MagicMock())) From 7b02dc434a8be92a02a1697bd81714b3f9e2651a Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 25 Jun 2016 09:10:03 +0200 Subject: [PATCH 36/79] Secrets support for configuration files (#2312) * ! secret based on yaml.py * Private Secrets Dict, removed cmdline, fixed log level * Secrets limited to yaml only * Add keyring & debug tests --- homeassistant/util/yaml.py | 44 +++++++++++++++++++++ tests/util/test_yaml.py | 81 +++++++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 58458986063093..0e6ec01f26e55c 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -5,10 +5,15 @@ import glob import yaml +try: + import keyring +except ImportError: + keyring = None from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) +_SECRET_NAMESPACE = 'homeassistant' # pylint: disable=too-many-ancestors @@ -119,10 +124,49 @@ def _env_var_yaml(loader, node): raise HomeAssistantError(node.value) +# pylint: disable=protected-access +def _secret_yaml(loader, node): + """Load secrets and embed it into the configuration YAML.""" + # Create secret cache on loader and load secret.yaml + if not hasattr(loader, '_SECRET_CACHE'): + loader._SECRET_CACHE = {} + + secret_path = os.path.join(os.path.dirname(loader.name), 'secrets.yaml') + if secret_path not in loader._SECRET_CACHE: + if os.path.isfile(secret_path): + loader._SECRET_CACHE[secret_path] = load_yaml(secret_path) + secrets = loader._SECRET_CACHE[secret_path] + if 'logger' in secrets: + logger = str(secrets['logger']).lower() + if logger == 'debug': + _LOGGER.setLevel(logging.DEBUG) + else: + _LOGGER.error("secrets.yaml: 'logger: debug' expected," + " but 'logger: %s' found", logger) + del secrets['logger'] + else: + loader._SECRET_CACHE[secret_path] = None + secrets = loader._SECRET_CACHE[secret_path] + + # Retrieve secret, first from secrets.yaml, then from keyring + if secrets is not None and node.value in secrets: + _LOGGER.debug('Secret %s retrieved from secrets.yaml.', node.value) + return secrets[node.value] + elif keyring: + # do ome keyring stuff + pwd = keyring.get_password(_SECRET_NAMESPACE, node.value) + if pwd: + _LOGGER.debug('Secret %s retrieved from keyring.', node.value) + return pwd + + _LOGGER.error('Secret %s not defined.', node.value) + raise HomeAssistantError(node.value) + yaml.SafeLoader.add_constructor('!include', _include_yaml) yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict) yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml) +yaml.SafeLoader.add_constructor('!secret', _secret_yaml) yaml.SafeLoader.add_constructor('!include_dir_list', _include_dir_list_yaml) yaml.SafeLoader.add_constructor('!include_dir_merge_list', _include_dir_merge_list_yaml) diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 244f932333475a..7bede7edca967d 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -3,8 +3,9 @@ import unittest import os import tempfile - from homeassistant.util import yaml +import homeassistant.config as config_util +from tests.common import get_test_config_dir class TestYaml(unittest.TestCase): @@ -135,3 +136,81 @@ def test_include_dir_merge_named(self): "key2": "two", "key3": "three" } + + +def load_yaml(fname, string): + """Write a string to file and return the parsed yaml.""" + with open(fname, 'w') as file: + file.write(string) + return config_util.load_yaml_config_file(fname) + + +class FakeKeyring(): + """Fake a keyring class.""" + + def __init__(self, secrets_dict): + """Store keyring dictionary.""" + self._secrets = secrets_dict + + # pylint: disable=protected-access + def get_password(self, domain, name): + """Retrieve password.""" + assert domain == yaml._SECRET_NAMESPACE + return self._secrets.get(name) + + +class TestSecrets(unittest.TestCase): + """Test the secrets parameter in the yaml utility.""" + + def setUp(self): # pylint: disable=invalid-name + """Create & load secrets file.""" + config_dir = get_test_config_dir() + self._yaml_path = os.path.join(config_dir, + config_util.YAML_CONFIG_FILE) + self._secret_path = os.path.join(config_dir, 'secrets.yaml') + + load_yaml(self._secret_path, + 'http_pw: pwhttp\n' + 'comp1_un: un1\n' + 'comp1_pw: pw1\n' + 'stale_pw: not_used\n' + 'logger: debug\n') + self._yaml = load_yaml(self._yaml_path, + 'http:\n' + ' api_password: !secret http_pw\n' + 'component:\n' + ' username: !secret comp1_un\n' + ' password: !secret comp1_pw\n' + '') + + def tearDown(self): # pylint: disable=invalid-name + """Clean up secrets.""" + for path in [self._yaml_path, self._secret_path]: + if os.path.isfile(path): + os.remove(path) + + def test_secrets_from_yaml(self): + """Did secrets load ok.""" + expected = {'api_password': 'pwhttp'} + self.assertEqual(expected, self._yaml['http']) + + expected = { + 'username': 'un1', + 'password': 'pw1'} + self.assertEqual(expected, self._yaml['component']) + + def test_secrets_keyring(self): + """Test keyring fallback & get_password.""" + yaml.keyring = None # Ensure its not there + yaml_str = 'http:\n api_password: !secret http_pw_keyring' + with self.assertRaises(yaml.HomeAssistantError): + load_yaml(self._yaml_path, yaml_str) + + yaml.keyring = FakeKeyring({'http_pw_keyring': 'yeah'}) + _yaml = load_yaml(self._yaml_path, yaml_str) + self.assertEqual({'http': {'api_password': 'yeah'}}, _yaml) + + def test_secrets_logger_removed(self): + """Ensure logger: debug was removed.""" + with self.assertRaises(yaml.HomeAssistantError): + load_yaml(self._yaml_path, 'api_password: !secret logger') From 04748e3ad168113ad86fe1d546369ac3766e9b95 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sat, 25 Jun 2016 15:10:19 +0200 Subject: [PATCH 37/79] First batch of (minor) fixes as suggested by @balloob --- homeassistant/components/homematic.py | 115 ++++++++++++-------------- 1 file changed, 54 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 751db436defb91..81b4fde9596d93 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,7 +15,6 @@ """ import time import logging -from collections import OrderedDict from homeassistant.const import EVENT_HOMEASSISTANT_STOP,\ EVENT_PLATFORM_DISCOVERED,\ ATTR_SERVICE,\ @@ -157,31 +156,27 @@ def system_callback_handler(src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: - try: - component = get_component(component_name) - config = {component.DOMAIN: found_devices} - - # Ensure component is loaded - homeassistant.bootstrap.setup_component( - _HM_DISCOVER_HASS, - component.DOMAIN, - config) - - # Fire discovery event - _HM_DISCOVER_HASS.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' - } + component = get_component(component_name) + config = {component.DOMAIN: found_devices} + + # Ensure component is loaded + homeassistant.bootstrap.setup_component( + _HM_DISCOVER_HASS, + component.DOMAIN, + config) + + # Fire discovery event + _HM_DISCOVER_HASS.bus.fire( + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' } - ) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed to autotetect %s with" + - "error '%s'", component_name, str(err)) + } + ) + for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: try: @@ -207,39 +202,37 @@ def _get_devices(device_type, keys): keys = HOMEMATIC.devices for key in keys: device = HOMEMATIC.devices[key] - if device.__class__.__name__ in HM_DEVICE_TYPES[device_type]: - elements = device.ELEMENT + 1 - metadata = {} - - # Load metadata if needed to generate a param list - if device_type is DISCOVER_SENSORS: - metadata.update(device.SENSORNODE) - elif device_type is DISCOVER_BINARY_SENSORS: - metadata.update(device.BINARYNODE) - - # Also add supported events as binary type - for event, channel in device.EVENTNODE.items(): - if event in SUPPORT_HM_EVENT_AS_BINMOD: - metadata.update({event: channel}) - - params = _create_params_list(device, metadata) - - # Generate options for 1...n elements with 1...n params - for channel in range(1, elements): - for param in params[channel]: - name = _create_ha_name(name=device.NAME, - channel=channel, - param=param) - ordered_device_dict = OrderedDict() - ordered_device_dict["platform"] = "homematic" - ordered_device_dict["address"] = key - ordered_device_dict["name"] = name - ordered_device_dict["button"] = channel - if param is not None: - ordered_device_dict["param"] = param - - # Add new device - device_arr.append(ordered_device_dict) + if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: + continue + elements = device.ELEMENT + 1 + metadata = {} + + # Load metadata if needed to generate a param list + if device_type is DISCOVER_SENSORS: + metadata.update(device.SENSORNODE) + elif device_type is DISCOVER_BINARY_SENSORS: + metadata.update(device.BINARYNODE) + + # Also add supported events as binary type + for event, channel in device.EVENTNODE.items(): + if event in SUPPORT_HM_EVENT_AS_BINMOD: + metadata.update({event: channel}) + + params = _create_params_list(device, metadata) + + # Generate options for 1...n elements with 1...n params + for channel in range(1, elements): + for param in params[channel]: + name = _create_ha_name(name=device.NAME, + channel=channel, + param=param) + device_dict = dict(platform="homematic", address=key, + name=name, button=channel) + if param is not None: + device_dict["param"] = param + + # Add new device + device_arr.append(device_dict) _LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) return device_arr @@ -282,15 +275,15 @@ def _create_ha_name(name, channel, param): # Has multiple elements/channels if channel > 1 and param is None: - return name + " " + str(channel) + return "{} {}".format(name, channel) # With multiple param first elements if channel == 1 and param is not None: - return name + " " + param + return "{} {}".format(name, param) # Multiple param on object with multiple elements if channel > 1 and param is not None: - return name + " " + str(channel) + " " + param + return "{} {} {}".format(name, channel, param) def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): From 5ca26fc13fb6ceed3282bf9eb68dc0c6abc56613 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sat, 25 Jun 2016 16:25:33 +0200 Subject: [PATCH 38/79] Moved try/except-block and moved delay to link_homematic --- homeassistant/components/homematic.py | 60 ++++++++++++++------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 81b4fde9596d93..7db0d926ddad47 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -124,16 +124,12 @@ def system_callback_handler(src, *args): # add remaining devices to list devices_not_created = [] for dev in key_dict: - try: - if dev in HOMEMATIC_DEVICES: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() - else: - devices_not_created.append(dev) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed to setup device %s: %s", str(dev), - str(err)) + if dev in HOMEMATIC_DEVICES: + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic() + else: + devices_not_created.append(dev) + # If configuration allows autodetection of devices, # all devices not configured are added. if HOMEMATIC_AUTODETECT and devices_not_created: @@ -179,16 +175,8 @@ def system_callback_handler(src, *args): for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: - try: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() - # Need to wait, if you have a lot devices we don't - # to overload CCU/Homegear - time.sleep(1) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed link %s with" + - "error '%s'", dev, str(err)) + for hm_element in HOMEMATIC_DEVICES[dev]: + hm_element.link_homematic(delay=1) def _get_devices(device_type, keys): @@ -374,7 +362,7 @@ def device_state_attributes(self): return attr - def link_homematic(self): + def link_homematic(self, delay=0): """Connect to homematic.""" # Does a HMDevice from pyhomematic exist? if self._address in HOMEMATIC.devices: @@ -385,14 +373,26 @@ def link_homematic(self): # Check if HM class is okay for HA class _LOGGER.info("Start linking %s to %s", self._address, self._name) if self._check_hm_to_ha_object(): - # Init datapoints of this object - self._init_data_struct() - self._load_init_data_from_hm() - _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) - - # Link events from pyhomatic - self._subscribe_homematic_events() - self._available = not self._hmdevice.UNREACH + try: + # Init datapoints of this object + self._init_data_struct() + if delay: + # We delay / pause loading of data to avoid overloading + # of CCU / Homegear when doing auto detection + time.sleep(delay) + self._load_init_data_from_hm() + _LOGGER.debug("%s datastruct: %s", + self._name, str(self._data)) + + # Link events from pyhomatic + self._subscribe_homematic_events() + self._available = not self._hmdevice.UNREACH + # pylint: disable=broad-except + except Exception as err: + self._connected = False + self._available = False + _LOGGER.error("Exception while linking %s: %s" % + (self._address, str(err))) else: _LOGGER.critical("Delink %s object from HM!", self._name) self._connected = False @@ -401,6 +401,8 @@ def link_homematic(self): # Update HA _LOGGER.debug("%s linking down, send update_ha_state", self._name) self.update_ha_state() + else: + _LOGGER.debug("%s not found in HOMEMATIC.devices", self._address) def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" From 43faeff42a60f9b14cd1e19e5c1926b75ca80956 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Sat, 25 Jun 2016 18:19:05 +0200 Subject: [PATCH 39/79] Moved trx/except, added debug messages, minor fixes --- homeassistant/components/homematic.py | 114 ++++++++++++++------------ 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 7db0d926ddad47..317397c0b9cf54 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,11 +15,11 @@ """ import time import logging -from homeassistant.const import EVENT_HOMEASSISTANT_STOP,\ - EVENT_PLATFORM_DISCOVERED,\ - ATTR_SERVICE,\ - ATTR_DISCOVERED,\ - STATE_UNKNOWN +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ + EVENT_PLATFORM_DISCOVERED, \ + ATTR_SERVICE, \ + ATTR_DISCOVERED, \ + STATE_UNKNOWN from homeassistant.loader import get_component from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -141,13 +141,8 @@ def system_callback_handler(src, *args): ('sensor', DISCOVER_SENSORS), ('thermostat', DISCOVER_THERMOSTATS)): # Get all devices of a specific type - try: - found_devices = _get_devices(discovery_type, - devices_not_created) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Failed generate opt %s with error '%s'", - component_name, str(err)) + found_devices = _get_devices(discovery_type, + devices_not_created) # When devices of this type are found # they are setup in HA and an event is fired @@ -157,21 +152,21 @@ def system_callback_handler(src, *args): # Ensure component is loaded homeassistant.bootstrap.setup_component( - _HM_DISCOVER_HASS, - component.DOMAIN, - config) + _HM_DISCOVER_HASS, + component.DOMAIN, + config) # Fire discovery event _HM_DISCOVER_HASS.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' } } - ) + ) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: @@ -192,13 +187,12 @@ def _get_devices(device_type, keys): device = HOMEMATIC.devices[key] if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: continue - elements = device.ELEMENT + 1 metadata = {} # Load metadata if needed to generate a param list - if device_type is DISCOVER_SENSORS: + if device_type == DISCOVER_SENSORS: metadata.update(device.SENSORNODE) - elif device_type is DISCOVER_BINARY_SENSORS: + elif device_type == DISCOVER_BINARY_SENSORS: metadata.update(device.BINARYNODE) # Also add supported events as binary type @@ -207,45 +201,57 @@ def _get_devices(device_type, keys): metadata.update({event: channel}) params = _create_params_list(device, metadata) - - # Generate options for 1...n elements with 1...n params - for channel in range(1, elements): - for param in params[channel]: - name = _create_ha_name(name=device.NAME, - channel=channel, - param=param) - device_dict = dict(platform="homematic", address=key, - name=name, button=channel) - if param is not None: - device_dict["param"] = param - - # Add new device - device_arr.append(device_dict) - _LOGGER.debug("%s autodiscovery: %s", device_type, str(device_arr)) + if params: + # Generate options for 1...n elements with 1...n params + for channel in range(1, device.ELEMENT + 1): + _LOGGER.debug("Handling %s:%i", key, channel) + if channel in params: + for param in params[channel]: + name = _create_ha_name(name=device.NAME, + channel=channel, + param=param) + device_dict = dict(platform="homematic", + address=key, + name=name, + button=channel) + if param is not None: + device_dict["param"] = param + + # Add new device + device_arr.append(device_dict) + else: + _LOGGER.debug("Channel %i not in params", channel) + else: + _LOGGER.debug("Got no params for %s", key) + _LOGGER.debug("%s autodiscovery: %s", + device_type, str(device_arr)) return device_arr def _create_params_list(hmdevice, metadata): """Create a list from HMDevice with all possible parameters in config.""" params = {} - elements = hmdevice.ELEMENT + 1 # Search in sensor and binary metadata per elements - for channel in range(1, elements): + for channel in range(1, hmdevice.ELEMENT + 1): param_chan = [] - for node, meta_chan in metadata.items(): - # Is this attribute ignored? - if node in HM_IGNORE_DISCOVERY_NODE: - continue - if meta_chan == 'c' or meta_chan is None: - # Only channel linked data - param_chan.append(node) - elif channel == 1: - # First channel can have other data channel - param_chan.append(node) - + try: + for node, meta_chan in metadata.items(): + # Is this attribute ignored? + if node in HM_IGNORE_DISCOVERY_NODE: + continue + if meta_chan == 'c' or meta_chan is None: + # Only channel linked data + param_chan.append(node) + elif channel == 1: + # First channel can have other data channel + param_chan.append(node) + # pylint: disable=broad-except + except Exception as err: + _LOGGER.error("Exception generating %s (%s): %s", + hmdevice.ADDRESS, str(metadata), str(err)) # Default parameter - if len(param_chan) == 0: + if not param_chan: param_chan.append(None) # Add to channel params.update({channel: param_chan}) From 30b7c6b6943cb338fbd1c5f298c6f12944e3a4c2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 18:34:35 +0200 Subject: [PATCH 40/79] Second batch of (minor) fixes as suggested by @balloob --- homeassistant/components/homematic.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 317397c0b9cf54..32ac7ac80eb9c0 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,6 +15,7 @@ """ import time import logging +from functools import partial from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ EVENT_PLATFORM_DISCOVERED, \ ATTR_SERVICE, \ @@ -29,7 +30,6 @@ HOMEMATIC = None HOMEMATIC_DEVICES = {} -HOMEMATIC_AUTODETECT = False DISCOVER_SWITCHES = "homematic.switch" DISCOVER_LIGHTS = "homematic.light" @@ -69,14 +69,13 @@ "VOLTAGE": ["Voltage", {}] } -_HM_DISCOVER_HASS = None _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup(hass, config): """Setup the Homematic component.""" - global HOMEMATIC, HOMEMATIC_AUTODETECT, _HM_DISCOVER_HASS + global HOMEMATIC from pyhomematic import HMConnection @@ -84,20 +83,18 @@ def setup(hass, config): local_port = config[DOMAIN].get("local_port", 8943) remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) - autodetect = config[DOMAIN].get("autodetect", False) if remote_ip is None or local_ip is None: _LOGGER.error("Missing remote CCU/Homegear or local address") return False # Create server thread - HOMEMATIC_AUTODETECT = autodetect - _HM_DISCOVER_HASS = hass + bound_system_callback = partial(system_callback_handler, hass, config) HOMEMATIC = HMConnection(local=local_ip, localport=local_port, remote=remote_ip, remoteport=remote_port, - systemcallback=system_callback_handler, + systemcallback=bound_system_callback, interface_id="homeassistant") # Start server thread, connect to peer, initialize to receive events @@ -111,8 +108,9 @@ def setup(hass, config): # pylint: disable=too-many-branches -def system_callback_handler(src, *args): +def system_callback_handler(hass, config, src, *args): """Callback handler.""" + delay = config[DOMAIN].get("delay", 0.5) if src == 'newDevices': # pylint: disable=unused-variable (interface_id, dev_descriptions) = args @@ -126,13 +124,14 @@ def system_callback_handler(src, *args): for dev in key_dict: if dev in HOMEMATIC_DEVICES: for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() + hm_element.link_homematic(delay=delay) else: devices_not_created.append(dev) # If configuration allows autodetection of devices, # all devices not configured are added. - if HOMEMATIC_AUTODETECT and devices_not_created: + autodetect = config[DOMAIN].get("autodetect", False) + if autodetect and devices_not_created: for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), ('light', DISCOVER_LIGHTS), @@ -152,12 +151,12 @@ def system_callback_handler(src, *args): # Ensure component is loaded homeassistant.bootstrap.setup_component( - _HM_DISCOVER_HASS, + hass, component.DOMAIN, config) # Fire discovery event - _HM_DISCOVER_HASS.bus.fire( + hass.bus.fire( EVENT_PLATFORM_DISCOVERED, { ATTR_SERVICE: discovery_type, ATTR_DISCOVERED: { @@ -171,7 +170,7 @@ def system_callback_handler(src, *args): for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic(delay=1) + hm_element.link_homematic(delay=delay) def _get_devices(device_type, keys): @@ -368,7 +367,7 @@ def device_state_attributes(self): return attr - def link_homematic(self, delay=0): + def link_homematic(self, delay=0.5): """Connect to homematic.""" # Does a HMDevice from pyhomematic exist? if self._address in HOMEMATIC.devices: From a19f7bff28d47975b0ae50318190362fdea25120 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 18:36:52 +0200 Subject: [PATCH 41/79] fix false autodetect with HM GongSensor types --- homeassistant/components/homematic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 32ac7ac80eb9c0..84a3ae33c52143 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -46,10 +46,11 @@ DISCOVER_LIGHTS: ["Dimmer"], DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", "RemoteMotion", "ThermostatWall", "AreaThermostat", - "RotaryHandleSensor", "GongSensor"], + "RotaryHandleSensor"], DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", - "Motion", "MotionV2", "RemoteMotion"], + "Motion", "MotionV2", "RemoteMotion", + "GongSensor"], DISCOVER_ROLLERSHUTTER: ["Blind"] } From b3acd7d21d43f7b201b5d041ba9f3a0b5347ffa8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 18:54:14 +0200 Subject: [PATCH 42/79] add resolvenames function support from pyhomematic (homegear only) --- homeassistant/components/homematic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 84a3ae33c52143..6662c6bbe0df4f 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -84,6 +84,7 @@ def setup(hass, config): local_port = config[DOMAIN].get("local_port", 8943) remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) + resolvenames = config[DOMAIN].get("resolvenames", False) if remote_ip is None or local_ip is None: _LOGGER.error("Missing remote CCU/Homegear or local address") @@ -96,6 +97,7 @@ def setup(hass, config): remote=remote_ip, remoteport=remote_port, systemcallback=bound_system_callback, + resolvenames=resolvenames, interface_id="homeassistant") # Start server thread, connect to peer, initialize to receive events From 87c138c5593ae7c9c7e4b1a20858c96d5a9f6b82 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 19:25:59 +0200 Subject: [PATCH 43/79] Third batch of (minor) fixes as suggested by @balloob --- homeassistant/components/homematic.py | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 6662c6bbe0df4f..040a15a368e1b2 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -22,6 +22,7 @@ ATTR_DISCOVERED, \ STATE_UNKNOWN from homeassistant.loader import get_component +from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -150,25 +151,11 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: component = get_component(component_name) - config = {component.DOMAIN: found_devices} - - # Ensure component is loaded - homeassistant.bootstrap.setup_component( - hass, - component.DOMAIN, - config) - - # Fire discovery event - hass.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' - } - } - ) + + # HA discovery event + discovery.load_platform(hass, component, DOMAIN, { + ATTR_DISCOVER_DEVICES: found_devices + }, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From 86ccf26a1a41a73ecb5d4514d66f4aacce4bf349 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 20:12:49 +0200 Subject: [PATCH 44/79] fix autodiscovery --- homeassistant/components/binary_sensor/homematic.py | 2 ++ homeassistant/components/homematic.py | 5 +---- homeassistant/components/light/homematic.py | 2 ++ homeassistant/components/rollershutter/homematic.py | 2 ++ homeassistant/components/sensor/homematic.py | 2 ++ homeassistant/components/switch/homematic.py | 2 ++ homeassistant/components/thermostat/homematic.py | 2 ++ 7 files changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index d2005f99ba54a9..acbc2eafe69a87 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,6 +55,8 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMBinarySensor, config, add_callback_devices) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 040a15a368e1b2..3bc23bbdb71f73 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -21,7 +21,6 @@ ATTR_SERVICE, \ ATTR_DISCOVERED, \ STATE_UNKNOWN -from homeassistant.loader import get_component from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -150,10 +149,8 @@ def system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: - component = get_component(component_name) - # HA discovery event - discovery.load_platform(hass, component, DOMAIN, { + discovery.load_platform(hass, component_name, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices }, config) diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 94dabb0f00ab90..6ccc2f636ba256 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,6 +29,8 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMLight, config, add_callback_devices) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index e0dd5e5469fc42..55a86be0bf652a 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,6 +29,8 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMRollershutter, config, add_callback_devices) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 52ece78f59e493..c07faedbf5bd02 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,6 +41,8 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSensor, config, add_callback_devices) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index 5a630f43022f0a..ca639b95ecbb51 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,6 +28,8 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSwitch, config, add_callback_devices) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index a1ed06bc4bd300..d98b674c692fc9 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,6 +28,8 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + config = discovery_info return homematic.setup_hmdevice_entity_helper(HMThermostat, config, add_callback_devices) From be72b048551a2a7b86c8ccb8f15bc55c1081eac2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 20:30:02 +0200 Subject: [PATCH 45/79] fix discovery function --- homeassistant/components/homematic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 3bc23bbdb71f73..c42058a04240fd 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -150,9 +150,9 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: # HA discovery event - discovery.load_platform(hass, component_name, DOMAIN, { + discovery.load_platform(hass, discovery_type, { ATTR_DISCOVER_DEVICES: found_devices - }, config) + }, component_name, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From 21381a95d41a34f7c1ba6aff1cc2902c5aa00e75 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sat, 25 Jun 2016 20:35:36 +0200 Subject: [PATCH 46/79] Zwave fixes. (#2373) * Fix move_up and move_down I managed to switch up the zwave move_up and move_down commands. This PR fixes it. Thank you @nunofgs for bringing this to my attention :) * Fix for aeotec 6 multisensor --- homeassistant/components/rollershutter/zwave.py | 4 ++-- homeassistant/components/zwave.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rollershutter/zwave.py b/homeassistant/components/rollershutter/zwave.py index ea0be0ddf74f7c..81b891d7bf1921 100644 --- a/homeassistant/components/rollershutter/zwave.py +++ b/homeassistant/components/rollershutter/zwave.py @@ -60,11 +60,11 @@ def current_position(self): def move_up(self, **kwargs): """Move the roller shutter up.""" - self._node.set_dimmer(self._value.value_id, 0) + self._node.set_dimmer(self._value.value_id, 100) def move_down(self, **kwargs): """Move the roller shutter down.""" - self._node.set_dimmer(self._value.value_id, 100) + self._node.set_dimmer(self._value.value_id, 0) def stop(self, **kwargs): """Stop the roller shutter.""" diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 98a24240a00eeb..84ec3dfd8470c6 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -108,7 +108,8 @@ TYPE_BOOL, GENRE_USER), ('binary_sensor', - [GENERIC_COMMAND_CLASS_BINARY_SENSOR], + [GENERIC_COMMAND_CLASS_BINARY_SENSOR, + GENERIC_COMMAND_CLASS_MULTILEVEL_SENSOR], [SPECIFIC_DEVICE_CLASS_WHATEVER], [COMMAND_CLASS_SENSOR_BINARY], TYPE_BOOL, From 57754cd2ff8b9e05b1c0c593973c74ad1bdbfd8f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:03:33 +0200 Subject: [PATCH 47/79] Revert "fix discovery function" This reverts commit be72b048551a2a7b86c8ccb8f15bc55c1081eac2. --- homeassistant/components/homematic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index c42058a04240fd..3bc23bbdb71f73 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -150,9 +150,9 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: # HA discovery event - discovery.load_platform(hass, discovery_type, { + discovery.load_platform(hass, component_name, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices - }, component_name, config) + }, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From 199fbc7a15275b8a449ab8fd737cdd6bef0a3fed Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:03:37 +0200 Subject: [PATCH 48/79] Revert "fix autodiscovery" This reverts commit 86ccf26a1a41a73ecb5d4514d66f4aacce4bf349. --- homeassistant/components/binary_sensor/homematic.py | 2 -- homeassistant/components/homematic.py | 5 ++++- homeassistant/components/light/homematic.py | 2 -- homeassistant/components/rollershutter/homematic.py | 2 -- homeassistant/components/sensor/homematic.py | 2 -- homeassistant/components/switch/homematic.py | 2 -- homeassistant/components/thermostat/homematic.py | 2 -- 7 files changed, 4 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index acbc2eafe69a87..d2005f99ba54a9 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,8 +55,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMBinarySensor, config, add_callback_devices) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 3bc23bbdb71f73..040a15a368e1b2 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -21,6 +21,7 @@ ATTR_SERVICE, \ ATTR_DISCOVERED, \ STATE_UNKNOWN +from homeassistant.loader import get_component from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -149,8 +150,10 @@ def system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: + component = get_component(component_name) + # HA discovery event - discovery.load_platform(hass, component_name, DOMAIN, { + discovery.load_platform(hass, component, DOMAIN, { ATTR_DISCOVER_DEVICES: found_devices }, config) diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 6ccc2f636ba256..94dabb0f00ab90 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,8 +29,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMLight, config, add_callback_devices) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index 55a86be0bf652a..e0dd5e5469fc42 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,8 +29,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMRollershutter, config, add_callback_devices) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index c07faedbf5bd02..52ece78f59e493 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,8 +41,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSensor, config, add_callback_devices) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index ca639b95ecbb51..5a630f43022f0a 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,8 +28,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMSwitch, config, add_callback_devices) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index d98b674c692fc9..a1ed06bc4bd300 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,8 +28,6 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - config = discovery_info return homematic.setup_hmdevice_entity_helper(HMThermostat, config, add_callback_devices) From a687bdb388ff03cbe5e253ea0802c06358f8f92d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:03:41 +0200 Subject: [PATCH 49/79] Revert "Third batch of (minor) fixes as suggested by @balloob" This reverts commit 87c138c5593ae7c9c7e4b1a20858c96d5a9f6b82. --- homeassistant/components/homematic.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 040a15a368e1b2..6662c6bbe0df4f 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -22,7 +22,6 @@ ATTR_DISCOVERED, \ STATE_UNKNOWN from homeassistant.loader import get_component -from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -151,11 +150,25 @@ def system_callback_handler(hass, config, src, *args): # they are setup in HA and an event is fired if found_devices: component = get_component(component_name) - - # HA discovery event - discovery.load_platform(hass, component, DOMAIN, { - ATTR_DISCOVER_DEVICES: found_devices - }, config) + config = {component.DOMAIN: found_devices} + + # Ensure component is loaded + homeassistant.bootstrap.setup_component( + hass, + component.DOMAIN, + config) + + # Fire discovery event + hass.bus.fire( + EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: { + ATTR_DISCOVER_DEVICES: + found_devices, + ATTR_DISCOVER_CONFIG: '' + } + } + ) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: From e0e9d3c57b6e61525b026b1f504b85a6a3de5fd4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 21:37:51 +0200 Subject: [PATCH 50/79] change autodiscovery --- .../components/binary_sensor/homematic.py | 5 +++ homeassistant/components/homematic.py | 37 ++++++++----------- homeassistant/components/light/homematic.py | 5 +++ .../components/rollershutter/homematic.py | 5 +++ homeassistant/components/sensor/homematic.py | 5 +++ homeassistant/components/switch/homematic.py | 5 +++ .../components/thermostat/homematic.py | 5 +++ 7 files changed, 45 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index d2005f99ba54a9..08ea209944548f 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,6 +55,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMBinarySensor, config, add_callback_devices) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 6662c6bbe0df4f..5c23462e98d217 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -17,11 +17,9 @@ import logging from functools import partial from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ - EVENT_PLATFORM_DISCOVERED, \ - ATTR_SERVICE, \ ATTR_DISCOVERED, \ STATE_UNKNOWN -from homeassistant.loader import get_component +from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity import homeassistant.bootstrap @@ -149,26 +147,10 @@ def system_callback_handler(hass, config, src, *args): # When devices of this type are found # they are setup in HA and an event is fired if found_devices: - component = get_component(component_name) - config = {component.DOMAIN: found_devices} - - # Ensure component is loaded - homeassistant.bootstrap.setup_component( - hass, - component.DOMAIN, - config) - # Fire discovery event - hass.bus.fire( - EVENT_PLATFORM_DISCOVERED, { - ATTR_SERVICE: discovery_type, - ATTR_DISCOVERED: { - ATTR_DISCOVER_DEVICES: - found_devices, - ATTR_DISCOVER_CONFIG: '' - } - } - ) + discovery.load_platform(hass, component_name, DOMAIN, { + ATTR_DISCOVER_DEVICES: found_devices + }, config) for dev in devices_not_created: if dev in HOMEMATIC_DEVICES: @@ -282,6 +264,17 @@ def _create_ha_name(name, channel, param): return "{} {} {}".format(name, channel, param) +def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, + add_callback_devices): + """Helper to setup Homematic devices with discovery info.""" + for config in discovery_info["devices"]: + ret = setup_hmdevice_entity_helper(hmdevicetype, config, + add_callback_devices) + if not ret: + _LOGGER.error("Setup discovery error with config %s", str(config)) + return True + + def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): """Helper to setup Homematic devices.""" if HOMEMATIC is None: diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 94dabb0f00ab90..159f3e4dbdcd2b 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,6 +29,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMLight, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMLight, config, add_callback_devices) diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index e0dd5e5469fc42..737a7eb017ddc0 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,6 +29,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMRollershutter, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMRollershutter, config, add_callback_devices) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 52ece78f59e493..f6f3825199b687 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,6 +41,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMSensor, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMSensor, config, add_callback_devices) diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index 5a630f43022f0a..16cc63a6708ad9 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,6 +28,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMSwitch, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMSwitch, config, add_callback_devices) diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index a1ed06bc4bd300..e654379d56e797 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,6 +28,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" + if discovery_info: + return homematic.setup_hmdevice_discovery_helper(HMThermostat, + discovery_info, + add_callback_devices) + # Manual return homematic.setup_hmdevice_entity_helper(HMThermostat, config, add_callback_devices) From 4ecd7245784d0f6717b4249eef3eaf5d4464848b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 22:10:47 +0200 Subject: [PATCH 51/79] fix linter errors --- homeassistant/components/homematic.py | 3 +-- homeassistant/components/thermostat/homematic.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 5c23462e98d217..749f372596b06a 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -17,11 +17,10 @@ import logging from functools import partial from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ - ATTR_DISCOVERED, \ + ATTR_DISCOVER_DEVICES, \ STATE_UNKNOWN from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -import homeassistant.bootstrap DOMAIN = 'homematic' REQUIREMENTS = ['pyhomematic==0.1.6'] diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index e654379d56e797..d7675a5cd472c3 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -38,6 +38,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): add_callback_devices) +# pylint: disable=abstract-method class HMThermostat(homematic.HMDevice, ThermostatDevice): """Represents a Homematic Thermostat in Home Assistant.""" From f3199e7daeb083a196d945875531a2013a61b83f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 22:13:29 +0200 Subject: [PATCH 52/79] fix wrong import --- homeassistant/components/homematic.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 749f372596b06a..ec092baf80b277 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -16,9 +16,7 @@ import time import logging from functools import partial -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ - ATTR_DISCOVER_DEVICES, \ - STATE_UNKNOWN +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity From c3b25f2cd5997139150e14d4545f9dad5768611e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 25 Jun 2016 22:20:09 +0200 Subject: [PATCH 53/79] fix logging-not-lazy --- homeassistant/components/homematic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index ec092baf80b277..c2c6c000fa262a 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -389,8 +389,8 @@ def link_homematic(self, delay=0.5): except Exception as err: self._connected = False self._available = False - _LOGGER.error("Exception while linking %s: %s" % - (self._address, str(err))) + _LOGGER.error("Exception while linking %s: %s", + self._address, str(err)) else: _LOGGER.critical("Delink %s object from HM!", self._name) self._connected = False From 206e7d7a678fe4ecd1dd18497992af251cf1d78d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 25 Jun 2016 16:40:33 -0700 Subject: [PATCH 54/79] Extend persistent notification support (#2371) --- homeassistant/bootstrap.py | 5 +- homeassistant/components/demo.py | 6 ++ .../components/persistent_notification.py | 70 +++++++++++++++++-- .../test_persistent_notification.py | 65 +++++++++++++++++ 4 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 tests/components/test_persistent_notification.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 754d4f4f5aafd3..ff7e73a00f1d06 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.components as core_components -import homeassistant.components.group as group +from homeassistant.components import group, persistent_notification import homeassistant.config as config_util import homeassistant.core as core import homeassistant.helpers.config_validation as cv @@ -262,9 +262,10 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, if not core_components.setup(hass, config): _LOGGER.error('Home Assistant core failed to initialize. ' 'Further initialization aborted.') - return hass + persistent_notification.setup(hass, config) + _LOGGER.info('Home Assistant core initialized') # Give event decorators access to HASS diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 148c57a12c3453..f083a96f5b2bd0 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -37,6 +37,7 @@ def setup(hass, config): """Setup a demo environment.""" group = loader.get_component('group') configurator = loader.get_component('configurator') + persistent_notification = loader.get_component('persistent_notification') config.setdefault(ha.DOMAIN, {}) config.setdefault(DOMAIN, {}) @@ -59,6 +60,11 @@ def setup(hass, config): demo_config[component] = {CONF_PLATFORM: 'demo'} bootstrap.setup_component(hass, component, demo_config) + # Setup example persistent notification + persistent_notification.create( + hass, 'This is an example of a persistent notification.', + title='Example Notification') + # Setup room groups lights = sorted(hass.states.entity_ids('light')) switches = sorted(hass.states.entity_ids('switch')) diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index 6c784eaf5ca771..66a634616faaff 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -4,15 +4,77 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/persistent_notification/ """ +import logging -DOMAIN = "persistent_notification" +import voluptuous as vol +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template, config_validation as cv +from homeassistant.helpers.entity import generate_entity_id +from homeassistant.util import slugify -def create(hass, entity, msg): - """Create a state for an error.""" - hass.states.set('{}.{}'.format(DOMAIN, entity), msg) +DOMAIN = 'persistent_notification' +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +SERVICE_CREATE = 'create' +ATTR_TITLE = 'title' +ATTR_MESSAGE = 'message' +ATTR_NOTIFICATION_ID = 'notification_id' + +SCHEMA_SERVICE_CREATE = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.template, + vol.Optional(ATTR_TITLE): cv.template, + vol.Optional(ATTR_NOTIFICATION_ID): cv.string, +}) + + +DEFAULT_OBJECT_ID = 'notification' +_LOGGER = logging.getLogger(__name__) + + +def create(hass, message, title=None, notification_id=None): + """Turn all or specified light off.""" + data = { + key: value for key, value in [ + (ATTR_TITLE, title), + (ATTR_MESSAGE, message), + (ATTR_NOTIFICATION_ID, notification_id), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_CREATE, data) def setup(hass, config): """Setup the persistent notification component.""" + def create_service(call): + """Handle a create notification service call.""" + title = call.data.get(ATTR_TITLE) + message = call.data.get(ATTR_MESSAGE) + notification_id = call.data.get(ATTR_NOTIFICATION_ID) + + if notification_id is not None: + entity_id = ENTITY_ID_FORMAT.format(slugify(notification_id)) + else: + entity_id = generate_entity_id(ENTITY_ID_FORMAT, DEFAULT_OBJECT_ID, + hass=hass) + attr = {} + if title is not None: + try: + title = template.render(hass, title) + except TemplateError as ex: + _LOGGER.error('Error rendering title %s: %s', title, ex) + + attr[ATTR_TITLE] = title + + try: + message = template.render(hass, message) + except TemplateError as ex: + _LOGGER.error('Error rendering message %s: %s', message, ex) + + hass.states.set(entity_id, message, attr) + + hass.services.register(DOMAIN, SERVICE_CREATE, create_service, {}, + SCHEMA_SERVICE_CREATE) + return True diff --git a/tests/components/test_persistent_notification.py b/tests/components/test_persistent_notification.py new file mode 100644 index 00000000000000..6f6d8b8e1b036b --- /dev/null +++ b/tests/components/test_persistent_notification.py @@ -0,0 +1,65 @@ +"""The tests for the persistent notification component.""" +import homeassistant.components.persistent_notification as pn + +from tests.common import get_test_home_assistant + + +class TestPersistentNotification: + """Test persistent notification component.""" + + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + pn.setup(self.hass, {}) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_create(self): + """Test creating notification without title or notification id.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, 'Hello World {{ 1 + 1 }}', + title='{{ 1 + 1 }} beers') + self.hass.pool.block_till_done() + + entity_ids = self.hass.states.entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + + state = self.hass.states.get(entity_ids[0]) + assert state.state == 'Hello World 2' + assert state.attributes.get('title') == '2 beers' + + def test_create_notification_id(self): + """Ensure overwrites existing notification with same id.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, 'test', notification_id='Beer 2') + self.hass.pool.block_till_done() + + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('persistent_notification.beer_2') + assert state.state == 'test' + + pn.create(self.hass, 'test 2', notification_id='Beer 2') + self.hass.pool.block_till_done() + + # We should have overwritten old one + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('persistent_notification.beer_2') + assert state.state == 'test 2' + + def test_create_template_error(self): + """Ensure we output templates if contain error.""" + assert len(self.hass.states.entity_ids(pn.DOMAIN)) == 0 + + pn.create(self.hass, '{{ message + 1 }}', '{{ title + 1 }}') + self.hass.pool.block_till_done() + + entity_ids = self.hass.states.entity_ids(pn.DOMAIN) + assert len(entity_ids) == 1 + + state = self.hass.states.get(entity_ids[0]) + assert state.state == '{{ message + 1 }}' + assert state.attributes.get('title') == '{{ title + 1 }}' From d13cc227cc769c444e46900c1d12185e84f88b84 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Sun, 26 Jun 2016 01:33:23 -0600 Subject: [PATCH 55/79] Push State (#2365) * Add ability to push state changes * Add tests for push state changes * Fix style issues * Use better name to force an update --- homeassistant/components/api.py | 3 ++- homeassistant/core.py | 5 +++-- homeassistant/helpers/entity.py | 12 +++++++++++- homeassistant/remote.py | 9 +++++---- tests/components/test_api.py | 21 +++++++++++++++++++++ tests/test_core.py | 14 ++++++++++++++ tests/test_remote.py | 17 ++++++++++++++++- 7 files changed, 72 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index ad8f21f069bd60..b538a62d0082aa 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -204,11 +204,12 @@ def post(self, request, entity_id): return self.json_message('No state specified', HTTP_BAD_REQUEST) attributes = request.json.get('attributes') + force_update = request.json.get('force_update', False) is_new_state = self.hass.states.get(entity_id) is None # Write state - self.hass.states.set(entity_id, new_state, attributes) + self.hass.states.set(entity_id, new_state, attributes, force_update) # Read the state back for our response resp = self.json(self.hass.states.get(entity_id)) diff --git a/homeassistant/core.py b/homeassistant/core.py index ffaccdeae43ff8..d3eed6ce5e03ca 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -456,7 +456,7 @@ def remove(self, entity_id): return True - def set(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None, force_update=False): """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -472,7 +472,8 @@ def set(self, entity_id, new_state, attributes=None): old_state = self._states.get(entity_id) is_existing = old_state is not None - same_state = is_existing and old_state.state == new_state + same_state = (is_existing and old_state.state == new_state and + not force_update) same_attr = is_existing and old_state.attributes == attributes if same_state and same_attr: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e4ccf11e1683c5..d120a3b2cf6406 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -125,6 +125,15 @@ def assumed_state(self): """Return True if unable to access real state of the entity.""" return False + @property + def force_update(self): + """Return True if state updates should be forced. + + If True, a state change will be triggered anytime the state property is + updated, not just when the value changes. + """ + return False + def update(self): """Retrieve latest state.""" pass @@ -190,7 +199,8 @@ def update_ha_state(self, force_refresh=False): state, attr[ATTR_UNIT_OF_MEASUREMENT]) state = str(state) - return self.hass.states.set(self.entity_id, state, attr) + return self.hass.states.set( + self.entity_id, state, attr, self.force_update) def _attr_setter(self, name, typ, attr, attrs): """Helper method to populate attributes based on properties.""" diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 4bfb01890cf962..b2dfc3ae18f1a6 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -259,9 +259,9 @@ def remove(self, entity_id): """ return remove_state(self._api, entity_id) - def set(self, entity_id, new_state, attributes=None): + def set(self, entity_id, new_state, attributes=None, force_update=False): """Call set_state on remote API.""" - set_state(self._api, entity_id, new_state, attributes) + set_state(self._api, entity_id, new_state, attributes, force_update) def mirror(self): """Discard current data and mirrors the remote state machine.""" @@ -450,7 +450,7 @@ def remove_state(api, entity_id): return False -def set_state(api, entity_id, new_state, attributes=None): +def set_state(api, entity_id, new_state, attributes=None, force_update=False): """Tell API to update state for entity_id. Return True if success. @@ -458,7 +458,8 @@ def set_state(api, entity_id, new_state, attributes=None): attributes = attributes or {} data = {'state': new_state, - 'attributes': attributes} + 'attributes': attributes, + 'force_update': force_update} try: req = api(METHOD_POST, diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 60ff19d4a4322e..8d1ee1c4ad52b0 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -136,6 +136,27 @@ def test_api_state_change_with_bad_data(self): self.assertEqual(400, req.status_code) + # pylint: disable=invalid-name + def test_api_state_change_push(self): + """Test if we can push a change the state of an entity.""" + hass.states.set("test.test", "not_to_be_set") + + events = [] + hass.bus.listen(const.EVENT_STATE_CHANGED, events.append) + + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "not_to_be_set"}), + headers=HA_HEADERS) + hass.bus._pool.block_till_done() + self.assertEqual(0, len(events)) + + requests.post(_url(const.URL_API_STATES_ENTITY.format("test.test")), + data=json.dumps({"state": "not_to_be_set", + "force_update": True}), + headers=HA_HEADERS) + hass.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + # pylint: disable=invalid-name def test_api_fire_event_with_no_data(self): """Test if the API allows us to fire an event.""" diff --git a/tests/test_core.py b/tests/test_core.py index 4930bcef6ed1b9..cb698cdc53cd7b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -334,6 +334,20 @@ def test_last_changed_not_updated_on_same_state(self): self.assertEqual(state.last_changed, self.states.get('light.Bowl').last_changed) + def test_force_update(self): + """Test force update option.""" + self.pool.add_worker() + events = [] + self.bus.listen(EVENT_STATE_CHANGED, events.append) + + self.states.set('light.bowl', 'on') + self.bus._pool.block_till_done() + self.assertEqual(0, len(events)) + + self.states.set('light.bowl', 'on', None, True) + self.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + class TestServiceCall(unittest.TestCase): """Test ServiceCall class.""" diff --git a/tests/test_remote.py b/tests/test_remote.py index 58b2f9b359d9d7..893f02bea31869 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -8,7 +8,7 @@ import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote import homeassistant.components.http as http -from homeassistant.const import HTTP_HEADER_HA_AUTH +from homeassistant.const import HTTP_HEADER_HA_AUTH, EVENT_STATE_CHANGED import homeassistant.util.dt as dt_util from tests.common import get_test_instance_port, get_test_home_assistant @@ -155,6 +155,21 @@ def test_set_state(self): self.assertFalse(remote.set_state(broken_api, 'test.test', 'set_test')) + def test_set_state_with_push(self): + """TestPython API set_state with push option.""" + events = [] + hass.bus.listen(EVENT_STATE_CHANGED, events.append) + + remote.set_state(master_api, 'test.test', 'set_test_2') + remote.set_state(master_api, 'test.test', 'set_test_2') + hass.bus._pool.block_till_done() + self.assertEqual(1, len(events)) + + remote.set_state( + master_api, 'test.test', 'set_test_2', force_update=True) + hass.bus._pool.block_till_done() + self.assertEqual(2, len(events)) + def test_is_state(self): """Test Python API is_state.""" self.assertTrue( From 254b1c46ac9381415dd5e1699954b59ab7f2ae62 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 26 Jun 2016 19:13:52 +0200 Subject: [PATCH 56/79] Remove lxml dependency (#2374) --- homeassistant/components/sensor/swiss_hydrological_data.py | 4 ++-- requirements_all.txt | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/swiss_hydrological_data.py b/homeassistant/components/sensor/swiss_hydrological_data.py index ddc31bb56ec9c8..2589bd449552af 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['beautifulsoup4==4.4.1', 'lxml==3.6.0'] +REQUIREMENTS = ['beautifulsoup4==4.4.1'] _LOGGER = logging.getLogger(__name__) _RESOURCE = 'http://www.hydrodaten.admin.ch/en/' @@ -148,7 +148,7 @@ def update(self): try: tables = BeautifulSoup(response.content, - 'lxml').findChildren('table') + 'html.parser').findChildren('table') rows = tables[0].findChildren(['th', 'tr']) details = [] diff --git a/requirements_all.txt b/requirements_all.txt index 62a87fcc368f61..a131813edbd009 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -180,9 +180,6 @@ lightify==1.0.3 # homeassistant.components.light.limitlessled limitlessled==1.0.0 -# homeassistant.components.sensor.swiss_hydrological_data -lxml==3.6.0 - # homeassistant.components.notify.message_bird messagebird==1.2.0 From fb3e388f0441240fe207274f0167f01e034ba4b6 Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 26 Jun 2016 14:49:46 -0400 Subject: [PATCH 57/79] Depreciate ssl2/3 (#2375) * Depreciate ssl2/3 Following the best practices as defind here: https://mozilla.github.io/server-side-tls/ssl-config-generator/ * Updated comment with better decription Links to the rational rather than the config generator; explains link. * add comment mentioning intermediate --- homeassistant/components/http.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index d7ce8e78013ea4..1f77aac5ad4fc6 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -10,6 +10,7 @@ import mimetypes import threading import re +import ssl import voluptuous as vol import homeassistant.core as ha @@ -36,6 +37,24 @@ DATA_API_PASSWORD = 'api_password' +# TLS configuation follows the best-practice guidelines +# specified here: https://wiki.mozilla.org/Security/Server_Side_TLS +# Intermediate guidelines are followed. +SSL_VERSION = ssl.PROTOCOL_TLSv1 +CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ + "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ + "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:" \ + "ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:" \ + "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:" \ + "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:" \ + "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:" \ + "DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:" \ + "DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:" \ + "ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:" \ + "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:" \ + "AES256-SHA:DES-CBC3-SHA:!DSS" + _FINGERPRINT = re.compile(r'^(.+)-[a-z0-9]{32}\.(\w+)$', re.IGNORECASE) _LOGGER = logging.getLogger(__name__) @@ -294,7 +313,8 @@ def start(self): sock = eventlet.listen((self.server_host, self.server_port)) if self.ssl_certificate: sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, - keyfile=self.ssl_key, server_side=True) + keyfile=self.ssl_key, server_side=True, + ssl_version=SSL_VERSION, ciphers=CIPHERS) wsgi.server(sock, self, log=_LOGGER) def dispatch_request(self, request): From 3afc566be11ec3d715415a5d4ff11271c5bdc234 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 26 Jun 2016 23:18:18 +0200 Subject: [PATCH 58/79] Fix timing bug while linking HM device to HA object https://github.com/danielperna84/home-assistant/issues/14 --- homeassistant/components/homematic.py | 39 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index c2c6c000fa262a..7b3e265a9ddd43 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -24,6 +24,7 @@ REQUIREMENTS = ['pyhomematic==0.1.6'] HOMEMATIC = None +HOMEMATIC_LINK_DELAY = 0.5 HOMEMATIC_DEVICES = {} DISCOVER_SWITCHES = "homematic.switch" @@ -71,7 +72,7 @@ # pylint: disable=unused-argument def setup(hass, config): """Setup the Homematic component.""" - global HOMEMATIC + global HOMEMATIC, HOMEMATIC_LINK_DELAY from pyhomematic import HMConnection @@ -80,6 +81,7 @@ def setup(hass, config): remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) resolvenames = config[DOMAIN].get("resolvenames", False) + HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5) if remote_ip is None or local_ip is None: _LOGGER.error("Missing remote CCU/Homegear or local address") @@ -108,27 +110,30 @@ def setup(hass, config): # pylint: disable=too-many-branches def system_callback_handler(hass, config, src, *args): """Callback handler.""" - delay = config[DOMAIN].get("delay", 0.5) if src == 'newDevices': + _LOGGER.debug("newDevices with: %s", str(args)) # pylint: disable=unused-variable (interface_id, dev_descriptions) = args key_dict = {} # Get list of all keys of the devices (ignoring channels) for dev in dev_descriptions: key_dict[dev['ADDRESS'].split(':')[0]] = True + # Connect devices already created in HA to pyhomematic and # add remaining devices to list devices_not_created = [] for dev in key_dict: if dev in HOMEMATIC_DEVICES: for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic(delay=delay) + hm_element.link_homematic() else: devices_not_created.append(dev) # If configuration allows autodetection of devices, # all devices not configured are added. autodetect = config[DOMAIN].get("autodetect", False) + _LOGGER.debug("Autodetect is %s / unknown device: %s", str(autodetect), + str(devices_not_created)) if autodetect and devices_not_created: for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), @@ -149,11 +154,6 @@ def system_callback_handler(hass, config, src, *args): ATTR_DISCOVER_DEVICES: found_devices }, config) - for dev in devices_not_created: - if dev in HOMEMATIC_DEVICES: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic(delay=delay) - def _get_devices(device_type, keys): """Get devices.""" @@ -269,6 +269,7 @@ def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, add_callback_devices) if not ret: _LOGGER.error("Setup discovery error with config %s", str(config)) + return True @@ -284,6 +285,8 @@ def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): "'address' missing in configuration.", address) return False + _LOGGER.debug("Add device %s from config: %s", + str(hmdevicetype), str(config)) # Create a new HA homematic object new_device = hmdevicetype(config) if address not in HOMEMATIC_DEVICES: @@ -292,6 +295,10 @@ def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): # Add to HA add_callback_devices([new_device]) + + # HM is connected + if address in HOMEMATIC.devices: + return new_device.link_homematic() return True @@ -360,8 +367,12 @@ def device_state_attributes(self): return attr - def link_homematic(self, delay=0.5): + def link_homematic(self): """Connect to homematic.""" + # device is already linked + if self._connected: + return True + # Does a HMDevice from pyhomematic exist? if self._address in HOMEMATIC.devices: # Init @@ -374,10 +385,10 @@ def link_homematic(self, delay=0.5): try: # Init datapoints of this object self._init_data_struct() - if delay: + if HOMEMATIC_LINK_DELAY: # We delay / pause loading of data to avoid overloading # of CCU / Homegear when doing auto detection - time.sleep(delay) + time.sleep(HOMEMATIC_LINK_DELAY) self._load_init_data_from_hm() _LOGGER.debug("%s datastruct: %s", self._name, str(self._data)) @@ -388,23 +399,21 @@ def link_homematic(self, delay=0.5): # pylint: disable=broad-except except Exception as err: self._connected = False - self._available = False _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) else: _LOGGER.critical("Delink %s object from HM!", self._name) self._connected = False - self._available = False # Update HA - _LOGGER.debug("%s linking down, send update_ha_state", self._name) + _LOGGER.debug("%s linking done, send update_ha_state", self._name) self.update_ha_state() else: _LOGGER.debug("%s not found in HOMEMATIC.devices", self._address) def _hm_event_callback(self, device, caller, attribute, value): """Handle all pyhomematic device events.""" - _LOGGER.debug("%s receive event '%s' value: %s", self._name, + _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) have_change = False From dc75b28b90c1c5d5b1decf58d7121bd8226be6aa Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Mon, 27 Jun 2016 12:01:41 -0400 Subject: [PATCH 59/79] Initial Support for Zwave color bulbs (#2376) * Initial Support for Zwave color bulbs * Revert name override for ZwaveColorLight --- homeassistant/components/light/zwave.py | 210 +++++++++++++++++++++++- homeassistant/components/zwave.py | 1 + 2 files changed, 206 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index b4aaf5e2b4f25b..7c9cb72db26e56 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -4,12 +4,31 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.zwave/ """ +import logging + # Because we do not compile openzwave on CI # pylint: disable=import-error from threading import Timer -from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ + ATTR_RGB_COLOR, DOMAIN, Light from homeassistant.components import zwave from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \ + color_temperature_mired_to_kelvin, color_temperature_to_rgb + +_LOGGER = logging.getLogger(__name__) + +COLOR_CHANNEL_WARM_WHITE = 0x01 +COLOR_CHANNEL_COLD_WHITE = 0x02 +COLOR_CHANNEL_RED = 0x04 +COLOR_CHANNEL_GREEN = 0x08 +COLOR_CHANNEL_BLUE = 0x10 + +# Generate midpoint color temperatures for bulbs that have limited +# support for white light colors +TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN +TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN +TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN def setup_platform(hass, config, add_devices, discovery_info=None): @@ -28,7 +47,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return value.set_change_verified(False) - add_devices([ZwaveDimmer(value)]) + + if node.has_command_class(zwave.COMMAND_CLASS_COLOR): + try: + add_devices([ZwaveColorLight(value)]) + except ValueError as exception: + _LOGGER.warning( + "Error initializing as color bulb: %s " + "Initializing as standard dimmer.", exception) + add_devices([ZwaveDimmer(value)]) + else: + add_devices([ZwaveDimmer(value)]) def brightness_state(value): @@ -49,8 +78,9 @@ def __init__(self, value): from pydispatch import dispatcher zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) - - self._brightness, self._state = brightness_state(value) + self._brightness = None + self._state = None + self.update_properties() # Used for value change event handling self._refreshing = False @@ -59,6 +89,11 @@ def __init__(self, value): dispatcher.connect( self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + def update_properties(self): + """Update internal properties based on zwave values.""" + # Brightness + self._brightness, self._state = brightness_state(self._value) + def _value_changed(self, value): """Called when a value has changed on the network.""" if self._value.value_id != value.value_id: @@ -66,7 +101,7 @@ def _value_changed(self, value): if self._refreshing: self._refreshing = False - self._brightness, self._state = brightness_state(value) + self.update_properties() else: def _refresh_value(): """Used timer callback for delayed value refresh.""" @@ -107,3 +142,168 @@ def turn_off(self, **kwargs): """Turn the device off.""" if self._value.node.set_dimmer(self._value.value_id, 0): self._state = STATE_OFF + + +def ct_to_rgb(temp): + """Convert color temperature (mireds) to RGB.""" + colorlist = list( + color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp))) + return [int(val) for val in colorlist] + + +class ZwaveColorLight(ZwaveDimmer): + """Representation of a Z-Wave color changing light.""" + + def __init__(self, value): + """Initialize the light.""" + self._value_color = None + self._value_color_channels = None + self._color_channels = None + self._rgb = None + self._ct = None + + # Here we attempt to find a zwave color value with the same instance + # id as the dimmer value. Currently zwave nodes that change colors + # only include one dimmer and one color command, but this will + # hopefully provide some forward compatibility for new devices that + # have multiple color changing elements. + for value_color in value.node.get_rgbbulbs().values(): + if value.instance == value_color.instance: + self._value_color = value_color + + if self._value_color is None: + raise ValueError("No matching color command found.") + + for value_color_channels in value.node.get_values( + class_id=zwave.COMMAND_CLASS_COLOR, genre='System', + type="Int").values(): + self._value_color_channels = value_color_channels + + if self._value_color_channels is None: + raise ValueError("Color Channels not found.") + + super().__init__(value) + + def update_properties(self): + """Update internal properties based on zwave values.""" + super().update_properties() + + # Color Channels + self._color_channels = self._value_color_channels.data + + # Color Data String + data = self._value_color.data + + # RGB is always present in the openzwave color data string. + self._rgb = [ + int(data[1:3], 16), + int(data[3:5], 16), + int(data[5:7], 16)] + + # Parse remaining color channels. Openzwave appends white channels + # that are present. + index = 7 + + # Warm white + if self._color_channels & COLOR_CHANNEL_WARM_WHITE: + warm_white = int(data[index:index+2], 16) + index += 2 + else: + warm_white = 0 + + # Cold white + if self._color_channels & COLOR_CHANNEL_COLD_WHITE: + cold_white = int(data[index:index+2], 16) + index += 2 + else: + cold_white = 0 + + # Color temperature. With two white channels, only two color + # temperatures are supported for the bulb. The channel values + # indicate brightness for warm/cold color temperature. + if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and + self._color_channels & COLOR_CHANNEL_COLD_WHITE): + if warm_white > 0: + self._ct = TEMP_WARM_HASS + self._rgb = ct_to_rgb(self._ct) + elif cold_white > 0: + self._ct = TEMP_COLD_HASS + self._rgb = ct_to_rgb(self._ct) + else: + # RGB color is being used. Just report midpoint. + self._ct = TEMP_MID_HASS + + # If only warm white is reported 0-255 is color temperature. + elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: + self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * ( + warm_white / 255) + self._rgb = ct_to_rgb(self._ct) + + # If only cold white is reported 0-255 is negative color temperature. + elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: + self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * ( + (255 - cold_white) / 255) + self._rgb = ct_to_rgb(self._ct) + + # If no rgb channels supported, report None. + if not (self._color_channels & COLOR_CHANNEL_RED or + self._color_channels & COLOR_CHANNEL_GREEN or + self._color_channels & COLOR_CHANNEL_BLUE): + self._rgb = None + + @property + def rgb_color(self): + """Return the rgb color.""" + return self._rgb + + @property + def color_temp(self): + """Return the color temperature.""" + return self._ct + + def turn_on(self, **kwargs): + """Turn the device on.""" + rgbw = None + + if ATTR_COLOR_TEMP in kwargs: + # With two white channels, only two color temperatures are + # supported for the bulb. + if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and + self._color_channels & COLOR_CHANNEL_COLD_WHITE): + if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS: + self._ct = TEMP_WARM_HASS + rgbw = b'#000000FF00' + else: + self._ct = TEMP_COLD_HASS + rgbw = b'#00000000FF' + + # If only warm white is reported 0-255 is color temperature + elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: + rgbw = b'#000000' + temp = ( + (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) / + (HASS_COLOR_MAX - HASS_COLOR_MIN) * 255) + rgbw += format(int(temp)).encode('utf-8') + + # If only cold white is reported 0-255 is negative color temp + elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: + rgbw = b'#000000' + temp = ( + 255 - (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) / + (HASS_COLOR_MAX - HASS_COLOR_MIN) * 255) + rgbw += format(int(temp)).encode('utf-8') + + elif ATTR_RGB_COLOR in kwargs: + self._rgb = kwargs[ATTR_RGB_COLOR] + + rgbw = b'#' + for colorval in self._rgb: + rgbw += format(colorval, '02x').encode('utf-8') + rgbw += b'0000' + + if rgbw is None: + _LOGGER.warning("rgbw string was not generated for turn_on") + else: + self._value_color.node.set_rgbw(self._value_color.value_id, rgbw) + + super().turn_on(**kwargs) diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 84ec3dfd8470c6..f8959f330330b5 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -41,6 +41,7 @@ COMMAND_CLASS_WHATEVER = None COMMAND_CLASS_SENSOR_MULTILEVEL = 49 +COMMAND_CLASS_COLOR = 51 COMMAND_CLASS_METER = 50 COMMAND_CLASS_ALARM = 113 COMMAND_CLASS_SWITCH_BINARY = 37 From 6714392e9cbd6a493fca03610f86bef875d7cb22 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 27 Jun 2016 09:02:45 -0700 Subject: [PATCH 60/79] Move elevation to core config and clean up HTTP mocking in tests (#2378) * Stick version numbers * Move elevation to core config * Migrate forecast test to requests-mock * Migrate YR tests to requests-mock * Add requests_mock to requirements_test.txt * Move conf code from bootstrap to config * More config fixes * Fix some more issues * Add test for set config and failing auto detect --- homeassistant/bootstrap.py | 129 +- homeassistant/components/__init__.py | 8 +- homeassistant/components/media_player/cmus.py | 2 +- homeassistant/components/sensor/yr.py | 7 +- homeassistant/components/sun.py | 3 +- .../components/thermostat/eq3btsmart.py | 2 +- homeassistant/config.py | 134 +- homeassistant/core.py | 1 + homeassistant/util/location.py | 85 +- requirements_all.txt | 4 +- requirements_test.txt | 11 +- tests/__init__.py | 38 +- ...est_yr.TestSensorYr.test_custom_setup.json | 1 - ...st_yr.TestSensorYr.test_default_setup.json | 1 - tests/common.py | 8 + .../components/{ => sensor}/test_forecast.py | 33 +- tests/components/sensor/test_yr.py | 58 +- tests/components/test_init.py | 2 +- tests/fixtures/freegeoip.io.json | 13 + tests/fixtures/google_maps_elevation.json | 13 + tests/fixtures/ip-api.com.json | 16 + tests/fixtures/yr.no.json | 1184 +++++++++++++++++ tests/test_bootstrap.py | 86 +- tests/test_config.py | 161 ++- tests/util/test_location.py | 114 +- tests/util/test_package.py | 2 +- 26 files changed, 1779 insertions(+), 337 deletions(-) delete mode 100644 tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json delete mode 100644 tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json rename tests/components/{ => sensor}/test_forecast.py (68%) create mode 100644 tests/fixtures/freegeoip.io.json create mode 100644 tests/fixtures/google_maps_elevation.json create mode 100644 tests/fixtures/ip-api.com.json create mode 100644 tests/fixtures/yr.no.json diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ff7e73a00f1d06..8b3d3ee6f239d3 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -3,7 +3,6 @@ import logging import logging.handlers import os -import shutil import sys from collections import defaultdict from threading import RLock @@ -12,21 +11,15 @@ import homeassistant.components as core_components from homeassistant.components import group, persistent_notification -import homeassistant.config as config_util +import homeassistant.config as conf_util import homeassistant.core as core import homeassistant.helpers.config_validation as cv import homeassistant.loader as loader -import homeassistant.util.dt as date_util -import homeassistant.util.location as loc_util import homeassistant.util.package as pkg_util -from homeassistant.const import ( - CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED, - TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__) +from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( - event_decorators, service, config_per_platform, extract_domain_configs, - entity) + event_decorators, service, config_per_platform, extract_domain_configs) _LOGGER = logging.getLogger(__name__) _SETUP_LOCK = RLock() @@ -208,11 +201,6 @@ def prepare_setup_platform(hass, config, domain, platform_name): return platform -def mount_local_lib_path(config_dir): - """Add local library to Python Path.""" - sys.path.insert(0, os.path.join(config_dir, 'deps')) - - # pylint: disable=too-many-branches, too-many-statements, too-many-arguments def from_config_dict(config, hass=None, config_dir=None, enable_log=True, verbose=False, skip_pip=False, @@ -226,18 +214,17 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, if config_dir is not None: config_dir = os.path.abspath(config_dir) hass.config.config_dir = config_dir - mount_local_lib_path(config_dir) + _mount_local_lib_path(config_dir) core_config = config.get(core.DOMAIN, {}) try: - process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA( - core_config)) - except vol.MultipleInvalid as ex: + conf_util.process_ha_core_config(hass, core_config) + except vol.Invalid as ex: cv.log_exception(_LOGGER, ex, 'homeassistant', core_config) return None - process_ha_config_upgrade(hass) + conf_util.process_ha_config_upgrade(hass) if enable_log: enable_logging(hass, verbose, log_rotate_days) @@ -292,12 +279,12 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True, # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - mount_local_lib_path(config_dir) + _mount_local_lib_path(config_dir) enable_logging(hass, verbose, log_rotate_days) try: - config_dict = config_util.load_yaml_config_file(config_path) + config_dict = conf_util.load_yaml_config_file(config_path) except HomeAssistantError: return None @@ -356,100 +343,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None): 'Unable to setup error log %s (access denied)', err_log_path) -def process_ha_config_upgrade(hass): - """Upgrade config if necessary.""" - version_path = hass.config.path('.HA_VERSION') - - try: - with open(version_path, 'rt') as inp: - conf_version = inp.readline().strip() - except FileNotFoundError: - # Last version to not have this file - conf_version = '0.7.7' - - if conf_version == __version__: - return - - _LOGGER.info('Upgrading config directory from %s to %s', conf_version, - __version__) - - # This was where dependencies were installed before v0.18 - # Probably should keep this around until ~v0.20. - lib_path = hass.config.path('lib') - if os.path.isdir(lib_path): - shutil.rmtree(lib_path) - - lib_path = hass.config.path('deps') - if os.path.isdir(lib_path): - shutil.rmtree(lib_path) - - with open(version_path, 'wt') as outp: - outp.write(__version__) - - -def process_ha_core_config(hass, config): - """Process the [homeassistant] section from the config.""" - hac = hass.config - - def set_time_zone(time_zone_str): - """Helper method to set time zone.""" - if time_zone_str is None: - return - - time_zone = date_util.get_time_zone(time_zone_str) - - if time_zone: - hac.time_zone = time_zone - date_util.set_default_time_zone(time_zone) - else: - _LOGGER.error('Received invalid time zone %s', time_zone_str) - - for key, attr in ((CONF_LATITUDE, 'latitude'), - (CONF_LONGITUDE, 'longitude'), - (CONF_NAME, 'location_name')): - if key in config: - setattr(hac, attr, config[key]) - - if CONF_TIME_ZONE in config: - set_time_zone(config.get(CONF_TIME_ZONE)) - - entity.set_customize(config.get(CONF_CUSTOMIZE)) - - if CONF_TEMPERATURE_UNIT in config: - hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] - - # If we miss some of the needed values, auto detect them - if None not in ( - hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone): - return - - _LOGGER.warning('Incomplete core config. Auto detecting location and ' - 'temperature unit') - - info = loc_util.detect_location_info() - - if info is None: - _LOGGER.error('Could not detect location information') - return - - if hac.latitude is None and hac.longitude is None: - hac.latitude = info.latitude - hac.longitude = info.longitude - - if hac.temperature_unit is None: - if info.use_fahrenheit: - hac.temperature_unit = TEMP_FAHRENHEIT - else: - hac.temperature_unit = TEMP_CELSIUS - - if hac.location_name is None: - hac.location_name = info.city - - if hac.time_zone is None: - set_time_zone(info.time_zone) - - def _ensure_loader_prepared(hass): """Ensure Home Assistant loader is prepared.""" if not loader.PREPARED: loader.prepare(hass) + + +def _mount_local_lib_path(config_dir): + """Add local library to Python Path.""" + sys.path.insert(0, os.path.join(config_dir, 'deps')) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index d625f9cd3cdf4d..38780ed9b28a03 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -121,16 +121,16 @@ def handle_turn_service(service): def handle_reload_config(call): """Service handler for reloading core config.""" from homeassistant.exceptions import HomeAssistantError - from homeassistant import config, bootstrap + from homeassistant import config as conf_util try: - path = config.find_config_file(hass.config.config_dir) - conf = config.load_yaml_config_file(path) + path = conf_util.find_config_file(hass.config.config_dir) + conf = conf_util.load_yaml_config_file(path) except HomeAssistantError as err: _LOGGER.error(err) return - bootstrap.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) + conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, handle_reload_config) diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index 43ddee3ba027a3..308d659e111665 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -17,7 +17,7 @@ CONF_PORT) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pycmus>=0.1.0'] +REQUIREMENTS = ['pycmus==0.1.0'] SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 725043c4da8380..ddfbc68d974abd 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -15,7 +15,6 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util -from homeassistant.util import location _LOGGER = logging.getLogger(__name__) @@ -54,16 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yr.no sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - elevation = config.get(CONF_ELEVATION) + elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - if elevation is None: - elevation = location.elevation(latitude, - longitude) - coordinates = dict(lat=latitude, lon=longitude, msl=elevation) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 791fec791f8950..4b2cd10b781789 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -12,7 +12,6 @@ from homeassistant.helpers.event import ( track_point_in_utc_time, track_utc_time_change) from homeassistant.util import dt as dt_util -from homeassistant.util import location as location_util from homeassistant.const import CONF_ELEVATION REQUIREMENTS = ['astral==1.2'] @@ -108,7 +107,7 @@ def setup(hass, config): elevation = platform_config.get(CONF_ELEVATION) if elevation is None: - elevation = location_util.elevation(latitude, longitude) + elevation = hass.config.elevation or 0 from astral import Location diff --git a/homeassistant/components/thermostat/eq3btsmart.py b/homeassistant/components/thermostat/eq3btsmart.py index c9bbdaeb0a4a85..a32a64e81a37a8 100644 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ b/homeassistant/components/thermostat/eq3btsmart.py @@ -10,7 +10,7 @@ from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.temperature import convert -REQUIREMENTS = ['bluepy_devices>=0.2.0'] +REQUIREMENTS = ['bluepy_devices==0.2.0'] CONF_MAC = 'mac' CONF_DEVICES = 'devices' diff --git a/homeassistant/config.py b/homeassistant/config.py index e8981e520c8d8e..55e97f67c7e43f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,31 +1,35 @@ """Module to help with parsing and generating configuration files.""" import logging import os +import shutil from types import MappingProxyType import voluptuous as vol -import homeassistant.util.location as loc_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, - CONF_TIME_ZONE, CONF_CUSTOMIZE) + CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_ELEVATION, TEMP_FAHRENHEIT, + TEMP_CELSIUS, __version__) from homeassistant.exceptions import HomeAssistantError from homeassistant.util.yaml import load_yaml import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import valid_entity_id +from homeassistant.helpers.entity import valid_entity_id, set_customize +from homeassistant.util import dt as date_util, location as loc_util _LOGGER = logging.getLogger(__name__) YAML_CONFIG_FILE = 'configuration.yaml' +VERSION_FILE = '.HA_VERSION' CONFIG_DIR_NAME = '.homeassistant' DEFAULT_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' 'running'), - (CONF_LATITUDE, None, 'latitude', 'Location required to calculate the time' + (CONF_LATITUDE, 0, 'latitude', 'Location required to calculate the time' ' the sun rises and sets'), - (CONF_LONGITUDE, None, 'longitude', None), + (CONF_LONGITUDE, 0, 'longitude', None), + (CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data'), (CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celsius, F for Fahrenheit'), (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), @@ -39,7 +43,7 @@ 'history:': 'Enables support for tracking state changes over time.', 'logbook:': 'View all events in a logbook', 'sun:': 'Track the sun', - 'sensor:\n platform: yr': 'Prediction of weather', + 'sensor:\n platform: yr': 'Weather Prediction', } @@ -61,6 +65,7 @@ def _valid_customize(value): CONF_NAME: vol.Coerce(str), CONF_LATITUDE: cv.latitude, CONF_LONGITUDE: cv.longitude, + CONF_ELEVATION: vol.Coerce(float), CONF_TEMPERATURE_UNIT: cv.temperature_unit, CONF_TIME_ZONE: cv.time_zone, vol.Required(CONF_CUSTOMIZE, @@ -97,6 +102,7 @@ def create_default_config(config_dir, detect_location=True): Return path to new config file if success, None if failed. """ config_path = os.path.join(config_dir, YAML_CONFIG_FILE) + version_path = os.path.join(config_dir, VERSION_FILE) info = {attr: default for attr, default, _, _ in DEFAULT_CONFIG} @@ -111,6 +117,10 @@ def create_default_config(config_dir, detect_location=True): continue info[attr] = getattr(location_info, prop) or default + if location_info.latitude and location_info.longitude: + info[CONF_ELEVATION] = loc_util.elevation(location_info.latitude, + location_info.longitude) + # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. try: @@ -130,6 +140,9 @@ def create_default_config(config_dir, detect_location=True): config_file.write("# {}\n".format(description)) config_file.write("{}\n\n".format(component)) + with open(version_path, 'wt') as version_file: + version_file.write(__version__) + return config_path except IOError: @@ -155,3 +168,112 @@ def load_yaml_config_file(config_path): raise HomeAssistantError(msg) return conf_dict + + +def process_ha_config_upgrade(hass): + """Upgrade config if necessary.""" + version_path = hass.config.path(VERSION_FILE) + + try: + with open(version_path, 'rt') as inp: + conf_version = inp.readline().strip() + except FileNotFoundError: + # Last version to not have this file + conf_version = '0.7.7' + + if conf_version == __version__: + return + + _LOGGER.info('Upgrading config directory from %s to %s', conf_version, + __version__) + + lib_path = hass.config.path('deps') + if os.path.isdir(lib_path): + shutil.rmtree(lib_path) + + with open(version_path, 'wt') as outp: + outp.write(__version__) + + +def process_ha_core_config(hass, config): + """Process the [homeassistant] section from the config.""" + # pylint: disable=too-many-branches + config = CORE_CONFIG_SCHEMA(config) + hac = hass.config + + def set_time_zone(time_zone_str): + """Helper method to set time zone.""" + if time_zone_str is None: + return + + time_zone = date_util.get_time_zone(time_zone_str) + + if time_zone: + hac.time_zone = time_zone + date_util.set_default_time_zone(time_zone) + else: + _LOGGER.error('Received invalid time zone %s', time_zone_str) + + for key, attr in ((CONF_LATITUDE, 'latitude'), + (CONF_LONGITUDE, 'longitude'), + (CONF_NAME, 'location_name'), + (CONF_ELEVATION, 'elevation')): + if key in config: + setattr(hac, attr, config[key]) + + if CONF_TIME_ZONE in config: + set_time_zone(config.get(CONF_TIME_ZONE)) + + set_customize(config.get(CONF_CUSTOMIZE) or {}) + + if CONF_TEMPERATURE_UNIT in config: + hac.temperature_unit = config[CONF_TEMPERATURE_UNIT] + + # Shortcut if no auto-detection necessary + if None not in (hac.latitude, hac.longitude, hac.temperature_unit, + hac.time_zone, hac.elevation): + return + + discovered = [] + + # If we miss some of the needed values, auto detect them + if None in (hac.latitude, hac.longitude, hac.temperature_unit, + hac.time_zone): + info = loc_util.detect_location_info() + + if info is None: + _LOGGER.error('Could not detect location information') + return + + if hac.latitude is None and hac.longitude is None: + hac.latitude = info.latitude + hac.longitude = info.longitude + discovered.append(('latitude', hac.latitude)) + discovered.append(('longitude', hac.longitude)) + + if hac.temperature_unit is None: + if info.use_fahrenheit: + hac.temperature_unit = TEMP_FAHRENHEIT + discovered.append(('temperature_unit', 'F')) + else: + hac.temperature_unit = TEMP_CELSIUS + discovered.append(('temperature_unit', 'C')) + + if hac.location_name is None: + hac.location_name = info.city + discovered.append(('name', info.city)) + + if hac.time_zone is None: + set_time_zone(info.time_zone) + discovered.append(('time_zone', info.time_zone)) + + if hac.elevation is None and hac.latitude is not None and \ + hac.longitude is not None: + elevation = loc_util.elevation(hac.latitude, hac.longitude) + hac.elevation = elevation + discovered.append(('elevation', elevation)) + + if discovered: + _LOGGER.warning( + 'Incomplete core config. Auto detected %s', + ', '.join('{}: {}'.format(key, val) for key, val in discovered)) diff --git a/homeassistant/core.py b/homeassistant/core.py index d3eed6ce5e03ca..cbf02ea587f7ec 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -681,6 +681,7 @@ def __init__(self): """Initialize a new config object.""" self.latitude = None self.longitude = None + self.elevation = None self.temperature_unit = None self.location_name = None self.time_zone = None diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index a596d9bc47635d..a9b980bc871722 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -8,7 +8,8 @@ import requests ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' -DATA_SOURCE = ['https://freegeoip.io/json/', 'http://ip-api.com/json'] +FREEGEO_API = 'https://freegeoip.io/json/' +IP_API = 'http://ip-api.com/json' # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 @@ -32,30 +33,13 @@ def detect_location_info(): """Detect location information.""" - success = None + data = _get_freegeoip() - for source in DATA_SOURCE: - try: - raw_info = requests.get(source, timeout=5).json() - success = source - break - except (requests.RequestException, ValueError): - success = False + if data is None: + data = _get_ip_api() - if success is False: + if data is None: return None - else: - data = {key: raw_info.get(key) for key in LocationInfo._fields} - if success is DATA_SOURCE[1]: - data['ip'] = raw_info.get('query') - data['country_code'] = raw_info.get('countryCode') - data['country_name'] = raw_info.get('country') - data['region_code'] = raw_info.get('region') - data['region_name'] = raw_info.get('regionName') - data['zip_code'] = raw_info.get('zip') - data['time_zone'] = raw_info.get('timezone') - data['latitude'] = raw_info.get('lat') - data['longitude'] = raw_info.get('lon') # From Wikipedia: Fahrenheit is used in the Bahamas, Belize, # the Cayman Islands, Palau, and the United States and associated @@ -73,11 +57,16 @@ def distance(lat1, lon1, lat2, lon2): def elevation(latitude, longitude): """Return elevation for given latitude and longitude.""" - req = requests.get(ELEVATION_URL, - params={'locations': '{},{}'.format(latitude, - longitude), - 'sensor': 'false'}, - timeout=10) + try: + req = requests.get( + ELEVATION_URL, + params={ + 'locations': '{},{}'.format(latitude, longitude), + 'sensor': 'false', + }, + timeout=10) + except requests.RequestException: + return 0 if req.status_code != 200: return 0 @@ -157,3 +146,45 @@ def vincenty(point1, point2, miles=False): s *= MILES_PER_KILOMETER # kilometers to miles return round(s, 6) + + +def _get_freegeoip(): + """Query freegeoip.io for location data.""" + try: + raw_info = requests.get(FREEGEO_API, timeout=5).json() + except (requests.RequestException, ValueError): + return None + + return { + 'ip': raw_info.get('ip'), + 'country_code': raw_info.get('country_code'), + 'country_name': raw_info.get('country_name'), + 'region_code': raw_info.get('region_code'), + 'region_name': raw_info.get('region_name'), + 'city': raw_info.get('city'), + 'zip_code': raw_info.get('zip_code'), + 'time_zone': raw_info.get('time_zone'), + 'latitude': raw_info.get('latitude'), + 'longitude': raw_info.get('longitude'), + } + + +def _get_ip_api(): + """Query ip-api.com for location data.""" + try: + raw_info = requests.get(IP_API, timeout=5).json() + except (requests.RequestException, ValueError): + return None + + return { + 'ip': raw_info.get('query'), + 'country_code': raw_info.get('countryCode'), + 'country_name': raw_info.get('country'), + 'region_code': raw_info.get('region'), + 'region_name': raw_info.get('regionName'), + 'city': raw_info.get('city'), + 'zip_code': raw_info.get('zip'), + 'time_zone': raw_info.get('timezone'), + 'latitude': raw_info.get('lat'), + 'longitude': raw_info.get('lon'), + } diff --git a/requirements_all.txt b/requirements_all.txt index a131813edbd009..42795291eb1f83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -41,7 +41,7 @@ blinkstick==1.1.7 blockchain==1.3.3 # homeassistant.components.thermostat.eq3btsmart -# bluepy_devices>=0.2.0 +# bluepy_devices==0.2.0 # homeassistant.components.notify.aws_lambda # homeassistant.components.notify.aws_sns @@ -245,7 +245,7 @@ pyasn1==0.1.9 pychromecast==0.7.2 # homeassistant.components.media_player.cmus -pycmus>=0.1.0 +pycmus==0.1.0 # homeassistant.components.envisalink # homeassistant.components.zwave diff --git a/requirements_test.txt b/requirements_test.txt index 5ec8619b37f7a4..649859f2506001 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,10 +1,9 @@ -flake8>=2.5.4 -pylint>=1.5.5 +flake8>=2.6.0 +pylint>=1.5.6 coveralls>=1.1 -pytest>=2.9.1 -pytest-cov>=2.2.0 +pytest>=2.9.2 +pytest-cov>=2.2.1 pytest-timeout>=1.0.0 pytest-capturelog>=0.7 -betamax==0.7.0 pydocstyle>=1.0.0 -httpretty==0.8.14 +requests_mock>=1.0 diff --git a/tests/__init__.py b/tests/__init__.py index c1f50d86dfb20f..a931604fdce059 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,27 +1,25 @@ -"""Test the initialization.""" -import betamax +"""Setup some common test helper things.""" +import functools from homeassistant import util from homeassistant.util import location -with betamax.Betamax.configure() as config: - config.cassette_library_dir = 'tests/cassettes' -# Automatically called during different setups. Too often forgotten -# so mocked by default. -location.detect_location_info = lambda: location.LocationInfo( - ip='1.1.1.1', - country_code='US', - country_name='United States', - region_code='CA', - region_name='California', - city='San Diego', - zip_code='92122', - time_zone='America/Los_Angeles', - latitude='2.0', - longitude='1.0', - use_fahrenheit=True, -) +def test_real(func): + """Force a function to require a keyword _test_real to be passed in.""" + @functools.wraps(func) + def guard_func(*args, **kwargs): + real = kwargs.pop('_test_real', None) -location.elevation = lambda latitude, longitude: 0 + if not real: + raise Exception('Forgot to mock or pass "_test_real=True" to %s', + func.__name__) + + return func(*args, **kwargs) + + return guard_func + +# Guard a few functions that would make network connections +location.detect_location_info = test_real(location.detect_location_info) +location.elevation = test_real(location.elevation) util.get_local_ip = lambda: '127.0.0.1' diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json deleted file mode 100644 index c647c4ae017696..00000000000000 --- a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json +++ /dev/null @@ -1 +0,0 @@ -{"http_interactions": [{"request": {"uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:23", "response": {"url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Location": ["http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"], "Age": ["0"], "X-Varnish": ["4249781791"], "Via": ["1.1 varnish"], "Server": ["Varnish"], "Date": ["Thu, 09 Jun 2016 04:02:21 GMT"], "Connection": ["close"], "Accept-Ranges": ["bytes"]}, "body": {"encoding": null, "string": ""}, "status": {"message": "Moved permanently", "code": 301}}}, {"request": {"uri": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:23", "response": {"url": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Content-Length": ["3637"], "X-Backend-Host": ["ravn_loc"], "X-Varnish": ["4249782320 4249780502"], "Via": ["1.1 varnish"], "Last-Modified": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-forecast-models": ["proff,ecdet"], "Accept-Ranges": ["bytes"], "Expires": ["Thu, 09 Jun 2016 05:01:15 GMT"], "Content-Encoding": ["gzip"], "Age": ["1"], "Content-Type": ["text/xml; charset=utf-8"], "Server": ["Apache"], "Vary": ["Accept-Encoding"], "Date": ["Thu, 09 Jun 2016 04:02:22 GMT"], "X-slicenumber": ["83"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "base64_string": "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"}, "status": {"message": "OK", "code": 200}}}], "recorded_with": "betamax/0.7.0"} \ No newline at end of file diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json deleted file mode 100644 index 8226cbbf96e209..00000000000000 --- a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json +++ /dev/null @@ -1 +0,0 @@ -{"http_interactions": [{"request": {"uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:22", "response": {"url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Location": ["http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"], "Age": ["0"], "X-Varnish": ["4249779869"], "Via": ["1.1 varnish"], "Server": ["Varnish"], "Date": ["Thu, 09 Jun 2016 04:02:20 GMT"], "Connection": ["close"], "Accept-Ranges": ["bytes"]}, "body": {"encoding": null, "string": ""}, "status": {"message": "Moved permanently", "code": 301}}}, {"request": {"uri": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:22", "response": {"url": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Content-Length": ["3637"], "X-Backend-Host": ["ravn_loc"], "X-Varnish": ["4249780502"], "Via": ["1.1 varnish"], "Last-Modified": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-forecast-models": ["proff,ecdet"], "Accept-Ranges": ["bytes"], "Expires": ["Thu, 09 Jun 2016 05:01:15 GMT"], "Content-Encoding": ["gzip"], "Age": ["0"], "Content-Type": ["text/xml; charset=utf-8"], "Server": ["Apache"], "Vary": ["Accept-Encoding"], "Date": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-slicenumber": ["83"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "base64_string": "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"}, "status": {"message": "OK", "code": 200}}}], "recorded_with": "betamax/0.7.0"} \ No newline at end of file diff --git a/tests/common.py b/tests/common.py index 98c61dfc16e890..26d466bc4b8b38 100644 --- a/tests/common.py +++ b/tests/common.py @@ -35,6 +35,7 @@ def get_test_home_assistant(num_threads=None): hass.config.config_dir = get_test_config_dir() hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 + hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone('US/Pacific') hass.config.temperature_unit = TEMP_CELSIUS @@ -105,6 +106,13 @@ def ensure_sun_set(hass): fire_time_changed(hass, sun.next_setting_utc(hass) + timedelta(seconds=10)) +def load_fixture(filename): + """Helper to load a fixture.""" + path = os.path.join(os.path.dirname(__file__), 'fixtures', filename) + with open(path) as fp: + return fp.read() + + def mock_state_change_event(hass, new_state, old_state=None): """Mock state change envent.""" event_data = { diff --git a/tests/components/test_forecast.py b/tests/components/sensor/test_forecast.py similarity index 68% rename from tests/components/test_forecast.py rename to tests/components/sensor/test_forecast.py index bfda22596c2c5d..55bdec20a3511a 100644 --- a/tests/components/test_forecast.py +++ b/tests/components/sensor/test_forecast.py @@ -1,17 +1,17 @@ """The tests for the forecast.io platform.""" -import json import re -import os import unittest from unittest.mock import MagicMock, patch import forecastio -import httpretty from requests.exceptions import HTTPError +import requests_mock from homeassistant.components.sensor import forecast from homeassistant import core as ha +from tests.common import load_fixture + class TestForecastSetup(unittest.TestCase): """Test the forecast.io platform.""" @@ -48,29 +48,14 @@ def test_setup_bad_api_key(self, mock_get_forecast): response = forecast.setup_platform(self.hass, self.config, MagicMock()) self.assertFalse(response) - @httpretty.activate + @requests_mock.Mocker() @patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast) - def test_setup(self, mock_get_forecast): + def test_setup(self, m, mock_get_forecast): """Test for successfully setting up the forecast.io platform.""" - def load_fixture_from_json(): - cwd = os.path.dirname(__file__) - fixture_path = os.path.join(cwd, '..', 'fixtures', 'forecast.json') - with open(fixture_path) as file: - content = json.load(file) - return json.dumps(content) - - # Mock out any calls to the actual API and - # return the fixture json instead - uri = 'api.forecast.io\/forecast\/(\w+)\/(-?\d+\.?\d*),(-?\d+\.?\d*)' - httpretty.register_uri( - httpretty.GET, - re.compile(uri), - body=load_fixture_from_json(), - ) - # The following will raise an error if the regex for the mock was - # incorrect and we actually try to go out to the internet. - httpretty.HTTPretty.allow_net_connect = False - + uri = ('https://api.forecast.io\/forecast\/(\w+)\/' + '(-?\d+\.?\d*),(-?\d+\.?\d*)') + m.get(re.compile(uri), + text=load_fixture('forecast.json')) forecast.setup_platform(self.hass, self.config, MagicMock()) self.assertTrue(mock_get_forecast.called) self.assertEqual(mock_get_forecast.call_count, 1) diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 43a14578690447..3ea94938f0d929 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -1,39 +1,40 @@ """The tests for the Yr sensor platform.""" from datetime import datetime +from unittest import TestCase from unittest.mock import patch -import pytest +import requests_mock from homeassistant.bootstrap import _setup_component import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture -@pytest.mark.usefixtures('betamax_session') -class TestSensorYr: +class TestSensorYr(TestCase): """Test the Yr sensor.""" - def setup_method(self, method): + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.latitude = 32.87336 self.hass.config.longitude = 117.22743 - def teardown_method(self, method): + def tearDown(self): """Stop everything that was started.""" self.hass.stop() - def test_default_setup(self, betamax_session): + @requests_mock.Mocker() + def test_default_setup(self, m): """Test the default setup.""" + m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.sensor.yr.requests.Session', - return_value=betamax_session): - with patch('homeassistant.components.sensor.yr.dt_util.utcnow', - return_value=now): - assert _setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0}}) + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert _setup_component(self.hass, 'sensor', { + 'sensor': {'platform': 'yr', + 'elevation': 0}}) state = self.hass.states.get('sensor.yr_symbol') @@ -41,23 +42,24 @@ def test_default_setup(self, betamax_session): assert state.state.isnumeric() assert state.attributes.get('unit_of_measurement') is None - def test_custom_setup(self, betamax_session): + @requests_mock.Mocker() + def test_custom_setup(self, m): """Test a custom setup.""" + m.get('http://api.yr.no/weatherapi/locationforecast/1.9/', + text=load_fixture('yr.no.json')) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.sensor.yr.requests.Session', - return_value=betamax_session): - with patch('homeassistant.components.sensor.yr.dt_util.utcnow', - return_value=now): - assert _setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0, - 'monitored_conditions': [ - 'pressure', - 'windDirection', - 'humidity', - 'fog', - 'windSpeed']}}) + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert _setup_component(self.hass, 'sensor', { + 'sensor': {'platform': 'yr', + 'elevation': 0, + 'monitored_conditions': [ + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed']}}) state = self.hass.states.get('sensor.yr_pressure') assert 'hPa' == state.attributes.get('unit_of_measurement') diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 68b0ca3be35d78..7abaf63b407703 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -131,7 +131,7 @@ def test_reload_core_conf(self): assert state.attributes.get('hello') == 'world' @patch('homeassistant.components._LOGGER.error') - @patch('homeassistant.bootstrap.process_ha_core_config') + @patch('homeassistant.config.process_ha_core_config') def test_reload_core_with_wrong_conf(self, mock_process, mock_error): """Test reload core conf service.""" with TemporaryDirectory() as conf_dir: diff --git a/tests/fixtures/freegeoip.io.json b/tests/fixtures/freegeoip.io.json new file mode 100644 index 00000000000000..8afdaba070e998 --- /dev/null +++ b/tests/fixtures/freegeoip.io.json @@ -0,0 +1,13 @@ +{ + "ip": "1.2.3.4", + "country_code": "US", + "country_name": "United States", + "region_code": "CA", + "region_name": "California", + "city": "San Diego", + "zip_code": "92122", + "time_zone": "America\/Los_Angeles", + "latitude": 32.8594, + "longitude": -117.2073, + "metro_code": 825 +} diff --git a/tests/fixtures/google_maps_elevation.json b/tests/fixtures/google_maps_elevation.json new file mode 100644 index 00000000000000..95eeb0fe239ad1 --- /dev/null +++ b/tests/fixtures/google_maps_elevation.json @@ -0,0 +1,13 @@ +{ + "results" : [ + { + "elevation" : 101.5, + "location" : { + "lat" : 32.54321, + "lng" : -117.12345 + }, + "resolution" : 4.8 + } + ], + "status" : "OK" +} diff --git a/tests/fixtures/ip-api.com.json b/tests/fixtures/ip-api.com.json new file mode 100644 index 00000000000000..d31d4560589b3b --- /dev/null +++ b/tests/fixtures/ip-api.com.json @@ -0,0 +1,16 @@ +{ + "as": "AS20001 Time Warner Cable Internet LLC", + "city": "San Diego", + "country": "United States", + "countryCode": "US", + "isp": "Time Warner Cable", + "lat": 32.8594, + "lon": -117.2073, + "org": "Time Warner Cable", + "query": "1.2.3.4", + "region": "CA", + "regionName": "California", + "status": "success", + "timezone": "America\/Los_Angeles", + "zip": "92122" +} diff --git a/tests/fixtures/yr.no.json b/tests/fixtures/yr.no.json new file mode 100644 index 00000000000000..b181fdfd85b225 --- /dev/null +++ b/tests/fixtures/yr.no.json @@ -0,0 +1,1184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 152818d02e4709..34aaa1b83eda04 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,6 +1,5 @@ """Test the bootstrapping.""" # pylint: disable=too-many-public-methods,protected-access -import os import tempfile from unittest import mock import threading @@ -8,10 +7,7 @@ import voluptuous as vol from homeassistant import bootstrap, loader -from homeassistant.const import (__version__, CONF_LATITUDE, CONF_LONGITUDE, - CONF_NAME, CONF_CUSTOMIZE) import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from tests.common import get_test_home_assistant, MockModule, MockPlatform @@ -24,23 +20,22 @@ class TestBootstrap: def setup_method(self, method): """Setup the test.""" + self.backup_cache = loader._COMPONENT_CACHE + if method == self.test_from_config_file: return self.hass = get_test_home_assistant() - self.backup_cache = loader._COMPONENT_CACHE def teardown_method(self, method): """Clean up.""" dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE - - if method == self.test_from_config_file: - return - self.hass.stop() loader._COMPONENT_CACHE = self.backup_cache - def test_from_config_file(self): + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=None) + def test_from_config_file(self, mock_detect): """Test with configuration file.""" components = ['browser', 'conversation', 'script'] with tempfile.NamedTemporaryFile() as fp: @@ -48,71 +43,10 @@ def test_from_config_file(self): fp.write('{}:\n'.format(comp).encode('utf-8')) fp.flush() - hass = bootstrap.from_config_file(fp.name) - - components.append('group') - - assert sorted(components) == sorted(hass.config.components) - - def test_remove_lib_on_upgrade(self): - """Test removal of library on upgrade.""" - with tempfile.TemporaryDirectory() as config_dir: - version_path = os.path.join(config_dir, '.HA_VERSION') - lib_dir = os.path.join(config_dir, 'deps') - check_file = os.path.join(lib_dir, 'check') - - with open(version_path, 'wt') as outp: - outp.write('0.7.0') - - os.mkdir(lib_dir) - - with open(check_file, 'w'): - pass - - self.hass.config.config_dir = config_dir - - assert os.path.isfile(check_file) - bootstrap.process_ha_config_upgrade(self.hass) - assert not os.path.isfile(check_file) - - def test_not_remove_lib_if_not_upgrade(self): - """Test removal of library with no upgrade.""" - with tempfile.TemporaryDirectory() as config_dir: - version_path = os.path.join(config_dir, '.HA_VERSION') - lib_dir = os.path.join(config_dir, 'deps') - check_file = os.path.join(lib_dir, 'check') - - with open(version_path, 'wt') as outp: - outp.write(__version__) - - os.mkdir(lib_dir) - - with open(check_file, 'w'): - pass - - self.hass.config.config_dir = config_dir - - bootstrap.process_ha_config_upgrade(self.hass) - - assert os.path.isfile(check_file) - - def test_entity_customization(self): - """Test entity customization through configuration.""" - config = {CONF_LATITUDE: 50, - CONF_LONGITUDE: 50, - CONF_NAME: 'Test', - CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} - - bootstrap.process_ha_core_config(self.hass, config) - - entity = Entity() - entity.entity_id = 'test.test' - entity.hass = self.hass - entity.update_ha_state() - - state = self.hass.states.get('test.test') + self.hass = bootstrap.from_config_file(fp.name) - assert state.attributes['hidden'] + components.append('group') + assert sorted(components) == sorted(self.hass.config.components) def test_handle_setup_circular_dependency(self): """Test the setup of circular dependencies.""" @@ -302,8 +236,7 @@ def exception_setup(hass, config): assert not bootstrap._setup_component(self.hass, 'comp', None) assert 'comp' not in self.hass.config.components - @mock.patch('homeassistant.bootstrap.process_ha_core_config') - def test_home_assistant_core_config_validation(self, mock_process): + def test_home_assistant_core_config_validation(self): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done in test_config.py assert None is bootstrap.from_config_dict({ @@ -311,7 +244,6 @@ def test_home_assistant_core_config_validation(self, mock_process): 'latitude': 'some string' } }) - assert not mock_process.called def test_component_setup_with_validation_and_dependency(self): """Test all config is passed to dependencies.""" diff --git a/tests/test_config.py b/tests/test_config.py index 8a5ec306b3bed7..6be3f585967c2a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,22 +1,28 @@ """Test config utils.""" # pylint: disable=too-many-public-methods,protected-access +import os +import tempfile import unittest import unittest.mock as mock -import os import pytest from voluptuous import MultipleInvalid -from homeassistant.core import DOMAIN, HomeAssistantError +from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, - CONF_TIME_ZONE) + CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, + TEMP_FAHRENHEIT) +from homeassistant.util import location as location_util, dt as dt_util +from homeassistant.helpers.entity import Entity -from tests.common import get_test_config_dir +from tests.common import ( + get_test_config_dir, get_test_home_assistant) CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE def create_file(path): @@ -30,9 +36,14 @@ class TestConfig(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """Clean up.""" + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE + if os.path.isfile(YAML_PATH): os.remove(YAML_PATH) + if hasattr(self, 'hass'): + self.hass.stop() + def test_create_default_config(self): """Test creation of default config.""" config_util.create_default_config(CONFIG_DIR, False) @@ -108,8 +119,15 @@ def test_load_yaml_config_preserves_key_order(self): [('hello', 0), ('world', 1)], list(config_util.load_yaml_config_file(YAML_PATH).items())) + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=location_util.LocationInfo( + '0.0.0.0', 'US', 'United States', 'CA', 'California', + 'San Diego', '92122', 'America/Los_Angeles', 32.8594, + -117.2073, True)) + @mock.patch('homeassistant.util.location.elevation', return_value=101) @mock.patch('builtins.print') - def test_create_default_config_detect_location(self, mock_print): + def test_create_default_config_detect_location(self, mock_detect, + mock_elev, mock_print): """Test that detect location sets the correct config keys.""" config_util.ensure_config_exists(CONFIG_DIR) @@ -120,15 +138,16 @@ def test_create_default_config_detect_location(self, mock_print): ha_conf = config[DOMAIN] expected_values = { - CONF_LATITUDE: 2.0, - CONF_LONGITUDE: 1.0, + CONF_LATITUDE: 32.8594, + CONF_LONGITUDE: -117.2073, + CONF_ELEVATION: 101, CONF_TEMPERATURE_UNIT: 'F', CONF_NAME: 'Home', CONF_TIME_ZONE: 'America/Los_Angeles' } - self.assertEqual(expected_values, ha_conf) - self.assertTrue(mock_print.called) + assert expected_values == ha_conf + assert mock_print.called @mock.patch('builtins.print') def test_create_default_config_returns_none_if_write_error(self, @@ -166,3 +185,127 @@ def test_core_config_schema(self): }, }, }) + + def test_entity_customization(self): + """Test entity customization through configuration.""" + self.hass = get_test_home_assistant() + + config = {CONF_LATITUDE: 50, + CONF_LONGITUDE: 50, + CONF_NAME: 'Test', + CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} + + config_util.process_ha_core_config(self.hass, config) + + entity = Entity() + entity.entity_id = 'test.test' + entity.hass = self.hass + entity.update_ha_state() + + state = self.hass.states.get('test.test') + + assert state.attributes['hidden'] + + def test_remove_lib_on_upgrade(self): + """Test removal of library on upgrade.""" + with tempfile.TemporaryDirectory() as config_dir: + version_path = os.path.join(config_dir, '.HA_VERSION') + lib_dir = os.path.join(config_dir, 'deps') + check_file = os.path.join(lib_dir, 'check') + + with open(version_path, 'wt') as outp: + outp.write('0.7.0') + + os.mkdir(lib_dir) + + with open(check_file, 'w'): + pass + + self.hass = get_test_home_assistant() + self.hass.config.config_dir = config_dir + + assert os.path.isfile(check_file) + config_util.process_ha_config_upgrade(self.hass) + assert not os.path.isfile(check_file) + + def test_not_remove_lib_if_not_upgrade(self): + """Test removal of library with no upgrade.""" + with tempfile.TemporaryDirectory() as config_dir: + version_path = os.path.join(config_dir, '.HA_VERSION') + lib_dir = os.path.join(config_dir, 'deps') + check_file = os.path.join(lib_dir, 'check') + + with open(version_path, 'wt') as outp: + outp.write(__version__) + + os.mkdir(lib_dir) + + with open(check_file, 'w'): + pass + + self.hass = get_test_home_assistant() + self.hass.config.config_dir = config_dir + + config_util.process_ha_config_upgrade(self.hass) + + assert os.path.isfile(check_file) + + def test_loading_configuration(self): + """Test loading core config onto hass object.""" + config = Config() + hass = mock.Mock(config=config) + + config_util.process_ha_core_config(hass, { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + 'temperature_unit': 'F', + 'time_zone': 'America/New_York', + }) + + assert config.latitude == 60 + assert config.longitude == 50 + assert config.elevation == 25 + assert config.location_name == 'Huis' + assert config.temperature_unit == TEMP_FAHRENHEIT + assert config.time_zone.zone == 'America/New_York' + + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=location_util.LocationInfo( + '0.0.0.0', 'US', 'United States', 'CA', 'California', + 'San Diego', '92122', 'America/Los_Angeles', 32.8594, + -117.2073, True)) + @mock.patch('homeassistant.util.location.elevation', return_value=101) + def test_discovering_configuration(self, mock_detect, mock_elevation): + """Test auto discovery for missing core configs.""" + config = Config() + hass = mock.Mock(config=config) + + config_util.process_ha_core_config(hass, {}) + + assert config.latitude == 32.8594 + assert config.longitude == -117.2073 + assert config.elevation == 101 + assert config.location_name == 'San Diego' + assert config.temperature_unit == TEMP_FAHRENHEIT + assert config.time_zone.zone == 'America/Los_Angeles' + + @mock.patch('homeassistant.util.location.detect_location_info', + return_value=None) + @mock.patch('homeassistant.util.location.elevation', return_value=0) + def test_discovering_configuration_auto_detect_fails(self, mock_detect, + mock_elevation): + """Test config remains unchanged if discovery fails.""" + config = Config() + hass = mock.Mock(config=config) + + config_util.process_ha_core_config(hass, {}) + + blankConfig = Config() + assert config.latitude == blankConfig.latitude + assert config.longitude == blankConfig.longitude + assert config.elevation == blankConfig.elevation + assert config.location_name == blankConfig.location_name + assert config.temperature_unit == blankConfig.temperature_unit + assert config.time_zone == blankConfig.time_zone diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 7d0052fe62c2a3..1dfb71a87bf5a2 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,9 +1,15 @@ """Test Home Assistant location util methods.""" # pylint: disable=too-many-public-methods -import unittest +from unittest import TestCase +from unittest.mock import patch + +import requests +import requests_mock import homeassistant.util.location as location_util +from tests.common import load_fixture + # Paris COORDINATES_PARIS = (48.864716, 2.349014) # New York @@ -20,26 +26,124 @@ DISTANCE_MILES = 3632.78 -class TestLocationUtil(unittest.TestCase): +class TestLocationUtil(TestCase): """Test util location methods.""" + def test_get_distance_to_same_place(self): + """Test getting the distance.""" + meters = location_util.distance(COORDINATES_PARIS[0], + COORDINATES_PARIS[1], + COORDINATES_PARIS[0], + COORDINATES_PARIS[1]) + + assert meters == 0 + def test_get_distance(self): """Test getting the distance.""" meters = location_util.distance(COORDINATES_PARIS[0], COORDINATES_PARIS[1], COORDINATES_NEW_YORK[0], COORDINATES_NEW_YORK[1]) - self.assertAlmostEqual(meters / 1000, DISTANCE_KM, places=2) + + assert meters/1000 - DISTANCE_KM < 0.01 def test_get_kilometers(self): """Test getting the distance between given coordinates in km.""" kilometers = location_util.vincenty(COORDINATES_PARIS, COORDINATES_NEW_YORK) - self.assertEqual(round(kilometers, 2), DISTANCE_KM) + assert round(kilometers, 2) == DISTANCE_KM def test_get_miles(self): """Test getting the distance between given coordinates in miles.""" miles = location_util.vincenty(COORDINATES_PARIS, COORDINATES_NEW_YORK, miles=True) - self.assertEqual(round(miles, 2), DISTANCE_MILES) + assert round(miles, 2) == DISTANCE_MILES + + @requests_mock.Mocker() + def test_detect_location_info_freegeoip(self, m): + """Test detect location info using freegeoip.""" + m.get(location_util.FREEGEO_API, + text=load_fixture('freegeoip.io.json')) + + info = location_util.detect_location_info(_test_real=True) + + assert info is not None + assert info.ip == '1.2.3.4' + assert info.country_code == 'US' + assert info.country_name == 'United States' + assert info.region_code == 'CA' + assert info.region_name == 'California' + assert info.city == 'San Diego' + assert info.zip_code == '92122' + assert info.time_zone == 'America/Los_Angeles' + assert info.latitude == 32.8594 + assert info.longitude == -117.2073 + assert info.use_fahrenheit + + @requests_mock.Mocker() + @patch('homeassistant.util.location._get_freegeoip', return_value=None) + def test_detect_location_info_ipapi(self, mock_req, mock_freegeoip): + """Test detect location info using freegeoip.""" + mock_req.get(location_util.IP_API, + text=load_fixture('ip-api.com.json')) + + info = location_util.detect_location_info(_test_real=True) + + assert info is not None + assert info.ip == '1.2.3.4' + assert info.country_code == 'US' + assert info.country_name == 'United States' + assert info.region_code == 'CA' + assert info.region_name == 'California' + assert info.city == 'San Diego' + assert info.zip_code == '92122' + assert info.time_zone == 'America/Los_Angeles' + assert info.latitude == 32.8594 + assert info.longitude == -117.2073 + assert info.use_fahrenheit + + @patch('homeassistant.util.location.elevation', return_value=0) + @patch('homeassistant.util.location._get_freegeoip', return_value=None) + @patch('homeassistant.util.location._get_ip_api', return_value=None) + def test_detect_location_info_both_queries_fail(self, mock_ipapi, + mock_freegeoip, + mock_elevation): + """Ensure we return None if both queries fail.""" + info = location_util.detect_location_info(_test_real=True) + assert info is None + + @patch('homeassistant.util.location.requests.get', + side_effect=requests.RequestException) + def test_freegeoip_query_raises(self, mock_get): + """Test freegeoip query when the request to API fails.""" + info = location_util._get_freegeoip() + assert info is None + + @patch('homeassistant.util.location.requests.get', + side_effect=requests.RequestException) + def test_ip_api_query_raises(self, mock_get): + """Test ip api query when the request to API fails.""" + info = location_util._get_ip_api() + assert info is None + + @patch('homeassistant.util.location.requests.get', + side_effect=requests.RequestException) + def test_elevation_query_raises(self, mock_get): + """Test elevation when the request to API fails.""" + elevation = location_util.elevation(10, 10, _test_real=True) + assert elevation == 0 + + @requests_mock.Mocker() + def test_elevation_query_fails(self, mock_req): + """Test elevation when the request to API fails.""" + mock_req.get(location_util.ELEVATION_URL, text='{}', status_code=401) + elevation = location_util.elevation(10, 10, _test_real=True) + assert elevation == 0 + + @requests_mock.Mocker() + def test_elevation_query_nonjson(self, mock_req): + """Test if elevation API returns a non JSON value.""" + mock_req.get(location_util.ELEVATION_URL, text='{ I am not JSON }') + elevation = location_util.elevation(10, 10, _test_real=True) + assert elevation == 0 diff --git a/tests/util/test_package.py b/tests/util/test_package.py index ea9a8f23dfae71..a4e0019695902e 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -49,7 +49,7 @@ def test_install_package_zip(self): self.assertTrue(package.check_package_exists( TEST_NEW_REQ, self.lib_dir)) - bootstrap.mount_local_lib_path(self.tmp_dir.name) + bootstrap._mount_local_lib_path(self.tmp_dir.name) try: import pyhelloworld3 From 592c59948898de337a24d4e43af4539e8d8bb3ac Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 28 Jun 2016 02:39:44 +0200 Subject: [PATCH 61/79] Upgrade Werkzeug to 0.11.10 (#2380) --- homeassistant/components/http.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 1f77aac5ad4fc6..218c202bcc2513 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -25,7 +25,7 @@ import homeassistant.helpers.config_validation as cv DOMAIN = "http" -REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5") +REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.10") CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" diff --git a/requirements_all.txt b/requirements_all.txt index 42795291eb1f83..a6faa5a3e3eb52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -23,7 +23,7 @@ SoCo==0.11.1 TwitterAPI==2.4.1 # homeassistant.components.http -Werkzeug==0.11.5 +Werkzeug==0.11.10 # homeassistant.components.apcupsd apcaccess==0.0.4 From 7a73dc7d6a24fa0e5d4c5de9708db90689112105 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 28 Jun 2016 08:47:35 +0200 Subject: [PATCH 62/79] Upgrade websocket-client to 0.37.0 (#2382) --- homeassistant/components/media_player/gpmdp.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 8259d043cf3231..eb6e15379d8259 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -15,7 +15,7 @@ STATE_PLAYING, STATE_PAUSED, STATE_OFF) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['websocket-client==0.35.0'] +REQUIREMENTS = ['websocket-client==0.37.0'] SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK diff --git a/requirements_all.txt b/requirements_all.txt index a6faa5a3e3eb52..9f92877194d543 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -410,7 +410,7 @@ vsure==0.8.1 wakeonlan==0.2.2 # homeassistant.components.media_player.gpmdp -websocket-client==0.35.0 +websocket-client==0.37.0 # homeassistant.components.zigbee xbee-helper==0.0.7 From 00179763efe1ccee5490ee8053978f7cf38a9957 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 28 Jun 2016 16:56:14 +0200 Subject: [PATCH 63/79] Upgrade influxdb to 3.0.0 (#2383) --- homeassistant/components/influxdb.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index e9ae7de81bcd74..311d3fe83df668 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -23,7 +23,7 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = False -REQUIREMENTS = ['influxdb==2.12.0'] +REQUIREMENTS = ['influxdb==3.0.0'] CONF_HOST = 'host' CONF_PORT = 'port' diff --git a/requirements_all.txt b/requirements_all.txt index 9f92877194d543..2be4536345de5f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753 https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 # homeassistant.components.influxdb -influxdb==2.12.0 +influxdb==3.0.0 # homeassistant.components.insteon_hub insteon_hub==0.4.5 From baa9bdf6fc75769aa45ce46ae175b989264a020f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 28 Jun 2016 22:53:53 +0200 Subject: [PATCH 64/79] change homematic to autodetect only --- .../components/binary_sensor/homematic.py | 58 +++-------------- homeassistant/components/homematic.py | 65 +++---------------- homeassistant/components/light/homematic.py | 22 ++----- .../components/rollershutter/homematic.py | 21 ++---- homeassistant/components/sensor/homematic.py | 22 ++----- homeassistant/components/switch/homematic.py | 22 ++----- .../components/thermostat/homematic.py | 21 ++---- 7 files changed, 49 insertions(+), 182 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 08ea209944548f..5452229ee541c7 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -6,26 +6,6 @@ Important: For this platform to work the homematic component has to be properly configured. - -Configuration (single channel, simple device): - -binary_sensor: - - platform: homematic - address: "" # e.g. "JEQ0XXXXXXX" - name: "" (optional) - - -Configuration (multiple channels, like motion detector with buttons): - -binary_sensor: - - platform: homematic - address: "" # e.g. "JEQ0XXXXXXX" - param: (device-dependent) (optional) - button: n (integer of channel to map, device-dependent) (optional) - name: "" (optional) -binary_sensor: - - platform: homematic - ... """ import logging @@ -55,14 +35,12 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMBinarySensor, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, + discovery_info, + add_callback_devices) class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): @@ -73,18 +51,6 @@ def is_on(self): """Return True if switch is on.""" if not self.available: return False - # no binary is defined, check all! - if self._state is None: - available_bin = self._create_binary_list_from_hm() - for binary in available_bin: - try: - if binary in self._data and self._data[binary] == 1: - return True - except (ValueError, TypeError): - _LOGGER.warning("%s datatype error!", self._name) - return False - - # single binary return bool(self._hm_get_state()) @property @@ -123,9 +89,10 @@ def _check_hm_to_ha_object(self): # only check and give a warining to User if self._state is None and len(available_bin) > 1: - _LOGGER.warning("%s have multible binary params. It use all " + - "binary nodes as one. Possible param values: %s", - self._name, str(available_bin)) + _LOGGER.critical("%s have multible binary params. It use all " + + "binary nodes as one. Possible param values: %s", + self._name, str(available_bin)) + return False return True @@ -141,11 +108,6 @@ def _init_data_struct(self): for value in available_bin: self._state = value - # no binary is definit, use all binary for state - if self._state is None and len(available_bin) > 1: - for node in available_bin: - self._data.update({node: STATE_UNKNOWN}) - # add state to data struct if self._state: _LOGGER.debug("%s init datastruct with main node '%s'", self._name, diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 7b3e265a9ddd43..a0a31566661117 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -11,7 +11,6 @@ local_port: remote_ip: "" remote_port: - autodetect: "" (optional, experimental, detect all devices) """ import time import logging @@ -119,22 +118,9 @@ def system_callback_handler(hass, config, src, *args): for dev in dev_descriptions: key_dict[dev['ADDRESS'].split(':')[0]] = True - # Connect devices already created in HA to pyhomematic and - # add remaining devices to list - devices_not_created = [] - for dev in key_dict: - if dev in HOMEMATIC_DEVICES: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() - else: - devices_not_created.append(dev) - # If configuration allows autodetection of devices, # all devices not configured are added. - autodetect = config[DOMAIN].get("autodetect", False) - _LOGGER.debug("Autodetect is %s / unknown device: %s", str(autodetect), - str(devices_not_created)) - if autodetect and devices_not_created: + if key_dict: for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), ('light', DISCOVER_LIGHTS), @@ -143,8 +129,7 @@ def system_callback_handler(hass, config, src, *args): ('sensor', DISCOVER_SENSORS), ('thermostat', DISCOVER_THERMOSTATS)): # Get all devices of a specific type - found_devices = _get_devices(discovery_type, - devices_not_created) + found_devices = _get_devices(discovery_type, key_dict) # When devices of this type are found # they are setup in HA and an event is fired @@ -162,8 +147,6 @@ def _get_devices(device_type, keys): # run device_arr = [] - if not keys: - keys = HOMEMATIC.devices for key in keys: device = HOMEMATIC.devices[key] if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: @@ -265,40 +248,16 @@ def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, add_callback_devices): """Helper to setup Homematic devices with discovery info.""" for config in discovery_info["devices"]: - ret = setup_hmdevice_entity_helper(hmdevicetype, config, - add_callback_devices) - if not ret: - _LOGGER.error("Setup discovery error with config %s", str(config)) - - return True + _LOGGER.debug("Add device %s from config: %s", + str(hmdevicetype), str(config)) + # create object and add to HA + new_device = hmdevicetype(config) + add_callback_devices([new_device]) -def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): - """Helper to setup Homematic devices.""" - if HOMEMATIC is None: - _LOGGER.error('Error setting up HMDevice: Server not configured.') - return False + # link to HM + new_device.link_homematic() - address = config.get('address', None) - if address is None: - _LOGGER.error("Error setting up device '%s': " + - "'address' missing in configuration.", address) - return False - - _LOGGER.debug("Add device %s from config: %s", - str(hmdevicetype), str(config)) - # Create a new HA homematic object - new_device = hmdevicetype(config) - if address not in HOMEMATIC_DEVICES: - HOMEMATIC_DEVICES[address] = [] - HOMEMATIC_DEVICES[address].append(new_device) - - # Add to HA - add_callback_devices([new_device]) - - # HM is connected - if address in HOMEMATIC.devices: - return new_device.link_homematic() return True @@ -312,7 +271,6 @@ def __init__(self, config): self._address = config.get("address", None) self._channel = config.get("button", 1) self._state = config.get("param", None) - self._hidden = config.get("hidden", False) self._data = {} self._hmdevice = None self._connected = False @@ -348,11 +306,6 @@ def available(self): """Return True if device is available.""" return self._available - @property - def hidden(self): - """Return True if the entity should be hidden from UIs.""" - return self._hidden - @property def device_state_attributes(self): """Return device specific state attributes.""" diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 159f3e4dbdcd2b..b1f08db0783f37 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -6,14 +6,6 @@ Important: For this platform to work the homematic component has to be properly configured. - -Configuration: - -light: - - platform: homematic - addresss: # e.g. "JEQ0XXXXXXX" - name: (optional) - button: n (integer of channel to map, device-dependent) """ import logging @@ -29,14 +21,12 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMLight, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMLight, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMLight, + discovery_info, + add_callback_devices) class HMLight(homematic.HMDevice, Light): diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index 737a7eb017ddc0..9bdad7ee68c643 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -6,13 +6,6 @@ Important: For this platform to work the homematic component has to be properly configured. - -Configuration: - -rollershutter: - - platform: homematic - address: "" # e.g. "JEQ0XXXXXXX" - name: "" (optional) """ import logging @@ -29,14 +22,12 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMRollershutter, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMRollershutter, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMRollershutter, + discovery_info, + add_callback_devices) class HMRollershutter(homematic.HMDevice, RollershutterDevice): diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index f6f3825199b687..2efa4fdef38c1d 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -6,14 +6,6 @@ Important: For this platform to work the homematic component has to be properly configured. - -Configuration: - -sensor: - - platform: homematic - address: # e.g. "JEQ0XXXXXXX" - name: (optional) - param: (optional) """ import logging @@ -41,14 +33,12 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMSensor, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMSensor, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMSensor, + discovery_info, + add_callback_devices) class HMSensor(homematic.HMDevice): diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index 16cc63a6708ad9..3e6a83afcc4d5a 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -6,14 +6,6 @@ Important: For this platform to work the homematic component has to be properly configured. - -Configuration: - -switch: - - platform: homematic - address: # e.g. "JEQ0XXXXXXX" - name: (optional) - button: n (integer of channel to map, device-dependent) (optional) """ import logging @@ -28,14 +20,12 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMSwitch, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMSwitch, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMSwitch, + discovery_info, + add_callback_devices) class HMSwitch(homematic.HMDevice, SwitchDevice): diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index d7675a5cd472c3..3a537792522717 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -6,13 +6,6 @@ Important: For this platform to work the homematic component has to be properly configured. - -Configuration: - -thermostat: - - platform: homematic - address: "" # e.g. "JEQ0XXXXXXX" - name: "" (optional) """ import logging @@ -28,14 +21,12 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMThermostat, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMThermostat, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMThermostat, + discovery_info, + add_callback_devices) # pylint: disable=abstract-method From 31d2a5d2d1b72af3e8876fa4c20001e3084b5ccd Mon Sep 17 00:00:00 2001 From: AlucardZero Date: Tue, 28 Jun 2016 19:48:25 -0400 Subject: [PATCH 65/79] Reenable TLS1.1 and 1.2 while leaving SSLv3 disabled (#2385) --- homeassistant/components/http.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 218c202bcc2513..d170f2a713e2d2 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -40,7 +40,8 @@ # TLS configuation follows the best-practice guidelines # specified here: https://wiki.mozilla.org/Security/Server_Side_TLS # Intermediate guidelines are followed. -SSL_VERSION = ssl.PROTOCOL_TLSv1 +SSL_VERSION = ssl.PROTOCOL_SSLv23 +SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_COMPRESSION CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ @@ -312,9 +313,11 @@ def start(self): sock = eventlet.listen((self.server_host, self.server_port)) if self.ssl_certificate: - sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, - keyfile=self.ssl_key, server_side=True, - ssl_version=SSL_VERSION, ciphers=CIPHERS) + context = ssl.SSLContext(SSL_VERSION) + context.options |= SSL_OPTS + context.set_ciphers(CIPHERS) + context.load_cert_chain(self.ssl_certificate, self.ssl_key) + sock = context.wrap_socket(sock, server_side=True) wsgi.server(sock, self, log=_LOGGER) def dispatch_request(self, request): From 78e7e1748474f8dfb6cf8bdd64a8f333f25ec06c Mon Sep 17 00:00:00 2001 From: Ardetus Date: Wed, 29 Jun 2016 04:39:16 +0300 Subject: [PATCH 66/79] Support more types of 1wire sensors and bus masters (#2384) * Support more types of 1wire sensors and bus masters - Added support for DS18S20, DS1822, DS1825 and DS28EA00 temperature sensors - Added support for bus masters which use fuse to mount device tree. Mount can be specified by 'mount_dir' configuration parameter. * Correct the lint problem --- homeassistant/components/sensor/onewire.py | 60 ++++++++++++++-------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 6941fc952a6d5d..a2a3f0811f2d0e 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -12,21 +12,24 @@ from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -BASE_DIR = '/sys/bus/w1/devices/' -DEVICE_FOLDERS = glob(os.path.join(BASE_DIR, '28*')) -SENSOR_IDS = [] -DEVICE_FILES = [] -for device_folder in DEVICE_FOLDERS: - SENSOR_IDS.append(os.path.split(device_folder)[1]) - DEVICE_FILES.append(os.path.join(device_folder, 'w1_slave')) - _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the one wire Sensors.""" - if DEVICE_FILES == []: + base_dir = config.get('mount_dir', '/sys/bus/w1/devices/') + device_folders = glob(os.path.join(base_dir, '[10,22,28,3B,42]*')) + sensor_ids = [] + device_files = [] + for device_folder in device_folders: + sensor_ids.append(os.path.split(device_folder)[1]) + if base_dir.startswith('/sys/bus/w1/devices'): + device_files.append(os.path.join(device_folder, 'w1_slave')) + else: + device_files.append(os.path.join(device_folder, 'temperature')) + + if device_files == []: _LOGGER.error('No onewire sensor found.') _LOGGER.error('Check if dtoverlay=w1-gpio,gpiopin=4.') _LOGGER.error('is in your /boot/config.txt and') @@ -34,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devs = [] - names = SENSOR_IDS + names = sensor_ids for key in config.keys(): if key == "names": @@ -47,9 +50,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # map names to ids. elif isinstance(config['names'], dict): names = [] - for sensor_id in SENSOR_IDS: + for sensor_id in sensor_ids: names.append(config['names'].get(sensor_id, sensor_id)) - for device_file, name in zip(DEVICE_FILES, names): + for device_file, name in zip(device_files, names): devs.append(OneWire(name, device_file)) add_devices(devs) @@ -88,14 +91,27 @@ def unit_of_measurement(self): def update(self): """Get the latest data from the device.""" - lines = self._read_temp_raw() - while lines[0].strip()[-3:] != 'YES': - time.sleep(0.2) + temp = -99 + if self._device_file.startswith('/sys/bus/w1/devices'): lines = self._read_temp_raw() - equals_pos = lines[1].find('t=') - if equals_pos != -1: - temp_string = lines[1][equals_pos+2:] - temp = round(float(temp_string) / 1000.0, 1) - if temp < -55 or temp > 125: - return - self._state = temp + while lines[0].strip()[-3:] != 'YES': + time.sleep(0.2) + lines = self._read_temp_raw() + equals_pos = lines[1].find('t=') + if equals_pos != -1: + temp_string = lines[1][equals_pos+2:] + temp = round(float(temp_string) / 1000.0, 1) + else: + ds_device_file = open(self._device_file, 'r') + temp_read = ds_device_file.readlines() + ds_device_file.close() + if len(temp_read) == 1: + try: + temp = round(float(temp_read[0]), 1) + except ValueError: + _LOGGER.warning('Invalid temperature value read from ' + + self._device_file) + + if temp < -55 or temp > 125: + return + self._state = temp From 3c5c018e3e1f9950bb35b847b19a8090577e9eae Mon Sep 17 00:00:00 2001 From: Brent Date: Tue, 28 Jun 2016 22:26:37 -0500 Subject: [PATCH 67/79] =?UTF-8?q?Fixed=20issue=20with=20roku=20timeouts=20?= =?UTF-8?q?throwing=20exceptions=20when=20roku=20losses=20n=E2=80=A6=20(#2?= =?UTF-8?q?386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed issue with roku timeouts throwing exceptions when roku losses networking * Fixed pylint errors --- homeassistant/components/media_player/roku.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index cd16dc4a620ddf..98372a3f65d85a 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -77,7 +77,8 @@ def update(self): self.current_app = self.roku.current_app else: self.current_app = None - except requests.exceptions.ConnectionError: + except (requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout): self.current_app = None def get_source_list(self): From bb0f484caf15d3001c711e14034ca185bfef9cfa Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 29 Jun 2016 22:42:35 +0200 Subject: [PATCH 68/79] update pyhomematic and homematic use now events from HA for remotes --- .../components/binary_sensor/homematic.py | 37 +---- homeassistant/components/homematic.py | 150 ++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 109 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 5452229ee541c7..1cd0d87c13b195 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -27,11 +27,6 @@ "RemoteMotion": None } -SUPPORT_HM_EVENT_AS_BINMOD = [ - "PRESS_LONG", - "PRESS_SHORT" -] - def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" @@ -78,20 +73,17 @@ def _check_hm_to_ha_object(self): _LOGGER.critical("This %s can't be use as binary!", self._name) return False - # load possible binary sensor - available_bin = self._create_binary_list_from_hm() - # if exists user value? - if self._state and self._state not in available_bin: + if self._state and self._state not in self._hmdevice.BINARYNODE: _LOGGER.critical("This %s have no binary with %s!", self._name, self._state) return False # only check and give a warining to User - if self._state is None and len(available_bin) > 1: + if self._state is None and len(self._hmdevice.BINARYNODE) > 1: _LOGGER.critical("%s have multible binary params. It use all " + "binary nodes as one. Possible param values: %s", - self._name, str(available_bin)) + self._name, str(self._hmdevice.BINARYNODE)) return False return True @@ -100,12 +92,9 @@ def _init_data_struct(self): """Generate a data struct (self._data) from hm metadata.""" super()._init_data_struct() - # load possible binary sensor - available_bin = self._create_binary_list_from_hm() - # object have 1 binary - if self._state is None and len(available_bin) == 1: - for value in available_bin: + if self._state is None and len(self._hmdevice.BINARYNODE) == 1: + for value in self._hmdevice.BINARYNODE: self._state = value # add state to data struct @@ -113,19 +102,3 @@ def _init_data_struct(self): _LOGGER.debug("%s init datastruct with main node '%s'", self._name, self._state) self._data.update({self._state: STATE_UNKNOWN}) - - def _create_binary_list_from_hm(self): - """Generate a own metadata for binary_sensors.""" - bin_data = {} - if not self._hmdevice: - return bin_data - - # copy all data from BINARYNODE - bin_data.update(self._hmdevice.BINARYNODE) - - # copy all hm event they are supportet by this object - for event, channel in self._hmdevice.EVENTNODE.items(): - if event in SUPPORT_HM_EVENT_AS_BINMOD: - bin_data.update({event: channel}) - - return bin_data diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index a0a31566661117..a5261b20a2f24c 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -20,11 +20,10 @@ from homeassistant.helpers.entity import Entity DOMAIN = 'homematic' -REQUIREMENTS = ['pyhomematic==0.1.6'] +REQUIREMENTS = ['pyhomematic==0.1.8'] HOMEMATIC = None HOMEMATIC_LINK_DELAY = 0.5 -HOMEMATIC_DEVICES = {} DISCOVER_SWITCHES = "homematic.switch" DISCOVER_LIGHTS = "homematic.light" @@ -34,18 +33,22 @@ DISCOVER_THERMOSTATS = "homematic.thermostat" ATTR_DISCOVER_DEVICES = "devices" -ATTR_DISCOVER_CONFIG = "config" +ATTR_PARAM = "param" +ATTR_CHANNEL = "channel" +ATTR_NAME = "name" +ATTR_ADDRESS = "address" + +EVENT_KEYPRESS = "homematic.keypress" HM_DEVICE_TYPES = { DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"], DISCOVER_LIGHTS: ["Dimmer"], DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", "RemoteMotion", "ThermostatWall", "AreaThermostat", - "RotaryHandleSensor"], + "RotaryHandleSensor", "WaterSensor"], DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], - DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", - "Motion", "MotionV2", "RemoteMotion", - "GongSensor"], + DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2", + "Motion", "MotionV2", "RemoteMotion"], DISCOVER_ROLLERSHUTTER: ["Blind"] } @@ -65,6 +68,13 @@ "VOLTAGE": ["Voltage", {}] } +HM_PRESS_EVENTS = [ + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE" +] + _LOGGER = logging.getLogger(__name__) @@ -80,6 +90,8 @@ def setup(hass, config): remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) resolvenames = config[DOMAIN].get("resolvenames", False) + username = config[DOMAIN].get("username", "Admin") + password = config[DOMAIN].get("password", "") HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5) if remote_ip is None or local_ip is None: @@ -88,12 +100,15 @@ def setup(hass, config): # Create server thread bound_system_callback = partial(system_callback_handler, hass, config) + # pylint: disable=unexpected-keyword-arg HOMEMATIC = HMConnection(local=local_ip, localport=local_port, remote=remote_ip, remoteport=remote_port, systemcallback=bound_system_callback, resolvenames=resolvenames, + rpcusername=username, + rpcpassword=password, interface_id="homeassistant") # Start server thread, connect to peer, initialize to receive events @@ -118,6 +133,20 @@ def system_callback_handler(hass, config, src, *args): for dev in dev_descriptions: key_dict[dev['ADDRESS'].split(':')[0]] = True + # Register EVENTS + # Search all device with a EVENTNODE that include data + bound_event_callback = partial(_hm_event_handler, hass) + for dev in key_dict: + if dev not in HOMEMATIC.devices: + continue + + hmdevice = HOMEMATIC.devices.get(dev) + # have events? + if len(hmdevice.EVENTNODE) > 0: + _LOGGER.debug("Register Events from %s", dev) + hmdevice.setEventCallback(callback=bound_event_callback, + bequeath=True) + # If configuration allows autodetection of devices, # all devices not configured are added. if key_dict: @@ -142,29 +171,24 @@ def system_callback_handler(hass, config, src, *args): def _get_devices(device_type, keys): """Get devices.""" - from homeassistant.components.binary_sensor.homematic import \ - SUPPORT_HM_EVENT_AS_BINMOD - # run device_arr = [] for key in keys: device = HOMEMATIC.devices[key] - if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: - continue + class_name = device.__class__.__name__ metadata = {} + # is class supported by discovery type + if class_name not in HM_DEVICE_TYPES[device_type]: + continue + # Load metadata if needed to generate a param list if device_type == DISCOVER_SENSORS: metadata.update(device.SENSORNODE) elif device_type == DISCOVER_BINARY_SENSORS: metadata.update(device.BINARYNODE) - # Also add supported events as binary type - for event, channel in device.EVENTNODE.items(): - if event in SUPPORT_HM_EVENT_AS_BINMOD: - metadata.update({event: channel}) - - params = _create_params_list(device, metadata) + params = _create_params_list(device, metadata, device_type) if params: # Generate options for 1...n elements with 1...n params for channel in range(1, device.ELEMENT + 1): @@ -177,9 +201,9 @@ def _get_devices(device_type, keys): device_dict = dict(platform="homematic", address=key, name=name, - button=channel) + channel=channel) if param is not None: - device_dict["param"] = param + device_dict[ATTR_PARAM] = param # Add new device device_arr.append(device_dict) @@ -192,15 +216,22 @@ def _get_devices(device_type, keys): return device_arr -def _create_params_list(hmdevice, metadata): +def _create_params_list(hmdevice, metadata, device_type): """Create a list from HMDevice with all possible parameters in config.""" params = {} + merge = False + + # use merge? + if device_type == DISCOVER_SENSORS: + merge = True + elif device_type == DISCOVER_BINARY_SENSORS: + merge = True # Search in sensor and binary metadata per elements for channel in range(1, hmdevice.ELEMENT + 1): param_chan = [] - try: - for node, meta_chan in metadata.items(): + for node, meta_chan in metadata.items(): + try: # Is this attribute ignored? if node in HM_IGNORE_DISCOVERY_NODE: continue @@ -210,15 +241,17 @@ def _create_params_list(hmdevice, metadata): elif channel == 1: # First channel can have other data channel param_chan.append(node) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Exception generating %s (%s): %s", - hmdevice.ADDRESS, str(metadata), str(err)) - # Default parameter - if not param_chan: + except (TypeError, ValueError): + _LOGGER.error("Exception generating %s (%s)", + hmdevice.ADDRESS, str(metadata)) + + # default parameter is merge is off + if len(param_chan) == 0 and not merge: param_chan.append(None) + # Add to channel - params.update({channel: param_chan}) + if len(param_chan) > 0: + params.update({channel: param_chan}) _LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS, str(params)) @@ -247,7 +280,7 @@ def _create_ha_name(name, channel, param): def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, add_callback_devices): """Helper to setup Homematic devices with discovery info.""" - for config in discovery_info["devices"]: + for config in discovery_info[ATTR_DISCOVER_DEVICES]: _LOGGER.debug("Add device %s from config: %s", str(hmdevicetype), str(config)) @@ -261,16 +294,41 @@ def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, return True +def _hm_event_handler(hass, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + channel = device.split(":")[1] + address = device.split(":")[0] + hmdevice = HOMEMATIC.devices.get(address) + + # is not a event? + if attribute not in hmdevice.EVENTNODE: + return + + _LOGGER.debug("Event %s for %s channel %s", attribute, + hmdevice.NAME, channel) + + # a keypress event + if attribute in HM_PRESS_EVENTS: + hass.bus.fire(EVENT_KEYPRESS, { + ATTR_NAME: hmdevice.NAME, + ATTR_PARAM: attribute, + ATTR_CHANNEL: channel + }) + return + + _LOGGER.warning("Event is unknown and not forwarded to HA") + + class HMDevice(Entity): """Homematic device base object.""" # pylint: disable=too-many-instance-attributes def __init__(self, config): """Initialize generic HM device.""" - self._name = config.get("name", None) - self._address = config.get("address", None) - self._channel = config.get("button", 1) - self._state = config.get("param", None) + self._name = config.get(ATTR_NAME, None) + self._address = config.get(ATTR_ADDRESS, None) + self._channel = config.get(ATTR_CHANNEL, 1) + self._state = config.get(ATTR_PARAM, None) self._data = {} self._hmdevice = None self._connected = False @@ -311,6 +369,10 @@ def device_state_attributes(self): """Return device specific state attributes.""" attr = {} + # no data available to create + if not self.available: + return attr + # Generate an attributes list for node, data in HM_ATTRIBUTE_SUPPORT.items(): # Is an attributes and exists for this object @@ -318,6 +380,9 @@ def device_state_attributes(self): value = data[1].get(self._data[node], self._data[node]) attr[data[0]] = value + # static attributes + attr["ID"] = self._hmdevice.ADDRESS + return attr def link_homematic(self): @@ -382,18 +447,12 @@ def _hm_event_callback(self, device, caller, attribute, value): self._available = bool(value) have_change = True - # If it has changed, update HA + # If it has changed data point, update HA if have_change: _LOGGER.debug("%s update_ha_state after '%s'", self._name, attribute) self.update_ha_state() - # Reset events - if attribute in self._hmdevice.EVENTNODE: - _LOGGER.debug("%s reset event", self._name) - self._data[attribute] = False - self.update_ha_state() - def _subscribe_homematic_events(self): """Subscribe all required events to handle job.""" channels_to_sub = {} @@ -441,18 +500,15 @@ def _load_init_data_from_hm(self): if node in self._data: self._data[node] = funct(name=node, channel=self._channel) - # Set events to False - for node in self._hmdevice.EVENTNODE: - if node in self._data: - self._data[node] = False - return True def _hm_set_state(self, value): + """Set data to main datapoint.""" if self._state in self._data: self._data[self._state] = value def _hm_get_state(self): + """Get data from main datapoint.""" if self._state in self._data: return self._data[self._state] return None diff --git a/requirements_all.txt b/requirements_all.txt index 2be4536345de5f..88a1bed04fd6d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -258,7 +258,7 @@ pyenvisalink==1.0 pyfttt==0.3 # homeassistant.components.homematic -pyhomematic==0.1.6 +pyhomematic==0.1.8 # homeassistant.components.device_tracker.icloud pyicloud==0.8.3 From 6a816116ab3124d662560f8d7125ded20f25d2bc Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 29 Jun 2016 17:16:53 -0400 Subject: [PATCH 69/79] Wink subscription support (#2324) --- .../components/binary_sensor/wink.py | 44 ++-------- homeassistant/components/garage_door/wink.py | 42 ++-------- homeassistant/components/light/wink.py | 32 ++----- homeassistant/components/lock/wink.py | 42 ++-------- .../components/rollershutter/wink.py | 27 +----- homeassistant/components/sensor/wink.py | 83 +++---------------- homeassistant/components/switch/wink.py | 26 +++++- homeassistant/components/wink.py | 56 +++++++++---- requirements_all.txt | 12 ++- 9 files changed, 111 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index d9c2b7d577a367..9ec85e63503d7e 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -7,10 +7,12 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.components.sensor.wink import WinkDevice +from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.entity import Entity +from homeassistant.loader import get_component -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { @@ -41,14 +43,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([WinkBinarySensorDevice(sensor)]) -class WinkBinarySensorDevice(BinarySensorDevice, Entity): +class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): """Representation of a Wink sensor.""" def __init__(self, wink): """Initialize the Wink binary sensor.""" - self.wink = wink + super().__init__(wink) + wink = get_component('wink') self._unit_of_measurement = self.wink.UNIT - self._battery = self.wink.battery_level self.capability = self.wink.capability() @property @@ -67,35 +69,3 @@ def is_on(self): def sensor_class(self): """Return the class of this sensor, from SENSOR_CLASSES.""" return SENSOR_TYPES.get(self.capability) - - @property - def unique_id(self): - """Return the ID of this wink sensor.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the sensor if any.""" - return self.wink.name() - - @property - def available(self): - """True if connection == True.""" - return self.wink.available - - def update(self): - """Update state of the sensor.""" - self.wink.update_state() - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py index 18ec6f2ba565e4..73692290f50292 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -7,9 +7,10 @@ import logging from homeassistant.components.garage_door import GarageDoorDevice -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.components.wink import WinkDevice +from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.get_garage_doors()) -class WinkGarageDoorDevice(GarageDoorDevice): +class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice): """Representation of a Wink garage door.""" def __init__(self, wink): """Initialize the garage door.""" - self.wink = wink - self._battery = self.wink.battery_level - - @property - def unique_id(self): - """Return the ID of this wink garage door.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the garage door if any.""" - return self.wink.name() - - def update(self): - """Update the state of the garage door.""" - self.wink.update_state() + WinkDevice.__init__(self, wink) @property def is_closed(self): """Return true if door is closed.""" return self.wink.state() == 0 - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def close_door(self): """Close the door.""" self.wink.set_state(0) @@ -70,16 +51,3 @@ def close_door(self): def open_door(self): """Open the door.""" self.wink.set_state(1) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 2438cdaab9abd4..5fdec96f5d4fbb 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -8,12 +8,13 @@ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ Light, ATTR_RGB_COLOR +from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -35,26 +36,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): WinkLight(light) for light in pywink.get_bulbs()) -class WinkLight(Light): +class WinkLight(WinkDevice, Light): """Representation of a Wink light.""" def __init__(self, wink): - """ - Initialize the light. - - :type wink: pywink.devices.standard.bulb.WinkBulb - """ - self.wink = wink - - @property - def unique_id(self): - """Return the ID of this Wink light.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the light if any.""" - return self.wink.name() + """Initialize the Wink device.""" + WinkDevice.__init__(self, wink) @property def is_on(self): @@ -66,11 +53,6 @@ def brightness(self): """Return the brightness of the light.""" return int(self.wink.brightness() * 255) - @property - def available(self): - """True if connection == True.""" - return self.wink.available - @property def xy_color(self): """Current bulb color in CIE 1931 (XY) color space.""" @@ -112,7 +94,3 @@ def turn_on(self, **kwargs): def turn_off(self): """Turn the switch off.""" self.wink.set_state(False) - - def update(self): - """Update state of the light.""" - self.wink.update_state(require_desired_state_fulfilled=True) diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 2572796df35a79..7551302499a482 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -7,9 +7,10 @@ import logging from homeassistant.components.lock import LockDevice -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.components.wink import WinkDevice +from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -30,38 +31,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) -class WinkLockDevice(LockDevice): +class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" def __init__(self, wink): """Initialize the lock.""" - self.wink = wink - self._battery = self.wink.battery_level - - @property - def unique_id(self): - """Return the id of this wink lock.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the lock if any.""" - return self.wink.name() - - def update(self): - """Update the state of the lock.""" - self.wink.update_state() + WinkDevice.__init__(self, wink) @property def is_locked(self): """Return true if device is locked.""" return self.wink.state() - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def lock(self, **kwargs): """Lock the device.""" self.wink.set_state(True) @@ -69,16 +50,3 @@ def lock(self, **kwargs): def unlock(self, **kwargs): """Unlock the device.""" self.wink.set_state(False) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/rollershutter/wink.py b/homeassistant/components/rollershutter/wink.py index e01b2573ac64be..8a31148da01b06 100644 --- a/homeassistant/components/rollershutter/wink.py +++ b/homeassistant/components/rollershutter/wink.py @@ -7,9 +7,10 @@ import logging from homeassistant.components.rollershutter import RollershutterDevice +from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.get_shades()) -class WinkRollershutterDevice(RollershutterDevice): +class WinkRollershutterDevice(WinkDevice, RollershutterDevice): """Representation of a Wink rollershutter (shades).""" def __init__(self, wink): """Initialize the rollershutter.""" - self.wink = wink - self._battery = None + WinkDevice.__init__(self, wink) @property def should_poll(self): """Wink Shades don't track their position.""" return False - @property - def unique_id(self): - """Return the ID of this wink rollershutter.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the rollershutter if any.""" - return self.wink.name() - - def update(self): - """Update the state of the rollershutter.""" - return self.wink.update_state() - - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def move_down(self): """Close the shade.""" self.wink.set_state(0) diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 3fb914d6cd9e61..ac885152a2edcb 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -7,11 +7,12 @@ import logging from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED, - STATE_OPEN, TEMP_CELSIUS, - ATTR_BATTERY_LEVEL) + STATE_OPEN, TEMP_CELSIUS) from homeassistant.helpers.entity import Entity +from homeassistant.components.wink import WinkDevice +from homeassistant.loader import get_component -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] SENSOR_TYPES = ['temperature', 'humidity'] @@ -38,14 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays()) -class WinkSensorDevice(Entity): +class WinkSensorDevice(WinkDevice, Entity): """Representation of a Wink sensor.""" def __init__(self, wink): - """Initialize the sensor.""" - self.wink = wink + """Initialize the Wink device.""" + super().__init__(wink) + wink = get_component('wink') self.capability = self.wink.capability() - self._battery = self.wink.battery_level if self.wink.UNIT == "°": self._unit_of_measurement = TEMP_CELSIUS else: @@ -55,9 +56,9 @@ def __init__(self, wink): def state(self): """Return the state.""" if self.capability == "humidity": - return self.wink.humidity_percentage() + return round(self.wink.humidity_percentage()) elif self.capability == "temperature": - return self.wink.temperature_float() + return round(self.wink.temperature_float(), 1) else: return STATE_OPEN if self.is_open else STATE_CLOSED @@ -66,80 +67,20 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @property - def unique_id(self): - """Return the ID of this wink sensor.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the sensor if any.""" - return self.wink.name() - - @property - def available(self): - """True if connection == True.""" - return self.wink.available - - def update(self): - """Update state of the sensor.""" - self.wink.update_state() - @property def is_open(self): """Return true if door is open.""" return self.wink.state() - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 - -class WinkEggMinder(Entity): +class WinkEggMinder(WinkDevice, Entity): """Representation of a Wink Egg Minder.""" def __init__(self, wink): """Initialize the sensor.""" - self.wink = wink - self._battery = self.wink.battery_level + WinkDevice.__init__(self, wink) @property def state(self): """Return the state.""" return self.wink.state() - - @property - def unique_id(self): - """Return the id of this wink Egg Minder.""" - return "{}.{}".format(self.__class__, self.wink.device_id()) - - @property - def name(self): - """Return the name of the Egg Minder if any.""" - return self.wink.name() - - def update(self): - """Update state of the Egg Minder.""" - self.wink.update_state() - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._battery: - return { - ATTR_BATTERY_LEVEL: self._battery_level, - } - - @property - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index a5b67f5ddcf0bd..64c19e34bc9c42 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -6,10 +6,11 @@ """ import logging -from homeassistant.components.wink import WinkToggleDevice +from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.entity import ToggleEntity -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -31,3 +32,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkToggleDevice(switch) for switch in pywink.get_powerstrip_outlets()) add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens()) + + +class WinkToggleDevice(WinkDevice, ToggleEntity): + """Represents a Wink toggle (switch) device.""" + + def __init__(self, wink): + """Initialize the Wink device.""" + WinkDevice.__init__(self, wink) + + @property + def is_on(self): + """Return true if device is on.""" + return self.wink.state() + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.wink.set_state(True) + + def turn_off(self): + """Turn the device off.""" + self.wink.set_state(False) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 85bc7f46cefc57..4e9fec77ba51d1 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -5,13 +5,17 @@ https://home-assistant.io/components/wink/ """ import logging +import json -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL from homeassistant.helpers import validate_config, discovery -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] + +SUBSCRIPTION_HANDLER = None +CHANNELS = [] def setup(hass, config): @@ -22,7 +26,11 @@ def setup(hass, config): return False import pywink + from pubnub import Pubnub pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN]) + global SUBSCRIPTION_HANDLER + SUBSCRIPTION_HANDLER = Pubnub("N/A", pywink.get_subscription_key()) + SUBSCRIPTION_HANDLER.set_heartbeat(120) # Load components for the devices in the Wink that we support for component_name, func_exists in ( @@ -41,13 +49,33 @@ def setup(hass, config): return True -class WinkToggleDevice(ToggleEntity): - """Represents a Wink toggle (switch) device.""" +class WinkDevice(Entity): + """Represents a base Wink device.""" def __init__(self, wink): """Initialize the Wink device.""" + from pubnub import Pubnub self.wink = wink self._battery = self.wink.battery_level + if self.wink.pubnub_channel in CHANNELS: + pubnub = Pubnub("N/A", self.wink.pubnub_key) + pubnub.set_heartbeat(120) + pubnub.subscribe(self.wink.pubnub_channel, + self._pubnub_update, + error=self._pubnub_error) + else: + CHANNELS.append(self.wink.pubnub_channel) + SUBSCRIPTION_HANDLER.subscribe(self.wink.pubnub_channel, + self._pubnub_update, + error=self._pubnub_error) + + def _pubnub_update(self, message, channel): + self.wink.pubnub_update(json.loads(message)) + self.update_ha_state() + + def _pubnub_error(self, message): + logging.getLogger(__name__).error( + "Error on pubnub update for " + self.wink.name()) @property def unique_id(self): @@ -59,28 +87,20 @@ def name(self): """Return the name of the device.""" return self.wink.name() - @property - def is_on(self): - """Return true if device is on.""" - return self.wink.state() - @property def available(self): """True if connection == True.""" return self.wink.available - def turn_on(self, **kwargs): - """Turn the device on.""" - self.wink.set_state(True) - - def turn_off(self): - """Turn the device off.""" - self.wink.set_state(False) - def update(self): """Update state of the device.""" self.wink.update_state() + @property + def should_poll(self): + """Only poll if we are not subscribed to pubnub.""" + return self.wink.pubnub_channel is None + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/requirements_all.txt b/requirements_all.txt index 88a1bed04fd6d3..dd97c51185c3b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,6 +220,16 @@ proliphix==0.1.0 # homeassistant.components.sensor.systemmonitor psutil==4.3.0 +# homeassistant.components.wink +# homeassistant.components.binary_sensor.wink +# homeassistant.components.garage_door.wink +# homeassistant.components.light.wink +# homeassistant.components.lock.wink +# homeassistant.components.rollershutter.wink +# homeassistant.components.sensor.wink +# homeassistant.components.switch.wink +pubnub==3.7.8 + # homeassistant.components.notify.pushbullet pushbullet.py==0.10.0 @@ -324,7 +334,7 @@ python-twitch==1.2.0 # homeassistant.components.rollershutter.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.7.7 +python-wink==0.7.8 # homeassistant.components.keyboard pyuserinput==0.1.9 From 5cce02ab621abd4ce84cdf81dc6436fedbebd873 Mon Sep 17 00:00:00 2001 From: rhooper Date: Wed, 29 Jun 2016 20:28:20 -0400 Subject: [PATCH 70/79] vera lock support (#2391) * vera lock support * fix formatting --- homeassistant/components/lock/vera.py | 65 +++++++++++++++++++++++++++ homeassistant/components/vera.py | 6 ++- requirements_all.txt | 2 +- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/lock/vera.py diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py new file mode 100644 index 00000000000000..2b34b209e77776 --- /dev/null +++ b/homeassistant/components/lock/vera.py @@ -0,0 +1,65 @@ +""" +Support for Vera locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.vera/ +""" +import logging + +from homeassistant.components.lock import LockDevice +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.components.vera import ( + VeraDevice, VERA_DEVICES, VERA_CONTROLLER) + +DEPENDENCIES = ['vera'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Find and return Vera switches.""" + add_devices_callback( + VeraLock(device, VERA_CONTROLLER) for + device in VERA_DEVICES['lock']) + + +class VeraLock(VeraDevice, LockDevice): + """Representation of a Vera Lock.""" + + def __init__(self, vera_device, controller): + """Initialize the Vera device.""" + self._state = None + VeraDevice.__init__(self, vera_device, controller) + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + if self.vera_device.has_battery: + attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' + + attr['Vera Device Id'] = self.vera_device.vera_device_id + return attr + + def lock(self, **kwargs): + """Lock.""" + self.vera_device.lock() + self._state = STATE_LOCKED + self.update_ha_state() + + def unlock(self, **kwargs): + """Unlock.""" + self.vera_device.unlock() + self._state = STATE_UNLOCKED + self.update_ha_state() + + @property + def is_locked(self): + """Return true if device is on.""" + return self._state == STATE_LOCKED + + def update(self): + """Called by the vera device callback to update state.""" + self._state = (STATE_LOCKED if self.vera_device.is_locked(True) + else STATE_UNLOCKED) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index ee55ec858cc5e7..455927ca9992f8 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -13,12 +13,13 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.10'] +REQUIREMENTS = ['pyvera==0.2.12'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'vera' + VERA_CONTROLLER = None CONF_EXCLUDE = 'exclude' @@ -33,6 +34,7 @@ 'Switch': 'switch', 'Armable Sensor': 'switch', 'On/Off Switch': 'switch', + 'Doorlock': 'lock', # 'Window Covering': NOT SUPPORTED YET } @@ -91,7 +93,7 @@ def stop_subscription(event): dev_type = 'light' VERA_DEVICES[dev_type].append(device) - for component in 'binary_sensor', 'sensor', 'light', 'switch': + for component in 'binary_sensor', 'sensor', 'light', 'switch', 'lock': discovery.load_platform(hass, component, DOMAIN, {}, base_config) return True diff --git a/requirements_all.txt b/requirements_all.txt index dd97c51185c3b3..af332e70fdea28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -340,7 +340,7 @@ python-wink==0.7.8 pyuserinput==0.1.9 # homeassistant.components.vera -pyvera==0.2.10 +pyvera==0.2.12 # homeassistant.components.wemo pywemo==0.4.3 From 8dd7ebb08e77e035127a617c9bfbd6d5a6bb6380 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 30 Jun 2016 02:44:35 +0200 Subject: [PATCH 71/79] Add the two next trains (#2390) --- .../components/sensor/deutsche_bahn.py | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index a38ee76b3bb886..99b96b971c9853 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -1,38 +1,43 @@ """ -Support for information about the German trans system. +Support for information about the German train system. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.deutsche_bahn/ """ import logging -from datetime import timedelta, datetime +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import (CONF_PLATFORM) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['schiene==0.17'] + +CONF_START = 'from' +CONF_DESTINATION = 'to' ICON = 'mdi:train' +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = vol.Schema({ + vol.Required(CONF_PLATFORM): 'deutsche_bahn', + vol.Required(CONF_START): cv.string, + vol.Required(CONF_DESTINATION): cv.string, +}) + # Return cached results if last scan was less then this time ago. MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Deutsche Bahn Sensor.""" - start = config.get('from') - goal = config.get('to') - - if start is None: - _LOGGER.error('Missing required variable: "from"') - return False + start = config.get(CONF_START) + destination = config.get(CONF_DESTINATION) - if goal is None: - _LOGGER.error('Missing required variable: "to"') - return False - - dev = [] - dev.append(DeutscheBahnSensor(start, goal)) - add_devices_callback(dev) + add_devices([DeutscheBahnSensor(start, destination)]) # pylint: disable=too-few-public-methods @@ -63,16 +68,17 @@ def state(self): @property def state_attributes(self): """Return the state attributes.""" - return self.data.connections[0] + connections = self.data.connections[0] + connections['next'] = self.data.connections[1]['departure'] + connections['next_on'] = self.data.connections[2]['departure'] + return connections def update(self): """Get the latest delay from bahn.de and updates the state.""" self.data.update() self._state = self.data.connections[0].get('departure', 'Unknown') if self.data.connections[0]['delay'] != 0: - self._state += " + {}".format( - self.data.connections[0]['delay'] - ) + self._state += " + {}".format(self.data.connections[0]['delay']) # pylint: disable=too-few-public-methods @@ -90,18 +96,15 @@ def __init__(self, start, goal): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the connection data.""" - self.connections = self.schiene.connections(self.start, - self.goal, - datetime.now()) + self.connections = self.schiene.connections(self.start, self.goal) + for con in self.connections: - # Details info is not useful. - # Having a more consistent interface simplifies - # usage of Template sensors later on + # Detail info is not useful. Having a more consistent interface + # simplifies usage of template sensors. if 'details' in con: con.pop('details') - delay = con.get('delay', - {'delay_departure': 0, - 'delay_arrival': 0}) - # IMHO only delay_departure is usefull + delay = con.get('delay', {'delay_departure': 0, + 'delay_arrival': 0}) + # IMHO only delay_departure is useful con['delay'] = delay['delay_departure'] con['ontime'] = con.get('ontime', False) From 419ff18afb987620e2459adfaf5adf42791a192c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 30 Jun 2016 10:33:34 +0200 Subject: [PATCH 72/79] Docstrings (#2395) * Replace switch with lock * Update docstrings * Add link to docs * Add link to docs and update docstrings * Update docstring * Update docstrings and fix typos * Add link to docs * Add link to docs * Add link to docs and update docstrings * Fix link to docs and update docstrings * Remove blank line * Add link to docs --- .../components/binary_sensor/homematic.py | 24 ++++++-------- homeassistant/components/camera/rpi_camera.py | 8 +++-- homeassistant/components/homematic.py | 32 +++++++------------ homeassistant/components/light/enocean.py | 3 +- homeassistant/components/light/homematic.py | 21 +++++------- .../components/light/osramlightify.py | 18 +++-------- homeassistant/components/lock/vera.py | 10 +++--- .../components/media_player/braviatv.py | 13 ++++---- homeassistant/components/media_player/cmus.py | 7 ++-- homeassistant/components/media_player/roku.py | 1 - .../components/media_player/snapcast.py | 1 - .../components/media_player/universal.py | 1 - .../components/sensor/openexchangerates.py | 11 +++++-- .../components/sensor/thinkingcleaner.py | 7 +++- homeassistant/components/switch/homematic.py | 18 ++++------- .../components/switch/thinkingcleaner.py | 7 +++- .../components/switch/wake_on_lan.py | 2 +- .../components/thermostat/eq3btsmart.py | 4 +-- .../components/thermostat/homematic.py | 16 ++++------ 19 files changed, 91 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 1cd0d87c13b195..8e874079ee60cf 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -1,13 +1,9 @@ """ -The homematic binary sensor platform. +Support for Homematic binary sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.homematic/ - -Important: For this platform to work the homematic component has to be -properly configured. """ - import logging from homeassistant.const import STATE_UNKNOWN from homeassistant.components.binary_sensor import BinarySensorDevice @@ -29,7 +25,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the platform.""" + """Setup the Homematic binary sensor platform.""" if discovery_info is None: return @@ -39,11 +35,11 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): - """Represents diverse binary Homematic units in Home Assistant.""" + """Representation of a binary Homematic device.""" @property def is_on(self): - """Return True if switch is on.""" + """Return true if switch is on.""" if not self.available: return False return bool(self._hm_get_state()) @@ -68,20 +64,20 @@ def _check_hm_to_ha_object(self): if not super()._check_hm_to_ha_object(): return False - # check if the homematic device correct for this HA device + # check if the Homematic device correct for this HA device if not isinstance(self._hmdevice, pyHMBinarySensor): - _LOGGER.critical("This %s can't be use as binary!", self._name) + _LOGGER.critical("This %s can't be use as binary", self._name) return False # if exists user value? if self._state and self._state not in self._hmdevice.BINARYNODE: - _LOGGER.critical("This %s have no binary with %s!", self._name, + _LOGGER.critical("This %s have no binary with %s", self._name, self._state) return False - # only check and give a warining to User + # only check and give a warning to the user if self._state is None and len(self._hmdevice.BINARYNODE) > 1: - _LOGGER.critical("%s have multible binary params. It use all " + + _LOGGER.critical("%s have multiple binary params. It use all " "binary nodes as one. Possible param values: %s", self._name, str(self._hmdevice.BINARYNODE)) return False @@ -89,7 +85,7 @@ def _check_hm_to_ha_object(self): return True def _init_data_struct(self): - """Generate a data struct (self._data) from hm metadata.""" + """Generate a data struct (self._data) from the Homematic metadata.""" super()._init_data_struct() # object have 1 binary diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index cda48d1ddfa147..ee67d097286501 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -1,5 +1,9 @@ -"""Camera platform that has a Raspberry Pi camera.""" +""" +Camera platform that has a Raspberry Pi camera. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/camera.rpi_camera/ +""" import os import subprocess import logging @@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RaspberryCamera(Camera): - """Raspberry Pi camera.""" + """Representation of a Raspberry Pi camera.""" def __init__(self, device_info): """Initialize Raspberry Pi camera component.""" diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index a5261b20a2f24c..fb31408bd8260d 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -1,16 +1,8 @@ """ -Support for Homematic Devices. +Support for Homematic devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematic/ - -Configuration: - -homematic: - local_ip: "" - local_port: - remote_ip: "" - remote_port: """ import time import logging @@ -170,7 +162,7 @@ def system_callback_handler(hass, config, src, *args): def _get_devices(device_type, keys): - """Get devices.""" + """Get the Homematic devices.""" # run device_arr = [] for key in keys: @@ -320,11 +312,11 @@ def _hm_event_handler(hass, device, caller, attribute, value): class HMDevice(Entity): - """Homematic device base object.""" + """The Homematic device base object.""" # pylint: disable=too-many-instance-attributes def __init__(self, config): - """Initialize generic HM device.""" + """Initialize a generic Homematic device.""" self._name = config.get(ATTR_NAME, None) self._address = config.get(ATTR_ADDRESS, None) self._channel = config.get(ATTR_CHANNEL, 1) @@ -346,7 +338,7 @@ def __init__(self, config): @property def should_poll(self): - """Return False. Homematic states are pushed by the XML RPC Server.""" + """Return false. Homematic states are pushed by the XML RPC Server.""" return False @property @@ -356,12 +348,12 @@ def name(self): @property def assumed_state(self): - """Return True if unable to access real state of the device.""" + """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.""" + """Return true if device is available.""" return self._available @property @@ -386,7 +378,7 @@ def device_state_attributes(self): return attr def link_homematic(self): - """Connect to homematic.""" + """Connect to Homematic.""" # device is already linked if self._connected: return True @@ -397,7 +389,7 @@ def link_homematic(self): self._hmdevice = HOMEMATIC.devices[self._address] self._connected = True - # Check if HM class is okay for HA class + # Check if Homematic class is okay for HA class _LOGGER.info("Start linking %s to %s", self._address, self._name) if self._check_hm_to_ha_object(): try: @@ -420,7 +412,7 @@ def link_homematic(self): _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) else: - _LOGGER.critical("Delink %s object from HM!", self._name) + _LOGGER.critical("Delink %s object from HM", self._name) self._connected = False # Update HA @@ -514,7 +506,7 @@ def _hm_get_state(self): return None def _check_hm_to_ha_object(self): - """Check if it is possible to use the HM Object as this HA type. + """Check if it is possible to use the Homematic object as this HA type. NEEDS overwrite by inherit! """ @@ -530,7 +522,7 @@ def _check_hm_to_ha_object(self): return True def _init_data_struct(self): - """Generate a data dict (self._data) from hm metadata. + """Generate a data dict (self._data) from the Homematic metadata. NEEDS overwrite by inherit! """ diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/light/enocean.py index adb10a20fda611..2c9db86e662022 100644 --- a/homeassistant/components/light/enocean.py +++ b/homeassistant/components/light/enocean.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.enocean/ """ - import logging import math @@ -86,7 +85,7 @@ def turn_off(self, **kwargs): self._on_state = False def value_changed(self, val): - """Update the internal state of this device in HA.""" + """Update the internal state of this device.""" self._brightness = math.floor(val / 100.0 * 256.0) self._on_state = bool(val != 0) self.update_ha_state() diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index b1f08db0783f37..b7e0328a574207 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -1,13 +1,9 @@ """ -The homematic light platform. +Support for Homematic lighs. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.homematic/ - -Important: For this platform to work the homematic component has to be -properly configured. """ - import logging from homeassistant.components.light import (ATTR_BRIGHTNESS, Light) from homeassistant.const import STATE_UNKNOWN @@ -15,12 +11,11 @@ _LOGGER = logging.getLogger(__name__) -# List of component names (string) your component depends upon. DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the platform.""" + """Setup the Homematic light platform.""" if discovery_info is None: return @@ -30,7 +25,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): class HMLight(homematic.HMDevice, Light): - """Represents a Homematic Light in Home Assistant.""" + """Representation of a Homematic light.""" @property def brightness(self): @@ -45,7 +40,7 @@ def brightness(self): @property def is_on(self): - """Return True if light is on.""" + """Return true if light is on.""" try: return self._hm_get_state() > 0 except TypeError: @@ -68,24 +63,24 @@ def turn_off(self, **kwargs): self._hmdevice.off(self._channel) def _check_hm_to_ha_object(self): - """Check if possible to use the HM Object as this HA type.""" + """Check if possible to use the Homematic object as this HA type.""" from pyhomematic.devicetypes.actors import Dimmer, Switch # Check compatibility from HMDevice if not super()._check_hm_to_ha_object(): return False - # Check if the homematic device is correct for this HA device + # Check if the Homematic device is correct for this HA device if isinstance(self._hmdevice, Switch): return True if isinstance(self._hmdevice, Dimmer): return True - _LOGGER.critical("This %s can't be use as light!", self._name) + _LOGGER.critical("This %s can't be use as light", self._name) return False def _init_data_struct(self): - """Generate a data dict (self._data) from hm metadata.""" + """Generate a data dict (self._data) from the Homematic metadata.""" from pyhomematic.devicetypes.actors import Dimmer, Switch super()._init_data_struct() diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 33c759b21d5013..243d11116da443 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -1,19 +1,9 @@ """ Support for Osram Lightify. -Uses: https://github.com/aneumeier/python-lightify for the Osram light -interface. - -In order to use the platform just add the following to the configuration.yaml: - -light: - platform: osramlightify - host: - -Todo: -Add support for Non RGBW lights. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.osramlightify/ """ - import logging import socket from datetime import timedelta @@ -40,7 +30,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Find and return lights.""" + """Setup Osram Lightify lights.""" import lightify host = config.get(CONF_HOST) if host: @@ -85,7 +75,7 @@ def update_lights(): class OsramLightifyLight(Light): - """Defines an Osram Lightify Light.""" + """Representation of an Osram Lightify Light.""" def __init__(self, light_id, light, update_lights): """Initialize the light.""" diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py index 2b34b209e77776..f10b8857499775 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/lock/vera.py @@ -18,14 +18,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): - """Find and return Vera switches.""" + """Find and return Vera locks.""" add_devices_callback( VeraLock(device, VERA_CONTROLLER) for device in VERA_DEVICES['lock']) class VeraLock(VeraDevice, LockDevice): - """Representation of a Vera Lock.""" + """Representation of a Vera lock.""" def __init__(self, vera_device, controller): """Initialize the Vera device.""" @@ -43,13 +43,13 @@ def device_state_attributes(self): return attr def lock(self, **kwargs): - """Lock.""" + """Lock the device.""" self.vera_device.lock() self._state = STATE_LOCKED self.update_ha_state() def unlock(self, **kwargs): - """Unlock.""" + """Unlock the device.""" self.vera_device.unlock() self._state = STATE_UNLOCKED self.update_ha_state() @@ -60,6 +60,6 @@ def is_locked(self): return self._state == STATE_LOCKED def update(self): - """Called by the vera device callback to update state.""" + """Called by the Vera device callback to update state.""" self._state = (STATE_LOCKED if self.vera_device.is_locked(True) else STATE_UNLOCKED) diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index ea316f5742532a..ef5f7516827f8d 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -1,10 +1,8 @@ """ Support for interface with a Sony Bravia TV. -By Antonio Parraga Navarro - -dedicated to Isabel - +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.braviatv/ """ import logging import os @@ -38,6 +36,7 @@ def _get_mac_address(ip_address): + """Get the MAC address of the device.""" from subprocess import Popen, PIPE pid = Popen(["arp", "-n", ip_address], stdout=PIPE) @@ -48,7 +47,7 @@ def _get_mac_address(ip_address): def _config_from_file(filename, config=None): - """Small configuration file management function.""" + """Create the configuration from a file.""" if config: # We're writing configuration bravia_config = _config_from_file(filename) @@ -104,7 +103,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=too-many-branches def setup_bravia(config, pin, hass, add_devices_callback): - """Setup a sony bravia based on host parameter.""" + """Setup a Sony Bravia TV based on host parameter.""" host = config.get(CONF_HOST) name = config.get(CONF_NAME) if name is None: @@ -176,7 +175,7 @@ class BraviaTVDevice(MediaPlayerDevice): """Representation of a Sony Bravia TV.""" def __init__(self, host, mac, name, pin): - """Initialize the sony bravia device.""" + """Initialize the Sony Bravia device.""" from braviarc import braviarc self._pin = pin diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index 308d659e111665..4726a1fa6a9bd8 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -2,9 +2,8 @@ Support for interacting with and controlling the cmus music player. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/media_player.mpd/ +https://home-assistant.io/components/media_player.cmus/ """ - import logging from homeassistant.components.media_player import ( @@ -25,7 +24,7 @@ def setup_platform(hass, config, add_devices, discover_info=None): - """Setup the Cmus platform.""" + """Setup the CMUS platform.""" from pycmus import exceptions host = config.get(CONF_HOST, None) @@ -44,7 +43,7 @@ def setup_platform(hass, config, add_devices, discover_info=None): class CmusDevice(MediaPlayerDevice): - """Representation of a running cmus.""" + """Representation of a running CMUS.""" # pylint: disable=no-member, too-many-public-methods, abstract-method def __init__(self, server, password, port, name): diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 98372a3f65d85a..6ff1ae1510fc9d 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.roku/ """ - import logging from homeassistant.components.media_player import ( diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 44cdd414da41d1..998490fb9b9be6 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.snapcast/ """ - import logging import socket diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index f5fa8cc486c735..8bfdeebf85d5b2 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,7 +4,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ - import logging # pylint: disable=import-error from copy import copy diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py index f95e5c3623356e..920dfc46a90864 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -1,4 +1,9 @@ -"""Support for openexchangerates.org exchange rates service.""" +""" +Support for openexchangerates.org exchange rates service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.openexchangerates/ +""" from datetime import timedelta import logging import requests @@ -41,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OpenexchangeratesSensor(Entity): - """Implementing the Openexchangerates sensor.""" + """Representation of an Openexchangerates sensor.""" def __init__(self, rest, name, quote): """Initialize the sensor.""" @@ -87,7 +92,7 @@ def __init__(self, resource, api_key, base, quote, data): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Get the latest data from openexchangerates.""" + """Get the latest data from openexchangerates.org.""" try: result = requests.get(self._resource, params={'base': self._base, 'app_id': diff --git a/homeassistant/components/sensor/thinkingcleaner.py b/homeassistant/components/sensor/thinkingcleaner.py index 1ba8593650ea2d..f956ec5037f6db 100644 --- a/homeassistant/components/sensor/thinkingcleaner.py +++ b/homeassistant/components/sensor/thinkingcleaner.py @@ -1,4 +1,9 @@ -"""Support for ThinkingCleaner.""" +""" +Support for ThinkingCleaner. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.thinkingcleaner/ +""" import logging from datetime import timedelta diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index 3e6a83afcc4d5a..e9f103b95fa88c 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -1,13 +1,9 @@ """ -The homematic switch platform. +Support for Homematic switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.homematic/ - -Important: For this platform to work the homematic component has to be -properly configured. """ - import logging from homeassistant.components.switch import SwitchDevice from homeassistant.const import STATE_UNKNOWN @@ -19,7 +15,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the platform.""" + """Setup the Homematic switch platform.""" if discovery_info is None: return @@ -29,7 +25,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): class HMSwitch(homematic.HMDevice, SwitchDevice): - """Represents a Homematic Switch in Home Assistant.""" + """Representation of a Homematic switch.""" @property def is_on(self): @@ -61,24 +57,24 @@ def turn_off(self, **kwargs): self._hmdevice.off(self._channel) def _check_hm_to_ha_object(self): - """Check if possible to use the HM Object as this HA type.""" + """Check if possible to use the Homematic object as this HA type.""" from pyhomematic.devicetypes.actors import Dimmer, Switch # Check compatibility from HMDevice if not super()._check_hm_to_ha_object(): return False - # Check if the homematic device is correct for this HA device + # Check if the Homematic device is correct for this HA device if isinstance(self._hmdevice, Switch): return True if isinstance(self._hmdevice, Dimmer): return True - _LOGGER.critical("This %s can't be use as switch!", self._name) + _LOGGER.critical("This %s can't be use as switch", self._name) return False def _init_data_struct(self): - """Generate a data dict (self._data) from hm metadata.""" + """Generate a data dict (self._data) from the Homematic metadata.""" from pyhomematic.devicetypes.actors import Dimmer,\ Switch, SwitchPowermeter diff --git a/homeassistant/components/switch/thinkingcleaner.py b/homeassistant/components/switch/thinkingcleaner.py index 3bc4484db3880e..46adc5a70524fd 100644 --- a/homeassistant/components/switch/thinkingcleaner.py +++ b/homeassistant/components/switch/thinkingcleaner.py @@ -1,4 +1,9 @@ -"""Support for ThinkingCleaner.""" +""" +Support for ThinkingCleaner. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.thinkingcleaner/ +""" import time import logging from datetime import timedelta diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index 28a44249e122f6..779f4759442596 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -52,7 +52,7 @@ def should_poll(self): @property def is_on(self): - """True if switch is on.""" + """Return true if switch is on.""" return self._state @property diff --git a/homeassistant/components/thermostat/eq3btsmart.py b/homeassistant/components/thermostat/eq3btsmart.py index a32a64e81a37a8..17f166a297e78b 100644 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ b/homeassistant/components/thermostat/eq3btsmart.py @@ -1,9 +1,9 @@ """ Support for eq3 Bluetooth Smart thermostats. -Uses bluepy_devices library. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/thermostat.eq3btsmart/ """ - import logging from homeassistant.components.thermostat import ThermostatDevice diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index 3a537792522717..345b8785b4262e 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -1,13 +1,9 @@ """ -The Homematic thermostat platform. +Support for Homematic thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.homematic/ - -Important: For this platform to work the homematic component has to be -properly configured. """ - import logging import homeassistant.components.homematic as homematic from homeassistant.components.thermostat import ThermostatDevice @@ -20,7 +16,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the platform.""" + """Setup the Homematic thermostat platform.""" if discovery_info is None: return @@ -31,7 +27,7 @@ def setup_platform(hass, config, add_callback_devices, discovery_info=None): # pylint: disable=abstract-method class HMThermostat(homematic.HMDevice, ThermostatDevice): - """Represents a Homematic Thermostat in Home Assistant.""" + """Representation of a Homematic thermostat.""" @property def unit_of_measurement(self): @@ -69,7 +65,7 @@ def max_temp(self): return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) def _check_hm_to_ha_object(self): - """Check if possible to use the HM Object as this HA type.""" + """Check if possible to use the Homematic object as this HA type.""" from pyhomematic.devicetypes.thermostats import HMThermostat\ as pyHMThermostat @@ -77,7 +73,7 @@ def _check_hm_to_ha_object(self): if not super()._check_hm_to_ha_object(): return False - # Check if the homematic device correct for this HA device + # Check if the Homematic device correct for this HA device if isinstance(self._hmdevice, pyHMThermostat): return True @@ -85,7 +81,7 @@ def _check_hm_to_ha_object(self): return False def _init_data_struct(self): - """Generate a data dict (self._data) from hm metadata.""" + """Generate a data dict (self._data) from the Homematic metadata.""" super()._init_data_struct() # Add state to data dict From 7582eb9f63ffde92663c83dea31ddaabdf39820f Mon Sep 17 00:00:00 2001 From: patkap Date: Thu, 30 Jun 2016 17:40:01 +0200 Subject: [PATCH 73/79] jsonrpc-request version bump (0.3) (#2397) --- homeassistant/components/media_player/kodi.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 432ff73c3671aa..2a14af969fb5da 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -15,7 +15,7 @@ STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['jsonrpc-requests==0.2'] +REQUIREMENTS = ['jsonrpc-requests==0.3'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ diff --git a/requirements_all.txt b/requirements_all.txt index af332e70fdea28..7ee914b24871e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ influxdb==3.0.0 insteon_hub==0.4.5 # homeassistant.components.media_player.kodi -jsonrpc-requests==0.2 +jsonrpc-requests==0.3 # homeassistant.components.light.lifx liffylights==0.9.4 From d1f4901d537e85a5efa68800016e83402b147724 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jun 2016 09:02:12 -0700 Subject: [PATCH 74/79] Migrate to cherrypy wsgi from eventlet (#2387) --- homeassistant/components/api.py | 37 ++--- homeassistant/components/camera/__init__.py | 5 +- homeassistant/components/http.py | 54 ++++-- homeassistant/core.py | 24 +++ homeassistant/remote.py | 15 +- requirements_all.txt | 7 +- setup.py | 1 - .../device_tracker/test_locative.py | 9 +- tests/components/test_alexa.py | 7 +- tests/components/test_api.py | 154 +++++++++--------- tests/components/test_frontend.py | 7 +- tests/components/test_http.py | 9 +- tests/test_remote.py | 18 +- 13 files changed, 181 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b538a62d0082aa..f0073bad838c24 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -6,7 +6,7 @@ """ import json import logging -from time import time +import queue import homeassistant.core as ha import homeassistant.remote as rem @@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView): def get(self, request): """Provide a streaming interface for the event bus.""" - from eventlet.queue import LightQueue, Empty - import eventlet - - cur_hub = eventlet.hubs.get_hub() - request.environ['eventlet.minimum_write_chunk_size'] = 0 - to_write = LightQueue() stop_obj = object() + to_write = queue.Queue() restrict = request.args.get('restrict') if restrict: - restrict = restrict.split(',') + restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] - def thread_forward_events(event): + def forward_events(event): """Forward events to the open request.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -99,28 +94,20 @@ def thread_forward_events(event): else: data = json.dumps(event, cls=rem.JSONEncoder) - cur_hub.schedule_call_global(0, lambda: to_write.put(data)) + to_write.put(data) def stream(): """Stream events to response.""" - self.hass.bus.listen(MATCH_ALL, thread_forward_events) + self.hass.bus.listen(MATCH_ALL, forward_events) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) - last_msg = time() # Fire off one message right away to have browsers fire open event to_write.put(STREAM_PING_PAYLOAD) while True: try: - # Somehow our queue.get sometimes takes too long to - # be notified of arrival of data. Probably - # because of our spawning on hub in other thread - # hack. Because current goal is to get this out, - # We just timeout every second because it will - # return right away if qsize() > 0. - # So yes, we're basically polling :( - payload = to_write.get(timeout=1) + payload = to_write.get(timeout=STREAM_PING_INTERVAL) if payload is stop_obj: break @@ -129,15 +116,13 @@ def stream(): _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), msg.strip()) yield msg.encode("UTF-8") - last_msg = time() - except Empty: - if time() - last_msg > 50: - to_write.put(STREAM_PING_PAYLOAD) + except queue.Empty: + to_write.put(STREAM_PING_PAYLOAD) except GeneratorExit: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) break - self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events) + _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + self.hass.bus.remove_listener(MATCH_ALL, forward_events) return self.Response(stream(), mimetype='text/event-stream') diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 873425289874b8..2f23118a1c3006 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/camera/ """ import logging +import time from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -81,8 +82,6 @@ def camera_image(self): def mjpeg_stream(self, response): """Generate an HTTP MJPEG stream from camera images.""" - import eventlet - def stream(): """Stream images as mjpeg stream.""" try: @@ -99,7 +98,7 @@ def stream(): last_image = img_bytes - eventlet.sleep(0.5) + time.sleep(0.5) except GeneratorExit: pass diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index d170f2a713e2d2..11aa18cad5c5aa 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -13,19 +13,19 @@ import ssl import voluptuous as vol -import homeassistant.core as ha import homeassistant.remote as rem from homeassistant import util from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, - HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS) + HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS, + EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) from homeassistant.helpers.entity import split_entity_id import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv DOMAIN = "http" -REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.10") +REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10") CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" @@ -118,11 +118,17 @@ def setup(hass, config): cors_origins=cors_origins ) - hass.bus.listen_once( - ha.EVENT_HOMEASSISTANT_START, - lambda event: - threading.Thread(target=server.start, daemon=True, - name='WSGI-server').start()) + def start_wsgi_server(event): + """Start the WSGI server.""" + server.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server) + + def stop_wsgi_server(event): + """Stop the WSGI server.""" + server.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server) hass.wsgi = server hass.config.api = rem.API(server_host if server_host != '0.0.0.0' @@ -241,6 +247,7 @@ def __init__(self, hass, development, api_password, ssl_certificate, self.server_port = server_port self.cors_origins = cors_origins self.event_forwarder = None + self.server = None def register_view(self, view): """Register a view with the WSGI server. @@ -308,17 +315,34 @@ def register_wsgi_app(self, url_root, app): def start(self): """Start the wsgi server.""" - from eventlet import wsgi - import eventlet + from cherrypy import wsgiserver + from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter + + # pylint: disable=too-few-public-methods,super-init-not-called + class ContextSSLAdapter(BuiltinSSLAdapter): + """SSL Adapter that takes in an SSL context.""" + + def __init__(self, context): + self.context = context + + # pylint: disable=no-member + self.server = wsgiserver.CherryPyWSGIServer( + (self.server_host, self.server_port), self, + server_name='Home Assistant') - sock = eventlet.listen((self.server_host, self.server_port)) if self.ssl_certificate: context = ssl.SSLContext(SSL_VERSION) context.options |= SSL_OPTS context.set_ciphers(CIPHERS) context.load_cert_chain(self.ssl_certificate, self.ssl_key) - sock = context.wrap_socket(sock, server_side=True) - wsgi.server(sock, self, log=_LOGGER) + self.server.ssl_adapter = ContextSSLAdapter(context) + + threading.Thread(target=self.server.start, daemon=True, + name='WSGI-server').start() + + def stop(self): + """Stop the wsgi server.""" + self.server.stop() def dispatch_request(self, request): """Handle incoming request.""" @@ -365,6 +389,10 @@ def __call__(self, environ, start_response): """Handle a request for base app + extra apps.""" from werkzeug.wsgi import DispatcherMiddleware + if not self.hass.is_running: + from werkzeug.exceptions import BadRequest + return BadRequest()(environ, start_response) + app = DispatcherMiddleware(self.base_app, self.extra_apps) # Strip out any cachebusting MD5 fingerprints fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', '')) diff --git a/homeassistant/core.py b/homeassistant/core.py index cbf02ea587f7ec..82ec20c82f9abb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -49,6 +49,19 @@ _LOGGER = logging.getLogger(__name__) +class CoreState(enum.Enum): + """Represent the current state of Home Assistant.""" + + not_running = "NOT_RUNNING" + starting = "STARTING" + running = "RUNNING" + stopping = "STOPPING" + + def __str__(self): + """Return the event.""" + return self.value + + class HomeAssistant(object): """Root object of the Home Assistant home automation.""" @@ -59,14 +72,23 @@ def __init__(self): self.services = ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus) self.config = Config() + self.state = CoreState.not_running + + @property + def is_running(self): + """Return if Home Assistant is running.""" + return self.state == CoreState.running def start(self): """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) + self.state = CoreState.starting create_timer(self) self.bus.fire(EVENT_HOMEASSISTANT_START) + self.pool.block_till_done() + self.state = CoreState.running def block_till_stopped(self): """Register service homeassistant/stop and will block until called.""" @@ -113,8 +135,10 @@ def restart_homeassistant(*args): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = CoreState.stopping self.bus.fire(EVENT_HOMEASSISTANT_STOP) self.pool.stop() + self.state = CoreState.not_running class JobPriority(util.OrderedEnum): diff --git a/homeassistant/remote.py b/homeassistant/remote.py index b2dfc3ae18f1a6..6c49decdff2c1b 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -11,6 +11,7 @@ import enum import json import logging +import time import threading import urllib.parse @@ -123,6 +124,7 @@ def __init__(self, remote_api, local_api=None): self.services = ha.ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus, self.remote_api) self.config = ha.Config() + self.state = ha.CoreState.not_running self.config.api = local_api @@ -134,17 +136,20 @@ def start(self): raise HomeAssistantError( 'Unable to setup local API to receive events') + self.state = ha.CoreState.starting ha.create_timer(self) self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) - # Give eventlet time to startup - import eventlet - eventlet.sleep(0.1) + # Ensure local HTTP is started + self.pool.block_till_done() + self.state = ha.CoreState.running + time.sleep(0.05) # Setup that events from remote_api get forwarded to local_api - # Do this after we fire START, otherwise HTTP is not started + # Do this after we are running, otherwise HTTP is not started + # or requests are blocked if not connect_remote_events(self.remote_api, self.config.api): raise HomeAssistantError(( 'Could not setup event forwarding from api {} to ' @@ -153,6 +158,7 @@ def start(self): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = ha.CoreState.stopping self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, origin=ha.EventOrigin.remote) @@ -161,6 +167,7 @@ def stop(self): # Disconnect master event forwarding disconnect_remote_events(self.remote_api, self.config.api) + self.state = ha.CoreState.not_running class EventBus(ha.EventBus): diff --git a/requirements_all.txt b/requirements_all.txt index 7ee914b24871e7..2dc4da447104b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,6 @@ pytz>=2016.4 pip>=7.0.0 jinja2>=2.8 voluptuous==0.8.9 -eventlet==0.19.0 # homeassistant.components.isy994 PyISY==1.0.6 @@ -48,6 +47,9 @@ blockchain==1.3.3 # homeassistant.components.notify.aws_sqs boto3==1.3.1 +# homeassistant.components.http +cherrypy==6.0.2 + # homeassistant.components.notify.xmpp dnspython3==1.12.0 @@ -61,9 +63,6 @@ eliqonline==1.0.12 # homeassistant.components.enocean enocean==0.31 -# homeassistant.components.http -eventlet==0.19.0 - # homeassistant.components.thermostat.honeywell evohomeclient==0.2.5 diff --git a/setup.py b/setup.py index b574e156931458..fbce912c3d6e4c 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ 'pip>=7.0.0', 'jinja2>=2.8', 'voluptuous==0.8.9', - 'eventlet==0.19.0', ] setup( diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 7445b5daf8ce3c..427980be5f19a2 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -1,8 +1,8 @@ """The tests the for Locative device tracker platform.""" +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -32,12 +32,9 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, http.DOMAIN, { http.DOMAIN: { http.CONF_SERVER_PORT: SERVER_PORT - } + }, }) - # Set up API - bootstrap.setup_component(hass, 'api') - # Set up device tracker bootstrap.setup_component(hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: { @@ -46,7 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index e1eb257577cc3c..97d73b8b49d4c0 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -1,9 +1,9 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access,too-many-public-methods -import unittest import json +import time +import unittest -import eventlet import requests from homeassistant import bootstrap, const @@ -86,8 +86,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - - eventlet.sleep(0.1) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 8d1ee1c4ad52b0..752980e65c8833 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,12 +1,12 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access,too-many-public-methods -# from contextlib import closing +from contextlib import closing import json import tempfile +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -48,10 +48,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # To start HTTP - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -387,25 +384,23 @@ def test_api_event_forward(self): headers=HA_HEADERS) self.assertEqual(422, req.status_code) - # TODO disabled because eventlet cannot validate - # a connection to itself, need a second instance - # # Setup a real one - # req = requests.post( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({ - # 'api_password': API_PASSWORD, - # 'host': '127.0.0.1', - # 'port': SERVER_PORT - # }), - # headers=HA_HEADERS) - # self.assertEqual(200, req.status_code) - - # # Delete it again.. - # req = requests.delete( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({}), - # headers=HA_HEADERS) - # self.assertEqual(400, req.status_code) + # Setup a real one + req = requests.post( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'api_password': API_PASSWORD, + 'host': '127.0.0.1', + 'port': SERVER_PORT + }), + headers=HA_HEADERS) + self.assertEqual(200, req.status_code) + + # Delete it again.. + req = requests.delete( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({}), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) req = requests.delete( _url(const.URL_API_EVENT_FORWARD), @@ -425,57 +420,58 @@ def test_api_event_forward(self): headers=HA_HEADERS) self.assertEqual(200, req.status_code) - # def test_stream(self): - # """Test the stream.""" - # listen_count = self._listen_count() - # with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, - # stream=True, headers=HA_HEADERS)) as req: - - # self.assertEqual(listen_count + 1, self._listen_count()) - - # hass.bus.fire('test_event') - - # data = self._stream_next_event(req) - - # self.assertEqual('test_event', data['event_type']) - - # def test_stream_with_restricted(self): - # """Test the stream with restrictions.""" - # listen_count = self._listen_count() - # url = _url('{}?restrict=test_event1,test_event3'.format( - # const.URL_API_STREAM)) - # with closing(requests.get(url, stream=True, timeout=3, - # headers=HA_HEADERS)) as req: - # self.assertEqual(listen_count + 1, self._listen_count()) - - # hass.bus.fire('test_event1') - # data = self._stream_next_event(req) - # self.assertEqual('test_event1', data['event_type']) - - # hass.bus.fire('test_event2') - # hass.bus.fire('test_event3') - - # data = self._stream_next_event(req) - # self.assertEqual('test_event3', data['event_type']) - - # def _stream_next_event(self, stream): - # """Read the stream for next event while ignoring ping.""" - # while True: - # data = b'' - # last_new_line = False - # for dat in stream.iter_content(1): - # if dat == b'\n' and last_new_line: - # break - # data += dat - # last_new_line = dat == b'\n' - - # conv = data.decode('utf-8').strip()[6:] - - # if conv != 'ping': - # break - - # return json.loads(conv) - - # def _listen_count(self): - # """Return number of event listeners.""" - # return sum(hass.bus.listeners.values()) + def test_stream(self): + """Test the stream.""" + listen_count = self._listen_count() + with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, + stream=True, headers=HA_HEADERS)) as req: + stream = req.iter_content(1) + self.assertEqual(listen_count + 1, self._listen_count()) + + hass.bus.fire('test_event') + + data = self._stream_next_event(stream) + + self.assertEqual('test_event', data['event_type']) + + def test_stream_with_restricted(self): + """Test the stream with restrictions.""" + listen_count = self._listen_count() + url = _url('{}?restrict=test_event1,test_event3'.format( + const.URL_API_STREAM)) + with closing(requests.get(url, stream=True, timeout=3, + headers=HA_HEADERS)) as req: + stream = req.iter_content(1) + self.assertEqual(listen_count + 1, self._listen_count()) + + hass.bus.fire('test_event1') + data = self._stream_next_event(stream) + self.assertEqual('test_event1', data['event_type']) + + hass.bus.fire('test_event2') + hass.bus.fire('test_event3') + + data = self._stream_next_event(stream) + self.assertEqual('test_event3', data['event_type']) + + def _stream_next_event(self, stream): + """Read the stream for next event while ignoring ping.""" + while True: + data = b'' + last_new_line = False + for dat in stream: + if dat == b'\n' and last_new_line: + break + data += dat + last_new_line = dat == b'\n' + + conv = data.decode('utf-8').strip()[6:] + + if conv != 'ping': + break + + return json.loads(conv) + + def _listen_count(self): + """Return number of event listeners.""" + return sum(hass.bus.listeners.values()) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 61e33931c24a7f..083ebd2eb0c1f5 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,9 +1,9 @@ """The tests for Home Assistant frontend.""" # pylint: disable=protected-access,too-many-public-methods import re +import time import unittest -import eventlet import requests import homeassistant.bootstrap as bootstrap @@ -42,10 +42,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'frontend') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_http.py b/tests/components/test_http.py index f665a9530c8c61..6ab79f3e0ccf6d 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -1,8 +1,8 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access,too-many-public-methods import logging +import time -import eventlet import requests from homeassistant import bootstrap, const @@ -43,8 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -83,7 +82,7 @@ def test_access_with_password_in_header(self, caplog): logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs def test_access_denied_with_wrong_password_in_url(self): @@ -106,5 +105,5 @@ def test_access_with_password_in_url(self, caplog): logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs diff --git a/tests/test_remote.py b/tests/test_remote.py index 893f02bea31869..f3ec35daee5ab7 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,9 +1,8 @@ """Test Home Assistant remote methods and classes.""" # pylint: disable=protected-access,too-many-public-methods +import time import unittest -import eventlet - import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote @@ -47,10 +46,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT) @@ -63,10 +59,6 @@ def setUpModule(): # pylint: disable=invalid-name slave.start() - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) - def tearDownModule(): # pylint: disable=invalid-name """Stop the Home Assistant server and slave.""" @@ -257,7 +249,6 @@ def test_statemachine_set(self): slave.pool.block_till_done() # Wait till master gives updated state hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual("remote.statemachine test", slave.states.get("remote.test").state) @@ -266,13 +257,11 @@ def test_statemachine_remove_from_master(self): """Remove statemachine from master.""" hass.states.set("remote.master_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.master_remove', slave.states.entity_ids()) hass.states.remove("remote.master_remove") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.master_remove', slave.states.entity_ids()) @@ -280,14 +269,12 @@ def test_statemachine_remove_from_slave(self): """Remove statemachine from slave.""" hass.states.set("remote.slave_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.slave_remove', slave.states.entity_ids()) self.assertTrue(slave.states.remove("remote.slave_remove")) slave.pool.block_till_done() hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) @@ -306,6 +293,5 @@ def listener(event): slave.pool.block_till_done() # Wait till master gives updated event hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual(1, len(test_value)) From 21be4c1828a5e49fe8fb839b2755f438aa6f2d1e Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Thu, 30 Jun 2016 22:21:57 +0100 Subject: [PATCH 75/79] Add Sonos unjoin functionality (#2379) --- .../components/media_player/services.yaml | 14 ++++- .../components/media_player/sonos.py | 60 +++++++++---------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index d1ef92ee4d5658..9ab831bdbb44ae 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -154,12 +154,20 @@ sonos_group_players: description: Name(s) of entites that will coordinate the grouping. Platform dependent. example: 'media_player.living_room_sonos' +sonos_unjoin: + description: Unjoin the player from a group. + + fields: + entity_id: + description: Name(s) of entites that will be unjoined from their group. Platform dependent. + example: 'media_player.living_room_sonos' + sonos_snapshot: description: Take a snapshot of the media player. fields: entity_id: - description: Name(s) of entites that will coordinate the grouping. Platform dependent. + description: Name(s) of entites that will be snapshot. Platform dependent. example: 'media_player.living_room_sonos' sonos_restore: @@ -167,5 +175,5 @@ sonos_restore: fields: entity_id: - description: Name(s) of entites that will coordinate the grouping. Platform dependent. - example: 'media_player.living_room_sonos' \ No newline at end of file + description: Name(s) of entites that will be restored. Platform dependent. + example: 'media_player.living_room_sonos' diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 9239f1edae8ed7..7d0cd12175a5e8 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -34,11 +34,12 @@ SUPPORT_SEEK SERVICE_GROUP_PLAYERS = 'sonos_group_players' +SERVICE_UNJOIN = 'sonos_unjoin' SERVICE_SNAPSHOT = 'sonos_snapshot' SERVICE_RESTORE = 'sonos_restore' -# pylint: disable=unused-argument +# pylint: disable=unused-argument, too-many-locals def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Sonos platform.""" import soco @@ -72,8 +73,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) _LOGGER.info('Added %s Sonos speakers', len(players)) - def group_players_service(service): - """Group media players, use player as coordinator.""" + def _apply_service(service, service_func, *service_func_args): + """Internal func for applying a service.""" entity_id = service.data.get('entity_id') if entity_id: @@ -83,36 +84,24 @@ def group_players_service(service): _devices = devices for device in _devices: - device.group_players() + service_func(device, *service_func_args) device.update_ha_state(True) - def snapshot(service): - """Take a snapshot.""" - entity_id = service.data.get('entity_id') + def group_players_service(service): + """Group media players, use player as coordinator.""" + _apply_service(service, SonosDevice.group_players) - if entity_id: - _devices = [device for device in devices - if device.entity_id == entity_id] - else: - _devices = devices + def unjoin_service(service): + """Unjoin the player from a group.""" + _apply_service(service, SonosDevice.unjoin) - for device in _devices: - device.snapshot(service) - device.update_ha_state(True) + def snapshot_service(service): + """Take a snapshot.""" + _apply_service(service, SonosDevice.snapshot) - def restore(service): + def restore_service(service): """Restore a snapshot.""" - entity_id = service.data.get('entity_id') - - if entity_id: - _devices = [device for device in devices - if device.entity_id == entity_id] - else: - _devices = devices - - for device in _devices: - device.restore(service) - device.update_ha_state(True) + _apply_service(service, SonosDevice.restore) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) @@ -121,12 +110,16 @@ def restore(service): group_players_service, descriptions.get(SERVICE_GROUP_PLAYERS)) + hass.services.register(DOMAIN, SERVICE_UNJOIN, + unjoin_service, + descriptions.get(SERVICE_UNJOIN)) + hass.services.register(DOMAIN, SERVICE_SNAPSHOT, - snapshot, + snapshot_service, descriptions.get(SERVICE_SNAPSHOT)) hass.services.register(DOMAIN, SERVICE_RESTORE, - restore, + restore_service, descriptions.get(SERVICE_RESTORE)) return True @@ -356,12 +349,17 @@ def group_players(self): self._player.partymode() @only_if_coordinator - def snapshot(self, service): + def unjoin(self): + """Unjoin the player from a group.""" + self._player.unjoin() + + @only_if_coordinator + def snapshot(self): """Snapshot the player.""" self.soco_snapshot.snapshot() @only_if_coordinator - def restore(self, service): + def restore(self): """Restore snapshot for the player.""" self.soco_snapshot.restore(True) From d326d187d1fb394940d891ec49ae203eeeabd3d5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 30 Jun 2016 23:54:04 +0200 Subject: [PATCH 76/79] fix bug in event handling and add cast for watersensor --- homeassistant/components/homematic.py | 12 ++++++++---- homeassistant/components/sensor/homematic.py | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index fb31408bd8260d..b2bfc94087a238 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -288,15 +288,19 @@ def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, def _hm_event_handler(hass, device, caller, attribute, value): """Handle all pyhomematic device events.""" - channel = device.split(":")[1] - address = device.split(":")[0] - hmdevice = HOMEMATIC.devices.get(address) + try: + channel = int(device.split(":")[1]) + address = device.split(":")[0] + hmdevice = HOMEMATIC.devices.get(address) + except (TypeError, ValueError): + _LOGGER.error("Event handling channel convert error!") + return # is not a event? if attribute not in hmdevice.EVENTNODE: return - _LOGGER.debug("Event %s for %s channel %s", attribute, + _LOGGER.debug("Event %s for %s channel %i", attribute, hmdevice.NAME, channel) # a keypress event diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 2efa4fdef38c1d..efd22b6cd6902a 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -17,7 +17,8 @@ DEPENDENCIES = ['homematic'] HM_STATE_HA_CAST = { - "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"} + "RotaryHandleSensor": {0: "closed", 1: "tilted", 2: "open"}, + "WaterSensor": {0: "dry", 1: "wet", 2: "water"} } HM_UNIT_HA_CAST = { From d67f79e2eb34339426cdb82f1c72869be635ac39 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 1 Jul 2016 00:01:16 +0200 Subject: [PATCH 77/79] remove unused pylint exeption --- homeassistant/components/homematic.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index b2bfc94087a238..f2d71bb409a8cd 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -92,7 +92,6 @@ def setup(hass, config): # Create server thread bound_system_callback = partial(system_callback_handler, hass, config) - # pylint: disable=unexpected-keyword-arg HOMEMATIC = HMConnection(local=local_ip, localport=local_port, remote=remote_ip, From 952b1a3e0cfdfe1c4fe28f6e5481c1b384ef45c2 Mon Sep 17 00:00:00 2001 From: patkap Date: Fri, 1 Jul 2016 01:35:20 +0200 Subject: [PATCH 78/79] kodi platform: following jsonrpc-request version bump (0.3), let kodi file abstraction layer handle a collection item, url or file to play (#2398) --- homeassistant/components/media_player/kodi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 2a14af969fb5da..3af270a05b0124 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -301,4 +301,4 @@ def media_seek(self, position): def play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - self._server.Player.Open({media_type: media_id}, {}) + self._server.Player.Open({"item": {"file": str(media_id)}}) From c44eefacb42b7431f8fd784d2b37a7cc6402ec48 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2016 00:57:55 -0700 Subject: [PATCH 79/79] Version bump to 0.23.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 92909f354354d1..ab9fc35ee4c123 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = "0.23.0.dev0" +__version__ = "0.23.0" REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}'