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 1b6ae128c18..92909f35435 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 e8666da60f4..265c653d636 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 00000000000..ebd54da1558 --- /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 00000000000..144de83aa53 --- /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 00000000000..f1a7009e059 --- /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 00000000000..cd71673b99f --- /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 368ea27649c..bafb405b956 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 5cfed006b81..306acab5361 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 c91a2ddd489..b4aaf5e2b4f 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/light.zwave/ # 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 00000000000..45928d1bfb4 --- /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 b2dd036074c..36bf0163424 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -50,6 +50,14 @@ COMMAND_CLASS_ALARM = 113 # 0x71 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 @@ TYPE_DECIMAL = "Decimal" # 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 setup(hass, config): 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 setup(hass, config): 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 265c653d636..a7464feb571 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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 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 00000000000..ea316f57425 --- /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 bafb405b956..36036655c2b 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 e1b1614a60f..5becb53b98a 100755 --- a/homeassistant/components/hvac/zwave.py +++ b/homeassistant/components/hvac/zwave.py @@ -211,6 +211,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): 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 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): 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 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): 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 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): 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 4eb18664a24..57287605e67 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -156,3 +156,4 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice): 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 b202f38fa7c..304d771f92a 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/notify.pushover/ 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 @@ class PushoverNotificationService(BaseNotificationService): """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 49b6f8acc93..5257c965cd6 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -10,7 +10,7 @@ from homeassistant.components.notify import DOMAIN, BaseNotificationService 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 36036655c2b..e876ccacbb3 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 20af7e71a59..c9767428aaa 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 @@ import homeassistant.util.dt as dt_util 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 e876ccacbb3..db0f7db3964 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 e9087d9c578..6db231f6bd7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -29,7 +29,7 @@ MQTT_CLIENT = None 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 db0f7db3964..4853c520429 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 a7464feb571..a1b63cf0559 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 00000000000..05f6003039e --- /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 4853c520429..e8a24fc2024 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 265c653d636..6416ad3e2b4 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 00000000000..0034eb131b6 --- /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 0034eb131b6..0c7c40c1214 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 3ccf92daea2..0baa7dd177c 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -437,7 +437,7 @@ class HomeAssistantView(object): 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 66fb97dfd33..60ff19d4a43 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -225,7 +225,7 @@ class TestAPI(unittest.TestCase): 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 304d771f92a..a8bc3cf5179 100644 --- a/homeassistant/components/notify/pushover.py +++ b/homeassistant/components/notify/pushover.py @@ -51,10 +51,11 @@ class PushoverNotificationService(BaseNotificationService): """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 22caab1d1fb..05498d41496 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -11,19 +11,29 @@ from homeassistant.helpers.entity import Entity 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 @@ class NetAtmoSensor(Entity): 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 @@ class NetAtmoSensor(Entity): 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 99382bebe74..754d4f4f5aa 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -25,8 +25,8 @@ from homeassistant.const import ( 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 process_ha_core_config(hass, config): 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 f2696bbbd1a..d625f9cd3cd 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -19,6 +19,8 @@ from homeassistant.const import ( _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 setup(hass, config): 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 b89d358045d..e8981e520c8 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 423a276f11a..ee1b786dce3 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.const import ( 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 @@ class Entity(object): """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 @@ class ToggleEntity(Entity): 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 ff663a95a53..68b0ca3be35 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 @@ class TestComponentsCore(unittest.TestCase): 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 @@ class TestComponentsCore(unittest.TestCase): 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 a317702b29f..a465c2f2c74 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -21,8 +21,7 @@ class TestHelpersEntity(unittest.TestCase): 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 @@ class TestHelpersEntity(unittest.TestCase): 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 @@ class TestHelpersEntity(unittest.TestCase): """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 6bfda3f55f5..ddc31bb56ec 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -47,7 +47,7 @@ HydroData = collections.namedtuple( '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 0c7c40c1214..f5a64b909af 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 @@ import homeassistant.helpers.config_validation as cv _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 @@ DEVICE_SCHEMA = vol.Schema({ }) -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 6416ad3e2b4..0f9904d80a2 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 f5a64b909af..89cbe789c58 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({ 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 282ae46ba85..725a49308be 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -83,8 +83,8 @@ class AsusWrtDeviceScanner(object): """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 210ce2c58fa..241e4a65a0f 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -67,3 +67,68 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): 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 f1a7009e059..23f9acef12f 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -12,7 +12,7 @@ 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'] +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 e8a24fc2024..a5ef32201c5 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 0baa7dd177c..d7ce8e78013 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.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.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 @@ class HomeAssistantView(object): 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 dec518db6ea..17ffad41f93 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/shell_command/ """ import logging import subprocess +import shlex import voluptuous as vol @@ -23,8 +24,6 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) -SHELL_COMMAND_SCHEMA = vol.Schema({}) - def setup(hass, config): """Setup the shell_command component.""" @@ -44,8 +43,7 @@ def setup(hass, config): _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 | 205 ++----- requirements_all.txt | 3 + 9 files changed, 1199 insertions(+), 151 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 a1b63cf0559..5932ed9a0d0 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 00000000000..fe50a5ef48c --- /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 00000000000..751db436def --- /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 00000000000..94dabb0f00a --- /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 00000000000..e0dd5e5469f --- /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 00000000000..52ece78f59e --- /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 00000000000..5a630f43022 --- /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 b4ecc6c166b..a1ed06bc4bd 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 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_callback_devices, discovery_info=None): + """Setup the platform.""" + return homematic.setup_hmdevice_entity_helper(HMThermostat, + config, + add_callback_devices) -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 __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 @@ class HomematicThermostat(ThermostatDevice): @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 @@ class HomematicThermostat(ThermostatDevice): """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) + 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 - 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 + # Check compatibility from HMDevice + if not super()._check_hm_to_ha_object(): + return False - except Error: - _LOGGER.exception("Did not receive any temperature data from the " - "homematic API.") + # 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 a5ef32201c5..f3670ea35da 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 | 130 +++++++++++++----- 3 files changed, 168 insertions(+), 52 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 00000000000..18a2ea96b86 --- /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 45928d1bfb4..288488fe057 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 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): @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 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): # 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 36bf0163424..98a24240a00 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -39,23 +39,39 @@ SERVICE_TEST_NETWORK = "test_network" 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 @@ TYPE_DECIMAL = "Decimal" # 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 setup(hass, config): 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: + value_genre) in DISCOVERY_COMPONENTS: - if value.command_class not in command_ids: + _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 value_type is not None and value_type != value.type: + 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_genre is not None and value_genre != value.genre: + 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 specific_device_class is not None and \ - specific_device_class != node.specific: + 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 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 fe50a5ef48c..d2005f99ba5 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -89,7 +89,7 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): 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 a1b63cf0559..da463380733 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 00000000000..6a50ffb408d --- /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 85d10671a17..560f3d13fd6 100644 --- a/homeassistant/components/hvac/__init__.py +++ b/homeassistant/components/hvac/__init__.py @@ -425,39 +425,39 @@ class HvacDevice(Entity): 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 5becb53b98a..c950200932a 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 27c68819909..2b0af395d02 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -248,7 +248,8 @@ def setup(hass, config): 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 5242746dc42..010088af824 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -4,6 +4,7 @@ Support for LimitlessLED bulbs. 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 04c0942e1f6..d8d288afd0e 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -4,6 +4,7 @@ Support for MySensors lights. 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 1f92b458d53..60b9c9fdcd8 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -108,7 +108,7 @@ def setup(hass, config): 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 e6e1eb76b75..7a1f3498f18 100644 --- a/homeassistant/components/switch/acer_projector.py +++ b/homeassistant/components/switch/acer_projector.py @@ -4,7 +4,6 @@ Use serial protocol of acer projector to obtain state of the projector. This component allows to control almost all projectors from acer using their RS232 serial communication protocol. """ - import logging import re @@ -61,7 +60,8 @@ class AcerSwitch(SwitchDevice): 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 @@ class AcerSwitch(SwitchDevice): 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 @@ class AcerSwitch(SwitchDevice): 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 1a166a9c2dc..08138db9e70 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -4,6 +4,7 @@ Support for device running with the aREST RESTful framework. 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 e77453fa6eb..28a44249e12 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -65,6 +65,10 @@ class WOLSwitch(SwitchDevice): 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 4c57a23ff9c..8d811c3a5cc 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -273,29 +273,29 @@ class ThermostatDevice(Entity): """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 5d47276c7bc..7718299ef6a 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 f07ef47269d..577a33c87f4 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -68,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 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 34c164f2c0d..c9bbdaeb0a4 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 64f95c2e517..3d5190bcc2f 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -41,7 +41,6 @@ PLATFORM_SCHEMA = vol.Schema({ }) -# 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 ec8bbeb1981..e7bbfd72f9b 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 b4ecc6c166b..6f1ff29c16c 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 633212e02b5..f45b07b9fd6 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 @@ class RoundThermostat(ThermostatDevice): 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 881eb821865..00a1acf07b4 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 4b86f556352..bf5c61d2be6 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 a6ae39434e7..963ef1a9a7f 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 57287605e67..8d7e36cc2aa 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 ee1b786dce3..e4ccf11e168 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -229,17 +229,15 @@ class ToggleEntity(Entity): @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 0f6318a0638..e7483740bc3 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 00000000000..6c784eaf5ca --- /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 50c61ad3af7..ac957e0661f 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 8bb155ea288..4bc3619c443 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;sBD0AuoxghyZMpsOYpxCJ zN1mqS{U+~XxlYt+vmQS0u)=@&(~lC|4iW6GqARx-2Z@CV?G>DKS#e+c3U~Iqz7yKm z>z!S1w;boZRTs6ZYj)vA&!f}z4+VWy()m>JKlSmn(DJm8Hal#W+UFUTANuexy6Eto zeSb2Hx9i!<&A2^9UiMksR!L1mFT<6zn9(c?0dE3jU`tqC! zPuOmrVOIWbvS5R(>MN_|&G$}}KHDad$#kp7q{yJ&b}jE3u1j_HY?W!N8f+x_1(;HO1TPup&e5=?q>C1$V9 z?A6ngD_0kVFA}sp=;0T_`n7o9LQ{cNhBK#1g{(WtsUkPkNB7b#wsSWk*Ktbo_;EBG zxKuWK(W8`+BQ%*DKRJwwBQBhey)CEk47(W?Fnk zMD(NfK8t+qY)2EBLsk;*mrY%2zj(90cS_x_H}SsH_7i%aqW@U9)OE{?FO~Up>Dh$c zvz+A~G5tJr$G@j0?H#Lfy!XzB;`N)?*d?zupP4G!^=56i{v9uY)s`~@)TGi@-dwa# zPs}3x`i65`?ge$4zmU@t>$qH2uxWn0p;YIEgb(k-x|_D2D%u$Cz2|?nOkL5|6=qkz ztyfGD_tG%95V9viqDrQ_V&e~|*vniBT8(mFnL4=al^9)G<>zTAYia2$dc@S>(eIZw@1z~#=YW>nRnrUpF+|Ze^dMK?s+F4e>Szg@YPKv#Y6a` zVdH|sy9{d{+0Bc)uVla`B6cFY{|YDD?}dA=8A^IOYj*wXztNJX9Cr5bqWJ|j{;VRo zvl9Ki^ukXB+>77*;|`0GoYl6J4>JA#=L@>pUavRuF`Y0;^X%ha>jbWsOUB$6{d})t zRsG}|k$;cuCO$m4<(IN#`q#es22Ed%2mR|&e%F7k;p>6GgCP~&>3PR{j@vg_$cZ=V ztX;-lqWe-QN8(f+tN;JfY0CMta^##JvNiP--Z<$prSSJcj~#y{ER<$)eP7i%(d@~t z_OnVqWw<{_)_;2T_t&od`_46}pJViDWvkxT^|f>A2dT4(0X_+QZ_^Q$qasTw&;_X@4)k3Q|zS(XpEZuwM&-b`1n>r@0tDpEparX-MCx)Ra zV&Bd$-C)1@*M(DGAG&=_+!iDs6_CZXMZ#@yMCY-axv!Qhg|5}Mc~Frn_iOV`eVZDE ziiTM#%RkxwzuS;v^_l(r9LJrT?f!(tX*_?$ny9{UN=0Pq87}sx)ejfEIq8`BJj2IQ zJWJ}f`p0i?Ym5|~N}s7-sb{!Uzju|!6zNTB1`8@O8rnWresl<3=Q+{qU{7`i@2Y+# zSN7?id~-}MFl>qsP%~W+ux#GzVxH9fw~OUmjH)v@pOv_zx&7L4NowZC8?75NxVGzh zFfUf??l@~Z<96oi>1d{Wn+jWZVL zyb-kX+O;$miI2&A!mXCPqOW8E?Za~yE@#l&q0z8vnl|H^8{Z!Zbvhn4y{WB{_*>p# z;@>8vjowTt^;!?Md6rh{an30|nIb&B{k_uL`o|lVr|c_HefnubzjBS{9e2mF*595J z4u7)jIwKQ1%kq{hhj_svi{yXG`#AV_FXQ4}xM%+ByJf94Cmr{O=eh5D{IF`*TbG5x z4;IuPyXYW3r>dhybBc0S&6EU(ePUBDyz`v!S8aZ-wT@%w1SifrEgu&ivzpRkGGA%G zN4U34qssnzkLL@+10;?msowA`G=+DV6D6 zA2ozU?GEuVUD(<5F4&j}tR?sz(apiB;8(O8x^(#ER#SdN3Vd+=nR&?M< zUKKL&>*sHEd+%T36K0>=?YRDK&f9DL+(&0{nkpR#*?Yxgp?1!XjY(BE`D~VLl~6X~ zJG+hh<;QK-oKN>zZ#R(C@Okzn^p5ZX-D;T=TO|5-9!j1fUAtw*%Zn51A9JP)2tB)d zIoc()CFb$GQ*TSPZl3R~Kh^YXx7_FD4p;n*ZZkA2k<*>D;eG9zM|Xl1&hGjuy^VGC zPxU!3qgS!sSRA(4o}qBV-TgXq?#wh@qHDUuc-N#=k#TW)a#y1zLZz8MrSQI9D|v06 z)V0}vHk@+2-2VUegoG<$+No`eZ->Q(>hkd2+FSShOVNcji!U5~E$Cg!wx_F12o&8ACbO_%a_FTb)w(&TOXVFQcM;Jx!0 zr#haQX1{8GUg*>AH{PzV&DQG961}u-#lqYb4;=%Y@4s@Jo8~X)dv)VKuPsr_yYEDv zy6C%mb4l05x7sDrV!O_B?L7AS!?K_~VITCP>#r?8x-TSk>(u!#F75K3ez@pKcm(r* zzJAf&o31Q%f>{c832l{I@3nR{kKf)I8MC`G-s`OCFN&BSu)XPA>jej% z?M~-H!rz6xI(0F-Y-!+~sp(gzHp~2q)e?27xOSwt^^OsrhTHCnCf#p>#akJsopM%V z|M`5z`*f|^`t~F4T$Qax?`Lu8cNePig&cp_v2_ER-K3SP94^+X{^Z}x@z8L!&Ff7L zeY&qUUG{zAn>mZgV`)|Usvn1G^fw(?>SSG zk@AfD$XjE+zdT|bT#s~fgPPcFZL%*cs?Pa3Z-exqDfK?=+8YRY&^9|J=jyw=}7LvAA9cZ>Tp%-m$s8A zIQhWlUsO=Rk?gR^OD?1@F7yA(E-a$sb((Et86za6XJ zoX@)THMiky*oi69X&qY|_dSnU^{)FxnP{?1@-wmfCP@Z{w`JsK9b9t5i#zO0eT(-r z%M;sdp0+kmtCM12GwfXZYDGWidZBNDZHpy}CHH4Ib{y3Eb#A4&@SOUjl#=*uZclO? z#5WvnG=6@>Yx)+yzXv20YV8WkPk8n_pFfdn*=jGvRc|D6&zZyco!^y16R*#lly&`j z{Y@3!85LKSufKZM_YSZ7+|=3PTGAhbj_j-t;d1}ExBJVN19t^gW#4_%cv-yttmj^t z^UlrBn-6Y2?5VO$C`CRnujo6|e6d^xGq;}~o$c8RMK45!6+1CK$#i=dbcoAdt664- zl<#XTxfxL#HF;C++*r5b=F68=5)6mz(vDBH+LNZ5{winho(btP&tz}k+;OlwcABBf z)hJ=+ih2{vDt+@0`DfcAzP8zPPdUCQtajwc=kN zoitvr$}(}&@`ziDQx-Di{NJ@KIcZt1(X1Iyy}mHLEbGf>WGcVC@qUKzfj{PJ+}}o+ z?wn@xRPW2*h)tHVzj|sOzf;@!II{2q$FqlTmfV&95`FxxW4-fz<(X4#eQvJaGeKCx zV$w7w2d;Cx0#_g3NmfqX_uF&|V|7CKp&wd)+A2j6fsZC|^8P;bisxJF0n11FPkO}f zd3)GC65L;U@4+jp2k#`+W8=zpS2etT8kn1;@b_Jr)%>n6GKFGYZ{LRLudmn8OURV- zQi!l(-_x+++nM)S=WXKaGtckg^le#?TK!ykz34X)x0`P!u9{%O^J8C^O6tRH>kDo_ zUSA}!T|i6XzAF2zPp7nHQ@vNOFY4IJw(^U>Z%t*rm{kjZ9}#}Ny4_Qz(&ygAQ~H0q z!tMzxKVAIMNz37d`JDSFJ06No`1Mn$oc+;XYi-@moD~M5oU?3>85(g^sa~s(-XtBq z`gTZS_oYQ#x2Jr&vMV&xYpw1&rQ3h3a*M7W*l{n?_?5RnGM{eKz7rd_>1WO33y-Q3 zubFE6GbkwaD0jdL)*m+yeb45*Z_ai9y!ON2UrRqMJ^G`lHD5y4zR_)8>60%h8y;xf zHWoYiIzdpfGAej)n0CMkLBXAe1Rj4(VyI^fkU!z2cllbv{cY!y_egj|<`(bxqMYeg zR1ID_4Q$6BC|mOgduG5w`PE)rb6d88>GC z-gm{N{zJ6RxtW||9Y;#!S1Qb&Tc#{}#8Y+7wyJdZl~ev@{#{aj=Up;`m-XJ8mYFyA z2S4PSzeev%QAx#WtFIz!Q({CsmrmR=^I9N>=C4ni65I}3d{av`U@lRc+}?D~@Qn(i z;+5IUCWRU7=*+ryVnXLuy~#SJZYfby()-0`icdXIsN7t?NM>tb-=#=H?H%E362u;E zTp{|j-?io)gL=E&<%CCPYI618Z<+8W?@E)^^O6_0)x>hUg-@JmE1Ed9jPc|I=Y#F8 ztZz>?Y?{lOy7_X;Gux*}jfFeP>;iTOJI+mt6Jr1Wed|?KqseI@1ykoqaXfe1)ZB9D zR^YTla*LR_m~A$Ru+>-m>|gld*3LtvF*kZo9FO|+wZf{-}J=AVHc%L|`*uG?m_PgZoh=J|)7yPquy-_4j;zm0Lm1Ks&P zwwK$>Vt35f){&g5b?&+A(y2;{u@bJW0q)vLpZ>h(S;=?xz0L8oM}EyqMK7NdU&ZwI z(an|Iw$tQv;w3LiD}9g^dYvPC?(y#m6PMM8^=}8hoalMBr}%Voc7A%(xg{%D3iuv! z?=Dn&vN5mgPqEy@YL&d_CQY53hkn)1u?oJz`1XhCd0RHAAR`S9X6gEz#g?DHv8!kI zG^K@QS3Ht!o2v5T_ceKmzp_8Kt*ErlvR`?0na7HkCvN0ip0#Ds#f8z2nkIZ#PVIlR ze5>r1FsFLWr|h2!dmaBAlIQJjjy5mPwRz;68QJUPmaVn6-EfYm-L)Uu?^yP;mA_!@ zsxPcBd(zERkl^O%D0=mRw6K-zO6{k!mVLX`8xnX!?vJT)f8l!FyQkeM&#wu(x+A*h z@rxQAiD$ewZr0sgr6;k{C2;G$xRCnRt|!xdudKUK7?pimOzY=^{oWVmS2#YnB>w4m z*+l6#ep+@jo@RYw+QTjCd?vO2zpVYq4S^fILRLxZDrn~A2wmA8Vy>m-tKGvgAy24Y fS2Q$|ZQa#{Q`Tne=b857+<&H$%gRkohZz|F+QWWG delta 4397 zcmaF+i}BVkMt1pb4i1r7sT_%!C8;9oRo7izzEk%#PVI zVTK6-C-Xq(nuzxDBor+QU?to;-waK0q-&2+X^-i$Z%mnQs> zi%mlkBggBMk)⪴achW#Sk8b>B``o2~1j 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 diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 34ef06d3ee9..08d2cdea3a4 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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

