From 45fe37a301e4932f0ba2859e8263642b9b7bc5ed Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Wed, 4 Nov 2015 04:53:59 +0100 Subject: [PATCH 001/129] Add mysensors component and switch platform * Add a general mysensors component. This sets up the serial comm with the gateway through pymysensors. The component also contains a decorator function for the callback function of mysensors platforms. Mysensors platforms should create a function that listens for the node update event fired by the mysensors component. This function should call another function, that uses the decorator, and returns a dict. The dict should contain a list of which mysensors V_TYPE values the platform handles, the platfrom class and the add_devices function (from setup_platform). * Change existing mysensors sensor platform to depend on the new mysensors component. * Add a mysensors switch platform. The switch platform takes advantage of new functionality from the the fork of pymysensors https://github.com/MartinHjelmare/pymysensors, that enables the gateway to send commands to change node child values. * Change const and is_metric to global constants, in the mysensors component and import const depending on the mysensors version used. * Change variables devices and gateway to global variables. * Add some debug logging at INFO log level. --- homeassistant/components/mysensors.py | 150 +++++++++++++++++++ homeassistant/components/sensor/mysensors.py | 105 ++++--------- homeassistant/components/switch/mysensors.py | 138 +++++++++++++++++ 3 files changed, 321 insertions(+), 72 deletions(-) create mode 100644 homeassistant/components/mysensors.py create mode 100644 homeassistant/components/switch/mysensors.py diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py new file mode 100644 index 00000000000..6bffe9afd2c --- /dev/null +++ b/homeassistant/components/mysensors.py @@ -0,0 +1,150 @@ +""" +homeassistant.components.mysensors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +MySensors component that connects to a MySensors gateway via pymysensors +API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mysensors.html +""" +import logging + +from homeassistant.helpers import (validate_config) + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + TEMP_CELCIUS) + +CONF_PORT = 'port' +CONF_DEBUG = 'debug' +CONF_PERSISTENCE = 'persistence' +CONF_PERSISTENCE_FILE = 'persistence_file' +CONF_VERSION = 'version' + +DOMAIN = 'mysensors' +DEPENDENCIES = [] +REQUIREMENTS = ['file:///home/martin/Dev/pymysensors-fifo_queue.zip' + '#pymysensors==0.3'] +_LOGGER = logging.getLogger(__name__) +ATTR_NODE_ID = 'node_id' +ATTR_CHILD_ID = 'child_id' + +PLATFORM_FORMAT = '{}.{}' +IS_METRIC = None +DEVICES = None +GATEWAY = None + +EVENT_MYSENSORS_NODE_UPDATE = 'MYSENSORS_NODE_UPDATE' +UPDATE_TYPE = 'update_type' +NODE_ID = 'nid' + +CONST = None + + +def setup(hass, config): + """ Setup the MySensors component. """ + + import mysensors.mysensors as mysensors + + if not validate_config(config, + {DOMAIN: [CONF_PORT]}, + _LOGGER): + return False + + version = config[DOMAIN].get(CONF_VERSION, '1.4') + + global CONST + if version == '1.4': + import mysensors.const_14 as const + CONST = const + _LOGGER.info('CONST = %s, 1.4', const) + elif version == '1.5': + import mysensors.const_15 as const + CONST = const + _LOGGER.info('CONST = %s, 1.5', const) + else: + import mysensors.const_14 as const + CONST = const + _LOGGER.info('CONST = %s, 1.4 default', const) + + global IS_METRIC + # Just assume celcius means that the user wants metric for now. + # It may make more sense to make this a global config option in the future. + IS_METRIC = (hass.config.temperature_unit == TEMP_CELCIUS) + global DEVICES + DEVICES = {} # keep track of devices added to HA + + def node_update(update_type, nid): + """ Callback for node updates from the MySensors gateway. """ + _LOGGER.info('update %s: node %s', update_type, nid) + + hass.bus.fire(EVENT_MYSENSORS_NODE_UPDATE, { + UPDATE_TYPE: update_type, + NODE_ID: nid + }) + + port = config[DOMAIN].get(CONF_PORT) + + persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) + persistence_file = config[DOMAIN].get( + CONF_PERSISTENCE_FILE, hass.config.path('mysensors.pickle')) + + global GATEWAY + GATEWAY = mysensors.SerialGateway(port, node_update, + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + GATEWAY.metric = IS_METRIC + GATEWAY.debug = config[DOMAIN].get(CONF_DEBUG, False) + GATEWAY.start() + + if persistence: + for nid in GATEWAY.sensors: + node_update('node_update', nid) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: GATEWAY.stop()) + + return True + + +def mysensors_update(platform_type): + """ + Decorator for callback function for sensor updates from the MySensors + component. + """ + def wrapper(gateway, devices, nid): + """Wrapper function in the decorator.""" + sensor = gateway.sensors[nid] + if sensor.sketch_name is None: + _LOGGER.info('No sketch_name: node %s', nid) + return + if nid not in devices: + devices[nid] = {} + node = devices[nid] + new_devices = [] + platform_def = platform_type(gateway, devices, nid) + platform_object = platform_def['platform_class'] + platform_v_types = platform_def['types_to_handle'] + add_devices = platform_def['add_devices'] + for child_id, child in sensor.children.items(): + if child_id not in node: + node[child_id] = {} + for value_type, value in child.values.items(): + if value_type not in node[child_id]: + name = '{} {}.{}'.format( + sensor.sketch_name, nid, child.id) + if value_type in platform_v_types: + node[child_id][value_type] = \ + platform_object( + gateway, nid, child_id, name, value_type) + new_devices.append(node[child_id][value_type]) + else: + node[child_id][value_type].update_sensor( + value, sensor.battery_level) + _LOGGER.info('sensor_update: %s', new_devices) + if new_devices: + _LOGGER.info('adding new devices: %s', new_devices) + add_devices(new_devices) + return + return wrapper diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index cb959522134..b49fe706f78 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -11,102 +11,63 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, + ATTR_BATTERY_LEVEL, TEMP_CELCIUS, TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) -CONF_PORT = "port" -CONF_DEBUG = "debug" -CONF_PERSISTENCE = "persistence" -CONF_PERSISTENCE_FILE = "persistence_file" -CONF_VERSION = "version" +import homeassistant.components.mysensors as mysensors ATTR_NODE_ID = "node_id" ATTR_CHILD_ID = "child_id" _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/' - 'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip' - '#pymysensors==0.3'] +DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): - """ Setup the mysensors platform. """ + """ Setup the mysensors platform for sensors. """ - import mysensors.mysensors as mysensors - import mysensors.const_14 as const + v_types = [] + for _, member in mysensors.CONST.SetReq.__members__.items(): + if (member.value != mysensors.CONST.SetReq.V_STATUS and + member.value != mysensors.CONST.SetReq.V_LIGHT and + member.value != mysensors.CONST.SetReq.V_LOCK_STATUS): + v_types.append(member) - devices = {} # keep track of devices added to HA - # Just assume celcius means that the user wants metric for now. - # It may make more sense to make this a global config option in the future. - is_metric = (hass.config.temperature_unit == TEMP_CELCIUS) + @mysensors.mysensors_update + def _sensor_update(gateway, devices, nid): + """Internal callback for sensor updates.""" + _LOGGER.info("sensor update = %s", devices) + return {'types_to_handle': v_types, + 'platform_class': MySensorsSensor, + 'add_devices': add_devices} - def sensor_update(update_type, nid): - """ Callback for sensor updates from the MySensors gateway. """ - _LOGGER.info("sensor_update %s: node %s", update_type, nid) - sensor = gateway.sensors[nid] - if sensor.sketch_name is None: - return - if nid not in devices: - devices[nid] = {} + def sensor_update(event): + """ Callback for sensor updates from the MySensors component. """ + _LOGGER.info( + 'update %s: node %s', event.data[mysensors.UPDATE_TYPE], + event.data[mysensors.NODE_ID]) + _sensor_update(mysensors.GATEWAY, mysensors.DEVICES, + event.data[mysensors.NODE_ID]) - node = devices[nid] - new_devices = [] - for child_id, child in sensor.children.items(): - if child_id not in node: - node[child_id] = {} - for value_type, value in child.values.items(): - if value_type not in node[child_id]: - name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id) - node[child_id][value_type] = \ - MySensorsNodeValue( - nid, child_id, name, value_type, is_metric, const) - new_devices.append(node[child_id][value_type]) - else: - node[child_id][value_type].update_sensor( - value, sensor.battery_level) - - if new_devices: - _LOGGER.info("adding new devices: %s", new_devices) - add_devices(new_devices) - - port = config.get(CONF_PORT) - if port is None: - _LOGGER.error("Missing required key 'port'") - return False - - persistence = config.get(CONF_PERSISTENCE, True) - persistence_file = config.get(CONF_PERSISTENCE_FILE, 'mysensors.pickle') - version = config.get(CONF_VERSION, '1.4') - - gateway = mysensors.SerialGateway(port, sensor_update, - persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - gateway.metric = is_metric - gateway.debug = config.get(CONF_DEBUG, False) - gateway.start() - - if persistence: - for nid in gateway.sensors: - sensor_update('sensor_update', nid) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) + hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) -class MySensorsNodeValue(Entity): +class MySensorsSensor(Entity): + """ Represents the value of a MySensors child node. """ # pylint: disable=too-many-arguments, too-many-instance-attributes - def __init__(self, node_id, child_id, name, value_type, metric, const): + + def __init__(self, gateway, node_id, child_id, name, value_type): + self.gateway = gateway self._name = name self.node_id = node_id self.child_id = child_id self.battery_level = 0 self.value_type = value_type - self.metric = metric + self.metric = mysensors.IS_METRIC self._value = '' - self.const = const + self.const = mysensors.CONST @property def should_poll(self): @@ -144,7 +105,7 @@ class MySensorsNodeValue(Entity): } def update_sensor(self, value, battery_level): - """ Update a sensor with the latest value from the controller. """ + """ Update the controller with the latest value from a sensor. """ _LOGGER.info("%s value = %s", self._name, value) if self.value_type == self.const.SetReq.V_TRIPPED or \ self.value_type == self.const.SetReq.V_ARMED: diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py new file mode 100644 index 00000000000..9c67ed44b93 --- /dev/null +++ b/homeassistant/components/switch/mysensors.py @@ -0,0 +1,138 @@ +""" +homeassistant.components.sensor.mysensors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for MySensors switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mysensors.html +""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + TEMP_CELCIUS, TEMP_FAHRENHEIT, + STATE_ON, STATE_OFF) + +import homeassistant.components.mysensors as mysensors + +ATTR_NODE_ID = "node_id" +ATTR_CHILD_ID = "child_id" + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['mysensors'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup the mysensors platform for switches. """ + + v_types = [] + for _, member in mysensors.CONST.SetReq.__members__.items(): + if (member.value == mysensors.CONST.SetReq.V_STATUS or + member.value == mysensors.CONST.SetReq.V_LIGHT or + member.value == mysensors.CONST.SetReq.V_LOCK_STATUS): + v_types.append(member) + + @mysensors.mysensors_update + def _sensor_update(gateway, devices, nid): + """Internal callback for sensor updates.""" + _LOGGER.info("sensor update = %s", devices) + return {'types_to_handle': v_types, + 'platform_class': MySensorsSwitch, + 'add_devices': add_devices} + + def sensor_update(event): + """ Callback for sensor updates from the MySensors component. """ + _LOGGER.info( + 'update %s: node %s', event.data[mysensors.UPDATE_TYPE], + event.data[mysensors.NODE_ID]) + _sensor_update(mysensors.GATEWAY, mysensors.DEVICES, + event.data[mysensors.NODE_ID]) + + hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) + + +class MySensorsSwitch(SwitchDevice): + + """ Represents the value of a MySensors child node. """ + # pylint: disable=too-many-arguments, too-many-instance-attributes + + def __init__(self, gateway, node_id, child_id, name, value_type): + self.gateway = gateway + self._name = name + self.node_id = node_id + self.child_id = child_id + self.battery_level = 0 + self.value_type = value_type + self.metric = mysensors.IS_METRIC + self._value = STATE_OFF + self.const = mysensors.CONST + + @property + def should_poll(self): + """ MySensor gateway pushes its state to HA. """ + return False + + @property + def name(self): + """ The name of this sensor. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._value + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity. """ + if self.value_type == self.const.SetReq.V_TEMP: + return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT + elif self.value_type == self.const.SetReq.V_HUM or \ + self.value_type == self.const.SetReq.V_DIMMER or \ + self.value_type == self.const.SetReq.V_LIGHT_LEVEL: + return '%' + return None + + @property + def state_attributes(self): + """ Returns the state attributes. """ + return { + ATTR_NODE_ID: self.node_id, + ATTR_CHILD_ID: self.child_id, + ATTR_BATTERY_LEVEL: self.battery_level, + } + + @property + def is_on(self): + """ Returns True if switch is on. """ + return self._value == STATE_ON + + def turn_on(self): + """ Turns the switch on. """ + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 1) + self._value = STATE_ON + self.update_ha_state() + + def turn_off(self): + """ Turns the pin to low/off. """ + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 0) + self._value = STATE_OFF + self.update_ha_state() + + def update_sensor(self, value, battery_level): + """ Update the controller with the latest value from a sensor. """ + _LOGGER.info("%s value = %s", self._name, value) + if self.value_type == self.const.SetReq.V_TRIPPED or \ + self.value_type == self.const.SetReq.V_ARMED or \ + self.value_type == self.const.SetReq.V_STATUS or \ + self.value_type == self.const.SetReq.V_LIGHT or \ + self.value_type == self.const.SetReq.V_LOCK_STATUS: + self._value = STATE_ON if int(value) == 1 else STATE_OFF + else: + self._value = value + self.battery_level = battery_level + self.update_ha_state() From 59524c7933c84680c5fb1b164814cd9b363d5727 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Sun, 6 Dec 2015 00:29:03 +0100 Subject: [PATCH 002/129] Add multiple gateways * Add support for multiple serial gateways. * Fix serialization of python objects by adding dict representation of classes. * Add support for showing more than one child value type per entity. The entity state is always only one value type. This is defined by the platform value types. Value types that are not defined as the platform value type are shown as state_attributes. * Add more unit of measurement types. * Clean up code. --- homeassistant/components/mysensors.py | 137 +++++++++++-------- homeassistant/components/sensor/mysensors.py | 112 ++++++++++----- homeassistant/components/switch/mysensors.py | 128 ++++++++++------- requirements_all.txt | 2 +- 4 files changed, 240 insertions(+), 139 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 6bffe9afd2c..59724a7d810 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -23,27 +23,25 @@ CONF_VERSION = 'version' DOMAIN = 'mysensors' DEPENDENCIES = [] -REQUIREMENTS = ['file:///home/martin/Dev/pymysensors-fifo_queue.zip' - '#pymysensors==0.3'] +REQUIREMENTS = [ + 'https://github.com/MartinHjelmare/pymysensors/archive/fifo_queue.zip' + '#pymysensors==0.3'] _LOGGER = logging.getLogger(__name__) +ATTR_PORT = 'port' +ATTR_DEVICES = 'devices' ATTR_NODE_ID = 'node_id' ATTR_CHILD_ID = 'child_id' +ATTR_UPDATE_TYPE = 'update_type' -PLATFORM_FORMAT = '{}.{}' IS_METRIC = None -DEVICES = None -GATEWAY = None - -EVENT_MYSENSORS_NODE_UPDATE = 'MYSENSORS_NODE_UPDATE' -UPDATE_TYPE = 'update_type' -NODE_ID = 'nid' - CONST = None +GATEWAYS = None +EVENT_MYSENSORS_NODE_UPDATE = 'MYSENSORS_NODE_UPDATE' -def setup(hass, config): +def setup(hass, config): # noqa """ Setup the MySensors component. """ - + # pylint:disable=no-name-in-module import mysensors.mysensors as mysensors if not validate_config(config, @@ -57,53 +55,83 @@ def setup(hass, config): if version == '1.4': import mysensors.const_14 as const CONST = const - _LOGGER.info('CONST = %s, 1.4', const) elif version == '1.5': import mysensors.const_15 as const CONST = const - _LOGGER.info('CONST = %s, 1.5', const) else: import mysensors.const_14 as const CONST = const - _LOGGER.info('CONST = %s, 1.4 default', const) - global IS_METRIC # Just assume celcius means that the user wants metric for now. # It may make more sense to make this a global config option in the future. + global IS_METRIC IS_METRIC = (hass.config.temperature_unit == TEMP_CELCIUS) - global DEVICES - DEVICES = {} # keep track of devices added to HA - def node_update(update_type, nid): - """ Callback for node updates from the MySensors gateway. """ - _LOGGER.info('update %s: node %s', update_type, nid) + def callback_generator(port, devices): + """ + Generator of callback, should be run once per gateway setup. + """ + def node_update(update_type, nid): + """ Callback for node updates from the MySensors gateway. """ + _LOGGER.info('update %s: node %s', update_type, nid) - hass.bus.fire(EVENT_MYSENSORS_NODE_UPDATE, { - UPDATE_TYPE: update_type, - NODE_ID: nid - }) + hass.bus.fire(EVENT_MYSENSORS_NODE_UPDATE, { + ATTR_PORT: port, + ATTR_DEVICES: devices, + ATTR_UPDATE_TYPE: update_type, + ATTR_NODE_ID: nid + }) + return + return node_update + + def setup_gateway(port, persistence, persistence_file): + """ + Instantiate gateway, set gateway attributes and start gateway. + If persistence is true, update all nodes. + Listen for stop of home-assistant, then stop gateway. + """ + devices = {} # keep track of devices added to HA + gateway = mysensors.SerialGateway(port, + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + gateway.event_callback = callback_generator(port, devices) + gateway.metric = IS_METRIC + gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) + gateway.start() + + if persistence: + for nid in gateway.sensors: + gateway.event_callback('node_update', nid) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: gateway.stop()) + return gateway port = config[DOMAIN].get(CONF_PORT) - - persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) persistence_file = config[DOMAIN].get( CONF_PERSISTENCE_FILE, hass.config.path('mysensors.pickle')) - global GATEWAY - GATEWAY = mysensors.SerialGateway(port, node_update, - persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) - GATEWAY.metric = IS_METRIC - GATEWAY.debug = config[DOMAIN].get(CONF_DEBUG, False) - GATEWAY.start() + if isinstance(port, str): + port = [port] + if isinstance(persistence_file, str): + persistence_file = [persistence_file] - if persistence: - for nid in GATEWAY.sensors: - node_update('node_update', nid) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: GATEWAY.stop()) + # Setup all ports from config + global GATEWAYS + GATEWAYS = {} + for index, port_item in enumerate(port): + persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) + try: + persistence_f_item = persistence_file[index] + except IndexError: + _LOGGER.exception( + 'No persistence_file is set for port %s,' + ' disabling persistence', port_item) + persistence = False + persistence_f_item = None + GATEWAYS[port_item] = setup_gateway( + port_item, persistence, persistence_f_item) return True @@ -113,7 +141,7 @@ def mysensors_update(platform_type): Decorator for callback function for sensor updates from the MySensors component. """ - def wrapper(gateway, devices, nid): + def wrapper(gateway, port, devices, nid): """Wrapper function in the decorator.""" sensor = gateway.sensors[nid] if sensor.sketch_name is None: @@ -123,26 +151,23 @@ def mysensors_update(platform_type): devices[nid] = {} node = devices[nid] new_devices = [] - platform_def = platform_type(gateway, devices, nid) - platform_object = platform_def['platform_class'] - platform_v_types = platform_def['types_to_handle'] - add_devices = platform_def['add_devices'] + # Get platform specific V_TYPES, class and add_devices function. + platform_v_types, platform_class, add_devices = platform_type( + gateway, port, devices, nid) for child_id, child in sensor.children.items(): if child_id not in node: node[child_id] = {} - for value_type, value in child.values.items(): - if value_type not in node[child_id]: + for value_type, _ in child.values.items(): + if ((value_type not in node[child_id]) and + (value_type in platform_v_types)): name = '{} {}.{}'.format( sensor.sketch_name, nid, child.id) - if value_type in platform_v_types: - node[child_id][value_type] = \ - platform_object( - gateway, nid, child_id, name, value_type) - new_devices.append(node[child_id][value_type]) - else: + node[child_id][value_type] = platform_class( + port, nid, child_id, name, value_type) + new_devices.append(node[child_id][value_type]) + elif value_type in platform_v_types: node[child_id][value_type].update_sensor( - value, sensor.battery_level) - _LOGGER.info('sensor_update: %s', new_devices) + child.values, sensor.battery_level) if new_devices: _LOGGER.info('adding new devices: %s', new_devices) add_devices(new_devices) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 4e9e03da0d0..c16980f7587 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -17,9 +17,6 @@ from homeassistant.const import ( import homeassistant.components.mysensors as mysensors -ATTR_NODE_ID = "node_id" -ATTR_CHILD_ID = "child_id" - _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mysensors'] @@ -27,28 +24,29 @@ DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup the mysensors platform for sensors. """ + # Define the V_TYPES that the platform should handle as states. v_types = [] for _, member in mysensors.CONST.SetReq.__members__.items(): - if (member.value != mysensors.CONST.SetReq.V_STATUS and + if (member.value != mysensors.CONST.SetReq.V_ARMED and + member.value != mysensors.CONST.SetReq.V_STATUS and member.value != mysensors.CONST.SetReq.V_LIGHT and member.value != mysensors.CONST.SetReq.V_LOCK_STATUS): v_types.append(member) @mysensors.mysensors_update - def _sensor_update(gateway, devices, nid): + def _sensor_update(gateway, port, devices, nid): """Internal callback for sensor updates.""" - _LOGGER.info("sensor update = %s", devices) - return {'types_to_handle': v_types, - 'platform_class': MySensorsSensor, - 'add_devices': add_devices} + return (v_types, MySensorsSensor, add_devices) def sensor_update(event): """ Callback for sensor updates from the MySensors component. """ _LOGGER.info( - 'update %s: node %s', event.data[mysensors.UPDATE_TYPE], - event.data[mysensors.NODE_ID]) - _sensor_update(mysensors.GATEWAY, mysensors.DEVICES, - event.data[mysensors.NODE_ID]) + 'update %s: node %s', event.data[mysensors.ATTR_UPDATE_TYPE], + event.data[mysensors.ATTR_NODE_ID]) + _sensor_update(mysensors.GATEWAYS[event.data[mysensors.ATTR_PORT]], + event.data[mysensors.ATTR_PORT], + event.data[mysensors.ATTR_DEVICES], + event.data[mysensors.ATTR_NODE_ID]) hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) @@ -58,16 +56,26 @@ class MySensorsSensor(Entity): """ Represents the value of a MySensors child node. """ # pylint: disable=too-many-arguments, too-many-instance-attributes - def __init__(self, gateway, node_id, child_id, name, value_type): - self.gateway = gateway + def __init__(self, port, node_id, child_id, name, value_type): + self.port = port self._name = name self.node_id = node_id self.child_id = child_id self.battery_level = 0 self.value_type = value_type - self.metric = mysensors.IS_METRIC - self._value = '' - self.const = mysensors.CONST + self._values = {} + + def as_dict(self): + """ Returns a dict representation of this Entity. """ + return { + 'port': self.port, + 'name': self._name, + 'node_id': self.node_id, + 'child_id': self.child_id, + 'battery_level': self.battery_level, + 'value_type': self.value_type, + 'values': self._values, + } @property def should_poll(self): @@ -82,35 +90,69 @@ class MySensorsSensor(Entity): @property def state(self): """ Returns the state of the device. """ - return self._value + if not self._values: + return '' + return self._values[self.value_type] @property def unit_of_measurement(self): """ Unit of measurement of this entity. """ - if self.value_type == self.const.SetReq.V_TEMP: - return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT - elif self.value_type == self.const.SetReq.V_HUM or \ - self.value_type == self.const.SetReq.V_DIMMER or \ - self.value_type == self.const.SetReq.V_LIGHT_LEVEL: + # pylint:disable=too-many-return-statements + if self.value_type == mysensors.CONST.SetReq.V_TEMP: + return TEMP_CELCIUS if mysensors.IS_METRIC else TEMP_FAHRENHEIT + elif self.value_type == mysensors.CONST.SetReq.V_HUM or \ + self.value_type == mysensors.CONST.SetReq.V_DIMMER or \ + self.value_type == mysensors.CONST.SetReq.V_PERCENTAGE or \ + self.value_type == mysensors.CONST.SetReq.V_LIGHT_LEVEL: return '%' + elif self.value_type == mysensors.CONST.SetReq.V_WATT: + return 'W' + elif self.value_type == mysensors.CONST.SetReq.V_KWH: + return 'kWh' + elif self.value_type == mysensors.CONST.SetReq.V_VOLTAGE: + return 'V' + elif self.value_type == mysensors.CONST.SetReq.V_CURRENT: + return 'A' + elif self.value_type == mysensors.CONST.SetReq.V_IMPEDANCE: + return 'ohm' + elif mysensors.CONST.SetReq.V_UNIT_PREFIX in self._values: + return self._values[mysensors.CONST.SetReq.V_UNIT_PREFIX] return None + @property + def device_state_attributes(self): + """ Returns device specific state attributes. """ + device_attr = dict(self._values) + device_attr.pop(self.value_type, None) + return device_attr + @property def state_attributes(self): """ Returns the state attributes. """ - return { - ATTR_NODE_ID: self.node_id, - ATTR_CHILD_ID: self.child_id, + + data = { + mysensors.ATTR_NODE_ID: self.node_id, + mysensors.ATTR_CHILD_ID: self.child_id, ATTR_BATTERY_LEVEL: self.battery_level, } - def update_sensor(self, value, battery_level): - """ Update the controller with the latest value from a sensor. """ - _LOGGER.info("%s value = %s", self._name, value) - if self.value_type == self.const.SetReq.V_TRIPPED or \ - self.value_type == self.const.SetReq.V_ARMED: - self._value = STATE_ON if int(value) == 1 else STATE_OFF - else: - self._value = value + device_attr = self.device_state_attributes + + if device_attr is not None: + data.update(device_attr) + + return data + + def update_sensor(self, values, battery_level): + """ Update the controller with the latest values from a sensor. """ + for value_type, value in values.items(): + _LOGGER.info( + "%s: value_type %s, value = %s", self._name, value_type, value) + if value_type == mysensors.CONST.SetReq.V_TRIPPED: + self._values[value_type] = STATE_ON if int( + value) == 1 else STATE_OFF + else: + self._values[value_type] = value + self.battery_level = battery_level self.update_ha_state() diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 9c67ed44b93..5db5b9d25fc 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -1,5 +1,5 @@ """ -homeassistant.components.sensor.mysensors +homeassistant.components.switch.mysensors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Support for MySensors switches. @@ -17,9 +17,6 @@ from homeassistant.const import ( import homeassistant.components.mysensors as mysensors -ATTR_NODE_ID = "node_id" -ATTR_CHILD_ID = "child_id" - _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mysensors'] @@ -27,28 +24,29 @@ DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup the mysensors platform for switches. """ + # Define the V_TYPES that the platform should handle as states. v_types = [] for _, member in mysensors.CONST.SetReq.__members__.items(): - if (member.value == mysensors.CONST.SetReq.V_STATUS or + if (member.value == mysensors.CONST.SetReq.V_ARMED or + member.value == mysensors.CONST.SetReq.V_STATUS or member.value == mysensors.CONST.SetReq.V_LIGHT or member.value == mysensors.CONST.SetReq.V_LOCK_STATUS): v_types.append(member) @mysensors.mysensors_update - def _sensor_update(gateway, devices, nid): + def _sensor_update(gateway, port, devices, nid): """Internal callback for sensor updates.""" - _LOGGER.info("sensor update = %s", devices) - return {'types_to_handle': v_types, - 'platform_class': MySensorsSwitch, - 'add_devices': add_devices} + return (v_types, MySensorsSwitch, add_devices) def sensor_update(event): """ Callback for sensor updates from the MySensors component. """ _LOGGER.info( - 'update %s: node %s', event.data[mysensors.UPDATE_TYPE], - event.data[mysensors.NODE_ID]) - _sensor_update(mysensors.GATEWAY, mysensors.DEVICES, - event.data[mysensors.NODE_ID]) + 'update %s: node %s', event.data[mysensors.ATTR_UPDATE_TYPE], + event.data[mysensors.ATTR_NODE_ID]) + _sensor_update(mysensors.GATEWAYS[event.data[mysensors.ATTR_PORT]], + event.data[mysensors.ATTR_PORT], + event.data[mysensors.ATTR_DEVICES], + event.data[mysensors.ATTR_NODE_ID]) hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) @@ -58,16 +56,26 @@ class MySensorsSwitch(SwitchDevice): """ Represents the value of a MySensors child node. """ # pylint: disable=too-many-arguments, too-many-instance-attributes - def __init__(self, gateway, node_id, child_id, name, value_type): - self.gateway = gateway + def __init__(self, port, node_id, child_id, name, value_type): + self.port = port self._name = name self.node_id = node_id self.child_id = child_id self.battery_level = 0 self.value_type = value_type - self.metric = mysensors.IS_METRIC - self._value = STATE_OFF - self.const = mysensors.CONST + self._values = {} + + def as_dict(self): + """ Returns a dict representation of this Entity. """ + return { + 'port': self.port, + 'name': self._name, + 'node_id': self.node_id, + 'child_id': self.child_id, + 'battery_level': self.battery_level, + 'value_type': self.value_type, + 'values': self._values, + } @property def should_poll(self): @@ -79,60 +87,86 @@ class MySensorsSwitch(SwitchDevice): """ The name of this sensor. """ return self._name - @property - def state(self): - """ Returns the state of the device. """ - return self._value - @property def unit_of_measurement(self): """ Unit of measurement of this entity. """ - if self.value_type == self.const.SetReq.V_TEMP: - return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT - elif self.value_type == self.const.SetReq.V_HUM or \ - self.value_type == self.const.SetReq.V_DIMMER or \ - self.value_type == self.const.SetReq.V_LIGHT_LEVEL: + # pylint:disable=too-many-return-statements + if self.value_type == mysensors.CONST.SetReq.V_TEMP: + return TEMP_CELCIUS if mysensors.IS_METRIC else TEMP_FAHRENHEIT + elif self.value_type == mysensors.CONST.SetReq.V_HUM or \ + self.value_type == mysensors.CONST.SetReq.V_DIMMER or \ + self.value_type == mysensors.CONST.SetReq.V_PERCENTAGE or \ + self.value_type == mysensors.CONST.SetReq.V_LIGHT_LEVEL: return '%' + elif self.value_type == mysensors.CONST.SetReq.V_WATT: + return 'W' + elif self.value_type == mysensors.CONST.SetReq.V_KWH: + return 'kWh' + elif self.value_type == mysensors.CONST.SetReq.V_VOLTAGE: + return 'V' + elif self.value_type == mysensors.CONST.SetReq.V_CURRENT: + return 'A' + elif self.value_type == mysensors.CONST.SetReq.V_IMPEDANCE: + return 'ohm' + elif mysensors.CONST.SetReq.V_UNIT_PREFIX in self._values: + return self._values[mysensors.CONST.SetReq.V_UNIT_PREFIX] return None + @property + def device_state_attributes(self): + """ Returns device specific state attributes. """ + device_attr = dict(self._values) + device_attr.pop(self.value_type, None) + return device_attr + @property def state_attributes(self): """ Returns the state attributes. """ - return { - ATTR_NODE_ID: self.node_id, - ATTR_CHILD_ID: self.child_id, + + data = { + mysensors.ATTR_NODE_ID: self.node_id, + mysensors.ATTR_CHILD_ID: self.child_id, ATTR_BATTERY_LEVEL: self.battery_level, } + device_attr = self.device_state_attributes + + if device_attr is not None: + data.update(device_attr) + + return data + @property def is_on(self): """ Returns True if switch is on. """ - return self._value == STATE_ON + return self._values[self.value_type] == STATE_ON def turn_on(self): """ Turns the switch on. """ - self.gateway.set_child_value( + mysensors.GATEWAYS[self.port].set_child_value( self.node_id, self.child_id, self.value_type, 1) - self._value = STATE_ON + self._values[self.value_type] = STATE_ON self.update_ha_state() def turn_off(self): """ Turns the pin to low/off. """ - self.gateway.set_child_value( + mysensors.GATEWAYS[self.port].set_child_value( self.node_id, self.child_id, self.value_type, 0) - self._value = STATE_OFF + self._values[self.value_type] = STATE_OFF self.update_ha_state() - def update_sensor(self, value, battery_level): + def update_sensor(self, values, battery_level): """ Update the controller with the latest value from a sensor. """ - _LOGGER.info("%s value = %s", self._name, value) - if self.value_type == self.const.SetReq.V_TRIPPED or \ - self.value_type == self.const.SetReq.V_ARMED or \ - self.value_type == self.const.SetReq.V_STATUS or \ - self.value_type == self.const.SetReq.V_LIGHT or \ - self.value_type == self.const.SetReq.V_LOCK_STATUS: - self._value = STATE_ON if int(value) == 1 else STATE_OFF - else: - self._value = value + for value_type, value in values.items(): + _LOGGER.info( + "%s: value_type %s, value = %s", self._name, value_type, value) + if value_type == mysensors.CONST.SetReq.V_ARMED or \ + value_type == mysensors.CONST.SetReq.V_STATUS or \ + value_type == mysensors.CONST.SetReq.V_LIGHT or \ + value_type == mysensors.CONST.SetReq.V_LOCK_STATUS: + self._values[value_type] = ( + STATE_ON if int(value) == 1 else STATE_OFF) + else: + self._values[value_type] = value self.battery_level = battery_level self.update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 2715ca3288d..87e7555b395 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -119,7 +119,7 @@ py-cpuinfo==0.1.6 python-forecastio==1.3.3 # homeassistant.components.sensor.mysensors -https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3 +https://github.com/MartinHjelmare/pymysensors/archive/fifo_queue.zip#pymysensors==0.3 # homeassistant.components.sensor.openweathermap pyowm==2.2.1 From 7cc707f1ce28e6810cb4cc30c4e9f08f2f25d170 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Tue, 8 Dec 2015 01:03:07 +0100 Subject: [PATCH 003/129] Fix docstrings to conform to pep --- homeassistant/components/mysensors.py | 17 ++---- homeassistant/components/sensor/mysensors.py | 46 +++++++++++------ homeassistant/components/switch/mysensors.py | 54 +++++++++++++------- 3 files changed, 71 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 59724a7d810..1dcd65ab8e6 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -68,11 +68,9 @@ def setup(hass, config): # noqa IS_METRIC = (hass.config.temperature_unit == TEMP_CELCIUS) def callback_generator(port, devices): - """ - Generator of callback, should be run once per gateway setup. - """ + """Return a new callback function. Run once per gateway setup.""" def node_update(update_type, nid): - """ Callback for node updates from the MySensors gateway. """ + """Callback for node updates from the MySensors gateway.""" _LOGGER.info('update %s: node %s', update_type, nid) hass.bus.fire(EVENT_MYSENSORS_NODE_UPDATE, { @@ -85,11 +83,7 @@ def setup(hass, config): # noqa return node_update def setup_gateway(port, persistence, persistence_file): - """ - Instantiate gateway, set gateway attributes and start gateway. - If persistence is true, update all nodes. - Listen for stop of home-assistant, then stop gateway. - """ + """Return gateway after setup of the gateway.""" devices = {} # keep track of devices added to HA gateway = mysensors.SerialGateway(port, persistence=persistence, @@ -137,10 +131,7 @@ def setup(hass, config): # noqa def mysensors_update(platform_type): - """ - Decorator for callback function for sensor updates from the MySensors - component. - """ + """Decorator for callback function for mysensor updates.""" def wrapper(gateway, port, devices, nid): """Wrapper function in the decorator.""" sensor = gateway.sensors[nid] diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index c16980f7587..f1ce4f38271 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -22,8 +22,7 @@ DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): - """ Setup the mysensors platform for sensors. """ - + """Setup the mysensors platform for sensors.""" # Define the V_TYPES that the platform should handle as states. v_types = [] for _, member in mysensors.CONST.SetReq.__members__.items(): @@ -39,7 +38,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return (v_types, MySensorsSensor, add_devices) def sensor_update(event): - """ Callback for sensor updates from the MySensors component. """ + """Callback for sensor updates from the MySensors component.""" _LOGGER.info( 'update %s: node %s', event.data[mysensors.ATTR_UPDATE_TYPE], event.data[mysensors.ATTR_NODE_ID]) @@ -52,21 +51,39 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MySensorsSensor(Entity): + """Represent the value of a MySensors child node.""" - """ Represents the value of a MySensors child node. """ # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, port, node_id, child_id, name, value_type): + """Setup class attributes on instantiation. + + Args: + port (str): Gateway port. + node_id (str): Id of node. + child_id (str): Id of child. + name (str): Sketch name. + value_type (str): Value type of child. Value is entity state. + + Attributes: + port (str): Gateway port. + node_id (str): Id of node. + child_id (str): Id of child. + _name (str): Sketch name. + value_type (str): Value type of child. Value is entity state. + battery_level (int): Node battery level. + _values (dict): Child values. Non state values set as state attributes. + """ self.port = port - self._name = name self.node_id = node_id self.child_id = child_id - self.battery_level = 0 + self._name = name self.value_type = value_type + self.battery_level = 0 self._values = {} def as_dict(self): - """ Returns a dict representation of this Entity. """ + """Return a dict representation of this Entity.""" return { 'port': self.port, 'name': self._name, @@ -79,24 +96,24 @@ class MySensorsSensor(Entity): @property def should_poll(self): - """ MySensor gateway pushes its state to HA. """ + """MySensor gateway pushes its state to HA.""" return False @property def name(self): - """ The name of this sensor. """ + """The name of this sensor.""" return self._name @property def state(self): - """ Returns the state of the device. """ + """Return the state of the device.""" if not self._values: return '' return self._values[self.value_type] @property def unit_of_measurement(self): - """ Unit of measurement of this entity. """ + """Unit of measurement of this entity.""" # pylint:disable=too-many-return-statements if self.value_type == mysensors.CONST.SetReq.V_TEMP: return TEMP_CELCIUS if mysensors.IS_METRIC else TEMP_FAHRENHEIT @@ -121,15 +138,14 @@ class MySensorsSensor(Entity): @property def device_state_attributes(self): - """ Returns device specific state attributes. """ + """Return device specific state attributes.""" device_attr = dict(self._values) device_attr.pop(self.value_type, None) return device_attr @property def state_attributes(self): - """ Returns the state attributes. """ - + """Return the state attributes.""" data = { mysensors.ATTR_NODE_ID: self.node_id, mysensors.ATTR_CHILD_ID: self.child_id, @@ -144,7 +160,7 @@ class MySensorsSensor(Entity): return data def update_sensor(self, values, battery_level): - """ Update the controller with the latest values from a sensor. """ + """Update the controller with the latest values from a sensor.""" for value_type, value in values.items(): _LOGGER.info( "%s: value_type %s, value = %s", self._name, value_type, value) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 5db5b9d25fc..541c305fafa 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -22,8 +22,7 @@ DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): - """ Setup the mysensors platform for switches. """ - + """Setup the mysensors platform for switches.""" # Define the V_TYPES that the platform should handle as states. v_types = [] for _, member in mysensors.CONST.SetReq.__members__.items(): @@ -39,7 +38,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return (v_types, MySensorsSwitch, add_devices) def sensor_update(event): - """ Callback for sensor updates from the MySensors component. """ + """Callback for sensor updates from the MySensors component.""" _LOGGER.info( 'update %s: node %s', event.data[mysensors.ATTR_UPDATE_TYPE], event.data[mysensors.ATTR_NODE_ID]) @@ -52,21 +51,39 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MySensorsSwitch(SwitchDevice): + """Represent the value of a MySensors child node.""" - """ Represents the value of a MySensors child node. """ # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, port, node_id, child_id, name, value_type): + """Setup class attributes on instantiation. + + Args: + port (str): Gateway port. + node_id (str): Id of node. + child_id (str): Id of child. + name (str): Sketch name. + value_type (str): Value type of child. Value is entity state. + + Attributes: + port (str): Gateway port. + node_id (str): Id of node. + child_id (str): Id of child. + _name (str): Sketch name. + value_type (str): Value type of child. Value is entity state. + battery_level (int): Node battery level. + _values (dict): Child values. Non state values set as state attributes. + """ self.port = port - self._name = name self.node_id = node_id self.child_id = child_id - self.battery_level = 0 + self._name = name self.value_type = value_type + self.battery_level = 0 self._values = {} def as_dict(self): - """ Returns a dict representation of this Entity. """ + """Return a dict representation of this Entity.""" return { 'port': self.port, 'name': self._name, @@ -79,17 +96,17 @@ class MySensorsSwitch(SwitchDevice): @property def should_poll(self): - """ MySensor gateway pushes its state to HA. """ + """MySensor gateway pushes its state to HA.""" return False @property def name(self): - """ The name of this sensor. """ + """The name of this sensor.""" return self._name @property def unit_of_measurement(self): - """ Unit of measurement of this entity. """ + """Unit of measurement of this entity.""" # pylint:disable=too-many-return-statements if self.value_type == mysensors.CONST.SetReq.V_TEMP: return TEMP_CELCIUS if mysensors.IS_METRIC else TEMP_FAHRENHEIT @@ -114,15 +131,14 @@ class MySensorsSwitch(SwitchDevice): @property def device_state_attributes(self): - """ Returns device specific state attributes. """ + """Return device specific state attributes.""" device_attr = dict(self._values) device_attr.pop(self.value_type, None) return device_attr @property def state_attributes(self): - """ Returns the state attributes. """ - + """Return the state attributes.""" data = { mysensors.ATTR_NODE_ID: self.node_id, mysensors.ATTR_CHILD_ID: self.child_id, @@ -138,25 +154,27 @@ class MySensorsSwitch(SwitchDevice): @property def is_on(self): - """ Returns True if switch is on. """ - return self._values[self.value_type] == STATE_ON + """Return True if switch is on.""" + if self.value_type in self._values: + return self._values[self.value_type] == STATE_ON + return False def turn_on(self): - """ Turns the switch on. """ + """Turn the switch on.""" mysensors.GATEWAYS[self.port].set_child_value( self.node_id, self.child_id, self.value_type, 1) self._values[self.value_type] = STATE_ON self.update_ha_state() def turn_off(self): - """ Turns the pin to low/off. """ + """Turn the pin to low/off.""" mysensors.GATEWAYS[self.port].set_child_value( self.node_id, self.child_id, self.value_type, 0) self._values[self.value_type] = STATE_OFF self.update_ha_state() def update_sensor(self, values, battery_level): - """ Update the controller with the latest value from a sensor. """ + """Update the controller with the latest value from a sensor.""" for value_type, value in values.items(): _LOGGER.info( "%s: value_type %s, value = %s", self._name, value_type, value) From 9463c84603a9e7310b98dd9b9727f9ebd1be14b0 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Tue, 8 Dec 2015 02:47:15 +0100 Subject: [PATCH 004/129] Clean up --- homeassistant/components/sensor/mysensors.py | 8 ++--- homeassistant/components/switch/mysensors.py | 36 +++----------------- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index f1ce4f38271..9c4d3d3fcc4 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -62,14 +62,14 @@ class MySensorsSensor(Entity): port (str): Gateway port. node_id (str): Id of node. child_id (str): Id of child. - name (str): Sketch name. + name (str): Entity name. value_type (str): Value type of child. Value is entity state. Attributes: port (str): Gateway port. node_id (str): Id of node. child_id (str): Id of child. - _name (str): Sketch name. + _name (str): Entity name. value_type (str): Value type of child. Value is entity state. battery_level (int): Node battery level. _values (dict): Child values. Non state values set as state attributes. @@ -83,7 +83,7 @@ class MySensorsSensor(Entity): self._values = {} def as_dict(self): - """Return a dict representation of this Entity.""" + """Return a dict representation of this entity.""" return { 'port': self.port, 'name': self._name, @@ -101,7 +101,7 @@ class MySensorsSensor(Entity): @property def name(self): - """The name of this sensor.""" + """The name of this entity.""" return self._name @property diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 541c305fafa..a2557900141 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -12,7 +12,6 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( ATTR_BATTERY_LEVEL, - TEMP_CELCIUS, TEMP_FAHRENHEIT, STATE_ON, STATE_OFF) import homeassistant.components.mysensors as mysensors @@ -62,14 +61,14 @@ class MySensorsSwitch(SwitchDevice): port (str): Gateway port. node_id (str): Id of node. child_id (str): Id of child. - name (str): Sketch name. + name (str): Entity name. value_type (str): Value type of child. Value is entity state. Attributes: port (str): Gateway port. node_id (str): Id of node. child_id (str): Id of child. - _name (str): Sketch name. + _name (str): Entity name. value_type (str): Value type of child. Value is entity state. battery_level (int): Node battery level. _values (dict): Child values. Non state values set as state attributes. @@ -83,7 +82,7 @@ class MySensorsSwitch(SwitchDevice): self._values = {} def as_dict(self): - """Return a dict representation of this Entity.""" + """Return a dict representation of this entity.""" return { 'port': self.port, 'name': self._name, @@ -101,34 +100,9 @@ class MySensorsSwitch(SwitchDevice): @property def name(self): - """The name of this sensor.""" + """The name of this entity.""" return self._name - @property - def unit_of_measurement(self): - """Unit of measurement of this entity.""" - # pylint:disable=too-many-return-statements - if self.value_type == mysensors.CONST.SetReq.V_TEMP: - return TEMP_CELCIUS if mysensors.IS_METRIC else TEMP_FAHRENHEIT - elif self.value_type == mysensors.CONST.SetReq.V_HUM or \ - self.value_type == mysensors.CONST.SetReq.V_DIMMER or \ - self.value_type == mysensors.CONST.SetReq.V_PERCENTAGE or \ - self.value_type == mysensors.CONST.SetReq.V_LIGHT_LEVEL: - return '%' - elif self.value_type == mysensors.CONST.SetReq.V_WATT: - return 'W' - elif self.value_type == mysensors.CONST.SetReq.V_KWH: - return 'kWh' - elif self.value_type == mysensors.CONST.SetReq.V_VOLTAGE: - return 'V' - elif self.value_type == mysensors.CONST.SetReq.V_CURRENT: - return 'A' - elif self.value_type == mysensors.CONST.SetReq.V_IMPEDANCE: - return 'ohm' - elif mysensors.CONST.SetReq.V_UNIT_PREFIX in self._values: - return self._values[mysensors.CONST.SetReq.V_UNIT_PREFIX] - return None - @property def device_state_attributes(self): """Return device specific state attributes.""" @@ -167,7 +141,7 @@ class MySensorsSwitch(SwitchDevice): self.update_ha_state() def turn_off(self): - """Turn the pin to low/off.""" + """Turn the switch off.""" mysensors.GATEWAYS[self.port].set_child_value( self.node_id, self.child_id, self.value_type, 0) self._values[self.value_type] = STATE_OFF From 1e52d5c7f22122df1a869e1ee107d741b692e95b Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Wed, 9 Dec 2015 04:43:18 +0100 Subject: [PATCH 005/129] Add S_TYPES to platform type and fix persistence * Add S_TYPES to platform type. * Fix persistence update on startup. * Clean up code. --- homeassistant/components/mysensors.py | 52 +++++++++++++++----- homeassistant/components/sensor/mysensors.py | 50 +++++++++++++------ homeassistant/components/switch/mysensors.py | 45 ++++++++++------- 3 files changed, 99 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 1dcd65ab8e6..3ab1a96d80f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -12,6 +12,7 @@ import logging from homeassistant.helpers import (validate_config) from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELCIUS) @@ -94,9 +95,15 @@ def setup(hass, config): # noqa gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) gateway.start() + def persistence_update(event): + """Callback to trigger update from persistence file.""" + for _ in range(2): + for nid in gateway.sensors: + gateway.event_callback('persistence', nid) + if persistence: - for nid in gateway.sensors: - gateway.event_callback('node_update', nid) + hass.bus.listen_once( + EVENT_HOMEASSISTANT_START, persistence_update) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: gateway.stop()) @@ -134,33 +141,52 @@ def mysensors_update(platform_type): """Decorator for callback function for mysensor updates.""" def wrapper(gateway, port, devices, nid): """Wrapper function in the decorator.""" - sensor = gateway.sensors[nid] - if sensor.sketch_name is None: + if gateway.sensors[nid].sketch_name is None: _LOGGER.info('No sketch_name: node %s', nid) return if nid not in devices: devices[nid] = {} node = devices[nid] new_devices = [] - # Get platform specific V_TYPES, class and add_devices function. - platform_v_types, platform_class, add_devices = platform_type( - gateway, port, devices, nid) - for child_id, child in sensor.children.items(): + # Get platform specific S_TYPES, V_TYPES, class and add_devices. + (platform_s_types, + platform_v_types, + platform_class, + add_devices) = platform_type(gateway, port, devices, nid) + for child_id, child in gateway.sensors[nid].children.items(): if child_id not in node: node[child_id] = {} for value_type, _ in child.values.items(): - if ((value_type not in node[child_id]) and - (value_type in platform_v_types)): + if (value_type not in node[child_id] and + child.type in platform_s_types and + value_type in platform_v_types): name = '{} {}.{}'.format( - sensor.sketch_name, nid, child.id) + gateway.sensors[nid].sketch_name, nid, child.id) node[child_id][value_type] = platform_class( port, nid, child_id, name, value_type) new_devices.append(node[child_id][value_type]) - elif value_type in platform_v_types: + elif (child.type in platform_s_types and + value_type in platform_v_types): node[child_id][value_type].update_sensor( - child.values, sensor.battery_level) + child.values, gateway.sensors[nid].battery_level) if new_devices: _LOGGER.info('adding new devices: %s', new_devices) add_devices(new_devices) return return wrapper + + +def event_update(update): + """Decorator for callback function for mysensor event updates.""" + def wrapper(event): + """Wrapper function in the decorator.""" + _LOGGER.info( + 'update %s: node %s', event.data[ATTR_UPDATE_TYPE], + event.data[ATTR_NODE_ID]) + sensor_update = update(event) + sensor_update(GATEWAYS[event.data[ATTR_PORT]], + event.data[ATTR_PORT], + event.data[ATTR_DEVICES], + event.data[ATTR_NODE_ID]) + return + return wrapper diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 9c4d3d3fcc4..e3843448763 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -23,31 +23,49 @@ DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for sensors.""" - # Define the V_TYPES that the platform should handle as states. + # Define the S_TYPES and V_TYPES that the platform should handle as states. + s_types = [ + mysensors.CONST.Presentation.S_TEMP, + mysensors.CONST.Presentation.S_HUM, + mysensors.CONST.Presentation.S_BARO, + mysensors.CONST.Presentation.S_WIND, + mysensors.CONST.Presentation.S_RAIN, + mysensors.CONST.Presentation.S_UV, + mysensors.CONST.Presentation.S_WEIGHT, + mysensors.CONST.Presentation.S_POWER, + mysensors.CONST.Presentation.S_DISTANCE, + mysensors.CONST.Presentation.S_LIGHT_LEVEL, + mysensors.CONST.Presentation.S_IR, + mysensors.CONST.Presentation.S_WATER, + mysensors.CONST.Presentation.S_AIR_QUALITY, + mysensors.CONST.Presentation.S_CUSTOM, + mysensors.CONST.Presentation.S_DUST, + mysensors.CONST.Presentation.S_SCENE_CONTROLLER, + mysensors.CONST.Presentation.S_COLOR_SENSOR, + mysensors.CONST.Presentation.S_MULTIMETER, + ] + not_v_types = [ + mysensors.CONST.SetReq.V_ARMED, + mysensors.CONST.SetReq.V_STATUS, + mysensors.CONST.SetReq.V_LIGHT, + mysensors.CONST.SetReq.V_LOCK_STATUS, + ] v_types = [] for _, member in mysensors.CONST.SetReq.__members__.items(): - if (member.value != mysensors.CONST.SetReq.V_ARMED and - member.value != mysensors.CONST.SetReq.V_STATUS and - member.value != mysensors.CONST.SetReq.V_LIGHT and - member.value != mysensors.CONST.SetReq.V_LOCK_STATUS): + if all(test != member.value for test in not_v_types): v_types.append(member) @mysensors.mysensors_update def _sensor_update(gateway, port, devices, nid): """Internal callback for sensor updates.""" - return (v_types, MySensorsSensor, add_devices) + return (s_types, v_types, MySensorsSensor, add_devices) - def sensor_update(event): - """Callback for sensor updates from the MySensors component.""" - _LOGGER.info( - 'update %s: node %s', event.data[mysensors.ATTR_UPDATE_TYPE], - event.data[mysensors.ATTR_NODE_ID]) - _sensor_update(mysensors.GATEWAYS[event.data[mysensors.ATTR_PORT]], - event.data[mysensors.ATTR_PORT], - event.data[mysensors.ATTR_DEVICES], - event.data[mysensors.ATTR_NODE_ID]) + @mysensors.event_update + def event_update(event): + """Callback for event updates from the MySensors component.""" + return _sensor_update - hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) + hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, event_update) class MySensorsSensor(Entity): diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index a2557900141..792502aef07 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -22,31 +22,38 @@ DEPENDENCIES = ['mysensors'] def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for switches.""" - # Define the V_TYPES that the platform should handle as states. - v_types = [] - for _, member in mysensors.CONST.SetReq.__members__.items(): - if (member.value == mysensors.CONST.SetReq.V_ARMED or - member.value == mysensors.CONST.SetReq.V_STATUS or - member.value == mysensors.CONST.SetReq.V_LIGHT or - member.value == mysensors.CONST.SetReq.V_LOCK_STATUS): - v_types.append(member) + # Define the S_TYPES and V_TYPES that the platform should handle as states. + s_types = [ + mysensors.CONST.Presentation.S_DOOR, + mysensors.CONST.Presentation.S_MOTION, + mysensors.CONST.Presentation.S_SMOKE, + mysensors.CONST.Presentation.S_LIGHT, + mysensors.CONST.Presentation.S_BINARY, + mysensors.CONST.Presentation.S_LOCK, + mysensors.CONST.Presentation.S_SPRINKLER, + mysensors.CONST.Presentation.S_WATER_LEAK, + mysensors.CONST.Presentation.S_SOUND, + mysensors.CONST.Presentation.S_VIBRATION, + mysensors.CONST.Presentation.S_MOISTURE, + ] + v_types = [ + mysensors.CONST.SetReq.V_ARMED, + mysensors.CONST.SetReq.V_STATUS, + mysensors.CONST.SetReq.V_LIGHT, + mysensors.CONST.SetReq.V_LOCK_STATUS, + ] @mysensors.mysensors_update def _sensor_update(gateway, port, devices, nid): """Internal callback for sensor updates.""" - return (v_types, MySensorsSwitch, add_devices) + return (s_types, v_types, MySensorsSwitch, add_devices) - def sensor_update(event): - """Callback for sensor updates from the MySensors component.""" - _LOGGER.info( - 'update %s: node %s', event.data[mysensors.ATTR_UPDATE_TYPE], - event.data[mysensors.ATTR_NODE_ID]) - _sensor_update(mysensors.GATEWAYS[event.data[mysensors.ATTR_PORT]], - event.data[mysensors.ATTR_PORT], - event.data[mysensors.ATTR_DEVICES], - event.data[mysensors.ATTR_NODE_ID]) + @mysensors.event_update + def event_update(event): + """Callback for event updates from the MySensors component.""" + return _sensor_update - hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, sensor_update) + hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, event_update) class MySensorsSwitch(SwitchDevice): From 659226886f471be446c903b8ae00bce22caf59b5 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Fri, 18 Dec 2015 03:37:49 +0100 Subject: [PATCH 006/129] Update .coveragerc and requirements --- .coveragerc | 4 +++- homeassistant/components/mysensors.py | 4 ++-- requirements_all.txt | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.coveragerc b/.coveragerc index 46a945e760b..bf5582c2115 100644 --- a/.coveragerc +++ b/.coveragerc @@ -32,6 +32,9 @@ omit = homeassistant/components/rfxtrx.py homeassistant/components/*/rfxtrx.py + homeassistant/components/mysensors.py + homeassistant/components/*/mysensors.py + homeassistant/components/binary_sensor/arest.py homeassistant/components/browser.py homeassistant/components/camera/* @@ -86,7 +89,6 @@ omit = homeassistant/components/sensor/efergy.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py - homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/rpi_gpio.py diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 3ab1a96d80f..a1491bbb1db 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -25,8 +25,8 @@ CONF_VERSION = 'version' DOMAIN = 'mysensors' DEPENDENCIES = [] REQUIREMENTS = [ - 'https://github.com/MartinHjelmare/pymysensors/archive/fifo_queue.zip' - '#pymysensors==0.3'] + 'https://github.com/theolind/pymysensors/archive/' + '2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3'] _LOGGER = logging.getLogger(__name__) ATTR_PORT = 'port' ATTR_DEVICES = 'devices' diff --git a/requirements_all.txt b/requirements_all.txt index 87e7555b395..25e1fd50618 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -118,8 +118,8 @@ py-cpuinfo==0.1.6 # homeassistant.components.sensor.forecast python-forecastio==1.3.3 -# homeassistant.components.sensor.mysensors -https://github.com/MartinHjelmare/pymysensors/archive/fifo_queue.zip#pymysensors==0.3 +# homeassistant.components.mysensors +https://github.com/theolind/pymysensors/archive/2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3 # homeassistant.components.sensor.openweathermap pyowm==2.2.1 From 845926236ef62a01cb0ac362208dabe17f5dc3c8 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Fri, 18 Dec 2015 03:58:21 +0100 Subject: [PATCH 007/129] Add config sample and fix requirements_all --- homeassistant/components/mysensors.py | 27 +++++++++++++++++++++++++++ requirements_all.txt | 6 +++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index a1491bbb1db..0e2ba92627f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -6,6 +6,33 @@ API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors.html + + +New features: + +New MySensors component. +Updated MySensors Sensor platform. +New MySensors Switch platform. +Multiple gateways are now supported. + +Configuration.yaml: + +mysensors: + port: + - '/dev/ttyUSB0' + - '/dev/ttyACM1' + debug: true + persistence: true + persistence_file: + - 'path/to/.homeassistant/mysensors.json' + - 'path/to/.homeassistant/mysensors2.json' + version: '1.5' + +sensor: + platform: mysensors + +switch: + platform: mysensors """ import logging diff --git a/requirements_all.txt b/requirements_all.txt index 5852cc45dee..303c51bd92a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -85,6 +85,9 @@ paho-mqtt==1.1 # homeassistant.components.mqtt jsonpath-rw==1.4.0 +# homeassistant.components.mysensors +https://github.com/theolind/pymysensors/archive/2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3 + # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 @@ -121,9 +124,6 @@ py-cpuinfo==0.1.6 # homeassistant.components.sensor.forecast python-forecastio==1.3.3 -# homeassistant.components.mysensors -https://github.com/theolind/pymysensors/archive/2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3 - # homeassistant.components.sensor.openweathermap pyowm==2.2.1 From 9f54bcc21b1744f2850a35e54903f3581d14badb Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Wed, 23 Dec 2015 23:20:39 +0100 Subject: [PATCH 008/129] Fix comments for pull request * Fix cleaner user config. * Remove bad disabling of linting. * Extract default mysensors version into constant. * Clean up selection of mysensors.CONST from version. * Update mysensors update decorator to add devices and update values in one go. * Fix persistence update. * Clean up setup of ports. * Setup of mysensors platforms from main mysensors component. * Clean up v_types selection in mysensors sensor platform. * Fix s_types and v_types selection version dependency in platforms. --- homeassistant/components/mysensors.py | 108 ++++++++++--------- homeassistant/components/sensor/mysensors.py | 17 +-- homeassistant/components/switch/mysensors.py | 17 +-- 3 files changed, 78 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 0e2ba92627f..89bc14a4ef8 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -17,6 +17,16 @@ Multiple gateways are now supported. Configuration.yaml: +mysensors: + gateways: + - port: '/dev/ttyUSB0' + persistence_file: 'path/mysensors.json' + - port: '/dev/ttyACM1' + persistence_file: 'path/mysensors2.json' + debug: true + persistence: true + version: '1.5' + mysensors: port: - '/dev/ttyUSB0' @@ -36,18 +46,23 @@ switch: """ import logging -from homeassistant.helpers import (validate_config) +from homeassistant.helpers import validate_config +import homeassistant.bootstrap as bootstrap from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - TEMP_CELCIUS) + TEMP_CELCIUS, + CONF_PLATFORM) +CONF_GATEWAYS = 'gateways' CONF_PORT = 'port' CONF_DEBUG = 'debug' CONF_PERSISTENCE = 'persistence' CONF_PERSISTENCE_FILE = 'persistence_file' CONF_VERSION = 'version' +DEFAULT_VERSION = '1.4' +VERSION = None DOMAIN = 'mysensors' DEPENDENCIES = [] @@ -61,29 +76,31 @@ ATTR_NODE_ID = 'node_id' ATTR_CHILD_ID = 'child_id' ATTR_UPDATE_TYPE = 'update_type' +COMPONENTS_WITH_MYSENSORS_PLATFORM = [ + 'sensor', + 'switch', +] + IS_METRIC = None CONST = None GATEWAYS = None EVENT_MYSENSORS_NODE_UPDATE = 'MYSENSORS_NODE_UPDATE' -def setup(hass, config): # noqa - """ Setup the MySensors component. """ - # pylint:disable=no-name-in-module +def setup(hass, config): + """Setup the MySensors component.""" import mysensors.mysensors as mysensors if not validate_config(config, - {DOMAIN: [CONF_PORT]}, + {DOMAIN: [CONF_GATEWAYS]}, _LOGGER): return False - version = config[DOMAIN].get(CONF_VERSION, '1.4') + global VERSION + VERSION = config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION) global CONST - if version == '1.4': - import mysensors.const_14 as const - CONST = const - elif version == '1.5': + if VERSION == '1.5': import mysensors.const_15 as const CONST = const else: @@ -95,7 +112,14 @@ def setup(hass, config): # noqa global IS_METRIC IS_METRIC = (hass.config.temperature_unit == TEMP_CELCIUS) - def callback_generator(port, devices): + # Setup mysensors platforms + mysensors_config = config.copy() + for component in COMPONENTS_WITH_MYSENSORS_PLATFORM: + mysensors_config[component] = {CONF_PLATFORM: 'mysensors'} + if not bootstrap.setup_component(hass, component, mysensors_config): + return False + + def callback_factory(port, devices): """Return a new callback function. Run once per gateway setup.""" def node_update(update_type, nid): """Callback for node updates from the MySensors gateway.""" @@ -107,7 +131,7 @@ def setup(hass, config): # noqa ATTR_UPDATE_TYPE: update_type, ATTR_NODE_ID: nid }) - return + return node_update def setup_gateway(port, persistence, persistence_file): @@ -116,17 +140,16 @@ def setup(hass, config): # noqa gateway = mysensors.SerialGateway(port, persistence=persistence, persistence_file=persistence_file, - protocol_version=version) - gateway.event_callback = callback_generator(port, devices) + protocol_version=VERSION) + gateway.event_callback = callback_factory(port, devices) gateway.metric = IS_METRIC gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) gateway.start() def persistence_update(event): """Callback to trigger update from persistence file.""" - for _ in range(2): - for nid in gateway.sensors: - gateway.event_callback('persistence', nid) + for nid in gateway.sensors: + gateway.event_callback('persistence', nid) if persistence: hass.bus.listen_once( @@ -134,32 +157,23 @@ def setup(hass, config): # noqa hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: gateway.stop()) + return gateway - port = config[DOMAIN].get(CONF_PORT) - persistence_file = config[DOMAIN].get( - CONF_PERSISTENCE_FILE, hass.config.path('mysensors.pickle')) - - if isinstance(port, str): - port = [port] - if isinstance(persistence_file, str): - persistence_file = [persistence_file] - # Setup all ports from config global GATEWAYS GATEWAYS = {} - for index, port_item in enumerate(port): - persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) - try: - persistence_f_item = persistence_file[index] - except IndexError: - _LOGGER.exception( - 'No persistence_file is set for port %s,' - ' disabling persistence', port_item) - persistence = False - persistence_f_item = None - GATEWAYS[port_item] = setup_gateway( - port_item, persistence, persistence_f_item) + conf_gateways = config[DOMAIN][CONF_GATEWAYS] + if isinstance(conf_gateways, dict): + conf_gateways = [conf_gateways] + persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) + for index, gway in enumerate(conf_gateways): + port = gway[CONF_PORT] + persistence_file = gway.get( + CONF_PERSISTENCE_FILE, + hass.config.path('mysensors{}.pickle'.format(index + 1))) + GATEWAYS[port] = setup_gateway( + port, persistence, persistence_file) return True @@ -174,7 +188,6 @@ def mysensors_update(platform_type): if nid not in devices: devices[nid] = {} node = devices[nid] - new_devices = [] # Get platform specific S_TYPES, V_TYPES, class and add_devices. (platform_s_types, platform_v_types, @@ -183,7 +196,7 @@ def mysensors_update(platform_type): for child_id, child in gateway.sensors[nid].children.items(): if child_id not in node: node[child_id] = {} - for value_type, _ in child.values.items(): + for value_type in child.values.keys(): if (value_type not in node[child_id] and child.type in platform_s_types and value_type in platform_v_types): @@ -191,15 +204,13 @@ def mysensors_update(platform_type): gateway.sensors[nid].sketch_name, nid, child.id) node[child_id][value_type] = platform_class( port, nid, child_id, name, value_type) - new_devices.append(node[child_id][value_type]) - elif (child.type in platform_s_types and - value_type in platform_v_types): + _LOGGER.info('adding new device: %s', + node[child_id][value_type]) + add_devices([node[child_id][value_type]]) + if (child.type in platform_s_types and + value_type in platform_v_types): node[child_id][value_type].update_sensor( child.values, gateway.sensors[nid].battery_level) - if new_devices: - _LOGGER.info('adding new devices: %s', new_devices) - add_devices(new_devices) - return return wrapper @@ -215,5 +226,4 @@ def event_update(update): event.data[ATTR_PORT], event.data[ATTR_DEVICES], event.data[ATTR_NODE_ID]) - return return wrapper diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index e3843448763..16f047beaf4 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -18,7 +18,7 @@ from homeassistant.const import ( import homeassistant.components.mysensors as mysensors _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['mysensors'] +DEPENDENCIES = [] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -41,19 +41,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mysensors.CONST.Presentation.S_CUSTOM, mysensors.CONST.Presentation.S_DUST, mysensors.CONST.Presentation.S_SCENE_CONTROLLER, - mysensors.CONST.Presentation.S_COLOR_SENSOR, - mysensors.CONST.Presentation.S_MULTIMETER, ] not_v_types = [ mysensors.CONST.SetReq.V_ARMED, - mysensors.CONST.SetReq.V_STATUS, mysensors.CONST.SetReq.V_LIGHT, mysensors.CONST.SetReq.V_LOCK_STATUS, ] - v_types = [] - for _, member in mysensors.CONST.SetReq.__members__.items(): - if all(test != member.value for test in not_v_types): - v_types.append(member) + if float(mysensors.VERSION) >= 1.5: + s_types.extend([ + mysensors.CONST.Presentation.S_COLOR_SENSOR, + mysensors.CONST.Presentation.S_MULTIMETER, + ]) + not_v_types.extend([mysensors.CONST.SetReq.V_STATUS, ]) + v_types = [member for member in mysensors.CONST.SetReq + if member.value not in not_v_types] @mysensors.mysensors_update def _sensor_update(gateway, port, devices, nid): diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 792502aef07..efa3bd7f7c4 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -17,7 +17,7 @@ from homeassistant.const import ( import homeassistant.components.mysensors as mysensors _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['mysensors'] +DEPENDENCIES = [] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -30,18 +30,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mysensors.CONST.Presentation.S_LIGHT, mysensors.CONST.Presentation.S_BINARY, mysensors.CONST.Presentation.S_LOCK, - mysensors.CONST.Presentation.S_SPRINKLER, - mysensors.CONST.Presentation.S_WATER_LEAK, - mysensors.CONST.Presentation.S_SOUND, - mysensors.CONST.Presentation.S_VIBRATION, - mysensors.CONST.Presentation.S_MOISTURE, ] v_types = [ mysensors.CONST.SetReq.V_ARMED, - mysensors.CONST.SetReq.V_STATUS, mysensors.CONST.SetReq.V_LIGHT, mysensors.CONST.SetReq.V_LOCK_STATUS, ] + if float(mysensors.VERSION) >= 1.5: + s_types.extend([ + mysensors.CONST.Presentation.S_SPRINKLER, + mysensors.CONST.Presentation.S_WATER_LEAK, + mysensors.CONST.Presentation.S_SOUND, + mysensors.CONST.Presentation.S_VIBRATION, + mysensors.CONST.Presentation.S_MOISTURE, + ]) + v_types.extend([mysensors.CONST.SetReq.V_STATUS, ]) @mysensors.mysensors_update def _sensor_update(gateway, port, devices, nid): From be25ea4f09c246a8317fb8b138d90a2fe728e5a9 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Thu, 24 Dec 2015 02:14:58 +0100 Subject: [PATCH 009/129] Fix avoid event bus for updates --- homeassistant/components/mysensors.py | 52 +++----------------- homeassistant/components/sensor/mysensors.py | 44 +++++++---------- homeassistant/components/switch/mysensors.py | 46 +++++++---------- 3 files changed, 43 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 89bc14a4ef8..6c3b1854b02 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -26,23 +26,6 @@ mysensors: debug: true persistence: true version: '1.5' - -mysensors: - port: - - '/dev/ttyUSB0' - - '/dev/ttyACM1' - debug: true - persistence: true - persistence_file: - - 'path/to/.homeassistant/mysensors.json' - - 'path/to/.homeassistant/mysensors2.json' - version: '1.5' - -sensor: - platform: mysensors - -switch: - platform: mysensors """ import logging @@ -70,11 +53,8 @@ REQUIREMENTS = [ 'https://github.com/theolind/pymysensors/archive/' '2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3'] _LOGGER = logging.getLogger(__name__) -ATTR_PORT = 'port' -ATTR_DEVICES = 'devices' ATTR_NODE_ID = 'node_id' ATTR_CHILD_ID = 'child_id' -ATTR_UPDATE_TYPE = 'update_type' COMPONENTS_WITH_MYSENSORS_PLATFORM = [ 'sensor', @@ -84,11 +64,11 @@ COMPONENTS_WITH_MYSENSORS_PLATFORM = [ IS_METRIC = None CONST = None GATEWAYS = None -EVENT_MYSENSORS_NODE_UPDATE = 'MYSENSORS_NODE_UPDATE' def setup(hass, config): """Setup the MySensors component.""" + # pylint: disable=too-many-locals import mysensors.mysensors as mysensors if not validate_config(config, @@ -119,18 +99,17 @@ def setup(hass, config): if not bootstrap.setup_component(hass, component, mysensors_config): return False - def callback_factory(port, devices): + import homeassistant.components.sensor.mysensors as mysensors_sensor + import homeassistant.components.switch.mysensors as mysensors_switch + + def callback_factory(gateway, port, devices): """Return a new callback function. Run once per gateway setup.""" def node_update(update_type, nid): """Callback for node updates from the MySensors gateway.""" _LOGGER.info('update %s: node %s', update_type, nid) - hass.bus.fire(EVENT_MYSENSORS_NODE_UPDATE, { - ATTR_PORT: port, - ATTR_DEVICES: devices, - ATTR_UPDATE_TYPE: update_type, - ATTR_NODE_ID: nid - }) + mysensors_sensor.sensor_update(gateway, port, devices, nid) + mysensors_switch.sensor_update(gateway, port, devices, nid) return node_update @@ -141,7 +120,7 @@ def setup(hass, config): persistence=persistence, persistence_file=persistence_file, protocol_version=VERSION) - gateway.event_callback = callback_factory(port, devices) + gateway.event_callback = callback_factory(gateway, port, devices) gateway.metric = IS_METRIC gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) gateway.start() @@ -212,18 +191,3 @@ def mysensors_update(platform_type): node[child_id][value_type].update_sensor( child.values, gateway.sensors[nid].battery_level) return wrapper - - -def event_update(update): - """Decorator for callback function for mysensor event updates.""" - def wrapper(event): - """Wrapper function in the decorator.""" - _LOGGER.info( - 'update %s: node %s', event.data[ATTR_UPDATE_TYPE], - event.data[ATTR_NODE_ID]) - sensor_update = update(event) - sensor_update(GATEWAYS[event.data[ATTR_PORT]], - event.data[ATTR_PORT], - event.data[ATTR_DEVICES], - event.data[ATTR_NODE_ID]) - return wrapper diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 16f047beaf4..eb8d4c57161 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -20,11 +20,24 @@ import homeassistant.components.mysensors as mysensors _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] +ADD_DEVICES = None +S_TYPES = None +V_TYPES = None + + +@mysensors.mysensors_update +def sensor_update(gateway, port, devices, nid): + """Internal callback for sensor updates.""" + return (S_TYPES, V_TYPES, MySensorsSensor, ADD_DEVICES) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for sensors.""" # Define the S_TYPES and V_TYPES that the platform should handle as states. - s_types = [ + global ADD_DEVICES + ADD_DEVICES = add_devices + global S_TYPES + S_TYPES = [ mysensors.CONST.Presentation.S_TEMP, mysensors.CONST.Presentation.S_HUM, mysensors.CONST.Presentation.S_BARO, @@ -48,26 +61,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mysensors.CONST.SetReq.V_LOCK_STATUS, ] if float(mysensors.VERSION) >= 1.5: - s_types.extend([ + S_TYPES.extend([ mysensors.CONST.Presentation.S_COLOR_SENSOR, mysensors.CONST.Presentation.S_MULTIMETER, ]) not_v_types.extend([mysensors.CONST.SetReq.V_STATUS, ]) - v_types = [member for member in mysensors.CONST.SetReq + global V_TYPES + V_TYPES = [member for member in mysensors.CONST.SetReq if member.value not in not_v_types] - @mysensors.mysensors_update - def _sensor_update(gateway, port, devices, nid): - """Internal callback for sensor updates.""" - return (s_types, v_types, MySensorsSensor, add_devices) - - @mysensors.event_update - def event_update(event): - """Callback for event updates from the MySensors component.""" - return _sensor_update - - hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, event_update) - class MySensorsSensor(Entity): """Represent the value of a MySensors child node.""" @@ -101,18 +103,6 @@ class MySensorsSensor(Entity): self.battery_level = 0 self._values = {} - def as_dict(self): - """Return a dict representation of this entity.""" - return { - 'port': self.port, - 'name': self._name, - 'node_id': self.node_id, - 'child_id': self.child_id, - 'battery_level': self.battery_level, - 'value_type': self.value_type, - 'values': self._values, - } - @property def should_poll(self): """MySensor gateway pushes its state to HA.""" diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index efa3bd7f7c4..4ca14cae27c 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -19,11 +19,24 @@ import homeassistant.components.mysensors as mysensors _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] +ADD_DEVICES = None +S_TYPES = None +V_TYPES = None + + +@mysensors.mysensors_update +def sensor_update(gateway, port, devices, nid): + """Internal callback for sensor updates.""" + return (S_TYPES, V_TYPES, MySensorsSwitch, ADD_DEVICES) + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for switches.""" # Define the S_TYPES and V_TYPES that the platform should handle as states. - s_types = [ + global ADD_DEVICES + ADD_DEVICES = add_devices + global S_TYPES + S_TYPES = [ mysensors.CONST.Presentation.S_DOOR, mysensors.CONST.Presentation.S_MOTION, mysensors.CONST.Presentation.S_SMOKE, @@ -31,32 +44,21 @@ def setup_platform(hass, config, add_devices, discovery_info=None): mysensors.CONST.Presentation.S_BINARY, mysensors.CONST.Presentation.S_LOCK, ] - v_types = [ + global V_TYPES + V_TYPES = [ mysensors.CONST.SetReq.V_ARMED, mysensors.CONST.SetReq.V_LIGHT, mysensors.CONST.SetReq.V_LOCK_STATUS, ] if float(mysensors.VERSION) >= 1.5: - s_types.extend([ + S_TYPES.extend([ mysensors.CONST.Presentation.S_SPRINKLER, mysensors.CONST.Presentation.S_WATER_LEAK, mysensors.CONST.Presentation.S_SOUND, mysensors.CONST.Presentation.S_VIBRATION, mysensors.CONST.Presentation.S_MOISTURE, ]) - v_types.extend([mysensors.CONST.SetReq.V_STATUS, ]) - - @mysensors.mysensors_update - def _sensor_update(gateway, port, devices, nid): - """Internal callback for sensor updates.""" - return (s_types, v_types, MySensorsSwitch, add_devices) - - @mysensors.event_update - def event_update(event): - """Callback for event updates from the MySensors component.""" - return _sensor_update - - hass.bus.listen(mysensors.EVENT_MYSENSORS_NODE_UPDATE, event_update) + V_TYPES.extend([mysensors.CONST.SetReq.V_STATUS, ]) class MySensorsSwitch(SwitchDevice): @@ -91,18 +93,6 @@ class MySensorsSwitch(SwitchDevice): self.battery_level = 0 self._values = {} - def as_dict(self): - """Return a dict representation of this entity.""" - return { - 'port': self.port, - 'name': self._name, - 'node_id': self.node_id, - 'child_id': self.child_id, - 'battery_level': self.battery_level, - 'value_type': self.value_type, - 'values': self._values, - } - @property def should_poll(self): """MySensor gateway pushes its state to HA.""" From b0734e613fc1849f78ab7431d2102fe343a90677 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Wed, 30 Dec 2015 13:36:47 -0500 Subject: [PATCH 010/129] Add support for deCONZ (Raspbee-GW hue-like API) - Doesn't support the none transition type, so don't send it --- homeassistant/components/light/hue.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 77672c9aaf5..29ee523dec4 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -121,10 +121,19 @@ def setup_bridge(host, hass, add_devices_callback): new_lights = [] + api_name = api.get('config').get('name') + if api_name == 'RaspBee-GW': + bridge_type = 'deconz' + _LOGGER.info("Found DeCONZ gateway (%s)", api_name) + else: + _LOGGER.info("Found Hue bridge (%s)", api_name) + bridge_type = 'hue' + for light_id, info in api_states.items(): if light_id not in lights: lights[light_id] = HueLight(int(light_id), info, - bridge, update_lights) + bridge, update_lights, + bridge_type=bridge_type) new_lights.append(lights[light_id]) else: lights[light_id].info = info @@ -163,11 +172,13 @@ def request_configuration(host, hass, add_devices_callback): class HueLight(Light): """ Represents a Hue light """ - def __init__(self, light_id, info, bridge, update_lights): + # pylint: disable=too-many-arguments + def __init__(self, light_id, info, bridge, update_lights, bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge self.update_lights = update_lights + self.bridge_type = bridge_type @property def unique_id(self): @@ -227,7 +238,7 @@ class HueLight(Light): command['alert'] = 'lselect' elif flash == FLASH_SHORT: command['alert'] = 'select' - else: + elif self.bridge_type == 'hue': command['alert'] = 'none' effect = kwargs.get(ATTR_EFFECT) @@ -237,7 +248,7 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - else: + elif self.bridge_type == 'hue': command['effect'] = 'none' self.bridge.set_light(self.light_id, command) From ae0dbbcfa599c5670f8176d75dae03d63a466282 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 30 Dec 2015 19:44:02 +0000 Subject: [PATCH 011/129] Added support for event subscriptions --- homeassistant/components/light/vera.py | 17 ++++++++++--- homeassistant/components/switch/vera.py | 33 ++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 829d3cfccdb..23daba4991f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -14,6 +14,8 @@ from homeassistant.components.switch.vera import VeraSwitch from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' '#python-vera==0.1.1'] @@ -36,10 +38,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device_data = config.get('device_data', {}) - controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: - devices = controller.get_devices([ + devices = vera_controller.get_devices([ 'Switch', 'On/Off Switch', 'Dimmable Switch']) @@ -54,7 +65,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): exclude = extra_data.get('exclude', False) if exclude is not True: - lights.append(VeraLight(device, extra_data)) + lights.append(VeraLight(device, vera_controller, extra_data)) add_devices_callback(lights) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 14983919c64..0df1c390929 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -13,7 +13,11 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) + ATTR_BATTERY_LEVEL, + ATTR_TRIPPED, + ATTR_ARMED, + ATTR_LAST_TRIP_TIME, + EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' @@ -37,7 +41,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: devices = vera_controller.get_devices([ @@ -53,7 +66,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_switches.append(VeraSwitch(device, extra_data)) + vera_switches.append( + VeraSwitch(device, vera_controller, extra_data)) return vera_switches @@ -66,9 +80,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSwitch(ToggleEntity): """ Represents a Vera Switch. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device self.extra_data = extra_data + self.controller = controller if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') else: @@ -77,6 +92,16 @@ class VeraSwitch(ToggleEntity): # for debouncing status check after command is sent self.last_command_send = 0 + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + @property def name(self): """ Get the mame of the switch. """ From 4e2d75a8f48cc7bfe3cc4de59508968488246dad Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Wed, 30 Dec 2015 16:59:22 -0500 Subject: [PATCH 012/129] fix style --- homeassistant/components/light/hue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 29ee523dec4..40875d8ea0e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -173,7 +173,8 @@ class HueLight(Light): """ Represents a Hue light """ # pylint: disable=too-many-arguments - def __init__(self, light_id, info, bridge, update_lights, bridge_type='hue'): + def __init__(self, light_id, info, bridge, update_lights, + bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge From 69ed6fe6e7abaa07be3be4d57530a3be8d1c88bf Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Thu, 31 Dec 2015 05:48:23 +0100 Subject: [PATCH 013/129] Add gateway wrapper, fix discovery and callbacks * Add gateway wrapper by subclassing serial gateway. * Fix platform setup with discovery service. * Fix platform callback functions with callback factory. --- homeassistant/components/mysensors.py | 219 ++++++++++--------- homeassistant/components/sensor/__init__.py | 6 +- homeassistant/components/sensor/mysensors.py | 137 ++++++------ homeassistant/components/switch/__init__.py | 3 +- homeassistant/components/switch/mysensors.py | 95 ++++---- 5 files changed, 244 insertions(+), 216 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 6c3b1854b02..a0601850fa7 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -29,14 +29,19 @@ mysensors: """ import logging +try: + import mysensors.mysensors as mysensors +except ImportError: + mysensors = None + from homeassistant.helpers import validate_config import homeassistant.bootstrap as bootstrap from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - TEMP_CELCIUS, - CONF_PLATFORM) + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, + TEMP_CELCIUS,) CONF_GATEWAYS = 'gateways' CONF_PORT = 'port' @@ -45,7 +50,6 @@ CONF_PERSISTENCE = 'persistence' CONF_PERSISTENCE_FILE = 'persistence_file' CONF_VERSION = 'version' DEFAULT_VERSION = '1.4' -VERSION = None DOMAIN = 'mysensors' DEPENDENCIES = [] @@ -56,86 +60,54 @@ _LOGGER = logging.getLogger(__name__) ATTR_NODE_ID = 'node_id' ATTR_CHILD_ID = 'child_id' -COMPONENTS_WITH_MYSENSORS_PLATFORM = [ - 'sensor', - 'switch', -] - -IS_METRIC = None -CONST = None GATEWAYS = None +SCAN_INTERVAL = 30 + +DISCOVER_SENSORS = "mysensors.sensors" +DISCOVER_SWITCHES = "mysensors.switches" + +# Maps discovered services to their platforms +DISCOVERY_COMPONENTS = [ + ('sensor', DISCOVER_SENSORS), + ('switch', DISCOVER_SWITCHES), +] def setup(hass, config): """Setup the MySensors component.""" # pylint: disable=too-many-locals - import mysensors.mysensors as mysensors if not validate_config(config, {DOMAIN: [CONF_GATEWAYS]}, _LOGGER): return False - global VERSION - VERSION = config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION) + global mysensors # pylint: disable=invalid-name + if mysensors is None: + import mysensors.mysensors as _mysensors + mysensors = _mysensors - global CONST - if VERSION == '1.5': - import mysensors.const_15 as const - CONST = const - else: - import mysensors.const_14 as const - CONST = const + version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION)) + is_metric = (hass.config.temperature_unit == TEMP_CELCIUS) - # Just assume celcius means that the user wants metric for now. - # It may make more sense to make this a global config option in the future. - global IS_METRIC - IS_METRIC = (hass.config.temperature_unit == TEMP_CELCIUS) - - # Setup mysensors platforms - mysensors_config = config.copy() - for component in COMPONENTS_WITH_MYSENSORS_PLATFORM: - mysensors_config[component] = {CONF_PLATFORM: 'mysensors'} - if not bootstrap.setup_component(hass, component, mysensors_config): - return False - - import homeassistant.components.sensor.mysensors as mysensors_sensor - import homeassistant.components.switch.mysensors as mysensors_switch - - def callback_factory(gateway, port, devices): - """Return a new callback function. Run once per gateway setup.""" - def node_update(update_type, nid): - """Callback for node updates from the MySensors gateway.""" - _LOGGER.info('update %s: node %s', update_type, nid) - - mysensors_sensor.sensor_update(gateway, port, devices, nid) - mysensors_switch.sensor_update(gateway, port, devices, nid) - - return node_update - - def setup_gateway(port, persistence, persistence_file): + def setup_gateway(port, persistence, persistence_file, version): """Return gateway after setup of the gateway.""" - devices = {} # keep track of devices added to HA - gateway = mysensors.SerialGateway(port, - persistence=persistence, - persistence_file=persistence_file, - protocol_version=VERSION) - gateway.event_callback = callback_factory(gateway, port, devices) - gateway.metric = IS_METRIC + gateway = GatewayWrapper( + port, persistence, persistence_file, version) + # pylint: disable=attribute-defined-outside-init + gateway.metric = is_metric gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) - gateway.start() - def persistence_update(event): - """Callback to trigger update from persistence file.""" - for nid in gateway.sensors: - gateway.event_callback('persistence', nid) + def gw_start(event): + """Callback to trigger start of gateway and any persistence.""" + gateway.start() + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: gateway.stop()) + if persistence: + for node_id in gateway.sensors: + gateway.event_callback('persistence', node_id) - if persistence: - hass.bus.listen_once( - EVENT_HOMEASSISTANT_START, persistence_update) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start) return gateway @@ -146,48 +118,99 @@ def setup(hass, config): if isinstance(conf_gateways, dict): conf_gateways = [conf_gateways] persistence = config[DOMAIN].get(CONF_PERSISTENCE, True) + for index, gway in enumerate(conf_gateways): port = gway[CONF_PORT] persistence_file = gway.get( CONF_PERSISTENCE_FILE, hass.config.path('mysensors{}.pickle'.format(index + 1))) GATEWAYS[port] = setup_gateway( - port, persistence, persistence_file) + port, persistence, persistence_file, version) + + for (component, discovery_service) in DISCOVERY_COMPONENTS: + # Ensure component is loaded + if not bootstrap.setup_component(hass, component, config): + return False + # Fire discovery event + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: discovery_service, + ATTR_DISCOVERED: {}}) return True -def mysensors_update(platform_type): - """Decorator for callback function for mysensor updates.""" - def wrapper(gateway, port, devices, nid): - """Wrapper function in the decorator.""" - if gateway.sensors[nid].sketch_name is None: - _LOGGER.info('No sketch_name: node %s', nid) +def pf_callback_factory( + s_types, v_types, devices, add_devices, entity_class): + """Return a new callback for the platform.""" + def mysensors_callback(gateway, node_id): + """Callback for mysensors platform.""" + if gateway.sensors[node_id].sketch_name is None: + _LOGGER.info('No sketch_name: node %s', node_id) return - if nid not in devices: - devices[nid] = {} - node = devices[nid] - # Get platform specific S_TYPES, V_TYPES, class and add_devices. - (platform_s_types, - platform_v_types, - platform_class, - add_devices) = platform_type(gateway, port, devices, nid) - for child_id, child in gateway.sensors[nid].children.items(): - if child_id not in node: - node[child_id] = {} + # previously discovered, just update state with latest info + if node_id in devices: + for entity in devices[node_id]: + entity.update_ha_state(True) + return + + # First time we see this node, detect sensors + for child in gateway.sensors[node_id].children.values(): + name = '{} {}.{}'.format( + gateway.sensors[node_id].sketch_name, node_id, child.id) + for value_type in child.values.keys(): - if (value_type not in node[child_id] and - child.type in platform_s_types and - value_type in platform_v_types): - name = '{} {}.{}'.format( - gateway.sensors[nid].sketch_name, nid, child.id) - node[child_id][value_type] = platform_class( - port, nid, child_id, name, value_type) - _LOGGER.info('adding new device: %s', - node[child_id][value_type]) - add_devices([node[child_id][value_type]]) - if (child.type in platform_s_types and - value_type in platform_v_types): - node[child_id][value_type].update_sensor( - child.values, gateway.sensors[nid].battery_level) - return wrapper + if child.type not in s_types or value_type not in v_types: + continue + + devices[node_id].append( + entity_class(gateway, node_id, child.id, name, value_type)) + if devices[node_id]: + _LOGGER.info('adding new devices: %s', devices[node_id]) + add_devices(devices[node_id]) + for entity in devices[node_id]: + entity.update_ha_state(True) + return mysensors_callback + + +class GatewayWrapper(mysensors.SerialGateway): + """Gateway wrapper class, by subclassing serial gateway.""" + + def __init__(self, port, persistence, persistence_file, version): + """Setup class attributes on instantiation. + + Args: + port: Port of gateway to wrap. + persistence: Persistence, true or false. + persistence_file: File to store persistence info. + version: Version of mysensors API. + + Attributes: + version (str): Version of mysensors API. + platform_callbacks (list): Callback functions, one per platform. + const (module): Mysensors API constants. + """ + super().__init__(port, event_callback=self.callback_factory(), + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) + self.version = version + self.platform_callbacks = [] + self.const = self.get_const() + + def get_const(self): + """Get mysensors API constants.""" + if self.version == '1.5': + import mysensors.const_15 as const + else: + import mysensors.const_14 as const + return const + + def callback_factory(self): + """Return a new callback function.""" + def node_update(update_type, node_id): + """Callback for node updates from the MySensors gateway.""" + _LOGGER.info('update %s: node %s', update_type, node_id) + for callback in self.platform_callbacks: + callback(self, node_id) + + return node_update diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 04770ced241..1689f7a8889 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -9,7 +9,8 @@ https://home-assistant.io/components/sensor/ import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.components import wink, zwave, isy994, verisure, ecobee +from homeassistant.components import ( + wink, zwave, isy994, verisure, ecobee, mysensors) DOMAIN = 'sensor' SCAN_INTERVAL = 30 @@ -22,7 +23,8 @@ DISCOVERY_PLATFORMS = { zwave.DISCOVER_SENSORS: 'zwave', isy994.DISCOVER_SENSORS: 'isy994', verisure.DISCOVER_SENSORS: 'verisure', - ecobee.DISCOVER_SENSORS: 'ecobee' + ecobee.DISCOVER_SENSORS: 'ecobee', + mysensors.DISCOVER_SENSORS: 'mysensors', } diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index eb8d4c57161..3944cf4f982 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -7,6 +7,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors/ """ import logging +from collections import defaultdict from homeassistant.helpers.entity import Entity @@ -20,74 +21,71 @@ import homeassistant.components.mysensors as mysensors _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] -ADD_DEVICES = None -S_TYPES = None -V_TYPES = None - - -@mysensors.mysensors_update -def sensor_update(gateway, port, devices, nid): - """Internal callback for sensor updates.""" - return (S_TYPES, V_TYPES, MySensorsSensor, ADD_DEVICES) - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for sensors.""" - # Define the S_TYPES and V_TYPES that the platform should handle as states. - global ADD_DEVICES - ADD_DEVICES = add_devices - global S_TYPES - S_TYPES = [ - mysensors.CONST.Presentation.S_TEMP, - mysensors.CONST.Presentation.S_HUM, - mysensors.CONST.Presentation.S_BARO, - mysensors.CONST.Presentation.S_WIND, - mysensors.CONST.Presentation.S_RAIN, - mysensors.CONST.Presentation.S_UV, - mysensors.CONST.Presentation.S_WEIGHT, - mysensors.CONST.Presentation.S_POWER, - mysensors.CONST.Presentation.S_DISTANCE, - mysensors.CONST.Presentation.S_LIGHT_LEVEL, - mysensors.CONST.Presentation.S_IR, - mysensors.CONST.Presentation.S_WATER, - mysensors.CONST.Presentation.S_AIR_QUALITY, - mysensors.CONST.Presentation.S_CUSTOM, - mysensors.CONST.Presentation.S_DUST, - mysensors.CONST.Presentation.S_SCENE_CONTROLLER, - ] - not_v_types = [ - mysensors.CONST.SetReq.V_ARMED, - mysensors.CONST.SetReq.V_LIGHT, - mysensors.CONST.SetReq.V_LOCK_STATUS, - ] - if float(mysensors.VERSION) >= 1.5: - S_TYPES.extend([ - mysensors.CONST.Presentation.S_COLOR_SENSOR, - mysensors.CONST.Presentation.S_MULTIMETER, - ]) - not_v_types.extend([mysensors.CONST.SetReq.V_STATUS, ]) - global V_TYPES - V_TYPES = [member for member in mysensors.CONST.SetReq - if member.value not in not_v_types] + # Only act if loaded via mysensors by discovery event. + # Otherwise gateway is not setup. + if discovery_info is None: + return + + for gateway in mysensors.GATEWAYS.values(): + # Define the S_TYPES and V_TYPES that the platform should handle as + # states. + s_types = [ + gateway.const.Presentation.S_TEMP, + gateway.const.Presentation.S_HUM, + gateway.const.Presentation.S_BARO, + gateway.const.Presentation.S_WIND, + gateway.const.Presentation.S_RAIN, + gateway.const.Presentation.S_UV, + gateway.const.Presentation.S_WEIGHT, + gateway.const.Presentation.S_POWER, + gateway.const.Presentation.S_DISTANCE, + gateway.const.Presentation.S_LIGHT_LEVEL, + gateway.const.Presentation.S_IR, + gateway.const.Presentation.S_WATER, + gateway.const.Presentation.S_AIR_QUALITY, + gateway.const.Presentation.S_CUSTOM, + gateway.const.Presentation.S_DUST, + gateway.const.Presentation.S_SCENE_CONTROLLER, + ] + not_v_types = [ + gateway.const.SetReq.V_ARMED, + gateway.const.SetReq.V_LIGHT, + gateway.const.SetReq.V_LOCK_STATUS, + ] + if float(gateway.version) >= 1.5: + s_types.extend([ + gateway.const.Presentation.S_COLOR_SENSOR, + gateway.const.Presentation.S_MULTIMETER, + ]) + not_v_types.extend([gateway.const.SetReq.V_STATUS, ]) + v_types = [member for member in gateway.const.SetReq + if member.value not in not_v_types] + + devices = defaultdict(list) + gateway.platform_callbacks.append(mysensors.pf_callback_factory( + s_types, v_types, devices, add_devices, MySensorsSensor)) class MySensorsSensor(Entity): """Represent the value of a MySensors child node.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes + # pylint: disable=too-many-arguments - def __init__(self, port, node_id, child_id, name, value_type): + def __init__(self, gateway, node_id, child_id, name, value_type): """Setup class attributes on instantiation. Args: - port (str): Gateway port. + gateway (str): Gateway. node_id (str): Id of node. child_id (str): Id of child. name (str): Entity name. value_type (str): Value type of child. Value is entity state. Attributes: - port (str): Gateway port. + gateway (str): Gateway. node_id (str): Id of node. child_id (str): Id of child. _name (str): Entity name. @@ -95,7 +93,7 @@ class MySensorsSensor(Entity): battery_level (int): Node battery level. _values (dict): Child values. Non state values set as state attributes. """ - self.port = port + self.gateway = gateway self.node_id = node_id self.child_id = child_id self._name = name @@ -124,25 +122,25 @@ class MySensorsSensor(Entity): def unit_of_measurement(self): """Unit of measurement of this entity.""" # pylint:disable=too-many-return-statements - if self.value_type == mysensors.CONST.SetReq.V_TEMP: - return TEMP_CELCIUS if mysensors.IS_METRIC else TEMP_FAHRENHEIT - elif self.value_type == mysensors.CONST.SetReq.V_HUM or \ - self.value_type == mysensors.CONST.SetReq.V_DIMMER or \ - self.value_type == mysensors.CONST.SetReq.V_PERCENTAGE or \ - self.value_type == mysensors.CONST.SetReq.V_LIGHT_LEVEL: + if self.value_type == self.gateway.const.SetReq.V_TEMP: + return TEMP_CELCIUS if self.gateway.metric else TEMP_FAHRENHEIT + elif self.value_type == self.gateway.const.SetReq.V_HUM or \ + self.value_type == self.gateway.const.SetReq.V_DIMMER or \ + self.value_type == self.gateway.const.SetReq.V_PERCENTAGE or \ + self.value_type == self.gateway.const.SetReq.V_LIGHT_LEVEL: return '%' - elif self.value_type == mysensors.CONST.SetReq.V_WATT: + elif self.value_type == self.gateway.const.SetReq.V_WATT: return 'W' - elif self.value_type == mysensors.CONST.SetReq.V_KWH: + elif self.value_type == self.gateway.const.SetReq.V_KWH: return 'kWh' - elif self.value_type == mysensors.CONST.SetReq.V_VOLTAGE: + elif self.value_type == self.gateway.const.SetReq.V_VOLTAGE: return 'V' - elif self.value_type == mysensors.CONST.SetReq.V_CURRENT: + elif self.value_type == self.gateway.const.SetReq.V_CURRENT: return 'A' - elif self.value_type == mysensors.CONST.SetReq.V_IMPEDANCE: + elif self.value_type == self.gateway.const.SetReq.V_IMPEDANCE: return 'ohm' - elif mysensors.CONST.SetReq.V_UNIT_PREFIX in self._values: - return self._values[mysensors.CONST.SetReq.V_UNIT_PREFIX] + elif self.gateway.const.SetReq.V_UNIT_PREFIX in self._values: + return self._values[self.gateway.const.SetReq.V_UNIT_PREFIX] return None @property @@ -168,16 +166,17 @@ class MySensorsSensor(Entity): return data - def update_sensor(self, values, battery_level): + def update(self): """Update the controller with the latest values from a sensor.""" - for value_type, value in values.items(): + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + for value_type, value in child.values.items(): _LOGGER.info( "%s: value_type %s, value = %s", self._name, value_type, value) - if value_type == mysensors.CONST.SetReq.V_TRIPPED: + if value_type == self.gateway.const.SetReq.V_TRIPPED: self._values[value_type] = STATE_ON if int( value) == 1 else STATE_OFF else: self._values[value_type] = value - self.battery_level = battery_level - self.update_ha_state() + self.battery_level = node.battery_level diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index e7b3c629f39..9f9bcc18604 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.components import ( - group, discovery, wink, isy994, verisure, zwave) + group, discovery, wink, isy994, verisure, zwave, mysensors) DOMAIN = 'switch' SCAN_INTERVAL = 30 @@ -40,6 +40,7 @@ DISCOVERY_PLATFORMS = { isy994.DISCOVER_SWITCHES: 'isy994', verisure.DISCOVER_SWITCHES: 'verisure', zwave.DISCOVER_SWITCHES: 'zwave', + mysensors.DISCOVER_SWITCHES: 'mysensors', } PROP_TO_ATTR = { diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 4ca14cae27c..2b886153d8f 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -7,6 +7,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mysensors.html """ import logging +from collections import defaultdict from homeassistant.components.switch import SwitchDevice @@ -24,49 +25,50 @@ S_TYPES = None V_TYPES = None -@mysensors.mysensors_update -def sensor_update(gateway, port, devices, nid): - """Internal callback for sensor updates.""" - return (S_TYPES, V_TYPES, MySensorsSwitch, ADD_DEVICES) - - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for switches.""" - # Define the S_TYPES and V_TYPES that the platform should handle as states. - global ADD_DEVICES - ADD_DEVICES = add_devices - global S_TYPES - S_TYPES = [ - mysensors.CONST.Presentation.S_DOOR, - mysensors.CONST.Presentation.S_MOTION, - mysensors.CONST.Presentation.S_SMOKE, - mysensors.CONST.Presentation.S_LIGHT, - mysensors.CONST.Presentation.S_BINARY, - mysensors.CONST.Presentation.S_LOCK, - ] - global V_TYPES - V_TYPES = [ - mysensors.CONST.SetReq.V_ARMED, - mysensors.CONST.SetReq.V_LIGHT, - mysensors.CONST.SetReq.V_LOCK_STATUS, - ] - if float(mysensors.VERSION) >= 1.5: - S_TYPES.extend([ - mysensors.CONST.Presentation.S_SPRINKLER, - mysensors.CONST.Presentation.S_WATER_LEAK, - mysensors.CONST.Presentation.S_SOUND, - mysensors.CONST.Presentation.S_VIBRATION, - mysensors.CONST.Presentation.S_MOISTURE, - ]) - V_TYPES.extend([mysensors.CONST.SetReq.V_STATUS, ]) + # Only act if loaded via mysensors by discovery event. + # Otherwise gateway is not setup. + if discovery_info is None: + return + + for gateway in mysensors.GATEWAYS.values(): + # Define the S_TYPES and V_TYPES that the platform should handle as + # states. + s_types = [ + gateway.const.Presentation.S_DOOR, + gateway.const.Presentation.S_MOTION, + gateway.const.Presentation.S_SMOKE, + gateway.const.Presentation.S_LIGHT, + gateway.const.Presentation.S_BINARY, + gateway.const.Presentation.S_LOCK, + ] + v_types = [ + gateway.const.SetReq.V_ARMED, + gateway.const.SetReq.V_LIGHT, + gateway.const.SetReq.V_LOCK_STATUS, + ] + if float(gateway.version) >= 1.5: + s_types.extend([ + gateway.const.Presentation.S_SPRINKLER, + gateway.const.Presentation.S_WATER_LEAK, + gateway.const.Presentation.S_SOUND, + gateway.const.Presentation.S_VIBRATION, + gateway.const.Presentation.S_MOISTURE, + ]) + v_types.extend([gateway.const.SetReq.V_STATUS, ]) + + devices = defaultdict(list) + gateway.platform_callbacks.append(mysensors.pf_callback_factory( + s_types, v_types, devices, add_devices, MySensorsSwitch)) class MySensorsSwitch(SwitchDevice): """Represent the value of a MySensors child node.""" - # pylint: disable=too-many-arguments, too-many-instance-attributes + # pylint: disable=too-many-arguments - def __init__(self, port, node_id, child_id, name, value_type): + def __init__(self, gateway, node_id, child_id, name, value_type): """Setup class attributes on instantiation. Args: @@ -85,7 +87,7 @@ class MySensorsSwitch(SwitchDevice): battery_level (int): Node battery level. _values (dict): Child values. Non state values set as state attributes. """ - self.port = port + self.gateway = gateway self.node_id = node_id self.child_id = child_id self._name = name @@ -135,30 +137,31 @@ class MySensorsSwitch(SwitchDevice): def turn_on(self): """Turn the switch on.""" - mysensors.GATEWAYS[self.port].set_child_value( + self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1) self._values[self.value_type] = STATE_ON self.update_ha_state() def turn_off(self): """Turn the switch off.""" - mysensors.GATEWAYS[self.port].set_child_value( + self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0) self._values[self.value_type] = STATE_OFF self.update_ha_state() - def update_sensor(self, values, battery_level): + def update(self): """Update the controller with the latest value from a sensor.""" - for value_type, value in values.items(): + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + for value_type, value in child.values.items(): _LOGGER.info( "%s: value_type %s, value = %s", self._name, value_type, value) - if value_type == mysensors.CONST.SetReq.V_ARMED or \ - value_type == mysensors.CONST.SetReq.V_STATUS or \ - value_type == mysensors.CONST.SetReq.V_LIGHT or \ - value_type == mysensors.CONST.SetReq.V_LOCK_STATUS: + if value_type == self.gateway.const.SetReq.V_ARMED or \ + value_type == self.gateway.const.SetReq.V_STATUS or \ + value_type == self.gateway.const.SetReq.V_LIGHT or \ + value_type == self.gateway.const.SetReq.V_LOCK_STATUS: self._values[value_type] = ( STATE_ON if int(value) == 1 else STATE_OFF) else: self._values[value_type] = value - self.battery_level = battery_level - self.update_ha_state() + self.battery_level = node.battery_level From 4c4e5d5f471ce6318a6d1c3a682dba745a1c9865 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Thu, 31 Dec 2015 06:19:47 +0100 Subject: [PATCH 014/129] Fix to remove old unused variables. --- homeassistant/components/switch/mysensors.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 2b886153d8f..33c214cda76 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -20,10 +20,6 @@ import homeassistant.components.mysensors as mysensors _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] -ADD_DEVICES = None -S_TYPES = None -V_TYPES = None - def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the mysensors platform for switches.""" From d82859b6ea7c790ef39cb2615ebd0d216b58d586 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 10:57:54 +0000 Subject: [PATCH 015/129] Turn off poll --- homeassistant/components/switch/vera.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 0df1c390929..52029a2c5ec 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -143,6 +143,11 @@ class VeraSwitch(ToggleEntity): self.vera_device.switch_off() self.is_on_status = False + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + @property def is_on(self): """ True if device is on. """ From a8bb75d0706030035aed9299e561382e6e3e873b Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 12:16:03 +0000 Subject: [PATCH 016/129] Update sensor with subscription code, change to use pyvera library --- homeassistant/components/light/vera.py | 4 +--- homeassistant/components/sensor/vera.py | 30 ++++++++++++++++++++----- homeassistant/components/switch/vera.py | 4 +--- requirements_all.txt | 2 +- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 23daba4991f..169fa442134 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,9 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 7fb72fd91b7..22fdffc8f1d 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,9 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +35,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + categories = ['Temperature Sensor', 'Light Sensor', 'Sensor'] devices = [] try: @@ -53,7 +60,7 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, extra_data)) + vera_sensors.append(VeraSensor(device, controller, extra_data)) return vera_sensors @@ -66,8 +73,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSensor(Entity): """ Represents a Vera Sensor. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device + self.controller = controller self.extra_data = extra_data if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') @@ -76,6 +84,16 @@ class VeraSensor(Entity): self.current_value = '' self._temperature_units = None + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + def __str__(self): return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 52029a2c5ec..68a0a1d8871 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,9 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a9ec467e8b1..387a7217f92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 +#pyvera==0.2.0 # homeassistant.components.wink # homeassistant.components.light.wink From 90ae5c6646fb9ff09f8c5dc09fa0f089a2208056 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 12:25:24 +0000 Subject: [PATCH 017/129] Add missed import, fix style error. --- homeassistant/components/sensor/vera.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 22fdffc8f1d..ccb71366df6 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -13,7 +13,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, - TEMP_CELCIUS, TEMP_FAHRENHEIT) + TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['#pyvera==0.2.0'] @@ -60,7 +60,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, controller, extra_data)) + vera_sensors.append( + VeraSensor(device, vera_controller, extra_data)) return vera_sensors From 5f89b34831e836f0b8ef60c7c264fde8b5fc41d8 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 16:09:05 +0000 Subject: [PATCH 018/129] Bump pyvera version --- homeassistant/components/light/vera.py | 2 +- homeassistant/components/sensor/vera.py | 2 +- homeassistant/components/switch/vera.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 169fa442134..2627b505ef8 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index ccb71366df6..836dfacf4f1 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 68a0a1d8871..7feea6c99c6 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 387a7217f92..156bc1657f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -#pyvera==0.2.0 +#pyvera==0.2.1 # homeassistant.components.wink # homeassistant.components.light.wink From f8e5df237bb98fabce2330d7a99b9b021f2e7139 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 18:58:12 +0000 Subject: [PATCH 019/129] Remove '#'' from requirements --- homeassistant/components/light/vera.py | 2 +- homeassistant/components/sensor/vera.py | 2 +- homeassistant/components/switch/vera.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 2627b505ef8..9135323fb1f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 836dfacf4f1..db283c51633 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 7feea6c99c6..614b588f36f 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 156bc1657f1..b32d49dcc74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -#pyvera==0.2.1 +pyvera==0.2.1 # homeassistant.components.wink # homeassistant.components.light.wink From 9e0946b207c2d2fb9d69c864424768cfd79d528a Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 19:15:21 +0000 Subject: [PATCH 020/129] Turn off polling for sensor too! --- homeassistant/components/sensor/vera.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index db283c51633..03b8d05d2f5 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -136,6 +136,11 @@ class VeraSensor(Entity): attr['Vera Device Id'] = self.vera_device.vera_device_id return attr + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + def update(self): if self.vera_device.category == "Temperature Sensor": self.vera_device.refresh_value('CurrentTemperature') From c703c89dbd0fea0b10dbddcf5f02d587c467f1a3 Mon Sep 17 00:00:00 2001 From: sander Date: Fri, 1 Jan 2016 15:29:58 +0100 Subject: [PATCH 021/129] implement away mode --- .../components/thermostat/honeywell.py | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 4139c5d8aa7..15162a4d279 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -6,8 +6,9 @@ Adds support for Honeywell Round Connected and Honeywell Evohome thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.honeywell/ """ -import socket import logging +import socket + from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) @@ -15,6 +16,7 @@ REQUIREMENTS = ['evohomeclient==0.2.4'] _LOGGER = logging.getLogger(__name__) +CONF_AWAY_TEMP = "away_temperature" # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -23,20 +25,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - + try: + away_temp = float(config.get(CONF_AWAY_TEMP,16)) + except ValueError: + _LOGGER.error("value entered for item {} should convert to a number" + .format(CONF_AWAY_TEMP)) if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", CONF_USERNAME, CONF_PASSWORD) return False evo_api = EvohomeClient(username, password) + try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0)]) + add_devices([RoundThermostat(evo_api, zone['id'], i == 0,away_temp)]) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" + "Connection error logging into the honeywell evohome web service" ) return False @@ -44,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ - def __init__(self, device, zone_id, master): + def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None self._target_temperature = None @@ -52,6 +59,8 @@ class RoundThermostat(ThermostatDevice): self._id = zone_id self._master = master self._is_dhw = False + self._away_temp = away_temp + self._away = False self.update() @property @@ -80,6 +89,25 @@ class RoundThermostat(ThermostatDevice): """ Set new target temperature """ self.device.set_temperature(self._name, temperature) + @property + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return self._away + + def turn_away_mode_on(self): + """ Turns away on. + Evohome does have a proprietary away mode, but it doesn't really work + the way it should. For example: If you set a temperature manually + it doesn't get overwritten when away mode is switched on. + """ + self._away = True + self.device.set_temperature(self._name, self._away_temp) + + def turn_away_mode_off(self): + """ Turns away off. """ + self._away = False + self.device.cancel_temp_override(self._name) + def update(self): try: # Only refresh if this is the "master" device, From a36ae4b24af3d4903203abe206aebfbc2734be2c Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Sat, 2 Jan 2016 01:01:11 -0500 Subject: [PATCH 022/129] Reduce chatiness --- homeassistant/components/light/hue.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 40875d8ea0e..61bff75bbd3 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -124,9 +124,7 @@ def setup_bridge(host, hass, add_devices_callback): api_name = api.get('config').get('name') if api_name == 'RaspBee-GW': bridge_type = 'deconz' - _LOGGER.info("Found DeCONZ gateway (%s)", api_name) else: - _LOGGER.info("Found Hue bridge (%s)", api_name) bridge_type = 'hue' for light_id, info in api_states.items(): From 8f2ca856c7bec90b55f788dc97ea09bd1c621259 Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 11:56:07 +0100 Subject: [PATCH 023/129] added return False --- homeassistant/components/thermostat/honeywell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 15162a4d279..de6d5ff6a56 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -18,6 +18,7 @@ _LOGGER = logging.getLogger(__name__) CONF_AWAY_TEMP = "away_temperature" + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the honeywel thermostat. """ @@ -26,10 +27,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) try: - away_temp = float(config.get(CONF_AWAY_TEMP,16)) + away_temp = float(config.get(CONF_AWAY_TEMP, 16)) except ValueError: _LOGGER.error("value entered for item {} should convert to a number" .format(CONF_AWAY_TEMP)) + return False if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", CONF_USERNAME, CONF_PASSWORD) @@ -40,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0,away_temp)]) + add_devices([RoundThermostat(evo_api, zone['id'], i == 0, away_temp)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" From cdf2179b3e09c50e21d49314da5cea36ad7ecd4a Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 2 Jan 2016 10:54:26 -0600 Subject: [PATCH 024/129] Describe device tracker see service --- .../components/device_tracker/services.yaml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index e69de29bb2d..63907f7457b 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -0,0 +1,33 @@ +# Describes the format for available device tracker services + +see: + description: Control tracked device + + fields: + mac: + description: MAC address of device + example: 'FF:FF:FF:FF:FF:FF' + + dev_id: + description: Id of device (find id in tracked_devices.yaml) + example: 'phonedave' + + host_name: + description: Hostname of device + example: 'Dave' + + location_name: + description: Name of location where device is located (not_home is away) + example: 'home' + + gps: + description: GPS coordinates where device is located (latitude, longitude) + example: '[51.509802, -0.086692]' + + gps_accuracy: + description: Accuracy of GPS coordinates + example: '80' + + battery: + description: Battery level of device + example: '100' From 5804dde0e979fe097e10c95fe785e69195902007 Mon Sep 17 00:00:00 2001 From: xifle Date: Sat, 2 Jan 2016 18:26:59 +0100 Subject: [PATCH 025/129] Enables the use of owntracks transition events By using the configuration option "use_events:yes" in the device_tracker section, only 'enter'/'leave' events are considered to calculate the state of a tracker device. The home zone is defined as the owntracks region 'home'. Other regions may also be defined, the name of the region is then used as state for the device. All owntracks regions, the 'Share' setting must be enabled in the app. --- .../components/device_tracker/owntracks.py | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index b98c3a1636c..a1818e60901 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,6 +1,6 @@ """ homeassistant.components.device_tracker.owntracks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OwnTracks platform for the device tracker. For more details about this platform, please refer to the documentation at @@ -10,14 +10,16 @@ import json import logging import homeassistant.components.mqtt as mqtt +from homeassistant.const import (STATE_HOME, STATE_NOT_HOME) DEPENDENCIES = ['mqtt'] +CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' - +EVENT_TOPIC = 'owntracks/+/+/event' def setup_scanner(hass, config, see): - """ Set up a OwnTracksks tracker. """ + """ Set up an OwnTracks tracker. """ def owntracks_location_update(topic, payload, qos): """ MQTT message received. """ @@ -47,7 +49,58 @@ def setup_scanner(hass, config, see): kwargs['battery'] = data['batt'] see(**kwargs) + + + def owntracks_event_update(topic, payload, qos): + """ MQTT event (geofences) received. """ - mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typetransition + try: + data = json.loads(payload) + except ValueError: + # If invalid JSON + logging.getLogger(__name__).error( + 'Unable to parse payload as JSON: %s', payload) + return + + if not isinstance(data, dict) or data.get('_type') != 'transition': + return + + + # check if in "home" fence or other zone + location = '' + if data['event'] == 'enter': + + if data['desc'] == 'home': + location = STATE_HOME + else: + location = data['desc'] + + elif data['event'] == 'leave': + location = STATE_NOT_HOME + else: + logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', data['event']) + return + + parts = topic.split('/') + kwargs = { + 'dev_id': '{}_{}'.format(parts[1], parts[2]), + 'host_name': parts[1], + 'gps': (data['lat'], data['lon']), + 'location_name': location, + } + if 'acc' in data: + kwargs['gps_accuracy'] = data['acc'] + + see(**kwargs) + + + use_events = config.get(CONF_TRANSITION_EVENTS) + + if use_events: + mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + else: + mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) return True From 217ffc215ba346a1c9f87b92da6eb862e3d52de0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 10:27:11 -0800 Subject: [PATCH 026/129] Update PyNetgear version --- homeassistant/components/device_tracker/netgear.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 5d20e98e992..ab1eccba769 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear==0.3'] +REQUIREMENTS = ['pynetgear==0.3.1'] def get_scanner(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index 18d308f6d5c..997cbb567e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -16,7 +16,7 @@ fuzzywuzzy==0.8.0 pyicloud==0.7.2 # homeassistant.components.device_tracker.netgear -pynetgear==0.3 +pynetgear==0.3.1 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.4.3 From 39de92960d040da71ccfe3b716be54f2ef8ad39f Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:27:40 +0100 Subject: [PATCH 027/129] line too long change --- homeassistant/components/thermostat/honeywell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index de6d5ff6a56..4d1a7fe708d 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -42,7 +42,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0, away_temp)]) + add_devices([RoundThermostat(evo_api, + zone['id'], + i == 0, + away_temp)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" From 8c7898ed054fc10c07f2c02bd093f54768be615b Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:53:25 +0100 Subject: [PATCH 028/129] pylinting.. --- homeassistant/components/thermostat/honeywell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 4d1a7fe708d..491fe57900e 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): away_temp)]) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" + "Connection error logging into the honeywell evohome web service" ) return False From 36f5caa214215a51b29870a60aa1fda4d7e333ba Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:59:45 +0100 Subject: [PATCH 029/129] more pylinting.. --- homeassistant/components/thermostat/honeywell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 491fe57900e..0b7479bd202 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -29,8 +29,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: away_temp = float(config.get(CONF_AWAY_TEMP, 16)) except ValueError: - _LOGGER.error("value entered for item {} should convert to a number" - .format(CONF_AWAY_TEMP)) + _LOGGER.error("value entered for item %s should convert to a number", + CONF_AWAY_TEMP) return False if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", From 55c5d254d58e6902667ea712dc9eb0d820b258db Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 21:09:03 +0100 Subject: [PATCH 030/129] some more pylinting.. --- homeassistant/components/thermostat/honeywell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 0b7479bd202..5475e1ce306 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -56,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ + # pylint: disable=too-many-instance-attributes def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None From 0361f37178bdc3a409718f5d73e254512925817a Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Sat, 2 Jan 2016 16:13:58 -0500 Subject: [PATCH 031/129] Support multiple hue hubs using a filename parameter. --- homeassistant/components/light/hue.py | 15 ++++++++------- homeassistant/const.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 61bff75bbd3..b828847ecb0 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from homeassistant.loader import get_component import homeassistant.util as util import homeassistant.util.color as color_util -from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME +from homeassistant.const import CONF_HOST, CONF_FILENAME, DEVICE_DEFAULT_NAME from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, @@ -35,9 +35,9 @@ _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -def _find_host_from_config(hass): +def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): """ Attempt to detect host based on existing configuration. """ - path = hass.config.path(PHUE_CONFIG_FILE) + path = hass.config.path(filename) if not os.path.isfile(path): return None @@ -54,13 +54,14 @@ def _find_host_from_config(hass): def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Gets the Hue lights. """ + filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE) if discovery_info is not None: host = urlparse(discovery_info[1]).hostname else: host = config.get(CONF_HOST, None) if host is None: - host = _find_host_from_config(hass) + host = _find_host_from_config(hass, filename) if host is None: _LOGGER.error('No host found in configuration') @@ -70,17 +71,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if host in _CONFIGURING: return - setup_bridge(host, hass, add_devices_callback) + setup_bridge(host, hass, add_devices_callback, filename) -def setup_bridge(host, hass, add_devices_callback): +def setup_bridge(host, hass, add_devices_callback, filename): """ Setup a phue bridge based on host parameter. """ import phue try: bridge = phue.Bridge( host, - config_file_path=hass.config.path(PHUE_CONFIG_FILE)) + config_file_path=hass.config.path(filename)) except ConnectionRefusedError: # Wrong host was given _LOGGER.exception("Error connecting to the Hue bridge at %s", host) diff --git a/homeassistant/const.py b/homeassistant/const.py index 82276d81b48..97e26f8d33a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,6 +24,7 @@ CONF_USERNAME = "username" CONF_PASSWORD = "password" CONF_API_KEY = "api_key" CONF_ACCESS_TOKEN = "access_token" +CONF_FILENAME = "filename" CONF_VALUE_TEMPLATE = "value_template" From 7edbb6aadc081475f521f6dfbc02dbb6f79ac3fb Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:21:04 +0100 Subject: [PATCH 032/129] Added rain sensor --- homeassistant/components/sensor/tellduslive.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 7cc49e3c611..dbf416787a2 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -22,10 +22,14 @@ DEPENDENCIES = ['tellduslive'] SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_RAINRATE = "rrate" +SENSOR_TYPE_RAINTOTAL = "rtot" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], } From 86047eceb1ce21f1d8ac7621396305792163d277 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:28:15 +0100 Subject: [PATCH 033/129] Added wind sensor --- homeassistant/components/sensor/tellduslive.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index dbf416787a2..022ae00a098 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -24,12 +24,18 @@ SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_RAINRATE = "rrate" SENSOR_TYPE_RAINTOTAL = "rtot" +SENSOR_TYPE_WINDDIRECTION = "wdir" +SENSOR_TYPE_WINDAVERAGE = "wavg" +SENSOR_TYPE_WINDGUST = "wgust" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], } From 4a421e25b08f453feba08ad78c4865c2abcc4790 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 13:29:33 -0800 Subject: [PATCH 034/129] Simplify Rest sensors --- .../components/binary_sensor/rest.py | 113 ++++-------------- homeassistant/components/sensor/rest.py | 80 +++---------- 2 files changed, 38 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 6cb6ede5e50..4d82d25e473 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.rest/ """ -from datetime import timedelta import logging -import requests from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.util import template, Throttle +from homeassistant.util import template +from homeassistant.components.sensor.rest import RestData from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) @@ -19,60 +18,33 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_METHOD = 'GET' -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): - """ Get the REST binary sensor. """ - - use_get = False - use_post = False - + """Setup REST binary sensors.""" resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error("Response status is '%s'", response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// or https:// to your URL") - return False - except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint: %s', resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestBinarySensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestBinarySensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): - """ Implements a REST binary sensor. """ + """REST binary sensor.""" def __init__(self, hass, rest, name, value_template): + """Initialize a REST binary sensor.""" self._hass = hass self.rest = rest self._name = name @@ -82,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice): @property def name(self): - """ The name of the binary sensor. """ + """Name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ - if self.rest.data is False: + """Return if the binary sensor is on.""" + if self.rest.data is None: return False - else: - if self._value_template is not None: - self.rest.data = template.render_with_possible_json_value( - self._hass, self._value_template, self.rest.data, False) - return bool(int(self.rest.data)) + + if self._value_template is not None: + self.rest.data = template.render_with_possible_json_value( + self._hass, self._value_template, self.rest.data, False) + return bool(int(self.rest.data)) def update(self): - """ Gets the latest data from REST API and updates the state. """ + """Get the latest data from REST API and updates the state.""" self.rest.update() - - -# pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ - - def __init__(self, resource, verify_ssl): - self._resource = resource - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with GET method. """ - try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index f6a56d3a99e..fdbc1ab26e3 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -26,47 +26,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the REST sensor. """ - - use_get = False - use_post = False - resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error("Response status is '%s'", response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// or https:// to your URL") - return False - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestSensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get('unit_of_measurement'), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestSensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get('unit_of_measurement'), config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments @@ -112,11 +86,11 @@ class RestSensor(Entity): # pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ +class RestData(object): + """Class for handling the data retrieval.""" - def __init__(self, resource, verify_ssl): - self._resource = resource + def __init__(self, method, resource, data, verify_ssl): + self._request = requests.Request(method, resource, data=data).prepare() self._verify_ssl = verify_ssl self.data = None @@ -124,31 +98,11 @@ class RestDataGet(object): def update(self): """ Gets the latest data from REST service with GET method. """ try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) + with requests.Session() as sess: + response = sess.send(self._request, timeout=10, + verify=self._verify_ssl) + self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = None - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) + except requests.exceptions.RequestException: + _LOGGER.error("Error fetching data: %s", self._request) self.data = None From 3abc78eef2c678fe6f0df27f89dfe3ceff997ae9 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:30:02 +0100 Subject: [PATCH 035/129] Added power sensor --- homeassistant/components/sensor/tellduslive.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 022ae00a098..a7bfca49b30 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -27,6 +27,7 @@ SENSOR_TYPE_RAINTOTAL = "rtot" SENSOR_TYPE_WINDDIRECTION = "wdir" SENSOR_TYPE_WINDAVERAGE = "wavg" SENSOR_TYPE_WINDGUST = "wgust" +SENSOR_TYPE_WATT = "watt" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], @@ -36,6 +37,7 @@ SENSOR_TYPES = { SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], + SENSOR_TYPE_WATT: ['Watt', 'W', ""], } From 305c87a9c9753d27f8f003e494009dab7188bdb7 Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 2 Jan 2016 16:01:58 -0600 Subject: [PATCH 036/129] Fix reference known_devices.yaml --- homeassistant/components/device_tracker/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 63907f7457b..dc573ae0275 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -9,7 +9,7 @@ see: example: 'FF:FF:FF:FF:FF:FF' dev_id: - description: Id of device (find id in tracked_devices.yaml) + description: Id of device (find id in known_devices.yaml) example: 'phonedave' host_name: From 7dc1499386dd8e45b58ff79a35b0c702baee782f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 14:23:12 -0800 Subject: [PATCH 037/129] Make CI erros more prominent --- .travis.yml | 4 +--- script/cibuild | 22 ++++++++++++++++++++++ script/test | 14 +++++++------- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4383d49f548..c01b0750360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,7 @@ python: - 3.4 - 3.5 install: - # Validate requirements_all.txt on Python 3.4 - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi - - script/bootstrap_server + - "true" script: - script/cibuild matrix: diff --git a/script/cibuild b/script/cibuild index beb7b22693d..11d91415405 100755 --- a/script/cibuild +++ b/script/cibuild @@ -5,6 +5,28 @@ cd "$(dirname "$0")/.." +if [ "$TRAVIS_PYTHON_VERSION" = "3.5" ]; then + echo "Verifying requirements_all.txt..." + python3 setup.py -q develop 2> /dev/null + tput setaf 1 + script/gen_requirements_all.py validate + VERIFY_REQUIREMENTS_STATUS=$? + tput sgr0 +else + VERIFY_REQUIREMENTS_STATUS=0 +fi + +if [ "$VERIFY_REQUIREMENTS_STATUS" != "0" ]; then + exit $VERIFY_REQUIREMENTS_STATUS +fi + +script/bootstrap_server > /dev/null +DEP_INSTALL_STATUS=$? + +if [ "$DEP_INSTALL_STATUS" != "0" ]; then + exit $DEP_INSTALL_STATUS +fi + if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then NO_LINT=1 fi diff --git a/script/test b/script/test index 25873492001..6a78ce42d41 100755 --- a/script/test +++ b/script/test @@ -5,13 +5,6 @@ cd "$(dirname "$0")/.." -if [ "$NO_LINT" = "1" ]; then - LINT_STATUS=0 -else - script/lint - LINT_STATUS=$? -fi - echo "Running tests..." if [ "$1" = "coverage" ]; then @@ -22,6 +15,13 @@ else TEST_STATUS=$? fi +if [ "$NO_LINT" = "1" ]; then + LINT_STATUS=0 +else + script/lint + LINT_STATUS=$? +fi + if [ $LINT_STATUS -eq 0 ] then exit $TEST_STATUS From 4b96a7c8202470999a862940a337da412eda4a86 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sun, 3 Jan 2016 01:00:10 +0100 Subject: [PATCH 038/129] Untabified lines --- homeassistant/components/sensor/tellduslive.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index a7bfca49b30..ae05ce47e19 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -32,12 +32,12 @@ SENSOR_TYPE_WATT = "watt" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], - SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], - SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], - SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], - SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], - SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], - SENSOR_TYPE_WATT: ['Watt', 'W', ""], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], + SENSOR_TYPE_WATT: ['Watt', 'W', ""], } From c9ff0ab7eb0ef8e52089d1d16699f6235739f52b Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Sun, 3 Jan 2016 00:36:22 -0700 Subject: [PATCH 039/129] Fix for sun if condition --- homeassistant/components/automation/sun.py | 10 ++-- tests/components/automation/test_sun.py | 61 ++++++++++++++++++---- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 394dc904be1..064f6a0a16a 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -82,21 +82,21 @@ def if_action(hass, config): if before is None: before_func = lambda: None elif before == EVENT_SUNRISE: - before_func = lambda: sun.next_rising_utc(hass) + before_offset + before_func = lambda: sun.next_rising(hass) + before_offset else: - before_func = lambda: sun.next_setting_utc(hass) + before_offset + before_func = lambda: sun.next_setting(hass) + before_offset if after is None: after_func = lambda: None elif after == EVENT_SUNRISE: - after_func = lambda: sun.next_rising_utc(hass) + after_offset + after_func = lambda: sun.next_rising(hass) + after_offset else: - after_func = lambda: sun.next_setting_utc(hass) + after_offset + after_func = lambda: sun.next_setting(hass) + after_offset def time_if(): """ Validate time based if-condition """ - now = dt_util.utcnow() + now = dt_util.now() before = before_func() after = after_func() diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 26ecc26c72a..db4782cfd46 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -162,14 +162,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -197,14 +197,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -233,14 +233,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -269,14 +269,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -306,21 +306,60 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_different_tz(self): + import pytz + + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '17:30:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunset', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + # Before + now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + # After + now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() From f8b2570cb3a8df1349f60ffa6393f65aa97d9f82 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 02:32:09 -0800 Subject: [PATCH 040/129] Group entities when reproducing a state --- homeassistant/helpers/state.py | 35 +++++---- tests/helpers/test_state.py | 129 +++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 tests/helpers/test_state.py diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 24a37c5b5ea..019e7ce6ce9 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,9 +1,5 @@ -""" -homeassistant.helpers.state -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Helpers that help with state related things. -""" +"""Helpers that help with state related things.""" +from collections import defaultdict import logging from homeassistant.core import State @@ -25,32 +21,36 @@ class TrackStates(object): that have changed since the start time to the return list when with-block is exited. """ + def __init__(self, hass): + """Initialize a TrackStates block.""" self.hass = hass self.states = [] def __enter__(self): + """Record time from which to track changes.""" self.now = dt_util.utcnow() return self.states def __exit__(self, exc_type, exc_value, traceback): + """Add changes states to changes list.""" self.states.extend(get_changed_since(self.hass.states.all(), self.now)) def get_changed_since(states, utc_point_in_time): - """ - Returns all states that have been changed since utc_point_in_time. - """ + """List of states that have been changed since utc_point_in_time.""" point_in_time = dt_util.strip_microseconds(utc_point_in_time) return [state for state in states if state.last_updated >= point_in_time] def reproduce_state(hass, states, blocking=False): - """ Takes in a state and will try to have the entity reproduce it. """ + """Reproduce given state.""" if isinstance(states, State): states = [states] + to_call = defaultdict(list) + for state in states: current_state = hass.states.get(state.entity_id) @@ -76,7 +76,16 @@ def reproduce_state(hass, states, blocking=False): state) continue - service_data = dict(state.attributes) - service_data[ATTR_ENTITY_ID] = state.entity_id + if state.domain == 'group': + service_domain = 'homeassistant' + else: + service_domain = state.domain - hass.services.call(state.domain, service, service_data, blocking) + # We group service calls for entities by service call + key = (service_domain, service, tuple(state.attributes.items())) + to_call[key].append(state.entity_id) + + for (service_domain, service, service_data), entity_ids in to_call.items(): + data = dict(service_data) + data[ATTR_ENTITY_ID] = entity_ids + hass.services.call(service_domain, service, data, blocking) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py new file mode 100644 index 00000000000..32924b1d6d5 --- /dev/null +++ b/tests/helpers/test_state.py @@ -0,0 +1,129 @@ +""" +tests.helpers.test_state +~~~~~~~~~~~~~~~~~~~~~~~~ + +Test state helpers. +""" +from datetime import timedelta +import unittest +from unittest.mock import patch + +import homeassistant.core as ha +import homeassistant.components as core_components +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.util import dt as dt_util +from homeassistant.helpers import state + +from tests.common import get_test_home_assistant, mock_service + + +class TestStateHelpers(unittest.TestCase): + """ + Tests the Home Assistant event helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = get_test_home_assistant() + core_components.setup(self.hass, {}) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_get_changed_since(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow', return_value=point1): + self.hass.states.set('light.test', 'on') + state1 = self.hass.states.get('light.test') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point2): + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point3): + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + [state2, state3], + state.get_changed_since([state1, state2, state3], point2)) + + def test_track_states(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = point2 + + with state.TrackStates(self.hass) as states: + mock_utcnow.return_value = point1 + self.hass.states.set('light.test', 'on') + + mock_utcnow.return_value = point2 + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + mock_utcnow.return_value = point3 + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + sorted([state2, state3], key=lambda state: state.entity_id), + sorted(states, key=lambda state: state.entity_id)) + + def test_reproduce_state_with_turn_on(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + state.reproduce_state(self.hass, ha.State('light.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test'], last_call.data.get('entity_id')) + + def test_reproduce_state_with_group(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('group.test', 'off', { + 'entity_id': ['light.test1', 'light.test2']}) + + state.reproduce_state(self.hass, ha.State('group.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + + def test_reproduce_state_group_states_with_same_domain_and_data(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test1', 'off') + self.hass.states.set('light.test2', 'off') + + state.reproduce_state(self.hass, [ + ha.State('light.test1', 'on', {'brightness': 95}), + ha.State('light.test2', 'on', {'brightness': 95})]) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + self.assertEqual(95, last_call.data.get('brightness')) From 82904c59ce4abaa7b47048f551611c8ceab067f9 Mon Sep 17 00:00:00 2001 From: xifle Date: Sun, 3 Jan 2016 17:12:11 +0100 Subject: [PATCH 041/129] Fixed code style --- .../components/device_tracker/owntracks.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index a1818e60901..c20b50e7e8c 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -49,8 +49,8 @@ def setup_scanner(hass, config, see): kwargs['battery'] = data['batt'] see(**kwargs) - - + + def owntracks_event_update(topic, payload, qos): """ MQTT event (geofences) received. """ @@ -67,20 +67,21 @@ def setup_scanner(hass, config, see): if not isinstance(data, dict) or data.get('_type') != 'transition': return - + # check if in "home" fence or other zone location = '' if data['event'] == 'enter': - + if data['desc'] == 'home': location = STATE_HOME else: location = data['desc'] - + elif data['event'] == 'leave': location = STATE_NOT_HOME else: - logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', data['event']) + logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', + data['event']) return parts = topic.split('/') @@ -94,10 +95,10 @@ def setup_scanner(hass, config, see): kwargs['gps_accuracy'] = data['acc'] see(**kwargs) - - + + use_events = config.get(CONF_TRANSITION_EVENTS) - + if use_events: mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) else: From d244d3b59958da19697548987574d411cd992acb Mon Sep 17 00:00:00 2001 From: xifle Date: Sun, 3 Jan 2016 17:42:49 +0100 Subject: [PATCH 042/129] Fixed flake8 style errors --- homeassistant/components/device_tracker/owntracks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index c20b50e7e8c..e81952eb770 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,6 +1,6 @@ """ homeassistant.components.device_tracker.owntracks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OwnTracks platform for the device tracker. For more details about this platform, please refer to the documentation at @@ -18,6 +18,7 @@ CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' EVENT_TOPIC = 'owntracks/+/+/event' + def setup_scanner(hass, config, see): """ Set up an OwnTracks tracker. """ @@ -50,7 +51,6 @@ def setup_scanner(hass, config, see): see(**kwargs) - def owntracks_event_update(topic, payload, qos): """ MQTT event (geofences) received. """ @@ -67,7 +67,6 @@ def setup_scanner(hass, config, see): if not isinstance(data, dict) or data.get('_type') != 'transition': return - # check if in "home" fence or other zone location = '' if data['event'] == 'enter': @@ -80,8 +79,9 @@ def setup_scanner(hass, config, see): elif data['event'] == 'leave': location = STATE_NOT_HOME else: - logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', - data['event']) + logging.getLogger(__name__).error( + 'Misformatted mqtt msgs, _type=transition, event=%s', + data['event']) return parts = topic.split('/') @@ -96,7 +96,6 @@ def setup_scanner(hass, config, see): see(**kwargs) - use_events = config.get(CONF_TRANSITION_EVENTS) if use_events: From 736183e6f5c09f08a5ae202832cb03a25a54198d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 11:27:30 -0800 Subject: [PATCH 043/129] Fix bug in reproduce_state with complex state attributes --- homeassistant/helpers/state.py | 7 +++++-- tests/helpers/test_state.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 019e7ce6ce9..c8f6f05661a 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,5 +1,6 @@ """Helpers that help with state related things.""" from collections import defaultdict +import json import logging from homeassistant.core import State @@ -82,10 +83,12 @@ def reproduce_state(hass, states, blocking=False): service_domain = state.domain # We group service calls for entities by service call - key = (service_domain, service, tuple(state.attributes.items())) + # json used to create a hashable version of dict with maybe lists in it + key = (service_domain, service, + json.dumps(state.attributes, sort_keys=True)) to_call[key].append(state.entity_id) for (service_domain, service, service_data), entity_ids in to_call.items(): - data = dict(service_data) + data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids hass.services.call(service_domain, service, data, blocking) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 32924b1d6d5..f4e28330f7a 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -91,6 +91,25 @@ class TestStateHelpers(unittest.TestCase): self.assertEqual(SERVICE_TURN_ON, last_call.service) self.assertEqual(['light.test'], last_call.data.get('entity_id')) + def test_reproduce_state_with_complex_service_data(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + complex_data = ['hello', {'11': '22'}] + + state.reproduce_state(self.hass, ha.State('light.test', 'on', { + 'complex': complex_data + })) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(complex_data, last_call.data.get('complex')) + def test_reproduce_state_with_group(self): light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) From 08aabd18add15ec8a12477b2b80d0ebe4de671d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 11:44:26 -0800 Subject: [PATCH 044/129] New version frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 26 +++++++++---------- .../www_static/home-assistant-polymer | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 64845a350ca..2ded702dc6b 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "be08c5a3ce12040bbdba2db83cb1a568" +VERSION = "72a8220d0db0f7f3702228cd556b8c40" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 8df0a4724a0..edc9635dbf4 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2134,7 +2134,7 @@ case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return }
\ No newline at end of file +o["default"])({SELECT_ENTITY:null,LOG_OUT:null})},function(t,e,n){"use strict";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){t.registerStores({moreInfoEntityId:u["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.getters=e.actions=void 0,e.register=o;var a=n(155),u=i(a),s=n(153),c=r(s),l=n(154),f=r(l);e.actions=c,e.getters=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(u["default"].SHOW_SIDEBAR,{show:e})}function o(t,e){t.dispatch(u["default"].NAVIGATE,{pane:e})}Object.defineProperty(e,"__esModule",{value:!0}),e.showSidebar=i,e.navigate=o;var a=n(26),u=r(a)},function(t,e){"use strict";function n(t){return[r,function(e){return e===t}]}Object.defineProperty(e,"__esModule",{value:!0}),e.isActivePane=n;var r=e.activePane=["selectedNavigationPanel"];e.showSidebar=["showSidebar"]},function(t,e,n){"use strict";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){t.registerStores({selectedNavigationPanel:u["default"],showSidebar:c["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.urlSync=e.getters=e.actions=void 0,e.register=o;var a=n(156),u=i(a),s=n(157),c=i(s),l=n(48),f=r(l),d=n(49),h=r(d),p=n(158),_=r(p);e.actions=f,e.getters=h,e.urlSync=_},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(6),o=r(i);e["default"]=(0,o["default"])({NOTIFICATION_CREATED:null})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(6),o=r(i);e["default"]=(0,o["default"])({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})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return[h(t),function(t){return!!t&&t.services.has(e)}]}function o(t){return[u.getters.byId(t),d,f["default"]]}Object.defineProperty(e,"__esModule",{value:!0}),e.byDomain=e.entityMap=e.hasData=void 0,e.hasService=i,e.canToggleEntity=o;var a=n(10),u=n(9),s=n(54),c=r(s),l=n(168),f=r(l),d=(e.hasData=(0,a.createHasDataGetter)(c["default"]),e.entityMap=(0,a.createEntityMapGetter)(c["default"])),h=e.byDomain=(0,a.createByIdGetter)(c["default"])},function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(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 o(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)}var a=function(){function t(t,e){for(var n=0;n6e4}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n},function(t,e,n){function r(t,e,n){function r(){y&&clearTimeout(y),h&&clearTimeout(h),g=0,h=y=m=void 0}function s(e,n){n&&clearTimeout(n),h=y=m=void 0,e&&(g=o(),p=t.apply(v,d),y||h||(d=v=void 0))}function c(){var t=e-(o()-_);0>=t||t>e?s(m,h):y=setTimeout(c,t)}function l(){s(S,y)}function f(){if(d=arguments,_=o(),v=this,m=S&&(y||!w),b===!1)var n=w&&!y;else{h||w||(g=_);var r=b-(_-g),i=0>=r||r>b;i?(h&&(h=clearTimeout(h)),g=_,p=t.apply(v,d)):h||(h=setTimeout(l,r))}return i&&y?y=clearTimeout(y):y||e===b||(y=setTimeout(c,e)),n&&(i=!0,p=t.apply(v,d)),!i||y||h||(d=v=void 0),p}var d,h,p,_,v,y,m,g=0,b=!1,S=!0;if("function"!=typeof t)throw new TypeError(a);if(e=0>e?0:+e||0,n===!0){var w=!0;S=!1}else i(n)&&(w=!!n.leading,b="maxWait"in n&&u(+n.maxWait||0,e),S="trailing"in n?!!n.trailing:S);return f.cancel=r,f}var i=n(63),o=n(188),a="Expected a function",u=Math.max;t.exports=r},function(t,e,n){function r(t,e){var n=null==t?void 0:t[e];return i(n)?n:void 0}var i=n(191);t.exports=r},function(t,e){function n(t){return!!t&&"object"==typeof t}t.exports=n},function(t,e,n){function r(t){return i(t)&&u.call(t)==o}var i=n(63),o="[object Function]",a=Object.prototype,u=a.toString;t.exports=r},function(t,e){function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){(function(t){!function(e,n){t.exports=n()}(this,function(){"use strict";function e(){return Nn.apply(null,arguments)}function n(t){Nn=t}function r(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function o(t,e){var n,r=[];for(n=0;n0)for(n in zn)r=zn[n],i=e[r],"undefined"!=typeof i&&(t[r]=i);return t}function p(t){h(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),xn===!1&&(xn=!0,e.updateOffset(this),xn=!1)}function _(t){return t instanceof p||null!=t&&null!=t._isAMomentObject}function v(t){return 0>t?Math.ceil(t):Math.floor(t)}function y(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=v(e)),n}function m(t,e,n){var r,i=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),a=0;for(r=0;i>r;r++)(n&&t[r]!==e[r]||!n&&y(t[r])!==y(e[r]))&&a++;return a+o}function g(){}function b(t){return t?t.toLowerCase().replace("_","-"):t}function S(t){for(var e,n,r,i,o=0;o0;){if(r=w(i.slice(0,e).join("-")))return r;if(n&&n.length>=e&&m(i,n,!0)>=e-1)break;e--}o++}return null}function w(e){var n=null;if(!Hn[e]&&"undefined"!=typeof t&&t&&t.exports)try{n=Rn._abbr,!function(){var t=new Error('Cannot find module "./locale"');throw t.code="MODULE_NOT_FOUND",t}(),O(n)}catch(r){}return Hn[e]}function O(t,e){var n;return t&&(n="undefined"==typeof e?T(t):M(t,e),n&&(Rn=n)),Rn._abbr}function M(t,e){return null!==e?(e.abbr=t,Hn[t]=Hn[t]||new g,Hn[t].set(e),O(t),Hn[t]):(delete Hn[t],null)}function T(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return Rn;if(!r(t)){if(e=w(t))return e;t=[t]}return S(t)}function I(t,e){var n=t.toLowerCase();Yn[n]=Yn[n+"s"]=Yn[e]=t}function E(t){return"string"==typeof t?Yn[t]||Yn[t.toLowerCase()]:void 0}function D(t){var e,n,r={};for(n in t)a(t,n)&&(e=E(n),e&&(r[e]=t[n]));return r}function C(t,n){return function(r){return null!=r?(A(this,t,r),e.updateOffset(this,n),this):j(this,t)}}function j(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function A(t,e,n){return t._d["set"+(t._isUTC?"UTC":"")+e](n)}function P(t,e){var n;if("object"==typeof t)for(n in t)this.set(n,t[n]);else if(t=E(t),"function"==typeof this[t])return this[t](e);return this}function k(t,e,n){var r=""+Math.abs(t),i=e-r.length,o=t>=0;return(o?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+r}function L(t,e,n,r){var i=r;"string"==typeof r&&(i=function(){return this[r]()}),t&&(Fn[t]=i),e&&(Fn[e[0]]=function(){return k(i.apply(this,arguments),e[1],e[2])}),n&&(Fn[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),t)})}function N(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function R(t){var e,n,r=t.match(Un);for(e=0,n=r.length;n>e;e++)Fn[r[e]]?r[e]=Fn[r[e]]:r[e]=N(r[e]);return function(i){var o="";for(e=0;n>e;e++)o+=r[e]instanceof Function?r[e].call(i,t):r[e];return o}}function z(t,e){return t.isValid()?(e=x(e,t.localeData()),Bn[e]=Bn[e]||R(e),Bn[e](t)):t.localeData().invalidDate()}function x(t,e){function n(t){return e.longDateFormat(t)||t}var r=5;for(Gn.lastIndex=0;r>=0&&Gn.test(t);)t=t.replace(Gn,n),Gn.lastIndex=0,r-=1;return t}function H(t){return"function"==typeof t&&"[object Function]"===Object.prototype.toString.call(t)}function Y(t,e,n){or[t]=H(e)?e:function(t){return t&&n?n:e}}function U(t,e){return a(or,t)?or[t](e._strict,e._locale):new RegExp(G(t))}function G(t){return t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,r,i){return e||n||r||i}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function B(t,e){var n,r=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(r=function(t,n){n[e]=y(t)}),n=0;nr;r++){if(i=s([2e3,r]),n&&!this._longMonthsParse[r]&&(this._longMonthsParse[r]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[r]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[r]||(o="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[r]=new RegExp(o.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[r].test(t))return r;if(n&&"MMM"===e&&this._shortMonthsParse[r].test(t))return r;if(!n&&this._monthsParse[r].test(t))return r}}function $(t,e){var n;return"string"==typeof e&&(e=t.localeData().monthsParse(e),"number"!=typeof e)?t:(n=Math.min(t.date(),q(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t)}function Z(t){return null!=t?($(this,t),e.updateOffset(this,!0),this):j(this,"Month")}function X(){return q(this.year(),this.month())}function Q(t){var e,n=t._a;return n&&-2===l(t).overflow&&(e=n[sr]<0||n[sr]>11?sr:n[cr]<1||n[cr]>q(n[ur],n[sr])?cr:n[lr]<0||n[lr]>24||24===n[lr]&&(0!==n[fr]||0!==n[dr]||0!==n[hr])?lr:n[fr]<0||n[fr]>59?fr:n[dr]<0||n[dr]>59?dr:n[hr]<0||n[hr]>999?hr:-1,l(t)._overflowDayOfYear&&(ur>e||e>cr)&&(e=cr),l(t).overflow=e),t}function tt(t){e.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function et(t,e){var n=!0;return u(function(){return n&&(tt(t+"\n"+(new Error).stack),n=!1),e.apply(this,arguments)},e)}function nt(t,e){vr[t]||(tt(e),vr[t]=!0)}function rt(t){var e,n,r=t._i,i=yr.exec(r);if(i){for(l(t).iso=!0,e=0,n=mr.length;n>e;e++)if(mr[e][1].exec(r)){t._f=mr[e][0];break}for(e=0,n=gr.length;n>e;e++)if(gr[e][1].exec(r)){t._f+=(i[6]||" ")+gr[e][0];break}r.match(nr)&&(t._f+="Z"),wt(t)}else t._isValid=!1}function it(t){var n=br.exec(t._i);return null!==n?void(t._d=new Date(+n[1])):(rt(t),void(t._isValid===!1&&(delete t._isValid,e.createFromInputFallback(t))))}function ot(t,e,n,r,i,o,a){var u=new Date(t,e,n,r,i,o,a);return 1970>t&&u.setFullYear(t),u}function at(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function ut(t){return st(t)?366:365}function st(t){return t%4===0&&t%100!==0||t%400===0}function ct(){return st(this.year())}function lt(t,e,n){var r,i=n-e,o=n-t.day();return o>i&&(o-=7),i-7>o&&(o+=7),r=jt(t).add(o,"d"),{week:Math.ceil(r.dayOfYear()/7),year:r.year()}}function ft(t){return lt(t,this._week.dow,this._week.doy).week}function dt(){return this._week.dow}function ht(){return this._week.doy}function pt(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function _t(t){var e=lt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function vt(t,e,n,r,i){var o,a=6+i-r,u=at(t,0,1+a),s=u.getUTCDay();return i>s&&(s+=7),n=null!=n?1*n:i,o=1+a+7*(e-1)-s+n,{year:o>0?t:t-1,dayOfYear:o>0?o:ut(t-1)+o}}function yt(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function mt(t,e,n){return null!=t?t:null!=e?e:n}function gt(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function bt(t){var e,n,r,i,o=[];if(!t._d){for(r=gt(t),t._w&&null==t._a[cr]&&null==t._a[sr]&&St(t),t._dayOfYear&&(i=mt(t._a[ur],r[ur]),t._dayOfYear>ut(i)&&(l(t)._overflowDayOfYear=!0),n=at(i,0,t._dayOfYear),t._a[sr]=n.getUTCMonth(),t._a[cr]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=o[e]=r[e];for(;7>e;e++)t._a[e]=o[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[lr]&&0===t._a[fr]&&0===t._a[dr]&&0===t._a[hr]&&(t._nextDay=!0,t._a[lr]=0),t._d=(t._useUTC?at:ot).apply(null,o),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[lr]=24)}}function St(t){var e,n,r,i,o,a,u;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(o=1,a=4,n=mt(e.GG,t._a[ur],lt(jt(),1,4).year),r=mt(e.W,1),i=mt(e.E,1)):(o=t._locale._week.dow,a=t._locale._week.doy,n=mt(e.gg,t._a[ur],lt(jt(),o,a).year),r=mt(e.w,1),null!=e.d?(i=e.d,o>i&&++r):i=null!=e.e?e.e+o:o),u=vt(n,r,i,a,o),t._a[ur]=u.year,t._dayOfYear=u.dayOfYear}function wt(t){if(t._f===e.ISO_8601)return void rt(t);t._a=[],l(t).empty=!0;var n,r,i,o,a,u=""+t._i,s=u.length,c=0;for(i=x(t._f,t._locale).match(Un)||[],n=0;n0&&l(t).unusedInput.push(a),u=u.slice(u.indexOf(r)+r.length),c+=r.length),Fn[o]?(r?l(t).empty=!1:l(t).unusedTokens.push(o),V(o,r,t)):t._strict&&!r&&l(t).unusedTokens.push(o);l(t).charsLeftOver=s-c,u.length>0&&l(t).unusedInput.push(u),l(t).bigHour===!0&&t._a[lr]<=12&&t._a[lr]>0&&(l(t).bigHour=void 0),t._a[lr]=Ot(t._locale,t._a[lr],t._meridiem),bt(t),Q(t)}function Ot(t,e,n){var r;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(r=t.isPM(n),r&&12>e&&(e+=12),r||12!==e||(e=0),e):e}function Mt(t){var e,n,r,i,o;if(0===t._f.length)return l(t).invalidFormat=!0,void(t._d=new Date(NaN));for(i=0;io)&&(r=o,n=e));u(t,n||e)}function Tt(t){if(!t._d){var e=D(t._i);t._a=[e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],bt(t)}}function It(t){var e=new p(Q(Et(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function Et(t){var e=t._i,n=t._f;return t._locale=t._locale||T(t._l),null===e||void 0===n&&""===e?d({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new p(Q(e)):(r(n)?Mt(t):n?wt(t):i(e)?t._d=e:Dt(t),t))}function Dt(t){var n=t._i;void 0===n?t._d=new Date:i(n)?t._d=new Date(+n):"string"==typeof n?it(t):r(n)?(t._a=o(n.slice(0),function(t){return parseInt(t,10)}),bt(t)):"object"==typeof n?Tt(t):"number"==typeof n?t._d=new Date(n):e.createFromInputFallback(t)}function Ct(t,e,n,r,i){var o={};return"boolean"==typeof n&&(r=n,n=void 0),o._isAMomentObject=!0,o._useUTC=o._isUTC=i,o._l=n,o._i=t,o._f=e,o._strict=r,It(o)}function jt(t,e,n,r){return Ct(t,e,n,r,!1)}function At(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return jt();for(n=e[0],i=1;it&&(t=-t,n="-"),n+k(~~(t/60),2)+e+k(~~t%60,2)})}function zt(t){var e=(t||"").match(nr)||[],n=e[e.length-1]||[],r=(n+"").match(Tr)||["-",0,0],i=+(60*r[1])+y(r[2]);return"+"===r[0]?i:-i}function xt(t,n){var r,o;return n._isUTC?(r=n.clone(),o=(_(t)||i(t)?+t:+jt(t))-+r,r._d.setTime(+r._d+o),e.updateOffset(r,!1),r):jt(t).local()}function Ht(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Yt(t,n){var r,i=this._offset||0;return null!=t?("string"==typeof t&&(t=zt(t)),Math.abs(t)<16&&(t=60*t),!this._isUTC&&n&&(r=Ht(this)),this._offset=t,this._isUTC=!0,null!=r&&this.add(r,"m"),i!==t&&(!n||this._changeInProgress?ne(this,Zt(t-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,e.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?i:Ht(this)}function Ut(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Gt(t){return this.utcOffset(0,t)}function Bt(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ht(this),"m")),this}function Ft(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(zt(this._i)),this}function Vt(t){return t=t?jt(t).utcOffset():0,(this.utcOffset()-t)%60===0}function qt(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Wt(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var t={};if(h(t,this),t=Et(t),t._a){var e=t._isUTC?s(t._a):jt(t._a);this._isDSTShifted=this.isValid()&&m(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Kt(){return!this._isUTC}function Jt(){return this._isUTC}function $t(){return this._isUTC&&0===this._offset}function Zt(t,e){var n,r,i,o=t,u=null;return Nt(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(u=Ir.exec(t))?(n="-"===u[1]?-1:1,o={y:0,d:y(u[cr])*n,h:y(u[lr])*n,m:y(u[fr])*n,s:y(u[dr])*n,ms:y(u[hr])*n}):(u=Er.exec(t))?(n="-"===u[1]?-1:1,o={y:Xt(u[2],n),M:Xt(u[3],n),d:Xt(u[4],n),h:Xt(u[5],n),m:Xt(u[6],n),s:Xt(u[7],n),w:Xt(u[8],n)}):null==o?o={}:"object"==typeof o&&("from"in o||"to"in o)&&(i=te(jt(o.from),jt(o.to)),o={},o.ms=i.milliseconds,o.M=i.months),r=new Lt(o),Nt(t)&&a(t,"_locale")&&(r._locale=t._locale),r}function Xt(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Qt(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function te(t,e){var n;return e=xt(e,t),t.isBefore(e)?n=Qt(t,e):(n=Qt(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n}function ee(t,e){return function(n,r){var i,o;return null===r||isNaN(+r)||(nt(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period)."),o=n,n=r,r=o),n="string"==typeof n?+n:n,i=Zt(n,r),ne(this,i,t),this}}function ne(t,n,r,i){var o=n._milliseconds,a=n._days,u=n._months;i=null==i?!0:i,o&&t._d.setTime(+t._d+o*r),a&&A(t,"Date",j(t,"Date")+a*r),u&&$(t,j(t,"Month")+u*r),i&&e.updateOffset(t,a||u)}function re(t,e){var n=t||jt(),r=xt(n,this).startOf("day"),i=this.diff(r,"days",!0),o=-6>i?"sameElse":-1>i?"lastWeek":0>i?"lastDay":1>i?"sameDay":2>i?"nextDay":7>i?"nextWeek":"sameElse";return this.format(e&&e[o]||this.localeData().calendar(o,this,jt(n)))}function ie(){return new p(this)}function oe(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=_(t)?t:jt(t),+this>+t):(n=_(t)?+t:+jt(t),n<+this.clone().startOf(e))}function ae(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=_(t)?t:jt(t),+t>+this):(n=_(t)?+t:+jt(t),+this.clone().endOf(e)e-o?(n=t.clone().add(i-1,"months"),r=(e-o)/(o-n)):(n=t.clone().add(i+1,"months"),r=(e-o)/(n-o)),-(i+r)}function fe(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function de(){var t=this.clone().utc();return 0e;e++)if(this._weekdaysParse[e]||(n=jt([2e3,1]).day(e),r="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(r.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e}function Ge(t){var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=ze(t,this.localeData()),this.add(t-e,"d")):e}function Be(t){var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function Fe(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)}function Ve(t,e){L(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function qe(t,e){return e._meridiemParse}function We(t){return"p"===(t+"").toLowerCase().charAt(0)}function Ke(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Je(t,e){e[hr]=y(1e3*("0."+t))}function $e(){return this._isUTC?"UTC":""}function Ze(){return this._isUTC?"Coordinated Universal Time":""}function Xe(t){return jt(1e3*t)}function Qe(){return jt.apply(null,arguments).parseZone()}function tn(t,e,n){var r=this._calendar[t];return"function"==typeof r?r.call(e,n):r}function en(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function nn(){return this._invalidDate}function rn(t){return this._ordinal.replace("%d",t)}function on(t){return t}function an(t,e,n,r){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,r):i.replace(/%d/i,t)}function un(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)}function sn(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function cn(t,e,n,r){var i=T(),o=s().set(r,e);return i[n](o,t)}function ln(t,e,n,r,i){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return cn(t,e,n,i);var o,a=[];for(o=0;r>o;o++)a[o]=cn(t,o,n,i);return a}function fn(t,e){return ln(t,e,"months",12,"month")}function dn(t,e){return ln(t,e,"monthsShort",12,"month")}function hn(t,e){return ln(t,e,"weekdays",7,"day")}function pn(t,e){return ln(t,e,"weekdaysShort",7,"day")}function _n(t,e){return ln(t,e,"weekdaysMin",7,"day")}function vn(){var t=this._data;return this._milliseconds=$r(this._milliseconds),this._days=$r(this._days),this._months=$r(this._months),t.milliseconds=$r(t.milliseconds),t.seconds=$r(t.seconds),t.minutes=$r(t.minutes),t.hours=$r(t.hours),t.months=$r(t.months),t.years=$r(t.years),this}function yn(t,e,n,r){var i=Zt(e,n);return t._milliseconds+=r*i._milliseconds,t._days+=r*i._days,t._months+=r*i._months,t._bubble()}function mn(t,e){return yn(this,t,e,1)}function gn(t,e){return yn(this,t,e,-1)}function bn(t){return 0>t?Math.floor(t):Math.ceil(t)}function Sn(){var t,e,n,r,i,o=this._milliseconds,a=this._days,u=this._months,s=this._data;return o>=0&&a>=0&&u>=0||0>=o&&0>=a&&0>=u||(o+=864e5*bn(On(u)+a),a=0,u=0),s.milliseconds=o%1e3,t=v(o/1e3),s.seconds=t%60,e=v(t/60),s.minutes=e%60,n=v(e/60),s.hours=n%24,a+=v(n/24),i=v(wn(a)),u+=i,a-=bn(On(i)),r=v(u/12),u%=12,s.days=a,s.months=u,s.years=r,this}function wn(t){return 4800*t/146097}function On(t){return 146097*t/4800}function Mn(t){var e,n,r=this._milliseconds;if(t=E(t),"month"===t||"year"===t)return e=this._days+r/864e5,n=this._months+wn(e),"month"===t?n:n/12;switch(e=this._days+Math.round(On(this._months)),t){case"week":return e/7+r/6048e5;case"day":return e+r/864e5;case"hour":return 24*e+r/36e5;case"minute":return 1440*e+r/6e4;case"second":return 86400*e+r/1e3;case"millisecond":return Math.floor(864e5*e)+r;default:throw new Error("Unknown unit "+t)}}function Tn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*y(this._months/12)}function In(t){return function(){return this.as(t)}}function En(t){return t=E(t),this[t+"s"]()}function Dn(t){return function(){return this._data[t]}}function Cn(){return v(this.days()/7)}function jn(t,e,n,r,i){return i.relativeTime(e||1,!!n,t,r)}function An(t,e,n){var r=Zt(t).abs(),i=di(r.as("s")),o=di(r.as("m")),a=di(r.as("h")),u=di(r.as("d")),s=di(r.as("M")),c=di(r.as("y")),l=i0,l[4]=n,jn.apply(null,l)}function Pn(t,e){return void 0===hi[t]?!1:void 0===e?hi[t]:(hi[t]=e,!0)}function kn(t){var e=this.localeData(),n=An(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function Ln(){var t,e,n,r=pi(this._milliseconds)/1e3,i=pi(this._days),o=pi(this._months);t=v(r/60),e=v(t/60),r%=60,t%=60,n=v(o/12),o%=12;var a=n,u=o,s=i,c=e,l=t,f=r,d=this.asSeconds();return d?(0>d?"-":"")+"P"+(a?a+"Y":"")+(u?u+"M":"")+(s?s+"D":"")+(c||l||f?"T":"")+(c?c+"H":"")+(l?l+"M":"")+(f?f+"S":""):"P0D"}var Nn,Rn,zn=e.momentProperties=[],xn=!1,Hn={},Yn={},Un=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Gn=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Bn={},Fn={},Vn=/\d/,qn=/\d\d/,Wn=/\d{3}/,Kn=/\d{4}/,Jn=/[+-]?\d{6}/,$n=/\d\d?/,Zn=/\d{1,3}/,Xn=/\d{1,4}/,Qn=/[+-]?\d{1,6}/,tr=/\d+/,er=/[+-]?\d+/,nr=/Z|[+-]\d\d:?\d\d/gi,rr=/[+-]?\d+(\.\d{1,3})?/,ir=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,or={},ar={},ur=0,sr=1,cr=2,lr=3,fr=4,dr=5,hr=6;L("M",["MM",2],"Mo",function(){return this.month()+1}),L("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),L("MMMM",0,0,function(t){return this.localeData().months(this,t)}),I("month","M"),Y("M",$n),Y("MM",$n,qn),Y("MMM",ir),Y("MMMM",ir),B(["M","MM"],function(t,e){e[sr]=y(t)-1}),B(["MMM","MMMM"],function(t,e,n,r){var i=n._locale.monthsParse(t,r,n._strict);null!=i?e[sr]=i:l(n).invalidMonth=t});var pr="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),_r="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),vr={}; +e.suppressDeprecationWarnings=!1;var yr=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mr=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],gr=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],br=/^\/?Date\((\-?\d+)/i;e.createFromInputFallback=et("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),L(0,["YY",2],0,function(){return this.year()%100}),L(0,["YYYY",4],0,"year"),L(0,["YYYYY",5],0,"year"),L(0,["YYYYYY",6,!0],0,"year"),I("year","y"),Y("Y",er),Y("YY",$n,qn),Y("YYYY",Xn,Kn),Y("YYYYY",Qn,Jn),Y("YYYYYY",Qn,Jn),B(["YYYYY","YYYYYY"],ur),B("YYYY",function(t,n){n[ur]=2===t.length?e.parseTwoDigitYear(t):y(t)}),B("YY",function(t,n){n[ur]=e.parseTwoDigitYear(t)}),e.parseTwoDigitYear=function(t){return y(t)+(y(t)>68?1900:2e3)};var Sr=C("FullYear",!1);L("w",["ww",2],"wo","week"),L("W",["WW",2],"Wo","isoWeek"),I("week","w"),I("isoWeek","W"),Y("w",$n),Y("ww",$n,qn),Y("W",$n),Y("WW",$n,qn),F(["w","ww","W","WW"],function(t,e,n,r){e[r.substr(0,1)]=y(t)});var wr={dow:0,doy:6};L("DDD",["DDDD",3],"DDDo","dayOfYear"),I("dayOfYear","DDD"),Y("DDD",Zn),Y("DDDD",Wn),B(["DDD","DDDD"],function(t,e,n){n._dayOfYear=y(t)}),e.ISO_8601=function(){};var Or=et("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var t=jt.apply(null,arguments);return this>t?this:t}),Mr=et("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var t=jt.apply(null,arguments);return t>this?this:t});Rt("Z",":"),Rt("ZZ",""),Y("Z",nr),Y("ZZ",nr),B(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=zt(t)});var Tr=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var Ir=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Er=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Zt.fn=Lt.prototype;var Dr=ee(1,"add"),Cr=ee(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var jr=et("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});L(0,["gg",2],0,function(){return this.weekYear()%100}),L(0,["GG",2],0,function(){return this.isoWeekYear()%100}),je("gggg","weekYear"),je("ggggg","weekYear"),je("GGGG","isoWeekYear"),je("GGGGG","isoWeekYear"),I("weekYear","gg"),I("isoWeekYear","GG"),Y("G",er),Y("g",er),Y("GG",$n,qn),Y("gg",$n,qn),Y("GGGG",Xn,Kn),Y("gggg",Xn,Kn),Y("GGGGG",Qn,Jn),Y("ggggg",Qn,Jn),F(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,r){e[r.substr(0,2)]=y(t)}),F(["gg","GG"],function(t,n,r,i){n[i]=e.parseTwoDigitYear(t)}),L("Q",0,0,"quarter"),I("quarter","Q"),Y("Q",Vn),B("Q",function(t,e){e[sr]=3*(y(t)-1)}),L("D",["DD",2],"Do","date"),I("date","D"),Y("D",$n),Y("DD",$n,qn),Y("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),B(["D","DD"],cr),B("Do",function(t,e){e[cr]=y(t.match($n)[0],10)});var Ar=C("Date",!0);L("d",0,"do","day"),L("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),L("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),L("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),L("e",0,0,"weekday"),L("E",0,0,"isoWeekday"),I("day","d"),I("weekday","e"),I("isoWeekday","E"),Y("d",$n),Y("e",$n),Y("E",$n),Y("dd",ir),Y("ddd",ir),Y("dddd",ir),F(["dd","ddd","dddd"],function(t,e,n){var r=n._locale.weekdaysParse(t);null!=r?e.d=r:l(n).invalidWeekday=t}),F(["d","e","E"],function(t,e,n,r){e[r]=y(t)});var Pr="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),kr="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Lr="Su_Mo_Tu_We_Th_Fr_Sa".split("_");L("H",["HH",2],0,"hour"),L("h",["hh",2],0,function(){return this.hours()%12||12}),Ve("a",!0),Ve("A",!1),I("hour","h"),Y("a",qe),Y("A",qe),Y("H",$n),Y("h",$n),Y("HH",$n,qn),Y("hh",$n,qn),B(["H","HH"],lr),B(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),B(["h","hh"],function(t,e,n){e[lr]=y(t),l(n).bigHour=!0});var Nr=/[ap]\.?m?\.?/i,Rr=C("Hours",!0);L("m",["mm",2],0,"minute"),I("minute","m"),Y("m",$n),Y("mm",$n,qn),B(["m","mm"],fr);var zr=C("Minutes",!1);L("s",["ss",2],0,"second"),I("second","s"),Y("s",$n),Y("ss",$n,qn),B(["s","ss"],dr);var xr=C("Seconds",!1);L("S",0,0,function(){return~~(this.millisecond()/100)}),L(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),L(0,["SSS",3],0,"millisecond"),L(0,["SSSS",4],0,function(){return 10*this.millisecond()}),L(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),L(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),L(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),L(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),L(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),I("millisecond","ms"),Y("S",Zn,Vn),Y("SS",Zn,qn),Y("SSS",Zn,Wn);var Hr;for(Hr="SSSS";Hr.length<=9;Hr+="S")Y(Hr,tr);for(Hr="S";Hr.length<=9;Hr+="S")B(Hr,Je);var Yr=C("Milliseconds",!1);L("z",0,0,"zoneAbbr"),L("zz",0,0,"zoneName");var Ur=p.prototype;Ur.add=Dr,Ur.calendar=re,Ur.clone=ie,Ur.diff=ce,Ur.endOf=Se,Ur.format=he,Ur.from=pe,Ur.fromNow=_e,Ur.to=ve,Ur.toNow=ye,Ur.get=P,Ur.invalidAt=Ce,Ur.isAfter=oe,Ur.isBefore=ae,Ur.isBetween=ue,Ur.isSame=se,Ur.isValid=Ee,Ur.lang=jr,Ur.locale=me,Ur.localeData=ge,Ur.max=Mr,Ur.min=Or,Ur.parsingFlags=De,Ur.set=P,Ur.startOf=be,Ur.subtract=Cr,Ur.toArray=Te,Ur.toObject=Ie,Ur.toDate=Me,Ur.toISOString=de,Ur.toJSON=de,Ur.toString=fe,Ur.unix=Oe,Ur.valueOf=we,Ur.year=Sr,Ur.isLeapYear=ct,Ur.weekYear=Pe,Ur.isoWeekYear=ke,Ur.quarter=Ur.quarters=Re,Ur.month=Z,Ur.daysInMonth=X,Ur.week=Ur.weeks=pt,Ur.isoWeek=Ur.isoWeeks=_t,Ur.weeksInYear=Ne,Ur.isoWeeksInYear=Le,Ur.date=Ar,Ur.day=Ur.days=Ge,Ur.weekday=Be,Ur.isoWeekday=Fe,Ur.dayOfYear=yt,Ur.hour=Ur.hours=Rr,Ur.minute=Ur.minutes=zr,Ur.second=Ur.seconds=xr,Ur.millisecond=Ur.milliseconds=Yr,Ur.utcOffset=Yt,Ur.utc=Gt,Ur.local=Bt,Ur.parseZone=Ft,Ur.hasAlignedHourOffset=Vt,Ur.isDST=qt,Ur.isDSTShifted=Wt,Ur.isLocal=Kt,Ur.isUtcOffset=Jt,Ur.isUtc=$t,Ur.isUTC=$t,Ur.zoneAbbr=$e,Ur.zoneName=Ze,Ur.dates=et("dates accessor is deprecated. Use date instead.",Ar),Ur.months=et("months accessor is deprecated. Use month instead",Z),Ur.years=et("years accessor is deprecated. Use year instead",Sr),Ur.zone=et("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ut);var Gr=Ur,Br={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Fr={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"},Vr="Invalid date",qr="%d",Wr=/\d{1,2}/,Kr={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"},Jr=g.prototype;Jr._calendar=Br,Jr.calendar=tn,Jr._longDateFormat=Fr,Jr.longDateFormat=en,Jr._invalidDate=Vr,Jr.invalidDate=nn,Jr._ordinal=qr,Jr.ordinal=rn,Jr._ordinalParse=Wr,Jr.preparse=on,Jr.postformat=on,Jr._relativeTime=Kr,Jr.relativeTime=an,Jr.pastFuture=un,Jr.set=sn,Jr.months=W,Jr._months=pr,Jr.monthsShort=K,Jr._monthsShort=_r,Jr.monthsParse=J,Jr.week=ft,Jr._week=wr,Jr.firstDayOfYear=ht,Jr.firstDayOfWeek=dt,Jr.weekdays=xe,Jr._weekdays=Pr,Jr.weekdaysMin=Ye,Jr._weekdaysMin=Lr,Jr.weekdaysShort=He,Jr._weekdaysShort=kr,Jr.weekdaysParse=Ue,Jr.isPM=We,Jr._meridiemParse=Nr,Jr.meridiem=Ke,O("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===y(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),e.lang=et("moment.lang is deprecated. Use moment.locale instead.",O),e.langData=et("moment.langData is deprecated. Use moment.localeData instead.",T);var $r=Math.abs,Zr=In("ms"),Xr=In("s"),Qr=In("m"),ti=In("h"),ei=In("d"),ni=In("w"),ri=In("M"),ii=In("y"),oi=Dn("milliseconds"),ai=Dn("seconds"),ui=Dn("minutes"),si=Dn("hours"),ci=Dn("days"),li=Dn("months"),fi=Dn("years"),di=Math.round,hi={s:45,m:45,h:22,d:26,M:11},pi=Math.abs,_i=Lt.prototype;_i.abs=vn,_i.add=mn,_i.subtract=gn,_i.as=Mn,_i.asMilliseconds=Zr,_i.asSeconds=Xr,_i.asMinutes=Qr,_i.asHours=ti,_i.asDays=ei,_i.asWeeks=ni,_i.asMonths=ri,_i.asYears=ii,_i.valueOf=Tn,_i._bubble=Sn,_i.get=En,_i.milliseconds=oi,_i.seconds=ai,_i.minutes=ui,_i.hours=si,_i.days=ci,_i.weeks=Cn,_i.months=li,_i.years=fi,_i.humanize=kn,_i.toISOString=Ln,_i.toString=Ln,_i.toJSON=Ln,_i.locale=me,_i.localeData=ge,_i.toIsoString=et("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Ln),_i.lang=jr,L("X",0,0,"unix"),L("x",0,0,"valueOf"),Y("x",er),Y("X",rr),B("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),B("x",function(t,e,n){n._d=new Date(y(t))}),e.version="2.10.6",n(jt),e.fn=Gr,e.min=Pt,e.max=kt,e.utc=s,e.unix=Xe,e.months=fn,e.isDate=i,e.locale=O,e.invalid=d,e.duration=Zt,e.isMoment=_,e.weekdays=hn,e.parseZone=Qe,e.localeData=T,e.isDuration=Nt,e.monthsShort=dn,e.weekdaysMin=_n,e.defineLocale=M,e.weekdaysShort=pn,e.normalizeUnits=E,e.relativeTimeThreshold=Pn;var vi=e;return vi})}).call(e,n(65)(t))},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e,n){"use strict";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){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var a=n(162),u=n(182),s=i(u),c=n(184),l=i(c),f=n(186),d=i(f),h=n(15),p=r(h),_=n(24),v=r(_),y=n(9),m=r(y),g=n(44),b=r(g),S=n(142),w=r(S),O=n(25),M=r(O),T=n(147),I=r(T),E=n(47),D=r(E),C=n(50),j=r(C),A=n(27),P=r(A),k=n(13),L=r(k),N=n(28),R=r(N),z=n(30),x=r(z),H=n(179),Y=r(H),U=n(10),G=r(U),B=function F(){o(this,F);var t=(0,s["default"])();Object.defineProperties(this,{demo:{value:!1,enumerable:!0},localStoragePreferences:{value:a.localStoragePreferences,enumerable:!0},reactor:{value:t,enumerable:!0},util:{value:d["default"],enumerable:!0},startLocalStoragePreferencesSync:{value:a.localStoragePreferences.startSync.bind(a.localStoragePreferences,t)},startUrlSync:{value:j.urlSync.startSync.bind(null,t)},stopUrlSync:{value:j.urlSync.stopSync.bind(null,t)}}),(0,l["default"])(this,t,{auth:p,config:v,entity:m,entityHistory:b,errorLog:w,event:M,logbook:I,moreInfo:D,navigation:j,notification:P,service:L,stream:R,sync:x,voice:Y,restApi:G})};e["default"]=B},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(74),e["default"]=new o["default"]({is:"ha-badges-card",properties:{states:{type:Array}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(21),c=r(s);n(35),n(34),n(19);var l=u["default"].moreInfoActions;e["default"]=new o["default"]({is:"ha-domain-card",properties:{domain:{type:String},states:{type:Array},groupEntity:{type:Object}},computeDomainTitle:function(t){return t.replace(/_/g," ")},entityTapped:function(t){if(!t.target.classList.contains("paper-toggle-button")&&!t.target.classList.contains("paper-icon-button")){t.stopPropagation();var e=t.model.item.entityId;this.async(function(){return l.selectEntity(e)},1)}},showGroupToggle:function(t,e){return!t||!e||"on"!==t.state&&"off"!==t.state?!1:e.reduce(function(t,e){return t+(0,c["default"])(e.entityId)},0)>1}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(35),e["default"]=new o["default"]({is:"ha-introduction-card",properties:{showInstallInstruction:{type:Boolean,value:!1},showHideInstruction:{type:Boolean,value:!0}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(40),u=r(a);e["default"]=new o["default"]({is:"display-time",properties:{dateObj:{type:Object}},computeTime:function(t){return t?(0,u["default"])(t):""}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].entityGetters;e["default"]=new u["default"]({is:"entity-list",behaviors:[c["default"]],properties:{entities:{type:Array,bindNuclear:[l.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.entityId}).toArray()}]}},entitySelected:function(t){t.preventDefault(),this.fire("entity-selected",{entityId:t.model.entity.entityId})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a);n(17);var s=u["default"].reactor,c=u["default"].entityGetters,l=u["default"].moreInfoActions;e["default"]=new o["default"]({is:"ha-entity-marker",properties:{entityId:{type:String,value:""},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:{click:"badgeTap"},badgeTap:function(t){var e=this;t.stopPropagation(),this.entityId&&this.async(function(){return l.selectEntity(e.entityId)},1)},computeState:function(t){return t&&s.evaluate(c.byId(t))},computeIcon:function(t){return!t&&"home"},computeImage:function(t){return t&&t.attributes.entity_picture},computeValue:function(t){return t&&t.entityDisplay.split(" ").map(function(t){return t.substr(0,1)}).join("")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(121),u=r(a);e["default"]=new o["default"]({is:"ha-state-icon",properties:{stateObj:{type:Object}},computeIcon:function(t){return(0,u["default"])(t)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(22),c=r(s),l=n(21),f=r(l);n(17);var d=u["default"].moreInfoActions,h=u["default"].serviceActions;e["default"]=new o["default"]({is:"ha-state-label-badge",properties:{state:{type:Object,observer:"stateChanged"}},listeners:{click:"badgeTap"},badgeTap:function(t){var e=this;return t.stopPropagation(),(0,f["default"])(this.state.entityId)?void("scene"===this.state.domain?h.callTurnOn(this.state.entityId):"off"===this.state.state?h.callTurnOn(this.state.entityId):h.callTurnOff(this.state.entityId)):void this.async(function(){return d.selectEntity(e.state.entityId)},1)},computeClasses:function(t){switch(t.domain){case"scene":return"green";case"binary_sensor":case"script":return"on"===t.state?"blue":"grey";case"updater":return"blue";default:return""}},computeValue:function(t){switch(t.domain){case"binary_sensor":case"device_tracker":case"updater":case"sun":case"scene":case"script":case"alarm_control_panel":return;case"sensor":return t.state;default:return t.state}},computeIcon:function(t){switch(t.domain){case"alarm_control_panel":return"pending"===t.state?"mdi:clock-fast":"armed_away"===t.state?"mdi:nature":"armed_home"===t.state?"mdi:home-variant":(0,c["default"])(t.domain,t.state);case"binary_sensor":case"device_tracker":case"scene":case"updater":case"script":return(0,c["default"])(t.domain,t.state);case"sun":return"above_horizon"===t.state?(0,c["default"])(t.domain):"mdi:brightness-3";default:return}},computeImage:function(t){return t.attributes.entity_picture},computeLabel:function(t){switch(t.domain){case"scene":case"script":return t.domain;case"device_tracker":return"not_home"===t.state?"Away":t.state;case"alarm_control_panel":return"pending"===t.state?"pend":"armed_away"===t.state||"armed_home"===t.state?"armed":"disarm";default:return t.attributes.unit_of_measurement}},computeDescription:function(t){return t.entityDisplay},stateChanged:function(){this.updateStyles()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(73),e["default"]=new o["default"]({is:"state-badge",properties:{stateObj:{type:Object,observer:"updateIconColor"}},updateIconColor:function(t){"light"===t.domain&&"on"===t.state&&t.attributes.rgb_color&&t.attributes.rgb_color.reduce(function(t,e){return t+e},0)<730?this.$.icon.style.color="rgb("+t.attributes.rgb_color.join(",")+")":this.$.icon.style.color=null}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].eventGetters;e["default"]=new u["default"]({is:"events-list",behaviors:[c["default"]],properties:{events:{type:Array,bindNuclear:[l.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.event}).toArray()}]}},eventSelected:function(t){t.preventDefault(),this.fire("event-selected",{eventType:t.model.event.event})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"ha-color-picker",properties:{color:{type:Object},width:{type:Number},height:{type:Number}},listeners:{mousedown:"onMouseDown",mouseup:"onMouseUp",touchstart:"onTouchStart",touchend:"onTouchEnd"},onMouseDown:function(t){this.onMouseMove(t),this.addEventListener("mousemove",this.onMouseMove)},onMouseUp:function(){this.removeEventListener("mousemove",this.onMouseMove)},onTouchStart:function(t){this.onTouchMove(t),this.addEventListener("touchmove",this.onTouchMove)},onTouchEnd:function(){this.removeEventListener("touchmove",this.onTouchMove)},onTouchMove:function(t){var e=this;this.mouseMoveIsThrottled&&(this.mouseMoveIsThrottled=!1,this.processColorSelect(t.touches[0]),this.async(function(){return e.mouseMoveIsThrottled=!0},100))},onMouseMove:function(t){var e=this;this.mouseMoveIsThrottled&&(this.mouseMoveIsThrottled=!1,this.processColorSelect(t),this.async(function(){return e.mouseMoveIsThrottled=!0},100))},processColorSelect:function(t){var e=this.canvas.getBoundingClientRect();t.clientX=e.left+e.width||t.clientY=e.top+e.height||this.onColorSelect(t.clientX-e.left,t.clientY-e.top)},onColorSelect:function(t,e){var n=this.context.getImageData(t,e,1,1).data;this.setColor({r:n[0],g:n[1],b:n[2]})},setColor:function(t){this.color=t,this.fire("colorselected",{rgb:this.color})},ready:function(){var t=this;this.setColor=this.setColor.bind(this),this.mouseMoveIsThrottled=!0,this.canvas=this.children[0],this.context=this.canvas.getContext("2d"),this.debounce("drawGradient",function(){var e=getComputedStyle(t),n=parseInt(e.width,10),r=parseInt(e.height,10);t.width=n,t.height=r;var i=t.context.createLinearGradient(0,0,n,0);i.addColorStop(0,"rgb(255,0,0)"),i.addColorStop(.16,"rgb(255,0,255)"),i.addColorStop(.32,"rgb(0,0,255)"),i.addColorStop(.48,"rgb(0,255,255)"),i.addColorStop(.64,"rgb(0,255,0)"),i.addColorStop(.8,"rgb(255,255,0)"),i.addColorStop(1,"rgb(255,0,0)"),t.context.fillStyle=i,t.context.fillRect(0,0,n,r);var o=t.context.createLinearGradient(0,0,0,r);o.addColorStop(0,"rgba(255,255,255,1)"),o.addColorStop(.5,"rgba(255,255,255,0)"),o.addColorStop(.5,"rgba(0,0,0,0)"),o.addColorStop(1,"rgba(0,0,0,1)"),t.context.fillStyle=o,t.context.fillRect(0,0,n,r)},100)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(17),e["default"]=new o["default"]({is:"ha-demo-badge"})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(82),e["default"]=new o["default"]({is:"ha-logbook",properties:{entries:{type:Object,value:[]}},noEntries:function(t){return!t.length}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(86);var l=o["default"].configGetters,f=o["default"].navigationGetters,d=o["default"].authActions,h=o["default"].navigationActions;e["default"]=new u["default"]({is:"ha-sidebar",behaviors:[c["default"]],properties:{menuShown:{type:Boolean},menuSelected:{type:String},selected:{type:String,bindNuclear:f.activePane,observer:"selectedChanged"},hasHistoryComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("history")},hasLogbookComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("logbook")}},selectedChanged:function(t){for(var e=this.querySelectorAll(".menu [data-panel]"),n=0;nd;d++)f._columns[d]=[];var h=0;return n&&a(),s.keySeq().sortBy(function(t){return i(t)}).forEach(function(t){if("a"===t)return void(f._demo=!0);var n=i(t);n>=0&&10>n?f._badges.push.apply(f._badges,r(s.get(t)).sortBy(o).toArray()):"group"===t?s.get(t).filter(function(t){return!t.attributes.auto}).sortBy(o).forEach(function(t){var n=l.expandGroup(t,e);n.forEach(function(t){return c[t.entityId]=!0}),u(t.entityDisplay,n.toArray(),t)}):u(t,r(s.get(t)).sortBy(o).toArray())}),f},computeShouldRenderColumn:function(t,e){return 0===t||e.length},computeShowIntroduction:function(t,e,n){return 0===t&&(e||n._demo)},computeShowHideInstruction:function(t,e){return t.size>0&&!0&&!e._demo},computeGroupEntityOfCard:function(t,e){return e in t&&t[e].groupEntity},computeStatesOfCard:function(t,e){return e in t&&t[e].entities}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(33),n(70),n(36);var s=o["default"].moreInfoActions;e["default"]=new u["default"]({is:"logbook-entry",entityClicked:function(t){t.preventDefault(),s.selectEntity(this.entryObj.entityId)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(33);var l=o["default"].serviceGetters;e["default"]=new u["default"]({is:"services-list",behaviors:[c["default"]],properties:{serviceDomains:{type:Array,bindNuclear:l.entityMap}},computeDomains:function(t){return t.valueSeq().map(function(t){return t.domain}).sort().toJS()},computeServices:function(t,e){return t.get(e).get("services").keySeq().toArray()},serviceClicked:function(t){t.preventDefault(),this.fire("service-selected",{domain:t.model.domain,service:t.model.service})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){var e=parseFloat(t);return!isNaN(e)&&isFinite(e)?e:null}Object.defineProperty(e,"__esModule",{value:!0});var o=n(199),a=r(o),u=n(1),s=r(u);e["default"]=new s["default"]({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 t=this.unit,e=this.data;if(0!==e.length){var n={legend:{position:"top"},interpolateNulls:!0,titlePosition:"none",vAxes:{0:{title:t}},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 r=new Date(Math.min.apply(null,e.map(function(t){return t[0].lastChangedAsDate}))),o=new Date(r);o.setDate(o.getDate()+1),o>new Date&&(o=new Date);var u=e.map(function(t){function e(t,e){c&&e&&s.push([t[0]].concat(c.slice(1).map(function(t,n){return e[n]?t:null}))),s.push(t),c=t}var n=t[t.length-1],r=n.domain,a=n.entityDisplay,u=new window.google.visualization.DataTable;u.addColumn({type:"datetime",id:"Time"});var s=[],c=void 0;if("thermostat"===r){var l=t.reduce(function(t,e){return t||e.attributes.target_temp_high!==e.attributes.target_temp_low},!1);u.addColumn("number",a+" current temperature");var f=void 0;l?!function(){u.addColumn("number",a+" target temperature high"),u.addColumn("number",a+" target temperature low");var t=[!1,!0,!0];f=function(n){var r=i(n.attributes.current_temperature),o=i(n.attributes.target_temp_high),a=i(n.attributes.target_temp_low);e([n.lastChangedAsDate,r,o,a],t)}}():!function(){u.addColumn("number",a+" target temperature");var t=[!1,!0];f=function(n){var r=i(n.attributes.current_temperature),o=i(n.attributes.temperature);e([n.lastChangedAsDate,r,o],t)}}(),t.forEach(f)}else!function(){u.addColumn("number",a);var n="sensor"!==r&&[!0];t.forEach(function(t){var r=i(t.state);e([t.lastChangedAsDate,r],n)})}();return e([o].concat(c.slice(1)),!1),u.addRows(s),u}),s=void 0;s=1===u.length?u[0]:u.slice(1).reduce(function(t,e){return window.google.visualization.data.join(t,e,"full",[[0,0]],(0,a["default"])(1,t.getNumberOfColumns()),(0,a["default"])(1,e.getNumberOfColumns()))},u[0]),this.chartEngine.draw(s,n)}}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({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 t(t,e,n,r){var o=e.replace(/_/g," ");i.addRow([t,o,n,r])}if(this.isAttached){for(var e=o["default"].dom(this),n=this.data;e.node.lastChild;)e.node.removeChild(e.node.lastChild);if(n&&0!==n.length){var r=new window.google.visualization.Timeline(this),i=new window.google.visualization.DataTable;i.addColumn({type:"string",id:"Entity"}),i.addColumn({type:"string",id:"State"}),i.addColumn({type:"date",id:"Start"}),i.addColumn({type:"date",id:"End"});var a=new Date(n.reduce(function(t,e){return Math.min(t,e[0].lastChangedAsDate)},new Date)),u=new Date(a);u.setDate(u.getDate()+1),u>new Date&&(u=new Date);var s=0;n.forEach(function(e){if(0!==e.length){var n=e[0].entityDisplay,r=void 0,i=null,o=null;e.forEach(function(e){null!==i&&e.state!==i?(r=e.lastChangedAsDate,t(n,i,o,r),i=e.state,o=r):null===i&&(i=e.state,o=e.lastChangedAsDate)}),t(n,i,o,u),s++}}),r.draw(i,{height:55+42*s,timeline:{showRowLabels:n.length>1},hAxis:{format:"H:mm"}})}}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].streamGetters,f=o["default"].streamActions;e["default"]=new u["default"]({is:"stream-status",behaviors:[c["default"]],properties:{isStreaming:{type:Boolean,bindNuclear:l.isStreamingEvents},hasError:{type:Boolean,bindNuclear:l.hasStreamingEventsError}},toggleChanged:function(){this.isStreaming?f.stop():f.start()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].voiceActions,f=o["default"].voiceGetters;e["default"]=new u["default"]({is:"ha-voice-command-dialog",behaviors:[c["default"]],properties:{dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},finalTranscript:{type:String,bindNuclear:f.finalTranscript},interimTranscript:{type:String,bindNuclear:f.extraInterimTranscript},isTransmitting:{type:Boolean,bindNuclear:f.isTransmitting},isListening:{type:Boolean,bindNuclear:f.isListening},showListenInterface:{type:Boolean,computed:"computeShowListenInterface(isListening, isTransmitting)",observer:"showListenInterfaceChanged"},_boundOnBackdropTap:{type:Function,value:function(){return this._onBackdropTap.bind(this)}}},computeShowListenInterface:function(t,e){return t||e},dialogOpenChanged:function(t){t?this.$.dialog.backdropElement.addEventListener("click",this._boundOnBackdropTap):!t&&this.isListening&&l.stop()},showListenInterfaceChanged:function(t){!t&&this.dialogOpen?this.dialogOpen=!1:t&&(this.dialogOpen=!0)},_onBackdropTap:function(){this.$.dialog.backdropElement.removeEventListener("click",this._boundOnBackdropTap),this.isListening&&l.stop()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(19),n(37),n(104);var l=o["default"].configGetters,f=o["default"].entityHistoryGetters,d=o["default"].entityHistoryActions,h=o["default"].moreInfoGetters,p=o["default"].moreInfoActions,_=["camera","configurator","scene"];e["default"]=new u["default"]({is:"more-info-dialog",behaviors:[c["default"]],properties:{stateObj:{type:Object,bindNuclear:h.currentEntity,observer:"stateObjChanged"},stateHistory:{type:Object,bindNuclear:[h.currentEntityHistory,function(t){return t?[t]:!1}]},isLoadingHistoryData:{type:Boolean,computed:"computeIsLoadingHistoryData(_delayedDialogOpen, _isLoadingHistoryData)"},_isLoadingHistoryData:{type:Boolean,bindNuclear:f.isLoadingEntityHistory},hasHistoryComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("history"),observer:"fetchHistoryData"},shouldFetchHistory:{type:Boolean,bindNuclear:h.isCurrentEntityHistoryStale,observer:"fetchHistoryData"},showHistoryComponent:{type:Boolean,value:!1},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},_delayedDialogOpen:{ +type:Boolean,value:!1}},computeIsLoadingHistoryData:function(t,e){return!t||e},fetchHistoryData:function(){this.stateObj&&this.hasHistoryComponent&&this.shouldFetchHistory&&d.fetchRecent(this.stateObj.entityId)},stateObjChanged:function(t){var e=this;return t?(this.showHistoryComponent=this.hasHistoryComponent&&-1===_.indexOf(this.stateObj.domain),void this.async(function(){e.fetchHistoryData(),e.dialogOpen=!0},10)):void(this.dialogOpen=!1)},dialogOpenChanged:function(t){var e=this;t?this.async(function(){return e._delayedDialogOpen=!0},10):!t&&this.stateObj&&(p.deselectEntity(),this._delayedDialogOpen=!1)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(4),c=r(s),l=n(41),f=r(l);n(80),n(99),n(97),n(96),n(98),n(91),n(92),n(94),n(95),n(93),n(100),n(88),n(87);var d=u["default"].navigationActions,h=u["default"].navigationGetters,p=u["default"].startUrlSync,_=u["default"].stopUrlSync;e["default"]=new o["default"]({is:"home-assistant-main",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},activePane:{type:String,bindNuclear:h.activePane,observer:"activePaneChanged"},isSelectedStates:{type:Boolean,bindNuclear:h.isActivePane("states")},isSelectedHistory:{type:Boolean,bindNuclear:h.isActivePane("history")},isSelectedMap:{type:Boolean,bindNuclear:h.isActivePane("map")},isSelectedLogbook:{type:Boolean,bindNuclear:h.isActivePane("logbook")},isSelectedDevEvent:{type:Boolean,bindNuclear:h.isActivePane("devEvent")},isSelectedDevState:{type:Boolean,bindNuclear:h.isActivePane("devState")},isSelectedDevTemplate:{type:Boolean,bindNuclear:h.isActivePane("devTemplate")},isSelectedDevService:{type:Boolean,bindNuclear:h.isActivePane("devService")},isSelectedDevInfo:{type:Boolean,bindNuclear:h.isActivePane("devInfo")},showSidebar:{type:Boolean,bindNuclear:h.showSidebar}},listeners:{"open-menu":"openMenu","close-menu":"closeMenu"},openMenu:function(){this.narrow?this.$.drawer.openDrawer():d.showSidebar(!0)},closeMenu:function(){this.$.drawer.closeDrawer(),this.showSidebar&&d.showSidebar(!1)},activePaneChanged:function(){this.narrow&&this.$.drawer.closeDrawer()},attached:function(){(0,f["default"])(),p()},computeForceNarrow:function(t,e){return t||!e},detached:function(){_()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(4),c=r(s),l=n(42),f=r(l),d=n(41),h=r(d),p=u["default"].authGetters;e["default"]=new o["default"]({is:"login-form",behaviors:[c["default"]],properties:{errorMessage:{type:String,bindNuclear:p.attemptErrorMessage},isInvalid:{type:Boolean,bindNuclear:p.isInvalidAttempt},isValidating:{type:Boolean,observer:"isValidatingChanged",bindNuclear:p.isValidating},loadingResources:{type:Boolean,value:!1},forceShowLoading:{type:Boolean,value:!1},showLoading:{type:Boolean,computed:"computeShowSpinner(forceShowLoading, isValidating)"}},listeners:{keydown:"passwordKeyDown","loginButton.click":"validatePassword"},observers:["validatingChanged(isValidating, isInvalid)"],attached:function(){(0,h["default"])()},computeShowSpinner:function(t,e){return t||e},validatingChanged:function(t,e){t||e||(this.$.passwordInput.value="")},isValidatingChanged:function(t){var e=this;t||this.async(function(){return e.$.passwordInput.focus()},10)},passwordKeyDown:function(t){13===t.keyCode?(this.validatePassword(),t.preventDefault()):this.isInvalid&&(this.isInvalid=!1)},validatePassword:function(){this.$.hideKeyboardOnFocus.focus(),(0,f["default"])(this.$.passwordInput.value,this.$.rememberLogin.checked)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(83);var s=o["default"].reactor,c=o["default"].serviceActions,l=o["default"].serviceGetters;e["default"]=new u["default"]({is:"partial-dev-call-service",properties:{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(domain, service)"}},computeDescription:function(t,e){return s.evaluate([l.entityMap,function(n){return n.has(t)&&n.get(t).get("services").has(e)?JSON.stringify(n.get(t).get("services").get(e).toJS(),null,2):"No description available"}])},serviceSelected:function(t){this.domain=t.detail.domain,this.service=t.detail.service},callService:function(){var t=void 0;try{t=this.serviceData?JSON.parse(this.serviceData):{}}catch(e){return void alert("Error parsing JSON: "+e)}c.callService(this.domain,this.service,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(76);var s=o["default"].eventActions;e["default"]=new u["default"]({is:"partial-dev-fire-event",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(t){this.eventType=t.detail.eventType},fireEvent:function(){var t=void 0;try{t=this.eventData?JSON.parse(this.eventData):{}}catch(e){return void alert("Error parsing JSON: "+e)}s.fireEvent(this.eventType,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7);var l=o["default"].configGetters,f=o["default"].errorLogActions;e["default"]=new u["default"]({is:"partial-dev-info",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:l.serverVersion},polymerVersion:{type:String,value:u["default"].version},nuclearVersion:{type:String,value:"1.2.1"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(t){var e=this;t&&t.preventDefault(),this.errorLog="Loading error log…",f.fetchErrorLog().then(function(t){return e.errorLog=t||"No errors have been reported."})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(71);var s=o["default"].reactor,c=o["default"].entityGetters,l=o["default"].entityActions;e["default"]=new u["default"]({is:"partial-dev-set-state",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(t){var e=t?JSON.stringify(t,null," "):"";this.$.inputData.value=e,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(t){var e=s.evaluate(c.byId(t.detail.entityId));this.entityId=e.entityId,this.state=e.state,this.stateAttributes=JSON.stringify(e.attributes,null," ")},handleSetState:function(){var t=void 0;try{t=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(e){return void alert("Error parsing JSON: "+e)}l.save({entityId:this.entityId,state:this.state,attributes:t})},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7);var l=o["default"].templateActions;e["default"]=new u["default"]({is:"partial-dev-template",behaviors:[c["default"]],properties:{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(t){return"content fit layout "+(t?"vertical":"horizontal")},computeRenderedClasses:function(t){return t?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate,500)},renderTemplate:function(){var t=this;this.rendering=!0,l.render(this.template).then(function(e){t.processed=e,t.rendering=!1},function(e){t.processed=e.message,t.error=!0,t.rendering=!1})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(37);var l=o["default"].entityHistoryGetters,f=o["default"].entityHistoryActions;e["default"]=new u["default"]({is:"partial-history",behaviors:[c["default"]],properties:{narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},isDataLoaded:{type:Boolean,bindNuclear:l.hasDataForCurrentDate,observer:"isDataLoadedChanged"},stateHistory:{type:Object,bindNuclear:l.entityHistoryForCurrentDate},isLoadingData:{type:Boolean,bindNuclear:l.isLoadingEntityHistory},selectedDate:{type:String,value:null,bindNuclear:l.currentDate}},isDataLoadedChanged:function(t){t||this.async(function(){return f.fetchSelectedDate()},1)},handleRefreshClick:function(){f.fetchSelectedDate()},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:f.changeCurrentDate})},detached:function(){this.datePicker.destroy()},computeContentClasses:function(t){return"flex content "+(t?"narrow":"wide")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(79),n(18);var l=o["default"].logbookGetters,f=o["default"].logbookActions;e["default"]=new u["default"]({is:"partial-logbook",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},selectedDate:{type:String,bindNuclear:l.currentDate},isLoading:{type:Boolean,bindNuclear:l.isLoadingEntries},isStale:{type:Boolean,bindNuclear:l.isCurrentStale,observer:"isStaleChanged"},entries:{type:Array,bindNuclear:[l.currentEntries,function(t){return t.reverse().toArray()}]},datePicker:{type:Object}},isStaleChanged:function(t){var e=this;t&&this.async(function(){return f.fetchDate(e.selectedDate)},1)},handleRefresh:function(){f.fetchDate(this.selectedDate)},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:f.changeCurrentDate})},detached:function(){this.datePicker.destroy()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(72);var l=o["default"].configGetters,f=o["default"].entityGetters;window.L.Icon.Default.imagePath="/static/images/leaflet",e["default"]=new u["default"]({is:"partial-map",behaviors:[c["default"]],properties:{locationGPS:{type:Number,bindNuclear:l.locationGPS},locationName:{type:String,bindNuclear:l.locationName},locationEntities:{type:Array,bindNuclear:[f.visibleEntityMap,function(t){return t.valueSeq().filter(function(t){return t.attributes.latitude&&"home"!==t.state}).toArray()}]},zoneEntities:{type:Array,bindNuclear:[f.entityMap,function(t){return t.valueSeq().filter(function(t){return"zone"===t.domain}).toArray()}]},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1}},attached:function(){var t=this;window.L.Browser.mobileWebkit&&this.async(function(){var e=t.$.map,n=e.style.display;e.style.display="none",t.async(function(){e.style.display=n},1)},1)},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},toggleMenu:function(){this.fire("open-menu")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(81);var l=o["default"].configGetters,f=o["default"].entityGetters,d=o["default"].voiceGetters,h=o["default"].streamGetters,p=o["default"].syncGetters,_=o["default"].syncActions,v=o["default"].voiceActions;e["default"]=new u["default"]({is:"partial-zone",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},isFetching:{type:Boolean,bindNuclear:p.isFetching},isStreaming:{type:Boolean,bindNuclear:h.isStreamingEvents},canListen:{type:Boolean,bindNuclear:[d.isVoiceSupported,l.isComponentLoaded("conversation"),function(t,e){return t&&e}]},introductionLoaded:{type:Boolean,bindNuclear:l.isComponentLoaded("introduction")},locationName:{type:String,bindNuclear:l.locationName},showMenu:{type:Boolean,value:!1,observer:"windowChange"},states:{type:Object,bindNuclear:f.visibleEntityMap},columns:{type:Number,value:1}},created:function(){var t=this;this.windowChange=this.windowChange.bind(this);for(var e=[],n=0;5>n;n++)e.push(300+300*n);this.mqls=e.map(function(e){var n=window.matchMedia("(min-width: "+e+"px)");return n.addListener(t.windowChange),n})},detached:function(){var t=this;this.mqls.forEach(function(e){return e.removeListener(t.windowChange)})},windowChange:function(){var t=this.mqls.reduce(function(t,e){return t+e.matches},0);this.columns=Math.max(1,t-this.showMenu)},handleRefresh:function(){_.fetchAll()},handleListenClick:function(){v.listen()},computeDomains:function(t){return t.keySeq().toArray()},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},computeStatesOfDomain:function(t,e){return t.get(e).toArray()},computeRefreshButtonClass:function(t){return t?"ha-spin":void 0},computeShowIntroduction:function(t,e){return t||0===e.size},toggleMenu:function(){this.fire("open-menu")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].notificationGetters;e["default"]=new u["default"]({is:"notification-manager",behaviors:[c["default"]],properties:{text:{type:String,bindNuclear:l.lastNotificationMessage,observer:"showNotification"}},showNotification:function(t){t&&this.$.toast.show()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=o["default"].serviceActions;e["default"]=new u["default"]({is:"more-info-alarm_control_panel",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})},properties:{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(t,e){var n=new RegExp(e);return null===e?!0:n.test(t)},stateObjChanged:function(t){var e=this;t&&(this.codeFormat=t.attributes.code_format,this.codeInputVisible=null!==this.codeFormat,this.codeInputEnabled="armed_home"===t.state||"armed_away"===t.state||"disarmed"===t.state||"pending"===t.state||"triggered"===t.state,this.disarmButtonVisible="armed_home"===t.state||"armed_away"===t.state||"pending"===t.state||"triggered"===t.state,this.armHomeButtonVisible="disarmed"===t.state,this.armAwayButtonVisible="disarmed"===t.state),this.async(function(){return e.fire("iron-resize")},500)},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,s.callService("alarm_control_panel",t,n)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-camera",properties:{stateObj:{type:Object},dialogOpen:{type:Boolean}},imageLoaded:function(){this.fire("iron-resize")},computeCameraImageUrl:function(t){return t?"/api/camera_proxy_stream/"+this.stateObj.entityId:"data:image/gif;base64,R0lGODlhAQABAAAAACw="}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(18);var l=o["default"].streamGetters,f=o["default"].syncActions,d=o["default"].serviceActions;e["default"]=new u["default"]({is:"more-info-configurator",behaviors:[c["default"]],properties:{stateObj:{type:Object},action:{type:String,value:"display"},isStreaming:{type:Boolean,bindNuclear:l.isStreamingEvents},isConfigurable:{type:Boolean,computed:"computeIsConfigurable(stateObj)"},isConfiguring:{type:Boolean,value:!1},submitCaption:{type:String,computed:"computeSubmitCaption(stateObj)"},fieldInput:{type:Object,value:{}}},computeIsConfigurable:function(t){return"configure"===t.state},computeSubmitCaption:function(t){return t.attributes.submit_caption||"Set configuration"},fieldChanged:function(t){var e=t.target;this.fieldInput[e.id]=e.value},submitClicked:function(){var t=this;this.isConfiguring=!0;var e={configure_id:this.stateObj.attributes.configure_id,fields:this.fieldInput};d.callService("configurator","configure",e).then(function(){t.isConfiguring=!1,t.isStreaming||f.fetchAll()},function(){t.isConfiguring=!1})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(122),u=r(a);n(105),n(106),n(110),n(103),n(111),n(109),n(107),n(108),n(102),n(112),n(101),e["default"]=new o["default"]({is:"more-info-content",properties:{stateObj:{type:Object,observer:"stateObjChanged"},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"}},dialogOpenChanged:function(t){var e=o["default"].dom(this);e.lastChild&&(e.lastChild.dialogOpen=t)},stateObjChanged:function(t,e){var n=o["default"].dom(this);if(!t)return void(n.lastChild&&n.removeChild(n.lastChild));var r=(0,u["default"])(t);if(e&&(0,u["default"])(e)===r)n.lastChild.dialogOpen=this.dialogOpen,n.lastChild.stateObj=t;else{n.lastChild&&n.removeChild(n.lastChild);var i=document.createElement("more-info-"+r);i.stateObj=t,i.dialogOpen=this.dialogOpen,n.appendChild(i)}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=["entity_picture","friendly_name","icon","unit_of_measurement"];e["default"]=new o["default"]({is:"more-info-default",properties:{stateObj:{type:Object}},computeDisplayAttributes:function(t){return t?Object.keys(t.attributes).filter(function(t){return-1===a.indexOf(t)}):[]},getAttributeValue:function(t,e){return t.attributes[e]}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(19);var l=o["default"].entityGetters,f=o["default"].moreInfoGetters;e["default"]=new u["default"]({is:"more-info-group",behaviors:[c["default"]],properties:{stateObj:{type:Object},states:{type:Array,bindNuclear:[f.currentEntity,l.entityMap,function(t,e){return t?t.attributes.entity_id.map(e.get.bind(e)):[]}]}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){f.callService("light","turn_on",{entity_id:t,rgb_color:[e.r,e.g,e.b]})}Object.defineProperty(e,"__esModule",{value:!0});var o=n(2),a=r(o),u=n(1),s=r(u),c=n(20),l=r(c);n(77);var f=a["default"].serviceActions,d=["brightness","rgb_color","color_temp"];e["default"]=new s["default"]({is:"more-info-light",properties:{stateObj:{type:Object,observer:"stateObjChanged"},brightnessSliderValue:{type:Number,value:0},ctSliderValue:{type:Number,value:0}},stateObjChanged:function(t){var e=this;t&&"on"===t.state&&(this.brightnessSliderValue=t.attributes.brightness,this.ctSliderValue=t.attributes.color_temp),this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return(0,l["default"])(t,d)},brightnessSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||(0===e?f.callTurnOff(this.stateObj.entityId):f.callService("light","turn_on",{entity_id:this.stateObj.entityId,brightness:e}))},ctSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||f.callService("light","turn_on",{entity_id:this.stateObj.entityId,color_temp:e})},colorPicked:function(t){var e=this;return this.skipColorPicked?void(this.colorChanged=!0):(this.color=t.detail.rgb,i(this.stateObj.entityId,this.color),this.colorChanged=!1,this.skipColorPicked=!0,void(this.colorDebounce=setTimeout(function(){e.colorChanged&&i(e.stateObj.entityId,e.color),e.skipColorPicked=!1},500)))}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(20),c=r(s),l=o["default"].serviceActions,f=["volume_level"];e["default"]=new u["default"]({is:"more-info-media_player",properties:{stateObj:{type:Object,observer:"stateObjChanged"},isOff:{type:Boolean,value:!1},isPlaying:{type:Boolean,value:!1},isMuted:{type:Boolean,value:!1},volumeSliderValue:{type:Number,value:0},supportsPause:{type:Boolean,value:!1},supportsVolumeSet:{type:Boolean,value:!1},supportsVolumeMute:{type:Boolean,value:!1},supportsPreviousTrack:{type:Boolean,value:!1},supportsNextTrack:{type:Boolean,value:!1},supportsTurnOn:{type:Boolean,value:!1},supportsTurnOff:{type:Boolean,value:!1},supportsVolumeButtons:{type:Boolean,value:!1},hasMediaControl:{type:Boolean,value:!1}},stateObjChanged:function(t){var e=this;if(t){var n=["playing","paused","unknown"];this.isOff="off"===t.state,this.isPlaying="playing"===t.state,this.hasMediaControl=-1!==n.indexOf(t.state),this.volumeSliderValue=100*t.attributes.volume_level,this.isMuted=t.attributes.is_volume_muted,this.supportsPause=0!==(1&t.attributes.supported_media_commands),this.supportsVolumeSet=0!==(4&t.attributes.supported_media_commands),this.supportsVolumeMute=0!==(8&t.attributes.supported_media_commands),this.supportsPreviousTrack=0!==(16&t.attributes.supported_media_commands),this.supportsNextTrack=0!==(32&t.attributes.supported_media_commands),this.supportsTurnOn=0!==(128&t.attributes.supported_media_commands),this.supportsTurnOff=0!==(256&t.attributes.supported_media_commands),this.supportsVolumeButtons=0!==(1024&t.attributes.supported_media_commands)}this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return(0,c["default"])(t,f)},computeIsOff:function(t){return"off"===t.state},computeMuteVolumeIcon:function(t){return t?"mdi:volume-off":"mdi:volume-high"},computeHideVolumeButtons:function(t,e){return!e||t},computeShowPlaybackControls:function(t,e){return!t&&e},computePlaybackControlIcon:function(){return this.isPlaying?this.supportsPause?"mdi:pause":"mdi:stop":"mdi:play"},computeHidePowerButton:function(t,e,n){return t?!e:!n},handleTogglePower:function(){this.callService(this.isOff?"turn_on":"turn_off")},handlePrevious:function(){this.callService("media_previous_track")},handlePlaybackControl:function(){this.callService("media_play_pause")},handleNext:function(){this.callService("media_next_track")},handleVolumeTap:function(){this.supportsVolumeMute&&this.callService("volume_mute",{is_volume_muted:!this.isMuted})},handleVolumeUp:function(){var t=this.$.volumeUp;this.handleVolumeWorker("volume_up",t,!0)},handleVolumeDown:function(){var t=this.$.volumeDown;this.handleVolumeWorker("volume_down",t,!0)},handleVolumeWorker:function(t,e,n){var r=this;(n||void 0!==e&&e.pointerDown)&&(this.callService(t),this.async(function(){return r.handleVolumeWorker(t,e,!1)},500))},volumeSliderChanged:function(t){var e=parseFloat(t.target.value),n=e>0?e/100:0;this.callService("volume_set",{volume_level:n})},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,l.callService("media_player",t,n)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-script",properties:{stateObj:{type:Object}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(40),c=r(s),l=u["default"].util.parseDateTime;e["default"]=new o["default"]({is:"more-info-sun",properties:{stateObj:{type:Object},risingDate:{type:Object,computed:"computeRising(stateObj)"},settingDate:{type:Object,computed:"computeSetting(stateObj)"}},computeRising:function(t){return l(t.attributes.next_rising)},computeSetting:function(t){return l(t.attributes.next_setting)},computeOrder:function(t,e){return t>e?["set","ris"]:["ris","set"]},itemCaption:function(t){return"ris"===t?"Rising ":"Setting "},itemDate:function(t){return"ris"===t?this.risingDate:this.settingDate},itemValue:function(t){return(0,c["default"])(this.itemDate(t))}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(20),c=r(s),l=o["default"].serviceActions,f=["away_mode"];e["default"]=new u["default"]({is:"more-info-thermostat",properties:{stateObj:{type:Object,observer:"stateObjChanged"},tempMin:{type:Number},tempMax:{type:Number},targetTemperatureSliderValue:{type:Number},awayToggleChecked:{type:Boolean}},stateObjChanged:function(t){this.targetTemperatureSliderValue=t.attributes.temperature,this.awayToggleChecked="on"===t.attributes.away_mode,this.tempMin=t.attributes.min_temp,this.tempMax=t.attributes.max_temp},computeClassNames:function(t){return(0,c["default"])(t,f)},targetTemperatureSliderChanged:function(t){l.callService("thermostat","set_temperature",{entity_id:this.stateObj.entityId,temperature:t.target.value})},toggleChanged:function(t){var e=t.target.checked;e&&"off"===this.stateObj.attributes.away_mode?this.service_set_away(!0):e||"on"!==this.stateObj.attributes.away_mode||this.service_set_away(!1)},service_set_away:function(t){var e=this;l.callService("thermostat","set_away_mode",{away_mode:t,entity_id:this.stateObj.entityId}).then(function(){return e.stateObjChanged(e.stateObj)})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-updater",properties:{}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),n(38),e["default"]=new o["default"]({is:"state-card-configurator",properties:{stateObj:{type:Object}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8);var a=["playing","paused"];e["default"]=new o["default"]({is:"state-card-media_player",properties:{stateObj:{type:Object},isPlaying:{type:Boolean,computed:"computeIsPlaying(stateObj)"}},computeIsPlaying:function(t){return-1!==a.indexOf(t.state)},computePrimaryText:function(t,e){return e?t.attributes.media_title:t.stateDisplay},computeSecondaryText:function(t){var e=void 0;return"music"===t.attributes.media_content_type?t.attributes.media_artist:"tvshow"===t.attributes.media_content_type?(e=t.attributes.media_series_title,t.attributes.media_season&&t.attributes.media_episode&&(e+=" S"+t.attributes.media_season+"E"+t.attributes.media_episode),e):t.attributes.app_name?t.attributes.app_name:""}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(8);var s=o["default"].serviceActions;e["default"]=new u["default"]({is:"state-card-rollershutter",properties:{stateObj:{type:Object}},computeIsFullyOpen:function(t){return 100===t.attributes.current_position},computeIsFullyClosed:function(t){return 0===t.attributes.current_position},onMoveUpTap:function(){s.callService("rollershutter","move_up",{entity_id:this.stateObj.entityId})},onMoveDownTap:function(){s.callService("rollershutter","move_down",{entity_id:this.stateObj.entityId})},onStopTap:function(){s.callService("rollershutter","stop",{entity_id:this.stateObj.entityId})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a);n(8);var s=u["default"].serviceActions;e["default"]=new o["default"]({is:"state-card-scene",properties:{stateObj:{type:Object}},activateScene:function(){s.callTurnOn(this.stateObj.entityId)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),e["default"]=new o["default"]({is:"state-card-thermostat",properties:{stateObj:{type:Object}},computeTargetTemperature:function(t){return t.attributes.temperature+" "+t.attributes.unit_of_measurement}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),n(34),e["default"]=new o["default"]({is:"state-card-toggle"})},function(t,e){"use strict";function n(t){return{attached:function(){var e=this;this.__unwatchFns=Object.keys(this.properties).reduce(function(n,r){if(!("bindNuclear"in e.properties[r]))return n;var i=e.properties[r].bindNuclear;if(!i)throw new Error("Undefined getter specified for key "+r);return e[r]=t.evaluate(i),n.concat(t.observe(i,function(t){e[r]=t}))},[])},detached:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return-1!==u.indexOf(t.domain)?t.domain:(0,a["default"])(t.entityId)?"toggle":"display"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(21),a=r(o),u=["thermostat","configurator","scene","media_player","rollershutter"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(!t)return a["default"];if(t.attributes.icon)return t.attributes.icon;var e=t.attributes.unit_of_measurement;return!e||"sensor"!==t.domain||e!==f.UNIT_TEMP_C&&e!==f.UNIT_TEMP_F?(0,s["default"])(t.domain,t.state):"mdi:thermometer"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(39),a=r(o),u=n(22),s=r(u),c=n(2),l=r(c),f=l["default"].util.temperatureUnits},function(t,e){"use strict";function n(t){return-1!==r.indexOf(t.domain)?t.domain:"default"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n;var r=["light","group","sun","configurator","thermostat","script","media_player","camera","updater","alarm_control_panel"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(187),i=n(15),o=function(t,e,n){var o=arguments.length<=3||void 0===arguments[3]?null:arguments[3],a=t.evaluate(i.getters.authInfo),u=a.host+"/api/"+n;return new r.Promise(function(t,n){var r=new XMLHttpRequest; +r.open(e,u,!0),r.setRequestHeader("X-HA-access",a.authToken),r.onload=function(){var e=void 0;try{e="application/json"===r.getResponseHeader("content-type")?JSON.parse(r.responseText):r.responseText}catch(i){e=r.responseText}r.status>199&&r.status<300?t(e):n(e)},r.onerror=function(){return n({})},o?r.send(JSON.stringify(o)):r.send()})};e["default"]=o},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2],r=n.useStreaming,i=void 0===r?t.evaluate(c.getters.isSupported):r,o=n.rememberAuth,a=void 0===o?!1:o,s=n.host,d=void 0===s?"":s;t.dispatch(u["default"].VALIDATING_AUTH_TOKEN,{authToken:e,host:d}),l.actions.fetchAll(t).then(function(){t.dispatch(u["default"].VALID_AUTH_TOKEN,{authToken:e,host:d,rememberAuth:a}),i?c.actions.start(t,{syncOnInitialConnect:!1}):l.actions.start(t,{skipInitialSync:!0})},function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],n=e.message,r=void 0===n?f:n;t.dispatch(u["default"].INVALID_AUTH_TOKEN,{errorMessage:r})})}function o(t){(0,s.callApi)(t,"POST","log_out"),t.dispatch(u["default"].LOG_OUT,{})}Object.defineProperty(e,"__esModule",{value:!0}),e.validate=i,e.logOut=o;var a=n(14),u=r(a),s=n(5),c=n(28),l=n(30),f="Unexpected result from API"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=e.isValidating=["authAttempt","isValidating"],r=(e.isInvalidAttempt=["authAttempt","isInvalid"],e.attemptErrorMessage=["authAttempt","errorMessage"],e.rememberAuth=["rememberAuth"],e.attemptAuthInfo=[["authAttempt","authToken"],["authAttempt","host"],function(t,e){return{authToken:t,host:e}}]),i=e.currentAuthToken=["authCurrent","authToken"],o=e.currentAuthInfo=[i,["authCurrent","host"],function(t,e){return{authToken:t,host:e}}];e.authToken=[n,["authAttempt","authToken"],["authCurrent","authToken"],function(t,e,n){return t?e:n}],e.authInfo=[n,r,o,function(t,e,n){return t?e:n}]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(null==t)throw new TypeError("Cannot destructure undefined")}function o(t,e){var n=e.authToken,r=e.host;return(0,s.toImmutable)({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function a(t,e){return i(e),f.getInitialState()}function u(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)})}Object.defineProperty(e,"__esModule",{value:!0});var s=n(3),c=n(14),l=r(c),f=new s.Store({getInitialState:function(){return(0,s.toImmutable)({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(l["default"].VALIDATING_AUTH_TOKEN,o),this.on(l["default"].VALID_AUTH_TOKEN,a),this.on(l["default"].INVALID_AUTH_TOKEN,u)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.authToken,r=e.host;return(0,a.toImmutable)({authToken:n,host:r})}function o(){return c.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(14),s=r(u),c=new a.Store({getInitialState:function(){return(0,a.toImmutable)({authToken:null,host:""})},initialize:function(){this.on(s["default"].VALID_AUTH_TOKEN,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=c},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.rememberAuth;return n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(3),a=n(14),u=r(a),s=new o.Store({getInitialState:function(){return!0},initialize:function(){this.on(u["default"].VALID_AUTH_TOKEN,i)}});e["default"]=s},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(c["default"].SERVER_CONFIG_LOADED,e)}function o(t){(0,u.callApi)(t,"GET","config").then(function(e){return i(t,e)})}function a(t,e){t.dispatch(c["default"].COMPONENT_LOADED,{component:e})}Object.defineProperty(e,"__esModule",{value:!0}),e.configLoaded=i,e.fetchAll=o,e.componentLoaded=a;var u=n(5),s=n(23),c=r(s)},function(t,e){"use strict";function n(t){return[["serverComponent"],function(e){return e.contains(t)}]}Object.defineProperty(e,"__esModule",{value:!0}),e.isComponentLoaded=n,e.locationGPS=[["serverConfig","latitude"],["serverConfig","longitude"],function(t,e){return{latitude:t,longitude:e}}],e.locationName=["serverConfig","location_name"],e.serverVersion=["serverConfig","serverVersion"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.component;return t.push(n)}function o(t,e){var n=e.components;return(0,u.toImmutable)(n)}function a(){return l.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var u=n(3),s=n(23),c=r(s),l=new u.Store({getInitialState:function(){return(0,u.toImmutable)([])},initialize:function(){this.on(c["default"].COMPONENT_LOADED,i),this.on(c["default"].SERVER_CONFIG_LOADED,o),this.on(c["default"].LOG_OUT,a)}});e["default"]=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.latitude,r=e.longitude,i=e.location_name,o=e.temperature_unit,u=e.time_zone,s=e.version;return(0,a.toImmutable)({latitude:n,longitude:r,location_name:i,temperature_unit:o,time_zone:u,serverVersion:s})}function o(){return c.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(23),s=r(u),c=new a.Store({getInitialState:function(){return(0,a.toImmutable)({latitude:null,longitude:null,location_name:"Home",temperature_unit:"°C",time_zone:"UTC",serverVersion:"unknown"})},initialize:function(){this.on(s["default"].SERVER_CONFIG_LOADED,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=c},function(t,e,n){"use strict";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){t.dispatch(f["default"].ENTITY_HISTORY_DATE_SELECTED,{date:e})}function a(t){var e=arguments.length<=1||void 0===arguments[1]?null:arguments[1];t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),(0,c.callApi)(t,"GET",n).then(function(e){return t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})},function(){return t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_ERROR,{})})}function u(t,e){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_START,{date:e}),(0,c.callApi)(t,"GET","history/period/"+e).then(function(n){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})},function(){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_ERROR,{})})}function s(t){var e=t.evaluate(h.currentDate);return u(t,e)}Object.defineProperty(e,"__esModule",{value:!0}),e.changeCurrentDate=o,e.fetchRecent=a,e.fetchDate=u,e.fetchSelectedDate=s;var c=n(5),l=n(11),f=i(l),d=n(43),h=r(d)},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.date;return(0,s["default"])(n)}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(31),s=r(u),c=n(11),l=r(c),f=new a.Store({getInitialState:function(){var t=new Date;return t.setDate(t.getDate()-1),(0,s["default"])(t)},initialize:function(){this.on(l["default"].ENTITY_HISTORY_DATE_SELECTED,i),this.on(l["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,(0,a.toImmutable)({})):t.withMutations(function(t){r.forEach(function(e){return t.setIn([n,e[0].entity_id],(0,a.toImmutable)(e.map(l["default"].fromJSON)))})})}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c=n(16),l=r(c),f=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(3),o=n(11),a=r(o),u=new i.Store({getInitialState:function(){return!1},initialize:function(){this.on(a["default"].ENTITY_HISTORY_FETCH_START,function(){return!0}),this.on(a["default"].ENTITY_HISTORY_FETCH_SUCCESS,function(){return!1}),this.on(a["default"].ENTITY_HISTORY_FETCH_ERROR,function(){return!1}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_START,function(){return!0}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,function(){return!1}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_ERROR,function(){return!1}),this.on(a["default"].LOG_OUT,function(){return!1})}});e["default"]=u},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.stateHistory;return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,(0,a.toImmutable)(e.map(l["default"].fromJSON)))})})}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c=n(16),l=r(c),f=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(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(c,r)})}function o(){return l.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c="ALL_ENTRY_FETCH",l=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(10),o=n(16),a=r(o),u=(0,i.createApiActions)(a["default"]);e["default"]=u},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e.visibleEntityMap=e.byId=e.entityMap=e.hasData=void 0;var i=n(10),o=n(16),a=r(o),u=(e.hasData=(0,i.createHasDataGetter)(a["default"]),e.entityMap=(0,i.createEntityMapGetter)(a["default"]));e.byId=(0,i.createByIdGetter)(a["default"]),e.visibleEntityMap=[u,function(t){return t.filter(function(t){return!t.attributes.hidden})}]},function(t,e,n){"use strict";function r(t){return(0,i.callApi)(t,"GET","error_log")}Object.defineProperty(e,"__esModule",{value:!0}),e.fetchErrorLog=r;var i=n(5)},function(t,e,n){"use strict";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}Object.defineProperty(e,"__esModule",{value:!0}),e.actions=void 0;var i=n(141),o=r(i);e.actions=o},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=n(10),a=n(27),u=n(45),s=r(u),c=(0,o.createApiActions)(s["default"]);c.fireEvent=function(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2];return(0,i.callApi)(t,"POST","events/"+e,n).then(function(){a.actions.createNotification(t,"Event "+e+" successful fired!")})},e["default"]=c},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e.byId=e.entityMap=e.hasData=void 0;var i=n(10),o=n(45),a=r(o);e.hasData=(0,i.createHasDataGetter)(a["default"]),e.entityMap=(0,i.createEntityMapGetter)(a["default"]),e.byId=(0,i.createByIdGetter)(a["default"])},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(s["default"].LOGBOOK_DATE_SELECTED,{date:e})}function o(t,e){t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_START,{date:e}),(0,a.callApi)(t,"GET","logbook/"+e).then(function(n){return t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})},function(){return t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_ERROR,{})})}Object.defineProperty(e,"__esModule",{value:!0}),e.changeCurrentDate=i,e.fetchDate=o;var a=n(5),u=n(12),s=r(u)},function(t,e,n){"use strict";function r(t){return!t||(new Date).getTime()-t>o}Object.defineProperty(e,"__esModule",{value:!0}),e.isLoadingEntries=e.currentEntries=e.isCurrentStale=e.currentDate=void 0;var i=n(3),o=6e4,a=e.currentDate=["currentLogbookDate"];e.isCurrentStale=[a,["logbookEntriesUpdated"],function(t,e){return r(e.get(t))}],e.currentEntries=[a,["logbookEntries"],function(t,e){return e.get(t)||(0,i.toImmutable)([])}],e.isLoadingEntries=["isLoadingLogbookEntries"]},function(t,e,n){"use strict";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){t.registerStores({currentLogbookDate:u["default"],isLoadingLogbookEntries:c["default"],logbookEntries:f["default"],logbookEntriesUpdated:h["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.getters=e.actions=void 0,e.register=o;var a=n(149),u=i(a),s=n(150),c=i(s),l=n(151),f=i(l),d=n(152),h=i(d),p=n(145),_=r(p),v=n(146),y=r(v);e.actions=_,e.getters=y},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(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 a(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)}var u=function(){function t(t,e){for(var n=0;nt;t+=2){var e=rt[t],n=rt[t+1];e(n),rt[t]=void 0,rt[t+1]=void 0}$=0}function y(){try{var t=n(203);return q=t.runOnLoop||t.runOnContext,d()}catch(e){return _()}}function m(){}function g(){return new TypeError("You cannot resolve a promise with itself")}function b(){return new TypeError("A promises callback cannot return that same promise.")}function S(t){try{return t.then}catch(e){return ut.error=e,ut}}function w(t,e,n,r){try{t.call(e,n,r)}catch(i){return i}}function O(t,e,n){Z(function(t){var r=!1,i=w(n,e,function(n){r||(r=!0,e!==n?I(t,n):D(t,n))},function(e){r||(r=!0,C(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&i&&(r=!0,C(t,i))},t)}function M(t,e){e._state===ot?D(t,e._result):e._state===at?C(t,e._result):j(e,void 0,function(e){I(t,e)},function(e){C(t,e)})}function T(t,e){if(e.constructor===t.constructor)M(t,e);else{var n=S(e);n===ut?C(t,ut.error):void 0===n?D(t,e):u(n)?O(t,e,n):D(t,e)}}function I(t,e){t===e?C(t,g()):a(e)?T(t,e):D(t,e)}function E(t){t._onerror&&t._onerror(t._result),A(t)}function D(t,e){t._state===it&&(t._result=e,t._state=ot,0!==t._subscribers.length&&Z(A,t))}function C(t,e){t._state===it&&(t._state=at,t._result=e,Z(E,t))}function j(t,e,n,r){var i=t._subscribers,o=i.length;t._onerror=null,i[o]=e,i[o+ot]=n,i[o+at]=r,0===o&&t._state&&Z(A,t)}function A(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,i,o=t._result,a=0;aa;a++)j(r.resolve(t[a]),void 0,e,n);return i}function H(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(m);return I(n,t),n}function Y(t){var e=this,n=new e(m);return C(n,t),n}function U(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function G(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function B(t){this._id=pt++,this._state=void 0,this._result=void 0,this._subscribers=[],m!==t&&(u(t)||U(),this instanceof B||G(),N(this,t))}function F(){var t;if("undefined"!=typeof i)t=i;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=_t)}var V;V=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var q,W,K,J=V,$=0,Z=({}.toString,function(t,e){rt[$]=t,rt[$+1]=e,$+=2,2===$&&(W?W(v):K())}),X="undefined"!=typeof window?window:void 0,Q=X||{},tt=Q.MutationObserver||Q.WebKitMutationObserver,et="undefined"!=typeof t&&"[object process]"==={}.toString.call(t),nt="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,rt=new Array(1e3);K=et?f():tt?h():nt?p():void 0===X?y():_();var it=void 0,ot=1,at=2,ut=new P,st=new P;R.prototype._validateInput=function(t){return J(t)},R.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},R.prototype._init=function(){this._result=new Array(this.length)};var ct=R;R.prototype._enumerate=function(){for(var t=this,e=t.length,n=t.promise,r=t._input,i=0;n._state===it&&e>i;i++)t._eachEntry(r[i],i)},R.prototype._eachEntry=function(t,e){var n=this,r=n._instanceConstructor;s(t)?t.constructor===r&&t._state!==it?(t._onerror=null,n._settledAt(t._state,e,t._result)):n._willSettleAt(r.resolve(t),e):(n._remaining--,n._result[e]=t)},R.prototype._settledAt=function(t,e,n){var r=this,i=r.promise;i._state===it&&(r._remaining--,t===at?C(i,n):r._result[e]=n),0===r._remaining&&D(i,r._result)},R.prototype._willSettleAt=function(t,e){var n=this;j(t,void 0,function(t){n._settledAt(ot,e,t)},function(t){n._settledAt(at,e,t)})};var lt=z,ft=x,dt=H,ht=Y,pt=0,_t=B;B.all=lt,B.race=ft,B.resolve=dt,B.reject=ht,B._setScheduler=c,B._setAsap=l,B._asap=Z,B.prototype={constructor:B,then:function(t,e){var n=this,r=n._state;if(r===ot&&!t||r===at&&!e)return this;var i=new this.constructor(m),o=n._result;if(r){var a=arguments[r-1];Z(function(){L(r,i,a,o)})}else j(n,i,t,e);return i},"catch":function(t){return this.then(null,t)}};var vt=F,yt={Promise:_t,polyfill:vt};n(201).amd?(r=function(){return yt}.call(e,n,e,o),!(void 0!==r&&(o.exports=r))):"undefined"!=typeof o&&o.exports?o.exports=yt:"undefined"!=typeof this&&(this.ES6Promise=yt),vt()}).call(this)}).call(e,n(202),function(){return this}(),n(65)(t))},function(t,e,n){var r=n(60),i=r(Date,"now"),o=i||function(){return(new Date).getTime()};t.exports=o},function(t,e){function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e,n){var r=n(60),i=n(189),o=n(61),a="[object Array]",u=Object.prototype,s=u.toString,c=r(Array,"isArray"),l=c||function(t){return o(t)&&i(t.length)&&s.call(t)==a};t.exports=l},function(t,e,n){function r(t){return null==t?!1:i(t)?l.test(s.call(t)):o(t)&&a.test(t)}var i=n(62),o=n(61),a=/^\[object .+?Constructor\]$/,u=Object.prototype,s=Function.prototype.toString,c=u.hasOwnProperty,l=RegExp("^"+s.call(c).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=r},function(t,e){function n(t){return function(e){return null==e?void 0:e[t]}}t.exports=n},function(t,e,n){var r=n(192),i=r("length");t.exports=i},function(t,e,n){function r(t){return null!=t&&o(i(t))}var i=n(193),o=n(197);t.exports=r},function(t,e){function n(t,e){return t="number"==typeof t||r.test(t)?+t:-1,e=null==e?i:e,t>-1&&t%1==0&&e>t}var r=/^\d+$/,i=9007199254740991;t.exports=n},function(t,e,n){function r(t,e,n){if(!a(n))return!1;var r=typeof e;if("number"==r?i(n)&&o(e,n.length):"string"==r&&e in n){var u=n[e];return t===t?t===u:u!==u}return!1}var i=n(194),o=n(195),a=n(198);t.exports=r},function(t,e){function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e){function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){function r(t,e,n){n&&i(t,e,n)&&(e=n=void 0),t=+t||0,n=null==n?1:+n||0,null==e?(e=t,t=0):e=+e||0;for(var r=-1,u=a(o((e-t)/(n||1)),0),s=Array(u);++r1)for(var n=1;n \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 78c348cb7b0..7def0c85efb 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 78c348cb7b0a60ba015e3b652e538155d3e94a11 +Subproject commit 7def0c85efbfe7a11a64560c21cb83059a5c7a3b From e6846e7eb9fc63b4713de567e5dd0a0e80a24e8c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 22:28:53 -0800 Subject: [PATCH 121/129] Convert asuswrt user/pass to strings --- homeassistant/components/device_tracker/asuswrt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index b90e1ee4448..472440d7307 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -58,8 +58,8 @@ class AsusWrtDeviceScanner(object): def __init__(self, config): self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + self.username = str(config[CONF_USERNAME]) + self.password = str(config[CONF_PASSWORD]) self.lock = threading.Lock() From 058dba50cc51ce2b77621d0a22158e7257aaaca2 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Wed, 13 Jan 2016 08:46:45 +0100 Subject: [PATCH 122/129] Correct name using format instead of concatenation --- homeassistant/components/sensor/netatmo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 01d837c737d..6c29c41f3f5 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -91,8 +91,8 @@ class NetAtmoSensor(Entity): """ Implements a NetAtmo sensor. """ def __init__(self, netatmo_data, module_name, sensor_type): - self.client_name = 'NetAtmo' - self._name = module_name + '_' + SENSOR_TYPES[sensor_type][0] + self._name = "NetAtmo {} {}".format(module_name, + SENSOR_TYPES[sensor_type][0]) self.netatmo_data = netatmo_data self.module_name = module_name self.type = sensor_type @@ -102,7 +102,7 @@ class NetAtmoSensor(Entity): @property def name(self): - return '{} {}'.format(self.client_name, self._name) + return self._name @property def state(self): From 2a377a6125753e649308f070eccab6e8a2a693a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 23:59:15 -0800 Subject: [PATCH 123/129] Refactor syslog component for Windows users --- homeassistant/components/notify/syslog.py | 82 ++++++++++++----------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 4ee9ead9152..56075a6dd09 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -7,59 +7,62 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.syslog/ """ import logging -import syslog from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) _LOGGER = logging.getLogger(__name__) -FACILITIES = {'kernel': syslog.LOG_KERN, - 'user': syslog.LOG_USER, - 'mail': syslog.LOG_MAIL, - 'daemon': syslog.LOG_DAEMON, - 'auth': syslog.LOG_KERN, - 'LPR': syslog.LOG_LPR, - 'news': syslog.LOG_NEWS, - 'uucp': syslog.LOG_UUCP, - 'cron': syslog.LOG_CRON, - 'syslog': syslog.LOG_SYSLOG, - 'local0': syslog.LOG_LOCAL0, - 'local1': syslog.LOG_LOCAL1, - 'local2': syslog.LOG_LOCAL2, - 'local3': syslog.LOG_LOCAL3, - 'local4': syslog.LOG_LOCAL4, - 'local5': syslog.LOG_LOCAL5, - 'local6': syslog.LOG_LOCAL6, - 'local7': syslog.LOG_LOCAL7} - -OPTIONS = {'pid': syslog.LOG_PID, - 'cons': syslog.LOG_CONS, - 'ndelay': syslog.LOG_NDELAY, - 'nowait': syslog.LOG_NOWAIT, - 'perror': syslog.LOG_PERROR} - -PRIORITIES = {5: syslog.LOG_EMERG, - 4: syslog.LOG_ALERT, - 3: syslog.LOG_CRIT, - 2: syslog.LOG_ERR, - 1: syslog.LOG_WARNING, - 0: syslog.LOG_NOTICE, - -1: syslog.LOG_INFO, - -2: syslog.LOG_DEBUG} def get_service(hass, config): - """ Get the mail notification service. """ - + """Get the syslog notification service.""" if not validate_config({DOMAIN: config}, {DOMAIN: ['facility', 'option', 'priority']}, _LOGGER): return None - _facility = FACILITIES.get(config['facility'], 40) - _option = OPTIONS.get(config['option'], 10) - _priority = PRIORITIES.get(config['priority'], -1) + import syslog + + _facility = { + 'kernel': syslog.LOG_KERN, + 'user': syslog.LOG_USER, + 'mail': syslog.LOG_MAIL, + 'daemon': syslog.LOG_DAEMON, + 'auth': syslog.LOG_KERN, + 'LPR': syslog.LOG_LPR, + 'news': syslog.LOG_NEWS, + 'uucp': syslog.LOG_UUCP, + 'cron': syslog.LOG_CRON, + 'syslog': syslog.LOG_SYSLOG, + 'local0': syslog.LOG_LOCAL0, + 'local1': syslog.LOG_LOCAL1, + 'local2': syslog.LOG_LOCAL2, + 'local3': syslog.LOG_LOCAL3, + 'local4': syslog.LOG_LOCAL4, + 'local5': syslog.LOG_LOCAL5, + 'local6': syslog.LOG_LOCAL6, + 'local7': syslog.LOG_LOCAL7, + }.get(config['facility'], 40) + + _option = { + 'pid': syslog.LOG_PID, + 'cons': syslog.LOG_CONS, + 'ndelay': syslog.LOG_NDELAY, + 'nowait': syslog.LOG_NOWAIT, + 'perror': syslog.LOG_PERROR + }.get(config['option'], 10) + + _priority = { + 5: syslog.LOG_EMERG, + 4: syslog.LOG_ALERT, + 3: syslog.LOG_CRIT, + 2: syslog.LOG_ERR, + 1: syslog.LOG_WARNING, + 0: syslog.LOG_NOTICE, + -1: syslog.LOG_INFO, + -2: syslog.LOG_DEBUG + }.get(config['priority'], -1) return SyslogNotificationService(_facility, _option, _priority) @@ -76,6 +79,7 @@ class SyslogNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """ Send a message to a user. """ + import syslog title = kwargs.get(ATTR_TITLE) From 58cee75c0ebd0b3adceb897daae4a887fe5712aa Mon Sep 17 00:00:00 2001 From: hydreliox Date: Wed, 13 Jan 2016 09:06:16 +0100 Subject: [PATCH 124/129] coverage and requirements updated --- .coveragerc | 1 + requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 272ace975c4..674516aaeba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -96,6 +96,7 @@ omit = homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py + homeassistant/components/sensor/netatmo.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/rpi_gpio.py diff --git a/requirements_all.txt b/requirements_all.txt index 1bc00b4c46d..289c1095258 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -137,6 +137,9 @@ eliqonline==1.0.11 # homeassistant.components.sensor.forecast python-forecastio==1.3.3 +# homeassistant.components.sensor.netatmo +https://github.com/HydrelioxGitHub/netatmo-api-python/archive/59d157d03db0aa167730044667591adea4457ca8.zip#lnetatmo==0.3.0.dev1 + # homeassistant.components.sensor.openweathermap pyowm==2.3.0 From 314d34a644317dd55175e1ed697f30c9d5d84f98 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Thu, 14 Jan 2016 03:00:51 +0100 Subject: [PATCH 125/129] Update library lnetatmo requirements Thanks to @rmkraus --- homeassistant/components/sensor/netatmo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 6c29c41f3f5..640d3519bdf 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -17,8 +17,8 @@ from homeassistant.util import Throttle REQUIREMENTS = [ 'https://github.com/HydrelioxGitHub/netatmo-api-python/archive/' - '59d157d03db0aa167730044667591adea4457ca8.zip' - '#lnetatmo==0.3.0.dev1'] + '43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip' + '#lnetatmo==0.4.0'] _LOGGER = logging.getLogger(__name__) From 4fc01631398bacbc559c86387f69d941fef0f7a3 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 13 Jan 2016 21:22:56 -0500 Subject: [PATCH 126/129] round min / max values for temperature In order for the polymer thermostat component to have sensible step values the min / max values have to be round numbers. The current code only does that for systems running in degrees C. For those of us in silly land that still function in degrees F, this causes some oddities in the UI. Always round mix / max values to make it good no matter what fundamental units we are in. --- homeassistant/components/thermostat/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index edfa22a7840..7610070b1f0 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -224,12 +224,12 @@ class ThermostatDevice(Entity): @property def min_temp(self): """ Return minimum temperature. """ - return convert(7, TEMP_CELCIUS, self.unit_of_measurement) + return round(convert(7, TEMP_CELCIUS, self.unit_of_measurement)) @property def max_temp(self): """ Return maxmum temperature. """ - return convert(35, TEMP_CELCIUS, self.unit_of_measurement) + return round(convert(35, TEMP_CELCIUS, self.unit_of_measurement)) def _convert(self, temp, round_dec=None): """ Convert temperature from this thermost into user preferred From 4dd558a42084eb1316c90b308438743905a2db55 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Thu, 14 Jan 2016 07:09:25 +0100 Subject: [PATCH 127/129] Update Requirements --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 289c1095258..7b43b7a3f1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ eliqonline==1.0.11 python-forecastio==1.3.3 # homeassistant.components.sensor.netatmo -https://github.com/HydrelioxGitHub/netatmo-api-python/archive/59d157d03db0aa167730044667591adea4457ca8.zip#lnetatmo==0.3.0.dev1 +https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip#lnetatmo==0.4.0 # homeassistant.components.sensor.openweathermap pyowm==2.3.0 From bdd6bb7918e00b58c8d1641ed0b60cfb58e21cd3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jan 2016 23:51:29 -0800 Subject: [PATCH 128/129] Update frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 1517 ++++++++--------- .../www_static/home-assistant-polymer | 2 +- 3 files changed, 753 insertions(+), 768 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 67454d11974..bc2dc90bdf9 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "63d38b69fc6582e75f892abc140a893a" +VERSION = "fe71771b9b24b0fb72a56e775c3e1112" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 86e5daca0aa..da7c887cda3 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,4 +1,4 @@ -
\ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 7def0c85efb..0b99a5933c3 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 7def0c85efbfe7a11a64560c21cb83059a5c7a3b +Subproject commit 0b99a5933c35b88c3369e992426fff8bf450aa01 From f0af23a4f5ff7e80a06a1b6317b8180611e73581 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 14 Jan 2016 09:16:20 +0100 Subject: [PATCH 129/129] Add link to docs and update pressure unit --- homeassistant/components/sensor/netatmo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 640d3519bdf..d1830cd9811 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -1,10 +1,10 @@ """ homeassistant.components.sensor.netatmo -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ NetAtmo Weather Service service. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/... +https://home-assistant.io/components/sensor.netatmo/ """ import logging from datetime import timedelta @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'temperature': ['Temperature', TEMP_CELCIUS], 'co2': ['CO2', 'ppm'], - 'pressure': ['Pressure', 'mb'], + 'pressure': ['Pressure', 'mbar'], 'noise': ['Noise', 'dB'], 'humidity': ['Humidity', '%'] }