From 45fe37a301e4932f0ba2859e8263642b9b7bc5ed Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Wed, 4 Nov 2015 04:53:59 +0100 Subject: [PATCH 01/69] 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 02/69] 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 03/69] 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 04/69] 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 05/69] 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 06/69] 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 07/69] 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 08/69] 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 09/69] 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 69ed6fe6e7abaa07be3be4d57530a3be8d1c88bf Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Thu, 31 Dec 2015 05:48:23 +0100 Subject: [PATCH 10/69] 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 11/69] 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 8983a97c704fc17e9ff6ae4c8480407343845127 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 21:25:15 -0800 Subject: [PATCH 12/69] Fix calling turn_on for groups with mixed content --- homeassistant/components/__init__.py | 10 +++++++++- homeassistant/helpers/__init__.py | 2 +- tests/components/test_init.py | 27 +++++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index e0b008cab5e..10e18216ea0 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -87,13 +87,21 @@ def setup(hass, config): lambda item: util.split_entity_id(item)[0]) for domain, ent_ids in by_domain: + # We want to block for all calls and only return when all calls + # have been processed. If a service does not exist it causes a 10 + # second delay while we're blocking waiting for a response. + # But services can be registered on other HA instances that are + # listening to the bus too. So as a in between solution, we'll + # block only if the service is defined in the current HA instance. + blocking = hass.services.has_service(domain, service.service) + # Create a new dict for this call data = dict(service.data) # ent_ids is a generator, convert it to a list. data[ATTR_ENTITY_ID] = list(ent_ids) - hass.services.call(domain, service.service, data, True) + hass.services.call(domain, service.service, data, blocking) hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 021146d1c32..95dfe7dd65e 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -36,7 +36,7 @@ def extract_entity_ids(hass, service): service_ent_id = service.data[ATTR_ENTITY_ID] if isinstance(service_ent_id, str): - return group.expand_entity_ids(hass, [service_ent_id.lower()]) + return group.expand_entity_ids(hass, [service_ent_id]) return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)] diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 4ff334c1b1e..cb170a5c24b 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -6,20 +6,22 @@ Tests core compoments. """ # pylint: disable=protected-access,too-many-public-methods import unittest +from unittest.mock import patch import homeassistant.core as ha -import homeassistant.loader as loader from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components as comps +from tests.common import get_test_home_assistant + class TestComponentsCore(unittest.TestCase): """ Tests homeassistant.components module. """ def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ - self.hass = ha.HomeAssistant() + self.hass = get_test_home_assistant() self.assertTrue(comps.setup(self.hass, {})) self.hass.states.set('light.Bowl', STATE_ON) @@ -58,3 +60,24 @@ class TestComponentsCore(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(runs)) + + @patch('homeassistant.core.ServiceRegistry.call') + def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): + self.hass.services.register('light', SERVICE_TURN_ON, lambda x: x) + + # We can't test if our service call results in services being called + # because by mocking out the call service method, we mock out all + # So we mimick how the service registry calls services + service_call = ha.ServiceCall('homeassistant', 'turn_on', { + 'entity_id': ['light.test', 'sensor.bla', 'light.bla'] + }) + self.hass.services._services['homeassistant']['turn_on'](service_call) + + self.assertEqual(2, mock_call.call_count) + self.assertEqual( + ('light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, + True), + mock_call.call_args_list[0][0]) + self.assertEqual( + ('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False), + mock_call.call_args_list[1][0]) From a174a06e5c48d443e310b0334113403395647e01 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 4 Jan 2016 19:25:52 +0100 Subject: [PATCH 13/69] No need to call update() here This also fixes a problem where the sensor is left uninitialized when the energy meter temporarily has lost connection with the hub. This caused the ELIQ Online server to return HTTP error 400: "user have no current power data", which in turn caused the used eliq library to fail during JSON parsing (issue reported). --- homeassistant/components/sensor/eliqonline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 151b679b10e..891144c4b5f 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -46,7 +46,6 @@ class EliqSensor(Entity): self.api = api self.channel_id = channel_id - self.update() @property def name(self): From 66f12afbb14e5f60e5fa505c7021e835ed50cb87 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 4 Jan 2016 21:12:10 +0100 Subject: [PATCH 14/69] don't fail if error don't fail if request for updated data raises exception in underlying library --- homeassistant/components/sensor/eliqonline.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 891144c4b5f..f8aaf024813 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -46,6 +46,7 @@ class EliqSensor(Entity): self.api = api self.channel_id = channel_id + self.update() @property def name(self): @@ -69,5 +70,8 @@ class EliqSensor(Entity): def update(self): """ Gets the latest data. """ - response = self.api.get_data_now(channelid=self.channel_id) - self._state = int(response.power) + try: + response = self.api.get_data_now(channelid=self.channel_id) + self._state = int(response.power) + except: + pass From 82cd2f4ed6fdfc71c6fb52f9b6bcfbe674434b00 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 4 Jan 2016 21:31:42 +0100 Subject: [PATCH 15/69] Update eliqonline.py --- homeassistant/components/sensor/eliqonline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index f8aaf024813..91b82f5dc88 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -73,5 +73,5 @@ class EliqSensor(Entity): try: response = self.api.get_data_now(channelid=self.channel_id) self._state = int(response.power) - except: + except TypeError: # raised by eliqonline library on any HTTP error pass From 5576649d60ec349bcc4c99a3630b1f4970775a3d Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 4 Jan 2016 21:36:39 +0100 Subject: [PATCH 16/69] Update eliqonline.py --- homeassistant/components/sensor/eliqonline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 91b82f5dc88..4e6b8a5c342 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -73,5 +73,5 @@ class EliqSensor(Entity): try: response = self.api.get_data_now(channelid=self.channel_id) self._state = int(response.power) - except TypeError: # raised by eliqonline library on any HTTP error + except TypeError: # raised by eliqonline library on any HTTP error pass From aac44f3a2b1ada16bd02885e21e567faa62bc7c1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 7 Jan 2016 11:48:42 +0100 Subject: [PATCH 17/69] Use the same unit for pressure as for the forecast sensor --- homeassistant/components/sensor/openweathermap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 14e8d60c7c6..230a0f8cc68 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -20,7 +20,7 @@ SENSOR_TYPES = { 'temperature': ['Temperature', ''], 'wind_speed': ['Wind speed', 'm/s'], 'humidity': ['Humidity', '%'], - 'pressure': ['Pressure', 'hPa'], + 'pressure': ['Pressure', 'mbar'], 'clouds': ['Cloud coverage', '%'], 'rain': ['Rain', 'mm'], 'snow': ['Snow', 'mm'] From 35c29dac3f6614715c53ed127b4f242dcfe4ee57 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 7 Jan 2016 11:50:02 +0100 Subject: [PATCH 18/69] Use mbar instead of hPa --- homeassistant/components/sensor/yr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index e4003cf7e9c..24f565feb48 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -25,7 +25,7 @@ SENSOR_TYPES = { 'precipitation': ['Condition', 'mm'], 'temperature': ['Temperature', '°C'], 'windSpeed': ['Wind speed', 'm/s'], - 'pressure': ['Pressure', 'hPa'], + 'pressure': ['Pressure', 'mbar'], 'windDirection': ['Wind direction', '°'], 'humidity': ['Humidity', '%'], 'fog': ['Fog', '%'], From d69c1b848ac85615f3bcf8f8e84109aa360f47c9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 7 Jan 2016 11:57:45 +0100 Subject: [PATCH 19/69] Fix docstrings --- homeassistant/components/media_player/plex.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 6925f942be4..52dd399cedf 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -35,7 +35,7 @@ SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK def config_from_file(filename, config=None): - ''' Small configuration file management function''' + """ Small configuration file management function. """ if config: # We're writing configuration try: @@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=too-many-branches def setup_plexserver(host, token, hass, add_devices_callback): - ''' Setup a plexserver based on host parameter''' + """ Setup a plexserver based on host parameter. """ import plexapi.server import plexapi.exceptions From bb8af3a2d5fc97fc4942fbdc4cb1379c745555ac Mon Sep 17 00:00:00 2001 From: pavoni Date: Fri, 8 Jan 2016 16:00:26 +0000 Subject: [PATCH 20/69] Bump pywemo version, turn off polling, tidy trace. --- homeassistant/components/switch/wemo.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index ad21463ea17..ed56305542d 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pywemo==0.3.7'] +REQUIREMENTS = ['pywemo==0.3.8'] _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None @@ -69,15 +69,14 @@ class WemoSwitch(SwitchDevice): def _update_callback(self, _device, _params): """ Called by the wemo device callback to update state. """ _LOGGER.info( - 'Subscription update for %s, sevice=%s', - self.name, _device) + 'Subscription update for %s', + _device) self.update_ha_state(True) @property def should_poll(self): - """ No polling should be needed with subscriptions """ - # but leave in for initial version in case of issues. - return True + """ No polling needed with subscriptions """ + return False @property def unique_id(self): From 5a1fed39803e321f579eea2dfbd2041223a97873 Mon Sep 17 00:00:00 2001 From: pavoni Date: Fri, 8 Jan 2016 16:00:56 +0000 Subject: [PATCH 21/69] Bump pywemo version. --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 5566e67fb9f..5a31ff4c379 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ hikvision==0.4 orvibo==1.1.0 # homeassistant.components.switch.wemo -pywemo==0.3.7 +pywemo==0.3.8 # homeassistant.components.tellduslive tellive-py==0.5.2 From d3cd304f68a3594029f8a97f2a48630a6ffd47ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Fri, 8 Jan 2016 19:50:49 +0100 Subject: [PATCH 22/69] correced status text --- homeassistant/components/alarm_control_panel/verisure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index cc9f8dde69d..ea48209a0fd 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -67,7 +67,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): self._state = STATE_ALARM_DISARMED elif verisure.ALARM_STATUS[self._id].status == 'armedhome': self._state = STATE_ALARM_ARMED_HOME - elif verisure.ALARM_STATUS[self._id].status == 'armedaway': + elif verisure.ALARM_STATUS[self._id].status == 'armed': self._state = STATE_ALARM_ARMED_AWAY elif verisure.ALARM_STATUS[self._id].status != 'pending': _LOGGER.error( From 15a046f20c378386ec51b5590c2e146379c59807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Fri, 8 Jan 2016 19:52:03 +0100 Subject: [PATCH 23/69] update module version --- homeassistant/components/verisure.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 5a4d7c7ea99..164835f07c7 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -28,7 +28,7 @@ DISCOVER_SWITCHES = 'verisure.switches' DISCOVER_ALARMS = 'verisure.alarm_control_panel' DEPENDENCIES = ['alarm_control_panel'] -REQUIREMENTS = ['vsure==0.4.3'] +REQUIREMENTS = ['vsure==0.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5566e67fb9f..2156490269c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -191,7 +191,7 @@ python-nest==2.6.0 radiotherm==1.2 # homeassistant.components.verisure -vsure==0.4.3 +vsure==0.4.5 # homeassistant.components.zwave pydispatcher==2.0.5 From 3db6faab4da6652563efd37af6e7d44e9894b999 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Fri, 8 Jan 2016 13:30:16 -0700 Subject: [PATCH 24/69] Fix yr test --- tests/components/sensor/test_yr.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index f58aefbce43..780176dd1b8 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -4,12 +4,14 @@ tests.components.sensor.test_yr Tests Yr sensor. """ +from datetime import datetime from unittest.mock import patch import pytest import homeassistant.core as ha import homeassistant.components.sensor as sensor +import homeassistant.util.dt as dt_util @pytest.mark.usefixtures('betamax_session') @@ -26,14 +28,18 @@ class TestSensorYr: self.hass.stop() def test_default_setup(self, betamax_session): + now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.sensor.yr.requests.Session', return_value=betamax_session): - assert sensor.setup(self.hass, { - 'sensor': { - 'platform': 'yr', - 'elevation': 0, - } - }) + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, + } + }) state = self.hass.states.get('sensor.yr_symbol') From 825c91f0c3a3d3a672c97f29bb0d31188c2ed6ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jan 2016 16:58:44 -0800 Subject: [PATCH 25/69] Add calling service functionality to Alexa --- homeassistant/components/alexa.py | 6 +++ .../components/automation/__init__.py | 23 ++------ homeassistant/helpers/service.py | 37 +++++++++++++ tests/components/test_alexa.py | 53 ++++++++++++++++++- 4 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 homeassistant/helpers/service.py diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 0b06f3c9a79..66ac9de0b43 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -11,6 +11,7 @@ import logging from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.util import template +from homeassistant.helpers.service import call_from_config DOMAIN = 'alexa' DEPENDENCIES = ['http'] @@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa' CONF_INTENTS = 'intents' CONF_CARD = 'card' CONF_SPEECH = 'speech' +CONF_ACTION = 'action' def setup(hass, config): @@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data): speech = config.get(CONF_SPEECH) card = config.get(CONF_CARD) + action = config.get(CONF_ACTION) # pylint: disable=unsubscriptable-object if speech is not None: @@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data): response.add_card(CardType[card['type']], card['title'], card['content']) + if action is not None: + call_from_config(handler.server.hass, action, True) + handler.write_json(response.as_dict()) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 23d83f554ca..9c464f6954e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/ import logging from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.util import split_entity_id -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook +from homeassistant.helpers.service import call_from_config DOMAIN = 'automation' @@ -19,8 +19,6 @@ DEPENDENCIES = ['group'] CONF_ALIAS = 'alias' CONF_SERVICE = 'service' -CONF_SERVICE_ENTITY_ID = 'entity_id' -CONF_SERVICE_DATA = 'data' CONF_CONDITION = 'condition' CONF_ACTION = 'action' @@ -96,22 +94,7 @@ def _get_action(hass, config, name): _LOGGER.info('Executing %s', name) logbook.log_entry(hass, name, 'has been triggered', DOMAIN) - domain, service = split_entity_id(config[CONF_SERVICE]) - service_data = config.get(CONF_SERVICE_DATA, {}) - - if not isinstance(service_data, dict): - _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) - service_data = {} - - if CONF_SERVICE_ENTITY_ID in config: - try: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID].split(",") - except AttributeError: - service_data[ATTR_ENTITY_ID] = \ - config[CONF_SERVICE_ENTITY_ID] - - hass.services.call(domain, service, service_data) + call_from_config(hass, config) return action diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py new file mode 100644 index 00000000000..632b3d39cbb --- /dev/null +++ b/homeassistant/helpers/service.py @@ -0,0 +1,37 @@ +"""Service calling related helpers.""" +import logging + +from homeassistant.util import split_entity_id +from homeassistant.const import ATTR_ENTITY_ID + +CONF_SERVICE = 'service' +CONF_SERVICE_ENTITY_ID = 'entity_id' +CONF_SERVICE_DATA = 'data' + +_LOGGER = logging.getLogger(__name__) + + +def call_from_config(hass, config, blocking=False): + """Call a service based on a config hash.""" + if CONF_SERVICE not in config: + _LOGGER.error('Missing key %s: %s', CONF_SERVICE, config) + return + + domain, service = split_entity_id(config[CONF_SERVICE]) + service_data = config.get(CONF_SERVICE_DATA) + + if service_data is None: + service_data = {} + elif isinstance(service_data, dict): + service_data = dict(service_data) + else: + _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) + service_data = {} + + entity_id = config.get(CONF_SERVICE_ENTITY_ID) + if isinstance(entity_id, str): + service_data[ATTR_ENTITY_ID] = entity_id.split(",") + elif entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(domain, service, service_data, blocking) diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 741cfff4bb8..42acf5b3f62 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -27,12 +27,13 @@ API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT) HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD} hass = None +calls = [] @patch('homeassistant.components.http.util.get_local_ip', return_value='127.0.0.1') def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name - """ Initalizes a Home Assistant server. """ + """Initalize a Home Assistant server for testing this module.""" global hass hass = ha.HomeAssistant() @@ -42,6 +43,8 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, http.CONF_SERVER_PORT: SERVER_PORT}}) + hass.services.register('test', 'alexa', lambda call: calls.append(call)) + bootstrap.setup_component(hass, alexa.DOMAIN, { 'alexa': { 'intents': { @@ -61,7 +64,20 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name 'GetZodiacHoroscopeIntent': { 'speech': { 'type': 'plaintext', - 'text': 'You told us your sign is {{ ZodiacSign }}.' + 'text': 'You told us your sign is {{ ZodiacSign }}.', + } + }, + 'CallServiceIntent': { + 'speech': { + 'type': 'plaintext', + 'text': 'Service called', + }, + 'action': { + 'service': 'test.alexa', + 'data': { + 'hello': 1 + }, + 'entity_id': 'switch.test', } } } @@ -231,6 +247,39 @@ class TestAlexa(unittest.TestCase): text = req.json().get('response', {}).get('outputSpeech', {}).get('text') self.assertEqual('You are both home, you silly', text) + def test_intent_request_calling_service(self): + data = { + 'version': '1.0', + 'session': { + 'new': False, + 'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000', + 'application': { + 'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe' + }, + 'attributes': {}, + 'user': { + 'userId': 'amzn1.account.AM3B00000000000000000000000' + } + }, + 'request': { + 'type': 'IntentRequest', + 'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000', + 'timestamp': '2015-05-13T12:34:56Z', + 'intent': { + 'name': 'CallServiceIntent', + } + } + } + call_count = len(calls) + req = _req(data) + self.assertEqual(200, req.status_code) + self.assertEqual(call_count + 1, len(calls)) + call = calls[-1] + self.assertEqual('test', call.domain) + self.assertEqual('alexa', call.service) + self.assertEqual(['switch.test'], call.data.get('entity_id')) + self.assertEqual(1, call.data.get('hello')) + def test_session_ended_request(self): data = { 'version': '1.0', From a2c6cde83d05faf674462be59c3b0e27267566ca Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 9 Jan 2016 13:20:51 +0100 Subject: [PATCH 26/69] Update pyowm to 2.3.0 --- homeassistant/components/sensor/openweathermap.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 230a0f8cc68..84784a19546 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -13,7 +13,7 @@ from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyowm==2.2.1'] +REQUIREMENTS = ['pyowm==2.3.0'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'weather': ['Condition', ''], diff --git a/requirements_all.txt b/requirements_all.txt index 5566e67fb9f..c703dbd28ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -135,7 +135,7 @@ python-forecastio==1.3.3 https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3 # homeassistant.components.sensor.openweathermap -pyowm==2.2.1 +pyowm==2.3.0 # homeassistant.components.sensor.rpi_gpio # homeassistant.components.switch.rpi_gpio From d61eb93c033434e31c29c73591a6bc96a72e48e7 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sat, 9 Jan 2016 16:16:41 +0000 Subject: [PATCH 27/69] Remove throttle doesn't play well with subscriptions. --- homeassistant/components/light/vera.py | 1 - homeassistant/components/switch/vera.py | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index bcf27eab5f7..95e0d849762 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -86,5 +86,4 @@ class VeraLight(VeraSwitch): else: self.vera_device.switch_on() - self.last_command_send = time.time() self.is_on_status = True diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 91a83086570..1f67d3cfa69 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -87,8 +87,6 @@ class VeraSwitch(ToggleEntity): else: self._name = self.vera_device.name self.is_on_status = False - # for debouncing status check after command is sent - self.last_command_send = 0 self.controller.register(vera_device) self.controller.on( @@ -132,12 +130,10 @@ class VeraSwitch(ToggleEntity): return attr def turn_on(self, **kwargs): - self.last_command_send = time.time() self.vera_device.switch_on() self.is_on_status = True def turn_off(self, **kwargs): - self.last_command_send = time.time() self.vera_device.switch_off() self.is_on_status = False @@ -152,10 +148,7 @@ class VeraSwitch(ToggleEntity): return self.is_on_status def update(self): - # We need to debounce the status call after turning switch on or off - # because the vera has some lag in updating the device status try: - if (self.last_command_send + 5) < time.time(): - self.is_on_status = self.vera_device.is_switched_on() + self.is_on_status = self.vera_device.is_switched_on() except RequestException: _LOGGER.warning('Could not update status for %s', self.name) From 9faedf0e67e90f013842cdbd6840eb59484acef9 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Fri, 8 Jan 2016 12:54:22 -0700 Subject: [PATCH 28/69] Default to MQTT protocol v3.1.1 (fix #854) --- homeassistant/components/mqtt/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b5ea258c5cc..86dce3d511b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -149,9 +149,9 @@ class MQTT(object): } if client_id is None: - self._mqttc = mqtt.Client() + self._mqttc = mqtt.Client(protocol=mqtt.MQTTv311) else: - self._mqttc = mqtt.Client(client_id) + self._mqttc = mqtt.Client(client_id, protocol=mqtt.MQTTv311) self._mqttc.user_data_set(self.userdata) From b64680e4a827c7191c24b05c373306e8e9b0f8f0 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sat, 9 Jan 2016 21:13:34 +0000 Subject: [PATCH 29/69] Revise to depend on vera subscription data updates, rather than talking to device. --- homeassistant/components/sensor/vera.py | 10 ++++------ homeassistant/components/switch/vera.py | 12 +++++------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index c7b95ce5d53..ef581f22dc3 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -119,18 +119,18 @@ class VeraSensor(Entity): attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' if self.vera_device.is_armable: - armed = self.vera_device.refresh_value('Armed') + armed = self.vera_device.get_value('Armed') attr[ATTR_ARMED] = 'True' if armed == '1' else 'False' if self.vera_device.is_trippable: - last_tripped = self.vera_device.refresh_value('LastTrip') + last_tripped = self.vera_device.get_value('LastTrip') if last_tripped is not None: utc_time = dt_util.utc_from_timestamp(int(last_tripped)) attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str( utc_time) else: attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.refresh_value('Tripped') + tripped = self.vera_device.get_value('Tripped') attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False' attr['Vera Device Id'] = self.vera_device.vera_device_id @@ -143,7 +143,6 @@ class VeraSensor(Entity): def update(self): if self.vera_device.category == "Temperature Sensor": - self.vera_device.refresh_value('CurrentTemperature') current_temp = self.vera_device.get_value('CurrentTemperature') vera_temp_units = self.vera_device.veraController.temperature_units @@ -161,10 +160,9 @@ class VeraSensor(Entity): self.current_value = current_temp elif self.vera_device.category == "Light Sensor": - self.vera_device.refresh_value('CurrentLevel') self.current_value = self.vera_device.get_value('CurrentLevel') elif self.vera_device.category == "Sensor": - tripped = self.vera_device.refresh_value('Tripped') + tripped = self.vera_device.get_value('Tripped') self.current_value = 'Tripped' if tripped == '1' else 'Not Tripped' else: self.current_value = 'Unknown' diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 1f67d3cfa69..0e29583bf5c 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -111,18 +111,18 @@ class VeraSwitch(ToggleEntity): attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' if self.vera_device.is_armable: - armed = self.vera_device.refresh_value('Armed') + armed = self.vera_device.get_value('Armed') attr[ATTR_ARMED] = 'True' if armed == '1' else 'False' if self.vera_device.is_trippable: - last_tripped = self.vera_device.refresh_value('LastTrip') + last_tripped = self.vera_device.get_value('LastTrip') if last_tripped is not None: utc_time = dt_util.utc_from_timestamp(int(last_tripped)) attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str( utc_time) else: attr[ATTR_LAST_TRIP_TIME] = None - tripped = self.vera_device.refresh_value('Tripped') + tripped = self.vera_device.get_value('Tripped') attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False' attr['Vera Device Id'] = self.vera_device.vera_device_id @@ -133,6 +133,7 @@ class VeraSwitch(ToggleEntity): self.vera_device.switch_on() self.is_on_status = True + def turn_off(self, **kwargs): self.vera_device.switch_off() self.is_on_status = False @@ -148,7 +149,4 @@ class VeraSwitch(ToggleEntity): return self.is_on_status def update(self): - try: - self.is_on_status = self.vera_device.is_switched_on() - except RequestException: - _LOGGER.warning('Could not update status for %s', self.name) + self.is_on_status = self.vera_device.is_switched_on() From af21f72d179312b3295fbfc7249751e978a7cf80 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sat, 9 Jan 2016 22:58:28 +0000 Subject: [PATCH 30/69] Update pyvera version. --- homeassistant/components/light/vera.py | 2 +- homeassistant/components/sensor/vera.py | 6 ++---- homeassistant/components/switch/vera.py | 6 ++---- requirements_all.txt | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 95e0d849762..76718e71eb4 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.2'] +REQUIREMENTS = ['pyvera==0.2.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index ef581f22dc3..6ef5d469c60 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.2'] +REQUIREMENTS = ['pyvera==0.2.3'] _LOGGER = logging.getLogger(__name__) @@ -85,9 +85,7 @@ class VeraSensor(Entity): self.current_value = '' self._temperature_units = None - self.controller.register(vera_device) - self.controller.on( - vera_device, self._update_callback) + self.controller.register(vera_device, self._update_callback) def _update_callback(self, _device): """ Called by the vera device callback to update state. """ diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 0e29583bf5c..0acc33bea4e 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.2'] +REQUIREMENTS = ['pyvera==0.2.3'] _LOGGER = logging.getLogger(__name__) @@ -88,9 +88,7 @@ class VeraSwitch(ToggleEntity): self._name = self.vera_device.name self.is_on_status = False - self.controller.register(vera_device) - self.controller.on( - vera_device, self._update_callback) + self.controller.register(vera_device, self._update_callback) def _update_callback(self, _device): """ Called by the vera device callback to update state. """ diff --git a/requirements_all.txt b/requirements_all.txt index 5a31ff4c379..64792267213 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.2 +pyvera==0.2.3 # homeassistant.components.wink # homeassistant.components.light.wink From 12b5caed70721b3f924b316239627cbba81021be Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 9 Jan 2016 15:51:51 -0800 Subject: [PATCH 31/69] ps - strip entity IDs in service call --- homeassistant/helpers/service.py | 3 ++- tests/helpers/test_service.py | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/helpers/test_service.py diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 632b3d39cbb..530f72fa22f 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -30,7 +30,8 @@ def call_from_config(hass, config, blocking=False): entity_id = config.get(CONF_SERVICE_ENTITY_ID) if isinstance(entity_id, str): - service_data[ATTR_ENTITY_ID] = entity_id.split(",") + service_data[ATTR_ENTITY_ID] = [ent.strip() for ent in + entity_id.split(",")] elif entity_id is not None: service_data[ATTR_ENTITY_ID] = entity_id diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py new file mode 100644 index 00000000000..32a36edc5f1 --- /dev/null +++ b/tests/helpers/test_service.py @@ -0,0 +1,39 @@ +""" +tests.helpers.test_service +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Test service helpers. +""" +from datetime import timedelta +import unittest +from unittest.mock import patch + +import homeassistant.core as ha +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.helpers import service + +from tests.common import get_test_home_assistant, mock_service + + +class TestServiceHelpers(unittest.TestCase): + """ + Tests the Home Assistant service helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = get_test_home_assistant() + self.calls = mock_service(self.hass, 'test_domain', 'test_service') + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_split_entity_string(self): + service.call_from_config(self.hass, { + 'service': 'test_domain.test_service', + 'entity_id': 'hello.world, sensor.beer' + }) + self.hass.pool.block_till_done() + self.assertEqual(['hello.world', 'sensor.beer'], + self.calls[-1].data.get('entity_id')) From 73cdf00512fd24e961656afb012a02e371626f39 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 9 Jan 2016 16:01:27 -0800 Subject: [PATCH 32/69] More service helper tests --- homeassistant/helpers/service.py | 9 +++++++-- tests/helpers/test_service.py | 33 ++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 530f72fa22f..15cfe9b97c6 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -13,11 +13,16 @@ _LOGGER = logging.getLogger(__name__) def call_from_config(hass, config, blocking=False): """Call a service based on a config hash.""" - if CONF_SERVICE not in config: + if not isinstance(config, dict) or CONF_SERVICE not in config: _LOGGER.error('Missing key %s: %s', CONF_SERVICE, config) return - domain, service = split_entity_id(config[CONF_SERVICE]) + try: + domain, service = split_entity_id(config[CONF_SERVICE]) + except ValueError: + _LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE]) + return + service_data = config.get(CONF_SERVICE_DATA) if service_data is None: diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 32a36edc5f1..aa2cab07d0d 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -4,11 +4,9 @@ tests.helpers.test_service Test service helpers. """ -from datetime import timedelta import unittest from unittest.mock import patch -import homeassistant.core as ha from homeassistant.const import SERVICE_TURN_ON from homeassistant.helpers import service @@ -37,3 +35,34 @@ class TestServiceHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(['hello.world', 'sensor.beer'], self.calls[-1].data.get('entity_id')) + + def test_not_mutate_input(self): + orig = { + 'service': 'test_domain.test_service', + 'entity_id': 'hello.world, sensor.beer', + 'data': { + 'hello': 1, + }, + } + service.call_from_config(self.hass, orig) + self.hass.pool.block_till_done() + self.assertEqual({ + 'service': 'test_domain.test_service', + 'entity_id': 'hello.world, sensor.beer', + 'data': { + 'hello': 1, + }, + }, orig) + + @patch('homeassistant.helpers.service._LOGGER.error') + def test_fail_silently_if_no_service(self, mock_log): + service.call_from_config(self.hass, None) + self.assertEqual(1, mock_log.call_count) + + service.call_from_config(self.hass, {}) + self.assertEqual(2, mock_log.call_count) + + service.call_from_config(self.hass, { + 'service': 'invalid' + }) + self.assertEqual(3, mock_log.call_count) From 438e78610d4ca9261bfcb8ab210043d0b1a52fc4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 9 Jan 2016 16:21:08 -0800 Subject: [PATCH 33/69] Update PyChromecast dependency --- homeassistant/components/media_player/cast.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 87117cfd367..c0717edc860 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -20,7 +20,7 @@ from homeassistant.components.media_player import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) -REQUIREMENTS = ['pychromecast==0.6.13'] +REQUIREMENTS = ['pychromecast==0.6.14'] CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ diff --git a/requirements_all.txt b/requirements_all.txt index 2444fac84c6..74cf5a1e993 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -69,7 +69,7 @@ pyvera==0.2.2 python-wink==0.3.1 # homeassistant.components.media_player.cast -pychromecast==0.6.13 +pychromecast==0.6.14 # homeassistant.components.media_player.kodi jsonrpc-requests==0.1 From 2d8cf7de447cf4a3fc8b0df65c0ba34066e287f3 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Sun, 10 Jan 2016 04:10:38 +0100 Subject: [PATCH 34/69] Fix wrapper and S_BINARY and bump req. version * Wrap existing SerialGateway instance instead of subclassing SerialGatewat class. * Add S_BINARY in switch platform only in version 1.5 of mysenors api. * Use version 0.4 of pymysensors. * Show gateway port as state attribute. --- homeassistant/components/mysensors.py | 62 ++++++++++++-------- homeassistant/components/sensor/mysensors.py | 5 +- homeassistant/components/switch/mysensors.py | 7 ++- requirements_all.txt | 2 +- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index a0601850fa7..7fb1a7cb1d7 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -12,7 +12,8 @@ New features: New MySensors component. Updated MySensors Sensor platform. -New MySensors Switch platform. +New MySensors Switch platform. Currently only in optimistic mode (compare +with MQTT). Multiple gateways are now supported. Configuration.yaml: @@ -29,11 +30,6 @@ mysensors: """ import logging -try: - import mysensors.mysensors as mysensors -except ImportError: - mysensors = None - from homeassistant.helpers import validate_config import homeassistant.bootstrap as bootstrap @@ -55,10 +51,11 @@ DOMAIN = 'mysensors' DEPENDENCIES = [] REQUIREMENTS = [ 'https://github.com/theolind/pymysensors/archive/' - '2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3'] + '005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4'] _LOGGER = logging.getLogger(__name__) ATTR_NODE_ID = 'node_id' ATTR_CHILD_ID = 'child_id' +ATTR_PORT = 'port' GATEWAYS = None SCAN_INTERVAL = 30 @@ -82,21 +79,22 @@ def setup(hass, config): _LOGGER): return False - global mysensors # pylint: disable=invalid-name - if mysensors is None: - import mysensors.mysensors as _mysensors - mysensors = _mysensors + import mysensors.mysensors as mysensors version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION)) is_metric = (hass.config.temperature_unit == TEMP_CELCIUS) def setup_gateway(port, persistence, persistence_file, version): """Return gateway after setup of the gateway.""" - gateway = GatewayWrapper( - port, persistence, persistence_file, version) - # pylint: disable=attribute-defined-outside-init + gateway = mysensors.SerialGateway(port, event_callback=None, + persistence=persistence, + persistence_file=persistence_file, + protocol_version=version) gateway.metric = is_metric gateway.debug = config[DOMAIN].get(CONF_DEBUG, False) + gateway = GatewayWrapper(gateway, version) + # pylint: disable=attribute-defined-outside-init + gateway.event_callback = gateway.callback_factory() def gw_start(event): """Callback to trigger start of gateway and any persistence.""" @@ -172,30 +170,46 @@ def pf_callback_factory( return mysensors_callback -class GatewayWrapper(mysensors.SerialGateway): +class GatewayWrapper(object): """Gateway wrapper class, by subclassing serial gateway.""" - def __init__(self, port, persistence, persistence_file, version): + def __init__(self, gateway, 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. + gateway (mysensors.SerialGateway): Gateway to wrap. + version (str): Version of mysensors API. Attributes: + _wrapped_gateway (mysensors.SerialGateway): Wrapped gateway. version (str): Version of mysensors API. platform_callbacks (list): Callback functions, one per platform. const (module): Mysensors API constants. + __initialised (bool): True if GatewayWrapper is initialised. """ - super().__init__(port, event_callback=self.callback_factory(), - persistence=persistence, - persistence_file=persistence_file, - protocol_version=version) + self._wrapped_gateway = gateway self.version = version self.platform_callbacks = [] self.const = self.get_const() + self.__initialised = True + + def __getattr__(self, name): + """See if this object has attribute name.""" + # Do not use hasattr, it goes into infinite recurrsion + if name in self.__dict__: + # this object has it + return getattr(self, name) + # proxy to the wrapped object + return getattr(self._wrapped_gateway, name) + + def __setattr__(self, name, value): + """See if this object has attribute name then set to value.""" + if '_GatewayWrapper__initialised' not in self.__dict__: + return object.__setattr__(self, name, value) + elif name in self.__dict__: + object.__setattr__(self, name, value) + else: + object.__setattr__(self._wrapped_gateway, name, value) def get_const(self): """Get mysensors API constants.""" diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 3944cf4f982..3562af1949d 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -78,14 +78,14 @@ class MySensorsSensor(Entity): """Setup class attributes on instantiation. Args: - gateway (str): Gateway. + gateway (GatewayWrapper): Gateway object. 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: - gateway (str): Gateway. + gateway (GatewayWrapper): Gateway object. node_id (str): Id of node. child_id (str): Id of child. _name (str): Entity name. @@ -154,6 +154,7 @@ class MySensorsSensor(Entity): def state_attributes(self): """Return the state attributes.""" data = { + mysensors.ATTR_PORT: self.gateway.port, mysensors.ATTR_NODE_ID: self.node_id, mysensors.ATTR_CHILD_ID: self.child_id, ATTR_BATTERY_LEVEL: self.battery_level, diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 33c214cda76..d8d7d4d2473 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 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 = [ @@ -46,6 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ] if float(gateway.version) >= 1.5: s_types.extend([ + gateway.const.Presentation.S_BINARY, gateway.const.Presentation.S_SPRINKLER, gateway.const.Presentation.S_WATER_LEAK, gateway.const.Presentation.S_SOUND, @@ -68,14 +68,14 @@ class MySensorsSwitch(SwitchDevice): """Setup class attributes on instantiation. Args: - port (str): Gateway port. + gateway (GatewayWrapper): Gateway object. 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 (GatewayWrapper): Gateway object node_id (str): Id of node. child_id (str): Id of child. _name (str): Entity name. @@ -112,6 +112,7 @@ class MySensorsSwitch(SwitchDevice): def state_attributes(self): """Return the state attributes.""" data = { + mysensors.ATTR_PORT: self.gateway.port, mysensors.ATTR_NODE_ID: self.node_id, mysensors.ATTR_CHILD_ID: self.child_id, ATTR_BATTERY_LEVEL: self.battery_level, diff --git a/requirements_all.txt b/requirements_all.txt index e8709c789e3..9bfdd3b62f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -90,7 +90,7 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 paho-mqtt==1.1 # homeassistant.components.mysensors -https://github.com/theolind/pymysensors/archive/2aa8f32908e8c5bb3e5c77c5851db778f8635792.zip#pymysensors==0.3 +https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4 # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 From 4b4fb038e3e8c3914448ac0a40d9a85f2e29235b Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 10 Jan 2016 12:30:47 +0000 Subject: [PATCH 35/69] Update for new library, slightly revise switch logic. --- homeassistant/components/light/vera.py | 7 +++--- homeassistant/components/sensor/vera.py | 6 ++--- homeassistant/components/switch/vera.py | 30 +++++++++++++++---------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 76718e71eb4..59abbe6b29d 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -14,7 +14,7 @@ from homeassistant.components.switch.vera import VeraSwitch from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON REQUIREMENTS = ['pyvera==0.2.3'] @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): lights = [] for device in devices: - extra_data = device_data.get(device.deviceId, {}) + extra_data = device_data.get(device.device_id, {}) exclude = extra_data.get('exclude', False) if exclude is not True: @@ -86,4 +86,5 @@ class VeraLight(VeraSwitch): else: self.vera_device.switch_on() - self.is_on_status = True + self._state = STATE_ON + self.update_ha_state() diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 6ef5d469c60..b381974ab31 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -56,7 +56,7 @@ def get_devices(hass, config): vera_sensors = [] for device in devices: - extra_data = device_data.get(device.deviceId, {}) + extra_data = device_data.get(device.device_id, {}) exclude = extra_data.get('exclude', False) if exclude is not True: @@ -89,12 +89,10 @@ class VeraSensor(Entity): 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) + return "%s %s %s" % (self.name, self.vera_device.device_id, self.state) @property def state(self): diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 0acc33bea4e..3f5d2a08cb8 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -12,12 +12,16 @@ from requests.exceptions import RequestException import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.switch import SwitchDevice + from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, - EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_STOP, + STATE_ON, + STATE_OFF) REQUIREMENTS = ['pyvera==0.2.3'] @@ -60,7 +64,7 @@ def get_devices(hass, config): vera_switches = [] for device in devices: - extra_data = device_data.get(device.deviceId, {}) + extra_data = device_data.get(device.device_id, {}) exclude = extra_data.get('exclude', False) if exclude is not True: @@ -75,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(get_devices(hass, config)) -class VeraSwitch(ToggleEntity): +class VeraSwitch(SwitchDevice): """ Represents a Vera Switch. """ def __init__(self, vera_device, controller, extra_data=None): @@ -86,15 +90,17 @@ class VeraSwitch(ToggleEntity): self._name = self.extra_data.get('name') else: self._name = self.vera_device.name - self.is_on_status = False + self._state = STATE_OFF self.controller.register(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) + if self.vera_device.is_switched_on(): + self._state = STATE_ON + else: + self._state = STATE_OFF + self.update_ha_state() @property def name(self): @@ -129,12 +135,14 @@ class VeraSwitch(ToggleEntity): def turn_on(self, **kwargs): self.vera_device.switch_on() - self.is_on_status = True + self._state = STATE_ON + self.update_ha_state() def turn_off(self, **kwargs): self.vera_device.switch_off() - self.is_on_status = False + self._state = STATE_OFF + self.update_ha_state() @property def should_poll(self): @@ -144,7 +152,5 @@ class VeraSwitch(ToggleEntity): @property def is_on(self): """ True if device is on. """ - return self.is_on_status + return self._state == STATE_ON - def update(self): - self.is_on_status = self.vera_device.is_switched_on() From fca8ad5b0b85cde597ab9f3bcdf7acb82dce99d8 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 10 Jan 2016 12:48:36 +0000 Subject: [PATCH 36/69] Tidy. --- homeassistant/components/light/vera.py | 1 - homeassistant/components/switch/vera.py | 4 ---- 2 files changed, 5 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 59abbe6b29d..42a5e7b7899 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -7,7 +7,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.vera/ """ import logging -import time from requests.exceptions import RequestException from homeassistant.components.switch.vera import VeraSwitch diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 3f5d2a08cb8..4094fe61f4f 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -7,11 +7,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.vera/ """ import logging -import time from requests.exceptions import RequestException import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( @@ -138,7 +136,6 @@ class VeraSwitch(SwitchDevice): self._state = STATE_ON self.update_ha_state() - def turn_off(self, **kwargs): self.vera_device.switch_off() self._state = STATE_OFF @@ -153,4 +150,3 @@ class VeraSwitch(SwitchDevice): def is_on(self): """ True if device is on. """ return self._state == STATE_ON - From 6c94650603280e88a42ff959ce67556dfb04d211 Mon Sep 17 00:00:00 2001 From: xifle Date: Sun, 10 Jan 2016 15:00:14 +0100 Subject: [PATCH 37/69] Accept lower & upper case for owntracks 'home' region --- homeassistant/components/device_tracker/owntracks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index e81952eb770..e1b0e1de306 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -71,7 +71,7 @@ def setup_scanner(hass, config, see): location = '' if data['event'] == 'enter': - if data['desc'] == 'home': + if data['desc'].lower() == 'home': location = STATE_HOME else: location = data['desc'] From 542b640ef0f82eb5edd01245b12386da938f378e Mon Sep 17 00:00:00 2001 From: hydreliox Date: Mon, 11 Jan 2016 09:25:26 +0100 Subject: [PATCH 38/69] FreeMobile Notify First Commit for FreeSMS platform --- homeassistant/components/notify/freesms.py | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 homeassistant/components/notify/freesms.py diff --git a/homeassistant/components/notify/freesms.py b/homeassistant/components/notify/freesms.py new file mode 100644 index 00000000000..e1e813b0d60 --- /dev/null +++ b/homeassistant/components/notify/freesms.py @@ -0,0 +1,53 @@ +""" +homeassistant.components.notify.freesms +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +FreeSMS platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify. ... / +""" +import logging +from homeassistant.helpers import validate_config +from homeassistant.components.notify import ( + DOMAIN, ATTR_TITLE, BaseNotificationService) +from homeassistant.const import CONF_USERNAME, CONF_ACCESS_TOKEN + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['freesms==0.1.0'] + + +def get_service(hass, config): + """ Get the FreeSMS notification service. """ + + if not validate_config({DOMAIN: config}, + {DOMAIN: [CONF_USERNAME, + CONF_ACCESS_TOKEN]}, + _LOGGER): + return None + + return FreeSMSNotificationService(config[CONF_USERNAME], + config[CONF_ACCESS_TOKEN]) + + +# pylint: disable=too-few-public-methods +class FreeSMSNotificationService(BaseNotificationService): + """ Implements notification service for the Free SMS service. """ + + + + def __init__(self, username, access_token): + from freesms import FreeClient + self.free_client = FreeClient(username, access_token) + + def send_message(self, message="", **kwargs): + """ Send a message to the Free Mobile user cell. """ + resp = self.free_client.send_sms(message) + + if resp.status_code == 400: + _LOGGER.error("At least one parameter is missing") + elif resp.status_code == 402: + _LOGGER.error("Too much sms send in a few time") + elif resp.status_code == 403: + _LOGGER.error("Wrong Username/Password") + elif resp.status_code == 500: + _LOGGER.error("Server error, try later") From bc73a6829def841c1649591d4c52d5e6d6773063 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Mon, 11 Jan 2016 09:30:32 +0100 Subject: [PATCH 39/69] Code formatting Correct pylint errors --- homeassistant/components/notify/freesms.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/notify/freesms.py b/homeassistant/components/notify/freesms.py index e1e813b0d60..6e765ecfb74 100644 --- a/homeassistant/components/notify/freesms.py +++ b/homeassistant/components/notify/freesms.py @@ -9,7 +9,7 @@ https://home-assistant.io/components/notify. ... / import logging from homeassistant.helpers import validate_config from homeassistant.components.notify import ( - DOMAIN, ATTR_TITLE, BaseNotificationService) + DOMAIN, BaseNotificationService) from homeassistant.const import CONF_USERNAME, CONF_ACCESS_TOKEN _LOGGER = logging.getLogger(__name__) @@ -33,8 +33,6 @@ def get_service(hass, config): class FreeSMSNotificationService(BaseNotificationService): """ Implements notification service for the Free SMS service. """ - - def __init__(self, username, access_token): from freesms import FreeClient self.free_client = FreeClient(username, access_token) From c1aa1fb0e0c149b9569b7f0751c285075103c611 Mon Sep 17 00:00:00 2001 From: ntouran Date: Mon, 11 Jan 2016 20:46:45 -0800 Subject: [PATCH 40/69] First attempt at adding Z-wave COMMAND_CLASS_ALARM --- homeassistant/components/sensor/zwave.py | 17 +++++++++++++++++ homeassistant/components/zwave.py | 4 +++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 1ed831b286d..fdbec56404c 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -73,6 +73,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif (value.command_class == zwave.COMMAND_CLASS_METER and value.type == zwave.TYPE_DECIMAL): add_devices([ZWaveMultilevelSensor(value)]) + + elif value.command_class == zwave.COMMAND_CLASS_ALARM: + add_devices([ZWaveAlarmSensor(value)]) class ZWaveSensor(Entity): @@ -216,3 +219,17 @@ class ZWaveMultilevelSensor(ZWaveSensor): return TEMP_FAHRENHEIT else: return unit + +class ZWaveAlarmSensor(ZWaveSensor): + """ A Z-wave sensor that sends Alarm alerts + + Examples include certain Multisensors that have motion and vibration capabilities. + Z-Wave defines various alarm types such as Smoke, Flood, Burglar, CarbonMonoxide, etc. + This wraps these events. + + COMMAND_CLASS_ALARM is what we get here. + """ + @property + def state(self): + """ Returns the state of the sensor. """ + return str(self._value.data) \ No newline at end of file diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index b52e430600a..d3cf70a2cd4 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -37,6 +37,7 @@ COMMAND_CLASS_SENSOR_BINARY = 48 COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_METER = 50 COMMAND_CLASS_BATTERY = 128 +COMMAND_CLASS_ALARM = 113 # 0x71 GENRE_WHATEVER = None GENRE_USER = "User" @@ -53,7 +54,8 @@ DISCOVERY_COMPONENTS = [ DISCOVER_SENSORS, [COMMAND_CLASS_SENSOR_BINARY, COMMAND_CLASS_SENSOR_MULTILEVEL, - COMMAND_CLASS_METER], + COMMAND_CLASS_METER, + COMMAND_CLASS_ALARM], TYPE_WHATEVER, GENRE_USER), ('light', From 1bbecce5eb880f51f82ed15323718308345cff06 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Mon, 11 Jan 2016 18:51:03 +0100 Subject: [PATCH 41/69] Add requirements and coverage exception --- .coveragerc | 1 + requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index ea9f302fbb1..815a69cf513 100644 --- a/.coveragerc +++ b/.coveragerc @@ -73,6 +73,7 @@ omit = homeassistant/components/media_player/plex.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py + homeassistant/components/notify/freesms.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py diff --git a/requirements_all.txt b/requirements_all.txt index f320408f138..5a6860e9284 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,6 +89,9 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # homeassistant.components.mqtt paho-mqtt==1.1 +# homeassistant.components.notify.freesms +freesms==0.1.0 + # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 From ea8d278f8fa78516ccff5a75ed70284f415a86b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 11 Jan 2016 21:05:32 -0800 Subject: [PATCH 42/69] Rename freesms to free_mobile --- .coveragerc | 2 +- homeassistant/components/notify/{freesms.py => free_mobile.py} | 0 requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename homeassistant/components/notify/{freesms.py => free_mobile.py} (100%) diff --git a/.coveragerc b/.coveragerc index c6c696cac57..272ace975c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -76,7 +76,7 @@ omit = homeassistant/components/media_player/plex.py homeassistant/components/media_player/sonos.py homeassistant/components/media_player/squeezebox.py - homeassistant/components/notify/freesms.py + homeassistant/components/notify/free_mobile.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py homeassistant/components/notify/pushbullet.py diff --git a/homeassistant/components/notify/freesms.py b/homeassistant/components/notify/free_mobile.py similarity index 100% rename from homeassistant/components/notify/freesms.py rename to homeassistant/components/notify/free_mobile.py diff --git a/requirements_all.txt b/requirements_all.txt index ab82ecfda3a..1bc00b4c46d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -92,7 +92,7 @@ paho-mqtt==1.1 # homeassistant.components.mysensors https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4 -# homeassistant.components.notify.freesms +# homeassistant.components.notify.free_mobile freesms==0.1.0 # homeassistant.components.notify.pushbullet From 5af48643269d0db652d5b616aa0e1fd43406c83e Mon Sep 17 00:00:00 2001 From: ntouran Date: Mon, 11 Jan 2016 23:27:53 -0800 Subject: [PATCH 43/69] ZWave alarm sensor cleanup (pylint fixes) --- homeassistant/components/sensor/zwave.py | 26 +++++++++++++----------- homeassistant/components/zwave.py | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index fdbec56404c..869f4dbe810 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif (value.command_class == zwave.COMMAND_CLASS_METER and value.type == zwave.TYPE_DECIMAL): add_devices([ZWaveMultilevelSensor(value)]) - + elif value.command_class == zwave.COMMAND_CLASS_ALARM: add_devices([ZWaveAlarmSensor(value)]) @@ -220,16 +220,18 @@ class ZWaveMultilevelSensor(ZWaveSensor): else: return unit + class ZWaveAlarmSensor(ZWaveSensor): - """ A Z-wave sensor that sends Alarm alerts - - Examples include certain Multisensors that have motion and vibration capabilities. - Z-Wave defines various alarm types such as Smoke, Flood, Burglar, CarbonMonoxide, etc. - This wraps these events. - - COMMAND_CLASS_ALARM is what we get here. + """ A Z-wave sensor that sends Alarm alerts + + Examples include certain Multisensors that have motion and + vibration capabilities. Z-Wave defines various alarm types + such as Smoke, Flood, Burglar, CarbonMonoxide, etc. + + This wraps these alarms and allows you to use them to + trigger things, etc. + + COMMAND_CLASS_ALARM is what we get here. """ - @property - def state(self): - """ Returns the state of the sensor. """ - return str(self._value.data) \ No newline at end of file + # Empty subclass for now. Allows for later customizations + pass diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index d3cf70a2cd4..9f6d7ca37aa 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -37,7 +37,7 @@ COMMAND_CLASS_SENSOR_BINARY = 48 COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_METER = 50 COMMAND_CLASS_BATTERY = 128 -COMMAND_CLASS_ALARM = 113 # 0x71 +COMMAND_CLASS_ALARM = 113 # 0x71 GENRE_WHATEVER = None GENRE_USER = "User" From 31fcd230b14d3e791d8e99fdd4d96288a671b61f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 12 Jan 2016 11:30:12 +0100 Subject: [PATCH 44/69] Update docstrings --- homeassistant/components/notify/free_mobile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notify/free_mobile.py b/homeassistant/components/notify/free_mobile.py index 6e765ecfb74..3589dc58658 100644 --- a/homeassistant/components/notify/free_mobile.py +++ b/homeassistant/components/notify/free_mobile.py @@ -1,10 +1,10 @@ """ -homeassistant.components.notify.freesms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -FreeSMS platform for notify component. +homeassistant.components.notify.free_mobile +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Free Mobile SMS platform for notify component. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify. ... / +https://home-assistant.io/components/notify.free_mobile/ """ import logging from homeassistant.helpers import validate_config @@ -17,7 +17,7 @@ REQUIREMENTS = ['freesms==0.1.0'] def get_service(hass, config): - """ Get the FreeSMS notification service. """ + """ Get the Free Mobile SMS notification service. """ if not validate_config({DOMAIN: config}, {DOMAIN: [CONF_USERNAME, @@ -31,7 +31,7 @@ def get_service(hass, config): # pylint: disable=too-few-public-methods class FreeSMSNotificationService(BaseNotificationService): - """ Implements notification service for the Free SMS service. """ + """ Implements notification service for the Free Mobile SMS service. """ def __init__(self, username, access_token): from freesms import FreeClient @@ -44,7 +44,7 @@ class FreeSMSNotificationService(BaseNotificationService): if resp.status_code == 400: _LOGGER.error("At least one parameter is missing") elif resp.status_code == 402: - _LOGGER.error("Too much sms send in a few time") + _LOGGER.error("Too much SMS send in a few time") elif resp.status_code == 403: _LOGGER.error("Wrong Username/Password") elif resp.status_code == 500: From 8ace6566573df10aa35402ab424b70a19816a8bc Mon Sep 17 00:00:00 2001 From: Moonshot Date: Sat, 9 Jan 2016 23:18:46 -0500 Subject: [PATCH 45/69] Create mqtt eventstream component --- homeassistant/components/mqtt_eventstream.py | 95 +++++++++++++ tests/components/test_mqtt_eventstream.py | 139 +++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 homeassistant/components/mqtt_eventstream.py create mode 100644 tests/components/test_mqtt_eventstream.py diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py new file mode 100644 index 00000000000..e52124182ff --- /dev/null +++ b/homeassistant/components/mqtt_eventstream.py @@ -0,0 +1,95 @@ +""" +homeassistant.components.mqtt_eventstream +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Connect two Home Assistant instances via mqtt. + +Configuration: + +To use the mqtt_eventstream component you will need to add the following to +your configuration.yaml file. + +If you do not specify a publish_topic you will not forward events to the queue. +If you do not specify a subscribe_topic then you will not receive events from +the remote server. + +mqtt_eventstream: + publish_topic: MyServerName + subscribe_topic: OtherHaServerName +""" +import json +from homeassistant.core import EventOrigin, State +from homeassistant.const import ( + MATCH_ALL, + EVENT_TIME_CHANGED, + EVENT_CALL_SERVICE, + EVENT_SERVICE_EXECUTED, + EVENT_STATE_CHANGED, +) + +import homeassistant.loader as loader +from homeassistant.remote import JSONEncoder + +# The domain of your component. Should be equal to the name of your component +DOMAIN = "mqtt_eventstream" + +# List of component names (string) your component depends upon +DEPENDENCIES = ['mqtt'] + + +def setup(hass, config): + """ Setup our mqtt_eventstream component. """ + def _event_handler(event): + """ Handle events by publishing them on the mqtt queue. """ + if event.origin != EventOrigin.local: + return + if event.event_type in ( + EVENT_TIME_CHANGED, + EVENT_CALL_SERVICE, + EVENT_SERVICE_EXECUTED + ): + return + event = {'event_type': event.event_type, 'event_data': event.data} + msg = json.dumps(event, cls=JSONEncoder) + mqtt.publish(hass, pub_topic, msg) + + mqtt = loader.get_component('mqtt') + pub_topic = config[DOMAIN].get('publish_topic', None) + sub_topic = config[DOMAIN].get('subscribe_topic', None) + + # Only listen for local events if you are going to publish them + if (pub_topic): + hass.bus.listen(MATCH_ALL, _event_handler) + + # Process events from a remote server that are received on a queue + def _event_receiver(topic, payload, qos): + """ + A new MQTT message, published by the other HA instance, + has been received. + """ + # TODO error handling + event = json.loads(payload) + event_type = event.get('event_type') + event_data = event.get('event_data') + + # Special case handling for event STATE_CHANGED + # We will try to convert state dicts back to State objects + if event_type == EVENT_STATE_CHANGED and event_data: + for key in ('old_state', 'new_state'): + state = State.from_dict(event_data.get(key)) + + if state: + event_data[key] = state + + hass.bus.fire( + event_type, + event_data=event_data, + origin=EventOrigin.remote + ) + + # Only subscribe if you specified a topic + if (sub_topic): + mqtt.subscribe(hass, sub_topic, _event_receiver) + + hass.states.set('{domain}.initialized'.format(domain=DOMAIN), True) + # return boolean to indicate that initialization was successful + return True diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py new file mode 100644 index 00000000000..5e1680ad2a4 --- /dev/null +++ b/tests/components/test_mqtt_eventstream.py @@ -0,0 +1,139 @@ +""" +tests.test_component_mqtt_eventstream +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests MQTT eventstream component. +""" +import json +import unittest +from unittest.mock import ANY, patch + +import homeassistant.components.mqtt_eventstream as eventstream +from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.core import State +from homeassistant.remote import JSONEncoder +import homeassistant.util.dt as dt_util + +from tests.common import ( + get_test_home_assistant, + mock_mqtt_component, + fire_mqtt_message, + mock_state_change_event, + fire_time_changed +) + + +class TestMqttEventStream(unittest.TestCase): + """ Test the MQTT eventstream module. """ + + def setUp(self): # pylint: disable=invalid-name + super(TestMqttEventStream, self).setUp() + self.hass = get_test_home_assistant() + self.mock_mqtt = mock_mqtt_component(self.hass) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def add_eventstream(self, sub_topic=None, pub_topic=None): + """ Add a mqtt_eventstream component to the hass. """ + config = {} + if sub_topic: + config['subscribe_topic'] = sub_topic + if pub_topic: + config['publish_topic'] = pub_topic + return eventstream.setup(self.hass, {eventstream.DOMAIN: config}) + + def test_setup_succeeds(self): + self.assertTrue(self.add_eventstream()) + + def test_setup_with_pub(self): + # Should start off with no listeners for all events + self.assertEqual(self.hass.bus.listeners.get('*'), None) + + self.assertTrue(self.add_eventstream(pub_topic='bar')) + self.hass.pool.block_till_done() + + # Verify that the event handler has been added as a listener + self.assertEqual(self.hass.bus.listeners.get('*'), 1) + + @patch('homeassistant.components.mqtt.subscribe') + def test_subscribe(self, mock_sub): + sub_topic = 'foo' + self.assertTrue(self.add_eventstream(sub_topic=sub_topic)) + self.hass.pool.block_till_done() + + # Verify that the this entity was subscribed to the topic + mock_sub.assert_called_with(self.hass, sub_topic, ANY) + + @patch('homeassistant.components.mqtt.publish') + @patch('homeassistant.core.dt_util.datetime_to_str') + def test_state_changed_event_sends_message(self, mock_datetime, mock_pub): + now = '00:19:19 11-01-2016' + e_id = 'fake.entity' + pub_topic = 'bar' + mock_datetime.return_value = now + + # Add the eventstream component for publishing events + self.assertTrue(self.add_eventstream(pub_topic=pub_topic)) + self.hass.pool.block_till_done() + + # Reset the mock because it will have already gotten calls for the + # mqtt_eventstream state change on initialization, etc. + mock_pub.reset_mock() + + # Set a state of an entity + mock_state_change_event(self.hass, State(e_id, 'on')) + self.hass.pool.block_till_done() + + # The order of the JSON is indeterminate, + # so first just check that publish was called + mock_pub.assert_called_with(self.hass, pub_topic, ANY) + self.assertTrue(mock_pub.called) + + # Get the actual call to publish and make sure it was the one + # we were looking for + msg = mock_pub.call_args[0][2] + event = {} + event['event_type'] = EVENT_STATE_CHANGED + new_state = { + "last_updated": now, + "state": "on", + "entity_id": e_id, + "attributes": {}, + "last_changed": now + } + event['event_data'] = {"new_state": new_state, "entity_id": e_id} + + # Verify that the message received was that expected + self.assertEqual(json.loads(msg), event) + + @patch('homeassistant.components.mqtt.publish') + def test_time_event_does_not_send_message(self, mock_pub): + self.assertTrue(self.add_eventstream(pub_topic='bar')) + self.hass.pool.block_till_done() + + # Reset the mock because it will have already gotten calls for the + # mqtt_eventstream state change on initialization, etc. + mock_pub.reset_mock() + + fire_time_changed(self.hass, dt_util.utcnow()) + self.assertFalse(mock_pub.called) + + def test_receiving_remote_event_fires_hass_event(self): + sub_topic = 'foo' + self.assertTrue(self.add_eventstream(sub_topic=sub_topic)) + self.hass.pool.block_till_done() + + calls = [] + self.hass.bus.listen_once('test_event', lambda _: calls.append(1)) + self.hass.pool.block_till_done() + + payload = json.dumps( + {'event_type': 'test_event', 'event_data': {}}, + cls=JSONEncoder + ) + fire_mqtt_message(self.hass, sub_topic, payload) + self.hass.pool.block_till_done() + + self.assertEqual(1, len(calls)) From 6f398f59df8771b0633c604a6acf8b387fa4b55c Mon Sep 17 00:00:00 2001 From: Moonshot Date: Tue, 12 Jan 2016 21:01:53 -0500 Subject: [PATCH 46/69] Fix filtering of EVENT_CALL_SERVICE and EVENT_SERVICE_EXECUTED events --- homeassistant/components/mqtt_eventstream.py | 61 +++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index e52124182ff..a90e4b0d42a 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -18,6 +18,8 @@ mqtt_eventstream: """ import json from homeassistant.core import EventOrigin, State +from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN +from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, @@ -25,7 +27,6 @@ from homeassistant.const import ( EVENT_SERVICE_EXECUTED, EVENT_STATE_CHANGED, ) - import homeassistant.loader as loader from homeassistant.remote import JSONEncoder @@ -38,41 +39,59 @@ DEPENDENCIES = ['mqtt'] def setup(hass, config): """ Setup our mqtt_eventstream component. """ - def _event_handler(event): - """ Handle events by publishing them on the mqtt queue. """ - if event.origin != EventOrigin.local: - return - if event.event_type in ( - EVENT_TIME_CHANGED, - EVENT_CALL_SERVICE, - EVENT_SERVICE_EXECUTED - ): - return - event = {'event_type': event.event_type, 'event_data': event.data} - msg = json.dumps(event, cls=JSONEncoder) - mqtt.publish(hass, pub_topic, msg) - mqtt = loader.get_component('mqtt') pub_topic = config[DOMAIN].get('publish_topic', None) sub_topic = config[DOMAIN].get('subscribe_topic', None) + def _event_publisher(event): + """ Handle events by publishing them on the mqtt queue. """ + if event.origin != EventOrigin.local: + return + if event.event_type == EVENT_TIME_CHANGED: + return + + # Filter out the events that were triggered by publishing + # to the MQTT topic, or you will end up in an infinite loop. + if event.event_type == EVENT_CALL_SERVICE: + if ( + event.data.get('domain') == MQTT_DOMAIN and + event.data.get('service') == MQTT_SVC_PUBLISH and + event.data.get('topic') == pub_topic + ): + return + + # Filter out all the "event service executed" events because they + # are only used internally by core as callbacks for blocking + # during the interval while a service is being executed. + # They will serve no purpose to the external system, + # and thus are unnecessary traffic. + # And at any rate it would cause an infinite loop to publish them + # because publishing to an MQTT topic itself triggers one. + if event.event_type == EVENT_SERVICE_EXECUTED: + return + + event_info = {'event_type': event.event_type, 'event_data': event.data} + msg = json.dumps(event_info, cls=JSONEncoder) + mqtt.publish(hass, pub_topic, msg) + # Only listen for local events if you are going to publish them - if (pub_topic): - hass.bus.listen(MATCH_ALL, _event_handler) + if pub_topic: + hass.bus.listen(MATCH_ALL, _event_publisher) # Process events from a remote server that are received on a queue def _event_receiver(topic, payload, qos): """ - A new MQTT message, published by the other HA instance, - has been received. + Receive events published by the other HA instance and fire + them on this hass instance. """ - # TODO error handling event = json.loads(payload) event_type = event.get('event_type') event_data = event.get('event_data') # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects + # Copied over from the _handle_api_post_events_event method + # of the api component. if event_type == EVENT_STATE_CHANGED and event_data: for key in ('old_state', 'new_state'): state = State.from_dict(event_data.get(key)) @@ -87,7 +106,7 @@ def setup(hass, config): ) # Only subscribe if you specified a topic - if (sub_topic): + if sub_topic: mqtt.subscribe(hass, sub_topic, _event_receiver) hass.states.set('{domain}.initialized'.format(domain=DOMAIN), True) From 3b7b12bbd5bc0787d71a7423da50655b171aa9e6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 21:53:27 -0800 Subject: [PATCH 47/69] Make Flake8 happy --- .../components/alarm_control_panel/manual.py | 3 ++- .../components/automation/numeric_state.py | 23 +++++++++--------- homeassistant/components/automation/sun.py | 24 ++++++++++++++----- homeassistant/components/automation/time.py | 4 ++-- .../components/device_tracker/snmp.py | 3 +-- .../components/device_tracker/tplink.py | 4 ++-- homeassistant/components/http.py | 12 +++++----- homeassistant/components/light/rfxtrx.py | 5 ++-- .../components/media_player/squeezebox.py | 15 ++++++------ homeassistant/components/sensor/arest.py | 4 ++-- homeassistant/components/switch/rfxtrx.py | 5 ++-- homeassistant/components/thermostat/ecobee.py | 5 ++-- 12 files changed, 60 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 63bc989f3df..2658e005aea 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -68,7 +68,8 @@ class ManualAlarm(alarm.AlarmControlPanel): @property def state(self): """ Returns the state of the device. """ - if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \ + if self._state in (STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY) and \ self._pending_time and self._state_ts + self._pending_time > \ dt_util.utcnow(): return STATE_ALARM_PENDING diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index f2baf760748..61e68aa8e8e 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -6,6 +6,7 @@ Offers numeric state listening automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#numeric-state-trigger """ +from functools import partial import logging from homeassistant.const import CONF_VALUE_TEMPLATE @@ -20,6 +21,14 @@ CONF_ABOVE = "above" _LOGGER = logging.getLogger(__name__) +def _renderer(hass, value_template, state): + """Render state value.""" + if value_template is None: + return state.state + + return template.render(hass, value_template, {'state': state}) + + def trigger(hass, config, action): """ Listen for state changes based on `config`. """ entity_id = config.get(CONF_ENTITY_ID) @@ -38,12 +47,7 @@ def trigger(hass, config, action): CONF_BELOW, CONF_ABOVE) return False - if value_template is not None: - renderer = lambda value: template.render(hass, - value_template, - {'state': value}) - else: - renderer = lambda value: value.state + renderer = partial(_renderer, hass, value_template) # pylint: disable=unused-argument def state_automation_listener(entity, from_s, to_s): @@ -79,12 +83,7 @@ def if_action(hass, config): CONF_BELOW, CONF_ABOVE) return None - if value_template is not None: - renderer = lambda value: template.render(hass, - value_template, - {'state': value}) - else: - renderer = lambda value: value.state + renderer = partial(_renderer, hass, value_template) def if_numeric_state(): """ Test numeric state condition. """ diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 064f6a0a16a..0616c0a48e6 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -80,18 +80,30 @@ def if_action(hass, config): return None if before is None: - before_func = lambda: None + def before_func(): + """Return no point in time.""" + return None elif before == EVENT_SUNRISE: - before_func = lambda: sun.next_rising(hass) + before_offset + def before_func(): + """Return time before sunrise.""" + return sun.next_rising(hass) + before_offset else: - before_func = lambda: sun.next_setting(hass) + before_offset + def before_func(): + """Return time before sunset.""" + return sun.next_setting(hass) + before_offset if after is None: - after_func = lambda: None + def after_func(): + """Return no point in time.""" + return None elif after == EVENT_SUNRISE: - after_func = lambda: sun.next_rising(hass) + after_offset + def after_func(): + """Return time after sunrise.""" + return sun.next_rising(hass) + after_offset else: - after_func = lambda: sun.next_setting(hass) + after_offset + def after_func(): + """Return time after sunset.""" + return sun.next_setting(hass) + after_offset def time_if(): """ Validate time based if-condition """ diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 7fc2c0d40e2..e8cf9c3b6ee 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -32,8 +32,8 @@ def trigger(hass, config, action): _error_time(config[CONF_AFTER], CONF_AFTER) return False hours, minutes, seconds = after.hour, after.minute, after.second - elif (CONF_HOURS in config or CONF_MINUTES in config - or CONF_SECONDS in config): + elif (CONF_HOURS in config or CONF_MINUTES in config or + CONF_SECONDS in config): hours = convert(config.get(CONF_HOURS), int) minutes = convert(config.get(CONF_MINUTES), int) seconds = convert(config.get(CONF_SECONDS), int) diff --git a/homeassistant/components/device_tracker/snmp.py b/homeassistant/components/device_tracker/snmp.py index 868f701673a..cd0e8239c38 100644 --- a/homeassistant/components/device_tracker/snmp.py +++ b/homeassistant/components/device_tracker/snmp.py @@ -105,8 +105,7 @@ class SnmpScanner(object): return if errstatus: _LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(), - errindex and restable[-1][int(errindex)-1] - or '?') + errindex and restable[-1][int(errindex)-1] or '?') return for resrow in restable: diff --git a/homeassistant/components/device_tracker/tplink.py b/homeassistant/components/device_tracker/tplink.py index 46556b3eca4..a661dac0c1e 100755 --- a/homeassistant/components/device_tracker/tplink.py +++ b/homeassistant/components/device_tracker/tplink.py @@ -242,8 +242,8 @@ class Tplink3DeviceScanner(TplinkDeviceScanner): _LOGGER.info("Loading wireless clients...") - url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \ - .format(self.host, self.stok) + url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?' + 'form=statistics').format(self.host, self.stok) referer = 'http://{}/webpages/index.html'.format(self.host) response = requests.post(url, diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index b7f57b0157e..35c215a8630 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -198,12 +198,12 @@ class RequestHandler(SimpleHTTPRequestHandler): "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) return - self.authenticated = (self.server.api_password is None - or self.headers.get(HTTP_HEADER_HA_AUTH) == - self.server.api_password - or data.get(DATA_API_PASSWORD) == - self.server.api_password - or self.verify_session()) + self.authenticated = (self.server.api_password is None or + self.headers.get(HTTP_HEADER_HA_AUTH) == + self.server.api_password or + data.get(DATA_API_PASSWORD) == + self.server.api_password or + self.verify_session()) if '_METHOD' in data: method = data.pop('_METHOD') diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 6132c10a99c..22bd2575242 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -13,8 +13,9 @@ from homeassistant.components.light import Light from homeassistant.util import slugify from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ - ATTR_NAME, EVENT_BUTTON_PRESSED +from homeassistant.components.rfxtrx import ( + ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, + ATTR_NAME, EVENT_BUTTON_PRESSED) DEPENDENCIES = ['rfxtrx'] diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index d3139d52c01..4fd13e8da42 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -22,9 +22,9 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) -SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ - SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF +SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \ + SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ + SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF # pylint: disable=unused-argument @@ -202,11 +202,10 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ Image url of current playing media. """ if 'artwork_url' in self._status: return self._status['artwork_url'] - return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\ - .format( - server=self._lms.host, - port=self._lms.http_port, - player=self._id) + return ('http://{server}:{port}/music/current/cover.jpg?' + 'player={player}').format(server=self._lms.host, + port=self._lms.http_port, + player=self._id) @property def media_title(self): diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index 298c9b8cb79..dd9281c484a 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -11,8 +11,8 @@ import logging import requests -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, \ - DEVICE_DEFAULT_NAME +from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, + DEVICE_DEFAULT_NAME) from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity from homeassistant.util import template, Throttle diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 69e08e7d129..84f4df82b1f 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -13,8 +13,9 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.util import slugify from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ - ATTR_NAME, EVENT_BUTTON_PRESSED +from homeassistant.components.rfxtrx import ( + ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, + ATTR_NAME, EVENT_BUTTON_PRESSED) DEPENDENCIES = ['rfxtrx'] diff --git a/homeassistant/components/thermostat/ecobee.py b/homeassistant/components/thermostat/ecobee.py index 30221689274..0b4e14f36b7 100644 --- a/homeassistant/components/thermostat/ecobee.py +++ b/homeassistant/components/thermostat/ecobee.py @@ -46,8 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return data = ecobee.NETWORK hold_temp = discovery_info['hold_temp'] - _LOGGER.info("Loading ecobee thermostat component with hold_temp set to " - + str(hold_temp)) + _LOGGER.info( + "Loading ecobee thermostat component with hold_temp set to %s", + hold_temp) add_devices(Thermostat(data, index, hold_temp) for index in range(len(data.ecobee.thermostats))) From 60f40800c4e2f54b1739caf7795c7f64d9279ad1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 21:56:09 -0800 Subject: [PATCH 48/69] Use mock HA for locative tests --- tests/components/device_tracker/test_locative.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index b86f24455de..619fe929ac7 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -11,10 +11,10 @@ from unittest.mock import patch import requests from homeassistant import bootstrap, const -import homeassistant.core as ha import homeassistant.components.device_tracker as device_tracker import homeassistant.components.http as http -import homeassistant.components.zone as zone + +from tests.common import get_test_home_assistant SERVER_PORT = 8126 HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) @@ -34,7 +34,7 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name """ Initalizes a Home Assistant server. """ global hass - hass = ha.HomeAssistant() + hass = get_test_home_assistant() # Set up server bootstrap.setup_component(hass, http.DOMAIN, { From 9cdf84dacf2b0010d2e2c17fa886805503f15963 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 21:57:43 -0800 Subject: [PATCH 49/69] Update flake8 and pylint versions --- requirements_test.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 679c0e99ce5..616c49c5ae4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,5 @@ -flake8>=2.5.0 -pylint>=1.5.1 +flake8>=2.5.1 +pylint>=1.5.3 coveralls>=1.1 pytest>=2.6.4 pytest-cov>=2.2.0 From ec85884d92a09b8762f26358c100e0ca7c09c046 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 10 Jan 2016 00:30:26 -0500 Subject: [PATCH 50/69] Added initial implementation of universal media player. --- .../components/media_player/__init__.py | 1 + .../components/media_player/universal.py | 405 ++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 homeassistant/components/media_player/universal.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8204052b4a9..bdb435e8a73 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -72,6 +72,7 @@ SUPPORT_YOUTUBE = 64 SUPPORT_TURN_ON = 128 SUPPORT_TURN_OFF = 256 SUPPORT_PLAY_MEDIA = 512 +SUPPORT_VOLUME_BUTTONS = 1024 YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py new file mode 100644 index 00000000000..dbdf5b02e57 --- /dev/null +++ b/homeassistant/components/media_player/universal.py @@ -0,0 +1,405 @@ +""" +homeassistant.components.media_player.universal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Combines multiple media players into one for a universal controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.universal/ +""" + +# pylint: disable=import-error +import logging + +from homeassistant.const import ( + STATE_IDLE, STATE_OFF, CONF_NAME, ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, + SERVICE_VOLUME_MUTE, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK) + +from homeassistant.components.media_player import ( + MediaPlayerDevice, DOMAIN, + SUPPORT_VOLUME_BUTTONS, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, + SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SERVICE_PLAY_MEDIA, SERVICE_YOUTUBE_VIDEO, + ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED, + ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION, + ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_TRACK, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_ALBUM_ARTIST, + ATTR_MEDIA_SEASON, ATTR_MEDIA_EPISODE, ATTR_MEDIA_CHANNEL, + ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_SEEK_POSITION) + +CONF_ATTRS = 'attributes' +CONF_CHILDREN = 'children' +CONF_COMMANDS = 'commands' +CONF_PLATFORM = 'platform' +CONF_SERVICE = 'service' +CONF_SERVICE_DATA = 'service_data' +CONF_STATE = 'state' +OFF_STATES = [STATE_IDLE, STATE_OFF] +REQUIREMENTS = [] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ sets up the universal media players """ + if not validate_config(config): + return + + player = UniversalMediaPlayer(config[CONF_NAME], + config[CONF_CHILDREN], + config[CONF_COMMANDS], + config[CONF_ATTRS]) + + add_devices([player]) + + +def validate_config(config): + """ validate universal media player configuration """ + del config[CONF_PLATFORM] + + # validate name + if CONF_NAME not in config: + _LOGGER.error('Universal Media Player configuration requires name') + return False + + validate_children(config) + validate_commands(config) + validate_attributes(config) + + del_keys = [] + for key in config: + if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]: + _LOGGER.warning( + 'Universal Media Player (%s) unrecognized parameter %s', + config[CONF_NAME], key) + del_keys.append(key) + for key in del_keys: + del config[key] + + return True + + +def validate_children(config): + """ validate children """ + if CONF_CHILDREN not in config: + _LOGGER.info( + 'No children under Universal Media Player (%s)', config[CONF_NAME]) + config[CONF_CHILDREN] = [] + elif not isinstance(config[CONF_CHILDREN], list): + _LOGGER.warning( + 'Universal Media Player (%s) children not list in config. ' + 'They will be ignored.', + config[CONF_NAME]) + config[CONF_CHILDREN] = [] + + +def validate_commands(config): + """ validate commands """ + if CONF_COMMANDS not in config: + config[CONF_COMMANDS] = {} + elif not isinstance(config[CONF_COMMANDS], dict): + _LOGGER.warning( + 'Universal Media Player (%s) specified commands not dict in config.' + ' They will be ignored.', + config[CONF_NAME]) + config[CONF_COMMANDS] = {} + + +def validate_attributes(config): + """ validate attributes """ + if CONF_ATTRS not in config: + config[CONF_ATTRS] = {} + elif not isinstance(config[CONF_ATTRS], dict): + _LOGGER.warning( + 'Universal Media Player (%s) specified attributes ' + 'not dict in config. They will be ignored.', + config[CONF_NAME]) + config[CONF_ATTRS] = {} + + for key, val in config[CONF_ATTRS].items(): + config[CONF_ATTRS][key] = val.split('|', 1) + + +class UniversalMediaPlayer(MediaPlayerDevice): + """ Represents a universal media player in HA """ + + def __init__(self, name, children, commands, attributes): + self._name = name + self._children = children + self._cmds = commands + self._attrs = attributes + + # [todo] Update when children update + + def _entity_lkp(self, entity_id=None, state_attr=None): + """ Looks up an entity state from hass """ + if entity_id is None: + return + + state_obj = self.hass.states.get(entity_id) + + if state_obj is None: + return + + return state_obj.attributes.get(state_attr) \ + if state_attr else state_obj + + def _override_or_child_attr(self, attr_name): + """ returns either the override or the active child for attr_name """ + if attr_name in self._attrs: + return self._entity_lkp(*self._attrs[attr_name]) + + return self._child_attr(attr_name) + + def _child_attr(self, attr_name): + """ returns the active child's attr """ + active_child = self.active_child_state + return active_child.attributes.get(attr_name) if active_child else None + + def _override_or_child_service(self, service_name, **service_data): + """ calls either a specified or active child's service """ + if service_name in self._cmds: + cmd_data = self._cmds[service_name] + domain, service = cmd_data[CONF_SERVICE].split('.', 1) + self.hass.services.call(domain, service, + cmd_data[CONF_SERVICE_DATA], + blocking=True) + return + + self._child_service(service_name, **service_data) + + def _child_service(self, service_name, **service_data): + """ calls the active child's specified service """ + active_child = self.active_child_state + service_data[ATTR_ENTITY_ID] = active_child.entity_id + + self.hass.services.call(DOMAIN, service_name, service_data, + blocking=True) + + @property + def master_state(self): + """ gets the master state from entity or none """ + if CONF_STATE in self._attrs: + master_state = self._entity_lkp(*self._attrs[CONF_STATE]) + return master_state.state if master_state else STATE_OFF + else: + return None + + @property + def children(self): + """ Gets children and their current states """ + return {child: self._entity_lkp(child) for child in self._children} + + @property + def active_child_state(self): + """ The state of the active child or None """ + for child_id, child_state in self.children.items(): + if child_state and child_state.state not in OFF_STATES: + return child_state + + @property + def name(self): + """ name of universal player """ + return self._name + + @property + def state(self): + """ + Current state of media player + + Off if master state is off + ELSE Status of first active child + ELSE master state or off + """ + master_state = self.master_state # avoid multiple lookups + if master_state == STATE_OFF: + return STATE_OFF + + active_child = self.active_child_state + if active_child: + return active_child.state + + return master_state if master_state else STATE_OFF + + @property + def volume_level(self): + """ Volume level of entity specified in attributes or active child """ + return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL) + + @property + def is_volume_muted(self): + """ boolean if volume is muted """ + return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + return self._child_attr(ATTR_MEDIA_CONTENT_ID) + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return self._child_attr(ATTR_MEDIA_CONTENT_TYPE) + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + return self._child_attr(ATTR_MEDIA_DURATION) + + @property + def media_image_url(self): + """ Image url of current playing media. """ + return self._child_attr(ATTR_ENTITY_PICTURE) + + @property + def media_title(self): + """ Title of current playing media. """ + return self._child_attr(ATTR_MEDIA_TITLE) + + @property + def media_artist(self): + """ Artist of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_ARTIST) + + @property + def media_album_name(self): + """ Album name of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_ALBUM_NAME) + + @property + def media_album_artist(self): + """ Album arist of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_ALBUM_ARTIST) + + @property + def media_track(self): + """ Track number of current playing media. (Music track only) """ + return self._child_attr(ATTR_MEDIA_TRACK) + + @property + def media_series_title(self): + """ Series title of current playing media. (TV Show only)""" + return self._child_attr(ATTR_MEDIA_SERIES_TITLE) + + @property + def media_season(self): + """ Season of current playing media. (TV Show only) """ + return self._child_attr(ATTR_MEDIA_SEASON) + + @property + def media_episode(self): + """ Episode of current playing media. (TV Show only) """ + return self._child_attr(ATTR_MEDIA_EPISODE) + + @property + def media_channel(self): + """ Channel currently playing. """ + return self._child_attr(ATTR_MEDIA_CHANNEL) + + @property + def media_playlist(self): + """ Title of Playlist currently playing. """ + return self._child_attr(ATTR_MEDIA_PLAYLIST) + + @property + def app_id(self): + """ ID of the current running app. """ + return self._child_attr(ATTR_APP_ID) + + @property + def app_name(self): + """ Name of the current running app. """ + return self._child_attr(ATTR_APP_NAME) + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + flags = self._child_attr(ATTR_SUPPORTED_MEDIA_COMMANDS) or 0 + + if SERVICE_TURN_ON in self._cmds: + flags |= SUPPORT_TURN_ON + if SERVICE_TURN_OFF in self._cmds: + flags |= SUPPORT_TURN_OFF + + if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, + SERVICE_VOLUME_DOWN]]): + flags |= SUPPORT_VOLUME_BUTTONS + flags &= ~SUPPORT_VOLUME_SET + + if SERVICE_VOLUME_MUTE in self._cmds and \ + ATTR_MEDIA_VOLUME_MUTED in self._attrs: + flags |= SUPPORT_VOLUME_MUTE + + return flags + + @property + def device_state_attributes(self): + """ + Extra attributes a device wants to expose. + Not supported for Universal Media Player. + """ + return None + + def turn_on(self): + """ turn the media player on. """ + self._override_or_child_service(SERVICE_TURN_ON) + + def turn_off(self): + """ turn the media player off. """ + self._override_or_child_service(SERVICE_TURN_OFF) + + def mute_volume(self, is_volume_muted): + """ mute the volume. """ + data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted} + self._override_or_child_service(SERVICE_VOLUME_MUTE, **data) + + def set_volume_level(self, volume_level): + """ set volume level, range 0..1. """ + data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} + self._child_service(SERVICE_VOLUME_SET, **data) + + def media_play(self): + """ Send play commmand. """ + self._child_service(SERVICE_MEDIA_PLAY) + + def media_pause(self): + """ Send pause command. """ + self._child_service(SERVICE_MEDIA_PAUSE) + + def media_previous_track(self): + """ Send previous track command. """ + self._child_service(SERVICE_MEDIA_PREVIOUS_TRACK) + + def media_next_track(self): + """ Send next track command. """ + self._child_service(SERVICE_MEDIA_NEXT_TRACK) + + def media_seek(self, position): + """ Send seek command. """ + data = {ATTR_MEDIA_SEEK_POSITION: position} + self._child_service(SERVICE_MEDIA_SEEK, **data) + + def play_youtube(self, media_id): + """ Plays a YouTube media. """ + self._child_service(SERVICE_YOUTUBE_VIDEO, media_id=media_id) + + def play_media(self, media_type, media_id): + """ Plays a piece of media. """ + self._child_service( + SERVICE_PLAY_MEDIA, media_type=media_type, media_id=media_id) + + def volume_up(self): + """ volume_up media player. """ + self._override_or_child_service(SERVICE_VOLUME_UP) + + def volume_down(self): + """ volume_down media player. """ + self._override_or_child_service(SERVICE_VOLUME_DOWN) + + def media_play_pause(self): + """ media_play_pause media player. """ + self._child_service(SERVICE_MEDIA_PLAY_PAUSE) From 20a1025a8c74177a49e9d4d2fce7b6286b586a16 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 10 Jan 2016 00:43:27 -0500 Subject: [PATCH 51/69] Added active_child attribute to universal media players. The entity of the first active child is now reported in the attributes for a universal media player. --- homeassistant/components/media_player/universal.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index dbdf5b02e57..7e721f85677 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -31,6 +31,8 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_SEEK_POSITION) +ATTR_ACTIVE_CHILD = 'active_child' + CONF_ATTRS = 'attributes' CONF_CHILDREN = 'children' CONF_COMMANDS = 'commands' @@ -38,6 +40,7 @@ CONF_PLATFORM = 'platform' CONF_SERVICE = 'service' CONF_SERVICE_DATA = 'service_data' CONF_STATE = 'state' + OFF_STATES = [STATE_IDLE, STATE_OFF] REQUIREMENTS = [] _LOGGER = logging.getLogger(__name__) @@ -338,11 +341,10 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def device_state_attributes(self): - """ - Extra attributes a device wants to expose. - Not supported for Universal Media Player. - """ - return None + """ Extra attributes a device wants to expose. """ + active_child = self.active_child_state + return {ATTR_ACTIVE_CHILD: active_child.entity_id} \ + if active_child else {} def turn_on(self): """ turn the media player on. """ From 36214c73eea0f8596910a19fe1c955995af69832 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 10 Jan 2016 20:34:30 -0500 Subject: [PATCH 52/69] Better handling of entity lookups in Universal media player. Allowed the lookup function in the Universal Media Player to return either a state object or the actual state of an entity during lookup. --- .../components/media_player/universal.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 7e721f85677..67720a9357f 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -138,7 +138,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): # [todo] Update when children update - def _entity_lkp(self, entity_id=None, state_attr=None): + def _entity_lkp(self, entity_id=None, state_attr=None, state_as_obj=True): """ Looks up an entity state from hass """ if entity_id is None: return @@ -148,13 +148,17 @@ class UniversalMediaPlayer(MediaPlayerDevice): if state_obj is None: return - return state_obj.attributes.get(state_attr) \ - if state_attr else state_obj + if state_attr: + return state_obj.attributes.get(state_attr) + if state_as_obj: + return state_obj + return state_obj.state def _override_or_child_attr(self, attr_name): """ returns either the override or the active child for attr_name """ if attr_name in self._attrs: - return self._entity_lkp(*self._attrs[attr_name]) + return self._entity_lkp(*self._attrs[attr_name], + state_as_obj=False) return self._child_attr(attr_name) @@ -187,8 +191,9 @@ class UniversalMediaPlayer(MediaPlayerDevice): def master_state(self): """ gets the master state from entity or none """ if CONF_STATE in self._attrs: - master_state = self._entity_lkp(*self._attrs[CONF_STATE]) - return master_state.state if master_state else STATE_OFF + master_state = self._entity_lkp(*self._attrs[CONF_STATE], + state_as_obj=False) + return master_state if master_state else STATE_OFF else: return None From 4a1f609893c048ac27b18a1d20738d8e4faa55f6 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Mon, 11 Jan 2016 21:56:07 -0500 Subject: [PATCH 53/69] Lint fixes and faster updating to universal media player. 1) Many lint fixes. 2) Bound the Universal Media Player to its dependencies so that its state will be updated when one of its dependencies is changed. --- .../components/media_player/universal.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 67720a9357f..87d05a11108 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -8,10 +8,14 @@ https://home-assistant.io/components/media_player.universal/ """ # pylint: disable=import-error +from copy import copy import logging +from homeassistant.helpers.event import track_state_change + from homeassistant.const import ( - STATE_IDLE, STATE_OFF, CONF_NAME, ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, + STATE_IDLE, STATE_ON, STATE_OFF, CONF_NAME, + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, SERVICE_VOLUME_MUTE, @@ -52,7 +56,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if not validate_config(config): return - player = UniversalMediaPlayer(config[CONF_NAME], + player = UniversalMediaPlayer(hass, + config[CONF_NAME], config[CONF_CHILDREN], config[CONF_COMMANDS], config[CONF_ATTRS]) @@ -106,8 +111,8 @@ def validate_commands(config): config[CONF_COMMANDS] = {} elif not isinstance(config[CONF_COMMANDS], dict): _LOGGER.warning( - 'Universal Media Player (%s) specified commands not dict in config.' - ' They will be ignored.', + 'Universal Media Player (%s) specified commands not dict in ' + 'config. They will be ignored.', config[CONF_NAME]) config[CONF_COMMANDS] = {} @@ -129,14 +134,17 @@ def validate_attributes(config): class UniversalMediaPlayer(MediaPlayerDevice): """ Represents a universal media player in HA """ + # pylint: disable=too-many-public-methods - def __init__(self, name, children, commands, attributes): + def __init__(self, hass, name, children, commands, attributes): + # pylint: disable=too-many-arguments + self.hass = hass self._name = name self._children = children self._cmds = commands self._attrs = attributes - # [todo] Update when children update + track_state_change(hass, self.dependencies, self.update_state) def _entity_lkp(self, entity_id=None, state_attr=None, state_as_obj=True): """ Looks up an entity state from hass """ @@ -187,6 +195,14 @@ class UniversalMediaPlayer(MediaPlayerDevice): self.hass.services.call(DOMAIN, service_name, service_data, blocking=True) + @property + def dependencies(self): + """ List of entity ids of entities that the mp depends on for state """ + depend = copy(self._children) + for entity in self._attrs.values(): + depend.append(entity[0]) + return depend + @property def master_state(self): """ gets the master state from entity or none """ @@ -205,7 +221,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def active_child_state(self): """ The state of the active child or None """ - for child_id, child_state in self.children.items(): + for child_state in self.children.values(): if child_state and child_state.state not in OFF_STATES: return child_state @@ -241,7 +257,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def is_volume_muted(self): """ boolean if volume is muted """ - return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) + return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) \ + in [True, STATE_ON] @property def media_content_id(self): @@ -410,3 +427,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): def media_play_pause(self): """ media_play_pause media player. """ self._child_service(SERVICE_MEDIA_PLAY_PAUSE) + + def update_state(self, *_): + """ event to trigger a state update in HA """ + self.update_ha_state() From 59456f20fb8d0292975eda96d8e8d9d3b2230740 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 00:43:34 -0500 Subject: [PATCH 54/69] Added tests to universal media player and fixed bug 1) Fixed universal media player to maintain specified child order when checking for active child. 2) Added many tests to universal media player. --- .../components/media_player/universal.py | 4 +- tests/components/media_player/__init__.py | 0 .../test_init.py} | 0 .../components/media_player/test_universal.py | 354 ++++++++++++++++++ 4 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 tests/components/media_player/__init__.py rename tests/components/{test_media_player.py => media_player/test_init.py} (100%) create mode 100644 tests/components/media_player/test_universal.py diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 87d05a11108..9a770b50d32 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -221,7 +221,9 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def active_child_state(self): """ The state of the active child or None """ - for child_state in self.children.values(): + children = self.children + for child_name in self._children: + child_state = children[child_name] if child_state and child_state.state not in OFF_STATES: return child_state diff --git a/tests/components/media_player/__init__.py b/tests/components/media_player/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/test_media_player.py b/tests/components/media_player/test_init.py similarity index 100% rename from tests/components/test_media_player.py rename to tests/components/media_player/test_init.py diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py new file mode 100644 index 00000000000..4be84b54297 --- /dev/null +++ b/tests/components/media_player/test_universal.py @@ -0,0 +1,354 @@ +""" +tests.component.media_player.test_universal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests universal media_player component. +""" +from copy import copy +import unittest + +import homeassistant.core as ha +from homeassistant.const import ( + STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED, + SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) +import homeassistant.components.switch as switch +import homeassistant.components.media_player as media_player +import homeassistant.components.media_player.universal as universal +from tests.common import mock_service + + +class MockMediaPlayer(media_player.MediaPlayerDevice): + """ Mock media player for testing """ + + def __init__(self, hass, name): + self.hass = hass + self._name = name + self.entity_id = media_player.ENTITY_ID_FORMAT.format(name) + self._state = STATE_OFF + self._volume_level = 0 + self._is_volume_muted = False + self._media_title = None + self._supported_media_commands = 0 + + @property + def name(self): + """ name of player """ + return self._name + + @property + def state(self): + """ state of the player """ + return self._state + + @property + def volume_level(self): + """ volume level of player """ + return self._volume_level + + @property + def is_volume_muted(self): + """ if the media player is muted """ + return self._is_volume_muted + + @property + def supported_media_commands(self): + """ supported media commands flag """ + return self._supported_media_commands + + def turn_on(self): + """ mock turn_on function """ + self._state = STATE_UNKNOWN + + def turn_off(self): + """ mock turn_off function """ + self._state = STATE_OFF + + def mute_volume(self): + """ mock mute function """ + self._is_volume_muted = ~self._is_volume_muted + + def set_volume_level(self, volume): + """ mock set volume level """ + self._volume_level = volume + + def media_play(self): + """ mock play """ + self._state = STATE_PLAYING + + def media_pause(self): + """ mock pause """ + self._state = STATE_PAUSED + + +class TestMediaPlayer(unittest.TestCase): + """ Test the media_player module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + + self.mock_mp_1 = MockMediaPlayer(self.hass, 'mock1') + self.mock_mp_1.update_ha_state() + + self.mock_mp_2 = MockMediaPlayer(self.hass, 'mock2') + self.mock_mp_2.update_ha_state() + + self.mock_mute_switch_id = switch.ENTITY_ID_FORMAT.format('mute') + self.hass.states.set(self.mock_mute_switch_id, STATE_OFF) + + self.mock_state_switch_id = switch.ENTITY_ID_FORMAT.format('state') + self.hass.states.set(self.mock_state_switch_id, STATE_OFF) + + self.config_children_only = \ + {'name': 'test', 'platform': 'universal', + 'children': [media_player.ENTITY_ID_FORMAT.format('mock1'), + media_player.ENTITY_ID_FORMAT.format('mock2')]} + self.config_children_and_attr = \ + {'name': 'test', 'platform': 'universal', + 'children': [media_player.ENTITY_ID_FORMAT.format('mock1'), + media_player.ENTITY_ID_FORMAT.format('mock2')], + 'attributes': { + 'is_volume_muted': self.mock_mute_switch_id, + 'state': self.mock_state_switch_id}} + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_check_config_children_only(self): + """ Check config with only children """ + config_start = copy(self.config_children_only) + del config_start['platform'] + config_start['commands'] = {} + config_start['attributes'] = {} + + response = universal.validate_config(self.config_children_only) + + self.assertTrue(response) + self.assertEqual(config_start, self.config_children_only) + + def test_check_config_children_and_attr(self): + """ Check config with children and attributes """ + config_start = copy(self.config_children_and_attr) + del config_start['platform'] + config_start['commands'] = {} + + response = universal.validate_config(self.config_children_and_attr) + + self.assertTrue(response) + self.assertEqual(config_start, self.config_children_and_attr) + + def test_dependencies(self): + """ test dependencies property """ + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + depend = ump.dependencies + depend.sort() + + check_depend = [media_player.ENTITY_ID_FORMAT.format('mock1'), + media_player.ENTITY_ID_FORMAT.format('mock2'), + self.mock_mute_switch_id, self.mock_state_switch_id] + check_depend.sort() + + self.assertEqual(depend, check_depend) + + def test_master_state(self): + """ test master state property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(None, ump.master_state) + + def test_master_state_with_attrs(self): + """ test master state property """ + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(STATE_OFF, ump.master_state) + + self.hass.states.set(self.mock_state_switch_id, STATE_ON) + + self.assertEqual(STATE_ON, ump.master_state) + + def test_master_state_with_bad_attrs(self): + """ test master state property """ + config = self.config_children_and_attr + config['attributes']['state'] = 'bad.entity_id' + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(STATE_OFF, ump.master_state) + + def test_children(self): + """ test children property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + children = ump.children + + check_children_ids = config['children'] + check_children_ids.sort() + children_ids = list(children.keys()) + children_ids.sort() + self.assertEqual(check_children_ids, children_ids) + + check_children_states = [STATE_OFF, STATE_OFF] + children_states = [val.state for val in children.values()] + self.assertEqual(check_children_states, children_states) + + def test_active_child_state(self): + """ test active child state property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(None, ump.active_child_state) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + self.assertEqual(self.mock_mp_1.entity_id, + ump.active_child_state.entity_id) + + self.mock_mp_2._state = STATE_PLAYING + self.mock_mp_2.update_ha_state() + self.assertEqual(self.mock_mp_1.entity_id, + ump.active_child_state.entity_id) + + self.mock_mp_1._state = STATE_OFF + self.mock_mp_1.update_ha_state() + self.assertEqual(self.mock_mp_2.entity_id, + ump.active_child_state.entity_id) + + def test_name(self): + """ test name property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(config['name'], ump.name) + + def test_state_children_only(self): + """ test media player state with only children """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertTrue(ump.state, STATE_OFF) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + self.assertEqual(STATE_PLAYING, ump.state) + + def test_state_with_children_and_attrs(self): + """ test media player with children and master state """ + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(ump.state, STATE_OFF) + + self.hass.states.set(self.mock_state_switch_id, STATE_ON) + self.assertEqual(ump.state, STATE_ON) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + self.assertEqual(ump.state, STATE_PLAYING) + + self.hass.states.set(self.mock_state_switch_id, STATE_OFF) + self.assertEqual(ump.state, STATE_OFF) + + def test_volume_level(self): + """ test volume level property """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(None, ump.volume_level) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + self.assertEqual(0, ump.volume_level) + + self.mock_mp_1._volume_level = 1 + self.mock_mp_1.update_ha_state() + self.assertEqual(1, ump.volume_level) + + def test_is_volume_muted_children_only(self): + """ test is volume muted property w/ children only """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertFalse(ump.is_volume_muted) + + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + self.assertFalse(ump.is_volume_muted) + + self.mock_mp_1._is_volume_muted = True + self.mock_mp_1.update_ha_state() + self.assertTrue(ump.is_volume_muted) + + def test_is_volume_muted_children_and_attr(self): + """ test is volume muted property w/ children and attrs """ + config = self.config_children_and_attr + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertFalse(ump.is_volume_muted) + + self.hass.states.set(self.mock_mute_switch_id, STATE_ON) + self.assertTrue(ump.is_volume_muted) + + def test_supported_media_commands_children_only(self): + """ test supported media commands with only children """ + config = self.config_children_only + universal.validate_config(config) + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.assertEqual(0, ump.supported_media_commands) + + self.mock_mp_1._supported_media_commands = 512 + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + self.assertEqual(512, ump.supported_media_commands) + + def test_supported_media_commands_children_and_cmds(self): + """ test supported media commands with children and attrs """ + config = self.config_children_and_attr + universal.validate_config(config) + config['commands']['turn_on'] = 'test' + config['commands']['turn_off'] = 'test' + config['commands']['volume_up'] = 'test' + config['commands']['volume_down'] = 'test' + config['commands']['volume_mute'] = 'test' + + ump = universal.UniversalMediaPlayer(self.hass, **config) + + self.mock_mp_1._supported_media_commands = \ + universal.SUPPORT_VOLUME_SET + self.mock_mp_1._state = STATE_PLAYING + self.mock_mp_1.update_ha_state() + + check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ + | universal.SUPPORT_VOLUME_BUTTONS | universal.SUPPORT_VOLUME_MUTE + + self.assertEqual(check_flags, ump.supported_media_commands) From ee4543d7390ab4f663499961403a4df4530e37c9 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 00:59:57 -0500 Subject: [PATCH 55/69] Using call_from_config in Universal Media Player Changed universal media player to use the call_from_config helper to call services specified in the configuration file. This was done to copy what is done in the Alexa and Automation components. --- homeassistant/components/media_player/universal.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 9a770b50d32..32b2640caaf 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -12,6 +12,7 @@ from copy import copy import logging from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.service import call_from_config from homeassistant.const import ( STATE_IDLE, STATE_ON, STATE_OFF, CONF_NAME, @@ -178,11 +179,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): def _override_or_child_service(self, service_name, **service_data): """ calls either a specified or active child's service """ if service_name in self._cmds: - cmd_data = self._cmds[service_name] - domain, service = cmd_data[CONF_SERVICE].split('.', 1) - self.hass.services.call(domain, service, - cmd_data[CONF_SERVICE_DATA], - blocking=True) + call_from_config( + self.hass, self._cmds[service_name], blocking=True) return self._child_service(service_name, **service_data) From 8f3e8d29f0b8135c996776fb25ea0d813b6338c5 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 01:05:02 -0500 Subject: [PATCH 56/69] Renamed SUPPORT_VOLUME_STEP flag in media_player 1) Renamed SUPPORT_VOLUME_BUTTONS to SUPPORT_VOLUME_STEP 2) Removed unused imports from tests. --- homeassistant/components/media_player/__init__.py | 2 +- homeassistant/components/media_player/universal.py | 4 ++-- tests/components/media_player/test_universal.py | 8 ++------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index bdb435e8a73..1b6b9fbfa44 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -72,7 +72,7 @@ SUPPORT_YOUTUBE = 64 SUPPORT_TURN_ON = 128 SUPPORT_TURN_OFF = 256 SUPPORT_PLAY_MEDIA = 512 -SUPPORT_VOLUME_BUTTONS = 1024 +SUPPORT_VOLUME_STEP = 1024 YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 32b2640caaf..8e409a9a840 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.components.media_player import ( MediaPlayerDevice, DOMAIN, - SUPPORT_VOLUME_BUTTONS, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SERVICE_PLAY_MEDIA, SERVICE_YOUTUBE_VIDEO, ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED, @@ -352,7 +352,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN]]): - flags |= SUPPORT_VOLUME_BUTTONS + flags |= SUPPORT_VOLUME_STEP flags &= ~SUPPORT_VOLUME_SET if SERVICE_VOLUME_MUTE in self._cmds and \ diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 4be84b54297..d8c3d5ac695 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -9,14 +9,10 @@ import unittest import homeassistant.core as ha from homeassistant.const import ( - STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED, - SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, - SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) + STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED) import homeassistant.components.switch as switch import homeassistant.components.media_player as media_player import homeassistant.components.media_player.universal as universal -from tests.common import mock_service class MockMediaPlayer(media_player.MediaPlayerDevice): @@ -349,6 +345,6 @@ class TestMediaPlayer(unittest.TestCase): self.mock_mp_1.update_ha_state() check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ - | universal.SUPPORT_VOLUME_BUTTONS | universal.SUPPORT_VOLUME_MUTE + | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE self.assertEqual(check_flags, ump.supported_media_commands) From 85d732a45a14855e2b5ceb5145146d897a4788af Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 01:16:57 -0500 Subject: [PATCH 57/69] Streamlined child state lookups in universal media player 1) Removed children property because it was only being used by one method. 2) Removed option to return state as object from _entity_lkp as it was no longer needed. 3) Used hass.states.get to get entity state objects. 4) Revised test to remove children property. --- .../components/media_player/universal.py | 18 ++++-------------- .../components/media_player/test_universal.py | 18 ------------------ 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 8e409a9a840..02130daa9c8 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -147,7 +147,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): track_state_change(hass, self.dependencies, self.update_state) - def _entity_lkp(self, entity_id=None, state_attr=None, state_as_obj=True): + def _entity_lkp(self, entity_id=None, state_attr=None): """ Looks up an entity state from hass """ if entity_id is None: return @@ -159,15 +159,12 @@ class UniversalMediaPlayer(MediaPlayerDevice): if state_attr: return state_obj.attributes.get(state_attr) - if state_as_obj: - return state_obj return state_obj.state def _override_or_child_attr(self, attr_name): """ returns either the override or the active child for attr_name """ if attr_name in self._attrs: - return self._entity_lkp(*self._attrs[attr_name], - state_as_obj=False) + return self._entity_lkp(*self._attrs[attr_name]) return self._child_attr(attr_name) @@ -205,23 +202,16 @@ class UniversalMediaPlayer(MediaPlayerDevice): def master_state(self): """ gets the master state from entity or none """ if CONF_STATE in self._attrs: - master_state = self._entity_lkp(*self._attrs[CONF_STATE], - state_as_obj=False) + master_state = self._entity_lkp(*self._attrs[CONF_STATE]) return master_state if master_state else STATE_OFF else: return None - @property - def children(self): - """ Gets children and their current states """ - return {child: self._entity_lkp(child) for child in self._children} - @property def active_child_state(self): """ The state of the active child or None """ - children = self.children for child_name in self._children: - child_state = children[child_name] + child_state = self.hass.states.get(child_name) if child_state and child_state.state not in OFF_STATES: return child_state diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index d8c3d5ac695..8775bee3c48 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -184,24 +184,6 @@ class TestMediaPlayer(unittest.TestCase): self.assertEqual(STATE_OFF, ump.master_state) - def test_children(self): - """ test children property """ - config = self.config_children_only - universal.validate_config(config) - - ump = universal.UniversalMediaPlayer(self.hass, **config) - children = ump.children - - check_children_ids = config['children'] - check_children_ids.sort() - children_ids = list(children.keys()) - children_ids.sort() - self.assertEqual(check_children_ids, children_ids) - - check_children_states = [STATE_OFF, STATE_OFF] - children_states = [val.state for val in children.values()] - self.assertEqual(check_children_states, children_states) - def test_active_child_state(self): """ test active child state property """ config = self.config_children_only From a8d5b0e5ecdbbd99a680975361524011e3c5683e Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 01:34:25 -0500 Subject: [PATCH 58/69] Made universal media player cache active player Revised universal media player to cache the active player when updating the state when any of the children change. Revised tests to accommodate this change. --- .../components/media_player/universal.py | 14 +++++++--- .../components/media_player/test_universal.py | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 02130daa9c8..cf49eee8918 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -144,6 +144,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): self._children = children self._cmds = commands self._attrs = attributes + self._child_state = None track_state_change(hass, self.dependencies, self.update_state) @@ -207,13 +208,19 @@ class UniversalMediaPlayer(MediaPlayerDevice): else: return None - @property - def active_child_state(self): + def _cache_active_child_state(self): """ The state of the active child or None """ for child_name in self._children: child_state = self.hass.states.get(child_name) if child_state and child_state.state not in OFF_STATES: - return child_state + self._child_state = child_state + return + self._child_state = None + + @property + def active_child_state(self): + """ the state of the active child or none """ + return self._child_state @property def name(self): @@ -420,4 +427,5 @@ class UniversalMediaPlayer(MediaPlayerDevice): def update_state(self, *_): """ event to trigger a state update in HA """ + self._cache_active_child_state() self.update_ha_state() diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 8775bee3c48..d9ebc43983f 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -190,21 +190,26 @@ class TestMediaPlayer(unittest.TestCase): universal.validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update_state() self.assertEqual(None, ump.active_child_state) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() + ump.update_state() self.assertEqual(self.mock_mp_1.entity_id, ump.active_child_state.entity_id) self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.update_ha_state() + ump.update_state() self.assertEqual(self.mock_mp_1.entity_id, ump.active_child_state.entity_id) self.mock_mp_1._state = STATE_OFF self.mock_mp_1.update_ha_state() + ump.update_state() self.assertEqual(self.mock_mp_2.entity_id, ump.active_child_state.entity_id) @@ -223,11 +228,14 @@ class TestMediaPlayer(unittest.TestCase): universal.validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update_state() self.assertTrue(ump.state, STATE_OFF) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() + ump.update_state() self.assertEqual(STATE_PLAYING, ump.state) def test_state_with_children_and_attrs(self): @@ -236,17 +244,22 @@ class TestMediaPlayer(unittest.TestCase): universal.validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update_state() self.assertEqual(ump.state, STATE_OFF) self.hass.states.set(self.mock_state_switch_id, STATE_ON) + ump.update_state() self.assertEqual(ump.state, STATE_ON) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() + ump.update_state() self.assertEqual(ump.state, STATE_PLAYING) self.hass.states.set(self.mock_state_switch_id, STATE_OFF) + ump.update_state() self.assertEqual(ump.state, STATE_OFF) def test_volume_level(self): @@ -255,15 +268,19 @@ class TestMediaPlayer(unittest.TestCase): universal.validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update_state() self.assertEqual(None, ump.volume_level) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() + ump.update_state() self.assertEqual(0, ump.volume_level) self.mock_mp_1._volume_level = 1 self.mock_mp_1.update_ha_state() + ump.update_state() self.assertEqual(1, ump.volume_level) def test_is_volume_muted_children_only(self): @@ -272,15 +289,19 @@ class TestMediaPlayer(unittest.TestCase): universal.validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update_state() self.assertFalse(ump.is_volume_muted) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() + ump.update_state() self.assertFalse(ump.is_volume_muted) self.mock_mp_1._is_volume_muted = True self.mock_mp_1.update_ha_state() + ump.update_state() self.assertTrue(ump.is_volume_muted) def test_is_volume_muted_children_and_attr(self): @@ -301,12 +322,15 @@ class TestMediaPlayer(unittest.TestCase): universal.validate_config(config) ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update_state() self.assertEqual(0, ump.supported_media_commands) self.mock_mp_1._supported_media_commands = 512 self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() + ump.update_state() self.assertEqual(512, ump.supported_media_commands) def test_supported_media_commands_children_and_cmds(self): @@ -320,11 +344,14 @@ class TestMediaPlayer(unittest.TestCase): config['commands']['volume_mute'] = 'test' ump = universal.UniversalMediaPlayer(self.hass, **config) + ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) + ump.update_state() self.mock_mp_1._supported_media_commands = \ universal.SUPPORT_VOLUME_SET self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() + ump.update_state() check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE From d829497c3de3324a59a06a90dfe3ec6cb80c435a Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 01:39:02 -0500 Subject: [PATCH 59/69] Changed universal media player to keep service attrs in dict Revised universal media player to keep service data in a dictionary rather than passing it around as magic parameters. --- .../components/media_player/universal.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index cf49eee8918..21aba691b33 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -174,16 +174,16 @@ class UniversalMediaPlayer(MediaPlayerDevice): active_child = self.active_child_state return active_child.attributes.get(attr_name) if active_child else None - def _override_or_child_service(self, service_name, **service_data): + def _override_or_child_service(self, service_name, service_data): """ calls either a specified or active child's service """ if service_name in self._cmds: call_from_config( self.hass, self._cmds[service_name], blocking=True) return - self._child_service(service_name, **service_data) + self._child_service(service_name, service_data) - def _child_service(self, service_name, **service_data): + def _child_service(self, service_name, service_data): """ calls the active child's specified service """ active_child = self.active_child_state service_data[ATTR_ENTITY_ID] = active_child.entity_id @@ -376,12 +376,12 @@ class UniversalMediaPlayer(MediaPlayerDevice): def mute_volume(self, is_volume_muted): """ mute the volume. """ data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted} - self._override_or_child_service(SERVICE_VOLUME_MUTE, **data) + self._override_or_child_service(SERVICE_VOLUME_MUTE, data) def set_volume_level(self, volume_level): """ set volume level, range 0..1. """ data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} - self._child_service(SERVICE_VOLUME_SET, **data) + self._child_service(SERVICE_VOLUME_SET, data) def media_play(self): """ Send play commmand. """ @@ -402,16 +402,17 @@ class UniversalMediaPlayer(MediaPlayerDevice): def media_seek(self, position): """ Send seek command. """ data = {ATTR_MEDIA_SEEK_POSITION: position} - self._child_service(SERVICE_MEDIA_SEEK, **data) + self._child_service(SERVICE_MEDIA_SEEK, data) def play_youtube(self, media_id): """ Plays a YouTube media. """ - self._child_service(SERVICE_YOUTUBE_VIDEO, media_id=media_id) + data = {'media_id': media_id} + self._child_service(SERVICE_YOUTUBE_VIDEO, data) def play_media(self, media_type, media_id): """ Plays a piece of media. """ - self._child_service( - SERVICE_PLAY_MEDIA, media_type=media_type, media_id=media_id) + data = {'media_type': media_type, 'media_id': media_id} + self._child_service(SERVICE_PLAY_MEDIA, data) def volume_up(self): """ volume_up media player. """ From 270a998e3cbf8731b884a2816a323ecac5d83974 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 01:50:01 -0500 Subject: [PATCH 60/69] Merged service calling method in universal media player --- .../components/media_player/universal.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 21aba691b33..953efc08ee6 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -174,17 +174,17 @@ class UniversalMediaPlayer(MediaPlayerDevice): active_child = self.active_child_state return active_child.attributes.get(attr_name) if active_child else None - def _override_or_child_service(self, service_name, service_data): + def _call_service(self, service_name, service_data=None, + allow_override=False): """ calls either a specified or active child's service """ - if service_name in self._cmds: + if allow_override and service_name in self._cmds: call_from_config( self.hass, self._cmds[service_name], blocking=True) return - self._child_service(service_name, service_data) + if service_data is None: + service_data = {} - def _child_service(self, service_name, service_data): - """ calls the active child's specified service """ active_child = self.active_child_state service_data[ATTR_ENTITY_ID] = active_child.entity_id @@ -367,64 +367,64 @@ class UniversalMediaPlayer(MediaPlayerDevice): def turn_on(self): """ turn the media player on. """ - self._override_or_child_service(SERVICE_TURN_ON) + self._call_service(SERVICE_TURN_ON, allow_override=True) def turn_off(self): """ turn the media player off. """ - self._override_or_child_service(SERVICE_TURN_OFF) + self._call_service(SERVICE_TURN_OFF, allow_override=True) def mute_volume(self, is_volume_muted): """ mute the volume. """ data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted} - self._override_or_child_service(SERVICE_VOLUME_MUTE, data) + self._call_service(SERVICE_VOLUME_MUTE, data, allow_override=True) def set_volume_level(self, volume_level): """ set volume level, range 0..1. """ data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level} - self._child_service(SERVICE_VOLUME_SET, data) + self._call_service(SERVICE_VOLUME_SET, data) def media_play(self): """ Send play commmand. """ - self._child_service(SERVICE_MEDIA_PLAY) + self._call_service(SERVICE_MEDIA_PLAY) def media_pause(self): """ Send pause command. """ - self._child_service(SERVICE_MEDIA_PAUSE) + self._call_service(SERVICE_MEDIA_PAUSE) def media_previous_track(self): """ Send previous track command. """ - self._child_service(SERVICE_MEDIA_PREVIOUS_TRACK) + self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK) def media_next_track(self): """ Send next track command. """ - self._child_service(SERVICE_MEDIA_NEXT_TRACK) + self._call_service(SERVICE_MEDIA_NEXT_TRACK) def media_seek(self, position): """ Send seek command. """ data = {ATTR_MEDIA_SEEK_POSITION: position} - self._child_service(SERVICE_MEDIA_SEEK, data) + self._call_service(SERVICE_MEDIA_SEEK, data) def play_youtube(self, media_id): """ Plays a YouTube media. """ data = {'media_id': media_id} - self._child_service(SERVICE_YOUTUBE_VIDEO, data) + self._call_service(SERVICE_YOUTUBE_VIDEO, data) def play_media(self, media_type, media_id): """ Plays a piece of media. """ data = {'media_type': media_type, 'media_id': media_id} - self._child_service(SERVICE_PLAY_MEDIA, data) + self._call_service(SERVICE_PLAY_MEDIA, data) def volume_up(self): """ volume_up media player. """ - self._override_or_child_service(SERVICE_VOLUME_UP) + self._call_service(SERVICE_VOLUME_UP, allow_override=True) def volume_down(self): """ volume_down media player. """ - self._override_or_child_service(SERVICE_VOLUME_DOWN) + self._call_service(SERVICE_VOLUME_DOWN, allow_override=True) def media_play_pause(self): """ media_play_pause media player. """ - self._child_service(SERVICE_MEDIA_PLAY_PAUSE) + self._call_service(SERVICE_MEDIA_PLAY_PAUSE) def update_state(self, *_): """ event to trigger a state update in HA """ From a1abab8ced5c7f5c917285844d622a11c852eb0d Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 01:54:22 -0500 Subject: [PATCH 61/69] Set universal media player to force refresh when updating HA --- homeassistant/components/media_player/universal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 953efc08ee6..09687828399 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -429,4 +429,4 @@ class UniversalMediaPlayer(MediaPlayerDevice): def update_state(self, *_): """ event to trigger a state update in HA """ self._cache_active_child_state() - self.update_ha_state() + self.update_ha_state(True) From 769f5aafb7de83b966676fa28d3f56f03079ae3e Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 01:55:42 -0500 Subject: [PATCH 62/69] Added should_poll = False to universal media player --- homeassistant/components/media_player/universal.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 09687828399..22137170165 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -191,6 +191,11 @@ class UniversalMediaPlayer(MediaPlayerDevice): self.hass.services.call(DOMAIN, service_name, service_data, blocking=True) + @property + def should_poll(self): + """ Indicates whether HA should poll for updates """ + return False + @property def dependencies(self): """ List of entity ids of entities that the mp depends on for state """ From 12da6f531e20ee6f2a80ccc5b3f95b5c708421d7 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 20:45:53 -0500 Subject: [PATCH 63/69] Removed property from universal media player The active_child_state property was unnecessary as it was not being referenced outside the class. This commit removes it and updates the tests accordingly. --- homeassistant/components/media_player/universal.py | 13 ++++--------- tests/components/media_player/test_universal.py | 8 ++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 22137170165..9bf7dff4d4f 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -171,7 +171,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): def _child_attr(self, attr_name): """ returns the active child's attr """ - active_child = self.active_child_state + active_child = self._child_state return active_child.attributes.get(attr_name) if active_child else None def _call_service(self, service_name, service_data=None, @@ -185,7 +185,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): if service_data is None: service_data = {} - active_child = self.active_child_state + active_child = self._child_state service_data[ATTR_ENTITY_ID] = active_child.entity_id self.hass.services.call(DOMAIN, service_name, service_data, @@ -222,11 +222,6 @@ class UniversalMediaPlayer(MediaPlayerDevice): return self._child_state = None - @property - def active_child_state(self): - """ the state of the active child or none """ - return self._child_state - @property def name(self): """ name of universal player """ @@ -245,7 +240,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): if master_state == STATE_OFF: return STATE_OFF - active_child = self.active_child_state + active_child = self._child_state if active_child: return active_child.state @@ -366,7 +361,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): @property def device_state_attributes(self): """ Extra attributes a device wants to expose. """ - active_child = self.active_child_state + active_child = self._child_state return {ATTR_ACTIVE_CHILD: active_child.entity_id} \ if active_child else {} diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index d9ebc43983f..910ffca2498 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -193,25 +193,25 @@ class TestMediaPlayer(unittest.TestCase): ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) ump.update_state() - self.assertEqual(None, ump.active_child_state) + self.assertEqual(None, ump._child_state) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() ump.update_state() self.assertEqual(self.mock_mp_1.entity_id, - ump.active_child_state.entity_id) + ump._child_state.entity_id) self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.update_ha_state() ump.update_state() self.assertEqual(self.mock_mp_1.entity_id, - ump.active_child_state.entity_id) + ump._child_state.entity_id) self.mock_mp_1._state = STATE_OFF self.mock_mp_1.update_ha_state() ump.update_state() self.assertEqual(self.mock_mp_2.entity_id, - ump.active_child_state.entity_id) + ump._child_state.entity_id) def test_name(self): """ test name property """ From 07953fb7e3cb69758b9baddf876ec24768dc83b0 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 20:50:28 -0500 Subject: [PATCH 64/69] Removed dependencies property from universal media player The dependencies property was only being called once by the __init__ method so it was removed and the code was moved to the __init__ method. The tests were updated to reflect this. --- .../components/media_player/universal.py | 14 +++++--------- tests/components/media_player/test_universal.py | 17 ----------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 9bf7dff4d4f..32b6cc1a990 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -146,7 +146,11 @@ class UniversalMediaPlayer(MediaPlayerDevice): self._attrs = attributes self._child_state = None - track_state_change(hass, self.dependencies, self.update_state) + depend = copy(children) + for entity in attributes.values(): + depend.append(entity[0]) + + track_state_change(hass, depend, self.update_state) def _entity_lkp(self, entity_id=None, state_attr=None): """ Looks up an entity state from hass """ @@ -196,14 +200,6 @@ class UniversalMediaPlayer(MediaPlayerDevice): """ Indicates whether HA should poll for updates """ return False - @property - def dependencies(self): - """ List of entity ids of entities that the mp depends on for state """ - depend = copy(self._children) - for entity in self._attrs.values(): - depend.append(entity[0]) - return depend - @property def master_state(self): """ gets the master state from entity or none """ diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 910ffca2498..511451d53a0 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -135,23 +135,6 @@ class TestMediaPlayer(unittest.TestCase): self.assertTrue(response) self.assertEqual(config_start, self.config_children_and_attr) - def test_dependencies(self): - """ test dependencies property """ - config = self.config_children_and_attr - universal.validate_config(config) - - ump = universal.UniversalMediaPlayer(self.hass, **config) - - depend = ump.dependencies - depend.sort() - - check_depend = [media_player.ENTITY_ID_FORMAT.format('mock1'), - media_player.ENTITY_ID_FORMAT.format('mock2'), - self.mock_mute_switch_id, self.mock_state_switch_id] - check_depend.sort() - - self.assertEqual(depend, check_depend) - def test_master_state(self): """ test master state property """ config = self.config_children_only From a84429538b1a40e5d01e2eb5f16d6836c205d93c Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 21:02:39 -0500 Subject: [PATCH 65/69] Fixed attribute configuration handling in universal media player Forced all parsed attribute configurations to be of length 2. Removed entity_id=None option from entity lookups. Explicitly passed entity lookup information to _entity_lkp. --- .../components/media_player/universal.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 32b6cc1a990..8bf689d3fd8 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -130,7 +130,10 @@ def validate_attributes(config): config[CONF_ATTRS] = {} for key, val in config[CONF_ATTRS].items(): - config[CONF_ATTRS][key] = val.split('|', 1) + attr = val.split('|', 1) + if len(attr) == 1: + attr.append(None) + config[CONF_ATTRS][key] = attr class UniversalMediaPlayer(MediaPlayerDevice): @@ -152,11 +155,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): track_state_change(hass, depend, self.update_state) - def _entity_lkp(self, entity_id=None, state_attr=None): + def _entity_lkp(self, entity_id, state_attr=None): """ Looks up an entity state from hass """ - if entity_id is None: - return - state_obj = self.hass.states.get(entity_id) if state_obj is None: @@ -169,7 +169,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): def _override_or_child_attr(self, attr_name): """ returns either the override or the active child for attr_name """ if attr_name in self._attrs: - return self._entity_lkp(*self._attrs[attr_name]) + return self._entity_lkp(self._attrs[attr_name][0], + self._attrs[attr_name][1]) return self._child_attr(attr_name) @@ -204,7 +205,8 @@ class UniversalMediaPlayer(MediaPlayerDevice): def master_state(self): """ gets the master state from entity or none """ if CONF_STATE in self._attrs: - master_state = self._entity_lkp(*self._attrs[CONF_STATE]) + master_state = self._entity_lkp(self._attrs[CONF_STATE][0], + self._attrs[CONF_STATE][1]) return master_state if master_state else STATE_OFF else: return None From 57c0f961185b7fbd7ae2cb303851c6c448370f86 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 21:21:41 -0500 Subject: [PATCH 66/69] Renamed update_state to update in universal media player Renamed update_state method in universal media player to update so that it would be called by HA when the state was being published. Moved the update_ha_state to a function inside of __init__. Updated the tests accordingly. --- .../components/media_player/universal.py | 16 ++++++-- .../components/media_player/test_universal.py | 40 +++++++++---------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 8bf689d3fd8..09bb12ec332 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -149,11 +149,15 @@ class UniversalMediaPlayer(MediaPlayerDevice): self._attrs = attributes self._child_state = None + def on_dependency_update(*_): + """ update ha state when dependencies update """ + self.update_ha_state(True) + depend = copy(children) for entity in attributes.values(): depend.append(entity[0]) - track_state_change(hass, depend, self.update_state) + track_state_change(hass, depend, on_dependency_update) def _entity_lkp(self, entity_id, state_attr=None): """ Looks up an entity state from hass """ @@ -424,7 +428,11 @@ class UniversalMediaPlayer(MediaPlayerDevice): """ media_play_pause media player. """ self._call_service(SERVICE_MEDIA_PLAY_PAUSE) - def update_state(self, *_): + def update(self): """ event to trigger a state update in HA """ - self._cache_active_child_state() - self.update_ha_state(True) + for child_name in self._children: + child_state = self.hass.states.get(child_name) + if child_state and child_state.state not in OFF_STATES: + self._child_state = child_state + return + self._child_state = None diff --git a/tests/components/media_player/test_universal.py b/tests/components/media_player/test_universal.py index 511451d53a0..eca863b935e 100644 --- a/tests/components/media_player/test_universal.py +++ b/tests/components/media_player/test_universal.py @@ -174,25 +174,25 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update_state() + ump.update() self.assertEqual(None, ump._child_state) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(self.mock_mp_1.entity_id, ump._child_state.entity_id) self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(self.mock_mp_1.entity_id, ump._child_state.entity_id) self.mock_mp_1._state = STATE_OFF self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(self.mock_mp_2.entity_id, ump._child_state.entity_id) @@ -212,13 +212,13 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update_state() + ump.update() self.assertTrue(ump.state, STATE_OFF) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(STATE_PLAYING, ump.state) def test_state_with_children_and_attrs(self): @@ -228,21 +228,21 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update_state() + ump.update() self.assertEqual(ump.state, STATE_OFF) self.hass.states.set(self.mock_state_switch_id, STATE_ON) - ump.update_state() + ump.update() self.assertEqual(ump.state, STATE_ON) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(ump.state, STATE_PLAYING) self.hass.states.set(self.mock_state_switch_id, STATE_OFF) - ump.update_state() + ump.update() self.assertEqual(ump.state, STATE_OFF) def test_volume_level(self): @@ -252,18 +252,18 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update_state() + ump.update() self.assertEqual(None, ump.volume_level) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(0, ump.volume_level) self.mock_mp_1._volume_level = 1 self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(1, ump.volume_level) def test_is_volume_muted_children_only(self): @@ -273,18 +273,18 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update_state() + ump.update() self.assertFalse(ump.is_volume_muted) self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertFalse(ump.is_volume_muted) self.mock_mp_1._is_volume_muted = True self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertTrue(ump.is_volume_muted) def test_is_volume_muted_children_and_attr(self): @@ -306,14 +306,14 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update_state() + ump.update() self.assertEqual(0, ump.supported_media_commands) self.mock_mp_1._supported_media_commands = 512 self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() self.assertEqual(512, ump.supported_media_commands) def test_supported_media_commands_children_and_cmds(self): @@ -328,13 +328,13 @@ class TestMediaPlayer(unittest.TestCase): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name']) - ump.update_state() + ump.update() self.mock_mp_1._supported_media_commands = \ universal.SUPPORT_VOLUME_SET self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.update_ha_state() - ump.update_state() + ump.update() check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \ | universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE From a0ddda4bc6ba1c5cb5f9563824ffeb4d0b3c6087 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Tue, 12 Jan 2016 22:06:42 -0500 Subject: [PATCH 67/69] Updated frontend to newest commit --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 1161 +++++++++-------- .../www_static/home-assistant-polymer | 2 +- 3 files changed, 594 insertions(+), 571 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 2ded702dc6b..67454d11974 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 = "72a8220d0db0f7f3702228cd556b8c40" +VERSION = "63d38b69fc6582e75f892abc140a893a" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index edc9635dbf4..86e5daca0aa 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -275,7 +275,7 @@ var n=this._rootDataHost;return n?n._scopeElementClass(t,e):void 0},stamp:functi html /deep/ .fixed-right { top: 0; right: 0; - bottom: 0; + botttom: 0; } html /deep/ .fixed-bottom { @@ -286,7 +286,7 @@ var n=this._rootDataHost;return n?n._scopeElementClass(t,e):void 0},stamp:functi html /deep/ .fixed-left { top: 0; - bottom: 0; + botttom: 0; left: 0; }
\ 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 68/69] 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 2a377a6125753e649308f070eccab6e8a2a693a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 23:59:15 -0800 Subject: [PATCH 69/69] 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)