< 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 HDGWt^Q;a2eDY+KraTZgod?QWgC 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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 18a2ea96b86..b527fc0052c 100644 --- a/homeassistant/components/garage_door/zwave.py +++ b/homeassistant/components/garage_door/zwave.py @@ -51,7 +51,7 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice): 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 @@ class ZwaveGarageDoor(zwave.ZWaveDeviceEntity, GarageDoorDevice): @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 c950200932a..2a9c0726f92 100755 --- a/homeassistant/components/hvac/zwave.py +++ b/homeassistant/components/hvac/zwave.py @@ -98,7 +98,7 @@ class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): 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 288488fe057..ea0be0ddf74 100644 --- a/homeassistant/components/rollershutter/zwave.py +++ b/homeassistant/components/rollershutter/zwave.py @@ -49,7 +49,7 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): 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 8d7e36cc2aa..ed653874af2 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -81,7 +81,7 @@ class ZWaveThermostat(zwave.ZWaveDeviceEntity, ThermostatDevice): 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 7d030d8640c..6a9d1fc11fa 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 00000000000..f95e5c36233 --- /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 6a9d1fc11fa..6526b2b1e3d 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 00000000000..43ddee3ba02 --- /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 a5ef32201c5..7360074d8ab 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 00000000000..24322b5bce0 --- /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 58458986063..0e6ec01f26e 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -5,10 +5,15 @@ from collections import OrderedDict 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 244f9323334..7bede7edca9 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -3,8 +3,9 @@ import io 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 @@ class TestYaml(unittest.TestCase): "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 | 101 ++++++++++++-------------- 1 file changed, 47 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 751db436def..81b4fde9596 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,7 +15,6 @@ homematic: """ 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} + component = get_component(component_name) + config = {component.DOMAIN: found_devices} - # Ensure component is loaded - homeassistant.bootstrap.setup_component( - _HM_DISCOVER_HASS, - component.DOMAIN, - config) + # 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: '' - } + # 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 = {} + 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) + # 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}) + # 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) - # 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 + # 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(ordered_device_dict) + # 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 | 58 ++++++++++++++------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 81b4fde9596..7db0d926dda 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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): # 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)) + 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 + # 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 @@ class HMDevice(Entity): # 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 | 112 ++++++++++++++------------ 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 7db0d926dda..317397c0b9c 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,11 +15,11 @@ homematic: """ 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) + 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 - # 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)) + # 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 317397c0b9c..32ac7ac80eb 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -15,6 +15,7 @@ homematic: """ import time import logging +from functools import partial from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ EVENT_PLATFORM_DISCOVERED, \ ATTR_SERVICE, \ @@ -29,7 +30,6 @@ REQUIREMENTS = ['pyhomematic==0.1.6'] HOMEMATIC = None HOMEMATIC_DEVICES = {} -HOMEMATIC_AUTODETECT = False DISCOVER_SWITCHES = "homematic.switch" DISCOVER_LIGHTS = "homematic.light" @@ -69,14 +69,13 @@ HM_ATTRIBUTE_SUPPORT = { "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 @@ class HMDevice(Entity): 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 32ac7ac80eb..84a3ae33c52 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -46,10 +46,11 @@ HM_DEVICE_TYPES = { 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 84a3ae33c52..6662c6bbe0d 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 | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 6662c6bbe0d..040a15a368e 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -22,6 +22,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ 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 d2005f99ba5..acbc2eafe69 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,6 +55,8 @@ SUPPORT_HM_EVENT_AS_BINMOD = [ 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 040a15a368e..3bc23bbdb71 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -21,7 +21,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ 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 94dabb0f00a..6ccc2f636ba 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,6 +29,8 @@ DEPENDENCIES = ['homematic'] 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 e0dd5e5469f..55a86be0bf6 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,6 +29,8 @@ DEPENDENCIES = ['homematic'] 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 52ece78f59e..c07faedbf5b 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,6 +41,8 @@ HM_UNIT_HA_CAST = { 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 5a630f43022..ca639b95ecb 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,6 +28,8 @@ DEPENDENCIES = ['homematic'] 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 a1ed06bc4bd..d98b674c692 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,6 +28,8 @@ _LOGGER = logging.getLogger(__name__) 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 3bc23bbdb71..c42058a0424 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 ea0be0ddf74..81b891d7bf1 100644 --- a/homeassistant/components/rollershutter/zwave.py +++ b/homeassistant/components/rollershutter/zwave.py @@ -60,11 +60,11 @@ class ZwaveRollershutter(zwave.ZWaveDeviceEntity, RollershutterDevice): 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 98a24240a00..84ec3dfd847 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -108,7 +108,8 @@ DISCOVERY_COMPONENTS = [ 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 c42058a0424..3bc23bbdb71 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 acbc2eafe69..d2005f99ba5 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,8 +55,6 @@ SUPPORT_HM_EVENT_AS_BINMOD = [ 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 3bc23bbdb71..040a15a368e 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -21,6 +21,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ 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 6ccc2f636ba..94dabb0f00a 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,8 +29,6 @@ DEPENDENCIES = ['homematic'] 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 55a86be0bf6..e0dd5e5469f 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,8 +29,6 @@ DEPENDENCIES = ['homematic'] 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 c07faedbf5b..52ece78f59e 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,8 +41,6 @@ HM_UNIT_HA_CAST = { 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 ca639b95ecb..5a630f43022 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,8 +28,6 @@ DEPENDENCIES = ['homematic'] 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 d98b674c692..a1ed06bc4bd 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,8 +28,6 @@ _LOGGER = logging.getLogger(__name__) 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 | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 040a15a368e..6662c6bbe0d 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -22,7 +22,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \ 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) + config = {component.DOMAIN: found_devices} - # HA discovery event - discovery.load_platform(hass, component, DOMAIN, { - ATTR_DISCOVER_DEVICES: found_devices - }, config) + # 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 d2005f99ba5..08ea2099445 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -55,6 +55,11 @@ SUPPORT_HM_EVENT_AS_BINMOD = [ 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 6662c6bbe0d..5c23462e98d 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -17,11 +17,9 @@ import time 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 94dabb0f00a..159f3e4dbdc 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -29,6 +29,11 @@ DEPENDENCIES = ['homematic'] 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 e0dd5e5469f..737a7eb017d 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -29,6 +29,11 @@ DEPENDENCIES = ['homematic'] 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 52ece78f59e..f6f3825199b 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -41,6 +41,11 @@ HM_UNIT_HA_CAST = { 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 5a630f43022..16cc63a6708 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -28,6 +28,11 @@ DEPENDENCIES = ['homematic'] 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 a1ed06bc4bd..e654379d56e 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -28,6 +28,11 @@ _LOGGER = logging.getLogger(__name__) 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 5c23462e98d..749f372596b 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -17,11 +17,10 @@ import time 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 e654379d56e..d7675a5cd47 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 749f372596b..ec092baf80b 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -16,9 +16,7 @@ homematic: 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 ec092baf80b..c2c6c000fa2 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -389,8 +389,8 @@ class HMDevice(Entity): 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 754d4f4f5aa..ff7e73a00f1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -11,7 +11,7 @@ from threading import RLock 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 148c57a12c3..f083a96f5b2 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 6c784eaf5ca..66a634616fa 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -4,15 +4,77 @@ 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/ """ +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 + +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, +}) -def create(hass, entity, msg): - """Create a state for an error.""" - hass.states.set('{}.{}'.format(DOMAIN, entity), msg) +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 00000000000..6f6d8b8e1b0 --- /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 ad8f21f069b..b538a62d008 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -204,11 +204,12 @@ class APIEntityStateView(HomeAssistantView): 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 ffaccdeae43..d3eed6ce5e0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -456,7 +456,7 @@ class StateMachine(object): 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 @@ class StateMachine(object): 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 e4ccf11e168..d120a3b2cf6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -125,6 +125,15 @@ class Entity(object): """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 @@ class Entity(object): 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 4bfb01890cf..b2dfc3ae18f 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -259,9 +259,9 @@ class StateMachine(ha.StateMachine): """ 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 60ff19d4a43..8d1ee1c4ad5 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -136,6 +136,27 @@ class TestAPI(unittest.TestCase): 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 4930bcef6ed..cb698cdc53c 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -334,6 +334,20 @@ class TestStateMachine(unittest.TestCase): 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 58b2f9b359d..893f02bea31 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -8,7 +8,7 @@ import homeassistant.core as ha 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 @@ class TestRemoteMethods(unittest.TestCase): 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 ddc31bb56ec..2589bd44955 100644 --- a/homeassistant/components/sensor/swiss_hydrological_data.py +++ b/homeassistant/components/sensor/swiss_hydrological_data.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv 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 @@ class HydrologicalData(object): 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 62a87fcc368..a131813edbd 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 d7ce8e78013..1f77aac5ad4 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -10,6 +10,7 @@ import logging import mimetypes import threading import re +import ssl import voluptuous as vol import homeassistant.core as ha @@ -36,6 +37,24 @@ CONF_CORS_ORIGINS = 'cors_allowed_origins' 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 @@ class HomeAssistantWSGI(object): 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 c2c6c000fa2..7b3e265a9dd 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -24,6 +24,7 @@ DOMAIN = 'homematic' REQUIREMENTS = ['pyhomematic==0.1.6'] HOMEMATIC = None +HOMEMATIC_LINK_DELAY = 0.5 HOMEMATIC_DEVICES = {} DISCOVER_SWITCHES = "homematic.switch" @@ -71,7 +72,7 @@ _LOGGER = logging.getLogger(__name__) # 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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): # 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 b4aaf5e2b4f..7c9cb72db26 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -4,12 +4,31 @@ Support for Z-Wave lights. 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 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): 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 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): 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 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): 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 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): """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 84ec3dfd847..f8959f33033 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -41,6 +41,7 @@ EVENT_SCENE_ACTIVATED = "zwave.scene_activated" 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 ff7e73a00f1..8b3d3ee6f23 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 voluptuous as vol 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 d625f9cd3cd..38780ed9b28 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -121,16 +121,16 @@ def setup(hass, config): 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 43ddee3ba02..308d659e111 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -17,7 +17,7 @@ from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING, 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 725043c4da8..ddfbc68d974 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -15,7 +15,6 @@ from homeassistant.const import ( ) 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 791fec791f8..4b2cd10b781 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -12,7 +12,6 @@ from homeassistant.helpers.entity import Entity 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 c9bbdaeb0a4..a32a64e81a3 100644 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ b/homeassistant/components/thermostat/eq3btsmart.py @@ -10,7 +10,7 @@ from homeassistant.components.thermostat import ThermostatDevice 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 e8981e520c8..55e97f67c7e 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 @@ DEFAULT_COMPONENTS = { '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 @@ CORE_CONFIG_SCHEMA = vol.Schema({ 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 d3eed6ce5e0..cbf02ea587f 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -681,6 +681,7 @@ class Config(object): """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 a596d9bc476..a9b980bc871 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -8,7 +8,8 @@ import math 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 @@ LocationInfo = collections.namedtuple( 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 a131813edbd..42795291eb1 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 5ec8619b37f..649859f2506 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 c1f50d86dfb..a931604fdce 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 c647c4ae017..00000000000 --- 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 8226cbbf96e..00000000000 --- 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 98c61dfc16e..26d466bc4b8 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 bfda22596c2..55bdec20a35 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 @@ class TestForecastSetup(unittest.TestCase): 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 43a14578690..3ea94938f0d 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 @@ class TestSensorYr: 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 68b0ca3be35..7abaf63b407 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -131,7 +131,7 @@ class TestComponentsCore(unittest.TestCase): 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 00000000000..8afdaba070e --- /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 00000000000..95eeb0fe239 --- /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 00000000000..d31d4560589 --- /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 00000000000..b181fdfd85b --- /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 152818d02e4..34aaa1b83ed 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 threading 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 @@ class TestBootstrap: fp.write('{}:\n'.format(comp).encode('utf-8')) fp.flush() - hass = bootstrap.from_config_file(fp.name) + self.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') - - 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 @@ class TestBootstrap: 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 @@ class TestBootstrap: '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 8a5ec306b3b..6be3f585967 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 @@ class TestConfig(unittest.TestCase): [('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 @@ class TestConfig(unittest.TestCase): 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 @@ class TestConfig(unittest.TestCase): }, }, }) + + 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 7d0052fe62c..1dfb71a87bf 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_KM = 5846.39 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 ea9a8f23dfa..a4e00196959 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -49,7 +49,7 @@ class TestPackageUtil(unittest.TestCase): 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 1f77aac5ad4..218c202bcc2 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -25,7 +25,7 @@ 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.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 42795291eb1..a6faa5a3e3e 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 8259d043cf3..eb6e15379d8 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -15,7 +15,7 @@ from homeassistant.const import ( 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 a6faa5a3e3e..9f92877194d 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 e9ae7de81bc..311d3fe83df 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -23,7 +23,7 @@ DEFAULT_DATABASE = 'home_assistant' 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 9f92877194d..2be4536345d 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 08ea2099445..5452229ee54 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -6,26 +6,6 @@ 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 @@ -55,14 +35,12 @@ SUPPORT_HM_EVENT_AS_BINMOD = [ 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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): """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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): # 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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): 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 7b3e265a9dd..a0a31566661 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -11,7 +11,6 @@ homematic: 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)) + _LOGGER.debug("Add device %s from config: %s", + str(hmdevicetype), str(config)) - return True + # create object and add to HA + new_device = hmdevicetype(config) + add_callback_devices([new_device]) + # link to HM + new_device.link_homematic() -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 - - _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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): """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 159f3e4dbdc..b1f08db0783 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -6,14 +6,6 @@ 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 @@ -29,14 +21,12 @@ DEPENDENCIES = ['homematic'] 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 737a7eb017d..9bdad7ee68c 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -6,13 +6,6 @@ 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 @@ -29,14 +22,12 @@ DEPENDENCIES = ['homematic'] 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 f6f3825199b..2efa4fdef38 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -6,14 +6,6 @@ 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 @@ -41,14 +33,12 @@ HM_UNIT_HA_CAST = { 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 16cc63a6708..3e6a83afcc4 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -6,14 +6,6 @@ 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 @@ -28,14 +20,12 @@ DEPENDENCIES = ['homematic'] 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 d7675a5cd47..3a537792522 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -6,13 +6,6 @@ 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 @@ -28,14 +21,12 @@ _LOGGER = logging.getLogger(__name__) 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 218c202bcc2..d170f2a713e 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -40,7 +40,8 @@ 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 +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 @@ class HomeAssistantWSGI(object): 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 6941fc952a6..a2a3f0811f2 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -12,21 +12,24 @@ from glob import glob 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 @@ class OneWire(Entity): 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 cd16dc4a620..98372a3f65d 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -77,7 +77,8 @@ class RokuDevice(MediaPlayerDevice): 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 5452229ee54..1cd0d87c13b 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -27,11 +27,6 @@ SENSOR_TYPES_CLASS = { "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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): _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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): """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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): _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 a0a31566661..a5261b20a2f 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -20,11 +20,10 @@ from homeassistant.helpers import discovery 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_ROLLERSHUTTER = "homematic.rollershutter" 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 @@ HM_ATTRIBUTE_SUPPORT = { "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 @@ class HMDevice(Entity): """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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): 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 2be4536345d..88a1bed04fd 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 d9c2b7d577a..9ec85e63503 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -7,10 +7,12 @@ at https://home-assistant.io/components/sensor.wink/ 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 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity): 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 18ec6f2ba56..73692290f50 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/garage_door.wink/ 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 @@ class WinkGarageDoorDevice(GarageDoorDevice): 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 2438cdaab9a..5fdec96f5d4 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -8,12 +8,13 @@ import logging 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 @@ class WinkLight(Light): """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 @@ class WinkLight(Light): 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 2572796df35..7551302499a 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/lock.wink/ 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 @@ class WinkLockDevice(LockDevice): 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 e01b2573ac6..8a31148da01 100644 --- a/homeassistant/components/rollershutter/wink.py +++ b/homeassistant/components/rollershutter/wink.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/rollershutter.wink/ 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 3fb914d6cd9..ac885152a2e 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -7,11 +7,12 @@ at https://home-assistant.io/components/sensor.wink/ 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 @@ class WinkSensorDevice(Entity): 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 @@ class WinkSensorDevice(Entity): """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 a5b67f5ddcf..64c19e34bc9 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -6,10 +6,11 @@ https://home-assistant.io/components/switch.wink/ """ 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 85bc7f46cef..4e9fec77ba5 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -5,13 +5,17 @@ For more details about this component, please refer to the documentation at 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 @@ class WinkToggleDevice(ToggleEntity): """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 88a1bed04fd..dd97c51185c 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 00000000000..2b34b209e77 --- /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 ee55ec858cc..455927ca999 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -13,12 +13,13 @@ from homeassistant.helpers import discovery 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 @@ DEVICE_CATEGORIES = { 'Switch': 'switch', 'Armable Sensor': 'switch', 'On/Off Switch': 'switch', + 'Doorlock': 'lock', # 'Window Covering': NOT SUPPORTED YET } @@ -91,7 +93,7 @@ def setup(hass, base_config): 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 dd97c51185c..af332e70fde 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 a38ee76b3bb..99b96b971c9 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') + start = config.get(CONF_START) + destination = config.get(CONF_DESTINATION) - if start is None: - _LOGGER.error('Missing required variable: "from"') - return False - - 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 @@ class DeutscheBahnSensor(Entity): @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 @@ class SchieneData(object): @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 1cd0d87c13b..8e874079ee6 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 @@ SENSOR_TYPES_CLASS = { 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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): 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 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): 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 cda48d1ddfa..ee67d097286 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 a5261b20a2f..fb31408bd82 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 @@ class HMDevice(Entity): @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 @@ class HMDevice(Entity): @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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): _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 @@ class HMDevice(Entity): 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 @@ class HMDevice(Entity): 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 adb10a20fda..2c9db86e662 100644 --- a/homeassistant/components/light/enocean.py +++ b/homeassistant/components/light/enocean.py @@ -4,7 +4,6 @@ Support for EnOcean light sources. 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 @@ class EnOceanLight(enocean.EnOceanDevice, Light): 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 b1f08db0783..b7e0328a574 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 @@ 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.""" + """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 @@ class HMLight(homematic.HMDevice, Light): @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 @@ class HMLight(homematic.HMDevice, Light): 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 33c759b21d5..243d11116da 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 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) 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 setup_bridge(bridge, add_devices_callback): 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 2b34b209e77..f10b8857499 100644 --- a/homeassistant/components/lock/vera.py +++ b/homeassistant/components/lock/vera.py @@ -18,14 +18,14 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class VeraLock(VeraDevice, LockDevice): 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 @@ class VeraLock(VeraDevice, LockDevice): 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 ea316f57425..ef5f7516827 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 @@ SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ 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 308d659e111..4726a1fa6a9 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 @@ SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ 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 98372a3f65d..6ff1ae1510f 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -4,7 +4,6 @@ Support for the roku media player. 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 44cdd414da4..998490fb9b9 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -4,7 +4,6 @@ Support for interacting with Snapcast clients. 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 f5fa8cc486c..8bfdeebf85d 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller. 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 f95e5c36233..920dfc46a90 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 @@ class OpenexchangeratesData(object): @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 1ba8593650e..f956ec5037f 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 3e6a83afcc4..e9f103b95fa 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 @@ DEPENDENCIES = ['homematic'] 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 @@ class HMSwitch(homematic.HMDevice, SwitchDevice): 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 3bc4484db38..46adc5a7052 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 28a44249e12..779f4759442 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -52,7 +52,7 @@ class WOLSwitch(SwitchDevice): @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 a32a64e81a3..17f166a297e 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 3a537792522..345b8785b42 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 @@ _LOGGER = logging.getLogger(__name__) 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 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice): 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 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice): 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 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice): 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 432ff73c367..2a14af969fb 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -15,7 +15,7 @@ from homeassistant.const import ( 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 af332e70fde..7ee914b2487 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 | 128 +++++++++--------- tests/components/test_frontend.py | 7 +- tests/components/test_http.py | 9 +- tests/test_remote.py | 18 +-- 13 files changed, 168 insertions(+), 153 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b538a62d008..f0073bad838 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -6,7 +6,7 @@ https://home-assistant.io/developers/api/ """ 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 @@ class APIEventStream(HomeAssistantView): 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 @@ class APIEventStream(HomeAssistantView): _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 87342528987..2f23118a1c3 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at 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 @@ class Camera(Entity): 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 @@ class Camera(Entity): 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 d170f2a713e..11aa18cad5c 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -13,19 +13,19 @@ import re 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 @@ class HomeAssistantWSGI(object): 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 @@ class HomeAssistantWSGI(object): 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 @@ class HomeAssistantWSGI(object): """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 cbf02ea587f..82ec20c82f9 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -49,6 +49,19 @@ MIN_WORKER_THREAD = 2 _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 @@ class HomeAssistant(object): 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 @@ class HomeAssistant(object): 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 b2dfc3ae18f..6c49decdff2 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -11,6 +11,7 @@ from datetime import datetime import enum import json import logging +import time import threading import urllib.parse @@ -123,6 +124,7 @@ class HomeAssistant(ha.HomeAssistant): 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 @@ class HomeAssistant(ha.HomeAssistant): 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 @@ class HomeAssistant(ha.HomeAssistant): 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 @@ class HomeAssistant(ha.HomeAssistant): # 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 7ee914b2487..2dc4da44710 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 b574e156931..fbce912c3d6 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ REQUIRES = [ '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 7445b5daf8c..427980be5f1 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 e1eb257577c..97d73b8b49d 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 8d1ee1c4ad5..752980e65c8 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 @@ class TestAPI(unittest.TestCase): 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) + # 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) + # 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 @@ class TestAPI(unittest.TestCase): 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: + 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()) - # self.assertEqual(listen_count + 1, self._listen_count()) + hass.bus.fire('test_event') - # hass.bus.fire('test_event') + data = self._stream_next_event(stream) - # data = self._stream_next_event(req) + self.assertEqual('test_event', data['event_type']) - # 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()) - # 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(stream) + self.assertEqual('test_event1', data['event_type']) - # 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') - # hass.bus.fire('test_event2') - # hass.bus.fire('test_event3') + data = self._stream_next_event(stream) + self.assertEqual('test_event3', data['event_type']) - # 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: + if dat == b'\n' and last_new_line: + break + data += dat + last_new_line = dat == b'\n' - # 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:] - # conv = data.decode('utf-8').strip()[6:] + if conv != 'ping': + break - # if conv != 'ping': - # break + return json.loads(conv) - # return json.loads(conv) - - # def _listen_count(self): - # """Return number of event listeners.""" - # return sum(hass.bus.listeners.values()) + 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 61e33931c24..083ebd2eb0c 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 f665a9530c8..6ab79f3e0cc 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 @@ class TestHttp: 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 @@ class TestHttp: 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 893f02bea31..f3ec35daee5 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 @@ class TestRemoteClasses(unittest.TestCase): 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 @@ class TestRemoteClasses(unittest.TestCase): """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 @@ class TestRemoteClasses(unittest.TestCase): """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 @@ class TestRemoteClasses(unittest.TestCase): 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 | 76 +++++++++---------- 2 files changed, 48 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index d1ef92ee4d5..9ab831bdbb4 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 9239f1edae8..7d0cd12175a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -34,11 +34,12 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ 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,47 +73,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) _LOGGER.info('Added %s Sonos speakers', len(players)) + 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: + _devices = [device for device in devices + if device.entity_id == entity_id] + else: + _devices = devices + + for device in _devices: + service_func(device, *service_func_args) + device.update_ha_state(True) + def group_players_service(service): """Group media players, use player as coordinator.""" - entity_id = service.data.get('entity_id') + _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.group_players() - device.update_ha_state(True) - - def snapshot(service): + def snapshot_service(service): """Take a snapshot.""" - entity_id = service.data.get('entity_id') + _apply_service(service, SonosDevice.snapshot) - if entity_id: - _devices = [device for device in devices - if device.entity_id == entity_id] - else: - _devices = devices - - for device in _devices: - device.snapshot(service) - device.update_ha_state(True) - - 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 setup_platform(hass, config, add_devices, discovery_info=None): 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 @@ class SonosDevice(MediaPlayerDevice): 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 fb31408bd82..b2bfc94087a 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 2efa4fdef38..efd22b6cd69 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -17,7 +17,8 @@ _LOGGER = logging.getLogger(__name__) 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 b2bfc94087a..f2d71bb409a 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 2a14af969fb..3af270a05b0 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -301,4 +301,4 @@ class KodiDevice(MediaPlayerDevice): 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 92909f35435..ab9fc35ee4c 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 = '{}.{}'