From 45fe37a301e4932f0ba2859e8263642b9b7bc5ed Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Wed, 4 Nov 2015 04:53:59 +0100 Subject: [PATCH 001/267] 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 d9fc2a8bf6673c6aecf1fb617938878a0f214c7d Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Mon, 30 Nov 2015 17:00:28 +0100 Subject: [PATCH 002/267] initial version of yr.no weather component --- homeassistant/components/sensor/yr.py | 179 ++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 homeassistant/components/sensor/yr.py diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py new file mode 100644 index 00000000000..f64f16c3f77 --- /dev/null +++ b/homeassistant/components/sensor/yr.py @@ -0,0 +1,179 @@ +""" +homeassistant.components.sensor.yr +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Yr.no weather service. + +Configuration: + +Will show a symbol for the current weather as default: +sensor: + platform: yr + +Will show temperatue and wind direction: +sensor: + platform: yr + monitored_conditions: + - temperature + - windDirection + +""" +import logging +import datetime +import urllib.request +import xmltodict + +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +# Sensor types are defined like so: +SENSOR_TYPES = { + 'symbol': ['Symbol', ''], + 'precipitation': ['Condition', ''], + 'temperature': ['Temperature', '°C'], + 'windSpeed': ['Wind speed', 'm/s'], + 'pressure': ['Pressure', 'hPa'], + 'windDirection': ['Wind direction', '°'], + 'humidity': ['Humidity', ''], + 'fog': ['Fog', '%'], + 'cloudiness': ['Cloudiness', '%'], + 'lowClouds': ['Low clouds', '%'], + 'mediumClouds': ['Medium clouds', '%'], + 'highClouds': ['High clouds', '%'], + 'dewpointTemperature': ['Dewpoint temperature', '°C'], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the yr.no sensor. """ + + if None in (hass.config.latitude, hass.config.longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return False + + from astral import Location, GoogleGeocoder + + location = Location(('', '', hass.config.latitude, hass.config.longitude, + hass.config.time_zone, 0)) + + google = GoogleGeocoder() + try: + google._get_elevation(location) # pylint: disable=protected-access + _LOGGER.info( + 'Retrieved elevation from Google: %s', location.elevation) + elevation = location.elevation + except urllib.error.URLError: + # If no internet connection available etc. + elevation = 0 + + coordinates = dict(lat=hass.config.latitude, + lon=hass.config.longitude, msl=elevation) + + dev = [] + if 'monitored_conditions' in config: + for variable in config['monitored_conditions']: + if variable not in SENSOR_TYPES: + _LOGGER.error('Sensor type: "%s" does not exist', variable) + else: + dev.append(YrSensor(coordinates, variable)) + + if len(dev) == 0: + dev.append(YrSensor(coordinates, "symbol")) + add_devices(dev) + + +# pylint: disable=too-many-instance-attributes +class YrSensor(Entity): + """ Implements an Yr.no sensor. """ + + def __init__(self, coordinates, sensor_type): + self.client_name = '' + self._name = SENSOR_TYPES[sensor_type][0] + self.type = sensor_type + self._state = None + self._weather_data = None + + self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._nextrun = datetime.datetime.fromtimestamp(0) + self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ + 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) + + self.update() + + @property + def name(self): + return '{} {}'.format(self.client_name, self._name) + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def state_attributes(self): + """ Returns state attributes. """ + data = {} + data[''] = "Weather forecast from yr.no, delivered by the"\ + " Norwegian Meteorological Institute and the NRK" + if self.type == 'symbol': + symbol_nr = self._state + data[ATTR_ENTITY_PICTURE] = "http://api.met.no/weatherapi/weathericon/1.1/" \ + "?symbol=" + str(symbol_nr) + \ + ";content_type=image/png" + return data + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self._unit_of_measurement + + # pylint: disable=too-many-branches + def update(self): + """ Gets the latest data from yr.no and updates the states. """ + if datetime.datetime.now() > self._nextrun: + try: + response = urllib.request.urlopen(self._url) + except urllib.error.URLError: + return + if response.status != 200: + return + _data = response.read().decode('utf-8') + self._weather_data = xmltodict.parse(_data)['weatherdata'] + model = self._weather_data['meta']['model'] + if '@nextrun' not in model: + model = model[0] + self._nextrun = datetime.datetime.strptime(model['@nextrun'], + "%Y-%m-%dT%H:%M:%SZ") + time_data = self._weather_data['product']['time'] + + for k in range(len(self._weather_data['product']['time'])): + temp_data = time_data[k]['location'] + if self.type in temp_data: + if self.type == 'precipitation': + self._state = temp_data[self.type]['@value'] + elif self.type == 'temperature': + self._state = temp_data[self.type]['@value'] + elif self.type == 'windSpeed': + self._state = temp_data[self.type]['@mps'] + elif self.type == 'pressure': + self._state = temp_data[self.type]['@value'] + elif self.type == 'windDirection': + self._state = float(temp_data[self.type]['@deg']) + elif self.type == 'humidity': + self._state = temp_data[self.type]['@value'] + elif self.type == 'fog': + self._state = temp_data[self.type]['@percent'] + elif self.type == 'cloudiness': + self._state = temp_data[self.type]['@percent'] + elif self.type == 'lowClouds': + self._state = temp_data[self.type]['@percent'] + elif self.type == 'mediumClouds': + self._state = temp_data[self.type]['@percent'] + elif self.type == 'highClouds': + self._state = temp_data[self.type]['@percent'] + elif self.type == 'dewpointTemperature': + self._state = temp_data[self.type]['@value'] + elif self.type == 'symbol': + self._state = temp_data[self.type]['@number'] + return From f912daf4b21265d289a68210ab492007863cfe79 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Tue, 1 Dec 2015 12:46:08 +0100 Subject: [PATCH 003/267] Updated yr sensor --- homeassistant/components/sensor/yr.py | 72 ++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index f64f16c3f77..98102fdb565 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -16,6 +16,24 @@ sensor: - temperature - windDirection +Will show all available sensors: +sensor: + platform: yr + monitored_conditions: + - temperature + - symbol + - precipitation + - windSpeed + - pressure + - windDirection + - humidity + - fog + - cloudiness + - lowClouds + - mediumClouds + - highClouds + - dewpointTemperature + """ import logging import datetime @@ -93,9 +111,10 @@ class YrSensor(Entity): self.type = sensor_type self._state = None self._weather_data = None - + self._info = '' self._unit_of_measurement = SENSOR_TYPES[self.type][1] self._nextrun = datetime.datetime.fromtimestamp(0) + self._update = datetime.datetime.fromtimestamp(0) self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) @@ -114,7 +133,7 @@ class YrSensor(Entity): def state_attributes(self): """ Returns state attributes. """ data = {} - data[''] = "Weather forecast from yr.no, delivered by the"\ + data[''] = self._info + "Weather forecast from yr.no, delivered by the"\ " Norwegian Meteorological Institute and the NRK" if self.type == 'symbol': symbol_nr = self._state @@ -128,52 +147,81 @@ class YrSensor(Entity): """ Unit of measurement of this entity, if any. """ return self._unit_of_measurement - # pylint: disable=too-many-branches + @property + def should_poll(self): + """ Return True if entity has to be polled for state. """ + return True + + # pylint: disable=too-many-branches, too-many-return-statements def update(self): """ Gets the latest data from yr.no and updates the states. """ - if datetime.datetime.now() > self._nextrun: + now = datetime.datetime.now() + if now > self._nextrun: try: response = urllib.request.urlopen(self._url) except urllib.error.URLError: return if response.status != 200: return - _data = response.read().decode('utf-8') - self._weather_data = xmltodict.parse(_data)['weatherdata'] + data = response.read().decode('utf-8') + self._weather_data = xmltodict.parse(data)['weatherdata'] model = self._weather_data['meta']['model'] if '@nextrun' not in model: model = model[0] self._nextrun = datetime.datetime.strptime(model['@nextrun'], "%Y-%m-%dT%H:%M:%SZ") + + if now > self._update: time_data = self._weather_data['product']['time'] - for k in range(len(self._weather_data['product']['time'])): + # pylint: disable=consider-using-enumerate + for k in range(len(time_data)): + valid_from = datetime.datetime.strptime(time_data[k]['@from'], + "%Y-%m-%dT%H:%M:%SZ") + valid_to = datetime.datetime.strptime(time_data[k]['@to'], + "%Y-%m-%dT%H:%M:%SZ") + self._update = valid_to + self._info = "Forecast between " + time_data[k]['@from'] \ + + " and " + time_data[k]['@to'] + ". " + temp_data = time_data[k]['location'] - if self.type in temp_data: - if self.type == 'precipitation': + if self.type in temp_data and now < valid_to: + if self.type == 'precipitation' and valid_from < now: self._state = temp_data[self.type]['@value'] + return + elif self.type == 'symbol' and valid_from < now: + self._state = temp_data[self.type]['@number'] + return elif self.type == 'temperature': self._state = temp_data[self.type]['@value'] + return elif self.type == 'windSpeed': self._state = temp_data[self.type]['@mps'] + return elif self.type == 'pressure': self._state = temp_data[self.type]['@value'] + return elif self.type == 'windDirection': self._state = float(temp_data[self.type]['@deg']) + return elif self.type == 'humidity': self._state = temp_data[self.type]['@value'] + return elif self.type == 'fog': self._state = temp_data[self.type]['@percent'] + return elif self.type == 'cloudiness': self._state = temp_data[self.type]['@percent'] + return elif self.type == 'lowClouds': self._state = temp_data[self.type]['@percent'] + return elif self.type == 'mediumClouds': self._state = temp_data[self.type]['@percent'] + return elif self.type == 'highClouds': self._state = temp_data[self.type]['@percent'] + return elif self.type == 'dewpointTemperature': self._state = temp_data[self.type]['@value'] - elif self.type == 'symbol': - self._state = temp_data[self.type]['@number'] - return + return From 72d7e6e9ddb0597415c43ed75a705a46249f304c Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Tue, 1 Dec 2015 12:57:08 +0100 Subject: [PATCH 004/267] Added requirements to yr sensor --- homeassistant/components/sensor/yr.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 98102fdb565..d5fa9490064 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -45,6 +45,9 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['xmltodict'] + # Sensor types are defined like so: SENSOR_TYPES = { 'symbol': ['Symbol', ''], From 2dc9bc98f787e38f3daa3c3d4b4077e9753a2358 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Tue, 1 Dec 2015 13:24:03 +0100 Subject: [PATCH 005/267] Tests for yr sensor --- tests/components/sensor/test_yr.py | 72 ++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/components/sensor/test_yr.py diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py new file mode 100644 index 00000000000..971d7890454 --- /dev/null +++ b/tests/components/sensor/test_yr.py @@ -0,0 +1,72 @@ +""" +tests.components.sensor.test_yr +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests Yr sensor. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.sensor as sensor + + + +class TestSensorYr(unittest.TestCase): + """ Test the Yr sensor. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + latitude = 32.87336 + longitude = 117.22743 + + # Compare it with the real data + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_default_setup(self): + self.assertTrue(sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + } + })) + state = self.hass.states.get('sensor.yr_symbol') + + self.assertTrue(state.state.isnumeric()) + self.assertEqual(None, + state.attributes.get('unit_of_measurement')) + + + def test_default_setup(self): + self.assertTrue(sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'monitored_conditions': {'pressure', 'windDirection', 'humidity', 'fog', 'windSpeed'} + } + })) + state = self.hass.states.get('sensor.yr_symbol') + self.assertEqual(None,state) + + state = self.hass.states.get('sensor.yr_pressure') + self.assertEqual('hPa', + state.attributes.get('unit_of_measurement')) + + state = self.hass.states.get('sensor.yr_wind_direction') + self.assertEqual('°', + state.attributes.get('unit_of_measurement')) + + state = self.hass.states.get('sensor.yr_humidity') + self.assertEqual(None, + state.attributes.get('unit_of_measurement')) + + state = self.hass.states.get('sensor.yr_fog') + self.assertEqual('%', + state.attributes.get('unit_of_measurement')) + + state = self.hass.states.get('sensor.yr_wind_speed') + self.assertEqual('m/s', + state.attributes.get('unit_of_measurement')) + From 2872c89f0cbbb874199fa4b4e215e3523496c3ec Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Tue, 1 Dec 2015 13:24:32 +0100 Subject: [PATCH 006/267] Fix in yr sensor --- homeassistant/components/sensor/yr.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index d5fa9490064..803fd60800d 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -38,7 +38,6 @@ sensor: import logging import datetime import urllib.request -import xmltodict from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import Entity @@ -46,7 +45,8 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['xmltodict'] +REQUIREMENTS = ['xmltodict', 'astral==0.8.1'] + # Sensor types are defined like so: SENSOR_TYPES = { @@ -74,7 +74,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False from astral import Location, GoogleGeocoder - location = Location(('', '', hass.config.latitude, hass.config.longitude, hass.config.time_zone, 0)) @@ -109,7 +108,7 @@ class YrSensor(Entity): """ Implements an Yr.no sensor. """ def __init__(self, coordinates, sensor_type): - self.client_name = '' + self.client_name = 'yr' self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type self._state = None @@ -167,6 +166,7 @@ class YrSensor(Entity): if response.status != 200: return data = response.read().decode('utf-8') + import xmltodict self._weather_data = xmltodict.parse(data)['weatherdata'] model = self._weather_data['meta']['model'] if '@nextrun' not in model: From ff15fea9f80283e34564357004b6e7d17ef07475 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Tue, 1 Dec 2015 13:31:55 +0100 Subject: [PATCH 007/267] Added units to sensor yr --- homeassistant/components/sensor/yr.py | 4 ++-- tests/components/sensor/test_yr.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 803fd60800d..c2b0e138857 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -51,12 +51,12 @@ REQUIREMENTS = ['xmltodict', 'astral==0.8.1'] # Sensor types are defined like so: SENSOR_TYPES = { 'symbol': ['Symbol', ''], - 'precipitation': ['Condition', ''], + 'precipitation': ['Condition', 'mm'], 'temperature': ['Temperature', '°C'], 'windSpeed': ['Wind speed', 'm/s'], 'pressure': ['Pressure', 'hPa'], 'windDirection': ['Wind direction', '°'], - 'humidity': ['Humidity', ''], + 'humidity': ['Humidity', '%'], 'fog': ['Fog', '%'], 'cloudiness': ['Cloudiness', '%'], 'lowClouds': ['Low clouds', '%'], diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 971d7890454..2f8d4ea838f 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -59,7 +59,7 @@ class TestSensorYr(unittest.TestCase): state.attributes.get('unit_of_measurement')) state = self.hass.states.get('sensor.yr_humidity') - self.assertEqual(None, + self.assertEqual('%', state.attributes.get('unit_of_measurement')) state = self.hass.states.get('sensor.yr_fog') From 618ebfe43cbd274be2795b293feab0f68dced51c Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Tue, 1 Dec 2015 13:40:26 +0100 Subject: [PATCH 008/267] try to fix requirements for yr sensor --- homeassistant/components/sensor/yr.py | 3 ++- requirements_all.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index c2b0e138857..4c728a5a5b2 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -38,6 +38,7 @@ sensor: import logging import datetime import urllib.request +import xmltodict from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import Entity @@ -166,7 +167,7 @@ class YrSensor(Entity): if response.status != 200: return data = response.read().decode('utf-8') - import xmltodict + self._weather_data = xmltodict.parse(data)['weatherdata'] model = self._weather_data['meta']['model'] if '@nextrun' not in model: diff --git a/requirements_all.txt b/requirements_all.txt index 1ff81bcc64a..4c046c61f11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -170,3 +170,6 @@ https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60 # homeassistant.components.zwave pydispatcher==2.0.5 + +# homeassistant.components.sensor.yr +xmltodict From 15770ff90f9233628dad26c2bf5582419fb72bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 1 Dec 2015 20:12:07 +0100 Subject: [PATCH 009/267] Update yr.py --- homeassistant/components/sensor/yr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 4c728a5a5b2..b2da0d7053a 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -37,6 +37,7 @@ sensor: """ import logging import datetime +import homeassistant.util.dt as dt_util import urllib.request import xmltodict @@ -158,7 +159,7 @@ class YrSensor(Entity): # pylint: disable=too-many-branches, too-many-return-statements def update(self): """ Gets the latest data from yr.no and updates the states. """ - now = datetime.datetime.now() + now = dt_util.utcnow() if now > self._nextrun: try: response = urllib.request.urlopen(self._url) From e68a8f9c0f126c4fa82821e8c26f6f06c012f0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 1 Dec 2015 20:16:04 +0100 Subject: [PATCH 010/267] Update yr.py --- 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 b2da0d7053a..3f6a7cf7fda 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -35,9 +35,9 @@ sensor: - dewpointTemperature """ +import homeassistant.util.dt as dt_util import logging import datetime -import homeassistant.util.dt as dt_util import urllib.request import xmltodict From 361ab0f92b113c0d0b9e5e4be902a855b7ddb764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 1 Dec 2015 20:19:59 +0100 Subject: [PATCH 011/267] Update yr.py --- 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 3f6a7cf7fda..7eef61c5136 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -35,11 +35,11 @@ sensor: - dewpointTemperature """ -import homeassistant.util.dt as dt_util import logging import datetime import urllib.request import xmltodict +import homeassistant.util.dt as dt_util from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import Entity From 31f1e1d7a47f69229dcc645b06ac3162c48808d3 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Wed, 2 Dec 2015 13:04:23 +0100 Subject: [PATCH 012/267] added comment for yr sensor --- homeassistant/components/sensor/yr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 4c728a5a5b2..37d199b18d7 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -99,6 +99,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): else: dev.append(YrSensor(coordinates, variable)) + # add symbol as default sensor if len(dev) == 0: dev.append(YrSensor(coordinates, "symbol")) add_devices(dev) @@ -159,6 +160,7 @@ class YrSensor(Entity): def update(self): """ Gets the latest data from yr.no and updates the states. """ now = datetime.datetime.now() + # check if new will be available if now > self._nextrun: try: response = urllib.request.urlopen(self._url) @@ -174,11 +176,12 @@ class YrSensor(Entity): model = model[0] self._nextrun = datetime.datetime.strptime(model['@nextrun'], "%Y-%m-%dT%H:%M:%SZ") - + # check if data should be updated if now > self._update: time_data = self._weather_data['product']['time'] # pylint: disable=consider-using-enumerate + # find sensor for k in range(len(time_data)): valid_from = datetime.datetime.strptime(time_data[k]['@from'], "%Y-%m-%dT%H:%M:%SZ") From 750ca79ac05383b85c8cb4302ed9f5700caac854 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Fri, 4 Dec 2015 15:05:23 +0100 Subject: [PATCH 013/267] Refactor yr sensor --- homeassistant/components/sensor/yr.py | 71 +++++++++++++++++---------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 37d199b18d7..b7ba0fcbd8d 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -38,7 +38,6 @@ sensor: import logging import datetime import urllib.request -import xmltodict from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import Entity @@ -48,7 +47,6 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['xmltodict', 'astral==0.8.1'] - # Sensor types are defined like so: SENSOR_TYPES = { 'symbol': ['Symbol', ''], @@ -91,17 +89,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): coordinates = dict(lat=hass.config.latitude, lon=hass.config.longitude, msl=elevation) + weather = YrData(coordinates) + dev = [] if 'monitored_conditions' in config: for variable in config['monitored_conditions']: if variable not in SENSOR_TYPES: _LOGGER.error('Sensor type: "%s" does not exist', variable) else: - dev.append(YrSensor(coordinates, variable)) + dev.append(YrSensor(variable, weather)) # add symbol as default sensor if len(dev) == 0: - dev.append(YrSensor(coordinates, "symbol")) + dev.append(YrSensor("symbol", weather)) add_devices(dev) @@ -109,18 +109,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class YrSensor(Entity): """ Implements an Yr.no sensor. """ - def __init__(self, coordinates, sensor_type): + def __init__(self, sensor_type, weather): self.client_name = 'yr' self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type self._state = None - self._weather_data = None + self._weather = weather self._info = '' self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._nextrun = datetime.datetime.fromtimestamp(0) self._update = datetime.datetime.fromtimestamp(0) - self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ - 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) self.update() @@ -144,6 +141,9 @@ class YrSensor(Entity): data[ATTR_ENTITY_PICTURE] = "http://api.met.no/weatherapi/weathericon/1.1/" \ "?symbol=" + str(symbol_nr) + \ ";content_type=image/png" + data["description_image"] = "http://api.met.no/weatherapi/weathericon/1.1/" \ + "?symbol=" + str(symbol_nr) + \ + ";content_type=image/png" return data @property @@ -159,26 +159,12 @@ class YrSensor(Entity): # pylint: disable=too-many-branches, too-many-return-statements def update(self): """ Gets the latest data from yr.no and updates the states. """ - now = datetime.datetime.now() - # check if new will be available - if now > self._nextrun: - try: - response = urllib.request.urlopen(self._url) - except urllib.error.URLError: - return - if response.status != 200: - return - data = response.read().decode('utf-8') - self._weather_data = xmltodict.parse(data)['weatherdata'] - model = self._weather_data['meta']['model'] - if '@nextrun' not in model: - model = model[0] - self._nextrun = datetime.datetime.strptime(model['@nextrun'], - "%Y-%m-%dT%H:%M:%SZ") + self._weather.update() + now = datetime.datetime.now() # check if data should be updated if now > self._update: - time_data = self._weather_data['product']['time'] + time_data = self._weather.data['product']['time'] # pylint: disable=consider-using-enumerate # find sensor @@ -232,3 +218,36 @@ class YrSensor(Entity): elif self.type == 'dewpointTemperature': self._state = temp_data[self.type]['@value'] return + + +# pylint: disable=too-few-public-methods +class YrData(object): + """ Gets the latest data and updates the states. """ + + def __init__(self, coordinates): + self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ + 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) + + self._nextrun = datetime.datetime.fromtimestamp(0) + self.update() + + def update(self): + """ Gets the latest data from yr.no """ + now = datetime.datetime.now() + # check if new will be available + if now > self._nextrun: + try: + response = urllib.request.urlopen(self._url) + except urllib.error.URLError: + return + if response.status != 200: + return + data = response.read().decode('utf-8') + + import xmltodict + self.data = xmltodict.parse(data)['weatherdata'] + model = self.data['meta']['model'] + if '@nextrun' not in model: + model = model[0] + self._nextrun = datetime.datetime.strptime(model['@nextrun'], + "%Y-%m-%dT%H:%M:%SZ") From ac41f3028ccd8a1b80545e485dddce060971fb75 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Fri, 4 Dec 2015 15:10:26 +0100 Subject: [PATCH 014/267] Refactor yr sensor --- homeassistant/components/sensor/yr.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index b7ba0fcbd8d..e5e7585eeef 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -141,9 +141,7 @@ class YrSensor(Entity): data[ATTR_ENTITY_PICTURE] = "http://api.met.no/weatherapi/weathericon/1.1/" \ "?symbol=" + str(symbol_nr) + \ ";content_type=image/png" - data["description_image"] = "http://api.met.no/weatherapi/weathericon/1.1/" \ - "?symbol=" + str(symbol_nr) + \ - ";content_type=image/png" + return data @property From 59524c7933c84680c5fb1b164814cd9b363d5727 Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Sun, 6 Dec 2015 00:29:03 +0100 Subject: [PATCH 015/267] 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 016/267] 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 017/267] 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 018/267] 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 019/267] 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 020/267] 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 35411cd57e52a3f0171451721c60371dd34c1975 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Dec 2015 15:32:51 -0800 Subject: [PATCH 021/267] Version bump to 0.11.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index deda344f4da..82276d81b48 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ Constants used by Home Assistant components. """ -__version__ = "0.10.0" +__version__ = "0.11.0.dev0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' From 8e16a443e5a9714ddd9a3a8ef30e4a0e01194365 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Dec 2015 10:24:22 +0100 Subject: [PATCH 022/267] Added yr sensor to requirements_all --- requirements_all.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 4c046c61f11..191607f551d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -141,7 +141,11 @@ https://github.com/rkabadi/temper-python/archive/3dbdaf2d87b8db9a3cd6e5585fc7045 # homeassistant.components.switch.transmission transmissionrpc==0.11 +# homeassistant.components.sensor.yr +xmltodict + # homeassistant.components.sun +# homeassistant.components.sensor.yr astral==0.8.1 # homeassistant.components.switch.edimax @@ -170,6 +174,3 @@ https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60 # homeassistant.components.zwave pydispatcher==2.0.5 - -# homeassistant.components.sensor.yr -xmltodict From 9a1883bb49b69516709082e5cb15babc61148b6e Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Dec 2015 10:29:12 +0100 Subject: [PATCH 023/267] changed to use requestes in stead of urllib for yr sensor --- homeassistant/components/sensor/yr.py | 141 +++++++++++++------------- 1 file changed, 73 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index e5e7585eeef..cad263b37b8 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -38,6 +38,7 @@ sensor: import logging import datetime import urllib.request +import requests from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import Entity @@ -161,61 +162,64 @@ class YrSensor(Entity): self._weather.update() now = datetime.datetime.now() # check if data should be updated - if now > self._update: - time_data = self._weather.data['product']['time'] + if now <= self._update: + return - # pylint: disable=consider-using-enumerate - # find sensor - for k in range(len(time_data)): - valid_from = datetime.datetime.strptime(time_data[k]['@from'], - "%Y-%m-%dT%H:%M:%SZ") - valid_to = datetime.datetime.strptime(time_data[k]['@to'], - "%Y-%m-%dT%H:%M:%SZ") - self._update = valid_to - self._info = "Forecast between " + time_data[k]['@from'] \ - + " and " + time_data[k]['@to'] + ". " + time_data = self._weather.data['product']['time'] - temp_data = time_data[k]['location'] - if self.type in temp_data and now < valid_to: - if self.type == 'precipitation' and valid_from < now: - self._state = temp_data[self.type]['@value'] - return - elif self.type == 'symbol' and valid_from < now: - self._state = temp_data[self.type]['@number'] - return - elif self.type == 'temperature': - self._state = temp_data[self.type]['@value'] - return - elif self.type == 'windSpeed': - self._state = temp_data[self.type]['@mps'] - return - elif self.type == 'pressure': - self._state = temp_data[self.type]['@value'] - return - elif self.type == 'windDirection': - self._state = float(temp_data[self.type]['@deg']) - return - elif self.type == 'humidity': - self._state = temp_data[self.type]['@value'] - return - elif self.type == 'fog': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'cloudiness': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'lowClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'mediumClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'highClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'dewpointTemperature': - self._state = temp_data[self.type]['@value'] - return + # pylint: disable=consider-using-enumerate + # find sensor + for k in range(len(time_data)): + valid_from = datetime.datetime.strptime(time_data[k]['@from'], + "%Y-%m-%dT%H:%M:%SZ") + valid_to = datetime.datetime.strptime(time_data[k]['@to'], + "%Y-%m-%dT%H:%M:%SZ") + self._update = valid_to + self._info = "Forecast between " + time_data[k]['@from'] \ + + " and " + time_data[k]['@to'] + ". " + + temp_data = time_data[k]['location'] + if self.type not in temp_data and now >= valid_to: + continue + if self.type == 'precipitation' and valid_from < now: + self._state = temp_data[self.type]['@value'] + return + elif self.type == 'symbol' and valid_from < now: + self._state = temp_data[self.type]['@number'] + return + elif self.type == 'temperature': + self._state = temp_data[self.type]['@value'] + return + elif self.type == 'windSpeed': + self._state = temp_data[self.type]['@mps'] + return + elif self.type == 'pressure': + self._state = temp_data[self.type]['@value'] + return + elif self.type == 'windDirection': + self._state = float(temp_data[self.type]['@deg']) + return + elif self.type == 'humidity': + self._state = temp_data[self.type]['@value'] + return + elif self.type == 'fog': + self._state = temp_data[self.type]['@percent'] + return + elif self.type == 'cloudiness': + self._state = temp_data[self.type]['@percent'] + return + elif self.type == 'lowClouds': + self._state = temp_data[self.type]['@percent'] + return + elif self.type == 'mediumClouds': + self._state = temp_data[self.type]['@percent'] + return + elif self.type == 'highClouds': + self._state = temp_data[self.type]['@percent'] + return + elif self.type == 'dewpointTemperature': + self._state = temp_data[self.type]['@value'] + return # pylint: disable=too-few-public-methods @@ -233,19 +237,20 @@ class YrData(object): """ Gets the latest data from yr.no """ now = datetime.datetime.now() # check if new will be available - if now > self._nextrun: - try: - response = urllib.request.urlopen(self._url) - except urllib.error.URLError: - return - if response.status != 200: - return - data = response.read().decode('utf-8') + if now <= self._nextrun: + return + try: + response = requests.get(self._url) + except requests.RequestException: + return + if response.status_code != 200: + return + data = response.text - import xmltodict - self.data = xmltodict.parse(data)['weatherdata'] - model = self.data['meta']['model'] - if '@nextrun' not in model: - model = model[0] - self._nextrun = datetime.datetime.strptime(model['@nextrun'], - "%Y-%m-%dT%H:%M:%SZ") + import xmltodict + self.data = xmltodict.parse(data)['weatherdata'] + model = self.data['meta']['model'] + if '@nextrun' not in model: + model = model[0] + self._nextrun = datetime.datetime.strptime(model['@nextrun'], + "%Y-%m-%dT%H:%M:%SZ") From cf4f4ce8c7067f7d6c57a6d4adb506b8ba32b21c Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Dec 2015 10:33:19 +0100 Subject: [PATCH 024/267] changed to use requestes in stead of urllib for yr sensor --- 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 cad263b37b8..36dd193da97 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -135,7 +135,7 @@ class YrSensor(Entity): def state_attributes(self): """ Returns state attributes. """ data = {} - data[''] = self._info + "Weather forecast from yr.no, delivered by the"\ + data[''] = "Weather forecast from yr.no, delivered by the"\ " Norwegian Meteorological Institute and the NRK" if self.type == 'symbol': symbol_nr = self._state From 8159d36114c13fe2d06c59db8b4213f6ce5142e5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Dec 2015 10:36:25 +0100 Subject: [PATCH 025/267] small fix in yr sensor name --- homeassistant/components/sensor/yr.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 36dd193da97..4dec49557e4 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -111,7 +111,6 @@ class YrSensor(Entity): """ Implements an Yr.no sensor. """ def __init__(self, sensor_type, weather): - self.client_name = 'yr' self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type self._state = None @@ -124,7 +123,7 @@ class YrSensor(Entity): @property def name(self): - return '{} {}'.format(self.client_name, self._name) + return self._name @property def state(self): From 9e89197284224c6e39e7edaf88036efaf35835e5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 21 Dec 2015 10:42:42 +0100 Subject: [PATCH 026/267] small fix in yr sensor name --- homeassistant/components/sensor/yr.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 4dec49557e4..36dd193da97 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -111,6 +111,7 @@ class YrSensor(Entity): """ Implements an Yr.no sensor. """ def __init__(self, sensor_type, weather): + self.client_name = 'yr' self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type self._state = None @@ -123,7 +124,7 @@ class YrSensor(Entity): @property def name(self): - return self._name + return '{} {}'.format(self.client_name, self._name) @property def state(self): From 2650d235eaaf59a30b167ec9b89d9553e4295fc6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Dec 2015 08:56:27 -0800 Subject: [PATCH 027/267] Fix: EntityComponent deadlock when adding new devices during update state --- homeassistant/helpers/entity_component.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ec22181bf5a..20be9a0f704 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -39,7 +39,8 @@ class EntityComponent(object): self.is_polling = False self.config = None - self.lock = Lock() + # Because updating state might cause an entity to be found + self.lock = RLock() def setup(self, config): """ From 07fb4ff24373fe550c47c3527aa5bb2720b7662a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Dec 2015 09:44:17 -0800 Subject: [PATCH 028/267] Revert last fix. Will fix better. --- homeassistant/helpers/entity_component.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 20be9a0f704..ec22181bf5a 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -39,8 +39,7 @@ class EntityComponent(object): self.is_polling = False self.config = None - # Because updating state might cause an entity to be found - self.lock = RLock() + self.lock = Lock() def setup(self, config): """ From 0ac1759395462bccc5c76979c5a79e8eae049046 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Mon, 21 Dec 2015 08:54:14 -0700 Subject: [PATCH 029/267] Rename geofancy to locative (fix #761) --- .coveragerc | 2 +- .../{geofancy.py => locative.py} | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) rename homeassistant/components/device_tracker/{geofancy.py => locative.py} (73%) diff --git a/.coveragerc b/.coveragerc index d078cd5bf8a..4b916a7fbcd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -41,7 +41,7 @@ omit = homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py - homeassistant/components/device_tracker/geofancy.py + homeassistant/components/device_tracker/locative.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py diff --git a/homeassistant/components/device_tracker/geofancy.py b/homeassistant/components/device_tracker/locative.py similarity index 73% rename from homeassistant/components/device_tracker/geofancy.py rename to homeassistant/components/device_tracker/locative.py index a5e6edee71a..2d238992cc7 100644 --- a/homeassistant/components/device_tracker/geofancy.py +++ b/homeassistant/components/device_tracker/locative.py @@ -1,10 +1,10 @@ """ -homeassistant.components.device_tracker.geofancy +homeassistant.components.device_tracker.locative ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Geofancy platform for the device tracker. +Locative platform for the device tracker. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/device_tracker.geofancy/ +https://home-assistant.io/components/device_tracker.locative/ """ from homeassistant.const import ( HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR) @@ -13,32 +13,32 @@ DEPENDENCIES = ['http'] _SEE = 0 -URL_API_GEOFANCY_ENDPOINT = "/api/geofancy" +URL_API_LOCATIVE_ENDPOINT = "/api/locative" def setup_scanner(hass, config, see): - """ Set up an endpoint for the Geofancy app. """ + """ Set up an endpoint for the Locative app. """ # Use a global variable to keep setup_scanner compact when using a callback global _SEE _SEE = see # POST would be semantically better, but that currently does not work - # since Geofancy sends the data as key1=value1&key2=value2 + # since Locative sends the data as key1=value1&key2=value2 # in the request body, while Home Assistant expects json there. hass.http.register_path( - 'GET', URL_API_GEOFANCY_ENDPOINT, _handle_get_api_geofancy) + 'GET', URL_API_LOCATIVE_ENDPOINT, _handle_get_api_locative) return True -def _handle_get_api_geofancy(handler, path_match, data): - """ Geofancy message received. """ +def _handle_get_api_locative(handler, path_match, data): + """ Locative message received. """ if not isinstance(data, dict): handler.write_json_message( - "Error while parsing Geofancy message.", + "Error while parsing Locative message.", HTTP_INTERNAL_SERVER_ERROR) return if 'latitude' not in data or 'longitude' not in data: @@ -67,4 +67,4 @@ def _handle_get_api_geofancy(handler, path_match, data): _SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id']) - handler.write_json_message("Geofancy message processed") + handler.write_json_message("Locative message processed") From 8c010c8df434f63680579db6fcbeba20483159ed Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Mon, 21 Dec 2015 16:09:27 -0700 Subject: [PATCH 030/267] Add ability to use sun as condition in automation --- homeassistant/components/automation/sun.py | 114 +++++++++++++++++---- 1 file changed, 94 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 84334493d0f..cf14d220fb0 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -17,6 +17,10 @@ DEPENDENCIES = ['sun'] CONF_OFFSET = 'offset' CONF_EVENT = 'event' +CONF_BEFORE = "before" +CONF_BEFORE_OFFSET = "before_offset" +CONF_AFTER = "after" +CONF_AFTER_OFFSET = "after_offset" EVENT_SUNSET = 'sunset' EVENT_SUNRISE = 'sunrise' @@ -37,26 +41,9 @@ def trigger(hass, config, action): _LOGGER.error("Invalid value for %s: %s", CONF_EVENT, event) return False - if CONF_OFFSET in config: - raw_offset = config.get(CONF_OFFSET) - - negative_offset = False - if raw_offset.startswith('-'): - negative_offset = True - raw_offset = raw_offset[1:] - - try: - (hour, minute, second) = [int(x) for x in raw_offset.split(':')] - except ValueError: - _LOGGER.error('Could not parse offset %s', raw_offset) - return False - - offset = timedelta(hours=hour, minutes=minute, seconds=second) - - if negative_offset: - offset *= -1 - else: - offset = timedelta(0) + offset = _parse_offset(config.get(CONF_OFFSET)) + if offset is False: + return False # Do something to call action if event == EVENT_SUNRISE: @@ -67,6 +54,70 @@ def trigger(hass, config, action): return True +def if_action(hass, config): + """ Wraps action method with sun based condition. """ + before = config.get(CONF_BEFORE) + after = config.get(CONF_AFTER) + + # Make sure required configuration keys are present + if before is None and after is None: + logging.getLogger(__name__).error( + "Missing if-condition configuration key %s or %s", + CONF_BEFORE, CONF_AFTER) + return None + + # Make sure configuration keys have the right value + if before is not None and before not in (EVENT_SUNRISE, EVENT_SUNSET) or \ + after is not None and after not in (EVENT_SUNRISE, EVENT_SUNSET): + logging.getLogger(__name__).error( + "%s and %s can only be set to %s or %s", + CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET) + return None + + before_offset = _parse_offset(config.get(CONF_BEFORE_OFFSET)) + after_offset = _parse_offset(config.get(CONF_AFTER_OFFSET)) + if before_offset is False or after_offset is False: + return None + + if before is None: + before_func = lambda: None + elif before == EVENT_SUNRISE: + before_func = lambda: sun.next_rising_utc(hass) + before_offset + else: + before_func = lambda: sun.next_setting_utc(hass) + before_offset + + if after is None: + after_func = lambda: None + elif after == EVENT_SUNRISE: + after_func = lambda: sun.next_rising_utc(hass) + after_offset + else: + after_func = lambda: sun.next_setting_utc(hass) + after_offset + + # This is needed for testing + time_func = dt_util.utcnow + + def time_if(): + """ Validate time based if-condition """ + + # This is needed for testing. + nonlocal time_func + now = time_func() + before = before_func() + after = after_func() + + if before is not None and now > now.replace(hour=before.hour, + minute=before.minute): + return False + + if after is not None and now < now.replace(hour=after.hour, + minute=after.minute): + return False + + return True + + return time_if + + def trigger_sunrise(hass, action, offset): """ Trigger action at next sun rise. """ def next_rise(): @@ -103,3 +154,26 @@ def trigger_sunset(hass, action, offset): action() track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + + +def _parse_offset(raw_offset): + if raw_offset is None: + return timedelta(0) + + negative_offset = False + if raw_offset.startswith('-'): + negative_offset = True + raw_offset = raw_offset[1:] + + try: + (hour, minute, second) = [int(x) for x in raw_offset.split(':')] + except ValueError: + _LOGGER.error('Could not parse offset %s', raw_offset) + return False + + offset = timedelta(hours=hour, minutes=minute, seconds=second) + + if negative_offset: + offset *= -1 + + return offset From 110d721c76a227093b1cc3128f301367e0e76d2d Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Mon, 21 Dec 2015 16:09:51 -0700 Subject: [PATCH 031/267] Add tests --- tests/components/automation/test_sun.py | 248 ++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index de8b2f8121b..3c4b2783c72 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -139,3 +139,251 @@ class TestAutomationSun(unittest.TestCase): fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + + def test_if_action_before_before(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_before(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_action_before_after(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_action_before_and_after_during(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', + sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'before': 'sunset' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_before_and_after_before(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', + sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 8, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'before': 'sunset' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_action_before_and_after_after(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', + sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'before': 'sunset' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_action_offset_before(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + 'before_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_offset_after(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'after_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) From ff8f22854c96e7f807161ebbf373cd88eaefbc68 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Mon, 21 Dec 2015 16:28:26 -0700 Subject: [PATCH 032/267] Add test --- tests/components/automation/test_sun.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 3c4b2783c72..87f04a325e8 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -230,6 +230,36 @@ class TestAutomationSun(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) + def test_if_action_after_after(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '14:00:00 16-09-2015', + }) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + entity_id = 'domain.test_entity' + + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunset', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + def test_if_action_before_and_after_during(self): self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', From d5179b4bdc6f0a183c53257ff89eb4e3bfb33df1 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Mon, 21 Dec 2015 19:49:39 -0500 Subject: [PATCH 033/267] add statecmd to command_switch --- .../components/switch/command_switch.py | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 91171be3680..1882d73fc51 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -10,6 +10,8 @@ import logging import subprocess from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.util import template _LOGGER = logging.getLogger(__name__) @@ -22,22 +24,36 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): devices = [] for dev_name, properties in switches.items(): + if 'statecmd' in properties and CONF_VALUE_TEMPLATE not in properties: + _LOGGER.warn("Specify a %s when using statemcd", + CONF_VALUE_TEMPLATE) + continue devices.append( CommandSwitch( + hass, properties.get('name', dev_name), properties.get('oncmd', 'true'), - properties.get('offcmd', 'true'))) + properties.get('offcmd', 'true'), + properties.get('statecmd', False), + properties.get(CONF_VALUE_TEMPLATE, False))) add_devices_callback(devices) class CommandSwitch(SwitchDevice): """ Represents a switch that can be togggled using shell commands. """ - def __init__(self, name, command_on, command_off): + + # pylint: disable=too-many-arguments + def __init__(self, hass, name, command_on, command_off, + command_state, value_template): + + self._hass = hass self._name = name self._state = False self._command_on = command_on self._command_off = command_off + self._command_state = command_state + self._value_template = value_template @staticmethod def _switch(command): @@ -51,10 +67,21 @@ class CommandSwitch(SwitchDevice): return success + @staticmethod + def _query_state(command): + """ Execute state command. """ + _LOGGER.info('Running state command: %s', command) + + try: + return_value = subprocess.check_output(command, shell=True) + return return_value.strip().decode('utf-8') + except subprocess.CalledProcessError: + _LOGGER.error('Command failed: %s', command) + @property def should_poll(self): """ No polling needed. """ - return False + return True @property def name(self): @@ -66,14 +93,24 @@ class CommandSwitch(SwitchDevice): """ True if device is on. """ return self._state + def update(self): + """ Update device state. """ + if self._command_state and self._value_template: + payload = CommandSwitch._query_state(self._command_state) + payload = template.render_with_possible_json_value( + self._hass, self._value_template, payload) + self._state = (payload == "True") + def turn_on(self, **kwargs): """ Turn the device on. """ if CommandSwitch._switch(self._command_on): - self._state = True - self.update_ha_state() + if not self._command_state: + self._state = True + self.update_ha_state() def turn_off(self, **kwargs): """ Turn the device off. """ if CommandSwitch._switch(self._command_off): - self._state = False - self.update_ha_state() + if not self._command_state: + self._state = False + self.update_ha_state() From fba5becd909ed826fbe8b8368abbdf848b5efcfd Mon Sep 17 00:00:00 2001 From: happyleaves Date: Mon, 21 Dec 2015 20:18:00 -0500 Subject: [PATCH 034/267] warn->warning --- homeassistant/components/switch/command_switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 1882d73fc51..5af197193d3 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -25,8 +25,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): for dev_name, properties in switches.items(): if 'statecmd' in properties and CONF_VALUE_TEMPLATE not in properties: - _LOGGER.warn("Specify a %s when using statemcd", - CONF_VALUE_TEMPLATE) + _LOGGER.warning("Specify a %s when using statemcd", + CONF_VALUE_TEMPLATE) continue devices.append( CommandSwitch( From edff53609fbad5e2601bd805657dbe4055837c88 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Tue, 22 Dec 2015 13:50:59 -0600 Subject: [PATCH 035/267] Reset log handlers to lowest level. This is necessary to enable logging lower than INFO for the error log file. --- homeassistant/components/logger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 9a5d1c59d1a..9a8f3f2d7f5 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -78,6 +78,7 @@ def setup(hass, config=None): # Set log filter for all log handler for handler in logging.root.handlers: + handler.setLevel(logging.NOTSET) handler.addFilter(HomeAssistantLogFilter(logfilter)) return True From 87961873892075594619a0475dd88619b9a98d5d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 Dec 2015 10:06:54 +0100 Subject: [PATCH 036/267] Equalize log messages --- .../components/binary_sensor/rest.py | 11 ++++----- homeassistant/components/sensor/rest.py | 23 +++++++++---------- homeassistant/components/switch/rest.py | 7 +++--- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 60963988f39..bbb8c0bf47d 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -23,7 +23,7 @@ DEFAULT_METHOD = 'GET' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -# pylint: disable=unused-variable +# pylint: disable=unused-variable, logging-too-many-args def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the REST binary sensor. """ @@ -47,15 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): response = requests.post(resource, data=payload, timeout=10, verify=verify_ssl) if not response.ok: - _LOGGER.error('Response status is "%s"', response.status_code) + _LOGGER.error("Response status is '%s'", response.status_code) return False except requests.exceptions.MissingSchema: - _LOGGER.error('Missing resource or schema in configuration. ' - 'Add http:// to your URL.') + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint: %s', - resource) + _LOGGER.error('No route to resource/endpoint: %s', resource) return False if use_get: diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4dcd036df5e..8afa5b26783 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -10,7 +10,7 @@ from datetime import timedelta import logging import requests -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import (CONF_VALUE_TEMPLATE, STATE_UNKNOWN) from homeassistant.util import template, Throttle from homeassistant.helpers.entity import Entity @@ -47,15 +47,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): response = requests.post(resource, data=payload, timeout=10, verify=verify_ssl) if not response.ok: - _LOGGER.error('Response status is "%s"', response.status_code) + _LOGGER.error("Response status is '%s'", response.status_code) return False except requests.exceptions.MissingSchema: - _LOGGER.error('Missing resource or schema in configuration. ' - 'Add http:// to your URL.') + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint. ' - 'Please check the URL in the configuration file.') + _LOGGER.error("No route to resource/endpoint: %s", resource) return False if use_get: @@ -78,7 +77,7 @@ class RestSensor(Entity): self._hass = hass self.rest = rest self._name = name - self._state = 'n/a' + self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template self.update() @@ -108,7 +107,7 @@ class RestSensor(Entity): else: if self._value_template is not None: value = template.render_with_possible_json_value( - self._hass, self._value_template, value, 'N/A') + self._hass, self._value_template, value, STATE_UNKNOWN) self._state = value @@ -131,8 +130,8 @@ class RestDataGet(object): del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint.") - self.data['error'] = 'N/A' + _LOGGER.error("No route to resource/endpoint: %s", self._resource) + self.data['error'] = STATE_UNKNOWN # pylint: disable=too-few-public-methods @@ -155,5 +154,5 @@ class RestDataPost(object): del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint.") - self.data['error'] = 'N/A' + _LOGGER.error("No route to resource/endpoint: %s", self._resource) + self.data['error'] = STATE_UNKNOWN diff --git a/homeassistant/components/switch/rest.py b/homeassistant/components/switch/rest.py index 2435829637e..5c4b9b37e1e 100644 --- a/homeassistant/components/switch/rest.py +++ b/homeassistant/components/switch/rest.py @@ -18,7 +18,7 @@ DEFAULT_BODY_ON = "ON" DEFAULT_BODY_OFF = "OFF" -# pylint: disable=unused-argument +# pylint: disable=unused-argument, def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Get REST switch. """ @@ -32,11 +32,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): requests.get(resource, timeout=10) except requests.exceptions.MissingSchema: _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL.") + "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint. " - "Please check the IP address in the configuration file.") + _LOGGER.error("No route to resource/endpoint: %s", resource) return False add_devices_callback([RestSwitch( From fb2da6be9a9ffe299a3a9543dc30b841d2d06813 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 Dec 2015 14:14:09 +0100 Subject: [PATCH 037/267] Remove space --- homeassistant/components/sensor/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 8afa5b26783..a6b5c518eee 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "Add http:// or https:// to your URL") return False except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", resource) + _LOGGER.error("No route to resource/endpoint: %s", resource) return False if use_get: From 56186232f3d23617efbc0530f7f98ddda6163128 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 Dec 2015 22:33:20 +0100 Subject: [PATCH 038/267] Enable logging-too-many-args --- homeassistant/components/binary_sensor/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index bbb8c0bf47d..6cb6ede5e50 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -23,7 +23,7 @@ DEFAULT_METHOD = 'GET' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -# pylint: disable=unused-variable, logging-too-many-args +# pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the REST binary sensor. """ From 987282da78381180973675ac77407c06f08b35d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Dec 2015 18:35:05 -0800 Subject: [PATCH 039/267] Logbook entry events are now lower case --- homeassistant/components/logbook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 5c2e7076955..16159404dec 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -28,7 +28,7 @@ QUERY_EVENTS_BETWEEN = """ SELECT * FROM events WHERE time_fired > ? AND time_fired < ? """ -EVENT_LOGBOOK_ENTRY = 'LOGBOOK_ENTRY' +EVENT_LOGBOOK_ENTRY = 'logbook_entry' GROUP_BY_MINUTES = 15 @@ -204,7 +204,7 @@ def humanify(events): event.time_fired, "Home Assistant", action, domain=HA_DOMAIN) - elif event.event_type == EVENT_LOGBOOK_ENTRY: + elif event.event_type.lower() == EVENT_LOGBOOK_ENTRY: domain = event.data.get(ATTR_DOMAIN) entity_id = event.data.get(ATTR_ENTITY_ID) if domain is None and entity_id is not None: From bdd945c1c4e3a308f872e94054b1fcfdb2232096 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Dec 2015 18:39:46 -0800 Subject: [PATCH 040/267] Set default log level to INFO --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index a7507fd12b8..b704fc082ac 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -275,7 +275,7 @@ def enable_logging(hass, verbose=False, daemon=False, log_rotate_days=None): datefmt='%y-%m-%d %H:%M:%S')) logger = logging.getLogger('') logger.addHandler(err_handler) - logger.setLevel(logging.NOTSET) # this sets the minimum log level + logger.setLevel(logging.INFO) else: _LOGGER.error( From c31a291a9c78d16daef50f17be1ed73e9e028f10 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Wed, 23 Dec 2015 00:57:41 -0600 Subject: [PATCH 041/267] Set the root logger to lowest level in logger component. In combination with resetting the log level on the handlers, this allows messages lower than the default INFO to be logged when using the logger component. --- homeassistant/components/logger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index 9a8f3f2d7f5..a0d769e3d82 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -76,6 +76,9 @@ def setup(hass, config=None): logfilter[LOGGER_LOGS] = logs + logger = logging.getLogger('') + logger.setLevel(logging.NOTSET) + # Set log filter for all log handler for handler in logging.root.handlers: handler.setLevel(logging.NOTSET) From 496ec4bccaffa9f02e535aa0283c7b7bbca9a409 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 23 Dec 2015 03:37:03 -0700 Subject: [PATCH 042/267] Fix bug in rest sensor --- homeassistant/components/sensor/rest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index a6b5c518eee..abb6b439a84 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -126,7 +126,7 @@ class RestDataGet(object): try: response = requests.get(self._resource, timeout=10, verify=self._verify_ssl) - if 'error' in self.data: + if isinstance(self.data, dict) and 'error' in self.data: del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: @@ -150,7 +150,7 @@ class RestDataPost(object): try: response = requests.post(self._resource, data=self._payload, timeout=10, verify=self._verify_ssl) - if 'error' in self.data: + if isinstance(self.data, dict) and 'error' in self.data: del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: From 1e2b5e699114ba4e54dc77a56858cdcd2f29a87c Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 23 Dec 2015 15:46:18 +0000 Subject: [PATCH 043/267] Add support for subscriptions --- homeassistant/components/switch/wemo.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index bad471ce437..9924153033e 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -11,9 +11,10 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY -REQUIREMENTS = ['pywemo==0.3.3'] +# REQUIREMENTS = ['pywemo==0.3.3'] _LOGGER = logging.getLogger(__name__) +_WEMO_SUBSCRIPTION_REGISTRY = None # pylint: disable=unused-argument, too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -21,6 +22,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo import pywemo.discovery as discovery + global _WEMO_SUBSCRIPTION_REGISTRY + if _WEMO_SUBSCRIPTION_REGISTRY is None: + _WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() + _WEMO_SUBSCRIPTION_REGISTRY.start() + if discovery_info is not None: location = discovery_info[2] mac = discovery_info[3] @@ -47,6 +53,21 @@ class WemoSwitch(SwitchDevice): self.insight_params = None self.maker_params = None + global _WEMO_SUBSCRIPTION_REGISTRY + _WEMO_SUBSCRIPTION_REGISTRY.register(wemo) + _WEMO_SUBSCRIPTION_REGISTRY.on(wemo, 'BinaryState', self._update_callback) + _WEMO_SUBSCRIPTION_REGISTRY.on(wemo, 'attributeList', self._update_callback) + + def _update_callback(self, _device, _params): + _LOGGER.info('Subscription update for %s, sevice=%s params=%s', self.name, _device, _params) + # import pdb; pdb.set_trace() + self.update() + + @property + def should_poll(self): + """ No polling should be needed with subscriptions, but leave in for initial version in case of issues. """ + return True + @property def unique_id(self): """ Returns the id of this WeMo switch """ From 09b894a4aac762cfd73e87c0685edd160b5335bd Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 23 Dec 2015 15:57:51 +0000 Subject: [PATCH 044/267] Fix style issues --- homeassistant/components/switch/wemo.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 9924153033e..7e3caf92e6e 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -16,6 +16,7 @@ _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None + # pylint: disable=unused-argument, too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return WeMo switches. """ @@ -23,7 +24,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): import pywemo.discovery as discovery global _WEMO_SUBSCRIPTION_REGISTRY - if _WEMO_SUBSCRIPTION_REGISTRY is None: + if _WEMO_SUBSCRIPTION_REGISTRY is None: _WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() _WEMO_SUBSCRIPTION_REGISTRY.start() @@ -53,19 +54,23 @@ class WemoSwitch(SwitchDevice): self.insight_params = None self.maker_params = None - global _WEMO_SUBSCRIPTION_REGISTRY _WEMO_SUBSCRIPTION_REGISTRY.register(wemo) - _WEMO_SUBSCRIPTION_REGISTRY.on(wemo, 'BinaryState', self._update_callback) - _WEMO_SUBSCRIPTION_REGISTRY.on(wemo, 'attributeList', self._update_callback) + _WEMO_SUBSCRIPTION_REGISTRY.on( + wemo, 'BinaryState', self._update_callback) + _WEMO_SUBSCRIPTION_REGISTRY.on( + wemo, 'attributeList', self._update_callback) def _update_callback(self, _device, _params): - _LOGGER.info('Subscription update for %s, sevice=%s params=%s', self.name, _device, _params) - # import pdb; pdb.set_trace() + """ Called by the wemo device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s, sevice=%s params=%s', + self.name, _device, _params) self.update() @property def should_poll(self): - """ No polling should be needed with subscriptions, but leave in for initial version in case of issues. """ + """ No polling should be needed with subscriptions """ + # but leave in for initial version in case of issues. return True @property From 6d236b81690554bba5a00ae29d79ceead4d32711 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 23 Dec 2015 18:03:40 +0000 Subject: [PATCH 045/267] Force state update --- homeassistant/components/switch/wemo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 7e3caf92e6e..f3aeac4c84e 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -66,6 +66,7 @@ class WemoSwitch(SwitchDevice): 'Subscription update for %s, sevice=%s params=%s', self.name, _device, _params) self.update() + self.update_ha_state() @property def should_poll(self): From 9f54bcc21b1744f2850a35e54903f3581d14badb Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Wed, 23 Dec 2015 23:20:39 +0100 Subject: [PATCH 046/267] 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 047/267] 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 2606e4d641d0f16e48fd4c2321adc6a918300e0e Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 24 Dec 2015 00:38:49 -0700 Subject: [PATCH 048/267] Simplify if statement --- homeassistant/components/automation/sun.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index cf14d220fb0..a5035c8936a 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -67,8 +67,8 @@ def if_action(hass, config): return None # Make sure configuration keys have the right value - if before is not None and before not in (EVENT_SUNRISE, EVENT_SUNSET) or \ - after is not None and after not in (EVENT_SUNRISE, EVENT_SUNSET): + if before not in (None, EVENT_SUNRISE, EVENT_SUNSET) or \ + after not in (None, EVENT_SUNRISE, EVENT_SUNSET): logging.getLogger(__name__).error( "%s and %s can only be set to %s or %s", CONF_BEFORE, CONF_AFTER, EVENT_SUNRISE, EVENT_SUNSET) From 3f151428b701ed7d1c8bf93861ec36cd8fa95df0 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 24 Dec 2015 09:35:02 +0000 Subject: [PATCH 049/267] Update pywemo version, use wildcard filter --- homeassistant/components/switch/wemo.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index f3aeac4c84e..1861b42f450 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -11,7 +11,7 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY -# REQUIREMENTS = ['pywemo==0.3.3'] +REQUIREMENTS = ['pywemo==0.3.4'] _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None @@ -56,9 +56,7 @@ class WemoSwitch(SwitchDevice): _WEMO_SUBSCRIPTION_REGISTRY.register(wemo) _WEMO_SUBSCRIPTION_REGISTRY.on( - wemo, 'BinaryState', self._update_callback) - _WEMO_SUBSCRIPTION_REGISTRY.on( - wemo, 'attributeList', self._update_callback) + wemo, None, self._update_callback) def _update_callback(self, _device, _params): """ Called by the wemo device callback to update state. """ From 0dfc1c4e7a8aa833ed34555f754ebf8493c843b8 Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 24 Dec 2015 21:10:27 -0600 Subject: [PATCH 050/267] Add functionality set random color Philips Hue --- homeassistant/components/light/__init__.py | 3 ++- homeassistant/components/light/hue.py | 6 +++++- homeassistant/components/light/services.yaml | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 1b80035fb0d..dd88de225d9 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -50,6 +50,7 @@ FLASH_LONG = "long" # Apply an effect to the light, can be EFFECT_COLORLOOP ATTR_EFFECT = "effect" EFFECT_COLORLOOP = "colorloop" +EFFECT_RANDOM = "random" EFFECT_WHITE = "white" LIGHT_PROFILES_FILE = "light_profiles.csv" @@ -228,7 +229,7 @@ def setup(hass, config): if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG): params[ATTR_FLASH] = dat[ATTR_FLASH] - if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE): + if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE, EFFECT_RANDOM): params[ATTR_EFFECT] = dat[ATTR_EFFECT] for light in target_lights: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 7c3af9f968d..a3895a0902e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -10,6 +10,7 @@ import json import logging import os import socket +import random from datetime import timedelta from urllib.parse import urlparse @@ -20,7 +21,7 @@ from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, - ATTR_EFFECT, EFFECT_COLORLOOP, ATTR_RGB_COLOR) + ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, ATTR_RGB_COLOR) REQUIREMENTS = ['phue==0.8'] MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -233,6 +234,9 @@ class HueLight(Light): if effect == EFFECT_COLORLOOP: command['effect'] = 'colorloop' + elif effect == EFFECT_RANDOM: + command['hue'] = random.randrange(0,65535) + command['sat'] = random.randrange(150,254) else: command['effect'] = 'none' diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 8a0c5b8fded..9908737b7b1 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -42,6 +42,7 @@ turn_on: description: Light effect values: - colorloop + - random turn_off: description: Turn a light off From 5c7fb5d7ae13eeb43ab7143ab0dfc114fe29ef3d Mon Sep 17 00:00:00 2001 From: richard Date: Thu, 24 Dec 2015 21:35:36 -0600 Subject: [PATCH 051/267] Fix styling issues --- homeassistant/components/light/__init__.py | 3 ++- homeassistant/components/light/hue.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index dd88de225d9..93321b5fd10 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -229,7 +229,8 @@ def setup(hass, config): if dat.get(ATTR_FLASH) in (FLASH_SHORT, FLASH_LONG): params[ATTR_FLASH] = dat[ATTR_FLASH] - if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE, EFFECT_RANDOM): + if dat.get(ATTR_EFFECT) in (EFFECT_COLORLOOP, EFFECT_WHITE, + EFFECT_RANDOM): params[ATTR_EFFECT] = dat[ATTR_EFFECT] for light in target_lights: diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index a3895a0902e..77672c9aaf5 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -235,8 +235,8 @@ class HueLight(Light): if effect == EFFECT_COLORLOOP: command['effect'] = 'colorloop' elif effect == EFFECT_RANDOM: - command['hue'] = random.randrange(0,65535) - command['sat'] = random.randrange(150,254) + command['hue'] = random.randrange(0, 65535) + command['sat'] = random.randrange(150, 254) else: command['effect'] = 'none' From f3db4306c218330043df881d27658b7e536bca14 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 25 Dec 2015 18:50:35 +0100 Subject: [PATCH 052/267] added icon --- homeassistant/components/sensor/eliqonline.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 608dc2f19fd..151b679b10e 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -53,6 +53,11 @@ class EliqSensor(Entity): """ Returns the name. """ return self._name + @property + def icon(self): + """ Returns icon. """ + return "mdi:speedometer" + @property def unit_of_measurement(self): """ Unit of measurement of this entity, if any. """ From b83b36274a880d169271a3b63b7f19f30a1d5995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Fri, 25 Dec 2015 20:16:51 +0100 Subject: [PATCH 053/267] changed to python-verisure 0.4.1 --- .../alarm_control_panel/verisure.py | 39 ++++++------ .../components/media_player/__init__.py | 3 + homeassistant/components/sensor/verisure.py | 18 +++--- homeassistant/components/switch/verisure.py | 24 ++++--- homeassistant/components/verisure.py | 63 +++++++++---------- requirements_all.txt | 2 +- 6 files changed, 72 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index e4c498a5044..62319e62189 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): alarms.extend([ VerisureAlarm(value) - for value in verisure.get_alarm_status().values() + for value in verisure.ALARM_STATUS.values() if verisure.SHOW_ALARM ]) @@ -42,7 +42,6 @@ class VerisureAlarm(alarm.AlarmControlPanel): def __init__(self, alarm_status): self._id = alarm_status.id - self._device = verisure.MY_PAGES.DEVICE_ALARM self._state = STATE_UNKNOWN @property @@ -62,36 +61,36 @@ class VerisureAlarm(alarm.AlarmControlPanel): def update(self): """ Update alarm status """ - verisure.update() + verisure.update_alarm() - if verisure.STATUS[self._device][self._id].status == 'unarmed': + if verisure.ALARM_STATUS[self._id].status == 'unarmed': self._state = STATE_ALARM_DISARMED - elif verisure.STATUS[self._device][self._id].status == 'armedhome': + elif verisure.ALARM_STATUS[self._id].status == 'armedhome': self._state = STATE_ALARM_ARMED_HOME - elif verisure.STATUS[self._device][self._id].status == 'armedaway': + elif verisure.ALARM_STATUS[self._id].status == 'armedaway': self._state = STATE_ALARM_ARMED_AWAY - elif verisure.STATUS[self._device][self._id].status != 'pending': + elif verisure.ALARM_STATUS[self._id].status != 'pending': _LOGGER.error( 'Unknown alarm state %s', - verisure.STATUS[self._device][self._id].status) + verisure.ALARM_STATUS[self._id].status) def alarm_disarm(self, code=None): """ Send disarm command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_DISARMED) - _LOGGER.warning('disarming') + verisure.MY_PAGES.alarm.set(code, 'DISARMED') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() + _LOGGER.info('disarming verisure alarm') def alarm_arm_home(self, code=None): """ Send arm home command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_HOME) - _LOGGER.warning('arming home') + verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() + _LOGGER.info('arming home verisure alarm') def alarm_arm_away(self, code=None): """ Send arm away command. """ - verisure.MY_PAGES.set_alarm_status( - code, - verisure.MY_PAGES.ALARM_ARMED_AWAY) - _LOGGER.warning('arming away') + verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY') + verisure.MY_PAGES.alarm.wait_while_pending() + verisure.update_alarm() + _LOGGER.info('arming away') diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 8204052b4a9..d566501568e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -154,6 +154,7 @@ def mute_volume(hass, mute, entity_id=None): def set_volume_level(hass, volume, entity_id=None): """ Send the media player the command for volume down. """ + print("AAAAAAAAAAAAAAAAAAAA") data = {ATTR_MEDIA_VOLUME_LEVEL: volume} if entity_id: @@ -231,9 +232,11 @@ def setup(hass, config): def volume_set_service(service): """ Set specified volume on the media player. """ + print(service.data) target_players = component.extract_from_service(service) if ATTR_MEDIA_VOLUME_LEVEL not in service.data: + print('returning') return volume = service.data[ATTR_MEDIA_VOLUME_LEVEL] diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index e946be9a3f4..e7c6a30b558 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -27,14 +27,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.extend([ VerisureThermometer(value) - for value in verisure.get_climate_status().values() + for value in verisure.CLIMATE_STATUS.values() if verisure.SHOW_THERMOMETERS and hasattr(value, 'temperature') and value.temperature ]) sensors.extend([ VerisureHygrometer(value) - for value in verisure.get_climate_status().values() + for value in verisure.CLIMATE_STATUS.values() if verisure.SHOW_HYGROMETERS and hasattr(value, 'humidity') and value.humidity ]) @@ -47,20 +47,19 @@ class VerisureThermometer(Entity): def __init__(self, climate_status): self._id = climate_status.id - self._device = verisure.MY_PAGES.DEVICE_CLIMATE @property def name(self): """ Returns the name of the device. """ return '{} {}'.format( - verisure.STATUS[self._device][self._id].location, + verisure.CLIMATE_STATUS[self._id].location, "Temperature") @property def state(self): """ Returns the state of the device. """ # remove ° character - return verisure.STATUS[self._device][self._id].temperature[:-1] + return verisure.CLIMATE_STATUS[self._id].temperature[:-1] @property def unit_of_measurement(self): @@ -69,7 +68,7 @@ class VerisureThermometer(Entity): def update(self): ''' update sensor ''' - verisure.update() + verisure.update_climate() class VerisureHygrometer(Entity): @@ -77,20 +76,19 @@ class VerisureHygrometer(Entity): def __init__(self, climate_status): self._id = climate_status.id - self._device = verisure.MY_PAGES.DEVICE_CLIMATE @property def name(self): """ Returns the name of the device. """ return '{} {}'.format( - verisure.STATUS[self._device][self._id].location, + verisure.CLIMATE_STATUS[self._id].location, "Humidity") @property def state(self): """ Returns the state of the device. """ # remove % character - return verisure.STATUS[self._device][self._id].humidity[:-1] + return verisure.CLIMATE_STATUS[self._id].humidity[:-1] @property def unit_of_measurement(self): @@ -99,4 +97,4 @@ class VerisureHygrometer(Entity): def update(self): ''' update sensor ''' - verisure.update() + verisure.update_climate() diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index a2893df76dd..c698a33ce18 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -25,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switches.extend([ VerisureSmartplug(value) - for value in verisure.get_smartplug_status().values() + for value in verisure.SMARTPLUG_STATUS.values() if verisure.SHOW_SMARTPLUGS ]) @@ -36,31 +36,29 @@ class VerisureSmartplug(SwitchDevice): """ Represents a Verisure smartplug. """ def __init__(self, smartplug_status): self._id = smartplug_status.id - self.status_on = verisure.MY_PAGES.SMARTPLUG_ON - self.status_off = verisure.MY_PAGES.SMARTPLUG_OFF @property def name(self): """ Get the name (location) of the smartplug. """ - return verisure.get_smartplug_status()[self._id].location + return verisure.SMARTPLUG_STATUS[self._id].location @property def is_on(self): """ Returns True if on """ - plug_status = verisure.get_smartplug_status()[self._id].status - return plug_status == self.status_on + plug_status = verisure.SMARTPLUG_STATUS[self._id].status + return plug_status == 'on' def turn_on(self): """ Set smartplug status on. """ - verisure.MY_PAGES.set_smartplug_status( - self._id, - self.status_on) + verisure.MY_PAGES.smartplug.set(self._id, 'on') + verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'on') + verisure.update_smartplug() def turn_off(self): """ Set smartplug status off. """ - verisure.MY_PAGES.set_smartplug_status( - self._id, - self.status_off) + verisure.MY_PAGES.smartplug.set(self._id, 'off') + verisure.MY_PAGES.smartplug.wait_while_updating(self._id, 'off') + verisure.update_smartplug() def update(self): - verisure.update() + verisure.update_smartplug() diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 837acbd18ae..821a2ddb0d0 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -7,6 +7,8 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/verisure/ """ import logging +import time + from datetime import timedelta from homeassistant import bootstrap @@ -28,13 +30,15 @@ DISCOVER_ALARMS = 'verisure.alarm_control_panel' DEPENDENCIES = ['alarm_control_panel'] REQUIREMENTS = [ 'https://github.com/persandstrom/python-verisure/archive/' - '9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6' -] + '0f53c1d6a9e370566a78e36093b02fbd5144b75d.zip#python-verisure==0.4.1' + ] _LOGGER = logging.getLogger(__name__) MY_PAGES = None -STATUS = {} +ALARM_STATUS = {} +SMARTPLUG_STATUS = {} +CLIMATE_STATUS = {} VERISURE_LOGIN_ERROR = None VERISURE_ERROR = None @@ -47,7 +51,7 @@ SHOW_SMARTPLUGS = True # if wrong password was given don't try again WRONG_PASSWORD_GIVEN = False -MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=5) +MIN_TIME_BETWEEN_REQUESTS = timedelta(seconds=1) def setup(hass, config): @@ -60,10 +64,6 @@ def setup(hass, config): from verisure import MyPages, LoginError, Error - STATUS[MyPages.DEVICE_ALARM] = {} - STATUS[MyPages.DEVICE_CLIMATE] = {} - STATUS[MyPages.DEVICE_SMARTPLUG] = {} - global SHOW_THERMOMETERS, SHOW_HYGROMETERS, SHOW_ALARM, SHOW_SMARTPLUGS SHOW_THERMOMETERS = int(config[DOMAIN].get('thermometers', '1')) SHOW_HYGROMETERS = int(config[DOMAIN].get('hygrometers', '1')) @@ -84,7 +84,9 @@ def setup(hass, config): _LOGGER.error('Could not log in to verisure mypages, %s', ex) return False - update() + update_alarm() + update_climate() + update_smartplug() # Load components for the devices in the ISY controller that we support for comp_name, discovery in ((('sensor', DISCOVER_SENSORS), @@ -101,24 +103,10 @@ def setup(hass, config): return True -def get_alarm_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_ALARM] - - -def get_climate_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_CLIMATE] - - -def get_smartplug_status(): - """ Return a list of status overviews for alarm components. """ - return STATUS[MY_PAGES.DEVICE_SMARTPLUG] - - def reconnect(): """ Reconnect to verisure mypages. """ try: + time.sleep(1) MY_PAGES.login() except VERISURE_LOGIN_ERROR as ex: _LOGGER.error("Could not login to Verisure mypages, %s", ex) @@ -129,19 +117,28 @@ def reconnect(): @Throttle(MIN_TIME_BETWEEN_REQUESTS) -def update(): +def update_alarm(): + update_component(MY_PAGES.alarm.get, ALARM_STATUS) + + +@Throttle(MIN_TIME_BETWEEN_REQUESTS) +def update_climate(): + update_component(MY_PAGES.climate.get, CLIMATE_STATUS) + + +@Throttle(MIN_TIME_BETWEEN_REQUESTS) +def update_smartplug(): + update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS) + + +def update_component(get_function, status): """ Updates the status of verisure components. """ if WRONG_PASSWORD_GIVEN: _LOGGER.error('Wrong password') return - try: - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_ALARM): - STATUS[MY_PAGES.DEVICE_ALARM][overview.id] = overview - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_CLIMATE): - STATUS[MY_PAGES.DEVICE_CLIMATE][overview.id] = overview - for overview in MY_PAGES.get_overview(MY_PAGES.DEVICE_SMARTPLUG): - STATUS[MY_PAGES.DEVICE_SMARTPLUG][overview.id] = overview - except ConnectionError as ex: + for overview in get_function(): + status[overview.id] = overview + except (ConnectionError, VERISURE_ERROR) as ex: _LOGGER.error('Caught connection error %s, tries to reconnect', ex) reconnect() diff --git a/requirements_all.txt b/requirements_all.txt index bb41f38c6f0..34715a19d2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ python-nest==2.6.0 radiotherm==1.2 # homeassistant.components.verisure -https://github.com/persandstrom/python-verisure/archive/9873c4527f01b1ba1f72ae60f7f35854390d59be.zip#python-verisure==0.2.6 +https://github.com/persandstrom/python-verisure/archive/0f53c1d6a9e370566a78e36093b02fbd5144b75d.zip#python-verisure==0.4.1 # homeassistant.components.zwave pydispatcher==2.0.5 From 160814f42541bf4d8e4e6c2bce0a309d0dcf47c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Fri, 25 Dec 2015 20:42:03 +0100 Subject: [PATCH 054/267] log texts --- homeassistant/components/alarm_control_panel/verisure.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 62319e62189..cc9f8dde69d 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -77,20 +77,20 @@ class VerisureAlarm(alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """ Send disarm command. """ verisure.MY_PAGES.alarm.set(code, 'DISARMED') + _LOGGER.info('verisure alarm disarming') verisure.MY_PAGES.alarm.wait_while_pending() verisure.update_alarm() - _LOGGER.info('disarming verisure alarm') def alarm_arm_home(self, code=None): """ Send arm home command. """ verisure.MY_PAGES.alarm.set(code, 'ARMED_HOME') + _LOGGER.info('verisure alarm arming home') verisure.MY_PAGES.alarm.wait_while_pending() verisure.update_alarm() - _LOGGER.info('arming home verisure alarm') def alarm_arm_away(self, code=None): """ Send arm away command. """ verisure.MY_PAGES.alarm.set(code, 'ARMED_AWAY') + _LOGGER.info('verisure alarm arming away') verisure.MY_PAGES.alarm.wait_while_pending() verisure.update_alarm() - _LOGGER.info('arming away') From a577d5c5fa5059b494c95e14db0e3f90ee3b8fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Fri, 25 Dec 2015 20:57:30 +0100 Subject: [PATCH 055/267] revert accidental change --- homeassistant/components/media_player/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index d566501568e..8204052b4a9 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -154,7 +154,6 @@ def mute_volume(hass, mute, entity_id=None): def set_volume_level(hass, volume, entity_id=None): """ Send the media player the command for volume down. """ - print("AAAAAAAAAAAAAAAAAAAA") data = {ATTR_MEDIA_VOLUME_LEVEL: volume} if entity_id: @@ -232,11 +231,9 @@ def setup(hass, config): def volume_set_service(service): """ Set specified volume on the media player. """ - print(service.data) target_players = component.extract_from_service(service) if ATTR_MEDIA_VOLUME_LEVEL not in service.data: - print('returning') return volume = service.data[ATTR_MEDIA_VOLUME_LEVEL] From 571073fe1f0d6a49f0c858c00c96f6fca74c3c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Fri, 25 Dec 2015 21:04:16 +0100 Subject: [PATCH 056/267] added docstring --- homeassistant/components/verisure.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 821a2ddb0d0..4b1bbd4c7d6 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -118,16 +118,19 @@ def reconnect(): @Throttle(MIN_TIME_BETWEEN_REQUESTS) def update_alarm(): + """ Updates the status of alarms. """ update_component(MY_PAGES.alarm.get, ALARM_STATUS) @Throttle(MIN_TIME_BETWEEN_REQUESTS) def update_climate(): + """ Updates the status of climate sensors. """ update_component(MY_PAGES.climate.get, CLIMATE_STATUS) @Throttle(MIN_TIME_BETWEEN_REQUESTS) def update_smartplug(): + """ Updates the status of smartplugs. """ update_component(MY_PAGES.smartplug.get, SMARTPLUG_STATUS) From 03491fcc097173db437312a275856c53ce0ebed2 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 25 Dec 2015 21:19:51 +0100 Subject: [PATCH 057/267] icons --- .../components/sensor/systemmonitor.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 110d58283c3..8e18979781e 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -69,6 +69,29 @@ class SystemMonitorSensor(Entity): def name(self): return self._name.rstrip() + @property + def icon(self): + return { + 'disk_use_percent': 'mdi:harddisk', + 'disk_use': 'mdi:harddisk', + 'disk_free': 'mdi:harddisk', + 'memory_use_percent': 'mdi:memory', + 'memory_use': 'mdi:memory', + 'memory_free': 'mdi:memory', + 'swap_use_percent': 'mdi:harddisk', + 'swap_use': 'mdi:harddisk', + 'swap_free': 'mdi:harddisk', + 'processor_use': 'mdi:memory', + 'process': 'mdi:memory', + 'network_out': 'server:network', + 'network_in': 'server:network', + 'packets_out': 'server:network', + 'packets_in': 'server:network', + 'ipv4_address': 'server:network', + 'ipv6_address': 'server:network', + 'last_boot': 'mdi:clock', + 'since_last_boot': 'mdi:clock' }.get(self.type) + @property def state(self): """ Returns the state of the device. """ From b8d2f2ba37c967c3305eeea171f80eca4fdb462b Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 25 Dec 2015 21:44:32 +0100 Subject: [PATCH 058/267] moved into sensor types --- .../components/sensor/systemmonitor.py | 61 +++++++------------ 1 file changed, 21 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 8e18979781e..32682b416e6 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -14,25 +14,25 @@ from homeassistant.const import STATE_ON, STATE_OFF REQUIREMENTS = ['psutil==3.2.2'] SENSOR_TYPES = { - 'disk_use_percent': ['Disk Use', '%'], - 'disk_use': ['Disk Use', 'GiB'], - 'disk_free': ['Disk Free', 'GiB'], - 'memory_use_percent': ['RAM Use', '%'], - 'memory_use': ['RAM Use', 'MiB'], - 'memory_free': ['RAM Free', 'MiB'], - 'processor_use': ['CPU Use', '%'], - 'process': ['Process', ''], - 'swap_use_percent': ['Swap Use', '%'], - 'swap_use': ['Swap Use', 'GiB'], - 'swap_free': ['Swap Free', 'GiB'], - 'network_out': ['Sent', 'MiB'], - 'network_in': ['Recieved', 'MiB'], - 'packets_out': ['Packets sent', ''], - 'packets_in': ['Packets recieved', ''], - 'ipv4_address': ['IPv4 address', ''], - 'ipv6_address': ['IPv6 address', ''], - 'last_boot': ['Last Boot', ''], - 'since_last_boot': ['Since Last Boot', ''] + 'disk_use_percent': ['Disk Use', '%', 'mdi:harddisk'], + 'disk_use': ['Disk Use', 'GiB', 'mdi:harddisk'], + 'disk_free': ['Disk Free', 'GiB', 'mdi:harddisk'], + 'memory_use_percent': ['RAM Use', '%', 'mdi:memory'], + 'memory_use': ['RAM Use', 'MiB', 'mdi:memory'], + 'memory_free': ['RAM Free', 'MiB', 'mdi:memory'], + 'processor_use': ['CPU Use', '%', 'mdi:memory'], + 'process': ['Process', '', 'mdi:memory'], + 'swap_use_percent': ['Swap Use', '%', 'mdi:harddisk'], + 'swap_use': ['Swap Use', 'GiB', 'mdi:harddisk'], + 'swap_free': ['Swap Free', 'GiB', 'mdi:harddisk'], + 'network_out': ['Sent', 'MiB', 'mdi:server-network'], + 'network_in': ['Recieved', 'MiB', 'mdi:server-network'], + 'packets_out': ['Packets sent', '', 'mdi:server-network'], + 'packets_in': ['Packets recieved', '', 'mdi:server-network'], + 'ipv4_address': ['IPv4 address', '', 'mdi:server-network'], + 'ipv6_address': ['IPv6 address', '', 'mdi:server-network'], + 'last_boot': ['Last Boot', '', 'mdi:clock'], + 'since_last_boot': ['Since Last Boot', '', 'mdi:clock'] } _LOGGER = logging.getLogger(__name__) @@ -71,27 +71,8 @@ class SystemMonitorSensor(Entity): @property def icon(self): - return { - 'disk_use_percent': 'mdi:harddisk', - 'disk_use': 'mdi:harddisk', - 'disk_free': 'mdi:harddisk', - 'memory_use_percent': 'mdi:memory', - 'memory_use': 'mdi:memory', - 'memory_free': 'mdi:memory', - 'swap_use_percent': 'mdi:harddisk', - 'swap_use': 'mdi:harddisk', - 'swap_free': 'mdi:harddisk', - 'processor_use': 'mdi:memory', - 'process': 'mdi:memory', - 'network_out': 'server:network', - 'network_in': 'server:network', - 'packets_out': 'server:network', - 'packets_in': 'server:network', - 'ipv4_address': 'server:network', - 'ipv6_address': 'server:network', - 'last_boot': 'mdi:clock', - 'since_last_boot': 'mdi:clock' }.get(self.type) - + return SENSOR_TYPES[self.type] + @property def state(self): """ Returns the state of the device. """ From 6f2078cda3a8325c11b3763440c91e50c71b49e1 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 25 Dec 2015 22:02:10 +0100 Subject: [PATCH 059/267] bugfix --- homeassistant/components/sensor/systemmonitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 32682b416e6..ecd56ad05d7 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -71,7 +71,7 @@ class SystemMonitorSensor(Entity): @property def icon(self): - return SENSOR_TYPES[self.type] + return SENSOR_TYPES[self.type][2] @property def state(self): From 40486676df90c9c46e1a5bd5b5eb54ee38181661 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Fri, 25 Dec 2015 14:34:30 -0700 Subject: [PATCH 060/267] Simplify error handling --- homeassistant/components/sensor/rest.py | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index abb6b439a84..f6a56d3a99e 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -102,13 +102,13 @@ class RestSensor(Entity): self.rest.update() value = self.rest.data - if 'error' in value: - self._state = value['error'] - else: - if self._value_template is not None: - value = template.render_with_possible_json_value( - self._hass, self._value_template, value, STATE_UNKNOWN) - self._state = value + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: + value = template.render_with_possible_json_value( + self._hass, self._value_template, value, STATE_UNKNOWN) + + self._state = value # pylint: disable=too-few-public-methods @@ -118,7 +118,7 @@ class RestDataGet(object): def __init__(self, resource, verify_ssl): self._resource = resource self._verify_ssl = verify_ssl - self.data = dict() + self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -126,12 +126,10 @@ class RestDataGet(object): try: response = requests.get(self._resource, timeout=10, verify=self._verify_ssl) - if isinstance(self.data, dict) and 'error' in self.data: - del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data['error'] = STATE_UNKNOWN + self.data = None # pylint: disable=too-few-public-methods @@ -142,7 +140,7 @@ class RestDataPost(object): self._resource = resource self._payload = payload self._verify_ssl = verify_ssl - self.data = dict() + self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -150,9 +148,7 @@ class RestDataPost(object): try: response = requests.post(self._resource, data=self._payload, timeout=10, verify=self._verify_ssl) - if isinstance(self.data, dict) and 'error' in self.data: - del self.data['error'] self.data = response.text except requests.exceptions.ConnectionError: _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data['error'] = STATE_UNKNOWN + self.data = None From 089bbfc5cc4d09e63fe3a25544ced9b5a9e1c2a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Dec 2015 17:19:53 -0800 Subject: [PATCH 061/267] Travis: Less verbose requirements_all.txt check --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a75cf6685d3..50f68a2c87f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: - 3.5 install: # Validate requirements_all.txt on Python 3.5 - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py develop; script/gen_requirements_all.py validate; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py -q develop; script/gen_requirements_all.py validate; fi - script/bootstrap_server script: - script/cibuild From add24915a3d8791bb86788fe3758de8e39592f65 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 Dec 2015 17:48:20 -0800 Subject: [PATCH 062/267] ps - clean up sun automation tests --- homeassistant/components/automation/sun.py | 7 +- tests/components/automation/test_sun.py | 394 ++++++++------------- 2 files changed, 152 insertions(+), 249 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index a5035c8936a..394dc904be1 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -93,15 +93,10 @@ def if_action(hass, config): else: after_func = lambda: sun.next_setting_utc(hass) + after_offset - # This is needed for testing - time_func = dt_util.utcnow - def time_if(): """ Validate time based if-condition """ - # This is needed for testing. - nonlocal time_func - now = time_func() + now = dt_util.utcnow() before = before_func() after = after_func() diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 87f04a325e8..26ecc26c72a 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -140,125 +140,147 @@ class TestAutomationSun(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - def test_if_action_before_before(self): + def test_if_action_before(self): self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', }) - now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - - with patch('homeassistant.components.automation.sun.dt_util.utcnow', - return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'before': 'sunrise', - }, - 'action': { - 'service': 'test.automation' - } + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + }, + 'action': { + 'service': 'test.automation' } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_if_action_after_before(self): - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + } }) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - with patch('homeassistant.components.automation.sun.dt_util.utcnow', return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'before': 'sunrise', - }, - 'action': { - 'service': 'test.automation' - } - } - }) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) + now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) - def test_if_action_before_after(self): + def test_if_action_after(self): self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', }) + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - with patch('homeassistant.components.automation.sun.dt_util.utcnow', return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'after': 'sunrise', - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_if_action_after_after(self): - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_SETTING: '14:00:00 16-09-2015', - }) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - with patch('homeassistant.components.automation.sun.dt_util.utcnow', return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'after': 'sunset', - }, - 'action': { - 'service': 'test.automation' - } - } - }) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) + def test_if_action_before_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'before': 'sunrise', + 'before_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_with_offset(self): + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'after_offset': '+1:00:00' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) def test_if_action_before_and_after_during(self): self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { @@ -266,154 +288,40 @@ class TestAutomationSun(unittest.TestCase): sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', }) + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunrise', + 'before': 'sunset' + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) + with patch('homeassistant.components.automation.sun.dt_util.utcnow', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - with patch('homeassistant.components.automation.sun.dt_util.utcnow', return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'after': 'sunrise', - 'before': 'sunset' - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_if_action_before_and_after_before(self): - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', - sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', - }) - - now = datetime(2015, 9, 16, 8, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - - with patch('homeassistant.components.automation.sun.dt_util.utcnow', - return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'after': 'sunrise', - 'before': 'sunset' - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_if_action_before_and_after_after(self): - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '10:00:00 16-09-2015', - sun.STATE_ATTR_NEXT_SETTING: '15:00:00 16-09-2015', - }) - - now = datetime(2015, 9, 16, 16, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - - with patch('homeassistant.components.automation.sun.dt_util.utcnow', - return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'after': 'sunrise', - 'before': 'sunset' - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(0, len(self.calls)) - - def test_if_action_offset_before(self): - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', - }) - - now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - - with patch('homeassistant.components.automation.sun.dt_util.utcnow', - return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'before': 'sunrise', - 'before_offset': '+1:00:00' - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) - - def test_if_action_offset_after(self): - self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { - sun.STATE_ATTR_NEXT_RISING: '14:00:00 16-09-2015', - }) - - now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - entity_id = 'domain.test_entity' - - with patch('homeassistant.components.automation.sun.dt_util.utcnow', - return_value=now): - automation.setup(self.hass, { - automation.DOMAIN: { - 'trigger': { - 'platform': 'event', - 'event_type': 'test_event', - }, - 'condition': { - 'platform': 'sun', - 'after': 'sunrise', - 'after_offset': '+1:00:00' - }, - 'action': { - 'service': 'test.automation' - } - } - }) - - self.hass.bus.fire('test_event') - self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) From d63e5a60ae8b912eace3ef60dc6e385920d61a2a Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 27 Dec 2015 12:32:08 +0100 Subject: [PATCH 063/267] added rudimentary support for telldus live --- .coveragerc | 4 + homeassistant/components/sensor/__init__.py | 6 +- .../components/sensor/tellduslive.py | 100 +++++++++ homeassistant/components/switch/__init__.py | 3 +- .../components/switch/tellduslive.py | 70 ++++++ homeassistant/components/tellduslive.py | 210 ++++++++++++++++++ requirements_all.txt | 3 + 7 files changed, 393 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/sensor/tellduslive.py create mode 100644 homeassistant/components/switch/tellduslive.py create mode 100644 homeassistant/components/tellduslive.py diff --git a/.coveragerc b/.coveragerc index 4b916a7fbcd..031c7ca445d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -15,6 +15,10 @@ omit = homeassistant/components/*/modbus.py homeassistant/components/*/tellstick.py + + homeassistant/components/tellduslive.py + homeassistant/components/*/tellduslive.py + homeassistant/components/*/vera.py homeassistant/components/ecobee.py diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 04770ced241..9a6456857b8 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, tellduslive) 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', + tellduslive.DISCOVER_SENSORS: 'tellduslive', } diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py new file mode 100644 index 00000000000..de745c1cde5 --- /dev/null +++ b/homeassistant/components/sensor/tellduslive.py @@ -0,0 +1,100 @@ +""" +homeassistant.components.sensor.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Shows sensor values from Tellstick Net/Telstick Live. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tellduslive/ + +""" +import logging + +from datetime import datetime + +from homeassistant.const import TEMP_CELCIUS, ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity +from homeassistant.components import tellduslive + +ATTR_LAST_UPDATED = "time_last_updated" + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['tellduslive'] + +SENSOR_TYPE_TEMP = "temp" +SENSOR_TYPE_HUMIDITY = "humidity" + +SENSOR_TYPES = { + SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], + SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up Tellstick sensors. """ + sensors = tellduslive.NETWORK.get_sensors() + devices = [] + + for component in sensors: + for sensor in component["data"]: + # one component can have more than one sensor + # (e.g. both humidity and temperature) + devices.append(TelldusLiveSensor(component["id"], + component["name"], + sensor["name"])) + add_devices(devices) + + +class TelldusLiveSensor(Entity): + """ Represents a Telldus Live sensor. """ + + def __init__(self, sensor_id, sensor_name, sensor_type): + self._sensor_id = sensor_id + self._sensor_type = sensor_type + self._state = None + self._name = sensor_name + ' ' + SENSOR_TYPES[sensor_type][0] + self._last_update = None + self._battery_level = None + self.update() + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def state_attributes(self): + attrs = dict() + if self._battery_level is not None: + attrs[ATTR_BATTERY_LEVEL] = self._battery_level + if self._last_update is not None: + attrs[ATTR_LAST_UPDATED] = self._last_update + return attrs + + @property + def unit_of_measurement(self): + return SENSOR_TYPES[self._sensor_type][1] + + @property + def icon(self): + return SENSOR_TYPES[self._sensor_type][2] + + def update(self): + values = tellduslive.NETWORK.get_sensor_value(self._sensor_id, + self._sensor_type) + self._state, self._battery_level, self._last_update = values + + self._state = float(self._state) + if self._sensor_type == SENSOR_TYPE_TEMP: + self._state = round(self._state, 1) + elif self._sensor_type == SENSOR_TYPE_HUMIDITY: + self._state = int(round(self._state)) + + self._battery_level = round(self._battery_level * 100 / 255) # percent + self._battery_level = "%d %%" % self._battery_level + + self._last_update = str(datetime.fromtimestamp(self._last_update)) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index e7b3c629f39..e2fbb256fb5 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, tellduslive) DOMAIN = 'switch' SCAN_INTERVAL = 30 @@ -40,6 +40,7 @@ DISCOVERY_PLATFORMS = { isy994.DISCOVER_SWITCHES: 'isy994', verisure.DISCOVER_SWITCHES: 'verisure', zwave.DISCOVER_SWITCHES: 'zwave', + tellduslive.DISCOVER_SWITCHES: 'tellduslive', } PROP_TO_ATTR = { diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py new file mode 100644 index 00000000000..a6b3b53f0b2 --- /dev/null +++ b/homeassistant/components/switch/tellduslive.py @@ -0,0 +1,70 @@ +""" +homeassistant.components.switch.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Tellstick switches using Tellstick Net and +the Telldus Live online service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.tellduslive/ + +""" +import logging + +from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN) +from homeassistant.components import tellduslive +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['tellduslive'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Find and return Tellstick switches. """ + switches = tellduslive.NETWORK.get_switches() + add_devices([TelldusLiveSwitch(switch["name"], + switch["id"]) + for switch in switches if switch["type"] == "device"]) + + +class TelldusLiveSwitch(ToggleEntity): + """ Represents a Tellstick switch. """ + + def __init__(self, name, switch_id): + self._name = name + self._id = switch_id + self._state = STATE_UNKNOWN + self.update() + + @property + def should_poll(self): + """ Tells Home Assistant to poll this entity. """ + return True + + @property + def name(self): + """ Returns the name of the switch if any. """ + return self._name + + def update(self): + from tellcore.constants import ( + TELLSTICK_TURNON, TELLSTICK_TURNOFF) + states = {TELLSTICK_TURNON: STATE_ON, + TELLSTICK_TURNOFF: STATE_OFF} + state = tellduslive.NETWORK.get_switch_state(self._id) + self._state = states[state] + + @property + def is_on(self): + """ True if switch is on. """ + self.update() + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """ Turns the switch on. """ + if tellduslive.NETWORK.turn_switch_on(self._id): + self._state = STATE_ON + + def turn_off(self, **kwargs): + """ Turns the switch off. """ + if tellduslive.NETWORK.turn_switch_off(self._id): + self._state = STATE_OFF diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py new file mode 100644 index 00000000000..b77bd20379f --- /dev/null +++ b/homeassistant/components/tellduslive.py @@ -0,0 +1,210 @@ +""" +homeassistant.components.tellduslive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tellduslive Component + +This component adds support for the Telldus Live service. +Telldus Live is the online service used with Tellstick Net devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.tellduslive/ + +Developer access to the Telldus Live service is neccessary +API keys can be aquired from https://api.telldus.com/keys/index + +Tellstick Net devices can be auto discovered using the method described in: +https://developer.telldus.com/doxygen/html/TellStickNet.html + +It might be possible to communicate with the Tellstick Net device +directly, bypassing the Tellstick Live service. +This however is poorly documented and yet not fully supported (?) according to +http://developer.telldus.se/ticket/114 and +https://developer.telldus.com/doxygen/html/TellStickNet.html + +API requests to certain methods, as described in +https://api.telldus.com/explore/sensor/info +are limited to one request every 10 minutes + +""" + +from datetime import timedelta +import logging + +from homeassistant.loader import get_component +from homeassistant import bootstrap +from homeassistant.util import Throttle +from homeassistant.helpers import validate_config +from homeassistant.const import ( + EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED) + + +DOMAIN = "tellduslive" +DISCOVER_SWITCHES = "tellduslive.switches" +DISCOVER_SENSORS = "tellduslive.sensors" + +CONF_PUBLIC_KEY = "public_key" +CONF_PRIVATE_KEY = "private_key" +CONF_TOKEN = "token" +CONF_TOKEN_SECRET = "token_secret" + +REQUIREMENTS = ['tellive-py==0.5.2'] +_LOGGER = logging.getLogger(__name__) + +NETWORK = None + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) + + +class TelldusLiveData(object): + """ Gets the latest data and update the states. """ + + def __init__(self, hass, config): + + public_key = config[DOMAIN].get(CONF_PUBLIC_KEY) + private_key = config[DOMAIN].get(CONF_PRIVATE_KEY) + token = config[DOMAIN].get(CONF_TOKEN) + token_secret = config[DOMAIN].get(CONF_TOKEN_SECRET) + + from tellive.client import LiveClient + from tellive.live import TelldusLive + + self._sensors = [] + self._switches = [] + + self._client = LiveClient(public_key=public_key, + private_key=private_key, + access_token=token, + access_secret=token_secret) + self._api = TelldusLive(self._client) + + def update(self, hass, config): + """ Send discovery event if component not yet discovered """ + self._update_sensors() + self._update_switches() + for component_name, found_devices, discovery_type in \ + (('sensor', self._sensors, DISCOVER_SENSORS), + ('switch', self._switches, DISCOVER_SWITCHES)): + if len(found_devices): + component = get_component(component_name) + bootstrap.setup_component(hass, component.DOMAIN, config) + hass.bus.fire(EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: discovery_type, + ATTR_DISCOVERED: {}}) + + def _request(self, what, **params): + """ Sends a request to the tellstick live API """ + + from tellcore.constants import ( + TELLSTICK_TURNON, TELLSTICK_TURNOFF, TELLSTICK_TOGGLE) + + supported_methods = TELLSTICK_TURNON \ + | TELLSTICK_TURNOFF \ + | TELLSTICK_TOGGLE + + default_params = {'supportedMethods': supported_methods, + "includeValues": 1, + "includeScale": 1} + + params.update(default_params) + + # room for improvement: the telllive library doesn't seem to + # re-use sessions, instead it opens a new session for each request + # this needs to be fixed + response = self._client.request(what, params) + return response + + def check_request(self, what, **params): + """ Make request, check result if successful """ + return self._request(what, **params) == "success" + + def validate_session(self): + """ Make a dummy request to see if the session is valid """ + try: + response = self._request("user/profile") + return 'email' in response + except RuntimeError: + return False + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def _update_sensors(self): + """ Get the latest sensor data from Telldus Live """ + _LOGGER.info("Updating sensors from Telldus Live") + self._sensors = self._request("sensors/list")["sensor"] + + def _update_switches(self): + """ Get the configured switches from Telldus Live""" + _LOGGER.info("Updating switches from Telldus Live") + self._switches = self._request("devices/list")["device"] + # filter out any group of switches + self._switches = [switch for switch in self._switches + if switch["type"] == "device"] + + def get_sensors(self): + """ Get the configured sensors """ + self._update_sensors() + return self._sensors + + def get_switches(self): + """ Get the configured switches """ + self._update_switches() + return self._switches + + def get_sensor_value(self, sensor_id, sensor_name): + """ Get the latest (possibly cached) sensor value """ + self._update_sensors() + for component in self._sensors: + if component["id"] == sensor_id: + for sensor in component["data"]: + if sensor["name"] == sensor_name: + return (sensor["value"], + component["battery"], + component["lastUpdated"]) + + def get_switch_state(self, switch_id): + """ returns state of switch. """ + _LOGGER.info("Updating switch state from Telldus Live") + return int(self._request("device/info", id=switch_id)["state"]) + + def turn_switch_on(self, switch_id): + """ turn switch off """ + return self.check_request("device/turnOn", id=switch_id) + + def turn_switch_off(self, switch_id): + """ turn switch on """ + return self.check_request("device/turnOff", id=switch_id) + + +def setup(hass, config): + """ Setup the tellduslive component """ + + # fixme: aquire app key and provide authentication + # using username + password + if not validate_config(config, + {DOMAIN: [CONF_PUBLIC_KEY, + CONF_PRIVATE_KEY, + CONF_TOKEN, + CONF_TOKEN_SECRET]}, + _LOGGER): + _LOGGER.error( + "Configuration Error: " + "Please make sure you have configured your keys " + "that can be aquired from https://api.telldus.com/keys/index") + return False + + # fixme: validate key? + + global NETWORK + NETWORK = TelldusLiveData(hass, config) + + if not NETWORK.validate_session(): + _LOGGER.error( + "Authentication Error: " + "Please make sure you have configured your keys " + "that can be aquired from https://api.telldus.com/keys/index") + return False + + NETWORK.update(hass, config) + + return True diff --git a/requirements_all.txt b/requirements_all.txt index f894e85afbe..62067ea4868 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,6 +176,9 @@ orvibo==1.1.0 # homeassistant.components.switch.wemo pywemo==0.3.3 +# homeassistant.components.tellduslive +tellive-py==0.5.2 + # homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 From 6e813917112b50128f8214c884227d5b55f4b5d2 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 27 Dec 2015 18:37:34 +0100 Subject: [PATCH 064/267] no percentage sign --- homeassistant/components/sensor/tellduslive.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index de745c1cde5..7cc49e3c611 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -95,6 +95,5 @@ class TelldusLiveSensor(Entity): self._state = int(round(self._state)) self._battery_level = round(self._battery_level * 100 / 255) # percent - self._battery_level = "%d %%" % self._battery_level self._last_update = str(datetime.fromtimestamp(self._last_update)) From f10ecb2a8d4a5f13f1c0806f7dd9d6c6092db580 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 27 Dec 2015 18:42:10 +0100 Subject: [PATCH 065/267] remove obsolete comment --- homeassistant/components/tellduslive.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index b77bd20379f..d16a45c6851 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -193,8 +193,6 @@ def setup(hass, config): "that can be aquired from https://api.telldus.com/keys/index") return False - # fixme: validate key? - global NETWORK NETWORK = TelldusLiveData(hass, config) From efbaf47dc7ca402749e92c6177aa1690203a6307 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 27 Dec 2015 18:49:11 +0100 Subject: [PATCH 066/267] reference constants from tellive package --- homeassistant/components/tellduslive.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index d16a45c6851..3c5c135ba3f 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -96,12 +96,11 @@ class TelldusLiveData(object): def _request(self, what, **params): """ Sends a request to the tellstick live API """ - from tellcore.constants import ( - TELLSTICK_TURNON, TELLSTICK_TURNOFF, TELLSTICK_TOGGLE) + from tellive.live import const - supported_methods = TELLSTICK_TURNON \ - | TELLSTICK_TURNOFF \ - | TELLSTICK_TOGGLE + supported_methods = const.TELLSTICK_TURNON \ + | const.TELLSTICK_TURNOFF \ + | const.TELLSTICK_TOGGLE default_params = {'supportedMethods': supported_methods, "includeValues": 1, From 9e1ecd7124af55e84d113a63d189262faafa5ad0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 09:39:22 -0800 Subject: [PATCH 067/267] Fix flaky history test --- homeassistant/components/recorder.py | 29 +++++----- tests/components/test_history.py | 86 +++++++++++++++------------- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/recorder.py b/homeassistant/components/recorder.py index 126d8c9f40e..802634715e9 100644 --- a/homeassistant/components/recorder.py +++ b/homeassistant/components/recorder.py @@ -16,7 +16,7 @@ import json import atexit from homeassistant.core import Event, EventOrigin, State -import homeassistant.util.dt as date_util +import homeassistant.util.dt as dt_util from homeassistant.remote import JSONEncoder from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, @@ -62,8 +62,8 @@ def row_to_state(row): try: return State( row[1], row[2], json.loads(row[3]), - date_util.utc_from_timestamp(row[4]), - date_util.utc_from_timestamp(row[5])) + dt_util.utc_from_timestamp(row[4]), + dt_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state: %s", row) @@ -74,7 +74,7 @@ def row_to_event(row): """ Convert a databse row to an event. """ try: return Event(row[1], json.loads(row[2]), EventOrigin(row[3]), - date_util.utc_from_timestamp(row[5])) + dt_util.utc_from_timestamp(row[5])) except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to event: %s", row) @@ -116,10 +116,10 @@ class RecorderRun(object): self.start = _INSTANCE.recording_start self.closed_incorrect = False else: - self.start = date_util.utc_from_timestamp(row[1]) + self.start = dt_util.utc_from_timestamp(row[1]) if row[2] is not None: - self.end = date_util.utc_from_timestamp(row[2]) + self.end = dt_util.utc_from_timestamp(row[2]) self.closed_incorrect = bool(row[3]) @@ -169,8 +169,8 @@ class Recorder(threading.Thread): self.queue = queue.Queue() self.quit_object = object() self.lock = threading.Lock() - self.recording_start = date_util.utcnow() - self.utc_offset = date_util.now().utcoffset().total_seconds() + self.recording_start = dt_util.utcnow() + self.utc_offset = dt_util.now().utcoffset().total_seconds() def start_recording(event): """ Start recording. """ @@ -217,10 +217,11 @@ class Recorder(threading.Thread): def shutdown(self, event): """ Tells the recorder to shut down. """ self.queue.put(self.quit_object) + self.block_till_done() def record_state(self, entity_id, state, event_id): """ Save a state to the database. """ - now = date_util.utcnow() + now = dt_util.utcnow() # State got deleted if state is None: @@ -247,7 +248,7 @@ class Recorder(threading.Thread): """ Save an event to the database. """ info = ( event.event_type, json.dumps(event.data, cls=JSONEncoder), - str(event.origin), date_util.utcnow(), event.time_fired, + str(event.origin), dt_util.utcnow(), event.time_fired, self.utc_offset ) @@ -307,7 +308,7 @@ class Recorder(threading.Thread): def save_migration(migration_id): """ Save and commit a migration to the database. """ cur.execute('INSERT INTO schema_version VALUES (?, ?)', - (migration_id, date_util.utcnow())) + (migration_id, dt_util.utcnow())) self.conn.commit() _LOGGER.info("Database migrated to version %d", migration_id) @@ -420,18 +421,18 @@ class Recorder(threading.Thread): self.query( """INSERT INTO recorder_runs (start, created, utc_offset) VALUES (?, ?, ?)""", - (self.recording_start, date_util.utcnow(), self.utc_offset)) + (self.recording_start, dt_util.utcnow(), self.utc_offset)) def _close_run(self): """ Save end time for current run. """ self.query( "UPDATE recorder_runs SET end=? WHERE start=?", - (date_util.utcnow(), self.recording_start)) + (dt_util.utcnow(), self.recording_start)) def _adapt_datetime(datetimestamp): """ Turn a datetime into an integer for in the DB. """ - return date_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() + return dt_util.as_utc(datetimestamp.replace(microsecond=0)).timestamp() def _verify_instance(): diff --git a/tests/components/test_history.py b/tests/components/test_history.py index fdd8270a661..f9e773c499a 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -5,11 +5,10 @@ tests.test_component_history Tests the history component. """ # pylint: disable=protected-access,too-many-public-methods -import time +from datetime import timedelta import os import unittest from unittest.mock import patch -from datetime import timedelta import homeassistant.core as ha import homeassistant.util.dt as dt_util @@ -25,21 +24,24 @@ class TestComponentHistory(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ self.hass = get_test_home_assistant(1) - self.init_rec = False def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ self.hass.stop() - if self.init_rec: - recorder._INSTANCE.block_till_done() - os.remove(self.hass.config.path(recorder.DB_FILE)) + db_path = self.hass.config.path(recorder.DB_FILE) + if os.path.isfile(db_path): + os.remove(db_path) def init_recorder(self): recorder.setup(self.hass, {}) self.hass.start() + self.wait_recording_done() + + def wait_recording_done(self): + """ Block till recording is done. """ + self.hass.pool.block_till_done() recorder._INSTANCE.block_till_done() - self.init_rec = True def test_setup(self): """ Test setup method of history. """ @@ -56,12 +58,11 @@ class TestComponentHistory(unittest.TestCase): for i in range(7): self.hass.states.set(entity_id, "State {}".format(i)) + self.wait_recording_done() + if i > 1: states.append(self.hass.states.get(entity_id)) - self.hass.pool.block_till_done() - recorder._INSTANCE.block_till_done() - self.assertEqual( list(reversed(states)), history.last_5_states(entity_id)) @@ -70,22 +71,9 @@ class TestComponentHistory(unittest.TestCase): self.init_recorder() states = [] - for i in range(5): - state = ha.State( - 'test.point_in_time_{}'.format(i % 5), - "State {}".format(i), - {'attribute_test': i}) - - mock_state_change_event(self.hass, state) - self.hass.pool.block_till_done() - - states.append(state) - - recorder._INSTANCE.block_till_done() - - point = dt_util.utcnow() + timedelta(seconds=1) - - with patch('homeassistant.util.dt.utcnow', return_value=point): + now = dt_util.utcnow() + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=now): for i in range(5): state = ha.State( 'test.point_in_time_{}'.format(i % 5), @@ -93,16 +81,32 @@ class TestComponentHistory(unittest.TestCase): {'attribute_test': i}) mock_state_change_event(self.hass, state) - self.hass.pool.block_till_done() + + states.append(state) + + self.wait_recording_done() + + future = now + timedelta(seconds=1) + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=future): + for i in range(5): + state = ha.State( + 'test.point_in_time_{}'.format(i % 5), + "State {}".format(i), + {'attribute_test': i}) + + mock_state_change_event(self.hass, state) + + self.wait_recording_done() # Get states returns everything before POINT self.assertEqual(states, - sorted(history.get_states(point), + sorted(history.get_states(future), key=lambda state: state.entity_id)) # Test get_state here because we have a DB setup self.assertEqual( - states[0], history.get_state(point, states[0].entity_id)) + states[0], history.get_state(future, states[0].entity_id)) def test_state_changes_during_period(self): self.init_recorder() @@ -110,19 +114,20 @@ class TestComponentHistory(unittest.TestCase): def set_state(state): self.hass.states.set(entity_id, state) - self.hass.pool.block_till_done() - recorder._INSTANCE.block_till_done() - + self.wait_recording_done() return self.hass.states.get(entity_id) - set_state('idle') - set_state('YouTube') - start = dt_util.utcnow() point = start + timedelta(seconds=1) end = point + timedelta(seconds=1) - with patch('homeassistant.util.dt.utcnow', return_value=point): + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=start): + set_state('idle') + set_state('YouTube') + + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=point): states = [ set_state('idle'), set_state('Netflix'), @@ -130,10 +135,11 @@ class TestComponentHistory(unittest.TestCase): set_state('YouTube'), ] - with patch('homeassistant.util.dt.utcnow', return_value=end): + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=end): set_state('Netflix') set_state('Plex') - self.assertEqual( - {entity_id: states}, - history.state_changes_during_period(start, end, entity_id)) + hist = history.state_changes_during_period(start, end, entity_id) + + self.assertEqual(states, hist[entity_id]) From eb3da8cb033dce1361f30a066ed4683391f6f9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 27 Dec 2015 20:07:09 +0100 Subject: [PATCH 068/267] verisure component from pypi --- homeassistant/components/verisure.py | 5 +---- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 4b1bbd4c7d6..5a4d7c7ea99 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -28,10 +28,7 @@ DISCOVER_SWITCHES = 'verisure.switches' DISCOVER_ALARMS = 'verisure.alarm_control_panel' DEPENDENCIES = ['alarm_control_panel'] -REQUIREMENTS = [ - 'https://github.com/persandstrom/python-verisure/archive/' - '0f53c1d6a9e370566a78e36093b02fbd5144b75d.zip#python-verisure==0.4.1' - ] +REQUIREMENTS = ['vsure==0.4.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f894e85afbe..c4201be19c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,7 +189,7 @@ python-nest==2.6.0 radiotherm==1.2 # homeassistant.components.verisure -https://github.com/persandstrom/python-verisure/archive/0f53c1d6a9e370566a78e36093b02fbd5144b75d.zip#python-verisure==0.4.1 +vsure==0.4.3 # homeassistant.components.zwave pydispatcher==2.0.5 From ab5a3f9de3215ddf9cb9e9ed10ebe31446ef2c29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 11:07:25 -0800 Subject: [PATCH 069/267] Clean up YR sensor --- homeassistant/components/sensor/yr.py | 134 ++++++++++---------------- homeassistant/util/dt.py | 4 +- homeassistant/util/location.py | 19 ++++ tests/components/sensor/test_yr.py | 18 ++-- 4 files changed, 82 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 36dd193da97..8e1619a5941 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -36,17 +36,17 @@ sensor: """ import logging -import datetime -import urllib.request + import requests from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.helpers.entity import Entity +from homeassistant.util import location, dt as dt_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['xmltodict', 'astral==0.8.1'] +REQUIREMENTS = ['xmltodict'] # Sensor types are defined like so: SENSOR_TYPES = { @@ -73,19 +73,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - from astral import Location, GoogleGeocoder - location = Location(('', '', hass.config.latitude, hass.config.longitude, - hass.config.time_zone, 0)) + elevation = config.get('elevation') - google = GoogleGeocoder() - try: - google._get_elevation(location) # pylint: disable=protected-access - _LOGGER.info( - 'Retrieved elevation from Google: %s', location.elevation) - elevation = location.elevation - except urllib.error.URLError: - # If no internet connection available etc. - elevation = 0 + if elevation is None: + elevation = location.elevation(hass.config.latitude, + hass.config.longitude) coordinates = dict(lat=hass.config.latitude, lon=hass.config.longitude, msl=elevation) @@ -116,9 +108,8 @@ class YrSensor(Entity): self.type = sensor_type self._state = None self._weather = weather - self._info = '' self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._update = datetime.datetime.fromtimestamp(0) + self._update = None self.update() @@ -134,14 +125,15 @@ class YrSensor(Entity): @property def state_attributes(self): """ Returns state attributes. """ - data = {} - data[''] = "Weather forecast from yr.no, delivered by the"\ - " Norwegian Meteorological Institute and the NRK" + data = { + 'about': "Weather forecast from yr.no, delivered by the" + " Norwegian Meteorological Institute and the NRK" + } if self.type == 'symbol': symbol_nr = self._state - data[ATTR_ENTITY_PICTURE] = "http://api.met.no/weatherapi/weathericon/1.1/" \ - "?symbol=" + str(symbol_nr) + \ - ";content_type=image/png" + data[ATTR_ENTITY_PICTURE] = \ + "http://api.met.no/weatherapi/weathericon/1.1/" \ + "?symbol={0};content_type=image/png".format(symbol_nr) return data @@ -150,76 +142,50 @@ class YrSensor(Entity): """ Unit of measurement of this entity, if any. """ return self._unit_of_measurement - @property - def should_poll(self): - """ Return True if entity has to be polled for state. """ - return True - - # pylint: disable=too-many-branches, too-many-return-statements def update(self): """ Gets the latest data from yr.no and updates the states. """ - self._weather.update() - now = datetime.datetime.now() + now = dt_util.utcnow() # check if data should be updated - if now <= self._update: + if self._update is not None and now <= self._update: return - time_data = self._weather.data['product']['time'] + self._weather.update() - # pylint: disable=consider-using-enumerate # find sensor - for k in range(len(time_data)): - valid_from = datetime.datetime.strptime(time_data[k]['@from'], - "%Y-%m-%dT%H:%M:%SZ") - valid_to = datetime.datetime.strptime(time_data[k]['@to'], - "%Y-%m-%dT%H:%M:%SZ") - self._update = valid_to - self._info = "Forecast between " + time_data[k]['@from'] \ - + " and " + time_data[k]['@to'] + ". " + for time_entry in self._weather.data['product']['time']: + valid_from = dt_util.str_to_datetime( + time_entry['@from'], "%Y-%m-%dT%H:%M:%SZ") + valid_to = dt_util.str_to_datetime( + time_entry['@to'], "%Y-%m-%dT%H:%M:%SZ") - temp_data = time_data[k]['location'] - if self.type not in temp_data and now >= valid_to: + loc_data = time_entry['location'] + + if self.type not in loc_data or now >= valid_to: continue + + self._update = valid_to + if self.type == 'precipitation' and valid_from < now: - self._state = temp_data[self.type]['@value'] - return + self._state = loc_data[self.type]['@value'] + break elif self.type == 'symbol' and valid_from < now: - self._state = temp_data[self.type]['@number'] - return - elif self.type == 'temperature': - self._state = temp_data[self.type]['@value'] - return + self._state = loc_data[self.type]['@number'] + break + elif self.type == ('temperature', 'pressure', 'humidity', + 'dewpointTemperature'): + self._state = loc_data[self.type]['@value'] + break elif self.type == 'windSpeed': - self._state = temp_data[self.type]['@mps'] - return - elif self.type == 'pressure': - self._state = temp_data[self.type]['@value'] - return + self._state = loc_data[self.type]['@mps'] + break elif self.type == 'windDirection': - self._state = float(temp_data[self.type]['@deg']) - return - elif self.type == 'humidity': - self._state = temp_data[self.type]['@value'] - return - elif self.type == 'fog': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'cloudiness': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'lowClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'mediumClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'highClouds': - self._state = temp_data[self.type]['@percent'] - return - elif self.type == 'dewpointTemperature': - self._state = temp_data[self.type]['@value'] - return + self._state = float(loc_data[self.type]['@deg']) + break + elif self.type in ('fog', 'cloudiness', 'lowClouds', + 'mediumClouds', 'highClouds'): + self._state = loc_data[self.type]['@percent'] + break # pylint: disable=too-few-public-methods @@ -230,14 +196,14 @@ class YrData(object): self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \ 'lat={lat};lon={lon};msl={msl}'.format(**coordinates) - self._nextrun = datetime.datetime.fromtimestamp(0) + self._nextrun = None + self.data = {} self.update() def update(self): """ Gets the latest data from yr.no """ - now = datetime.datetime.now() # check if new will be available - if now <= self._nextrun: + if self._nextrun is not None and dt_util.utcnow() <= self._nextrun: return try: response = requests.get(self._url) @@ -252,5 +218,5 @@ class YrData(object): model = self.data['meta']['model'] if '@nextrun' not in model: model = model[0] - self._nextrun = datetime.datetime.strptime(model['@nextrun'], - "%Y-%m-%dT%H:%M:%SZ") + self._nextrun = dt_util.str_to_datetime(model['@nextrun'], + "%Y-%m-%dT%H:%M:%SZ") diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 35795a7ae7f..a2c796c20eb 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -108,14 +108,14 @@ def datetime_to_date_str(dattim): return dattim.strftime(DATE_STR_FORMAT) -def str_to_datetime(dt_str): +def str_to_datetime(dt_str, dt_format=DATETIME_STR_FORMAT): """ Converts a string to a UTC datetime object. @rtype: datetime """ try: return dt.datetime.strptime( - dt_str, DATETIME_STR_FORMAT).replace(tzinfo=pytz.utc) + dt_str, dt_format).replace(tzinfo=pytz.utc) except ValueError: # If dt_str did not match our format return None diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 398a0a0c56c..185745d9207 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -4,6 +4,8 @@ import collections import requests from vincenty import vincenty +ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' + LocationInfo = collections.namedtuple( "LocationInfo", @@ -34,3 +36,20 @@ def detect_location_info(): def distance(lat1, lon1, lat2, lon2): """ Calculate the distance in meters between two points. """ return vincenty((lat1, lon1), (lat2, lon2)) * 1000 + + +def elevation(latitude, longitude): + """ Return elevation for given latitude and longitude. """ + + req = requests.get(ELEVATION_URL, params={ + 'locations': '{},{}'.format(latitude, longitude), + 'sensor': 'false', + }) + + if req.status_code != 200: + return 0 + + try: + return int(float(req.json()['results'][0]['elevation'])) + except (ValueError, KeyError): + return 0 diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index aa9a5a59944..7e95194aa4b 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -15,12 +15,8 @@ class TestSensorYr(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name self.hass = ha.HomeAssistant() - latitude = 32.87336 - longitude = 117.22743 - - # Compare it with the real data - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude + self.hass.config.latitude = 32.87336 + self.hass.config.longitude = 117.22743 def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -30,6 +26,7 @@ class TestSensorYr(unittest.TestCase): self.assertTrue(sensor.setup(self.hass, { 'sensor': { 'platform': 'yr', + 'elevation': 0, } })) state = self.hass.states.get('sensor.yr_symbol') @@ -42,7 +39,14 @@ class TestSensorYr(unittest.TestCase): self.assertTrue(sensor.setup(self.hass, { 'sensor': { 'platform': 'yr', - 'monitored_conditions': {'pressure', 'windDirection', 'humidity', 'fog', 'windSpeed'} + 'elevation': 0, + 'monitored_conditions': { + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed' + } } })) state = self.hass.states.get('sensor.yr_symbol') From 8c1ebde1de67d7b2e1a4d7f9cd525531d29b8f20 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 11:10:14 -0800 Subject: [PATCH 070/267] Update requirements_all.txt --- requirements_all.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index f894e85afbe..7d3a2732965 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,6 @@ python-twitch==1.2.0 xmltodict # homeassistant.components.sun -# homeassistant.components.sensor.yr astral==0.8.1 # homeassistant.components.switch.edimax From 87c88078c87257cde4786997fedb865be6813545 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 11:22:10 -0800 Subject: [PATCH 071/267] Update sun to use elevation util --- homeassistant/components/sun.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 2e1c0c9b377..9d61cce0582 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -11,7 +11,7 @@ from datetime import timedelta import urllib import homeassistant.util as util -import homeassistant.util.dt as dt_util +from homeassistant.util import location as location_util, dt as dt_util from homeassistant.helpers.event import ( track_point_in_utc_time, track_utc_time_change) from homeassistant.helpers.entity import Entity @@ -111,21 +111,13 @@ def setup(hass, config): platform_config = config.get(DOMAIN, {}) elevation = platform_config.get(CONF_ELEVATION) + if elevation is None: + elevation = location_util.elevation(latitude, longitude) - from astral import Location, GoogleGeocoder + from astral import Location location = Location(('', '', latitude, longitude, hass.config.time_zone, - elevation or 0)) - - if elevation is None: - google = GoogleGeocoder() - try: - google._get_elevation(location) # pylint: disable=protected-access - _LOGGER.info( - 'Retrieved elevation from Google: %s', location.elevation) - except urllib.error.URLError: - # If no internet connection available etc. - pass + elevation)) sun = Sun(hass, location) sun.point_in_time_listener(dt_util.utcnow()) From d4b6a7343f2675ea5a064bbbc9007f94a2881fbc Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Sun, 27 Dec 2015 13:24:34 -0600 Subject: [PATCH 072/267] Fix issue with scene component when using YAML aliases. YAML aliases/anchors can make repetitive configuration sections easier to deal with. However when dealing with dictionaries, care needs to be taken to not modify the original anchor since PyYAML utilizes a reference when encountering an alias instead of a copy of the dictionary. --- homeassistant/components/scene.py | 5 +-- tests/components/test_scene.py | 54 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 7c96230ccd4..ce1a3242542 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -73,8 +73,9 @@ def _process_config(scene_config): for entity_id in c_entities: if isinstance(c_entities[entity_id], dict): - state = c_entities[entity_id].pop('state', None) - attributes = c_entities[entity_id] + entity_attrs = c_entities[entity_id].copy() + state = entity_attrs.pop('state', None) + attributes = entity_attrs else: state = c_entities[entity_id] attributes = {} diff --git a/tests/components/test_scene.py b/tests/components/test_scene.py index 2fc8fe085c2..0f6663354dd 100644 --- a/tests/components/test_scene.py +++ b/tests/components/test_scene.py @@ -32,6 +32,60 @@ class TestScene(unittest.TestCase): 'scene': [[]] })) + def test_config_yaml_alias_anchor(self): + """ + Tests the usage of YAML aliases and anchors. The following test scene + configuration is equivalent to: + + scene: + - name: test + entities: + light_1: &light_1_state + state: 'on' + brightness: 100 + light_2: *light_1_state + + When encountering a YAML alias/anchor, the PyYAML parser will use a + reference to the original dictionary, instead of creating a copy, so + care needs to be taken to not modify the original. + """ + test_light = loader.get_component('light.test') + test_light.init() + + self.assertTrue(light.setup(self.hass, { + light.DOMAIN: {'platform': 'test'} + })) + + light_1, light_2 = test_light.DEVICES[0:2] + + light.turn_off(self.hass, [light_1.entity_id, light_2.entity_id]) + + self.hass.pool.block_till_done() + + entity_state = { + 'state': 'on', + 'brightness': 100, + } + self.assertTrue(scene.setup(self.hass, { + 'scene': [{ + 'name': 'test', + 'entities': { + light_1.entity_id: entity_state, + light_2.entity_id: entity_state, + } + }] + })) + + scene.activate(self.hass, 'scene.test') + self.hass.pool.block_till_done() + + self.assertTrue(light_1.is_on) + self.assertTrue(light_2.is_on) + self.assertEqual(100, + light_1.last_call('turn_on')[1].get('brightness')) + self.assertEqual(100, + light_2.last_call('turn_on')[1].get('brightness')) + def test_activate_scene(self): test_light = loader.get_component('light.test') test_light.init() From c8fa6cc12778339e221b02244f6ff5376a391d22 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 27 Dec 2015 21:47:49 +0100 Subject: [PATCH 073/267] bug fix --- homeassistant/components/tellduslive.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index 3c5c135ba3f..5c314032b27 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -116,7 +116,8 @@ class TelldusLiveData(object): def check_request(self, what, **params): """ Make request, check result if successful """ - return self._request(what, **params) == "success" + response = self._request(what, **params) + return response['status'] == "success" def validate_session(self): """ Make a dummy request to see if the session is valid """ @@ -164,7 +165,8 @@ class TelldusLiveData(object): def get_switch_state(self, switch_id): """ returns state of switch. """ _LOGGER.info("Updating switch state from Telldus Live") - return int(self._request("device/info", id=switch_id)["state"]) + response = self._request("device/info", id=switch_id)["state"] + return int(response) def turn_switch_on(self, switch_id): """ turn switch off """ From 27e35d5f345a71d0261bf6290ed5f64d0e67f305 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 27 Dec 2015 21:49:45 +0100 Subject: [PATCH 074/267] don't poll (turns out a request for state immediately a state turn on/off request will not return the newly updated state. import constants from tellive, not tellcore --- homeassistant/components/switch/tellduslive.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py index a6b3b53f0b2..d515dcb50a2 100644 --- a/homeassistant/components/switch/tellduslive.py +++ b/homeassistant/components/switch/tellduslive.py @@ -38,7 +38,7 @@ class TelldusLiveSwitch(ToggleEntity): @property def should_poll(self): """ Tells Home Assistant to poll this entity. """ - return True + return False @property def name(self): @@ -46,25 +46,28 @@ class TelldusLiveSwitch(ToggleEntity): return self._name def update(self): - from tellcore.constants import ( - TELLSTICK_TURNON, TELLSTICK_TURNOFF) - states = {TELLSTICK_TURNON: STATE_ON, - TELLSTICK_TURNOFF: STATE_OFF} + from tellive.live import const state = tellduslive.NETWORK.get_switch_state(self._id) - self._state = states[state] + if state == const.TELLSTICK_TURNON: + self._state = STATE_ON + elif state == const.TELLSTICK_TURNOFF: + self._state = STATE_OFF + else: + self._state = STATE_UNKNOWN @property def is_on(self): """ True if switch is on. """ - self.update() return self._state == STATE_ON def turn_on(self, **kwargs): """ Turns the switch on. """ if tellduslive.NETWORK.turn_switch_on(self._id): self._state = STATE_ON + self.update_ha_state() def turn_off(self, **kwargs): """ Turns the switch off. """ if tellduslive.NETWORK.turn_switch_off(self._id): self._state = STATE_OFF + self.update_ha_state() From 9c3b1b7a966a050dd7081ad3e4f27c9efd5523bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 15:00:49 -0800 Subject: [PATCH 075/267] Fix sun import issue --- homeassistant/components/sun.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 9d61cce0582..fc08a4c09d8 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -8,7 +8,6 @@ https://home-assistant.io/components/sun/ """ import logging from datetime import timedelta -import urllib import homeassistant.util as util from homeassistant.util import location as location_util, dt as dt_util From 4feef3dd0d12b392041c3f12d0b1c5b8da9e1a82 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 27 Dec 2015 23:09:39 +0000 Subject: [PATCH 076/267] Simplify update state call, shutdown properly. --- homeassistant/components/switch/wemo.py | 8 ++++++-- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 1861b42f450..c15ef4612f0 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -27,6 +27,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if _WEMO_SUBSCRIPTION_REGISTRY is None: _WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() _WEMO_SUBSCRIPTION_REGISTRY.start() + def stop_wemo(event): + """ Shutdown Wemo subscriptions and subscription thread on exit""" + _WEMO_SUBSCRIPTION_REGISTRY.stop() + + hass.bus.listen_once(hass.EVENT_HOMEASSISTANT_STOP, stop_wemo) if discovery_info is not None: location = discovery_info[2] @@ -63,8 +68,7 @@ class WemoSwitch(SwitchDevice): _LOGGER.info( 'Subscription update for %s, sevice=%s params=%s', self.name, _device, _params) - self.update() - self.update_ha_state() + self.update_ha_state(True) @property def should_poll(self): diff --git a/requirements_all.txt b/requirements_all.txt index 2375721fa7d..8494573de42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,7 +174,7 @@ hikvision==0.4 orvibo==1.1.0 # homeassistant.components.switch.wemo -pywemo==0.3.3 +pywemo==0.3.4 # homeassistant.components.thermostat.heatmiser heatmiserV3==0.9.1 From 0d32bd7a19116621e0865e2a8067694d363906cc Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 27 Dec 2015 23:26:35 +0000 Subject: [PATCH 077/267] Simplify callback logging, add stop log --- homeassistant/components/switch/wemo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index c15ef4612f0..41d7034425a 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -29,6 +29,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _WEMO_SUBSCRIPTION_REGISTRY.start() def stop_wemo(event): """ Shutdown Wemo subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") _WEMO_SUBSCRIPTION_REGISTRY.stop() hass.bus.listen_once(hass.EVENT_HOMEASSISTANT_STOP, stop_wemo) @@ -66,8 +67,8 @@ 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 params=%s', - self.name, _device, _params) + 'Subscription update for %s, sevice=%s', + self.name, _device) self.update_ha_state(True) @property From b114ba56ea58e03ca3a22286f082e4b7eb63199d Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 27 Dec 2015 23:31:48 +0000 Subject: [PATCH 078/267] Fix style issue --- homeassistant/components/switch/wemo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 41d7034425a..8d91beeaf5e 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -27,6 +27,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if _WEMO_SUBSCRIPTION_REGISTRY is None: _WEMO_SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry() _WEMO_SUBSCRIPTION_REGISTRY.start() + def stop_wemo(event): """ Shutdown Wemo subscriptions and subscription thread on exit""" _LOGGER.info("Shutting down subscriptions.") From f5dd1466767a5c94e903e0eae7d2f38383251e4b Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 27 Dec 2015 23:56:18 +0000 Subject: [PATCH 079/267] Fix bug in shutdown --- homeassistant/components/switch/wemo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 8d91beeaf5e..97712493e5f 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -9,7 +9,7 @@ https://home-assistant.io/components/switch.wemo/ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY +from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP REQUIREMENTS = ['pywemo==0.3.4'] _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _LOGGER.info("Shutting down subscriptions.") _WEMO_SUBSCRIPTION_REGISTRY.stop() - hass.bus.listen_once(hass.EVENT_HOMEASSISTANT_STOP, stop_wemo) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) if discovery_info is not None: location = discovery_info[2] From 976d9f2d08294c13786e9f91c8bbd252ad997f24 Mon Sep 17 00:00:00 2001 From: pavoni Date: Mon, 28 Dec 2015 00:04:30 +0000 Subject: [PATCH 080/267] Fix style issue --- homeassistant/components/switch/wemo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 97712493e5f..a343711ccc3 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -9,7 +9,8 @@ https://home-assistant.io/components/switch.wemo/ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['pywemo==0.3.4'] _LOGGER = logging.getLogger(__name__) From 244fde880e1f1d10848becdc6f5ae2d569ae8bd9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 15:46:44 -0800 Subject: [PATCH 081/267] Convert MQTT event to lowercase --- homeassistant/components/mqtt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 37a7a63c72b..b5ea258c5cc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -30,7 +30,7 @@ DEFAULT_QOS = 0 DEFAULT_RETAIN = False SERVICE_PUBLISH = 'publish' -EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' +EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' REQUIREMENTS = ['paho-mqtt==1.1'] From 4403fe941dc3186cd4a01e3b768170b2c14051b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 16:15:39 -0800 Subject: [PATCH 082/267] Test config clean up --- .gitignore | 1 + pytest.ini => setup.cfg | 3 +++ setup.py | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) rename pytest.ini => setup.cfg (54%) diff --git a/.gitignore b/.gitignore index 8935ffedc17..3ee71808ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ Icon dist build eggs +.eggs parts bin var diff --git a/pytest.ini b/setup.cfg similarity index 54% rename from pytest.ini rename to setup.cfg index 5ee64771657..35d8212de78 100644 --- a/pytest.ini +++ b/setup.cfg @@ -1,2 +1,5 @@ +[wheel] +universal = 1 + [pytest] testpaths = tests diff --git a/setup.py b/setup.py index 5bdc07700d9..9cc79615c71 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ REQUIRES = [ 'pytz>=2015.4', 'pip>=7.0.0', 'vincenty==0.1.3', - 'jinja2>=2.8' + 'jinja2>=2.8', ] setup( @@ -33,6 +33,7 @@ setup( zip_safe=False, platforms='any', install_requires=REQUIRES, + test_suite='tests', keywords=['home', 'automation'], entry_points={ 'console_scripts': [ @@ -46,5 +47,5 @@ setup( 'Operating System :: OS Independent', 'Programming Language :: Python :: 3.4', 'Topic :: Home Automation' - ] + ], ) From acfbbb3898c659b7917b140bc1e22c720d00e319 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 16:19:37 -0800 Subject: [PATCH 083/267] Update copyright --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index b3c5e1df750..42a425b4118 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Paulus Schoutsen +Copyright (c) 2016 Paulus Schoutsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 62f21c3ac66fbdd9f86baacf977a50eabeefc81e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 16:30:32 -0800 Subject: [PATCH 084/267] colorize lint errors --- script/lint | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/script/lint b/script/lint index 75667ef88a4..1cf877e86fa 100755 --- a/script/lint +++ b/script/lint @@ -3,13 +3,16 @@ cd "$(dirname "$0")/.." echo "Checking style with flake8..." +tput setaf 1 flake8 --exclude www_static homeassistant - FLAKE8_STATUS=$? +tput sgr0 echo "Checking style with pylint..." -pylint homeassistant +tput setaf 1 +tput setaf 1; pylint homeassistant PYLINT_STATUS=$? +tput sgr0 if [ $FLAKE8_STATUS -eq 0 ] then From 680385df93a13117fbba3ec043a9cbbcb007468a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 16:30:53 -0800 Subject: [PATCH 085/267] Hide some build log spam --- .travis.yml | 2 +- script/gen_requirements_all.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 50f68a2c87f..91e989502ed 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: - 3.5 install: # Validate requirements_all.txt on Python 3.5 - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py -q develop; script/gen_requirements_all.py validate; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; fi - script/bootstrap_server script: - script/cibuild diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d2626a2701a..a134afaa359 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -115,7 +115,6 @@ def main(): if sys.argv[-1] == 'validate': if validate_file(data): - print("requirements_all.txt is up to date.") sys.exit(0) print("******* ERROR") print("requirements_all.txt is not up to date") From ca6b95783911aa350efd373585113bae3eba47d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 16:57:16 -0800 Subject: [PATCH 086/267] Make test deps explicit --- requirements.txt | 5 ----- requirements_test.txt | 6 ++++++ script/bootstrap_server | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 requirements.txt create mode 100644 requirements_test.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 14dfca13f23..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -requests>=2,<3 -pyyaml>=3.11,<4 -pytz>=2015.4 -pip>=7.0.0 -vincenty==0.1.3 \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 00000000000..679c0e99ce5 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,6 @@ +flake8>=2.5.0 +pylint>=1.5.1 +coveralls>=1.1 +pytest>=2.6.4 +pytest-cov>=2.2.0 +betamax>=0.5.1 \ No newline at end of file diff --git a/script/bootstrap_server b/script/bootstrap_server index a5533b0596d..f71abda0e65 100755 --- a/script/bootstrap_server +++ b/script/bootstrap_server @@ -6,7 +6,7 @@ python3 -m pip install -r requirements_all.txt REQ_STATUS=$? echo "Installing development dependencies.." -python3 -m pip install flake8 pylint coveralls pytest pytest-cov +python3 -m pip install -r requirements_test.txt REQ_DEV_STATUS=$? From c1eaf60461b6dd19431473e2fb54b7c0f51b7032 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 17:37:32 -0800 Subject: [PATCH 087/267] VCR YR sensor test --- homeassistant/components/sensor/yr.py | 3 +- tests/__init__.py | 4 + ...est_yr.TestSensorYr.test_custom_setup.json | 1 + ...st_yr.TestSensorYr.test_default_setup.json | 1 + tests/components/sensor/test_yr.py | 82 +++++++++---------- 5 files changed, 49 insertions(+), 42 deletions(-) create mode 100644 tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json create mode 100644 tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 8e1619a5941..cda9ba1b78f 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -206,7 +206,8 @@ class YrData(object): if self._nextrun is not None and dt_util.utcnow() <= self._nextrun: return try: - response = requests.get(self._url) + with requests.Session() as sess: + response = sess.get(self._url) except requests.RequestException: return if response.status_code != 200: diff --git a/tests/__init__.py b/tests/__init__.py index e69de29bb2d..37d3307a4ae 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +import betamax + +with betamax.Betamax.configure() as config: + config.cassette_library_dir = 'tests/cassettes' diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json new file mode 100644 index 00000000000..6bd1601260d --- /dev/null +++ b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json @@ -0,0 +1 @@ +{"http_interactions": [{"recorded_at": "2015-12-28T01:34:34", "request": {"method": "GET", "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.9.1"], "Connection": ["keep-alive"]}, "body": {"string": "", "encoding": "utf-8"}, "uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"}, "response": {"headers": {"Content-Length": ["3598"], "X-forecast-models": ["proff,ecdet"], "Via": ["1.1 varnish"], "Content-Encoding": ["gzip"], "Date": ["Mon, 28 Dec 2015 01:34:34 GMT"], "X-Varnish": ["2670913442 2670013167"], "Expires": ["Mon, 28 Dec 2015 02:01:51 GMT"], "Server": ["Apache"], "Age": ["1574"], "Content-Type": ["text/xml; charset=utf-8"], "X-Backend-Host": ["snipe_loc"], "X-slicenumber": ["30"], "Accept-Ranges": ["bytes"], "Last-Modified": ["Mon, 28 Dec 2015 01:08:20 GMT"], "Vary": ["Accept-Encoding"], "Connection": ["keep-alive"]}, "url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "body": {"base64_string": "H4sIAAAAAAAAA+2dW4/bNhOG7/srBN+vLFKiDotNb5o0LZBD8WU/tOidYytZoT7BlrNJf31lrw+yNKRNiocRdoFcJJFFS5T18OU7w+HdYz4qH/LVZFSOvO+z6Xx9+31dvBo8lOXydjh8fHz0H0N/sfo6pEFAhn+9f/dp/JDPRjfFfF2O5uN84FWfv50vPoxm+Xo5GudPx98txqOyWMyPLY2WhT/LS3++GO6/svqf4XT/sS+LVT4ercsh8bPhetfCwBuvqg/mk1cDGhB2Q+gNTe8DchuktzT4e/DzT57n3VVNjnZ/2/1jMcmn3ry6kleDN7/4b9989ANC2cAr89WsmNcaSu4JvQ2C6s/fA2+1mefzydkXVcez25DdBqQ6Ps+/l9Vnzq8jPZ7+ZbWYnR8Lj8fKxe5IfBOQmyCuf+nweNXD4z3cLVeLyWZceuPpaL1+NVguinn5unoyg+OHy2KWe9uHVf5YVnd56LerrqJ95NDstuXDo/BG07IoN5Oq9WDgTUeHf4TUT5Mwqv5rMf+6/z9CEp/SJKo3tLvKfLasHnC5WeVeUfXr/f39wNvMi/LVYJxP18VmPfC+jaabqonITwbDxumPxXzyuqjubHc92wYmk4E3yb9W3xj4dLB/xB/AEz8t83yyO+nLl4E3W1b9GPjhwPucjzZVd5W723pq4FNZTKd5q5WHzayYFOWPwyXGic8Ol1/d1jifl61zlqt8vT7c7nJ1+PjDH6PjnZIgTIGbHU8Xm0kxr07fnfvhw8Dbf0n1zakftE74svi6++SvH9/WPhoAn5wuHn/Ztv7U8ruPf15qepZPis2sds77N69////7s69J2/1VfH2onfPb729/u3Bhk/xx99u+b/5OXvN+Jjek0czdkR2nF2n7cqi8KYG7N6X63YyLZVE+Nfh077PZ8bb3nXe3/jH7vJju+uiP0aqc/tj1d/U73sw+56vqK830TXJPyUvfcH83MbdvYkSEZX4mQ9isYpQ8YJM6YMmhgV+no433rTrnImNZsuW6DsbGAG9EjM1SPzLEWLDpaxjb/horjKU+Mfau8NWI8XdFniMNgkQORh4svVLp5tYviM8a4odPZ42+S5wVP/3uHJI84z6HDBXJQxmSx7Smld9cz/K0I8tjsh0OdLCcSerlNAZO0MNysGnELCeNy7Wje4y/LYhZzh/hnnGv1ByPRq+cjiAgawhMa0UaeSvsDkbTtWAlftwRrEmwbUKPSG6PJCKwJmlDGnrawAo2jRqs1L4MMf+y9HeyjaZv5KTyHjl9k8qEcZ8DQwR0Csy8RbYyibYe8d4WlkB6V98jSaofjxOks8SYtww2jRrpLvSP8dcFPdL5w92z7xuScvsmRYRZIqebSbgTwfKYzbpiNtNlLzNJe7liYbuL9GCWQtdiArNR5DM9nI0dyBLj7wt6lvDHICx9I+syJwrSOXyKg7l7DvxY6ukIAqYHkhkZUXwyQ2xK55TqYjrk/gjTMijAQz1MJylwLSaYzqDEG6WYoTE7hK+BzL8v6JnOHe+ee99k3Khq/QgKzkrlZRC2zdg6cPZazNJzzB7jgZ++jf65WjprMp0hFSnCbARxWRNmQ8AtuYzZMAO8akupGcy2LLHxuuBGCX8IQtQ3ctI52PvHctJ5P813iHROcDXDlcwcNya43gWkJ7TmOl+L9HA7DHRCOgurX4Em5dwew4RIj4ETNCVoAH1/mehR6C7ZzhDRhQLo2SftCka7Z943GTfGmuFKaIZWUYgom4U10/l64Rx2pGyU6lo2EgDBTFfCWY2yTZfAsxfaSx1oEizJuxin4Fj6RlY3Zwq6eU8qh8+BE2LNcCU2Q2s2hEQnPlEgOulI9DjZOt0aiE4zSSsE9CtcEp0084s9S0QPfBfyB2EG77uqs8sGRwxZ8cKx7rl3DS+6muHKcJZNiEuSWlTvesTSjohNt4JNE2Il1440E3s9bYiFPm8ipgfegoJoDkzlaYgECZo0XoTTbzR9IyeaqdJqwNS12cyLrWa4UpwhBSYiepqcVnZbFM1prCvDGQKio0UrtogO3oIS0Y2ZzQL988yzeIWj3bPvG35oFVmGs6Q1kTqxJtJMU0hP2pqAoNYvyGqympulkewoEiwZvDWOfNrMT/wwVFFEOO5g6RPZrOZAQS3vw8juOM4PqCLLapbKtqMkqPkff14P8qgjyDNda7wplKt2odicqaihLZKDt4DLAOHLnpfEXf4wh7Fv7LnMYcALqJ4dQUBZIlehiFaQClQo29VmznbpHS5yM0jQf8EM34NKLM+YOuSoEivvC3rMwkMQpr6RTc6IlJKaQ4fPYdvbcFD17AgCpidySc00qlUQtSic42S7SlwP0uWWA4aRsQqitogO3oI80EljkmVHACHM27WagSAY6xB2jWXGwjHVsyMIGEuaaU3eBcimRAmyrOvKkUhTTjPNgFQFEWRpky1e7yAL3oIKZM1UKxIrEixpuwgn4Fi6xkap5gOo3D0HOKR6dgQB0GPAfBXwPCRhrey+BNDjrqu7NaY0S+bbhcC8ol9AB29BHujUUEqzWP4gzNtFM/1+9n3DCa6eHUFAWWhZmYiyu4VxspubhN2TmmNNkA2b2s+7FNNLep/UDN6CitdsZv8ooSRBk7eLcP6Npm9kc5qpJtlsmehc0x9Vut1NIId0kp3SNK4menfZnEW6iE4lK2j0P3qobWW3meChWP30M7Xs8FZJ55ZFDnL7QsI1WwiubAdZWMVs657KbsjUPTk4Y7qyHZik/nyhlQVacUZ2Gy+LGVoRpa0xjpCzjyuOyCX9DjKlNVxJ0KqrtmJEV3UyqMStMJBvLMQEtYyXVqZWi4lH9n6GURSlFTQhtAMrnrgl2Kw9qZ0l0kRhR7buxXFTomtx64uy8tSUlamCMMJxvbfmlYqwcsgqrrRFZVpBCYMiz4rS2kp8i8pKW7C3UlZy2TuZubqvtnAF3oJS3VdDOZLCob2fvlWgxCvqqoBIfBOQ6g8gcttHUGBLroBIyFT2SOxerjpN9ZWrlsRWiiyzezdySHIr0bPRi6l9b8Fh3t77YmpSqFZm3/o+3ceObknd9hEEyIKyhoV7UyWnhA8ZpdV1MQoL9JXjkFvFnZmr/ayGrLTpJnkXkZVAuYQ4KnJcMcT308cKlPbUy9qWuy1itcVu+wgCYkmWNqZBViPW1YnA5Hxlh9pG1JqIBZUs69eKaPiKLqyg01KozcBuepdH+J6aWbFSvZysXS/HGrG4MheVm8Xkli5QGtQqnkkQq/P+n4GuUsExEBcVrkVLjblZJFUjViS9MR1YwR3Fkt8rRvh+2llMaVYYO1px9dTZlCt1KS43C9ohQVjgK1PBloYK57q2oq+wJVenIE2MbVtsbcUVdAsq0NJe3+vSIG/jbTG0TlTFgz+gzpEHT7lyl+IytAi0/6AIW4ycKmZZFFuM6SpLGEkmvCf9X/gP3oI8tZpBVztjfD/trFBtdXvoar3Uobt5mpficrUk54gkztzsPZBqSs+iTDKVFJul5So9qxlxtTLC99TQUpsekrAttcxmZx27mSt1UflZ0jtfxUo7X3UNGWZUn8J6YZXarNAYqwTDej+tLLWtnYBZoQ1UhVx9G/bawyJpVCuffD2quu5snYW6ZBVUdvIFVQ7jhPxR3carYsjAihVQFdrOeT/2MkfWhsiMK9kwIUlUNtTYOicdnStdpd4ptFOyMBUr6r3fDt6CPK6ai5vsjOzPyrlybLiHXIkb4nKuoGxoYdJ7eloEfX0G6W4X427ZDURTOXMKRRgubJvZe8MdugUVwz2xP8r31LxKlPJHEWCLK3dRmVhMbhMGEmSntTpXUyvonpO18/n1UEuydgOUKO+WWu3sjAsppNAtqFBLe6XDK8b4ftpYe3krSa0D6xxBK+KK3gibndWePAiglTCfSjOLnCdkKTArS3QZ70xyeWEElXPVxaw2fS4zK5FWWlFzq0RPMUxozE/hDfA23hVD1numZL07XRQdcRVvhMzXgjKpRTX9YrWafmnX7a5CXUpLNiErhnpID7WY4qroGICQDRs+0V975oohvq++lorUOryO7rjF07wRLl8rlfO14kAFW9F5RtZxhvkuL0vv86pYX54gBrpsrUQydGhwYTTY9GVsRZAzZwNbTTPByijfU18rVfO1AGzZWWYYCQQvMkdLqmhWnJwMreuBFXaumZVmzoBlLnyYQk7TZWDFmXQpBz3AMpb5Lhze+2lpMaX1Omnb0rLDK8ZVugyXmSXJq0TFgA/Pp4Uq+krbfm7Qts+uyjhk0HZ6Zuo40Galdk/R0DIUOuSP7zbeF0PEUpkZsr0N5oJYHJHLcFlZUCEWUQllcqo8IzEjDDsSqwKlNvsdT+EZ1ToOCsSCz0GU7SAc4t2bWYlFkdWqlvV6Vfz77zSvQcvQ1JxxlS7DZWRBVQ1E1IpPKfAS1GJodBaTTM8ySi2w7cvYAmsjW9k7NzJV4k80yju3stghR1SyYJYmaP1vVNQz4Q2lmzCB0kXmZEnlZqXbAK78zDDuSiyNqVlyRhZGYqkILQYtAECUnyUc4l2bWUxxaqiydiduTw3tICvmKt2412ZWnJxEloz5HnVdaKgtWghF6J4Ds3RNDQ2F1/ljvI0XBpWb1UrPsuNmxVydG2Nzs6SSSdNtTpD8hmBdK84kma4yyrKrDTESC6wFbSfDwcUI797LshkwdFRHOeaq3BiXkQWVqhQAi4WnaeH1GivovtAw1lWPVHYre4zEcqWwmCmFJRrfnftYqksNVRSWVmDdDZerxWQzLqsGh4/5qHzIV9uH8bP3038Aw70aUvUAAA==", "encoding": "utf-8"}, "status": {"code": 200, "message": "OK"}}}], "recorded_with": "betamax/0.5.1"} \ No newline at end of file diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json new file mode 100644 index 00000000000..4ff2ff18df5 --- /dev/null +++ b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json @@ -0,0 +1 @@ +{"http_interactions": [{"recorded_at": "2015-12-28T01:34:34", "request": {"method": "GET", "headers": {"Accept": ["*/*"], "Accept-Encoding": ["gzip, deflate"], "User-Agent": ["python-requests/2.9.1"], "Connection": ["keep-alive"]}, "body": {"string": "", "encoding": "utf-8"}, "uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"}, "response": {"headers": {"Content-Length": ["3598"], "X-forecast-models": ["proff,ecdet"], "Via": ["1.1 varnish"], "Content-Encoding": ["gzip"], "Date": ["Mon, 28 Dec 2015 01:34:33 GMT"], "X-Varnish": ["2670913258 2670013167"], "Expires": ["Mon, 28 Dec 2015 02:01:51 GMT"], "Server": ["Apache"], "Age": ["1573"], "Content-Type": ["text/xml; charset=utf-8"], "X-Backend-Host": ["snipe_loc"], "X-slicenumber": ["30"], "Accept-Ranges": ["bytes"], "Last-Modified": ["Mon, 28 Dec 2015 01:08:20 GMT"], "Vary": ["Accept-Encoding"], "Connection": ["keep-alive"]}, "url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "body": {"base64_string": "H4sIAAAAAAAAA+2dW4/bNhOG7/srBN+vLFKiDotNb5o0LZBD8WU/tOidYytZoT7BlrNJf31lrw+yNKRNiocRdoFcJJFFS5T18OU7w+HdYz4qH/LVZFSOvO+z6Xx9+31dvBo8lOXydjh8fHz0H0N/sfo6pEFAhn+9f/dp/JDPRjfFfF2O5uN84FWfv50vPoxm+Xo5GudPx98txqOyWMyPLY2WhT/LS3++GO6/svqf4XT/sS+LVT4ercsh8bPhetfCwBuvqg/mk1cDGhB2Q+gNTe8DchuktzT4e/DzT57n3VVNjnZ/2/1jMcmn3ry6kleDN7/4b9989ANC2cAr89WsmNcaSu4JvQ2C6s/fA2+1mefzydkXVcez25DdBqQ6Ps+/l9Vnzq8jPZ7+ZbWYnR8Lj8fKxe5IfBOQmyCuf+nweNXD4z3cLVeLyWZceuPpaL1+NVguinn5unoyg+OHy2KWe9uHVf5YVnd56LerrqJ95NDstuXDo/BG07IoN5Oq9WDgTUeHf4TUT5Mwqv5rMf+6/z9CEp/SJKo3tLvKfLasHnC5WeVeUfXr/f39wNvMi/LVYJxP18VmPfC+jaabqonITwbDxumPxXzyuqjubHc92wYmk4E3yb9W3xj4dLB/xB/AEz8t83yyO+nLl4E3W1b9GPjhwPucjzZVd5W723pq4FNZTKd5q5WHzayYFOWPwyXGic8Ol1/d1jifl61zlqt8vT7c7nJ1+PjDH6PjnZIgTIGbHU8Xm0kxr07fnfvhw8Dbf0n1zakftE74svi6++SvH9/WPhoAn5wuHn/Ztv7U8ruPf15qepZPis2sds77N69////7s69J2/1VfH2onfPb729/u3Bhk/xx99u+b/5OXvN+Jjek0czdkR2nF2n7cqi8KYG7N6X63YyLZVE+Nfh077PZ8bb3nXe3/jH7vJju+uiP0aqc/tj1d/U73sw+56vqK830TXJPyUvfcH83MbdvYkSEZX4mQ9isYpQ8YJM6YMmhgV+no433rTrnImNZsuW6DsbGAG9EjM1SPzLEWLDpaxjb/horjKU+Mfau8NWI8XdFniMNgkQORh4svVLp5tYviM8a4odPZ42+S5wVP/3uHJI84z6HDBXJQxmSx7Smld9cz/K0I8tjsh0OdLCcSerlNAZO0MNysGnELCeNy7Wje4y/LYhZzh/hnnGv1ByPRq+cjiAgawhMa0UaeSvsDkbTtWAlftwRrEmwbUKPSG6PJCKwJmlDGnrawAo2jRqs1L4MMf+y9HeyjaZv5KTyHjl9k8qEcZ8DQwR0Csy8RbYyibYe8d4WlkB6V98jSaofjxOks8SYtww2jRrpLvSP8dcFPdL5w92z7xuScvsmRYRZIqebSbgTwfKYzbpiNtNlLzNJe7liYbuL9GCWQtdiArNR5DM9nI0dyBLj7wt6lvDHICx9I+syJwrSOXyKg7l7DvxY6ukIAqYHkhkZUXwyQ2xK55TqYjrk/gjTMijAQz1MJylwLSaYzqDEG6WYoTE7hK+BzL8v6JnOHe+ee99k3Khq/QgKzkrlZRC2zdg6cPZazNJzzB7jgZ++jf65WjprMp0hFSnCbARxWRNmQ8AtuYzZMAO8akupGcy2LLHxuuBGCX8IQtQ3ctI52PvHctJ5P813iHROcDXDlcwcNya43gWkJ7TmOl+L9HA7DHRCOgurX4Em5dwew4RIj4ETNCVoAH1/mehR6C7ZzhDRhQLo2SftCka7Z943GTfGmuFKaIZWUYgom4U10/l64Rx2pGyU6lo2EgDBTFfCWY2yTZfAsxfaSx1oEizJuxin4Fj6RlY3Zwq6eU8qh8+BE2LNcCU2Q2s2hEQnPlEgOulI9DjZOt0aiE4zSSsE9CtcEp0084s9S0QPfBfyB2EG77uqs8sGRwxZ8cKx7rl3DS+6muHKcJZNiEuSWlTvesTSjohNt4JNE2Il1440E3s9bYiFPm8ipgfegoJoDkzlaYgECZo0XoTTbzR9IyeaqdJqwNS12cyLrWa4UpwhBSYiepqcVnZbFM1prCvDGQKio0UrtogO3oIS0Y2ZzQL988yzeIWj3bPvG35oFVmGs6Q1kTqxJtJMU0hP2pqAoNYvyGqympulkewoEiwZvDWOfNrMT/wwVFFEOO5g6RPZrOZAQS3vw8juOM4PqCLLapbKtqMkqPkff14P8qgjyDNda7wplKt2odicqaihLZKDt4DLAOHLnpfEXf4wh7Fv7LnMYcALqJ4dQUBZIlehiFaQClQo29VmznbpHS5yM0jQf8EM34NKLM+YOuSoEivvC3rMwkMQpr6RTc6IlJKaQ4fPYdvbcFD17AgCpidySc00qlUQtSic42S7SlwP0uWWA4aRsQqitogO3oI80EljkmVHACHM27WagSAY6xB2jWXGwjHVsyMIGEuaaU3eBcimRAmyrOvKkUhTTjPNgFQFEWRpky1e7yAL3oIKZM1UKxIrEixpuwgn4Fi6xkap5gOo3D0HOKR6dgQB0GPAfBXwPCRhrey+BNDjrqu7NaY0S+bbhcC8ol9AB29BHujUUEqzWP4gzNtFM/1+9n3DCa6eHUFAWWhZmYiyu4VxspubhN2TmmNNkA2b2s+7FNNLep/UDN6CitdsZv8ooSRBk7eLcP6Npm9kc5qpJtlsmehc0x9Vut1NIId0kp3SNK4menfZnEW6iE4lK2j0P3qobWW3meChWP30M7Xs8FZJ55ZFDnL7QsI1WwiubAdZWMVs657KbsjUPTk4Y7qyHZik/nyhlQVacUZ2Gy+LGVoRpa0xjpCzjyuOyCX9DjKlNVxJ0KqrtmJEV3UyqMStMJBvLMQEtYyXVqZWi4lH9n6GURSlFTQhtAMrnrgl2Kw9qZ0l0kRhR7buxXFTomtx64uy8tSUlamCMMJxvbfmlYqwcsgqrrRFZVpBCYMiz4rS2kp8i8pKW7C3UlZy2TuZubqvtnAF3oJS3VdDOZLCob2fvlWgxCvqqoBIfBOQ6g8gcttHUGBLroBIyFT2SOxerjpN9ZWrlsRWiiyzezdySHIr0bPRi6l9b8Fh3t77YmpSqFZm3/o+3ceObknd9hEEyIKyhoV7UyWnhA8ZpdV1MQoL9JXjkFvFnZmr/ayGrLTpJnkXkZVAuYQ4KnJcMcT308cKlPbUy9qWuy1itcVu+wgCYkmWNqZBViPW1YnA5Hxlh9pG1JqIBZUs69eKaPiKLqyg01KozcBuepdH+J6aWbFSvZysXS/HGrG4MheVm8Xkli5QGtQqnkkQq/P+n4GuUsExEBcVrkVLjblZJFUjViS9MR1YwR3Fkt8rRvh+2llMaVYYO1px9dTZlCt1KS43C9ohQVjgK1PBloYK57q2oq+wJVenIE2MbVtsbcUVdAsq0NJe3+vSIG/jbTG0TlTFgz+gzpEHT7lyl+IytAi0/6AIW4ycKmZZFFuM6SpLGEkmvCf9X/gP3oI8tZpBVztjfD/trFBtdXvoar3Uobt5mpficrUk54gkztzsPZBqSs+iTDKVFJul5So9qxlxtTLC99TQUpsekrAttcxmZx27mSt1UflZ0jtfxUo7X3UNGWZUn8J6YZXarNAYqwTDej+tLLWtnYBZoQ1UhVx9G/bawyJpVCuffD2quu5snYW6ZBVUdvIFVQ7jhPxR3carYsjAihVQFdrOeT/2MkfWhsiMK9kwIUlUNtTYOicdnStdpd4ptFOyMBUr6r3fDt6CPK6ai5vsjOzPyrlybLiHXIkb4nKuoGxoYdJ7eloEfX0G6W4X427ZDURTOXMKRRgubJvZe8MdugUVwz2xP8r31LxKlPJHEWCLK3dRmVhMbhMGEmSntTpXUyvonpO18/n1UEuydgOUKO+WWu3sjAsppNAtqFBLe6XDK8b4ftpYe3krSa0D6xxBK+KK3gibndWePAiglTCfSjOLnCdkKTArS3QZ70xyeWEElXPVxaw2fS4zK5FWWlFzq0RPMUxozE/hDfA23hVD1numZL07XRQdcRVvhMzXgjKpRTX9YrWafmnX7a5CXUpLNiErhnpID7WY4qroGICQDRs+0V975oohvq++lorUOryO7rjF07wRLl8rlfO14kAFW9F5RtZxhvkuL0vv86pYX54gBrpsrUQydGhwYTTY9GVsRZAzZwNbTTPByijfU18rVfO1AGzZWWYYCQQvMkdLqmhWnJwMreuBFXaumZVmzoBlLnyYQk7TZWDFmXQpBz3AMpb5Lhze+2lpMaX1Omnb0rLDK8ZVugyXmSXJq0TFgA/Pp4Uq+krbfm7Qts+uyjhk0HZ6Zuo40Galdk/R0DIUOuSP7zbeF0PEUpkZsr0N5oJYHJHLcFlZUCEWUQllcqo8IzEjDDsSqwKlNvsdT+EZ1ToOCsSCz0GU7SAc4t2bWYlFkdWqlvV6Vfz77zSvQcvQ1JxxlS7DZWRBVQ1E1IpPKfAS1GJodBaTTM8ySi2w7cvYAmsjW9k7NzJV4k80yju3stghR1SyYJYmaP1vVNQz4Q2lmzCB0kXmZEnlZqXbAK78zDDuSiyNqVlyRhZGYqkILQYtAECUnyUc4l2bWUxxaqiydiduTw3tICvmKt2412ZWnJxEloz5HnVdaKgtWghF6J4Ds3RNDQ2F1/ljvI0XBpWb1UrPsuNmxVydG2Nzs6SSSdNtTpD8hmBdK84kma4yyrKrDTESC6wFbSfDwcUI797LshkwdFRHOeaq3BiXkQWVqhQAi4WnaeH1GivovtAw1lWPVHYre4zEcqWwmCmFJRrfnftYqksNVRSWVmDdDZerxWQzLqsGh4/5qHzIV9uH8bP3038Aw70aUvUAAA==", "encoding": "utf-8"}, "status": {"code": 200, "message": "OK"}}}], "recorded_with": "betamax/0.5.1"} \ No newline at end of file diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 7e95194aa4b..f58aefbce43 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -4,70 +4,70 @@ tests.components.sensor.test_yr Tests Yr sensor. """ -import unittest +from unittest.mock import patch + +import pytest import homeassistant.core as ha import homeassistant.components.sensor as sensor -class TestSensorYr(unittest.TestCase): +@pytest.mark.usefixtures('betamax_session') +class TestSensorYr: """ Test the Yr sensor. """ - def setUp(self): # pylint: disable=invalid-name + def setup_method(self, method): self.hass = ha.HomeAssistant() self.hass.config.latitude = 32.87336 self.hass.config.longitude = 117.22743 - def tearDown(self): # pylint: disable=invalid-name + def teardown_method(self, method): """ Stop down stuff we started. """ self.hass.stop() - def test_default_setup(self): - self.assertTrue(sensor.setup(self.hass, { - 'sensor': { - 'platform': 'yr', - 'elevation': 0, - } - })) - state = self.hass.states.get('sensor.yr_symbol') - - self.assertTrue(state.state.isnumeric()) - self.assertEqual(None, - state.attributes.get('unit_of_measurement')) - - def test_custom_setup(self): - self.assertTrue(sensor.setup(self.hass, { - 'sensor': { - 'platform': 'yr', - 'elevation': 0, - 'monitored_conditions': { - 'pressure', - 'windDirection', - 'humidity', - 'fog', - 'windSpeed' + def test_default_setup(self, betamax_session): + with patch('homeassistant.components.sensor.yr.requests.Session', + return_value=betamax_session): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, } - } - })) + }) + state = self.hass.states.get('sensor.yr_symbol') - self.assertEqual(None, state) + + assert state.state.isnumeric() + assert state.attributes.get('unit_of_measurement') is None + + def test_custom_setup(self, betamax_session): + with patch('homeassistant.components.sensor.yr.requests.Session', + return_value=betamax_session): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'yr', + 'elevation': 0, + 'monitored_conditions': { + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed' + } + } + }) state = self.hass.states.get('sensor.yr_pressure') - self.assertEqual('hPa', - state.attributes.get('unit_of_measurement')) + assert 'hPa', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_wind_direction') - self.assertEqual('°', - state.attributes.get('unit_of_measurement')) + assert '°', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_humidity') - self.assertEqual('%', - state.attributes.get('unit_of_measurement')) + assert '%', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_fog') - self.assertEqual('%', - state.attributes.get('unit_of_measurement')) + assert '%', state.attributes.get('unit_of_measurement') state = self.hass.states.get('sensor.yr_wind_speed') - self.assertEqual('m/s', - state.attributes.get('unit_of_measurement')) + assert 'm/s', state.attributes.get('unit_of_measurement') From c7183a14a50b3e0603f5076b2719e6f043db7a4c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 18:03:23 -0800 Subject: [PATCH 088/267] Tweak lint script colors for travis --- script/lint | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/lint b/script/lint index 1cf877e86fa..7e742f801a8 100755 --- a/script/lint +++ b/script/lint @@ -6,13 +6,13 @@ echo "Checking style with flake8..." tput setaf 1 flake8 --exclude www_static homeassistant FLAKE8_STATUS=$? -tput sgr0 +tput setaf 7; tput sgr0 echo "Checking style with pylint..." tput setaf 1 -tput setaf 1; pylint homeassistant +pylint homeassistant PYLINT_STATUS=$? -tput sgr0 +tput setaf 7; tput sgr0 if [ $FLAKE8_STATUS -eq 0 ] then From 7f17a50b4a2e9ba162a86cdae18a070ae32b5ca2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 18:04:38 -0800 Subject: [PATCH 089/267] Swap lint/requirements validation between Python versions --- .travis.yml | 4 ++-- script/cibuild | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 91e989502ed..78477e511bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,8 @@ python: - 3.4 - 3.5 install: - # Validate requirements_all.txt on Python 3.5 - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; fi + # Validate requirements_all.txt on Python 3.4 + - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; fi - script/bootstrap_server script: - script/cibuild diff --git a/script/cibuild b/script/cibuild index 778cbe0db52..beb7b22693d 100755 --- a/script/cibuild +++ b/script/cibuild @@ -5,7 +5,7 @@ cd "$(dirname "$0")/.." -if [ "$TRAVIS_PYTHON_VERSION" != "3.4" ]; then +if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then NO_LINT=1 fi From 473d6b1d05df9bb2ef3ae0d2e444f5131c89077d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 19:18:03 -0800 Subject: [PATCH 090/267] Fix console coloring for scripts --- .travis.yml | 2 +- script/lint | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 78477e511bd..4383d49f548 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ python: - 3.5 install: # Validate requirements_all.txt on Python 3.4 - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi - script/bootstrap_server script: - script/cibuild diff --git a/script/lint b/script/lint index 7e742f801a8..d99d030c86d 100755 --- a/script/lint +++ b/script/lint @@ -6,13 +6,13 @@ echo "Checking style with flake8..." tput setaf 1 flake8 --exclude www_static homeassistant FLAKE8_STATUS=$? -tput setaf 7; tput sgr0 +tput sgr0 echo "Checking style with pylint..." tput setaf 1 pylint homeassistant PYLINT_STATUS=$? -tput setaf 7; tput sgr0 +tput sgr0 if [ $FLAKE8_STATUS -eq 0 ] then From e9059a3ed9b6d311b79750eeb672b153a6c1bfe0 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Sun, 27 Dec 2015 22:49:55 -0500 Subject: [PATCH 091/267] added test; addressed comments --- .../components/switch/command_switch.py | 38 +++-- .../components/switch/test_command_switch.py | 158 ++++++++++++++++++ 2 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 tests/components/switch/test_command_switch.py diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 5af197193d3..c36ca4e9ce9 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -24,10 +24,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): devices = [] for dev_name, properties in switches.items(): - if 'statecmd' in properties and CONF_VALUE_TEMPLATE not in properties: - _LOGGER.warning("Specify a %s when using statemcd", - CONF_VALUE_TEMPLATE) - continue devices.append( CommandSwitch( hass, @@ -68,8 +64,8 @@ class CommandSwitch(SwitchDevice): return success @staticmethod - def _query_state(command): - """ Execute state command. """ + def _query_state_value(command): + """ Execute state command for return value. """ _LOGGER.info('Running state command: %s', command) try: @@ -78,10 +74,16 @@ class CommandSwitch(SwitchDevice): except subprocess.CalledProcessError: _LOGGER.error('Command failed: %s', command) + @staticmethod + def _query_state_code(command): + """ Execute state command for return code. """ + _LOGGER.info('Running state command: %s', command) + return subprocess.call(command, shell=True) == 0 + @property def should_poll(self): - """ No polling needed. """ - return True + """ Only poll if we have statecmd. """ + return self._command_state is not None @property def name(self): @@ -93,13 +95,23 @@ class CommandSwitch(SwitchDevice): """ True if device is on. """ return self._state + def _query_state(self): + """ Query for state. """ + if not self._command_state: + _LOGGER.error('No state command specified') + return + if self._value_template: + return CommandSwitch._query_state_value(self._command_state) + return CommandSwitch._query_state_code(self._command_state) + def update(self): """ Update device state. """ - if self._command_state and self._value_template: - payload = CommandSwitch._query_state(self._command_state) - payload = template.render_with_possible_json_value( - self._hass, self._value_template, payload) - self._state = (payload == "True") + if self._command_state: + payload = str(self._query_state()) + if self._value_template: + payload = template.render_with_possible_json_value( + self._hass, self._value_template, payload) + self._state = (payload.lower() == "true") def turn_on(self, **kwargs): """ Turn the device on. """ diff --git a/tests/components/switch/test_command_switch.py b/tests/components/switch/test_command_switch.py new file mode 100644 index 00000000000..3684f78fff4 --- /dev/null +++ b/tests/components/switch/test_command_switch.py @@ -0,0 +1,158 @@ +""" +tests.components.switch.test_command_switch +~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests command switch. +""" +import json +import os +import tempfile +import unittest + +from homeassistant import core +from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.components.switch as switch + + +class TestCommandSwitch(unittest.TestCase): + """ Test the command switch. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = core.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_state_none(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + 'value_template': '{{ value=="1" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_json_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + oncmd = json.dumps({'status': 'ok'}) + offcmd = json.dumps({'status': 'nope'}) + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo \'{}\' > {}'.format(oncmd, path), + 'offcmd': 'echo \'{}\' > {}'.format(offcmd, path), + 'value_template': '{{ value_json.status=="ok" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + def test_state_code(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) From d9b30d1421b1dedb9df53b935f8dd048fe7231ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 Dec 2015 21:14:35 -0800 Subject: [PATCH 092/267] Pep257 fixes for core. --- homeassistant/core.py | 144 +++++++++++++++++++++--------------------- setup.cfg | 3 + 2 files changed, 75 insertions(+), 72 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 8ea55c653e3..e2650969eb0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1,6 +1,5 @@ """ -homeassistant -~~~~~~~~~~~~~ +Core components of Home Assistant. Home Assistant is a Home Automation framework for observing the state of entities and react to changes. @@ -53,9 +52,10 @@ _MockHA = namedtuple("MockHomeAssistant", ['bus']) class HomeAssistant(object): - """ Core class to route all communication to right components. """ + """Root object of the Home Assistant home automation.""" def __init__(self): + """Initialize new Home Assistant object.""" self.pool = pool = create_worker_pool() self.bus = EventBus(pool) self.services = ServiceRegistry(self.bus, pool) @@ -63,7 +63,7 @@ class HomeAssistant(object): self.config = Config() def start(self): - """ Start home assistant. """ + """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) @@ -71,12 +71,11 @@ class HomeAssistant(object): self.bus.fire(EVENT_HOMEASSISTANT_START) def block_till_stopped(self): - """ Will register service homeassistant/stop and - will block until called. """ + """Register service homeassistant/stop and will block until called.""" request_shutdown = threading.Event() def stop_homeassistant(*args): - """ Stops Home Assistant. """ + """Stop Home Assistant.""" request_shutdown.set() self.services.register( @@ -98,7 +97,7 @@ class HomeAssistant(object): self.stop() def stop(self): - """ Stops Home Assistant and shuts down all threads. """ + """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") self.bus.fire(EVENT_HOMEASSISTANT_STOP) @@ -150,8 +149,7 @@ class HomeAssistant(object): class JobPriority(util.OrderedEnum): - """ Provides priorities for bus events. """ - # pylint: disable=no-init,too-few-public-methods + """Provides job priorities for event bus jobs.""" EVENT_CALLBACK = 0 EVENT_SERVICE = 1 @@ -161,7 +159,7 @@ class JobPriority(util.OrderedEnum): @staticmethod def from_event_type(event_type): - """ Returns a priority based on event type. """ + """Return a priority based on event type.""" if event_type == EVENT_TIME_CHANGED: return JobPriority.EVENT_TIME elif event_type == EVENT_STATE_CHANGED: @@ -175,8 +173,7 @@ class JobPriority(util.OrderedEnum): class EventOrigin(enum.Enum): - """ Distinguish between origin of event. """ - # pylint: disable=no-init,too-few-public-methods + """Represents origin of an event.""" local = "LOCAL" remote = "REMOTE" @@ -185,14 +182,15 @@ class EventOrigin(enum.Enum): return self.value -# pylint: disable=too-few-public-methods class Event(object): - """ Represents an event within the Bus. """ + # pylint: disable=too-few-public-methods + """Represents an event within the Bus.""" __slots__ = ['event_type', 'data', 'origin', 'time_fired'] def __init__(self, event_type, data=None, origin=EventOrigin.local, time_fired=None): + """Initialize a new event.""" self.event_type = event_type self.data = data or {} self.origin = origin @@ -200,7 +198,7 @@ class Event(object): time_fired or dt_util.utcnow()) def as_dict(self): - """ Returns a dict representation of this Event. """ + """Create a dict representation of this Event.""" return { 'event_type': self.event_type, 'data': dict(self.data), @@ -227,26 +225,23 @@ class Event(object): class EventBus(object): - """ Class that allows different components to communicate via services - and events. - """ + """Allows firing of and listening for events.""" def __init__(self, pool=None): + """Initialize a new event bus.""" self._listeners = {} self._lock = threading.Lock() self._pool = pool or create_worker_pool() @property def listeners(self): - """ Dict with events that is being listened for and the number - of listeners. - """ + """Dict with events and the number of listeners.""" with self._lock: return {key: len(self._listeners[key]) for key in self._listeners} def fire(self, event_type, event_data=None, origin=EventOrigin.local): - """ Fire an event. """ + """Fire an event.""" if not self._pool.running: raise HomeAssistantError('Home Assistant has shut down.') @@ -271,7 +266,7 @@ class EventBus(object): self._pool.add_job(job_priority, (func, event)) def listen(self, event_type, listener): - """ Listen for all events or events of a specific type. + """Listen for all events or events of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. @@ -283,7 +278,7 @@ class EventBus(object): self._listeners[event_type] = [listener] def listen_once(self, event_type, listener): - """ Listen once for event of a specific type. + """Listen once for event of a specific type. To listen to all events specify the constant ``MATCH_ALL`` as event_type. @@ -292,7 +287,7 @@ class EventBus(object): """ @ft.wraps(listener) def onetime_listener(event): - """ Removes listener from eventbus and then fires listener. """ + """Remove listener from eventbus and then fires listener.""" if hasattr(onetime_listener, 'run'): return # Set variable so that we will never run twice. @@ -311,7 +306,7 @@ class EventBus(object): return onetime_listener def remove_listener(self, event_type, listener): - """ Removes a listener of a specific event_type. """ + """Remove a listener of a specific event_type.""" with self._lock: try: self._listeners[event_type].remove(listener) @@ -343,6 +338,7 @@ class State(object): # pylint: disable=too-many-arguments def __init__(self, entity_id, state, attributes=None, last_changed=None, last_updated=None): + """Initialize a new state.""" if not ENTITY_ID_PATTERN.match(entity_id): raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " @@ -363,31 +359,33 @@ class State(object): @property def domain(self): - """ Returns domain of this state. """ + """Domain of this state.""" return util.split_entity_id(self.entity_id)[0] @property def object_id(self): - """ Returns object_id of this state. """ + """Object id of this state.""" return util.split_entity_id(self.entity_id)[1] @property def name(self): - """ Name to represent this state. """ + """Name of this state.""" return ( self.attributes.get(ATTR_FRIENDLY_NAME) or self.object_id.replace('_', ' ')) def copy(self): - """ Creates a copy of itself. """ + """Return a copy of the state.""" return State(self.entity_id, self.state, dict(self.attributes), self.last_changed, self.last_updated) def as_dict(self): - """ Converts State to a dict to be used within JSON. - Ensures: state == State.from_dict(state.as_dict()) """ + """Return a dict representation of the State. + To be used for JSON serialization. + Ensures: state == State.from_dict(state.as_dict()) + """ return {'entity_id': self.entity_id, 'state': self.state, 'attributes': self.attributes, @@ -396,11 +394,11 @@ class State(object): @classmethod def from_dict(cls, json_dict): - """ Static method to create a state from a dict. - Ensures: state == State.from_json_dict(state.to_json_dict()) """ + """Initialize a state from a dict. - if not (json_dict and - 'entity_id' in json_dict and + Ensures: state == State.from_json_dict(state.to_json_dict()) + """ + if not (json_dict and 'entity_id' in json_dict and 'state' in json_dict): return None @@ -433,15 +431,16 @@ class State(object): class StateMachine(object): - """ Helper class that tracks the state of different entities. """ + """Helper class that tracks the state of different entities.""" def __init__(self, bus): + """Initialize state machine.""" self._states = {} self._bus = bus self._lock = threading.Lock() def entity_ids(self, domain_filter=None): - """ List of entity ids that are being tracked. """ + """List of entity ids that are being tracked.""" if domain_filter is None: return list(self._states.keys()) @@ -451,35 +450,36 @@ class StateMachine(object): if state.domain == domain_filter] def all(self): - """ Returns a list of all states. """ + """Create a list of all states.""" with self._lock: return [state.copy() for state in self._states.values()] def get(self, entity_id): - """ Returns the state of the specified entity. """ + """Retrieve state of entity_id or None if not found.""" state = self._states.get(entity_id.lower()) # Make a copy so people won't mutate the state return state.copy() if state else None def is_state(self, entity_id, state): - """ Returns True if entity exists and is specified state. """ + """Test if entity exists and is specified state.""" entity_id = entity_id.lower() return (entity_id in self._states and self._states[entity_id].state == state) def remove(self, entity_id): - """ Removes an entity from the state machine. + """Remove the state of an entity. - Returns boolean to indicate if an entity was removed. """ + Returns boolean to indicate if an entity was removed. + """ entity_id = entity_id.lower() with self._lock: return self._states.pop(entity_id, None) is not None def set(self, entity_id, new_state, attributes=None): - """ Set the state of an entity, add entity if it does not exist. + """Set the state of an entity, add entity if it does not exist. Attributes is an optional dict to specify attributes of this state. @@ -514,9 +514,7 @@ class StateMachine(object): self._bus.fire(EVENT_STATE_CHANGED, event_data) def track_change(self, entity_ids, action, from_state=None, to_state=None): - """ - DEPRECATED AS OF 8/4/2015 - """ + """DEPRECATED AS OF 8/4/2015.""" _LOGGER.warning( 'hass.states.track_change is deprecated. ' 'Use homeassistant.helpers.event.track_state_change instead.') @@ -527,33 +525,36 @@ class StateMachine(object): # pylint: disable=too-few-public-methods class Service(object): - """ Represents a service. """ + """Represents a callable service.""" __slots__ = ['func', 'description', 'fields'] def __init__(self, func, description, fields): + """Initialize a service.""" self.func = func self.description = description or '' self.fields = fields or {} def as_dict(self): - """ Return dictionary representation of this service. """ + """Return dictionary representation of this service.""" return { 'description': self.description, 'fields': self.fields, } def __call__(self, call): + """Execute the service.""" self.func(call) # pylint: disable=too-few-public-methods class ServiceCall(object): - """ Represents a call to a service. """ + """Represents a call to a service.""" __slots__ = ['domain', 'service', 'data'] def __init__(self, domain, service, data=None): + """Initialize a service call.""" self.domain = domain self.service = service self.data = data or {} @@ -567,9 +568,10 @@ class ServiceCall(object): class ServiceRegistry(object): - """ Offers services over the eventbus. """ + """Offers services over the eventbus.""" def __init__(self, bus, pool=None): + """Initialize a service registry.""" self._services = {} self._lock = threading.Lock() self._pool = pool or create_worker_pool() @@ -579,14 +581,14 @@ class ServiceRegistry(object): @property def services(self): - """ Dict with per domain a list of available services. """ + """Dict with per domain a list of available services.""" with self._lock: return {domain: {key: value.as_dict() for key, value in self._services[domain].items()} for domain in self._services} def has_service(self, domain, service): - """ Returns True if specified service exists. """ + """Test if specified service exists.""" return service in self._services.get(domain, []) def register(self, domain, service, service_func, description=None): @@ -611,7 +613,8 @@ class ServiceRegistry(object): def call(self, domain, service, service_data=None, blocking=False): """ - Calls specified service. + Call a service. + Specify blocking=True to wait till service is executed. Waits a maximum of SERVICE_CALL_LIMIT. @@ -635,10 +638,7 @@ class ServiceRegistry(object): executed_event = threading.Event() def service_executed(call): - """ - Called when a service is executed. - Will set the event if matches our service call. - """ + """Callback method that is called when service is executed.""" if call.data[ATTR_SERVICE_CALL_ID] == call_id: executed_event.set() @@ -653,7 +653,7 @@ class ServiceRegistry(object): return success def _event_to_service_call(self, event): - """ Calls a service from an event. """ + """Callback for SERVICE_CALLED events from the event bus.""" service_data = dict(event.data) domain = service_data.pop(ATTR_DOMAIN, None) service = service_data.pop(ATTR_SERVICE, None) @@ -670,7 +670,7 @@ class ServiceRegistry(object): (service_handler, service_call))) def _execute_service(self, service_and_call): - """ Executes a service and fires a SERVICE_EXECUTED event. """ + """Execute a service and fires a SERVICE_EXECUTED event.""" service, call = service_and_call service(call) @@ -680,16 +680,17 @@ class ServiceRegistry(object): {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) def _generate_unique_id(self): - """ Generates a unique service call id. """ + """Generate a unique service call id.""" self._cur_id += 1 return "{}-{}".format(id(self), self._cur_id) class Config(object): - """ Configuration settings for Home Assistant. """ + """Configuration settings for Home Assistant.""" # pylint: disable=too-many-instance-attributes def __init__(self): + """Initialize a new config object.""" self.latitude = None self.longitude = None self.temperature_unit = None @@ -709,15 +710,15 @@ class Config(object): self.config_dir = get_default_config_dir() def distance(self, lat, lon): - """ Calculate distance from Home Assistant in meters. """ + """Calculate distance from Home Assistant in meters.""" return location.distance(self.latitude, self.longitude, lat, lon) def path(self, *path): - """ Returns path to the file within the config dir. """ + """Generate path to the file within the config dir.""" return os.path.join(self.config_dir, *path) def temperature(self, value, unit): - """ Converts temperature to user preferred unit if set. """ + """Convert temperature to user preferred unit if set.""" if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and self.temperature_unit and unit != self.temperature_unit): return value, unit @@ -732,7 +733,7 @@ class Config(object): self.temperature_unit) def as_dict(self): - """ Converts config to a dictionary. """ + """Create a dict representation of this dict.""" time_zone = self.time_zone or dt_util.UTC return { @@ -747,7 +748,7 @@ class Config(object): def create_timer(hass, interval=TIMER_INTERVAL): - """ Creates a timer. Timer will start on HOMEASSISTANT_START. """ + """Create a timer that will start on HOMEASSISTANT_START.""" # We want to be able to fire every time a minute starts (seconds=0). # We want this so other modules can use that to make sure they fire # every minute. @@ -810,12 +811,12 @@ def create_timer(hass, interval=TIMER_INTERVAL): def create_worker_pool(worker_count=None): - """ Creates a worker pool to be used. """ + """Create a worker pool.""" if worker_count is None: worker_count = MIN_WORKER_THREAD def job_handler(job): - """ Called whenever a job is available to do. """ + """Called whenever a job is available to do.""" try: func, arg = job func(arg) @@ -825,8 +826,7 @@ def create_worker_pool(worker_count=None): _LOGGER.exception("BusHandler:Exception doing job") def busy_callback(worker_count, current_jobs, pending_jobs_count): - """ Callback to be called when the pool queue gets too big. """ - + """Callback to be called when the pool queue gets too big.""" _LOGGER.warning( "WorkerPool:All %d threads are busy and %d jobs pending", worker_count, pending_jobs_count) diff --git a/setup.cfg b/setup.cfg index 35d8212de78..aab4b18bc12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ universal = 1 [pytest] testpaths = tests + +[pep257] +ignore = D203,D105 From 586be7fad13ffcbd3b41155a057c9a6fa1b137de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 Dec 2015 00:09:38 -0800 Subject: [PATCH 093/267] Prevent division by 0 xy->rgb color conversion --- homeassistant/util/color.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 26dca7ab0c6..06c9b4c6862 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -53,8 +53,12 @@ def color_xy_brightness_to_RGB(vX, vY, brightness): return (0, 0, 0) Y = brightness - X = (Y / vY) * vX - Z = (Y / vY) * (1 - vX - vY) + if vY != 0: + X = (Y / vY) * vX + Z = (Y / vY) * (1 - vX - vY) + else: + X = 0 + Z = 0 # Convert to RGB using Wide RGB D65 conversion. r = X * 1.612 - Y * 0.203 - Z * 0.302 From 6e2fb17f191047553d81c22d189d494d9d80faac Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Tue, 29 Dec 2015 17:52:05 +0100 Subject: [PATCH 094/267] Fix KeyError on 'title' when title is empty --- homeassistant/components/media_player/mpd.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index a61dac88150..9d48f1458eb 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -142,12 +142,15 @@ class MpdDevice(MediaPlayerDevice): def media_title(self): """ Title of current playing media. """ name = self.currentsong.get('name', None) - title = self.currentsong['title'] + title = self.currentsong.get('title', None) if name is None: return title else: - return '{}: {}'.format(name, title) + if title is None: + return name + else: + return '{}: {}'.format(name, title) @property def media_artist(self): From 56a2ffca1dcbc404c9a33eb8bab33f572934569c Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Tue, 29 Dec 2015 22:10:09 +0100 Subject: [PATCH 095/267] Changed if else statements. The following situations are handled now: - name and title can be None - name can be None - title can be None - name and title can contain data --- homeassistant/components/media_player/mpd.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 9d48f1458eb..c15982945a9 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -144,13 +144,14 @@ class MpdDevice(MediaPlayerDevice): name = self.currentsong.get('name', None) title = self.currentsong.get('title', None) - if name is None: + if name is None and title is None: + return "No information received from MPD" + elif name is None: return title + elif title is None: + return name else: - if title is None: - return name - else: - return '{}: {}'.format(name, title) + return '{}: {}'.format(name, title) @property def media_artist(self): From 41a36df80170b05b26f1077c13c6d4796cca4564 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 30 Dec 2015 11:54:21 +0000 Subject: [PATCH 096/267] Update pywemo version --- homeassistant/components/switch/wemo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index a343711ccc3..6057681c53c 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.4'] +REQUIREMENTS = ['pywemo==0.3.5'] _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None diff --git a/requirements_all.txt b/requirements_all.txt index a9ec467e8b1..cb623ce0920 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.4 +pywemo==0.3.5 # homeassistant.components.tellduslive tellive-py==0.5.2 From 429904c437b38223274a95d4f2be4b113068a080 Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Wed, 30 Dec 2015 13:00:34 +0100 Subject: [PATCH 097/267] Returning None when name and title are both not available Removed trailing whitespaces --- homeassistant/components/media_player/mpd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index c15982945a9..285607360ac 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -145,11 +145,11 @@ class MpdDevice(MediaPlayerDevice): title = self.currentsong.get('title', None) if name is None and title is None: - return "No information received from MPD" + return "None" elif name is None: return title elif title is None: - return name + return name else: return '{}: {}'.format(name, title) From 913c5ab47c2e07a5b964a484c7ef1c67cf8c0365 Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Wed, 30 Dec 2015 13:26:42 +0100 Subject: [PATCH 098/267] identing error... sorry --- homeassistant/components/media_player/mpd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 285607360ac..27b5aa3863c 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -145,7 +145,7 @@ class MpdDevice(MediaPlayerDevice): title = self.currentsong.get('title', None) if name is None and title is None: - return "None" + return "None" elif name is None: return title elif title is None: From b0734e613fc1849f78ab7431d2102fe343a90677 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Wed, 30 Dec 2015 13:36:47 -0500 Subject: [PATCH 099/267] Add support for deCONZ (Raspbee-GW hue-like API) - Doesn't support the none transition type, so don't send it --- homeassistant/components/light/hue.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 77672c9aaf5..29ee523dec4 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -121,10 +121,19 @@ def setup_bridge(host, hass, add_devices_callback): new_lights = [] + api_name = api.get('config').get('name') + if api_name == 'RaspBee-GW': + bridge_type = 'deconz' + _LOGGER.info("Found DeCONZ gateway (%s)", api_name) + else: + _LOGGER.info("Found Hue bridge (%s)", api_name) + bridge_type = 'hue' + for light_id, info in api_states.items(): if light_id not in lights: lights[light_id] = HueLight(int(light_id), info, - bridge, update_lights) + bridge, update_lights, + bridge_type=bridge_type) new_lights.append(lights[light_id]) else: lights[light_id].info = info @@ -163,11 +172,13 @@ def request_configuration(host, hass, add_devices_callback): class HueLight(Light): """ Represents a Hue light """ - def __init__(self, light_id, info, bridge, update_lights): + # pylint: disable=too-many-arguments + def __init__(self, light_id, info, bridge, update_lights, bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge self.update_lights = update_lights + self.bridge_type = bridge_type @property def unique_id(self): @@ -227,7 +238,7 @@ class HueLight(Light): command['alert'] = 'lselect' elif flash == FLASH_SHORT: command['alert'] = 'select' - else: + elif self.bridge_type == 'hue': command['alert'] = 'none' effect = kwargs.get(ATTR_EFFECT) @@ -237,7 +248,7 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - else: + elif self.bridge_type == 'hue': command['effect'] = 'none' self.bridge.set_light(self.light_id, command) From adfcfad48886298cfe949705969dc0e62699cf98 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 23 Dec 2015 03:52:52 -0700 Subject: [PATCH 100/267] Update locative functionality --- .../components/device_tracker/locative.py | 112 +++++++++++++----- 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 2d238992cc7..0ed97b6c4f8 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -6,12 +6,15 @@ Locative platform for the device tracker. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ +import logging +from functools import partial + from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR) + HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR, STATE_NOT_HOME) -DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) -_SEE = 0 +DEPENDENCIES = ['http', 'zone'] URL_API_LOCATIVE_ENDPOINT = "/api/locative" @@ -19,52 +22,97 @@ URL_API_LOCATIVE_ENDPOINT = "/api/locative" def setup_scanner(hass, config, see): """ Set up an endpoint for the Locative app. """ - # Use a global variable to keep setup_scanner compact when using a callback - global _SEE - _SEE = see - # POST would be semantically better, but that currently does not work # since Locative sends the data as key1=value1&key2=value2 # in the request body, while Home Assistant expects json there. hass.http.register_path( - 'GET', URL_API_LOCATIVE_ENDPOINT, _handle_get_api_locative) + 'GET', URL_API_LOCATIVE_ENDPOINT, + partial(_handle_get_api_locative, hass, see)) return True -def _handle_get_api_locative(handler, path_match, data): +# TODO: What happens with HA turns off? +def _handle_get_api_locative(hass, see, handler, path_match, data): """ Locative message received. """ - if not isinstance(data, dict): - handler.write_json_message( - "Error while parsing Locative message.", - HTTP_INTERNAL_SERVER_ERROR) - return - if 'latitude' not in data or 'longitude' not in data: - handler.write_json_message( - "Location not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - if 'device' not in data or 'id' not in data: - handler.write_json_message( - "Device id or location id not specified.", - HTTP_UNPROCESSABLE_ENTITY) + if not _check_data(handler, data): return + device = data['device'].replace('-', '') + location_name = data['id'] + direction = data['trigger'] + try: gps_coords = (float(data['latitude']), float(data['longitude'])) except ValueError: - # If invalid latitude / longitude format - handler.write_json_message( - "Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_json_message("Invalid latitude / longitude format.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Received invalid latitude / longitude format.") return - # entity id's in Home Assistant must be alphanumerical - device_uuid = data['device'] - device_entity_id = device_uuid.replace('-', '') + if direction == 'enter': + zones = [state for state in hass.states.entity_ids('zone')] + _LOGGER.info(zones) - _SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id']) + if "zone.{}".format(location_name.lower()) in zones: + see(dev_id=device, location_name=location_name) + handler.write_json_message("Set new location to {}".format(location_name)) + else: + see(dev_id=device, gps=gps_coords) + handler.write_json_message("Set new location to {}".format(gps_coords)) + + elif direction == 'exit': + current_zone = hass.states.get("{}.{}".format("device_tracker", device)).state + + if current_zone.lower() == location_name.lower(): + see(dev_id=device, location_name=STATE_NOT_HOME) + handler.write_json_message("Set new location to not home") + else: + # Ignore the message if it is telling us to exit a zone that we aren't + # currently in. This occurs when a zone is entered before the previous + # zone was exited. The enter message will be sent first, then the exit + # message will be sent second. + handler.write_json_message("Ignoring transition to {}".format(location_name)) + + else: + handler.write_json_message("Received unidentified message: {}".format(direction)) + _LOGGER.error("Received unidentified message from Locative: %s", + direction) + + +def _check_data(handler, data): + if not isinstance(data, dict): + handler.write_json_message("Error while parsing Locative message.", + HTTP_INTERNAL_SERVER_ERROR) + _LOGGER.error("Error while parsing Locative message: " + "data is not a dict.") + return False + + if 'latitude' not in data or 'longitude' not in data: + handler.write_json_message("Latitude and longitude not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Latitude and longitude not specified.") + return False + + if 'device' not in data: + handler.write_json_message("Device id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Device id not specified.") + return False + + if 'id' not in data: + handler.write_json_message("Location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Location id not specified.") + return False + + if 'trigger' not in data: + handler.write_json_message("Trigger is not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Trigger is not specified.") + return False + + return True - handler.write_json_message("Locative message processed") From 25e1432403f2afc9ca050fc530122f63a38c0fa7 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 30 Dec 2015 12:30:49 -0700 Subject: [PATCH 101/267] Fix style issues --- .../components/device_tracker/locative.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 0ed97b6c4f8..cb8e42fd1c4 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -33,7 +33,6 @@ def setup_scanner(hass, config, see): return True -# TODO: What happens with HA turns off? def _handle_get_api_locative(hass, see, handler, path_match, data): """ Locative message received. """ @@ -58,26 +57,31 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if "zone.{}".format(location_name.lower()) in zones: see(dev_id=device, location_name=location_name) - handler.write_json_message("Set new location to {}".format(location_name)) + handler.write_json_message( + "Set new location to {}".format(location_name)) else: see(dev_id=device, gps=gps_coords) - handler.write_json_message("Set new location to {}".format(gps_coords)) + handler.write_json_message( + "Set new location to {}".format(gps_coords)) elif direction == 'exit': - current_zone = hass.states.get("{}.{}".format("device_tracker", device)).state + current_zone = hass.states.get( + "{}.{}".format("device_tracker", device)).state if current_zone.lower() == location_name.lower(): see(dev_id=device, location_name=STATE_NOT_HOME) handler.write_json_message("Set new location to not home") else: - # Ignore the message if it is telling us to exit a zone that we aren't - # currently in. This occurs when a zone is entered before the previous - # zone was exited. The enter message will be sent first, then the exit - # message will be sent second. - handler.write_json_message("Ignoring transition to {}".format(location_name)) + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered before + # the previous zone was exited. The enter message will be sent + # first, then the exit message will be sent second. + handler.write_json_message( + "Ignoring transition to {}".format(location_name)) else: - handler.write_json_message("Received unidentified message: {}".format(direction)) + handler.write_json_message( + "Received unidentified message: {}".format(direction)) _LOGGER.error("Received unidentified message from Locative: %s", direction) @@ -115,4 +119,3 @@ def _check_data(handler, data): return False return True - From ae0dbbcfa599c5670f8176d75dae03d63a466282 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 30 Dec 2015 19:44:02 +0000 Subject: [PATCH 102/267] Added support for event subscriptions --- homeassistant/components/light/vera.py | 17 ++++++++++--- homeassistant/components/switch/vera.py | 33 ++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 829d3cfccdb..23daba4991f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -14,6 +14,8 @@ from homeassistant.components.switch.vera import VeraSwitch from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' '#python-vera==0.1.1'] @@ -36,10 +38,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device_data = config.get('device_data', {}) - controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: - devices = controller.get_devices([ + devices = vera_controller.get_devices([ 'Switch', 'On/Off Switch', 'Dimmable Switch']) @@ -54,7 +65,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): exclude = extra_data.get('exclude', False) if exclude is not True: - lights.append(VeraLight(device, extra_data)) + lights.append(VeraLight(device, vera_controller, extra_data)) add_devices_callback(lights) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 14983919c64..0df1c390929 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -13,7 +13,11 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) + ATTR_BATTERY_LEVEL, + ATTR_TRIPPED, + ATTR_ARMED, + ATTR_LAST_TRIP_TIME, + EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' @@ -37,7 +41,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: devices = vera_controller.get_devices([ @@ -53,7 +66,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_switches.append(VeraSwitch(device, extra_data)) + vera_switches.append( + VeraSwitch(device, vera_controller, extra_data)) return vera_switches @@ -66,9 +80,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSwitch(ToggleEntity): """ Represents a Vera Switch. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device self.extra_data = extra_data + self.controller = controller if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') else: @@ -77,6 +92,16 @@ class VeraSwitch(ToggleEntity): # for debouncing status check after command is sent self.last_command_send = 0 + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + @property def name(self): """ Get the mame of the switch. """ From 4e2d75a8f48cc7bfe3cc4de59508968488246dad Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Wed, 30 Dec 2015 16:59:22 -0500 Subject: [PATCH 103/267] fix style --- homeassistant/components/light/hue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 29ee523dec4..40875d8ea0e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -173,7 +173,8 @@ class HueLight(Light): """ Represents a Hue light """ # pylint: disable=too-many-arguments - def __init__(self, light_id, info, bridge, update_lights, bridge_type='hue'): + def __init__(self, light_id, info, bridge, update_lights, + bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge From 69ed6fe6e7abaa07be3be4d57530a3be8d1c88bf Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Thu, 31 Dec 2015 05:48:23 +0100 Subject: [PATCH 104/267] 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 105/267] 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 c23375a18b1793dfe0e9066763c85bf45cbf255c Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 30 Dec 2015 17:30:20 -0700 Subject: [PATCH 106/267] Add case for test message --- homeassistant/components/device_tracker/locative.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index cb8e42fd1c4..72e458bc314 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -77,7 +77,12 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. handler.write_json_message( - "Ignoring transition to {}".format(location_name)) + "Ignoring transition from {}".format(location_name)) + + elif direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + handler.write_text("Received test message.") else: handler.write_json_message( From 7d41ce4e46c0f6a77218621619bd287c550a4665 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 30 Dec 2015 22:43:00 -0700 Subject: [PATCH 107/267] Switch from json messages to plain text messages --- .../components/device_tracker/locative.py | 34 +++++++++---------- homeassistant/components/http.py | 14 +++++++- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 72e458bc314..c635aa47858 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -46,8 +46,8 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): try: gps_coords = (float(data['latitude']), float(data['longitude'])) except ValueError: - handler.write_json_message("Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Invalid latitude / longitude format.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Received invalid latitude / longitude format.") return @@ -57,11 +57,11 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if "zone.{}".format(location_name.lower()) in zones: see(dev_id=device, location_name=location_name) - handler.write_json_message( + handler.write_text( "Set new location to {}".format(location_name)) else: see(dev_id=device, gps=gps_coords) - handler.write_json_message( + handler.write_text( "Set new location to {}".format(gps_coords)) elif direction == 'exit': @@ -70,13 +70,13 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if current_zone.lower() == location_name.lower(): see(dev_id=device, location_name=STATE_NOT_HOME) - handler.write_json_message("Set new location to not home") + handler.write_text("Set new location to not home") else: # Ignore the message if it is telling us to exit a zone that we # aren't currently in. This occurs when a zone is entered before # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. - handler.write_json_message( + handler.write_text( "Ignoring transition from {}".format(location_name)) elif direction == 'test': @@ -85,7 +85,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): handler.write_text("Received test message.") else: - handler.write_json_message( + handler.write_text( "Received unidentified message: {}".format(direction)) _LOGGER.error("Received unidentified message from Locative: %s", direction) @@ -93,33 +93,33 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): def _check_data(handler, data): if not isinstance(data, dict): - handler.write_json_message("Error while parsing Locative message.", - HTTP_INTERNAL_SERVER_ERROR) + handler.write_text("Error while parsing Locative message.", + HTTP_INTERNAL_SERVER_ERROR) _LOGGER.error("Error while parsing Locative message: " "data is not a dict.") return False if 'latitude' not in data or 'longitude' not in data: - handler.write_json_message("Latitude and longitude not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Latitude and longitude not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Latitude and longitude not specified.") return False if 'device' not in data: - handler.write_json_message("Device id not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Device id not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Device id not specified.") return False if 'id' not in data: - handler.write_json_message("Location id not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Location id not specified.") return False if 'trigger' not in data: - handler.write_json_message("Trigger is not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Trigger is not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Trigger is not specified.") return False diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 7a4e87de5a8..cd701c24bb6 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs import homeassistant.core as ha from homeassistant.const import ( - SERVER_PORT, CONTENT_TYPE_JSON, + SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED, @@ -293,6 +293,18 @@ class RequestHandler(SimpleHTTPRequestHandler): json.dumps(data, indent=4, sort_keys=True, cls=rem.JSONEncoder).encode("UTF-8")) + def write_text(self, message, status_code=HTTP_OK): + """ Helper method to return a text message to the caller. """ + self.send_response(status_code) + self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN) + + self.set_session_cookie_header() + + self.end_headers() + + if message is not None: + self.wfile.write(message.encode("UTF-8")) + def write_file(self, path, cache_headers=True): """ Returns a file to the user. """ try: From d82859b6ea7c790ef39cb2615ebd0d216b58d586 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 10:57:54 +0000 Subject: [PATCH 108/267] Turn off poll --- homeassistant/components/switch/vera.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 0df1c390929..52029a2c5ec 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -143,6 +143,11 @@ class VeraSwitch(ToggleEntity): self.vera_device.switch_off() self.is_on_status = False + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + @property def is_on(self): """ True if device is on. """ From a8bb75d0706030035aed9299e561382e6e3e873b Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 12:16:03 +0000 Subject: [PATCH 109/267] Update sensor with subscription code, change to use pyvera library --- homeassistant/components/light/vera.py | 4 +--- homeassistant/components/sensor/vera.py | 30 ++++++++++++++++++++----- homeassistant/components/switch/vera.py | 4 +--- requirements_all.txt | 2 +- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 23daba4991f..169fa442134 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,9 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 7fb72fd91b7..22fdffc8f1d 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,9 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +35,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + categories = ['Temperature Sensor', 'Light Sensor', 'Sensor'] devices = [] try: @@ -53,7 +60,7 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, extra_data)) + vera_sensors.append(VeraSensor(device, controller, extra_data)) return vera_sensors @@ -66,8 +73,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSensor(Entity): """ Represents a Vera Sensor. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device + self.controller = controller self.extra_data = extra_data if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') @@ -76,6 +84,16 @@ class VeraSensor(Entity): self.current_value = '' self._temperature_units = None + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + def __str__(self): return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 52029a2c5ec..68a0a1d8871 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,9 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a9ec467e8b1..387a7217f92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 +#pyvera==0.2.0 # homeassistant.components.wink # homeassistant.components.light.wink From 90ae5c6646fb9ff09f8c5dc09fa0f089a2208056 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 12:25:24 +0000 Subject: [PATCH 110/267] Add missed import, fix style error. --- homeassistant/components/sensor/vera.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 22fdffc8f1d..ccb71366df6 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -13,7 +13,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, - TEMP_CELCIUS, TEMP_FAHRENHEIT) + TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['#pyvera==0.2.0'] @@ -60,7 +60,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, controller, extra_data)) + vera_sensors.append( + VeraSensor(device, vera_controller, extra_data)) return vera_sensors From 5f89b34831e836f0b8ef60c7c264fde8b5fc41d8 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 16:09:05 +0000 Subject: [PATCH 111/267] Bump pyvera version --- homeassistant/components/light/vera.py | 2 +- homeassistant/components/sensor/vera.py | 2 +- homeassistant/components/switch/vera.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 169fa442134..2627b505ef8 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index ccb71366df6..836dfacf4f1 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 68a0a1d8871..7feea6c99c6 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 387a7217f92..156bc1657f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -#pyvera==0.2.0 +#pyvera==0.2.1 # homeassistant.components.wink # homeassistant.components.light.wink From 6773c35760e82940eaf2c5a6f27fcb39b9afbeb4 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 18:47:12 +0000 Subject: [PATCH 112/267] Bump pywemo version --- homeassistant/components/switch/wemo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 6057681c53c..ad21463ea17 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.5'] +REQUIREMENTS = ['pywemo==0.3.7'] _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None diff --git a/requirements_all.txt b/requirements_all.txt index cb623ce0920..7aca45c6069 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.5 +pywemo==0.3.7 # homeassistant.components.tellduslive tellive-py==0.5.2 From 55d1ad94ef8a34fb0c5e5b06e709e79a3be4d322 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 00:33:48 -0700 Subject: [PATCH 113/267] Add tests for Locative --- .coveragerc | 1 - .../device_tracker/test_locative.py | 246 ++++++++++++++++++ 2 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 tests/components/device_tracker/test_locative.py diff --git a/.coveragerc b/.coveragerc index 4b916a7fbcd..f5d1789a174 100644 --- a/.coveragerc +++ b/.coveragerc @@ -41,7 +41,6 @@ omit = homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py - homeassistant/components/device_tracker/locative.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py new file mode 100644 index 00000000000..a6e5a431d36 --- /dev/null +++ b/tests/components/device_tracker/test_locative.py @@ -0,0 +1,246 @@ +""" +tests.components.device_tracker.locative +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the locative device tracker component. +""" + +import unittest +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 + +SERVER_PORT = 8126 +HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) + +hass = None + + +def _url(data={}): + """ Helper method to generate urls. """ + data = "&".join(["{}={}".format(name, value) for name, value in data.items()]) + return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data) + + +@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. """ + global hass + + hass = ha.HomeAssistant() + + # Set up a couple of zones + bootstrap.setup_component(hass, zone.DOMAIN, { + zone.DOMAIN: [ + { + 'name': 'Home', + 'latitude': 41.7855, + 'longitude': -110.7367, + 'radius': 200 + }, + { + 'name': 'Work', + 'latitude': 41.5855, + 'longitude': -110.9367, + 'radius': 100 + } + ] + }) + + # Set up server + bootstrap.setup_component(hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_SERVER_PORT: SERVER_PORT + } + }) + + # Set up API + bootstrap.setup_component(hass, 'api') + + # Set up device tracker + bootstrap.setup_component(hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + 'platform': 'locative' + } + }) + + hass.start() + + +def tearDownModule(): # pylint: disable=invalid-name + """ Stops the Home Assistant server. """ + hass.stop() + + +class TestLocative(unittest.TestCase): + """ Test Locative """ + + def test_missing_data(self): + data = { + 'latitude': 1.0, + 'longitude': 1.1, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # No data + req = requests.get(_url({})) + self.assertEqual(422, req.status_code) + + # No latitude + copy = data.copy() + del copy['latitude'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No device + copy = data.copy() + del copy['device'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No location + copy = data.copy() + del copy['id'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No trigger + copy = data.copy() + del copy['trigger'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # Bad longitude + copy = data.copy() + copy['longitude'] = 'hello world' + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # Test message + copy = data.copy() + copy['trigger'] = 'test' + req = requests.get(_url(copy)) + self.assertEqual(200, req.status_code) + + # Unknown trigger + copy = data.copy() + copy['trigger'] = 'foobar' + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + + def test_known_zone(self): + """ Test when there is a known zone """ + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter the Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + data['id'] = 'HOME' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') + + data['id'] = 'hOmE' + data['trigger'] = 'enter' + + # Enter Home again + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + + def test_unknown_zone(self): + """ Test when there is no known zone """ + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Foobar', + 'trigger': 'enter' + } + + # Enter Foobar + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'not_home') + self.assertEqual(state.attributes['latitude'], data['latitude']) + self.assertEqual(state.attributes['longitude'], data['longitude']) + + data['trigger'] = 'exit' + + # Exit Foobar + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'not_home') + self.assertEqual(state.attributes['latitude'], data['latitude']) + self.assertEqual(state.attributes['longitude'], data['longitude']) + + + def test_exit_after_enter(self): + """ Test when an exit message comes after an enter message """ + + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'home') + + data['id'] = 'Work' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') + + data['id'] = 'Home' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') + + print(req.text) + + From bdb6182921b0319aa347199393bd9f4494ab2c85 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 00:34:06 -0700 Subject: [PATCH 114/267] Changes to locative based on tests --- .../components/device_tracker/locative.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index c635aa47858..3263c424c74 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -40,7 +40,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): return device = data['device'].replace('-', '') - location_name = data['id'] + location_name = data['id'].lower() direction = data['trigger'] try: @@ -53,9 +53,8 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if direction == 'enter': zones = [state for state in hass.states.entity_ids('zone')] - _LOGGER.info(zones) - if "zone.{}".format(location_name.lower()) in zones: + if "zone.{}".format(location_name) in zones: see(dev_id=device, location_name=location_name) handler.write_text( "Set new location to {}".format(location_name)) @@ -68,7 +67,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): current_zone = hass.states.get( "{}.{}".format("device_tracker", device)).state - if current_zone.lower() == location_name.lower(): + if current_zone == location_name: see(dev_id=device, location_name=STATE_NOT_HOME) handler.write_text("Set new location to not home") else: @@ -77,7 +76,9 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. handler.write_text( - "Ignoring transition from {}".format(location_name)) + 'Ignoring exit from "{}". Already in "{}".'.format( + location_name, + current_zone.split('.')[-1])) elif direction == 'test': # In the app, a test message can be sent. Just return something to @@ -86,7 +87,8 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): else: handler.write_text( - "Received unidentified message: {}".format(direction)) + "Received unidentified message: {}".format(direction), + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Received unidentified message from Locative: %s", direction) From 1bcca8cba14f3dcac9efd41141b5141517c3be00 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 00:52:12 -0700 Subject: [PATCH 115/267] Fix problem with test --- tests/components/device_tracker/test_locative.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index a6e5a431d36..81c8152238e 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -78,11 +78,12 @@ def tearDownModule(): # pylint: disable=invalid-name """ Stops the Home Assistant server. """ hass.stop() - +# Stub out update_config or else Travis CI raises an exception +@patch('homeassistant.components.device_tracker.update_config') class TestLocative(unittest.TestCase): """ Test Locative """ - def test_missing_data(self): + def test_missing_data(self, update_config): data = { 'latitude': 1.0, 'longitude': 1.1, @@ -138,7 +139,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(422, req.status_code) - def test_known_zone(self): + def test_known_zone(self, update_config): """ Test when there is a known zone """ data = { 'latitude': 40.7855, @@ -173,7 +174,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(state_name, 'home') - def test_unknown_zone(self): + def test_unknown_zone(self, update_config): """ Test when there is no known zone """ data = { 'latitude': 40.7855, @@ -204,7 +205,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(state.attributes['longitude'], data['longitude']) - def test_exit_after_enter(self): + def test_exit_after_enter(self, update_config): """ Test when an exit message comes after an enter message """ data = { @@ -240,7 +241,3 @@ class TestLocative(unittest.TestCase): state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) self.assertEqual(state.state, 'work') - - print(req.text) - - From 5d953061e823c585a3b27ee182f45287304d27a0 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 01:43:18 -0700 Subject: [PATCH 116/267] Remove unnecessary error checking --- .../components/device_tracker/locative.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 3263c424c74..f4fd72d0c5f 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -10,7 +10,7 @@ import logging from functools import partial from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR, STATE_NOT_HOME) + HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) _LOGGER = logging.getLogger(__name__) @@ -57,11 +57,11 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if "zone.{}".format(location_name) in zones: see(dev_id=device, location_name=location_name) handler.write_text( - "Set new location to {}".format(location_name)) + "Setting location to {}".format(location_name)) else: see(dev_id=device, gps=gps_coords) handler.write_text( - "Set new location to {}".format(gps_coords)) + "Setting location to {}".format(gps_coords)) elif direction == 'exit': current_zone = hass.states.get( @@ -69,14 +69,14 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if current_zone == location_name: see(dev_id=device, location_name=STATE_NOT_HOME) - handler.write_text("Set new location to not home") + handler.write_text("Setting location to not home") else: # Ignore the message if it is telling us to exit a zone that we # aren't currently in. This occurs when a zone is entered before # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. handler.write_text( - 'Ignoring exit from "{}". Already in "{}".'.format( + 'Ignoring exit from {} (already in {})'.format( location_name, current_zone.split('.')[-1])) @@ -94,13 +94,6 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): def _check_data(handler, data): - if not isinstance(data, dict): - handler.write_text("Error while parsing Locative message.", - HTTP_INTERNAL_SERVER_ERROR) - _LOGGER.error("Error while parsing Locative message: " - "data is not a dict.") - return False - if 'latitude' not in data or 'longitude' not in data: handler.write_text("Latitude and longitude not specified.", HTTP_UNPROCESSABLE_ENTITY) From f8e5df237bb98fabce2330d7a99b9b021f2e7139 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 18:58:12 +0000 Subject: [PATCH 117/267] Remove '#'' from requirements --- homeassistant/components/light/vera.py | 2 +- homeassistant/components/sensor/vera.py | 2 +- homeassistant/components/switch/vera.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 2627b505ef8..9135323fb1f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 836dfacf4f1..db283c51633 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 7feea6c99c6..614b588f36f 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 156bc1657f1..b32d49dcc74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -#pyvera==0.2.1 +pyvera==0.2.1 # homeassistant.components.wink # homeassistant.components.light.wink From 9e0946b207c2d2fb9d69c864424768cfd79d528a Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 19:15:21 +0000 Subject: [PATCH 118/267] Turn off polling for sensor too! --- homeassistant/components/sensor/vera.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index db283c51633..03b8d05d2f5 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -136,6 +136,11 @@ class VeraSensor(Entity): attr['Vera Device Id'] = self.vera_device.vera_device_id return attr + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + def update(self): if self.vera_device.category == "Temperature Sensor": self.vera_device.refresh_value('CurrentTemperature') From ce152e9c94a5212f60f1c5560f8b7f36d9586eb5 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 12:02:50 -0700 Subject: [PATCH 119/267] Simplify logic --- .../components/device_tracker/locative.py | 32 +++------- .../device_tracker/test_locative.py | 62 ++++--------------- 2 files changed, 20 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index f4fd72d0c5f..e7532d1075d 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -11,10 +11,11 @@ from functools import partial from homeassistant.const import ( HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) +from homeassistant.components.device_tracker import DOMAIN _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['http', 'zone'] +DEPENDENCIES = ['http'] URL_API_LOCATIVE_ENDPOINT = "/api/locative" @@ -43,31 +44,15 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): location_name = data['id'].lower() direction = data['trigger'] - try: - gps_coords = (float(data['latitude']), float(data['longitude'])) - except ValueError: - handler.write_text("Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) - _LOGGER.error("Received invalid latitude / longitude format.") - return - if direction == 'enter': - zones = [state for state in hass.states.entity_ids('zone')] - - if "zone.{}".format(location_name) in zones: - see(dev_id=device, location_name=location_name) - handler.write_text( - "Setting location to {}".format(location_name)) - else: - see(dev_id=device, gps=gps_coords) - handler.write_text( - "Setting location to {}".format(gps_coords)) + see(dev_id=device, location_name=location_name) + handler.write_text("Setting location to {}".format(location_name)) elif direction == 'exit': - current_zone = hass.states.get( - "{}.{}".format("device_tracker", device)).state + current_state = hass.states.get( + "{}.{}".format(DOMAIN, device)).state - if current_zone == location_name: + if current_state == location_name: see(dev_id=device, location_name=STATE_NOT_HOME) handler.write_text("Setting location to not home") else: @@ -77,8 +62,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): # first, then the exit message will be sent second. handler.write_text( 'Ignoring exit from {} (already in {})'.format( - location_name, - current_zone.split('.')[-1])) + location_name, current_state)) elif direction == 'test': # In the app, a test message can be sent. Just return something to diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 81c8152238e..b86f24455de 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -36,24 +36,6 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name hass = ha.HomeAssistant() - # Set up a couple of zones - bootstrap.setup_component(hass, zone.DOMAIN, { - zone.DOMAIN: [ - { - 'name': 'Home', - 'latitude': 41.7855, - 'longitude': -110.7367, - 'radius': 200 - }, - { - 'name': 'Work', - 'latitude': 41.5855, - 'longitude': -110.9367, - 'radius': 100 - } - ] - }) - # Set up server bootstrap.setup_component(hass, http.DOMAIN, { http.DOMAIN: { @@ -120,12 +102,6 @@ class TestLocative(unittest.TestCase): req = requests.get(_url(copy)) self.assertEqual(422, req.status_code) - # Bad longitude - copy = data.copy() - copy['longitude'] = 'hello world' - req = requests.get(_url(copy)) - self.assertEqual(422, req.status_code) - # Test message copy = data.copy() copy['trigger'] = 'test' @@ -139,7 +115,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(422, req.status_code) - def test_known_zone(self, update_config): + def test_enter_and_exit(self, update_config): """ Test when there is a known zone """ data = { 'latitude': 40.7855, @@ -173,36 +149,22 @@ class TestLocative(unittest.TestCase): state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state self.assertEqual(state_name, 'home') - - def test_unknown_zone(self, update_config): - """ Test when there is no known zone """ - data = { - 'latitude': 40.7855, - 'longitude': -111.7367, - 'device': '123', - 'id': 'Foobar', - 'trigger': 'enter' - } - - # Enter Foobar - req = requests.get(_url(data)) - self.assertEqual(200, req.status_code) - - state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) - self.assertEqual(state.state, 'not_home') - self.assertEqual(state.attributes['latitude'], data['latitude']) - self.assertEqual(state.attributes['longitude'], data['longitude']) - data['trigger'] = 'exit' - # Exit Foobar + # Exit Home req = requests.get(_url(data)) self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') - state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) - self.assertEqual(state.state, 'not_home') - self.assertEqual(state.attributes['latitude'], data['latitude']) - self.assertEqual(state.attributes['longitude'], data['longitude']) + data['id'] = 'work' + data['trigger'] = 'enter' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'work') def test_exit_after_enter(self, update_config): From 394c87c40b23eeb50d293a31bb9655d0c32dda43 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 13:05:24 -0700 Subject: [PATCH 120/267] Remove unnecessary condition in write_text --- homeassistant/components/http.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index cd701c24bb6..b7f57b0157e 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -302,8 +302,7 @@ class RequestHandler(SimpleHTTPRequestHandler): self.end_headers() - if message is not None: - self.wfile.write(message.encode("UTF-8")) + self.wfile.write(message.encode("UTF-8")) def write_file(self, path, cache_headers=True): """ Returns a file to the user. """ From 11f32d050028c6cf396b71811e025b064926ff56 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Thu, 31 Dec 2015 14:58:18 -0600 Subject: [PATCH 121/267] Add is_state_attr method. Returns True if the entity exists and has an attribute with the given name and value. --- homeassistant/core.py | 7 +++++++ homeassistant/util/template.py | 3 ++- tests/test_core.py | 12 ++++++++++++ tests/util/test_template.py | 8 ++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index e2650969eb0..55ceddb37c7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -468,6 +468,13 @@ class StateMachine(object): return (entity_id in self._states and self._states[entity_id].state == state) + def is_state_attr(self, entity_id, name, value): + """Test if entity exists and has a state attribute set to value.""" + entity_id = entity_id.lower() + + return (entity_id in self._states and + self._states[entity_id].attributes.get(name, None) == value) + def remove(self, entity_id): """Remove the state of an entity. diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index d0a07507bdf..bc7431ebf6d 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs): try: return ENV.from_string(template, { 'states': AllStates(hass), - 'is_state': hass.states.is_state + 'is_state': hass.states.is_state, + 'is_state_attr': hass.states.is_state_attr }).render(kwargs).strip() except jinja2.TemplateError as err: raise TemplateError(err) diff --git a/tests/test_core.py b/tests/test_core.py index fee46fe2dd4..ca935e2d106 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -321,6 +321,18 @@ class TestStateMachine(unittest.TestCase): self.assertFalse(self.states.is_state('light.Bowl', 'off')) self.assertFalse(self.states.is_state('light.Non_existing', 'on')) + def test_is_state_attr(self): + """ Test is_state_attr method. """ + self.states.set("light.Bowl", "on", {"brightness": 100}) + self.assertTrue( + self.states.is_state_attr('light.Bowl', 'brightness', 100)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 200)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 'Bowl')) + self.assertFalse( + self.states.is_state_attr('light.Non_existing', 'brightness', 100)) + def test_entity_ids(self): """ Test get_entity_ids method. """ ent_ids = self.states.entity_ids() diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 1ecd7d5b894..844826f80d5 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -117,6 +117,14 @@ class TestUtilTemplate(unittest.TestCase): self.hass, '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}')) + def test_is_state_attr(self): + self.hass.states.set('test.object', 'available', {'mode': 'on'}) + self.assertEqual( + 'yes', + template.render( + self.hass, + '{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}')) + def test_states_function(self): self.hass.states.set('test.object', 'available') self.assertEqual( From 9c85702c875b4934595de0b9e2625be2949bc728 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Thu, 31 Dec 2015 18:39:40 -0500 Subject: [PATCH 122/267] combine ifs --- .../components/switch/command_switch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index c36ca4e9ce9..a90ed61c3e2 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -115,14 +115,14 @@ class CommandSwitch(SwitchDevice): def turn_on(self, **kwargs): """ Turn the device on. """ - if CommandSwitch._switch(self._command_on): - if not self._command_state: - self._state = True - self.update_ha_state() + if (CommandSwitch._switch(self._command_on) and + not self._command_state): + self._state = True + self.update_ha_state() def turn_off(self, **kwargs): """ Turn the device off. """ - if CommandSwitch._switch(self._command_off): - if not self._command_state: - self._state = False - self.update_ha_state() + if (CommandSwitch._switch(self._command_off) and + not self._command_state): + self._state = False + self.update_ha_state() From c703c89dbd0fea0b10dbddcf5f02d587c467f1a3 Mon Sep 17 00:00:00 2001 From: sander Date: Fri, 1 Jan 2016 15:29:58 +0100 Subject: [PATCH 123/267] implement away mode --- .../components/thermostat/honeywell.py | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 4139c5d8aa7..15162a4d279 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -6,8 +6,9 @@ Adds support for Honeywell Round Connected and Honeywell Evohome thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.honeywell/ """ -import socket import logging +import socket + from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) @@ -15,6 +16,7 @@ REQUIREMENTS = ['evohomeclient==0.2.4'] _LOGGER = logging.getLogger(__name__) +CONF_AWAY_TEMP = "away_temperature" # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -23,20 +25,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - + try: + away_temp = float(config.get(CONF_AWAY_TEMP,16)) + except ValueError: + _LOGGER.error("value entered for item {} should convert to a number" + .format(CONF_AWAY_TEMP)) if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", CONF_USERNAME, CONF_PASSWORD) return False evo_api = EvohomeClient(username, password) + try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0)]) + add_devices([RoundThermostat(evo_api, zone['id'], i == 0,away_temp)]) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" + "Connection error logging into the honeywell evohome web service" ) return False @@ -44,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ - def __init__(self, device, zone_id, master): + def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None self._target_temperature = None @@ -52,6 +59,8 @@ class RoundThermostat(ThermostatDevice): self._id = zone_id self._master = master self._is_dhw = False + self._away_temp = away_temp + self._away = False self.update() @property @@ -80,6 +89,25 @@ class RoundThermostat(ThermostatDevice): """ Set new target temperature """ self.device.set_temperature(self._name, temperature) + @property + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return self._away + + def turn_away_mode_on(self): + """ Turns away on. + Evohome does have a proprietary away mode, but it doesn't really work + the way it should. For example: If you set a temperature manually + it doesn't get overwritten when away mode is switched on. + """ + self._away = True + self.device.set_temperature(self._name, self._away_temp) + + def turn_away_mode_off(self): + """ Turns away off. """ + self._away = False + self.device.cancel_temp_override(self._name) + def update(self): try: # Only refresh if this is the "master" device, From a36ae4b24af3d4903203abe206aebfbc2734be2c Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Sat, 2 Jan 2016 01:01:11 -0500 Subject: [PATCH 124/267] Reduce chatiness --- homeassistant/components/light/hue.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 40875d8ea0e..61bff75bbd3 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -124,9 +124,7 @@ def setup_bridge(host, hass, add_devices_callback): api_name = api.get('config').get('name') if api_name == 'RaspBee-GW': bridge_type = 'deconz' - _LOGGER.info("Found DeCONZ gateway (%s)", api_name) else: - _LOGGER.info("Found Hue bridge (%s)", api_name) bridge_type = 'hue' for light_id, info in api_states.items(): From 8f2ca856c7bec90b55f788dc97ea09bd1c621259 Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 11:56:07 +0100 Subject: [PATCH 125/267] added return False --- homeassistant/components/thermostat/honeywell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 15162a4d279..de6d5ff6a56 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -18,6 +18,7 @@ _LOGGER = logging.getLogger(__name__) CONF_AWAY_TEMP = "away_temperature" + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the honeywel thermostat. """ @@ -26,10 +27,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) try: - away_temp = float(config.get(CONF_AWAY_TEMP,16)) + away_temp = float(config.get(CONF_AWAY_TEMP, 16)) except ValueError: _LOGGER.error("value entered for item {} should convert to a number" .format(CONF_AWAY_TEMP)) + return False if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", CONF_USERNAME, CONF_PASSWORD) @@ -40,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0,away_temp)]) + add_devices([RoundThermostat(evo_api, zone['id'], i == 0, away_temp)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" From cdf2179b3e09c50e21d49314da5cea36ad7ecd4a Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 2 Jan 2016 10:54:26 -0600 Subject: [PATCH 126/267] Describe device tracker see service --- .../components/device_tracker/services.yaml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index e69de29bb2d..63907f7457b 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -0,0 +1,33 @@ +# Describes the format for available device tracker services + +see: + description: Control tracked device + + fields: + mac: + description: MAC address of device + example: 'FF:FF:FF:FF:FF:FF' + + dev_id: + description: Id of device (find id in tracked_devices.yaml) + example: 'phonedave' + + host_name: + description: Hostname of device + example: 'Dave' + + location_name: + description: Name of location where device is located (not_home is away) + example: 'home' + + gps: + description: GPS coordinates where device is located (latitude, longitude) + example: '[51.509802, -0.086692]' + + gps_accuracy: + description: Accuracy of GPS coordinates + example: '80' + + battery: + description: Battery level of device + example: '100' From e9b2cf1600ff97d8ee3d069d15d24559ba0e9f05 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Fri, 1 Jan 2016 08:30:59 -0700 Subject: [PATCH 127/267] Ensure we send data to influx as float and not as a string value. --- homeassistant/components/influxdb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 2286dd2d659..7cbba00afbb 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -7,7 +7,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/influxdb/ """ import logging - import homeassistant.util as util from homeassistant.helpers import validate_config from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF, @@ -77,6 +76,10 @@ def setup(hass, config): _state = 0 else: _state = state.state + try: + _state = float(_state) + except ValueError: + pass measurement = state.attributes.get('unit_of_measurement', state.domain) From 5804dde0e979fe097e10c95fe785e69195902007 Mon Sep 17 00:00:00 2001 From: xifle Date: Sat, 2 Jan 2016 18:26:59 +0100 Subject: [PATCH 128/267] Enables the use of owntracks transition events By using the configuration option "use_events:yes" in the device_tracker section, only 'enter'/'leave' events are considered to calculate the state of a tracker device. The home zone is defined as the owntracks region 'home'. Other regions may also be defined, the name of the region is then used as state for the device. All owntracks regions, the 'Share' setting must be enabled in the app. --- .../components/device_tracker/owntracks.py | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index b98c3a1636c..a1818e60901 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,6 +1,6 @@ """ homeassistant.components.device_tracker.owntracks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OwnTracks platform for the device tracker. For more details about this platform, please refer to the documentation at @@ -10,14 +10,16 @@ import json import logging import homeassistant.components.mqtt as mqtt +from homeassistant.const import (STATE_HOME, STATE_NOT_HOME) DEPENDENCIES = ['mqtt'] +CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' - +EVENT_TOPIC = 'owntracks/+/+/event' def setup_scanner(hass, config, see): - """ Set up a OwnTracksks tracker. """ + """ Set up an OwnTracks tracker. """ def owntracks_location_update(topic, payload, qos): """ MQTT message received. """ @@ -47,7 +49,58 @@ def setup_scanner(hass, config, see): kwargs['battery'] = data['batt'] see(**kwargs) + + + def owntracks_event_update(topic, payload, qos): + """ MQTT event (geofences) received. """ - mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typetransition + try: + data = json.loads(payload) + except ValueError: + # If invalid JSON + logging.getLogger(__name__).error( + 'Unable to parse payload as JSON: %s', payload) + return + + if not isinstance(data, dict) or data.get('_type') != 'transition': + return + + + # check if in "home" fence or other zone + location = '' + if data['event'] == 'enter': + + if data['desc'] == 'home': + location = STATE_HOME + else: + location = data['desc'] + + elif data['event'] == 'leave': + location = STATE_NOT_HOME + else: + logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', data['event']) + return + + parts = topic.split('/') + kwargs = { + 'dev_id': '{}_{}'.format(parts[1], parts[2]), + 'host_name': parts[1], + 'gps': (data['lat'], data['lon']), + 'location_name': location, + } + if 'acc' in data: + kwargs['gps_accuracy'] = data['acc'] + + see(**kwargs) + + + use_events = config.get(CONF_TRANSITION_EVENTS) + + if use_events: + mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + else: + mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) return True From 217ffc215ba346a1c9f87b92da6eb862e3d52de0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 10:27:11 -0800 Subject: [PATCH 129/267] Update PyNetgear version --- homeassistant/components/device_tracker/netgear.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 5d20e98e992..ab1eccba769 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear==0.3'] +REQUIREMENTS = ['pynetgear==0.3.1'] def get_scanner(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index 18d308f6d5c..997cbb567e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -16,7 +16,7 @@ fuzzywuzzy==0.8.0 pyicloud==0.7.2 # homeassistant.components.device_tracker.netgear -pynetgear==0.3 +pynetgear==0.3.1 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.4.3 From 39de92960d040da71ccfe3b716be54f2ef8ad39f Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:27:40 +0100 Subject: [PATCH 130/267] line too long change --- homeassistant/components/thermostat/honeywell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index de6d5ff6a56..4d1a7fe708d 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -42,7 +42,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0, away_temp)]) + add_devices([RoundThermostat(evo_api, + zone['id'], + i == 0, + away_temp)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" From 8c7898ed054fc10c07f2c02bd093f54768be615b Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:53:25 +0100 Subject: [PATCH 131/267] pylinting.. --- homeassistant/components/thermostat/honeywell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 4d1a7fe708d..491fe57900e 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): away_temp)]) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" + "Connection error logging into the honeywell evohome web service" ) return False From 36f5caa214215a51b29870a60aa1fda4d7e333ba Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:59:45 +0100 Subject: [PATCH 132/267] more pylinting.. --- homeassistant/components/thermostat/honeywell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 491fe57900e..0b7479bd202 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -29,8 +29,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: away_temp = float(config.get(CONF_AWAY_TEMP, 16)) except ValueError: - _LOGGER.error("value entered for item {} should convert to a number" - .format(CONF_AWAY_TEMP)) + _LOGGER.error("value entered for item %s should convert to a number", + CONF_AWAY_TEMP) return False if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", From 55c5d254d58e6902667ea712dc9eb0d820b258db Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 21:09:03 +0100 Subject: [PATCH 133/267] some more pylinting.. --- homeassistant/components/thermostat/honeywell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 0b7479bd202..5475e1ce306 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -56,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ + # pylint: disable=too-many-instance-attributes def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None From 0361f37178bdc3a409718f5d73e254512925817a Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Sat, 2 Jan 2016 16:13:58 -0500 Subject: [PATCH 134/267] Support multiple hue hubs using a filename parameter. --- homeassistant/components/light/hue.py | 15 ++++++++------- homeassistant/const.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 61bff75bbd3..b828847ecb0 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from homeassistant.loader import get_component import homeassistant.util as util import homeassistant.util.color as color_util -from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME +from homeassistant.const import CONF_HOST, CONF_FILENAME, DEVICE_DEFAULT_NAME from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, @@ -35,9 +35,9 @@ _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -def _find_host_from_config(hass): +def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): """ Attempt to detect host based on existing configuration. """ - path = hass.config.path(PHUE_CONFIG_FILE) + path = hass.config.path(filename) if not os.path.isfile(path): return None @@ -54,13 +54,14 @@ def _find_host_from_config(hass): def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Gets the Hue lights. """ + filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE) if discovery_info is not None: host = urlparse(discovery_info[1]).hostname else: host = config.get(CONF_HOST, None) if host is None: - host = _find_host_from_config(hass) + host = _find_host_from_config(hass, filename) if host is None: _LOGGER.error('No host found in configuration') @@ -70,17 +71,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if host in _CONFIGURING: return - setup_bridge(host, hass, add_devices_callback) + setup_bridge(host, hass, add_devices_callback, filename) -def setup_bridge(host, hass, add_devices_callback): +def setup_bridge(host, hass, add_devices_callback, filename): """ Setup a phue bridge based on host parameter. """ import phue try: bridge = phue.Bridge( host, - config_file_path=hass.config.path(PHUE_CONFIG_FILE)) + config_file_path=hass.config.path(filename)) except ConnectionRefusedError: # Wrong host was given _LOGGER.exception("Error connecting to the Hue bridge at %s", host) diff --git a/homeassistant/const.py b/homeassistant/const.py index 82276d81b48..97e26f8d33a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,6 +24,7 @@ CONF_USERNAME = "username" CONF_PASSWORD = "password" CONF_API_KEY = "api_key" CONF_ACCESS_TOKEN = "access_token" +CONF_FILENAME = "filename" CONF_VALUE_TEMPLATE = "value_template" From 7edbb6aadc081475f521f6dfbc02dbb6f79ac3fb Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:21:04 +0100 Subject: [PATCH 135/267] Added rain sensor --- homeassistant/components/sensor/tellduslive.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 7cc49e3c611..dbf416787a2 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -22,10 +22,14 @@ DEPENDENCIES = ['tellduslive'] SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_RAINRATE = "rrate" +SENSOR_TYPE_RAINTOTAL = "rtot" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], } From 86047eceb1ce21f1d8ac7621396305792163d277 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:28:15 +0100 Subject: [PATCH 136/267] Added wind sensor --- homeassistant/components/sensor/tellduslive.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index dbf416787a2..022ae00a098 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -24,12 +24,18 @@ SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_RAINRATE = "rrate" SENSOR_TYPE_RAINTOTAL = "rtot" +SENSOR_TYPE_WINDDIRECTION = "wdir" +SENSOR_TYPE_WINDAVERAGE = "wavg" +SENSOR_TYPE_WINDGUST = "wgust" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], } From 4a421e25b08f453feba08ad78c4865c2abcc4790 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 13:29:33 -0800 Subject: [PATCH 137/267] Simplify Rest sensors --- .../components/binary_sensor/rest.py | 113 ++++-------------- homeassistant/components/sensor/rest.py | 80 +++---------- 2 files changed, 38 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 6cb6ede5e50..4d82d25e473 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.rest/ """ -from datetime import timedelta import logging -import requests from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.util import template, Throttle +from homeassistant.util import template +from homeassistant.components.sensor.rest import RestData from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) @@ -19,60 +18,33 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_METHOD = 'GET' -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): - """ Get the REST binary sensor. """ - - use_get = False - use_post = False - + """Setup REST binary sensors.""" resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error("Response status is '%s'", response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// or https:// to your URL") - return False - except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint: %s', resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestBinarySensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestBinarySensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): - """ Implements a REST binary sensor. """ + """REST binary sensor.""" def __init__(self, hass, rest, name, value_template): + """Initialize a REST binary sensor.""" self._hass = hass self.rest = rest self._name = name @@ -82,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice): @property def name(self): - """ The name of the binary sensor. """ + """Name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ - if self.rest.data is False: + """Return if the binary sensor is on.""" + if self.rest.data is None: return False - else: - if self._value_template is not None: - self.rest.data = template.render_with_possible_json_value( - self._hass, self._value_template, self.rest.data, False) - return bool(int(self.rest.data)) + + if self._value_template is not None: + self.rest.data = template.render_with_possible_json_value( + self._hass, self._value_template, self.rest.data, False) + return bool(int(self.rest.data)) def update(self): - """ Gets the latest data from REST API and updates the state. """ + """Get the latest data from REST API and updates the state.""" self.rest.update() - - -# pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ - - def __init__(self, resource, verify_ssl): - self._resource = resource - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with GET method. """ - try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index f6a56d3a99e..fdbc1ab26e3 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -26,47 +26,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the REST sensor. """ - - use_get = False - use_post = False - resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error("Response status is '%s'", response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// or https:// to your URL") - return False - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestSensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get('unit_of_measurement'), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestSensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get('unit_of_measurement'), config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments @@ -112,11 +86,11 @@ class RestSensor(Entity): # pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ +class RestData(object): + """Class for handling the data retrieval.""" - def __init__(self, resource, verify_ssl): - self._resource = resource + def __init__(self, method, resource, data, verify_ssl): + self._request = requests.Request(method, resource, data=data).prepare() self._verify_ssl = verify_ssl self.data = None @@ -124,31 +98,11 @@ class RestDataGet(object): def update(self): """ Gets the latest data from REST service with GET method. """ try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) + with requests.Session() as sess: + response = sess.send(self._request, timeout=10, + verify=self._verify_ssl) + self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = None - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) + except requests.exceptions.RequestException: + _LOGGER.error("Error fetching data: %s", self._request) self.data = None From 3abc78eef2c678fe6f0df27f89dfe3ceff997ae9 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:30:02 +0100 Subject: [PATCH 138/267] Added power sensor --- homeassistant/components/sensor/tellduslive.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 022ae00a098..a7bfca49b30 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -27,6 +27,7 @@ SENSOR_TYPE_RAINTOTAL = "rtot" SENSOR_TYPE_WINDDIRECTION = "wdir" SENSOR_TYPE_WINDAVERAGE = "wavg" SENSOR_TYPE_WINDGUST = "wgust" +SENSOR_TYPE_WATT = "watt" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], @@ -36,6 +37,7 @@ SENSOR_TYPES = { SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], + SENSOR_TYPE_WATT: ['Watt', 'W', ""], } From 305c87a9c9753d27f8f003e494009dab7188bdb7 Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 2 Jan 2016 16:01:58 -0600 Subject: [PATCH 139/267] Fix reference known_devices.yaml --- homeassistant/components/device_tracker/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 63907f7457b..dc573ae0275 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -9,7 +9,7 @@ see: example: 'FF:FF:FF:FF:FF:FF' dev_id: - description: Id of device (find id in tracked_devices.yaml) + description: Id of device (find id in known_devices.yaml) example: 'phonedave' host_name: From 7dc1499386dd8e45b58ff79a35b0c702baee782f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 14:23:12 -0800 Subject: [PATCH 140/267] Make CI erros more prominent --- .travis.yml | 4 +--- script/cibuild | 22 ++++++++++++++++++++++ script/test | 14 +++++++------- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4383d49f548..c01b0750360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,7 @@ python: - 3.4 - 3.5 install: - # Validate requirements_all.txt on Python 3.4 - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi - - script/bootstrap_server + - "true" script: - script/cibuild matrix: diff --git a/script/cibuild b/script/cibuild index beb7b22693d..11d91415405 100755 --- a/script/cibuild +++ b/script/cibuild @@ -5,6 +5,28 @@ cd "$(dirname "$0")/.." +if [ "$TRAVIS_PYTHON_VERSION" = "3.5" ]; then + echo "Verifying requirements_all.txt..." + python3 setup.py -q develop 2> /dev/null + tput setaf 1 + script/gen_requirements_all.py validate + VERIFY_REQUIREMENTS_STATUS=$? + tput sgr0 +else + VERIFY_REQUIREMENTS_STATUS=0 +fi + +if [ "$VERIFY_REQUIREMENTS_STATUS" != "0" ]; then + exit $VERIFY_REQUIREMENTS_STATUS +fi + +script/bootstrap_server > /dev/null +DEP_INSTALL_STATUS=$? + +if [ "$DEP_INSTALL_STATUS" != "0" ]; then + exit $DEP_INSTALL_STATUS +fi + if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then NO_LINT=1 fi diff --git a/script/test b/script/test index 25873492001..6a78ce42d41 100755 --- a/script/test +++ b/script/test @@ -5,13 +5,6 @@ cd "$(dirname "$0")/.." -if [ "$NO_LINT" = "1" ]; then - LINT_STATUS=0 -else - script/lint - LINT_STATUS=$? -fi - echo "Running tests..." if [ "$1" = "coverage" ]; then @@ -22,6 +15,13 @@ else TEST_STATUS=$? fi +if [ "$NO_LINT" = "1" ]; then + LINT_STATUS=0 +else + script/lint + LINT_STATUS=$? +fi + if [ $LINT_STATUS -eq 0 ] then exit $TEST_STATUS From 4b96a7c8202470999a862940a337da412eda4a86 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sun, 3 Jan 2016 01:00:10 +0100 Subject: [PATCH 141/267] Untabified lines --- homeassistant/components/sensor/tellduslive.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index a7bfca49b30..ae05ce47e19 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -32,12 +32,12 @@ SENSOR_TYPE_WATT = "watt" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], - SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], - SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], - SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], - SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], - SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], - SENSOR_TYPE_WATT: ['Watt', 'W', ""], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], + SENSOR_TYPE_WATT: ['Watt', 'W', ""], } From c9ff0ab7eb0ef8e52089d1d16699f6235739f52b Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Sun, 3 Jan 2016 00:36:22 -0700 Subject: [PATCH 142/267] Fix for sun if condition --- homeassistant/components/automation/sun.py | 10 ++-- tests/components/automation/test_sun.py | 61 ++++++++++++++++++---- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 394dc904be1..064f6a0a16a 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -82,21 +82,21 @@ def if_action(hass, config): if before is None: before_func = lambda: None elif before == EVENT_SUNRISE: - before_func = lambda: sun.next_rising_utc(hass) + before_offset + before_func = lambda: sun.next_rising(hass) + before_offset else: - before_func = lambda: sun.next_setting_utc(hass) + before_offset + before_func = lambda: sun.next_setting(hass) + before_offset if after is None: after_func = lambda: None elif after == EVENT_SUNRISE: - after_func = lambda: sun.next_rising_utc(hass) + after_offset + after_func = lambda: sun.next_rising(hass) + after_offset else: - after_func = lambda: sun.next_setting_utc(hass) + after_offset + after_func = lambda: sun.next_setting(hass) + after_offset def time_if(): """ Validate time based if-condition """ - now = dt_util.utcnow() + now = dt_util.now() before = before_func() after = after_func() diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 26ecc26c72a..db4782cfd46 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -162,14 +162,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -197,14 +197,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -233,14 +233,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -269,14 +269,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -306,21 +306,60 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_different_tz(self): + import pytz + + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '17:30:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunset', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + # Before + now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + # After + now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() From f8b2570cb3a8df1349f60ffa6393f65aa97d9f82 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 02:32:09 -0800 Subject: [PATCH 143/267] Group entities when reproducing a state --- homeassistant/helpers/state.py | 35 +++++---- tests/helpers/test_state.py | 129 +++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 tests/helpers/test_state.py diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 24a37c5b5ea..019e7ce6ce9 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,9 +1,5 @@ -""" -homeassistant.helpers.state -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Helpers that help with state related things. -""" +"""Helpers that help with state related things.""" +from collections import defaultdict import logging from homeassistant.core import State @@ -25,32 +21,36 @@ class TrackStates(object): that have changed since the start time to the return list when with-block is exited. """ + def __init__(self, hass): + """Initialize a TrackStates block.""" self.hass = hass self.states = [] def __enter__(self): + """Record time from which to track changes.""" self.now = dt_util.utcnow() return self.states def __exit__(self, exc_type, exc_value, traceback): + """Add changes states to changes list.""" self.states.extend(get_changed_since(self.hass.states.all(), self.now)) def get_changed_since(states, utc_point_in_time): - """ - Returns all states that have been changed since utc_point_in_time. - """ + """List of states that have been changed since utc_point_in_time.""" point_in_time = dt_util.strip_microseconds(utc_point_in_time) return [state for state in states if state.last_updated >= point_in_time] def reproduce_state(hass, states, blocking=False): - """ Takes in a state and will try to have the entity reproduce it. """ + """Reproduce given state.""" if isinstance(states, State): states = [states] + to_call = defaultdict(list) + for state in states: current_state = hass.states.get(state.entity_id) @@ -76,7 +76,16 @@ def reproduce_state(hass, states, blocking=False): state) continue - service_data = dict(state.attributes) - service_data[ATTR_ENTITY_ID] = state.entity_id + if state.domain == 'group': + service_domain = 'homeassistant' + else: + service_domain = state.domain - hass.services.call(state.domain, service, service_data, blocking) + # We group service calls for entities by service call + key = (service_domain, service, tuple(state.attributes.items())) + to_call[key].append(state.entity_id) + + for (service_domain, service, service_data), entity_ids in to_call.items(): + data = dict(service_data) + data[ATTR_ENTITY_ID] = entity_ids + hass.services.call(service_domain, service, data, blocking) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py new file mode 100644 index 00000000000..32924b1d6d5 --- /dev/null +++ b/tests/helpers/test_state.py @@ -0,0 +1,129 @@ +""" +tests.helpers.test_state +~~~~~~~~~~~~~~~~~~~~~~~~ + +Test state helpers. +""" +from datetime import timedelta +import unittest +from unittest.mock import patch + +import homeassistant.core as ha +import homeassistant.components as core_components +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.util import dt as dt_util +from homeassistant.helpers import state + +from tests.common import get_test_home_assistant, mock_service + + +class TestStateHelpers(unittest.TestCase): + """ + Tests the Home Assistant event helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = get_test_home_assistant() + core_components.setup(self.hass, {}) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_get_changed_since(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow', return_value=point1): + self.hass.states.set('light.test', 'on') + state1 = self.hass.states.get('light.test') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point2): + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point3): + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + [state2, state3], + state.get_changed_since([state1, state2, state3], point2)) + + def test_track_states(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = point2 + + with state.TrackStates(self.hass) as states: + mock_utcnow.return_value = point1 + self.hass.states.set('light.test', 'on') + + mock_utcnow.return_value = point2 + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + mock_utcnow.return_value = point3 + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + sorted([state2, state3], key=lambda state: state.entity_id), + sorted(states, key=lambda state: state.entity_id)) + + def test_reproduce_state_with_turn_on(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + state.reproduce_state(self.hass, ha.State('light.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test'], last_call.data.get('entity_id')) + + def test_reproduce_state_with_group(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('group.test', 'off', { + 'entity_id': ['light.test1', 'light.test2']}) + + state.reproduce_state(self.hass, ha.State('group.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + + def test_reproduce_state_group_states_with_same_domain_and_data(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test1', 'off') + self.hass.states.set('light.test2', 'off') + + state.reproduce_state(self.hass, [ + ha.State('light.test1', 'on', {'brightness': 95}), + ha.State('light.test2', 'on', {'brightness': 95})]) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + self.assertEqual(95, last_call.data.get('brightness')) From 82904c59ce4abaa7b47048f551611c8ceab067f9 Mon Sep 17 00:00:00 2001 From: xifle Date: Sun, 3 Jan 2016 17:12:11 +0100 Subject: [PATCH 144/267] Fixed code style --- .../components/device_tracker/owntracks.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index a1818e60901..c20b50e7e8c 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -49,8 +49,8 @@ def setup_scanner(hass, config, see): kwargs['battery'] = data['batt'] see(**kwargs) - - + + def owntracks_event_update(topic, payload, qos): """ MQTT event (geofences) received. """ @@ -67,20 +67,21 @@ def setup_scanner(hass, config, see): if not isinstance(data, dict) or data.get('_type') != 'transition': return - + # check if in "home" fence or other zone location = '' if data['event'] == 'enter': - + if data['desc'] == 'home': location = STATE_HOME else: location = data['desc'] - + elif data['event'] == 'leave': location = STATE_NOT_HOME else: - logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', data['event']) + logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', + data['event']) return parts = topic.split('/') @@ -94,10 +95,10 @@ def setup_scanner(hass, config, see): kwargs['gps_accuracy'] = data['acc'] see(**kwargs) - - + + use_events = config.get(CONF_TRANSITION_EVENTS) - + if use_events: mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) else: From d244d3b59958da19697548987574d411cd992acb Mon Sep 17 00:00:00 2001 From: xifle Date: Sun, 3 Jan 2016 17:42:49 +0100 Subject: [PATCH 145/267] Fixed flake8 style errors --- homeassistant/components/device_tracker/owntracks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index c20b50e7e8c..e81952eb770 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,6 +1,6 @@ """ homeassistant.components.device_tracker.owntracks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OwnTracks platform for the device tracker. For more details about this platform, please refer to the documentation at @@ -18,6 +18,7 @@ CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' EVENT_TOPIC = 'owntracks/+/+/event' + def setup_scanner(hass, config, see): """ Set up an OwnTracks tracker. """ @@ -50,7 +51,6 @@ def setup_scanner(hass, config, see): see(**kwargs) - def owntracks_event_update(topic, payload, qos): """ MQTT event (geofences) received. """ @@ -67,7 +67,6 @@ def setup_scanner(hass, config, see): if not isinstance(data, dict) or data.get('_type') != 'transition': return - # check if in "home" fence or other zone location = '' if data['event'] == 'enter': @@ -80,8 +79,9 @@ def setup_scanner(hass, config, see): elif data['event'] == 'leave': location = STATE_NOT_HOME else: - logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', - data['event']) + logging.getLogger(__name__).error( + 'Misformatted mqtt msgs, _type=transition, event=%s', + data['event']) return parts = topic.split('/') @@ -96,7 +96,6 @@ def setup_scanner(hass, config, see): see(**kwargs) - use_events = config.get(CONF_TRANSITION_EVENTS) if use_events: From 736183e6f5c09f08a5ae202832cb03a25a54198d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 11:27:30 -0800 Subject: [PATCH 146/267] Fix bug in reproduce_state with complex state attributes --- homeassistant/helpers/state.py | 7 +++++-- tests/helpers/test_state.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 019e7ce6ce9..c8f6f05661a 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,5 +1,6 @@ """Helpers that help with state related things.""" from collections import defaultdict +import json import logging from homeassistant.core import State @@ -82,10 +83,12 @@ def reproduce_state(hass, states, blocking=False): service_domain = state.domain # We group service calls for entities by service call - key = (service_domain, service, tuple(state.attributes.items())) + # json used to create a hashable version of dict with maybe lists in it + key = (service_domain, service, + json.dumps(state.attributes, sort_keys=True)) to_call[key].append(state.entity_id) for (service_domain, service, service_data), entity_ids in to_call.items(): - data = dict(service_data) + data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids hass.services.call(service_domain, service, data, blocking) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 32924b1d6d5..f4e28330f7a 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -91,6 +91,25 @@ class TestStateHelpers(unittest.TestCase): self.assertEqual(SERVICE_TURN_ON, last_call.service) self.assertEqual(['light.test'], last_call.data.get('entity_id')) + def test_reproduce_state_with_complex_service_data(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + complex_data = ['hello', {'11': '22'}] + + state.reproduce_state(self.hass, ha.State('light.test', 'on', { + 'complex': complex_data + })) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(complex_data, last_call.data.get('complex')) + def test_reproduce_state_with_group(self): light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) From 08aabd18add15ec8a12477b2b80d0ebe4de671d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 11:44:26 -0800 Subject: [PATCH 147/267] New version frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 26 +++++++++---------- .../www_static/home-assistant-polymer | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 64845a350ca..2ded702dc6b 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "be08c5a3ce12040bbdba2db83cb1a568" +VERSION = "72a8220d0db0f7f3702228cd556b8c40" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 8df0a4724a0..edc9635dbf4 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2134,7 +2134,7 @@ case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return }
\ No newline at end of file +o["default"])({SELECT_ENTITY:null,LOG_OUT:null})},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({moreInfoEntityId:u["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.getters=e.actions=void 0,e.register=o;var a=n(155),u=i(a),s=n(153),c=r(s),l=n(154),f=r(l);e.actions=c,e.getters=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(u["default"].SHOW_SIDEBAR,{show:e})}function o(t,e){t.dispatch(u["default"].NAVIGATE,{pane:e})}Object.defineProperty(e,"__esModule",{value:!0}),e.showSidebar=i,e.navigate=o;var a=n(26),u=r(a)},function(t,e){"use strict";function n(t){return[r,function(e){return e===t}]}Object.defineProperty(e,"__esModule",{value:!0}),e.isActivePane=n;var r=e.activePane=["selectedNavigationPanel"];e.showSidebar=["showSidebar"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({selectedNavigationPanel:u["default"],showSidebar:c["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.urlSync=e.getters=e.actions=void 0,e.register=o;var a=n(156),u=i(a),s=n(157),c=i(s),l=n(48),f=r(l),d=n(49),h=r(d),p=n(158),_=r(p);e.actions=f,e.getters=h,e.urlSync=_},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(6),o=r(i);e["default"]=(0,o["default"])({NOTIFICATION_CREATED:null})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(6),o=r(i);e["default"]=(0,o["default"])({API_FETCH_SUCCESS:null,API_FETCH_START:null,API_FETCH_FAIL:null,API_SAVE_SUCCESS:null,API_SAVE_START:null,API_SAVE_FAIL:null,API_DELETE_SUCCESS:null,API_DELETE_START:null,API_DELETE_FAIL:null,LOG_OUT:null})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){return[h(t),function(t){return!!t&&t.services.has(e)}]}function o(t){return[u.getters.byId(t),d,f["default"]]}Object.defineProperty(e,"__esModule",{value:!0}),e.byDomain=e.entityMap=e.hasData=void 0,e.hasService=i,e.canToggleEntity=o;var a=n(10),u=n(9),s=n(54),c=r(s),l=n(168),f=r(l),d=(e.hasData=(0,a.createHasDataGetter)(c["default"]),e.entityMap=(0,a.createEntityMapGetter)(c["default"])),h=e.byDomain=(0,a.createByIdGetter)(c["default"])},function(t,e,n){"use strict";function r(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function o(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var a=function(){function t(t,e){for(var n=0;n6e4}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n},function(t,e,n){function r(t,e,n){function r(){y&&clearTimeout(y),h&&clearTimeout(h),g=0,h=y=m=void 0}function s(e,n){n&&clearTimeout(n),h=y=m=void 0,e&&(g=o(),p=t.apply(v,d),y||h||(d=v=void 0))}function c(){var t=e-(o()-_);0>=t||t>e?s(m,h):y=setTimeout(c,t)}function l(){s(S,y)}function f(){if(d=arguments,_=o(),v=this,m=S&&(y||!w),b===!1)var n=w&&!y;else{h||w||(g=_);var r=b-(_-g),i=0>=r||r>b;i?(h&&(h=clearTimeout(h)),g=_,p=t.apply(v,d)):h||(h=setTimeout(l,r))}return i&&y?y=clearTimeout(y):y||e===b||(y=setTimeout(c,e)),n&&(i=!0,p=t.apply(v,d)),!i||y||h||(d=v=void 0),p}var d,h,p,_,v,y,m,g=0,b=!1,S=!0;if("function"!=typeof t)throw new TypeError(a);if(e=0>e?0:+e||0,n===!0){var w=!0;S=!1}else i(n)&&(w=!!n.leading,b="maxWait"in n&&u(+n.maxWait||0,e),S="trailing"in n?!!n.trailing:S);return f.cancel=r,f}var i=n(63),o=n(188),a="Expected a function",u=Math.max;t.exports=r},function(t,e,n){function r(t,e){var n=null==t?void 0:t[e];return i(n)?n:void 0}var i=n(191);t.exports=r},function(t,e){function n(t){return!!t&&"object"==typeof t}t.exports=n},function(t,e,n){function r(t){return i(t)&&u.call(t)==o}var i=n(63),o="[object Function]",a=Object.prototype,u=a.toString;t.exports=r},function(t,e){function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){(function(t){!function(e,n){t.exports=n()}(this,function(){"use strict";function e(){return Nn.apply(null,arguments)}function n(t){Nn=t}function r(t){return"[object Array]"===Object.prototype.toString.call(t)}function i(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function o(t,e){var n,r=[];for(n=0;n0)for(n in zn)r=zn[n],i=e[r],"undefined"!=typeof i&&(t[r]=i);return t}function p(t){h(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),xn===!1&&(xn=!0,e.updateOffset(this),xn=!1)}function _(t){return t instanceof p||null!=t&&null!=t._isAMomentObject}function v(t){return 0>t?Math.ceil(t):Math.floor(t)}function y(t){var e=+t,n=0;return 0!==e&&isFinite(e)&&(n=v(e)),n}function m(t,e,n){var r,i=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),a=0;for(r=0;i>r;r++)(n&&t[r]!==e[r]||!n&&y(t[r])!==y(e[r]))&&a++;return a+o}function g(){}function b(t){return t?t.toLowerCase().replace("_","-"):t}function S(t){for(var e,n,r,i,o=0;o0;){if(r=w(i.slice(0,e).join("-")))return r;if(n&&n.length>=e&&m(i,n,!0)>=e-1)break;e--}o++}return null}function w(e){var n=null;if(!Hn[e]&&"undefined"!=typeof t&&t&&t.exports)try{n=Rn._abbr,!function(){var t=new Error('Cannot find module "./locale"');throw t.code="MODULE_NOT_FOUND",t}(),O(n)}catch(r){}return Hn[e]}function O(t,e){var n;return t&&(n="undefined"==typeof e?T(t):M(t,e),n&&(Rn=n)),Rn._abbr}function M(t,e){return null!==e?(e.abbr=t,Hn[t]=Hn[t]||new g,Hn[t].set(e),O(t),Hn[t]):(delete Hn[t],null)}function T(t){var e;if(t&&t._locale&&t._locale._abbr&&(t=t._locale._abbr),!t)return Rn;if(!r(t)){if(e=w(t))return e;t=[t]}return S(t)}function I(t,e){var n=t.toLowerCase();Yn[n]=Yn[n+"s"]=Yn[e]=t}function E(t){return"string"==typeof t?Yn[t]||Yn[t.toLowerCase()]:void 0}function D(t){var e,n,r={};for(n in t)a(t,n)&&(e=E(n),e&&(r[e]=t[n]));return r}function C(t,n){return function(r){return null!=r?(A(this,t,r),e.updateOffset(this,n),this):j(this,t)}}function j(t,e){return t._d["get"+(t._isUTC?"UTC":"")+e]()}function A(t,e,n){return t._d["set"+(t._isUTC?"UTC":"")+e](n)}function P(t,e){var n;if("object"==typeof t)for(n in t)this.set(n,t[n]);else if(t=E(t),"function"==typeof this[t])return this[t](e);return this}function k(t,e,n){var r=""+Math.abs(t),i=e-r.length,o=t>=0;return(o?n?"+":"":"-")+Math.pow(10,Math.max(0,i)).toString().substr(1)+r}function L(t,e,n,r){var i=r;"string"==typeof r&&(i=function(){return this[r]()}),t&&(Fn[t]=i),e&&(Fn[e[0]]=function(){return k(i.apply(this,arguments),e[1],e[2])}),n&&(Fn[n]=function(){return this.localeData().ordinal(i.apply(this,arguments),t)})}function N(t){return t.match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function R(t){var e,n,r=t.match(Un);for(e=0,n=r.length;n>e;e++)Fn[r[e]]?r[e]=Fn[r[e]]:r[e]=N(r[e]);return function(i){var o="";for(e=0;n>e;e++)o+=r[e]instanceof Function?r[e].call(i,t):r[e];return o}}function z(t,e){return t.isValid()?(e=x(e,t.localeData()),Bn[e]=Bn[e]||R(e),Bn[e](t)):t.localeData().invalidDate()}function x(t,e){function n(t){return e.longDateFormat(t)||t}var r=5;for(Gn.lastIndex=0;r>=0&&Gn.test(t);)t=t.replace(Gn,n),Gn.lastIndex=0,r-=1;return t}function H(t){return"function"==typeof t&&"[object Function]"===Object.prototype.toString.call(t)}function Y(t,e,n){or[t]=H(e)?e:function(t){return t&&n?n:e}}function U(t,e){return a(or,t)?or[t](e._strict,e._locale):new RegExp(G(t))}function G(t){return t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,n,r,i){return e||n||r||i}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function B(t,e){var n,r=e;for("string"==typeof t&&(t=[t]),"number"==typeof e&&(r=function(t,n){n[e]=y(t)}),n=0;nr;r++){if(i=s([2e3,r]),n&&!this._longMonthsParse[r]&&(this._longMonthsParse[r]=new RegExp("^"+this.months(i,"").replace(".","")+"$","i"),this._shortMonthsParse[r]=new RegExp("^"+this.monthsShort(i,"").replace(".","")+"$","i")),n||this._monthsParse[r]||(o="^"+this.months(i,"")+"|^"+this.monthsShort(i,""),this._monthsParse[r]=new RegExp(o.replace(".",""),"i")),n&&"MMMM"===e&&this._longMonthsParse[r].test(t))return r;if(n&&"MMM"===e&&this._shortMonthsParse[r].test(t))return r;if(!n&&this._monthsParse[r].test(t))return r}}function $(t,e){var n;return"string"==typeof e&&(e=t.localeData().monthsParse(e),"number"!=typeof e)?t:(n=Math.min(t.date(),q(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,n),t)}function Z(t){return null!=t?($(this,t),e.updateOffset(this,!0),this):j(this,"Month")}function X(){return q(this.year(),this.month())}function Q(t){var e,n=t._a;return n&&-2===l(t).overflow&&(e=n[sr]<0||n[sr]>11?sr:n[cr]<1||n[cr]>q(n[ur],n[sr])?cr:n[lr]<0||n[lr]>24||24===n[lr]&&(0!==n[fr]||0!==n[dr]||0!==n[hr])?lr:n[fr]<0||n[fr]>59?fr:n[dr]<0||n[dr]>59?dr:n[hr]<0||n[hr]>999?hr:-1,l(t)._overflowDayOfYear&&(ur>e||e>cr)&&(e=cr),l(t).overflow=e),t}function tt(t){e.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function et(t,e){var n=!0;return u(function(){return n&&(tt(t+"\n"+(new Error).stack),n=!1),e.apply(this,arguments)},e)}function nt(t,e){vr[t]||(tt(e),vr[t]=!0)}function rt(t){var e,n,r=t._i,i=yr.exec(r);if(i){for(l(t).iso=!0,e=0,n=mr.length;n>e;e++)if(mr[e][1].exec(r)){t._f=mr[e][0];break}for(e=0,n=gr.length;n>e;e++)if(gr[e][1].exec(r)){t._f+=(i[6]||" ")+gr[e][0];break}r.match(nr)&&(t._f+="Z"),wt(t)}else t._isValid=!1}function it(t){var n=br.exec(t._i);return null!==n?void(t._d=new Date(+n[1])):(rt(t),void(t._isValid===!1&&(delete t._isValid,e.createFromInputFallback(t))))}function ot(t,e,n,r,i,o,a){var u=new Date(t,e,n,r,i,o,a);return 1970>t&&u.setFullYear(t),u}function at(t){var e=new Date(Date.UTC.apply(null,arguments));return 1970>t&&e.setUTCFullYear(t),e}function ut(t){return st(t)?366:365}function st(t){return t%4===0&&t%100!==0||t%400===0}function ct(){return st(this.year())}function lt(t,e,n){var r,i=n-e,o=n-t.day();return o>i&&(o-=7),i-7>o&&(o+=7),r=jt(t).add(o,"d"),{week:Math.ceil(r.dayOfYear()/7),year:r.year()}}function ft(t){return lt(t,this._week.dow,this._week.doy).week}function dt(){return this._week.dow}function ht(){return this._week.doy}function pt(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")}function _t(t){var e=lt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")}function vt(t,e,n,r,i){var o,a=6+i-r,u=at(t,0,1+a),s=u.getUTCDay();return i>s&&(s+=7),n=null!=n?1*n:i,o=1+a+7*(e-1)-s+n,{year:o>0?t:t-1,dayOfYear:o>0?o:ut(t-1)+o}}function yt(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")}function mt(t,e,n){return null!=t?t:null!=e?e:n}function gt(t){var e=new Date;return t._useUTC?[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()]:[e.getFullYear(),e.getMonth(),e.getDate()]}function bt(t){var e,n,r,i,o=[];if(!t._d){for(r=gt(t),t._w&&null==t._a[cr]&&null==t._a[sr]&&St(t),t._dayOfYear&&(i=mt(t._a[ur],r[ur]),t._dayOfYear>ut(i)&&(l(t)._overflowDayOfYear=!0),n=at(i,0,t._dayOfYear),t._a[sr]=n.getUTCMonth(),t._a[cr]=n.getUTCDate()),e=0;3>e&&null==t._a[e];++e)t._a[e]=o[e]=r[e];for(;7>e;e++)t._a[e]=o[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[lr]&&0===t._a[fr]&&0===t._a[dr]&&0===t._a[hr]&&(t._nextDay=!0,t._a[lr]=0),t._d=(t._useUTC?at:ot).apply(null,o),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[lr]=24)}}function St(t){var e,n,r,i,o,a,u;e=t._w,null!=e.GG||null!=e.W||null!=e.E?(o=1,a=4,n=mt(e.GG,t._a[ur],lt(jt(),1,4).year),r=mt(e.W,1),i=mt(e.E,1)):(o=t._locale._week.dow,a=t._locale._week.doy,n=mt(e.gg,t._a[ur],lt(jt(),o,a).year),r=mt(e.w,1),null!=e.d?(i=e.d,o>i&&++r):i=null!=e.e?e.e+o:o),u=vt(n,r,i,a,o),t._a[ur]=u.year,t._dayOfYear=u.dayOfYear}function wt(t){if(t._f===e.ISO_8601)return void rt(t);t._a=[],l(t).empty=!0;var n,r,i,o,a,u=""+t._i,s=u.length,c=0;for(i=x(t._f,t._locale).match(Un)||[],n=0;n0&&l(t).unusedInput.push(a),u=u.slice(u.indexOf(r)+r.length),c+=r.length),Fn[o]?(r?l(t).empty=!1:l(t).unusedTokens.push(o),V(o,r,t)):t._strict&&!r&&l(t).unusedTokens.push(o);l(t).charsLeftOver=s-c,u.length>0&&l(t).unusedInput.push(u),l(t).bigHour===!0&&t._a[lr]<=12&&t._a[lr]>0&&(l(t).bigHour=void 0),t._a[lr]=Ot(t._locale,t._a[lr],t._meridiem),bt(t),Q(t)}function Ot(t,e,n){var r;return null==n?e:null!=t.meridiemHour?t.meridiemHour(e,n):null!=t.isPM?(r=t.isPM(n),r&&12>e&&(e+=12),r||12!==e||(e=0),e):e}function Mt(t){var e,n,r,i,o;if(0===t._f.length)return l(t).invalidFormat=!0,void(t._d=new Date(NaN));for(i=0;io)&&(r=o,n=e));u(t,n||e)}function Tt(t){if(!t._d){var e=D(t._i);t._a=[e.year,e.month,e.day||e.date,e.hour,e.minute,e.second,e.millisecond],bt(t)}}function It(t){var e=new p(Q(Et(t)));return e._nextDay&&(e.add(1,"d"),e._nextDay=void 0),e}function Et(t){var e=t._i,n=t._f;return t._locale=t._locale||T(t._l),null===e||void 0===n&&""===e?d({nullInput:!0}):("string"==typeof e&&(t._i=e=t._locale.preparse(e)),_(e)?new p(Q(e)):(r(n)?Mt(t):n?wt(t):i(e)?t._d=e:Dt(t),t))}function Dt(t){var n=t._i;void 0===n?t._d=new Date:i(n)?t._d=new Date(+n):"string"==typeof n?it(t):r(n)?(t._a=o(n.slice(0),function(t){return parseInt(t,10)}),bt(t)):"object"==typeof n?Tt(t):"number"==typeof n?t._d=new Date(n):e.createFromInputFallback(t)}function Ct(t,e,n,r,i){var o={};return"boolean"==typeof n&&(r=n,n=void 0),o._isAMomentObject=!0,o._useUTC=o._isUTC=i,o._l=n,o._i=t,o._f=e,o._strict=r,It(o)}function jt(t,e,n,r){return Ct(t,e,n,r,!1)}function At(t,e){var n,i;if(1===e.length&&r(e[0])&&(e=e[0]),!e.length)return jt();for(n=e[0],i=1;it&&(t=-t,n="-"),n+k(~~(t/60),2)+e+k(~~t%60,2)})}function zt(t){var e=(t||"").match(nr)||[],n=e[e.length-1]||[],r=(n+"").match(Tr)||["-",0,0],i=+(60*r[1])+y(r[2]);return"+"===r[0]?i:-i}function xt(t,n){var r,o;return n._isUTC?(r=n.clone(),o=(_(t)||i(t)?+t:+jt(t))-+r,r._d.setTime(+r._d+o),e.updateOffset(r,!1),r):jt(t).local()}function Ht(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Yt(t,n){var r,i=this._offset||0;return null!=t?("string"==typeof t&&(t=zt(t)),Math.abs(t)<16&&(t=60*t),!this._isUTC&&n&&(r=Ht(this)),this._offset=t,this._isUTC=!0,null!=r&&this.add(r,"m"),i!==t&&(!n||this._changeInProgress?ne(this,Zt(t-i,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,e.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?i:Ht(this)}function Ut(t,e){return null!=t?("string"!=typeof t&&(t=-t),this.utcOffset(t,e),this):-this.utcOffset()}function Gt(t){return this.utcOffset(0,t)}function Bt(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t&&this.subtract(Ht(this),"m")),this}function Ft(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(zt(this._i)),this}function Vt(t){return t=t?jt(t).utcOffset():0,(this.utcOffset()-t)%60===0}function qt(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Wt(){if("undefined"!=typeof this._isDSTShifted)return this._isDSTShifted;var t={};if(h(t,this),t=Et(t),t._a){var e=t._isUTC?s(t._a):jt(t._a);this._isDSTShifted=this.isValid()&&m(t._a,e.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Kt(){return!this._isUTC}function Jt(){return this._isUTC}function $t(){return this._isUTC&&0===this._offset}function Zt(t,e){var n,r,i,o=t,u=null;return Nt(t)?o={ms:t._milliseconds,d:t._days,M:t._months}:"number"==typeof t?(o={},e?o[e]=t:o.milliseconds=t):(u=Ir.exec(t))?(n="-"===u[1]?-1:1,o={y:0,d:y(u[cr])*n,h:y(u[lr])*n,m:y(u[fr])*n,s:y(u[dr])*n,ms:y(u[hr])*n}):(u=Er.exec(t))?(n="-"===u[1]?-1:1,o={y:Xt(u[2],n),M:Xt(u[3],n),d:Xt(u[4],n),h:Xt(u[5],n),m:Xt(u[6],n),s:Xt(u[7],n),w:Xt(u[8],n)}):null==o?o={}:"object"==typeof o&&("from"in o||"to"in o)&&(i=te(jt(o.from),jt(o.to)),o={},o.ms=i.milliseconds,o.M=i.months),r=new Lt(o),Nt(t)&&a(t,"_locale")&&(r._locale=t._locale),r}function Xt(t,e){var n=t&&parseFloat(t.replace(",","."));return(isNaN(n)?0:n)*e}function Qt(t,e){var n={milliseconds:0,months:0};return n.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(n.months,"M").isAfter(e)&&--n.months,n.milliseconds=+e-+t.clone().add(n.months,"M"),n}function te(t,e){var n;return e=xt(e,t),t.isBefore(e)?n=Qt(t,e):(n=Qt(e,t),n.milliseconds=-n.milliseconds,n.months=-n.months),n}function ee(t,e){return function(n,r){var i,o;return null===r||isNaN(+r)||(nt(e,"moment()."+e+"(period, number) is deprecated. Please use moment()."+e+"(number, period)."),o=n,n=r,r=o),n="string"==typeof n?+n:n,i=Zt(n,r),ne(this,i,t),this}}function ne(t,n,r,i){var o=n._milliseconds,a=n._days,u=n._months;i=null==i?!0:i,o&&t._d.setTime(+t._d+o*r),a&&A(t,"Date",j(t,"Date")+a*r),u&&$(t,j(t,"Month")+u*r),i&&e.updateOffset(t,a||u)}function re(t,e){var n=t||jt(),r=xt(n,this).startOf("day"),i=this.diff(r,"days",!0),o=-6>i?"sameElse":-1>i?"lastWeek":0>i?"lastDay":1>i?"sameDay":2>i?"nextDay":7>i?"nextWeek":"sameElse";return this.format(e&&e[o]||this.localeData().calendar(o,this,jt(n)))}function ie(){return new p(this)}function oe(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=_(t)?t:jt(t),+this>+t):(n=_(t)?+t:+jt(t),n<+this.clone().startOf(e))}function ae(t,e){var n;return e=E("undefined"!=typeof e?e:"millisecond"),"millisecond"===e?(t=_(t)?t:jt(t),+t>+this):(n=_(t)?+t:+jt(t),+this.clone().endOf(e)e-o?(n=t.clone().add(i-1,"months"),r=(e-o)/(o-n)):(n=t.clone().add(i+1,"months"),r=(e-o)/(n-o)),-(i+r)}function fe(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function de(){var t=this.clone().utc();return 0e;e++)if(this._weekdaysParse[e]||(n=jt([2e3,1]).day(e),r="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(r.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e}function Ge(t){var e=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=t?(t=ze(t,this.localeData()),this.add(t-e,"d")):e}function Be(t){var e=(this.day()+7-this.localeData()._week.dow)%7;return null==t?e:this.add(t-e,"d")}function Fe(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)}function Ve(t,e){L(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function qe(t,e){return e._meridiemParse}function We(t){return"p"===(t+"").toLowerCase().charAt(0)}function Ke(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"}function Je(t,e){e[hr]=y(1e3*("0."+t))}function $e(){return this._isUTC?"UTC":""}function Ze(){return this._isUTC?"Coordinated Universal Time":""}function Xe(t){return jt(1e3*t)}function Qe(){return jt.apply(null,arguments).parseZone()}function tn(t,e,n){var r=this._calendar[t];return"function"==typeof r?r.call(e,n):r}function en(t){var e=this._longDateFormat[t],n=this._longDateFormat[t.toUpperCase()];return e||!n?e:(this._longDateFormat[t]=n.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])}function nn(){return this._invalidDate}function rn(t){return this._ordinal.replace("%d",t)}function on(t){return t}function an(t,e,n,r){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,r):i.replace(/%d/i,t)}function un(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)}function sn(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function cn(t,e,n,r){var i=T(),o=s().set(r,e);return i[n](o,t)}function ln(t,e,n,r,i){if("number"==typeof t&&(e=t,t=void 0),t=t||"",null!=e)return cn(t,e,n,i);var o,a=[];for(o=0;r>o;o++)a[o]=cn(t,o,n,i);return a}function fn(t,e){return ln(t,e,"months",12,"month")}function dn(t,e){return ln(t,e,"monthsShort",12,"month")}function hn(t,e){return ln(t,e,"weekdays",7,"day")}function pn(t,e){return ln(t,e,"weekdaysShort",7,"day")}function _n(t,e){return ln(t,e,"weekdaysMin",7,"day")}function vn(){var t=this._data;return this._milliseconds=$r(this._milliseconds),this._days=$r(this._days),this._months=$r(this._months),t.milliseconds=$r(t.milliseconds),t.seconds=$r(t.seconds),t.minutes=$r(t.minutes),t.hours=$r(t.hours),t.months=$r(t.months),t.years=$r(t.years),this}function yn(t,e,n,r){var i=Zt(e,n);return t._milliseconds+=r*i._milliseconds,t._days+=r*i._days,t._months+=r*i._months,t._bubble()}function mn(t,e){return yn(this,t,e,1)}function gn(t,e){return yn(this,t,e,-1)}function bn(t){return 0>t?Math.floor(t):Math.ceil(t)}function Sn(){var t,e,n,r,i,o=this._milliseconds,a=this._days,u=this._months,s=this._data;return o>=0&&a>=0&&u>=0||0>=o&&0>=a&&0>=u||(o+=864e5*bn(On(u)+a),a=0,u=0),s.milliseconds=o%1e3,t=v(o/1e3),s.seconds=t%60,e=v(t/60),s.minutes=e%60,n=v(e/60),s.hours=n%24,a+=v(n/24),i=v(wn(a)),u+=i,a-=bn(On(i)),r=v(u/12),u%=12,s.days=a,s.months=u,s.years=r,this}function wn(t){return 4800*t/146097}function On(t){return 146097*t/4800}function Mn(t){var e,n,r=this._milliseconds;if(t=E(t),"month"===t||"year"===t)return e=this._days+r/864e5,n=this._months+wn(e),"month"===t?n:n/12;switch(e=this._days+Math.round(On(this._months)),t){case"week":return e/7+r/6048e5;case"day":return e+r/864e5;case"hour":return 24*e+r/36e5;case"minute":return 1440*e+r/6e4;case"second":return 86400*e+r/1e3;case"millisecond":return Math.floor(864e5*e)+r;default:throw new Error("Unknown unit "+t)}}function Tn(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*y(this._months/12)}function In(t){return function(){return this.as(t)}}function En(t){return t=E(t),this[t+"s"]()}function Dn(t){return function(){return this._data[t]}}function Cn(){return v(this.days()/7)}function jn(t,e,n,r,i){return i.relativeTime(e||1,!!n,t,r)}function An(t,e,n){var r=Zt(t).abs(),i=di(r.as("s")),o=di(r.as("m")),a=di(r.as("h")),u=di(r.as("d")),s=di(r.as("M")),c=di(r.as("y")),l=i0,l[4]=n,jn.apply(null,l)}function Pn(t,e){return void 0===hi[t]?!1:void 0===e?hi[t]:(hi[t]=e,!0)}function kn(t){var e=this.localeData(),n=An(this,!t,e);return t&&(n=e.pastFuture(+this,n)),e.postformat(n)}function Ln(){var t,e,n,r=pi(this._milliseconds)/1e3,i=pi(this._days),o=pi(this._months);t=v(r/60),e=v(t/60),r%=60,t%=60,n=v(o/12),o%=12;var a=n,u=o,s=i,c=e,l=t,f=r,d=this.asSeconds();return d?(0>d?"-":"")+"P"+(a?a+"Y":"")+(u?u+"M":"")+(s?s+"D":"")+(c||l||f?"T":"")+(c?c+"H":"")+(l?l+"M":"")+(f?f+"S":""):"P0D"}var Nn,Rn,zn=e.momentProperties=[],xn=!1,Hn={},Yn={},Un=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,Gn=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Bn={},Fn={},Vn=/\d/,qn=/\d\d/,Wn=/\d{3}/,Kn=/\d{4}/,Jn=/[+-]?\d{6}/,$n=/\d\d?/,Zn=/\d{1,3}/,Xn=/\d{1,4}/,Qn=/[+-]?\d{1,6}/,tr=/\d+/,er=/[+-]?\d+/,nr=/Z|[+-]\d\d:?\d\d/gi,rr=/[+-]?\d+(\.\d{1,3})?/,ir=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,or={},ar={},ur=0,sr=1,cr=2,lr=3,fr=4,dr=5,hr=6;L("M",["MM",2],"Mo",function(){return this.month()+1}),L("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),L("MMMM",0,0,function(t){return this.localeData().months(this,t)}),I("month","M"),Y("M",$n),Y("MM",$n,qn),Y("MMM",ir),Y("MMMM",ir),B(["M","MM"],function(t,e){e[sr]=y(t)-1}),B(["MMM","MMMM"],function(t,e,n,r){var i=n._locale.monthsParse(t,r,n._strict);null!=i?e[sr]=i:l(n).invalidMonth=t});var pr="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),_r="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),vr={}; +e.suppressDeprecationWarnings=!1;var yr=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mr=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],gr=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],br=/^\/?Date\((\-?\d+)/i;e.createFromInputFallback=et("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),L(0,["YY",2],0,function(){return this.year()%100}),L(0,["YYYY",4],0,"year"),L(0,["YYYYY",5],0,"year"),L(0,["YYYYYY",6,!0],0,"year"),I("year","y"),Y("Y",er),Y("YY",$n,qn),Y("YYYY",Xn,Kn),Y("YYYYY",Qn,Jn),Y("YYYYYY",Qn,Jn),B(["YYYYY","YYYYYY"],ur),B("YYYY",function(t,n){n[ur]=2===t.length?e.parseTwoDigitYear(t):y(t)}),B("YY",function(t,n){n[ur]=e.parseTwoDigitYear(t)}),e.parseTwoDigitYear=function(t){return y(t)+(y(t)>68?1900:2e3)};var Sr=C("FullYear",!1);L("w",["ww",2],"wo","week"),L("W",["WW",2],"Wo","isoWeek"),I("week","w"),I("isoWeek","W"),Y("w",$n),Y("ww",$n,qn),Y("W",$n),Y("WW",$n,qn),F(["w","ww","W","WW"],function(t,e,n,r){e[r.substr(0,1)]=y(t)});var wr={dow:0,doy:6};L("DDD",["DDDD",3],"DDDo","dayOfYear"),I("dayOfYear","DDD"),Y("DDD",Zn),Y("DDDD",Wn),B(["DDD","DDDD"],function(t,e,n){n._dayOfYear=y(t)}),e.ISO_8601=function(){};var Or=et("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var t=jt.apply(null,arguments);return this>t?this:t}),Mr=et("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var t=jt.apply(null,arguments);return t>this?this:t});Rt("Z",":"),Rt("ZZ",""),Y("Z",nr),Y("ZZ",nr),B(["Z","ZZ"],function(t,e,n){n._useUTC=!0,n._tzm=zt(t)});var Tr=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var Ir=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,Er=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Zt.fn=Lt.prototype;var Dr=ee(1,"add"),Cr=ee(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var jr=et("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});L(0,["gg",2],0,function(){return this.weekYear()%100}),L(0,["GG",2],0,function(){return this.isoWeekYear()%100}),je("gggg","weekYear"),je("ggggg","weekYear"),je("GGGG","isoWeekYear"),je("GGGGG","isoWeekYear"),I("weekYear","gg"),I("isoWeekYear","GG"),Y("G",er),Y("g",er),Y("GG",$n,qn),Y("gg",$n,qn),Y("GGGG",Xn,Kn),Y("gggg",Xn,Kn),Y("GGGGG",Qn,Jn),Y("ggggg",Qn,Jn),F(["gggg","ggggg","GGGG","GGGGG"],function(t,e,n,r){e[r.substr(0,2)]=y(t)}),F(["gg","GG"],function(t,n,r,i){n[i]=e.parseTwoDigitYear(t)}),L("Q",0,0,"quarter"),I("quarter","Q"),Y("Q",Vn),B("Q",function(t,e){e[sr]=3*(y(t)-1)}),L("D",["DD",2],"Do","date"),I("date","D"),Y("D",$n),Y("DD",$n,qn),Y("Do",function(t,e){return t?e._ordinalParse:e._ordinalParseLenient}),B(["D","DD"],cr),B("Do",function(t,e){e[cr]=y(t.match($n)[0],10)});var Ar=C("Date",!0);L("d",0,"do","day"),L("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),L("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),L("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),L("e",0,0,"weekday"),L("E",0,0,"isoWeekday"),I("day","d"),I("weekday","e"),I("isoWeekday","E"),Y("d",$n),Y("e",$n),Y("E",$n),Y("dd",ir),Y("ddd",ir),Y("dddd",ir),F(["dd","ddd","dddd"],function(t,e,n){var r=n._locale.weekdaysParse(t);null!=r?e.d=r:l(n).invalidWeekday=t}),F(["d","e","E"],function(t,e,n,r){e[r]=y(t)});var Pr="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),kr="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),Lr="Su_Mo_Tu_We_Th_Fr_Sa".split("_");L("H",["HH",2],0,"hour"),L("h",["hh",2],0,function(){return this.hours()%12||12}),Ve("a",!0),Ve("A",!1),I("hour","h"),Y("a",qe),Y("A",qe),Y("H",$n),Y("h",$n),Y("HH",$n,qn),Y("hh",$n,qn),B(["H","HH"],lr),B(["a","A"],function(t,e,n){n._isPm=n._locale.isPM(t),n._meridiem=t}),B(["h","hh"],function(t,e,n){e[lr]=y(t),l(n).bigHour=!0});var Nr=/[ap]\.?m?\.?/i,Rr=C("Hours",!0);L("m",["mm",2],0,"minute"),I("minute","m"),Y("m",$n),Y("mm",$n,qn),B(["m","mm"],fr);var zr=C("Minutes",!1);L("s",["ss",2],0,"second"),I("second","s"),Y("s",$n),Y("ss",$n,qn),B(["s","ss"],dr);var xr=C("Seconds",!1);L("S",0,0,function(){return~~(this.millisecond()/100)}),L(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),L(0,["SSS",3],0,"millisecond"),L(0,["SSSS",4],0,function(){return 10*this.millisecond()}),L(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),L(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),L(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),L(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),L(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),I("millisecond","ms"),Y("S",Zn,Vn),Y("SS",Zn,qn),Y("SSS",Zn,Wn);var Hr;for(Hr="SSSS";Hr.length<=9;Hr+="S")Y(Hr,tr);for(Hr="S";Hr.length<=9;Hr+="S")B(Hr,Je);var Yr=C("Milliseconds",!1);L("z",0,0,"zoneAbbr"),L("zz",0,0,"zoneName");var Ur=p.prototype;Ur.add=Dr,Ur.calendar=re,Ur.clone=ie,Ur.diff=ce,Ur.endOf=Se,Ur.format=he,Ur.from=pe,Ur.fromNow=_e,Ur.to=ve,Ur.toNow=ye,Ur.get=P,Ur.invalidAt=Ce,Ur.isAfter=oe,Ur.isBefore=ae,Ur.isBetween=ue,Ur.isSame=se,Ur.isValid=Ee,Ur.lang=jr,Ur.locale=me,Ur.localeData=ge,Ur.max=Mr,Ur.min=Or,Ur.parsingFlags=De,Ur.set=P,Ur.startOf=be,Ur.subtract=Cr,Ur.toArray=Te,Ur.toObject=Ie,Ur.toDate=Me,Ur.toISOString=de,Ur.toJSON=de,Ur.toString=fe,Ur.unix=Oe,Ur.valueOf=we,Ur.year=Sr,Ur.isLeapYear=ct,Ur.weekYear=Pe,Ur.isoWeekYear=ke,Ur.quarter=Ur.quarters=Re,Ur.month=Z,Ur.daysInMonth=X,Ur.week=Ur.weeks=pt,Ur.isoWeek=Ur.isoWeeks=_t,Ur.weeksInYear=Ne,Ur.isoWeeksInYear=Le,Ur.date=Ar,Ur.day=Ur.days=Ge,Ur.weekday=Be,Ur.isoWeekday=Fe,Ur.dayOfYear=yt,Ur.hour=Ur.hours=Rr,Ur.minute=Ur.minutes=zr,Ur.second=Ur.seconds=xr,Ur.millisecond=Ur.milliseconds=Yr,Ur.utcOffset=Yt,Ur.utc=Gt,Ur.local=Bt,Ur.parseZone=Ft,Ur.hasAlignedHourOffset=Vt,Ur.isDST=qt,Ur.isDSTShifted=Wt,Ur.isLocal=Kt,Ur.isUtcOffset=Jt,Ur.isUtc=$t,Ur.isUTC=$t,Ur.zoneAbbr=$e,Ur.zoneName=Ze,Ur.dates=et("dates accessor is deprecated. Use date instead.",Ar),Ur.months=et("months accessor is deprecated. Use month instead",Z),Ur.years=et("years accessor is deprecated. Use year instead",Sr),Ur.zone=et("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ut);var Gr=Ur,Br={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Fr={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},Vr="Invalid date",qr="%d",Wr=/\d{1,2}/,Kr={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Jr=g.prototype;Jr._calendar=Br,Jr.calendar=tn,Jr._longDateFormat=Fr,Jr.longDateFormat=en,Jr._invalidDate=Vr,Jr.invalidDate=nn,Jr._ordinal=qr,Jr.ordinal=rn,Jr._ordinalParse=Wr,Jr.preparse=on,Jr.postformat=on,Jr._relativeTime=Kr,Jr.relativeTime=an,Jr.pastFuture=un,Jr.set=sn,Jr.months=W,Jr._months=pr,Jr.monthsShort=K,Jr._monthsShort=_r,Jr.monthsParse=J,Jr.week=ft,Jr._week=wr,Jr.firstDayOfYear=ht,Jr.firstDayOfWeek=dt,Jr.weekdays=xe,Jr._weekdays=Pr,Jr.weekdaysMin=Ye,Jr._weekdaysMin=Lr,Jr.weekdaysShort=He,Jr._weekdaysShort=kr,Jr.weekdaysParse=Ue,Jr.isPM=We,Jr._meridiemParse=Nr,Jr.meridiem=Ke,O("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10,n=1===y(t%100/10)?"th":1===e?"st":2===e?"nd":3===e?"rd":"th";return t+n}}),e.lang=et("moment.lang is deprecated. Use moment.locale instead.",O),e.langData=et("moment.langData is deprecated. Use moment.localeData instead.",T);var $r=Math.abs,Zr=In("ms"),Xr=In("s"),Qr=In("m"),ti=In("h"),ei=In("d"),ni=In("w"),ri=In("M"),ii=In("y"),oi=Dn("milliseconds"),ai=Dn("seconds"),ui=Dn("minutes"),si=Dn("hours"),ci=Dn("days"),li=Dn("months"),fi=Dn("years"),di=Math.round,hi={s:45,m:45,h:22,d:26,M:11},pi=Math.abs,_i=Lt.prototype;_i.abs=vn,_i.add=mn,_i.subtract=gn,_i.as=Mn,_i.asMilliseconds=Zr,_i.asSeconds=Xr,_i.asMinutes=Qr,_i.asHours=ti,_i.asDays=ei,_i.asWeeks=ni,_i.asMonths=ri,_i.asYears=ii,_i.valueOf=Tn,_i._bubble=Sn,_i.get=En,_i.milliseconds=oi,_i.seconds=ai,_i.minutes=ui,_i.hours=si,_i.days=ci,_i.weeks=Cn,_i.months=li,_i.years=fi,_i.humanize=kn,_i.toISOString=Ln,_i.toString=Ln,_i.toJSON=Ln,_i.locale=me,_i.localeData=ge,_i.toIsoString=et("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Ln),_i.lang=jr,L("X",0,0,"unix"),L("x",0,0,"valueOf"),Y("x",er),Y("X",rr),B("X",function(t,e,n){n._d=new Date(1e3*parseFloat(t,10))}),B("x",function(t,e,n){n._d=new Date(y(t))}),e.version="2.10.6",n(jt),e.fn=Gr,e.min=Pt,e.max=kt,e.utc=s,e.unix=Xe,e.months=fn,e.isDate=i,e.locale=O,e.invalid=d,e.duration=Zt,e.isMoment=_,e.weekdays=hn,e.parseZone=Qe,e.localeData=T,e.isDuration=Nt,e.monthsShort=dn,e.weekdaysMin=_n,e.defineLocale=M,e.weekdaysShort=pn,e.normalizeUnits=E,e.relativeTimeThreshold=Pn;var vi=e;return vi})}).call(e,n(65)(t))},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(e,"__esModule",{value:!0});var a=n(162),u=n(182),s=i(u),c=n(184),l=i(c),f=n(186),d=i(f),h=n(15),p=r(h),_=n(24),v=r(_),y=n(9),m=r(y),g=n(44),b=r(g),S=n(142),w=r(S),O=n(25),M=r(O),T=n(147),I=r(T),E=n(47),D=r(E),C=n(50),j=r(C),A=n(27),P=r(A),k=n(13),L=r(k),N=n(28),R=r(N),z=n(30),x=r(z),H=n(179),Y=r(H),U=n(10),G=r(U),B=function F(){o(this,F);var t=(0,s["default"])();Object.defineProperties(this,{demo:{value:!1,enumerable:!0},localStoragePreferences:{value:a.localStoragePreferences,enumerable:!0},reactor:{value:t,enumerable:!0},util:{value:d["default"],enumerable:!0},startLocalStoragePreferencesSync:{value:a.localStoragePreferences.startSync.bind(a.localStoragePreferences,t)},startUrlSync:{value:j.urlSync.startSync.bind(null,t)},stopUrlSync:{value:j.urlSync.stopSync.bind(null,t)}}),(0,l["default"])(this,t,{auth:p,config:v,entity:m,entityHistory:b,errorLog:w,event:M,logbook:I,moreInfo:D,navigation:j,notification:P,service:L,stream:R,sync:x,voice:Y,restApi:G})};e["default"]=B},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(74),e["default"]=new o["default"]({is:"ha-badges-card",properties:{states:{type:Array}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(21),c=r(s);n(35),n(34),n(19);var l=u["default"].moreInfoActions;e["default"]=new o["default"]({is:"ha-domain-card",properties:{domain:{type:String},states:{type:Array},groupEntity:{type:Object}},computeDomainTitle:function(t){return t.replace(/_/g," ")},entityTapped:function(t){if(!t.target.classList.contains("paper-toggle-button")&&!t.target.classList.contains("paper-icon-button")){t.stopPropagation();var e=t.model.item.entityId;this.async(function(){return l.selectEntity(e)},1)}},showGroupToggle:function(t,e){return!t||!e||"on"!==t.state&&"off"!==t.state?!1:e.reduce(function(t,e){return t+(0,c["default"])(e.entityId)},0)>1}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(35),e["default"]=new o["default"]({is:"ha-introduction-card",properties:{showInstallInstruction:{type:Boolean,value:!1},showHideInstruction:{type:Boolean,value:!0}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(40),u=r(a);e["default"]=new o["default"]({is:"display-time",properties:{dateObj:{type:Object}},computeTime:function(t){return t?(0,u["default"])(t):""}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].entityGetters;e["default"]=new u["default"]({is:"entity-list",behaviors:[c["default"]],properties:{entities:{type:Array,bindNuclear:[l.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.entityId}).toArray()}]}},entitySelected:function(t){t.preventDefault(),this.fire("entity-selected",{entityId:t.model.entity.entityId})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a);n(17);var s=u["default"].reactor,c=u["default"].entityGetters,l=u["default"].moreInfoActions;e["default"]=new o["default"]({is:"ha-entity-marker",properties:{entityId:{type:String,value:""},state:{type:Object,computed:"computeState(entityId)"},icon:{type:Object,computed:"computeIcon(state)"},image:{type:Object,computed:"computeImage(state)"},value:{type:String,computed:"computeValue(state)"}},listeners:{click:"badgeTap"},badgeTap:function(t){var e=this;t.stopPropagation(),this.entityId&&this.async(function(){return l.selectEntity(e.entityId)},1)},computeState:function(t){return t&&s.evaluate(c.byId(t))},computeIcon:function(t){return!t&&"home"},computeImage:function(t){return t&&t.attributes.entity_picture},computeValue:function(t){return t&&t.entityDisplay.split(" ").map(function(t){return t.substr(0,1)}).join("")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(121),u=r(a);e["default"]=new o["default"]({is:"ha-state-icon",properties:{stateObj:{type:Object}},computeIcon:function(t){return(0,u["default"])(t)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(22),c=r(s),l=n(21),f=r(l);n(17);var d=u["default"].moreInfoActions,h=u["default"].serviceActions;e["default"]=new o["default"]({is:"ha-state-label-badge",properties:{state:{type:Object,observer:"stateChanged"}},listeners:{click:"badgeTap"},badgeTap:function(t){var e=this;return t.stopPropagation(),(0,f["default"])(this.state.entityId)?void("scene"===this.state.domain?h.callTurnOn(this.state.entityId):"off"===this.state.state?h.callTurnOn(this.state.entityId):h.callTurnOff(this.state.entityId)):void this.async(function(){return d.selectEntity(e.state.entityId)},1)},computeClasses:function(t){switch(t.domain){case"scene":return"green";case"binary_sensor":case"script":return"on"===t.state?"blue":"grey";case"updater":return"blue";default:return""}},computeValue:function(t){switch(t.domain){case"binary_sensor":case"device_tracker":case"updater":case"sun":case"scene":case"script":case"alarm_control_panel":return;case"sensor":return t.state;default:return t.state}},computeIcon:function(t){switch(t.domain){case"alarm_control_panel":return"pending"===t.state?"mdi:clock-fast":"armed_away"===t.state?"mdi:nature":"armed_home"===t.state?"mdi:home-variant":(0,c["default"])(t.domain,t.state);case"binary_sensor":case"device_tracker":case"scene":case"updater":case"script":return(0,c["default"])(t.domain,t.state);case"sun":return"above_horizon"===t.state?(0,c["default"])(t.domain):"mdi:brightness-3";default:return}},computeImage:function(t){return t.attributes.entity_picture},computeLabel:function(t){switch(t.domain){case"scene":case"script":return t.domain;case"device_tracker":return"not_home"===t.state?"Away":t.state;case"alarm_control_panel":return"pending"===t.state?"pend":"armed_away"===t.state||"armed_home"===t.state?"armed":"disarm";default:return t.attributes.unit_of_measurement}},computeDescription:function(t){return t.entityDisplay},stateChanged:function(){this.updateStyles()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(73),e["default"]=new o["default"]({is:"state-badge",properties:{stateObj:{type:Object,observer:"updateIconColor"}},updateIconColor:function(t){"light"===t.domain&&"on"===t.state&&t.attributes.rgb_color&&t.attributes.rgb_color.reduce(function(t,e){return t+e},0)<730?this.$.icon.style.color="rgb("+t.attributes.rgb_color.join(",")+")":this.$.icon.style.color=null}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].eventGetters;e["default"]=new u["default"]({is:"events-list",behaviors:[c["default"]],properties:{events:{type:Array,bindNuclear:[l.entityMap,function(t){return t.valueSeq().sortBy(function(t){return t.event}).toArray()}]}},eventSelected:function(t){t.preventDefault(),this.fire("event-selected",{eventType:t.model.event.event})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"ha-color-picker",properties:{color:{type:Object},width:{type:Number},height:{type:Number}},listeners:{mousedown:"onMouseDown",mouseup:"onMouseUp",touchstart:"onTouchStart",touchend:"onTouchEnd"},onMouseDown:function(t){this.onMouseMove(t),this.addEventListener("mousemove",this.onMouseMove)},onMouseUp:function(){this.removeEventListener("mousemove",this.onMouseMove)},onTouchStart:function(t){this.onTouchMove(t),this.addEventListener("touchmove",this.onTouchMove)},onTouchEnd:function(){this.removeEventListener("touchmove",this.onTouchMove)},onTouchMove:function(t){var e=this;this.mouseMoveIsThrottled&&(this.mouseMoveIsThrottled=!1,this.processColorSelect(t.touches[0]),this.async(function(){return e.mouseMoveIsThrottled=!0},100))},onMouseMove:function(t){var e=this;this.mouseMoveIsThrottled&&(this.mouseMoveIsThrottled=!1,this.processColorSelect(t),this.async(function(){return e.mouseMoveIsThrottled=!0},100))},processColorSelect:function(t){var e=this.canvas.getBoundingClientRect();t.clientX=e.left+e.width||t.clientY=e.top+e.height||this.onColorSelect(t.clientX-e.left,t.clientY-e.top)},onColorSelect:function(t,e){var n=this.context.getImageData(t,e,1,1).data;this.setColor({r:n[0],g:n[1],b:n[2]})},setColor:function(t){this.color=t,this.fire("colorselected",{rgb:this.color})},ready:function(){var t=this;this.setColor=this.setColor.bind(this),this.mouseMoveIsThrottled=!0,this.canvas=this.children[0],this.context=this.canvas.getContext("2d"),this.debounce("drawGradient",function(){var e=getComputedStyle(t),n=parseInt(e.width,10),r=parseInt(e.height,10);t.width=n,t.height=r;var i=t.context.createLinearGradient(0,0,n,0);i.addColorStop(0,"rgb(255,0,0)"),i.addColorStop(.16,"rgb(255,0,255)"),i.addColorStop(.32,"rgb(0,0,255)"),i.addColorStop(.48,"rgb(0,255,255)"),i.addColorStop(.64,"rgb(0,255,0)"),i.addColorStop(.8,"rgb(255,255,0)"),i.addColorStop(1,"rgb(255,0,0)"),t.context.fillStyle=i,t.context.fillRect(0,0,n,r);var o=t.context.createLinearGradient(0,0,0,r);o.addColorStop(0,"rgba(255,255,255,1)"),o.addColorStop(.5,"rgba(255,255,255,0)"),o.addColorStop(.5,"rgba(0,0,0,0)"),o.addColorStop(1,"rgba(0,0,0,1)"),t.context.fillStyle=o,t.context.fillRect(0,0,n,r)},100)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(17),e["default"]=new o["default"]({is:"ha-demo-badge"})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(82),e["default"]=new o["default"]({is:"ha-logbook",properties:{entries:{type:Object,value:[]}},noEntries:function(t){return!t.length}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(86);var l=o["default"].configGetters,f=o["default"].navigationGetters,d=o["default"].authActions,h=o["default"].navigationActions;e["default"]=new u["default"]({is:"ha-sidebar",behaviors:[c["default"]],properties:{menuShown:{type:Boolean},menuSelected:{type:String},selected:{type:String,bindNuclear:f.activePane,observer:"selectedChanged"},hasHistoryComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("history")},hasLogbookComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("logbook")}},selectedChanged:function(t){for(var e=this.querySelectorAll(".menu [data-panel]"),n=0;nd;d++)f._columns[d]=[];var h=0;return n&&a(),s.keySeq().sortBy(function(t){return i(t)}).forEach(function(t){if("a"===t)return void(f._demo=!0);var n=i(t);n>=0&&10>n?f._badges.push.apply(f._badges,r(s.get(t)).sortBy(o).toArray()):"group"===t?s.get(t).filter(function(t){return!t.attributes.auto}).sortBy(o).forEach(function(t){var n=l.expandGroup(t,e);n.forEach(function(t){return c[t.entityId]=!0}),u(t.entityDisplay,n.toArray(),t)}):u(t,r(s.get(t)).sortBy(o).toArray())}),f},computeShouldRenderColumn:function(t,e){return 0===t||e.length},computeShowIntroduction:function(t,e,n){return 0===t&&(e||n._demo)},computeShowHideInstruction:function(t,e){return t.size>0&&!0&&!e._demo},computeGroupEntityOfCard:function(t,e){return e in t&&t[e].groupEntity},computeStatesOfCard:function(t,e){return e in t&&t[e].entities}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(33),n(70),n(36);var s=o["default"].moreInfoActions;e["default"]=new u["default"]({is:"logbook-entry",entityClicked:function(t){t.preventDefault(),s.selectEntity(this.entryObj.entityId)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(33);var l=o["default"].serviceGetters;e["default"]=new u["default"]({is:"services-list",behaviors:[c["default"]],properties:{serviceDomains:{type:Array,bindNuclear:l.entityMap}},computeDomains:function(t){return t.valueSeq().map(function(t){return t.domain}).sort().toJS()},computeServices:function(t,e){return t.get(e).get("services").keySeq().toArray()},serviceClicked:function(t){t.preventDefault(),this.fire("service-selected",{domain:t.model.domain,service:t.model.service})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){var e=parseFloat(t);return!isNaN(e)&&isFinite(e)?e:null}Object.defineProperty(e,"__esModule",{value:!0});var o=n(199),a=r(o),u=n(1),s=r(u);e["default"]=new s["default"]({is:"state-history-chart-line",properties:{data:{type:Object,observer:"dataChanged"},unit:{type:String},isSingleDevice:{type:Boolean,value:!1},isAttached:{type:Boolean,value:!1,observer:"dataChanged"},chartEngine:{type:Object}},created:function(){this.style.display="block"},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){if(this.isAttached){this.chartEngine||(this.chartEngine=new window.google.visualization.LineChart(this));var t=this.unit,e=this.data;if(0!==e.length){var n={legend:{position:"top"},interpolateNulls:!0,titlePosition:"none",vAxes:{0:{title:t}},hAxis:{format:"H:mm"},chartArea:{left:"60",width:"95%"},explorer:{actions:["dragToZoom","rightClickToReset","dragToPan"],keepInBounds:!0,axis:"horizontal",maxZoomIn:.1}};this.isSingleDevice&&(n.legend.position="none",n.vAxes[0].title=null,n.chartArea.left=40,n.chartArea.height="80%",n.chartArea.top=5,n.enableInteractivity=!1);var r=new Date(Math.min.apply(null,e.map(function(t){return t[0].lastChangedAsDate}))),o=new Date(r);o.setDate(o.getDate()+1),o>new Date&&(o=new Date);var u=e.map(function(t){function e(t,e){c&&e&&s.push([t[0]].concat(c.slice(1).map(function(t,n){return e[n]?t:null}))),s.push(t),c=t}var n=t[t.length-1],r=n.domain,a=n.entityDisplay,u=new window.google.visualization.DataTable;u.addColumn({type:"datetime",id:"Time"});var s=[],c=void 0;if("thermostat"===r){var l=t.reduce(function(t,e){return t||e.attributes.target_temp_high!==e.attributes.target_temp_low},!1);u.addColumn("number",a+" current temperature");var f=void 0;l?!function(){u.addColumn("number",a+" target temperature high"),u.addColumn("number",a+" target temperature low");var t=[!1,!0,!0];f=function(n){var r=i(n.attributes.current_temperature),o=i(n.attributes.target_temp_high),a=i(n.attributes.target_temp_low);e([n.lastChangedAsDate,r,o,a],t)}}():!function(){u.addColumn("number",a+" target temperature");var t=[!1,!0];f=function(n){var r=i(n.attributes.current_temperature),o=i(n.attributes.temperature);e([n.lastChangedAsDate,r,o],t)}}(),t.forEach(f)}else!function(){u.addColumn("number",a);var n="sensor"!==r&&[!0];t.forEach(function(t){var r=i(t.state);e([t.lastChangedAsDate,r],n)})}();return e([o].concat(c.slice(1)),!1),u.addRows(s),u}),s=void 0;s=1===u.length?u[0]:u.slice(1).reduce(function(t,e){return window.google.visualization.data.join(t,e,"full",[[0,0]],(0,a["default"])(1,t.getNumberOfColumns()),(0,a["default"])(1,e.getNumberOfColumns()))},u[0]),this.chartEngine.draw(s,n)}}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"state-history-chart-timeline",properties:{data:{type:Object,observer:"dataChanged"},isAttached:{type:Boolean,value:!1,observer:"dataChanged"}},attached:function(){this.isAttached=!0},dataChanged:function(){this.drawChart()},drawChart:function(){function t(t,e,n,r){var o=e.replace(/_/g," ");i.addRow([t,o,n,r])}if(this.isAttached){for(var e=o["default"].dom(this),n=this.data;e.node.lastChild;)e.node.removeChild(e.node.lastChild);if(n&&0!==n.length){var r=new window.google.visualization.Timeline(this),i=new window.google.visualization.DataTable;i.addColumn({type:"string",id:"Entity"}),i.addColumn({type:"string",id:"State"}),i.addColumn({type:"date",id:"Start"}),i.addColumn({type:"date",id:"End"});var a=new Date(n.reduce(function(t,e){return Math.min(t,e[0].lastChangedAsDate)},new Date)),u=new Date(a);u.setDate(u.getDate()+1),u>new Date&&(u=new Date);var s=0;n.forEach(function(e){if(0!==e.length){var n=e[0].entityDisplay,r=void 0,i=null,o=null;e.forEach(function(e){null!==i&&e.state!==i?(r=e.lastChangedAsDate,t(n,i,o,r),i=e.state,o=r):null===i&&(i=e.state,o=e.lastChangedAsDate)}),t(n,i,o,u),s++}}),r.draw(i,{height:55+42*s,timeline:{showRowLabels:n.length>1},hAxis:{format:"H:mm"}})}}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].streamGetters,f=o["default"].streamActions;e["default"]=new u["default"]({is:"stream-status",behaviors:[c["default"]],properties:{isStreaming:{type:Boolean,bindNuclear:l.isStreamingEvents},hasError:{type:Boolean,bindNuclear:l.hasStreamingEventsError}},toggleChanged:function(){this.isStreaming?f.stop():f.start()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].voiceActions,f=o["default"].voiceGetters;e["default"]=new u["default"]({is:"ha-voice-command-dialog",behaviors:[c["default"]],properties:{dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},finalTranscript:{type:String,bindNuclear:f.finalTranscript},interimTranscript:{type:String,bindNuclear:f.extraInterimTranscript},isTransmitting:{type:Boolean,bindNuclear:f.isTransmitting},isListening:{type:Boolean,bindNuclear:f.isListening},showListenInterface:{type:Boolean,computed:"computeShowListenInterface(isListening, isTransmitting)",observer:"showListenInterfaceChanged"},_boundOnBackdropTap:{type:Function,value:function(){return this._onBackdropTap.bind(this)}}},computeShowListenInterface:function(t,e){return t||e},dialogOpenChanged:function(t){t?this.$.dialog.backdropElement.addEventListener("click",this._boundOnBackdropTap):!t&&this.isListening&&l.stop()},showListenInterfaceChanged:function(t){!t&&this.dialogOpen?this.dialogOpen=!1:t&&(this.dialogOpen=!0)},_onBackdropTap:function(){this.$.dialog.backdropElement.removeEventListener("click",this._boundOnBackdropTap),this.isListening&&l.stop()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(19),n(37),n(104);var l=o["default"].configGetters,f=o["default"].entityHistoryGetters,d=o["default"].entityHistoryActions,h=o["default"].moreInfoGetters,p=o["default"].moreInfoActions,_=["camera","configurator","scene"];e["default"]=new u["default"]({is:"more-info-dialog",behaviors:[c["default"]],properties:{stateObj:{type:Object,bindNuclear:h.currentEntity,observer:"stateObjChanged"},stateHistory:{type:Object,bindNuclear:[h.currentEntityHistory,function(t){return t?[t]:!1}]},isLoadingHistoryData:{type:Boolean,computed:"computeIsLoadingHistoryData(_delayedDialogOpen, _isLoadingHistoryData)"},_isLoadingHistoryData:{type:Boolean,bindNuclear:f.isLoadingEntityHistory},hasHistoryComponent:{type:Boolean,bindNuclear:l.isComponentLoaded("history"),observer:"fetchHistoryData"},shouldFetchHistory:{type:Boolean,bindNuclear:h.isCurrentEntityHistoryStale,observer:"fetchHistoryData"},showHistoryComponent:{type:Boolean,value:!1},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"},_delayedDialogOpen:{ +type:Boolean,value:!1}},computeIsLoadingHistoryData:function(t,e){return!t||e},fetchHistoryData:function(){this.stateObj&&this.hasHistoryComponent&&this.shouldFetchHistory&&d.fetchRecent(this.stateObj.entityId)},stateObjChanged:function(t){var e=this;return t?(this.showHistoryComponent=this.hasHistoryComponent&&-1===_.indexOf(this.stateObj.domain),void this.async(function(){e.fetchHistoryData(),e.dialogOpen=!0},10)):void(this.dialogOpen=!1)},dialogOpenChanged:function(t){var e=this;t?this.async(function(){return e._delayedDialogOpen=!0},10):!t&&this.stateObj&&(p.deselectEntity(),this._delayedDialogOpen=!1)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(4),c=r(s),l=n(41),f=r(l);n(80),n(99),n(97),n(96),n(98),n(91),n(92),n(94),n(95),n(93),n(100),n(88),n(87);var d=u["default"].navigationActions,h=u["default"].navigationGetters,p=u["default"].startUrlSync,_=u["default"].stopUrlSync;e["default"]=new o["default"]({is:"home-assistant-main",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},activePane:{type:String,bindNuclear:h.activePane,observer:"activePaneChanged"},isSelectedStates:{type:Boolean,bindNuclear:h.isActivePane("states")},isSelectedHistory:{type:Boolean,bindNuclear:h.isActivePane("history")},isSelectedMap:{type:Boolean,bindNuclear:h.isActivePane("map")},isSelectedLogbook:{type:Boolean,bindNuclear:h.isActivePane("logbook")},isSelectedDevEvent:{type:Boolean,bindNuclear:h.isActivePane("devEvent")},isSelectedDevState:{type:Boolean,bindNuclear:h.isActivePane("devState")},isSelectedDevTemplate:{type:Boolean,bindNuclear:h.isActivePane("devTemplate")},isSelectedDevService:{type:Boolean,bindNuclear:h.isActivePane("devService")},isSelectedDevInfo:{type:Boolean,bindNuclear:h.isActivePane("devInfo")},showSidebar:{type:Boolean,bindNuclear:h.showSidebar}},listeners:{"open-menu":"openMenu","close-menu":"closeMenu"},openMenu:function(){this.narrow?this.$.drawer.openDrawer():d.showSidebar(!0)},closeMenu:function(){this.$.drawer.closeDrawer(),this.showSidebar&&d.showSidebar(!1)},activePaneChanged:function(){this.narrow&&this.$.drawer.closeDrawer()},attached:function(){(0,f["default"])(),p()},computeForceNarrow:function(t,e){return t||!e},detached:function(){_()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(4),c=r(s),l=n(42),f=r(l),d=n(41),h=r(d),p=u["default"].authGetters;e["default"]=new o["default"]({is:"login-form",behaviors:[c["default"]],properties:{errorMessage:{type:String,bindNuclear:p.attemptErrorMessage},isInvalid:{type:Boolean,bindNuclear:p.isInvalidAttempt},isValidating:{type:Boolean,observer:"isValidatingChanged",bindNuclear:p.isValidating},loadingResources:{type:Boolean,value:!1},forceShowLoading:{type:Boolean,value:!1},showLoading:{type:Boolean,computed:"computeShowSpinner(forceShowLoading, isValidating)"}},listeners:{keydown:"passwordKeyDown","loginButton.click":"validatePassword"},observers:["validatingChanged(isValidating, isInvalid)"],attached:function(){(0,h["default"])()},computeShowSpinner:function(t,e){return t||e},validatingChanged:function(t,e){t||e||(this.$.passwordInput.value="")},isValidatingChanged:function(t){var e=this;t||this.async(function(){return e.$.passwordInput.focus()},10)},passwordKeyDown:function(t){13===t.keyCode?(this.validatePassword(),t.preventDefault()):this.isInvalid&&(this.isInvalid=!1)},validatePassword:function(){this.$.hideKeyboardOnFocus.focus(),(0,f["default"])(this.$.passwordInput.value,this.$.rememberLogin.checked)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(83);var s=o["default"].reactor,c=o["default"].serviceActions,l=o["default"].serviceGetters;e["default"]=new u["default"]({is:"partial-dev-call-service",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},domain:{type:String,value:""},service:{type:String,value:""},serviceData:{type:String,value:""},description:{type:String,computed:"computeDescription(domain, service)"}},computeDescription:function(t,e){return s.evaluate([l.entityMap,function(n){return n.has(t)&&n.get(t).get("services").has(e)?JSON.stringify(n.get(t).get("services").get(e).toJS(),null,2):"No description available"}])},serviceSelected:function(t){this.domain=t.detail.domain,this.service=t.detail.service},callService:function(){var t=void 0;try{t=this.serviceData?JSON.parse(this.serviceData):{}}catch(e){return void alert("Error parsing JSON: "+e)}c.callService(this.domain,this.service,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(76);var s=o["default"].eventActions;e["default"]=new u["default"]({is:"partial-dev-fire-event",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},eventType:{type:String,value:""},eventData:{type:String,value:""}},eventSelected:function(t){this.eventType=t.detail.eventType},fireEvent:function(){var t=void 0;try{t=this.eventData?JSON.parse(this.eventData):{}}catch(e){return void alert("Error parsing JSON: "+e)}s.fireEvent(this.eventType,t)},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7);var l=o["default"].configGetters,f=o["default"].errorLogActions;e["default"]=new u["default"]({is:"partial-dev-info",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},hassVersion:{type:String,bindNuclear:l.serverVersion},polymerVersion:{type:String,value:u["default"].version},nuclearVersion:{type:String,value:"1.2.1"},errorLog:{type:String,value:""}},attached:function(){this.refreshErrorLog()},refreshErrorLog:function(t){var e=this;t&&t.preventDefault(),this.errorLog="Loading error log…",f.fetchErrorLog().then(function(t){return e.errorLog=t||"No errors have been reported."})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(7),n(71);var s=o["default"].reactor,c=o["default"].entityGetters,l=o["default"].entityActions;e["default"]=new u["default"]({is:"partial-dev-set-state",properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},entityId:{type:String,value:""},state:{type:String,value:""},stateAttributes:{type:String,value:""}},setStateData:function(t){var e=t?JSON.stringify(t,null," "):"";this.$.inputData.value=e,this.$.inputDataWrapper.update(this.$.inputData)},entitySelected:function(t){var e=s.evaluate(c.byId(t.detail.entityId));this.entityId=e.entityId,this.state=e.state,this.stateAttributes=JSON.stringify(e.attributes,null," ")},handleSetState:function(){var t=void 0;try{t=this.stateAttributes?JSON.parse(this.stateAttributes):{}}catch(e){return void alert("Error parsing JSON: "+e)}l.save({entityId:this.entityId,state:this.state,attributes:t})},computeFormClasses:function(t){return"layout "+(t?"vertical":"horizontal")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7);var l=o["default"].templateActions;e["default"]=new u["default"]({is:"partial-dev-template",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'{%- if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(t){return"content fit layout "+(t?"vertical":"horizontal")},computeRenderedClasses:function(t){return t?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate,500)},renderTemplate:function(){var t=this;this.rendering=!0,l.render(this.template).then(function(e){t.processed=e,t.rendering=!1},function(e){t.processed=e.message,t.error=!0,t.rendering=!1})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(37);var l=o["default"].entityHistoryGetters,f=o["default"].entityHistoryActions;e["default"]=new u["default"]({is:"partial-history",behaviors:[c["default"]],properties:{narrow:{type:Boolean},showMenu:{type:Boolean,value:!1},isDataLoaded:{type:Boolean,bindNuclear:l.hasDataForCurrentDate,observer:"isDataLoadedChanged"},stateHistory:{type:Object,bindNuclear:l.entityHistoryForCurrentDate},isLoadingData:{type:Boolean,bindNuclear:l.isLoadingEntityHistory},selectedDate:{type:String,value:null,bindNuclear:l.currentDate}},isDataLoadedChanged:function(t){t||this.async(function(){return f.fetchSelectedDate()},1)},handleRefreshClick:function(){f.fetchSelectedDate()},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:f.changeCurrentDate})},detached:function(){this.datePicker.destroy()},computeContentClasses:function(t){return"flex content "+(t?"narrow":"wide")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(79),n(18);var l=o["default"].logbookGetters,f=o["default"].logbookActions;e["default"]=new u["default"]({is:"partial-logbook",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},selectedDate:{type:String,bindNuclear:l.currentDate},isLoading:{type:Boolean,bindNuclear:l.isLoadingEntries},isStale:{type:Boolean,bindNuclear:l.isCurrentStale,observer:"isStaleChanged"},entries:{type:Array,bindNuclear:[l.currentEntries,function(t){return t.reverse().toArray()}]},datePicker:{type:Object}},isStaleChanged:function(t){var e=this;t&&this.async(function(){return f.fetchDate(e.selectedDate)},1)},handleRefresh:function(){f.fetchDate(this.selectedDate)},datepickerFocus:function(){this.datePicker.adjustPosition()},attached:function(){this.datePicker=new window.Pikaday({field:this.$.datePicker.inputElement,onSelect:f.changeCurrentDate})},detached:function(){this.datePicker.destroy()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(72);var l=o["default"].configGetters,f=o["default"].entityGetters;window.L.Icon.Default.imagePath="/static/images/leaflet",e["default"]=new u["default"]({is:"partial-map",behaviors:[c["default"]],properties:{locationGPS:{type:Number,bindNuclear:l.locationGPS},locationName:{type:String,bindNuclear:l.locationName},locationEntities:{type:Array,bindNuclear:[f.visibleEntityMap,function(t){return t.valueSeq().filter(function(t){return t.attributes.latitude&&"home"!==t.state}).toArray()}]},zoneEntities:{type:Array,bindNuclear:[f.entityMap,function(t){return t.valueSeq().filter(function(t){return"zone"===t.domain}).toArray()}]},narrow:{type:Boolean},showMenu:{type:Boolean,value:!1}},attached:function(){var t=this;window.L.Browser.mobileWebkit&&this.async(function(){var e=t.$.map,n=e.style.display;e.style.display="none",t.async(function(){e.style.display=n},1)},1)},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},toggleMenu:function(){this.fire("open-menu")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(7),n(81);var l=o["default"].configGetters,f=o["default"].entityGetters,d=o["default"].voiceGetters,h=o["default"].streamGetters,p=o["default"].syncGetters,_=o["default"].syncActions,v=o["default"].voiceActions;e["default"]=new u["default"]({is:"partial-zone",behaviors:[c["default"]],properties:{narrow:{type:Boolean,value:!1},isFetching:{type:Boolean,bindNuclear:p.isFetching},isStreaming:{type:Boolean,bindNuclear:h.isStreamingEvents},canListen:{type:Boolean,bindNuclear:[d.isVoiceSupported,l.isComponentLoaded("conversation"),function(t,e){return t&&e}]},introductionLoaded:{type:Boolean,bindNuclear:l.isComponentLoaded("introduction")},locationName:{type:String,bindNuclear:l.locationName},showMenu:{type:Boolean,value:!1,observer:"windowChange"},states:{type:Object,bindNuclear:f.visibleEntityMap},columns:{type:Number,value:1}},created:function(){var t=this;this.windowChange=this.windowChange.bind(this);for(var e=[],n=0;5>n;n++)e.push(300+300*n);this.mqls=e.map(function(e){var n=window.matchMedia("(min-width: "+e+"px)");return n.addListener(t.windowChange),n})},detached:function(){var t=this;this.mqls.forEach(function(e){return e.removeListener(t.windowChange)})},windowChange:function(){var t=this.mqls.reduce(function(t,e){return t+e.matches},0);this.columns=Math.max(1,t-this.showMenu)},handleRefresh:function(){_.fetchAll()},handleListenClick:function(){v.listen()},computeDomains:function(t){return t.keySeq().toArray()},computeMenuButtonClass:function(t,e){return!t&&e?"invisible":""},computeStatesOfDomain:function(t,e){return t.get(e).toArray()},computeRefreshButtonClass:function(t){return t?"ha-spin":void 0},computeShowIntroduction:function(t,e){return t||0===e.size},toggleMenu:function(){this.fire("open-menu")}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s),l=o["default"].notificationGetters;e["default"]=new u["default"]({is:"notification-manager",behaviors:[c["default"]],properties:{text:{type:String,bindNuclear:l.lastNotificationMessage,observer:"showNotification"}},showNotification:function(t){t&&this.$.toast.show()}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=o["default"].serviceActions;e["default"]=new u["default"]({is:"more-info-alarm_control_panel",handleDisarmTap:function(){this.callService("alarm_disarm",{code:this.enteredCode})},handleHomeTap:function(){this.callService("alarm_arm_home",{code:this.enteredCode})},handleAwayTap:function(){this.callService("alarm_arm_away",{code:this.enteredCode})},properties:{stateObj:{type:Object,observer:"stateObjChanged"},enteredCode:{type:String,value:""},disarmButtonVisible:{type:Boolean,value:!1},armHomeButtonVisible:{type:Boolean,value:!1},armAwayButtonVisible:{type:Boolean,value:!1},codeInputVisible:{type:Boolean,value:!1},codeInputEnabled:{type:Boolean,value:!1},codeFormat:{type:String,value:""},codeValid:{type:Boolean,computed:"validateCode(enteredCode, codeFormat)"}},validateCode:function(t,e){var n=new RegExp(e);return null===e?!0:n.test(t)},stateObjChanged:function(t){var e=this;t&&(this.codeFormat=t.attributes.code_format,this.codeInputVisible=null!==this.codeFormat,this.codeInputEnabled="armed_home"===t.state||"armed_away"===t.state||"disarmed"===t.state||"pending"===t.state||"triggered"===t.state,this.disarmButtonVisible="armed_home"===t.state||"armed_away"===t.state||"pending"===t.state||"triggered"===t.state,this.armHomeButtonVisible="disarmed"===t.state,this.armAwayButtonVisible="disarmed"===t.state),this.async(function(){return e.fire("iron-resize")},500)},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,s.callService("alarm_control_panel",t,n)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-camera",properties:{stateObj:{type:Object},dialogOpen:{type:Boolean}},imageLoaded:function(){this.fire("iron-resize")},computeCameraImageUrl:function(t){return t?"/api/camera_proxy_stream/"+this.stateObj.entityId:"data:image/gif;base64,R0lGODlhAQABAAAAACw="}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(18);var l=o["default"].streamGetters,f=o["default"].syncActions,d=o["default"].serviceActions;e["default"]=new u["default"]({is:"more-info-configurator",behaviors:[c["default"]],properties:{stateObj:{type:Object},action:{type:String,value:"display"},isStreaming:{type:Boolean,bindNuclear:l.isStreamingEvents},isConfigurable:{type:Boolean,computed:"computeIsConfigurable(stateObj)"},isConfiguring:{type:Boolean,value:!1},submitCaption:{type:String,computed:"computeSubmitCaption(stateObj)"},fieldInput:{type:Object,value:{}}},computeIsConfigurable:function(t){return"configure"===t.state},computeSubmitCaption:function(t){return t.attributes.submit_caption||"Set configuration"},fieldChanged:function(t){var e=t.target;this.fieldInput[e.id]=e.value},submitClicked:function(){var t=this;this.isConfiguring=!0;var e={configure_id:this.stateObj.attributes.configure_id,fields:this.fieldInput};d.callService("configurator","configure",e).then(function(){t.isConfiguring=!1,t.isStreaming||f.fetchAll()},function(){t.isConfiguring=!1})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(122),u=r(a);n(105),n(106),n(110),n(103),n(111),n(109),n(107),n(108),n(102),n(112),n(101),e["default"]=new o["default"]({is:"more-info-content",properties:{stateObj:{type:Object,observer:"stateObjChanged"},dialogOpen:{type:Boolean,value:!1,observer:"dialogOpenChanged"}},dialogOpenChanged:function(t){var e=o["default"].dom(this);e.lastChild&&(e.lastChild.dialogOpen=t)},stateObjChanged:function(t,e){var n=o["default"].dom(this);if(!t)return void(n.lastChild&&n.removeChild(n.lastChild));var r=(0,u["default"])(t);if(e&&(0,u["default"])(e)===r)n.lastChild.dialogOpen=this.dialogOpen,n.lastChild.stateObj=t;else{n.lastChild&&n.removeChild(n.lastChild);var i=document.createElement("more-info-"+r);i.stateObj=t,i.dialogOpen=this.dialogOpen,n.appendChild(i)}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=["entity_picture","friendly_name","icon","unit_of_measurement"];e["default"]=new o["default"]({is:"more-info-default",properties:{stateObj:{type:Object}},computeDisplayAttributes:function(t){return t?Object.keys(t.attributes).filter(function(t){return-1===a.indexOf(t)}):[]},getAttributeValue:function(t,e){return t.attributes[e]}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(4),c=r(s);n(19);var l=o["default"].entityGetters,f=o["default"].moreInfoGetters;e["default"]=new u["default"]({is:"more-info-group",behaviors:[c["default"]],properties:{stateObj:{type:Object},states:{type:Array,bindNuclear:[f.currentEntity,l.entityMap,function(t,e){return t?t.attributes.entity_id.map(e.get.bind(e)):[]}]}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){f.callService("light","turn_on",{entity_id:t,rgb_color:[e.r,e.g,e.b]})}Object.defineProperty(e,"__esModule",{value:!0});var o=n(2),a=r(o),u=n(1),s=r(u),c=n(20),l=r(c);n(77);var f=a["default"].serviceActions,d=["brightness","rgb_color","color_temp"];e["default"]=new s["default"]({is:"more-info-light",properties:{stateObj:{type:Object,observer:"stateObjChanged"},brightnessSliderValue:{type:Number,value:0},ctSliderValue:{type:Number,value:0}},stateObjChanged:function(t){var e=this;t&&"on"===t.state&&(this.brightnessSliderValue=t.attributes.brightness,this.ctSliderValue=t.attributes.color_temp),this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return(0,l["default"])(t,d)},brightnessSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||(0===e?f.callTurnOff(this.stateObj.entityId):f.callService("light","turn_on",{entity_id:this.stateObj.entityId,brightness:e}))},ctSliderChanged:function(t){var e=parseInt(t.target.value,10);isNaN(e)||f.callService("light","turn_on",{entity_id:this.stateObj.entityId,color_temp:e})},colorPicked:function(t){var e=this;return this.skipColorPicked?void(this.colorChanged=!0):(this.color=t.detail.rgb,i(this.stateObj.entityId,this.color),this.colorChanged=!1,this.skipColorPicked=!0,void(this.colorDebounce=setTimeout(function(){e.colorChanged&&i(e.stateObj.entityId,e.color),e.skipColorPicked=!1},500)))}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(20),c=r(s),l=o["default"].serviceActions,f=["volume_level"];e["default"]=new u["default"]({is:"more-info-media_player",properties:{stateObj:{type:Object,observer:"stateObjChanged"},isOff:{type:Boolean,value:!1},isPlaying:{type:Boolean,value:!1},isMuted:{type:Boolean,value:!1},volumeSliderValue:{type:Number,value:0},supportsPause:{type:Boolean,value:!1},supportsVolumeSet:{type:Boolean,value:!1},supportsVolumeMute:{type:Boolean,value:!1},supportsPreviousTrack:{type:Boolean,value:!1},supportsNextTrack:{type:Boolean,value:!1},supportsTurnOn:{type:Boolean,value:!1},supportsTurnOff:{type:Boolean,value:!1},supportsVolumeButtons:{type:Boolean,value:!1},hasMediaControl:{type:Boolean,value:!1}},stateObjChanged:function(t){var e=this;if(t){var n=["playing","paused","unknown"];this.isOff="off"===t.state,this.isPlaying="playing"===t.state,this.hasMediaControl=-1!==n.indexOf(t.state),this.volumeSliderValue=100*t.attributes.volume_level,this.isMuted=t.attributes.is_volume_muted,this.supportsPause=0!==(1&t.attributes.supported_media_commands),this.supportsVolumeSet=0!==(4&t.attributes.supported_media_commands),this.supportsVolumeMute=0!==(8&t.attributes.supported_media_commands),this.supportsPreviousTrack=0!==(16&t.attributes.supported_media_commands),this.supportsNextTrack=0!==(32&t.attributes.supported_media_commands),this.supportsTurnOn=0!==(128&t.attributes.supported_media_commands),this.supportsTurnOff=0!==(256&t.attributes.supported_media_commands),this.supportsVolumeButtons=0!==(1024&t.attributes.supported_media_commands)}this.async(function(){return e.fire("iron-resize")},500)},computeClassNames:function(t){return(0,c["default"])(t,f)},computeIsOff:function(t){return"off"===t.state},computeMuteVolumeIcon:function(t){return t?"mdi:volume-off":"mdi:volume-high"},computeHideVolumeButtons:function(t,e){return!e||t},computeShowPlaybackControls:function(t,e){return!t&&e},computePlaybackControlIcon:function(){return this.isPlaying?this.supportsPause?"mdi:pause":"mdi:stop":"mdi:play"},computeHidePowerButton:function(t,e,n){return t?!e:!n},handleTogglePower:function(){this.callService(this.isOff?"turn_on":"turn_off")},handlePrevious:function(){this.callService("media_previous_track")},handlePlaybackControl:function(){this.callService("media_play_pause")},handleNext:function(){this.callService("media_next_track")},handleVolumeTap:function(){this.supportsVolumeMute&&this.callService("volume_mute",{is_volume_muted:!this.isMuted})},handleVolumeUp:function(){var t=this.$.volumeUp;this.handleVolumeWorker("volume_up",t,!0)},handleVolumeDown:function(){var t=this.$.volumeDown;this.handleVolumeWorker("volume_down",t,!0)},handleVolumeWorker:function(t,e,n){var r=this;(n||void 0!==e&&e.pointerDown)&&(this.callService(t),this.async(function(){return r.handleVolumeWorker(t,e,!1)},500))},volumeSliderChanged:function(t){var e=parseFloat(t.target.value),n=e>0?e/100:0;this.callService("volume_set",{volume_level:n})},callService:function(t,e){var n=e||{};n.entity_id=this.stateObj.entityId,l.callService("media_player",t,n)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-script",properties:{stateObj:{type:Object}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a),s=n(40),c=r(s),l=u["default"].util.parseDateTime;e["default"]=new o["default"]({is:"more-info-sun",properties:{stateObj:{type:Object},risingDate:{type:Object,computed:"computeRising(stateObj)"},settingDate:{type:Object,computed:"computeSetting(stateObj)"}},computeRising:function(t){return l(t.attributes.next_rising)},computeSetting:function(t){return l(t.attributes.next_setting)},computeOrder:function(t,e){return t>e?["set","ris"]:["ris","set"]},itemCaption:function(t){return"ris"===t?"Rising ":"Setting "},itemDate:function(t){return"ris"===t?this.risingDate:this.settingDate},itemValue:function(t){return(0,c["default"])(this.itemDate(t))}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a),s=n(20),c=r(s),l=o["default"].serviceActions,f=["away_mode"];e["default"]=new u["default"]({is:"more-info-thermostat",properties:{stateObj:{type:Object,observer:"stateObjChanged"},tempMin:{type:Number},tempMax:{type:Number},targetTemperatureSliderValue:{type:Number},awayToggleChecked:{type:Boolean}},stateObjChanged:function(t){this.targetTemperatureSliderValue=t.attributes.temperature,this.awayToggleChecked="on"===t.attributes.away_mode,this.tempMin=t.attributes.min_temp,this.tempMax=t.attributes.max_temp},computeClassNames:function(t){return(0,c["default"])(t,f)},targetTemperatureSliderChanged:function(t){l.callService("thermostat","set_temperature",{entity_id:this.stateObj.entityId,temperature:t.target.value})},toggleChanged:function(t){var e=t.target.checked;e&&"off"===this.stateObj.attributes.away_mode?this.service_set_away(!0):e||"on"!==this.stateObj.attributes.away_mode||this.service_set_away(!1)},service_set_away:function(t){var e=this;l.callService("thermostat","set_away_mode",{away_mode:t,entity_id:this.stateObj.entityId}).then(function(){return e.stateObjChanged(e.stateObj)})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);e["default"]=new o["default"]({is:"more-info-updater",properties:{}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),n(38),e["default"]=new o["default"]({is:"state-card-configurator",properties:{stateObj:{type:Object}}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8);var a=["playing","paused"];e["default"]=new o["default"]({is:"state-card-media_player",properties:{stateObj:{type:Object},isPlaying:{type:Boolean,computed:"computeIsPlaying(stateObj)"}},computeIsPlaying:function(t){return-1!==a.indexOf(t.state)},computePrimaryText:function(t,e){return e?t.attributes.media_title:t.stateDisplay},computeSecondaryText:function(t){var e=void 0;return"music"===t.attributes.media_content_type?t.attributes.media_artist:"tvshow"===t.attributes.media_content_type?(e=t.attributes.media_series_title,t.attributes.media_season&&t.attributes.media_episode&&(e+=" S"+t.attributes.media_season+"E"+t.attributes.media_episode),e):t.attributes.app_name?t.attributes.app_name:""}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(2),o=r(i),a=n(1),u=r(a);n(8);var s=o["default"].serviceActions;e["default"]=new u["default"]({is:"state-card-rollershutter",properties:{stateObj:{type:Object}},computeIsFullyOpen:function(t){return 100===t.attributes.current_position},computeIsFullyClosed:function(t){return 0===t.attributes.current_position},onMoveUpTap:function(){s.callService("rollershutter","move_up",{entity_id:this.stateObj.entityId})},onMoveDownTap:function(){s.callService("rollershutter","move_down",{entity_id:this.stateObj.entityId})},onStopTap:function(){s.callService("rollershutter","stop",{entity_id:this.stateObj.entityId})}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i),a=n(2),u=r(a);n(8);var s=u["default"].serviceActions;e["default"]=new o["default"]({is:"state-card-scene",properties:{stateObj:{type:Object}},activateScene:function(){s.callTurnOn(this.stateObj.entityId)}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),e["default"]=new o["default"]({is:"state-card-thermostat",properties:{stateObj:{type:Object}},computeTargetTemperature:function(t){return t.attributes.temperature+" "+t.attributes.unit_of_measurement}})},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(1),o=r(i);n(8),n(34),e["default"]=new o["default"]({is:"state-card-toggle"})},function(t,e){"use strict";function n(t){return{attached:function(){var e=this;this.__unwatchFns=Object.keys(this.properties).reduce(function(n,r){if(!("bindNuclear"in e.properties[r]))return n;var i=e.properties[r].bindNuclear;if(!i)throw new Error("Undefined getter specified for key "+r);return e[r]=t.evaluate(i),n.concat(t.observe(i,function(t){e[r]=t}))},[])},detached:function(){for(;this.__unwatchFns.length;)this.__unwatchFns.shift()()}}}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){return-1!==u.indexOf(t.domain)?t.domain:(0,a["default"])(t.entityId)?"toggle":"display"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(21),a=r(o),u=["thermostat","configurator","scene","media_player","rollershutter"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(!t)return a["default"];if(t.attributes.icon)return t.attributes.icon;var e=t.attributes.unit_of_measurement;return!e||"sensor"!==t.domain||e!==f.UNIT_TEMP_C&&e!==f.UNIT_TEMP_F?(0,s["default"])(t.domain,t.state):"mdi:thermometer"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=i;var o=n(39),a=r(o),u=n(22),s=r(u),c=n(2),l=r(c),f=l["default"].util.temperatureUnits},function(t,e){"use strict";function n(t){return-1!==r.indexOf(t.domain)?t.domain:"default"}Object.defineProperty(e,"__esModule",{value:!0}),e["default"]=n;var r=["light","group","sun","configurator","thermostat","script","media_player","camera","updater","alarm_control_panel"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(187),i=n(15),o=function(t,e,n){var o=arguments.length<=3||void 0===arguments[3]?null:arguments[3],a=t.evaluate(i.getters.authInfo),u=a.host+"/api/"+n;return new r.Promise(function(t,n){var r=new XMLHttpRequest; +r.open(e,u,!0),r.setRequestHeader("X-HA-access",a.authToken),r.onload=function(){var e=void 0;try{e="application/json"===r.getResponseHeader("content-type")?JSON.parse(r.responseText):r.responseText}catch(i){e=r.responseText}r.status>199&&r.status<300?t(e):n(e)},r.onerror=function(){return n({})},o?r.send(JSON.stringify(o)):r.send()})};e["default"]=o},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2],r=n.useStreaming,i=void 0===r?t.evaluate(c.getters.isSupported):r,o=n.rememberAuth,a=void 0===o?!1:o,s=n.host,d=void 0===s?"":s;t.dispatch(u["default"].VALIDATING_AUTH_TOKEN,{authToken:e,host:d}),l.actions.fetchAll(t).then(function(){t.dispatch(u["default"].VALID_AUTH_TOKEN,{authToken:e,host:d,rememberAuth:a}),i?c.actions.start(t,{syncOnInitialConnect:!1}):l.actions.start(t,{skipInitialSync:!0})},function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],n=e.message,r=void 0===n?f:n;t.dispatch(u["default"].INVALID_AUTH_TOKEN,{errorMessage:r})})}function o(t){(0,s.callApi)(t,"POST","log_out"),t.dispatch(u["default"].LOG_OUT,{})}Object.defineProperty(e,"__esModule",{value:!0}),e.validate=i,e.logOut=o;var a=n(14),u=r(a),s=n(5),c=n(28),l=n(30),f="Unexpected result from API"},function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=e.isValidating=["authAttempt","isValidating"],r=(e.isInvalidAttempt=["authAttempt","isInvalid"],e.attemptErrorMessage=["authAttempt","errorMessage"],e.rememberAuth=["rememberAuth"],e.attemptAuthInfo=[["authAttempt","authToken"],["authAttempt","host"],function(t,e){return{authToken:t,host:e}}]),i=e.currentAuthToken=["authCurrent","authToken"],o=e.currentAuthInfo=[i,["authCurrent","host"],function(t,e){return{authToken:t,host:e}}];e.authToken=[n,["authAttempt","authToken"],["authCurrent","authToken"],function(t,e,n){return t?e:n}],e.authInfo=[n,r,o,function(t,e,n){return t?e:n}]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t){if(null==t)throw new TypeError("Cannot destructure undefined")}function o(t,e){var n=e.authToken,r=e.host;return(0,s.toImmutable)({authToken:n,host:r,isValidating:!0,isInvalid:!1,errorMessage:""})}function a(t,e){return i(e),f.getInitialState()}function u(t,e){var n=e.errorMessage;return t.withMutations(function(t){return t.set("isValidating",!1).set("isInvalid",!0).set("errorMessage",n)})}Object.defineProperty(e,"__esModule",{value:!0});var s=n(3),c=n(14),l=r(c),f=new s.Store({getInitialState:function(){return(0,s.toImmutable)({isValidating:!1,authToken:!1,host:null,isInvalid:!1,errorMessage:""})},initialize:function(){this.on(l["default"].VALIDATING_AUTH_TOKEN,o),this.on(l["default"].VALID_AUTH_TOKEN,a),this.on(l["default"].INVALID_AUTH_TOKEN,u)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.authToken,r=e.host;return(0,a.toImmutable)({authToken:n,host:r})}function o(){return c.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(14),s=r(u),c=new a.Store({getInitialState:function(){return(0,a.toImmutable)({authToken:null,host:""})},initialize:function(){this.on(s["default"].VALID_AUTH_TOKEN,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=c},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.rememberAuth;return n}Object.defineProperty(e,"__esModule",{value:!0});var o=n(3),a=n(14),u=r(a),s=new o.Store({getInitialState:function(){return!0},initialize:function(){this.on(u["default"].VALID_AUTH_TOKEN,i)}});e["default"]=s},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(c["default"].SERVER_CONFIG_LOADED,e)}function o(t){(0,u.callApi)(t,"GET","config").then(function(e){return i(t,e)})}function a(t,e){t.dispatch(c["default"].COMPONENT_LOADED,{component:e})}Object.defineProperty(e,"__esModule",{value:!0}),e.configLoaded=i,e.fetchAll=o,e.componentLoaded=a;var u=n(5),s=n(23),c=r(s)},function(t,e){"use strict";function n(t){return[["serverComponent"],function(e){return e.contains(t)}]}Object.defineProperty(e,"__esModule",{value:!0}),e.isComponentLoaded=n,e.locationGPS=[["serverConfig","latitude"],["serverConfig","longitude"],function(t,e){return{latitude:t,longitude:e}}],e.locationName=["serverConfig","location_name"],e.serverVersion=["serverConfig","serverVersion"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.component;return t.push(n)}function o(t,e){var n=e.components;return(0,u.toImmutable)(n)}function a(){return l.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var u=n(3),s=n(23),c=r(s),l=new u.Store({getInitialState:function(){return(0,u.toImmutable)([])},initialize:function(){this.on(c["default"].COMPONENT_LOADED,i),this.on(c["default"].SERVER_CONFIG_LOADED,o),this.on(c["default"].LOG_OUT,a)}});e["default"]=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.latitude,r=e.longitude,i=e.location_name,o=e.temperature_unit,u=e.time_zone,s=e.version;return(0,a.toImmutable)({latitude:n,longitude:r,location_name:i,temperature_unit:o,time_zone:u,serverVersion:s})}function o(){return c.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(23),s=r(u),c=new a.Store({getInitialState:function(){return(0,a.toImmutable)({latitude:null,longitude:null,location_name:"Home",temperature_unit:"°C",time_zone:"UTC",serverVersion:"unknown"})},initialize:function(){this.on(s["default"].SERVER_CONFIG_LOADED,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=c},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t,e){t.dispatch(f["default"].ENTITY_HISTORY_DATE_SELECTED,{date:e})}function a(t){var e=arguments.length<=1||void 0===arguments[1]?null:arguments[1];t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_START,{});var n="history/period";return null!==e&&(n+="?filter_entity_id="+e),(0,c.callApi)(t,"GET",n).then(function(e){return t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,{stateHistory:e})},function(){return t.dispatch(f["default"].RECENT_ENTITY_HISTORY_FETCH_ERROR,{})})}function u(t,e){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_START,{date:e}),(0,c.callApi)(t,"GET","history/period/"+e).then(function(n){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_SUCCESS,{date:e,stateHistory:n})},function(){return t.dispatch(f["default"].ENTITY_HISTORY_FETCH_ERROR,{})})}function s(t){var e=t.evaluate(h.currentDate);return u(t,e)}Object.defineProperty(e,"__esModule",{value:!0}),e.changeCurrentDate=o,e.fetchRecent=a,e.fetchDate=u,e.fetchSelectedDate=s;var c=n(5),l=n(11),f=i(l),d=n(43),h=r(d)},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.date;return(0,s["default"])(n)}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(31),s=r(u),c=n(11),l=r(c),f=new a.Store({getInitialState:function(){var t=new Date;return t.setDate(t.getDate()-1),(0,s["default"])(t)},initialize:function(){this.on(l["default"].ENTITY_HISTORY_DATE_SELECTED,i),this.on(l["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.date,r=e.stateHistory;return 0===r.length?t.set(n,(0,a.toImmutable)({})):t.withMutations(function(t){r.forEach(function(e){return t.setIn([n,e[0].entity_id],(0,a.toImmutable)(e.map(l["default"].fromJSON)))})})}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c=n(16),l=r(c),f=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(3),o=n(11),a=r(o),u=new i.Store({getInitialState:function(){return!1},initialize:function(){this.on(a["default"].ENTITY_HISTORY_FETCH_START,function(){return!0}),this.on(a["default"].ENTITY_HISTORY_FETCH_SUCCESS,function(){return!1}),this.on(a["default"].ENTITY_HISTORY_FETCH_ERROR,function(){return!1}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_START,function(){return!0}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,function(){return!1}),this.on(a["default"].RECENT_ENTITY_HISTORY_FETCH_ERROR,function(){return!1}),this.on(a["default"].LOG_OUT,function(){return!1})}});e["default"]=u},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.stateHistory;return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,(0,a.toImmutable)(e.map(l["default"].fromJSON)))})})}function o(){return f.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c=n(16),l=r(c),f=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){var n=e.stateHistory,r=(new Date).getTime();return t.withMutations(function(t){n.forEach(function(e){return t.set(e[0].entity_id,r)}),history.length>1&&t.set(c,r)})}function o(){return l.getInitialState()}Object.defineProperty(e,"__esModule",{value:!0});var a=n(3),u=n(11),s=r(u),c="ALL_ENTRY_FETCH",l=new a.Store({getInitialState:function(){return(0,a.toImmutable)({})},initialize:function(){this.on(s["default"].RECENT_ENTITY_HISTORY_FETCH_SUCCESS,i),this.on(s["default"].LOG_OUT,o)}});e["default"]=l},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(10),o=n(16),a=r(o),u=(0,i.createApiActions)(a["default"]);e["default"]=u},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e.visibleEntityMap=e.byId=e.entityMap=e.hasData=void 0;var i=n(10),o=n(16),a=r(o),u=(e.hasData=(0,i.createHasDataGetter)(a["default"]),e.entityMap=(0,i.createEntityMapGetter)(a["default"]));e.byId=(0,i.createByIdGetter)(a["default"]),e.visibleEntityMap=[u,function(t){return t.filter(function(t){return!t.attributes.hidden})}]},function(t,e,n){"use strict";function r(t){return(0,i.callApi)(t,"GET","error_log")}Object.defineProperty(e,"__esModule",{value:!0}),e.fetchErrorLog=r;var i=n(5)},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}Object.defineProperty(e,"__esModule",{value:!0}),e.actions=void 0;var i=n(141),o=r(i);e.actions=o},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),o=n(10),a=n(27),u=n(45),s=r(u),c=(0,o.createApiActions)(s["default"]);c.fireEvent=function(t,e){var n=arguments.length<=2||void 0===arguments[2]?{}:arguments[2];return(0,i.callApi)(t,"POST","events/"+e,n).then(function(){a.actions.createNotification(t,"Event "+e+" successful fired!")})},e["default"]=c},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0}),e.byId=e.entityMap=e.hasData=void 0;var i=n(10),o=n(45),a=r(o);e.hasData=(0,i.createHasDataGetter)(a["default"]),e.entityMap=(0,i.createEntityMapGetter)(a["default"]),e.byId=(0,i.createByIdGetter)(a["default"])},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){t.dispatch(s["default"].LOGBOOK_DATE_SELECTED,{date:e})}function o(t,e){t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_START,{date:e}),(0,a.callApi)(t,"GET","logbook/"+e).then(function(n){return t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_SUCCESS,{date:e,entries:n})},function(){return t.dispatch(s["default"].LOGBOOK_ENTRIES_FETCH_ERROR,{})})}Object.defineProperty(e,"__esModule",{value:!0}),e.changeCurrentDate=i,e.fetchDate=o;var a=n(5),u=n(12),s=r(u)},function(t,e,n){"use strict";function r(t){return!t||(new Date).getTime()-t>o}Object.defineProperty(e,"__esModule",{value:!0}),e.isLoadingEntries=e.currentEntries=e.isCurrentStale=e.currentDate=void 0;var i=n(3),o=6e4,a=e.currentDate=["currentLogbookDate"];e.isCurrentStale=[a,["logbookEntriesUpdated"],function(t,e){return r(e.get(t))}],e.currentEntries=[a,["logbookEntries"],function(t,e){return e.get(t)||(0,i.toImmutable)([])}],e.isLoadingEntries=["isLoadingLogbookEntries"]},function(t,e,n){"use strict";function r(t){if(t&&t.__esModule)return t;var e={};if(null!=t)for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n]);return e["default"]=t,e}function i(t){return t&&t.__esModule?t:{"default":t}}function o(t){t.registerStores({currentLogbookDate:u["default"],isLoadingLogbookEntries:c["default"],logbookEntries:f["default"],logbookEntriesUpdated:h["default"]})}Object.defineProperty(e,"__esModule",{value:!0}),e.getters=e.actions=void 0,e.register=o;var a=n(149),u=i(a),s=n(150),c=i(s),l=n(151),f=i(l),d=n(152),h=i(d),p=n(145),_=r(p),v=n(146),y=r(v);e.actions=_,e.getters=y},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function o(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function a(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}var u=function(){function t(t,e){for(var n=0;nt;t+=2){var e=rt[t],n=rt[t+1];e(n),rt[t]=void 0,rt[t+1]=void 0}$=0}function y(){try{var t=n(203);return q=t.runOnLoop||t.runOnContext,d()}catch(e){return _()}}function m(){}function g(){return new TypeError("You cannot resolve a promise with itself")}function b(){return new TypeError("A promises callback cannot return that same promise.")}function S(t){try{return t.then}catch(e){return ut.error=e,ut}}function w(t,e,n,r){try{t.call(e,n,r)}catch(i){return i}}function O(t,e,n){Z(function(t){var r=!1,i=w(n,e,function(n){r||(r=!0,e!==n?I(t,n):D(t,n))},function(e){r||(r=!0,C(t,e))},"Settle: "+(t._label||" unknown promise"));!r&&i&&(r=!0,C(t,i))},t)}function M(t,e){e._state===ot?D(t,e._result):e._state===at?C(t,e._result):j(e,void 0,function(e){I(t,e)},function(e){C(t,e)})}function T(t,e){if(e.constructor===t.constructor)M(t,e);else{var n=S(e);n===ut?C(t,ut.error):void 0===n?D(t,e):u(n)?O(t,e,n):D(t,e)}}function I(t,e){t===e?C(t,g()):a(e)?T(t,e):D(t,e)}function E(t){t._onerror&&t._onerror(t._result),A(t)}function D(t,e){t._state===it&&(t._result=e,t._state=ot,0!==t._subscribers.length&&Z(A,t))}function C(t,e){t._state===it&&(t._state=at,t._result=e,Z(E,t))}function j(t,e,n,r){var i=t._subscribers,o=i.length;t._onerror=null,i[o]=e,i[o+ot]=n,i[o+at]=r,0===o&&t._state&&Z(A,t)}function A(t){var e=t._subscribers,n=t._state;if(0!==e.length){for(var r,i,o=t._result,a=0;aa;a++)j(r.resolve(t[a]),void 0,e,n);return i}function H(t){var e=this;if(t&&"object"==typeof t&&t.constructor===e)return t;var n=new e(m);return I(n,t),n}function Y(t){var e=this,n=new e(m);return C(n,t),n}function U(){throw new TypeError("You must pass a resolver function as the first argument to the promise constructor")}function G(){throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.")}function B(t){this._id=pt++,this._state=void 0,this._result=void 0,this._subscribers=[],m!==t&&(u(t)||U(),this instanceof B||G(),N(this,t))}function F(){var t;if("undefined"!=typeof i)t=i;else if("undefined"!=typeof self)t=self;else try{t=Function("return this")()}catch(e){throw new Error("polyfill failed because global object is unavailable in this environment")}var n=t.Promise;(!n||"[object Promise]"!==Object.prototype.toString.call(n.resolve())||n.cast)&&(t.Promise=_t)}var V;V=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)};var q,W,K,J=V,$=0,Z=({}.toString,function(t,e){rt[$]=t,rt[$+1]=e,$+=2,2===$&&(W?W(v):K())}),X="undefined"!=typeof window?window:void 0,Q=X||{},tt=Q.MutationObserver||Q.WebKitMutationObserver,et="undefined"!=typeof t&&"[object process]"==={}.toString.call(t),nt="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel,rt=new Array(1e3);K=et?f():tt?h():nt?p():void 0===X?y():_();var it=void 0,ot=1,at=2,ut=new P,st=new P;R.prototype._validateInput=function(t){return J(t)},R.prototype._validationError=function(){return new Error("Array Methods must be provided an Array")},R.prototype._init=function(){this._result=new Array(this.length)};var ct=R;R.prototype._enumerate=function(){for(var t=this,e=t.length,n=t.promise,r=t._input,i=0;n._state===it&&e>i;i++)t._eachEntry(r[i],i)},R.prototype._eachEntry=function(t,e){var n=this,r=n._instanceConstructor;s(t)?t.constructor===r&&t._state!==it?(t._onerror=null,n._settledAt(t._state,e,t._result)):n._willSettleAt(r.resolve(t),e):(n._remaining--,n._result[e]=t)},R.prototype._settledAt=function(t,e,n){var r=this,i=r.promise;i._state===it&&(r._remaining--,t===at?C(i,n):r._result[e]=n),0===r._remaining&&D(i,r._result)},R.prototype._willSettleAt=function(t,e){var n=this;j(t,void 0,function(t){n._settledAt(ot,e,t)},function(t){n._settledAt(at,e,t)})};var lt=z,ft=x,dt=H,ht=Y,pt=0,_t=B;B.all=lt,B.race=ft,B.resolve=dt,B.reject=ht,B._setScheduler=c,B._setAsap=l,B._asap=Z,B.prototype={constructor:B,then:function(t,e){var n=this,r=n._state;if(r===ot&&!t||r===at&&!e)return this;var i=new this.constructor(m),o=n._result;if(r){var a=arguments[r-1];Z(function(){L(r,i,a,o)})}else j(n,i,t,e);return i},"catch":function(t){return this.then(null,t)}};var vt=F,yt={Promise:_t,polyfill:vt};n(201).amd?(r=function(){return yt}.call(e,n,e,o),!(void 0!==r&&(o.exports=r))):"undefined"!=typeof o&&o.exports?o.exports=yt:"undefined"!=typeof this&&(this.ES6Promise=yt),vt()}).call(this)}).call(e,n(202),function(){return this}(),n(65)(t))},function(t,e,n){var r=n(60),i=r(Date,"now"),o=i||function(){return(new Date).getTime()};t.exports=o},function(t,e){function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e,n){var r=n(60),i=n(189),o=n(61),a="[object Array]",u=Object.prototype,s=u.toString,c=r(Array,"isArray"),l=c||function(t){return o(t)&&i(t.length)&&s.call(t)==a};t.exports=l},function(t,e,n){function r(t){return null==t?!1:i(t)?l.test(s.call(t)):o(t)&&a.test(t)}var i=n(62),o=n(61),a=/^\[object .+?Constructor\]$/,u=Object.prototype,s=Function.prototype.toString,c=u.hasOwnProperty,l=RegExp("^"+s.call(c).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");t.exports=r},function(t,e){function n(t){return function(e){return null==e?void 0:e[t]}}t.exports=n},function(t,e,n){var r=n(192),i=r("length");t.exports=i},function(t,e,n){function r(t){return null!=t&&o(i(t))}var i=n(193),o=n(197);t.exports=r},function(t,e){function n(t,e){return t="number"==typeof t||r.test(t)?+t:-1,e=null==e?i:e,t>-1&&t%1==0&&e>t}var r=/^\d+$/,i=9007199254740991;t.exports=n},function(t,e,n){function r(t,e,n){if(!a(n))return!1;var r=typeof e;if("number"==r?i(n)&&o(e,n.length):"string"==r&&e in n){var u=n[e];return t===t?t===u:u!==u}return!1}var i=n(194),o=n(195),a=n(198);t.exports=r},function(t,e){function n(t){return"number"==typeof t&&t>-1&&t%1==0&&r>=t}var r=9007199254740991;t.exports=n},function(t,e){function n(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}t.exports=n},function(t,e,n){function r(t,e,n){n&&i(t,e,n)&&(e=n=void 0),t=+t||0,n=null==n?1:+n||0,null==e?(e=t,t=0):e=+e||0;for(var r=-1,u=a(o((e-t)/(n||1)),0),s=Array(u);++r1)for(var n=1;n \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 78c348cb7b0..7def0c85efb 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 78c348cb7b0a60ba015e3b652e538155d3e94a11 +Subproject commit 7def0c85efbfe7a11a64560c21cb83059a5c7a3b From e6846e7eb9fc63b4713de567e5dd0a0e80a24e8c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 22:28:53 -0800 Subject: [PATCH 226/267] Convert asuswrt user/pass to strings --- homeassistant/components/device_tracker/asuswrt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index b90e1ee4448..472440d7307 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -58,8 +58,8 @@ class AsusWrtDeviceScanner(object): def __init__(self, config): self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] + self.username = str(config[CONF_USERNAME]) + self.password = str(config[CONF_PASSWORD]) self.lock = threading.Lock() From 058dba50cc51ce2b77621d0a22158e7257aaaca2 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Wed, 13 Jan 2016 08:46:45 +0100 Subject: [PATCH 227/267] Correct name using format instead of concatenation --- homeassistant/components/sensor/netatmo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 01d837c737d..6c29c41f3f5 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -91,8 +91,8 @@ class NetAtmoSensor(Entity): """ Implements a NetAtmo sensor. """ def __init__(self, netatmo_data, module_name, sensor_type): - self.client_name = 'NetAtmo' - self._name = module_name + '_' + SENSOR_TYPES[sensor_type][0] + self._name = "NetAtmo {} {}".format(module_name, + SENSOR_TYPES[sensor_type][0]) self.netatmo_data = netatmo_data self.module_name = module_name self.type = sensor_type @@ -102,7 +102,7 @@ class NetAtmoSensor(Entity): @property def name(self): - return '{} {}'.format(self.client_name, self._name) + return self._name @property def state(self): From 2a377a6125753e649308f070eccab6e8a2a693a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jan 2016 23:59:15 -0800 Subject: [PATCH 228/267] Refactor syslog component for Windows users --- homeassistant/components/notify/syslog.py | 82 ++++++++++++----------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/notify/syslog.py b/homeassistant/components/notify/syslog.py index 4ee9ead9152..56075a6dd09 100644 --- a/homeassistant/components/notify/syslog.py +++ b/homeassistant/components/notify/syslog.py @@ -7,59 +7,62 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.syslog/ """ import logging -import syslog from homeassistant.helpers import validate_config from homeassistant.components.notify import ( DOMAIN, ATTR_TITLE, BaseNotificationService) _LOGGER = logging.getLogger(__name__) -FACILITIES = {'kernel': syslog.LOG_KERN, - 'user': syslog.LOG_USER, - 'mail': syslog.LOG_MAIL, - 'daemon': syslog.LOG_DAEMON, - 'auth': syslog.LOG_KERN, - 'LPR': syslog.LOG_LPR, - 'news': syslog.LOG_NEWS, - 'uucp': syslog.LOG_UUCP, - 'cron': syslog.LOG_CRON, - 'syslog': syslog.LOG_SYSLOG, - 'local0': syslog.LOG_LOCAL0, - 'local1': syslog.LOG_LOCAL1, - 'local2': syslog.LOG_LOCAL2, - 'local3': syslog.LOG_LOCAL3, - 'local4': syslog.LOG_LOCAL4, - 'local5': syslog.LOG_LOCAL5, - 'local6': syslog.LOG_LOCAL6, - 'local7': syslog.LOG_LOCAL7} - -OPTIONS = {'pid': syslog.LOG_PID, - 'cons': syslog.LOG_CONS, - 'ndelay': syslog.LOG_NDELAY, - 'nowait': syslog.LOG_NOWAIT, - 'perror': syslog.LOG_PERROR} - -PRIORITIES = {5: syslog.LOG_EMERG, - 4: syslog.LOG_ALERT, - 3: syslog.LOG_CRIT, - 2: syslog.LOG_ERR, - 1: syslog.LOG_WARNING, - 0: syslog.LOG_NOTICE, - -1: syslog.LOG_INFO, - -2: syslog.LOG_DEBUG} def get_service(hass, config): - """ Get the mail notification service. """ - + """Get the syslog notification service.""" if not validate_config({DOMAIN: config}, {DOMAIN: ['facility', 'option', 'priority']}, _LOGGER): return None - _facility = FACILITIES.get(config['facility'], 40) - _option = OPTIONS.get(config['option'], 10) - _priority = PRIORITIES.get(config['priority'], -1) + import syslog + + _facility = { + 'kernel': syslog.LOG_KERN, + 'user': syslog.LOG_USER, + 'mail': syslog.LOG_MAIL, + 'daemon': syslog.LOG_DAEMON, + 'auth': syslog.LOG_KERN, + 'LPR': syslog.LOG_LPR, + 'news': syslog.LOG_NEWS, + 'uucp': syslog.LOG_UUCP, + 'cron': syslog.LOG_CRON, + 'syslog': syslog.LOG_SYSLOG, + 'local0': syslog.LOG_LOCAL0, + 'local1': syslog.LOG_LOCAL1, + 'local2': syslog.LOG_LOCAL2, + 'local3': syslog.LOG_LOCAL3, + 'local4': syslog.LOG_LOCAL4, + 'local5': syslog.LOG_LOCAL5, + 'local6': syslog.LOG_LOCAL6, + 'local7': syslog.LOG_LOCAL7, + }.get(config['facility'], 40) + + _option = { + 'pid': syslog.LOG_PID, + 'cons': syslog.LOG_CONS, + 'ndelay': syslog.LOG_NDELAY, + 'nowait': syslog.LOG_NOWAIT, + 'perror': syslog.LOG_PERROR + }.get(config['option'], 10) + + _priority = { + 5: syslog.LOG_EMERG, + 4: syslog.LOG_ALERT, + 3: syslog.LOG_CRIT, + 2: syslog.LOG_ERR, + 1: syslog.LOG_WARNING, + 0: syslog.LOG_NOTICE, + -1: syslog.LOG_INFO, + -2: syslog.LOG_DEBUG + }.get(config['priority'], -1) return SyslogNotificationService(_facility, _option, _priority) @@ -76,6 +79,7 @@ class SyslogNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """ Send a message to a user. """ + import syslog title = kwargs.get(ATTR_TITLE) From 58cee75c0ebd0b3adceb897daae4a887fe5712aa Mon Sep 17 00:00:00 2001 From: hydreliox Date: Wed, 13 Jan 2016 09:06:16 +0100 Subject: [PATCH 229/267] coverage and requirements updated --- .coveragerc | 1 + requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 272ace975c4..674516aaeba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -96,6 +96,7 @@ omit = homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py + homeassistant/components/sensor/netatmo.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/rpi_gpio.py diff --git a/requirements_all.txt b/requirements_all.txt index 1bc00b4c46d..289c1095258 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -137,6 +137,9 @@ eliqonline==1.0.11 # homeassistant.components.sensor.forecast python-forecastio==1.3.3 +# homeassistant.components.sensor.netatmo +https://github.com/HydrelioxGitHub/netatmo-api-python/archive/59d157d03db0aa167730044667591adea4457ca8.zip#lnetatmo==0.3.0.dev1 + # homeassistant.components.sensor.openweathermap pyowm==2.3.0 From 314d34a644317dd55175e1ed697f30c9d5d84f98 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Thu, 14 Jan 2016 03:00:51 +0100 Subject: [PATCH 230/267] Update library lnetatmo requirements Thanks to @rmkraus --- homeassistant/components/sensor/netatmo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 6c29c41f3f5..640d3519bdf 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -17,8 +17,8 @@ from homeassistant.util import Throttle REQUIREMENTS = [ 'https://github.com/HydrelioxGitHub/netatmo-api-python/archive/' - '59d157d03db0aa167730044667591adea4457ca8.zip' - '#lnetatmo==0.3.0.dev1'] + '43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip' + '#lnetatmo==0.4.0'] _LOGGER = logging.getLogger(__name__) From 4fc01631398bacbc559c86387f69d941fef0f7a3 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Wed, 13 Jan 2016 21:22:56 -0500 Subject: [PATCH 231/267] round min / max values for temperature In order for the polymer thermostat component to have sensible step values the min / max values have to be round numbers. The current code only does that for systems running in degrees C. For those of us in silly land that still function in degrees F, this causes some oddities in the UI. Always round mix / max values to make it good no matter what fundamental units we are in. --- homeassistant/components/thermostat/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index edfa22a7840..7610070b1f0 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -224,12 +224,12 @@ class ThermostatDevice(Entity): @property def min_temp(self): """ Return minimum temperature. """ - return convert(7, TEMP_CELCIUS, self.unit_of_measurement) + return round(convert(7, TEMP_CELCIUS, self.unit_of_measurement)) @property def max_temp(self): """ Return maxmum temperature. """ - return convert(35, TEMP_CELCIUS, self.unit_of_measurement) + return round(convert(35, TEMP_CELCIUS, self.unit_of_measurement)) def _convert(self, temp, round_dec=None): """ Convert temperature from this thermost into user preferred From 4dd558a42084eb1316c90b308438743905a2db55 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Thu, 14 Jan 2016 07:09:25 +0100 Subject: [PATCH 232/267] Update Requirements --- requirements_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 289c1095258..7b43b7a3f1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -138,7 +138,7 @@ eliqonline==1.0.11 python-forecastio==1.3.3 # homeassistant.components.sensor.netatmo -https://github.com/HydrelioxGitHub/netatmo-api-python/archive/59d157d03db0aa167730044667591adea4457ca8.zip#lnetatmo==0.3.0.dev1 +https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip#lnetatmo==0.4.0 # homeassistant.components.sensor.openweathermap pyowm==2.3.0 From 308969e6dded921d7a50ad35168aee253f407389 Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Wed, 13 Jan 2016 23:09:39 -0700 Subject: [PATCH 233/267] Adding in alarm.com control panel. --- .../alarm_control_panel/alarmdotcom.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/alarmdotcom.py diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py new file mode 100644 index 00000000000..0736f4c2a8e --- /dev/null +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -0,0 +1,97 @@ +""" +homeassistant.components.alarm_control_panel.alarmdotcom +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Interfaces with Verisure alarm control panel. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarmdotcom/ +""" +import logging + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +from homeassistant.const import ( + STATE_UNKNOWN, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) + +_LOGGER = logging.getLogger(__name__) + + +REQUIREMENTS = ['https://github.com/Xorso/pyalarmdotcom' + '/archive/0.0.7.zip' + '#pyalarmdotcom==0.0.7'] +DEFAULT_NAME = 'Alarm.com' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup Alarm.com control panel """ + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + if username is None or password is None: + _LOGGER.error('Must specify username and password!') + return False + + add_devices([AlarmDotCom(hass, + config.get('name', DEFAULT_NAME), + config.get('code'), + username, + password)]) + + +class AlarmDotCom(alarm.AlarmControlPanel): + """ Represents a Alarm.com status within HA """ + + def __init__(self, hass, name, code, username, password): + from pyalarmdotcom.pyalarmdotcom import Alarmdotcom + self._alarm = Alarmdotcom(username, password, timeout=10) + self._hass = hass + self._name = name + self._code = str(code) if code else None + + @property + def should_poll(self): + return True + + @property + def name(self): + return self._name + + @property + def code_format(self): + """ One or more characters if code is defined """ + return None if self._code is None else '.+' + + @property + def state(self): + if self._alarm.state == 'Disarmed': + return STATE_ALARM_DISARMED + elif self._alarm.state == 'Armed Stay': + return STATE_ALARM_ARMED_HOME + elif self._alarm.state == 'Armed Away': + return STATE_ALARM_ARMED_AWAY + else: + return STATE_UNKNOWN + + def alarm_disarm(self, code=None): + if not self._validate_code(code, 'arming home'): + return + self._alarm.disarm() + + def alarm_arm_home(self, code=None): + if not self._validate_code(code, 'arming home'): + return + self._alarm.arm_stay() + + def alarm_arm_away(self, code=None): + if not self._validate_code(code, 'arming home'): + return + self._alarm.arm_away() + + def _validate_code(self, code, state): + """ Validate given code. """ + check = self._code is None or code == self._code + if not check: + _LOGGER.warning('Wrong code entered for %s', state) + return check From 303cb8e3501fe21a25adb20f385d3c8e406706e3 Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Wed, 13 Jan 2016 23:18:52 -0700 Subject: [PATCH 234/267] Adding alarmdotcom to coveragerc --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 272ace975c4..cc2653a022e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,8 @@ omit = homeassistant/__main__.py # omit pieces of code that rely on external devices being present + homeassistant/components/alarm_control_panel/alarmdotcom.py + homeassistant/components/arduino.py homeassistant/components/*/arduino.py From 87cecd7e950e02ac768c1406f0667588e1192aef Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Wed, 13 Jan 2016 23:22:42 -0700 Subject: [PATCH 235/267] Adding to requirements_all --- requirements_all.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/requirements_all.txt b/requirements_all.txt index 1bc00b4c46d..369067ff3fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,6 +6,9 @@ pip>=7.0.0 vincenty==0.1.3 jinja2>=2.8 +# homeassistant.components.alarm_control_panel.alarmdotcom +https://github.com/Xorso/pyalarmdotcom/archive/0.0.7.zip#pyalarmdotcom==0.0.7 + # homeassistant.components.arduino PyMata==2.07a @@ -48,6 +51,9 @@ blinkstick==1.1.7 # homeassistant.components.light.hue phue==0.8 +# homeassistant.components.light.lifx +https://github.com/avaidyam/lazylights/archive/master.zip#lazylights==3.0.0 + # homeassistant.components.light.limitlessled limitlessled==1.0.0 From 7a2d049ce303cf374dfb6d4f5299ccb713b4170f Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Wed, 13 Jan 2016 23:33:19 -0700 Subject: [PATCH 236/267] Removing LIFX from requirements_all.txt --- requirements_all.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 369067ff3fc..780cb941dd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -51,9 +51,6 @@ blinkstick==1.1.7 # homeassistant.components.light.hue phue==0.8 -# homeassistant.components.light.lifx -https://github.com/avaidyam/lazylights/archive/master.zip#lazylights==3.0.0 - # homeassistant.components.light.limitlessled limitlessled==1.0.0 From bdd6bb7918e00b58c8d1641ed0b60cfb58e21cd3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jan 2016 23:51:29 -0800 Subject: [PATCH 237/267] Update frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 1517 ++++++++--------- .../www_static/home-assistant-polymer | 2 +- 3 files changed, 753 insertions(+), 768 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 67454d11974..bc2dc90bdf9 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "63d38b69fc6582e75f892abc140a893a" +VERSION = "fe71771b9b24b0fb72a56e775c3e1112" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 86e5daca0aa..da7c887cda3 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,4 +1,4 @@ -
\ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 7def0c85efb..0b99a5933c3 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 7def0c85efbfe7a11a64560c21cb83059a5c7a3b +Subproject commit 0b99a5933c35b88c3369e992426fff8bf450aa01 From f0af23a4f5ff7e80a06a1b6317b8180611e73581 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 14 Jan 2016 09:16:20 +0100 Subject: [PATCH 238/267] Add link to docs and update pressure unit --- homeassistant/components/sensor/netatmo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 640d3519bdf..d1830cd9811 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -1,10 +1,10 @@ """ homeassistant.components.sensor.netatmo -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ NetAtmo Weather Service service. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/... +https://home-assistant.io/components/sensor.netatmo/ """ import logging from datetime import timedelta @@ -25,7 +25,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'temperature': ['Temperature', TEMP_CELCIUS], 'co2': ['CO2', 'ppm'], - 'pressure': ['Pressure', 'mb'], + 'pressure': ['Pressure', 'mbar'], 'noise': ['Noise', 'dB'], 'humidity': ['Humidity', '%'] } From d867366be135c1d31040ebdd6175ac48fd9716bc Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Tue, 12 Jan 2016 21:15:14 -0500 Subject: [PATCH 239/267] add proliphix thermostat support The proliphix nt10e is an early network thermostat that supports an HTTP interface. This adds basic support for it to home-assistant (get / set heating setback). --- .coveragerc | 1 + .../components/thermostat/proliphix.py | 88 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 92 insertions(+) create mode 100644 homeassistant/components/thermostat/proliphix.py diff --git a/.coveragerc b/.coveragerc index 272ace975c4..f37af10f295 100644 --- a/.coveragerc +++ b/.coveragerc @@ -121,6 +121,7 @@ omit = homeassistant/components/thermostat/homematic.py homeassistant/components/thermostat/honeywell.py homeassistant/components/thermostat/nest.py + homeassistant/components/thermostat/proliphix.py homeassistant/components/thermostat/radiotherm.py diff --git a/homeassistant/components/thermostat/proliphix.py b/homeassistant/components/thermostat/proliphix.py new file mode 100644 index 00000000000..d9e9dbabdeb --- /dev/null +++ b/homeassistant/components/thermostat/proliphix.py @@ -0,0 +1,88 @@ +"""homeassistant.components.thermostat.proliphix +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Proliphix NT10e Thermostat is an ethernet connected thermostat. It +has a local HTTP interface that is based on get/set of OID values. A +complete collection of the API is available in this API doc: + +https://github.com/sdague/thermostat.rb/blob/master/docs/PDP_API_R1_11.pdf +""" + +from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, + STATE_IDLE, STATE_HEAT) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, + CONF_HOST, TEMP_FAHRENHEIT) + +REQUIREMENTS = ['proliphix==0.1.0'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the proliphix thermostats. """ + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + host = config.get(CONF_HOST) + + import proliphix + + pdp = proliphix.PDP(host, username, password) + + add_devices([ + ProliphixThermostat(pdp) + ]) + + +class ProliphixThermostat(ThermostatDevice): + """ Represents a Proliphix thermostat. """ + + def __init__(self, pdp): + self._pdp = pdp + # initial data + self._pdp.update() + self._name = self._pdp.name + + @property + def should_poll(self): + return True + + def update(self): + self._pdp.update() + + @property + def name(self): + """ Returns the name. """ + return self._name + + @property + def device_state_attributes(self): + return { + "fan": self._pdp.fan_state + } + + @property + def unit_of_measurement(self): + """ Returns the unit of measurement. """ + return TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """ Returns the current temperature. """ + return self._pdp.cur_temp + + @property + def target_temperature(self): + """ Returns the temperature we try to reach. """ + return self._pdp.setback_heat + + @property + def operation(self): + state = self._pdp.hvac_state + if state in (1, 2): + return STATE_IDLE + elif state == 3: + return STATE_HEAT + elif state == 6: + return STATE_COOL + + def set_temperature(self, temperature): + """ Set new target temperature. """ + self._pdp.setback_heat = temperature diff --git a/requirements_all.txt b/requirements_all.txt index 1bc00b4c46d..92350b3bd9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -190,6 +190,9 @@ evohomeclient==0.2.4 # homeassistant.components.thermostat.nest python-nest==2.6.0 +# homeassistant.components.thermostat.proliphix +proliphix==0.1.0 + # homeassistant.components.thermostat.radiotherm radiotherm==1.2 From 34f124190c76aaf5f9e25a436b048704c397f94b Mon Sep 17 00:00:00 2001 From: Daren Lord Date: Thu, 14 Jan 2016 06:29:12 -0700 Subject: [PATCH 240/267] Fixing pylint errors --- homeassistant/components/alarm_control_panel/alarmdotcom.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 0736f4c2a8e..d10afc429e6 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -40,6 +40,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password)]) +# pylint: disable=too-many-arguments, too-many-instance-attributes +# pylint: disable=abstract-method class AlarmDotCom(alarm.AlarmControlPanel): """ Represents a Alarm.com status within HA """ @@ -78,16 +80,19 @@ class AlarmDotCom(alarm.AlarmControlPanel): if not self._validate_code(code, 'arming home'): return self._alarm.disarm() + self.update_ha_state() def alarm_arm_home(self, code=None): if not self._validate_code(code, 'arming home'): return self._alarm.arm_stay() + self.update_ha_state() def alarm_arm_away(self, code=None): if not self._validate_code(code, 'arming home'): return self._alarm.arm_away() + self.update_ha_state() def _validate_code(self, code, state): """ Validate given code. """ From de5bee635914ff781a35d3c109d3372a39303ee8 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 14 Jan 2016 08:56:59 -0500 Subject: [PATCH 241/267] Updated python-wink version --- homeassistant/components/light/wink.py | 2 +- homeassistant/components/lock/wink.py | 2 +- homeassistant/components/sensor/wink.py | 2 +- homeassistant/components/switch/wink.py | 5 +++-- homeassistant/components/wink.py | 2 +- requirements_all.txt | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index b5066ca49f7..294f092c050 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.4.0'] +REQUIREMENTS = ['python-wink==0.4.1'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 10324c4e46e..6c457c722f9 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -11,7 +11,7 @@ import logging from homeassistant.components.lock import LockDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.4.0'] +REQUIREMENTS = ['python-wink==0.4.1'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 77b53e5e9af..8acc99f2d3d 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -11,7 +11,7 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED -REQUIREMENTS = ['python-wink==0.4.0'] +REQUIREMENTS = ['python-wink==0.4.1'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index f8335fc3c54..e01065c3cd4 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -11,7 +11,7 @@ import logging from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.4.0'] +REQUIREMENTS = ['python-wink==0.4.1'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -30,4 +30,5 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.set_bearer_token(token) add_devices(WinkToggleDevice(switch) for switch in pywink.get_switches()) - add_devices(WinkToggleDevice(switch) for switch in pywink.get_powerstrip_outlets()) + add_devices(WinkToggleDevice(switch) for switch in + pywink.get_powerstrip_outlets()) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 996d5ebb44f..a719c5613a4 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.4.0'] +REQUIREMENTS = ['python-wink==0.4.1'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" diff --git a/requirements_all.txt b/requirements_all.txt index ef7ab2a67ae..0457fbb0266 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -66,7 +66,7 @@ pyvera==0.2.2 # homeassistant.components.lock.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.4.0 +python-wink==0.4.1 # homeassistant.components.media_player.cast pychromecast==0.6.13 From 1dd99a6d5d896eb26233856b065ddcb930921363 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jan 2016 22:02:02 -0800 Subject: [PATCH 242/267] Update frontend --- homeassistant/components/frontend/version.py | 2 +- homeassistant/components/frontend/www_static/frontend.html | 1 + .../components/frontend/www_static/home-assistant-polymer | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index bc2dc90bdf9..e0eaf447f0a 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 = "fe71771b9b24b0fb72a56e775c3e1112" +VERSION = "ab1affef682040e0a7752b98b8ba7f52" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index da7c887cda3..7cef9a04419 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -3607,6 +3607,7 @@ case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return iron-image { border-radius: 50%; + background-color: #FFFFFF; } ha-state-icon { diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 0b99a5933c3..2b7d4ead1e8 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 0b99a5933c35b88c3369e992426fff8bf450aa01 +Subproject commit 2b7d4ead1e8ab05d0df7a20d3fe7a76d5791f94b From 475b631d9c6a86b2bdc2f272137bd65d940c8c2f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jan 2016 22:51:28 -0800 Subject: [PATCH 243/267] Initial version input_boolean --- homeassistant/components/input_boolean.py | 124 ++++++++++++++++++++++ tests/components/test_input_boolean.py | 95 +++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 homeassistant/components/input_boolean.py create mode 100644 tests/components/test_input_boolean.py diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py new file mode 100644 index 00000000000..27216782ba1 --- /dev/null +++ b/homeassistant/components/input_boolean.py @@ -0,0 +1,124 @@ +""" +Component to keep track of user controlled booleans for within automation. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/input_boolean/ +""" +import logging + +from homeassistant.const import ( + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.util import slugify + +DOMAIN = 'input_boolean' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +_LOGGER = logging.getLogger(__name__) + +CONF_NAME = "name" +CONF_INITIAL = "initial" +CONF_ICON = "icon" + + +def is_on(hass, entity_id): + """Test if input_boolean is True.""" + return hass.states.is_state(entity_id, STATE_ON) + + +def turn_on(hass, entity_id): + """Set input_boolean to True.""" + hass.services.call(DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}) + + +def turn_off(hass, entity_id): + """Set input_boolean to False.""" + hass.services.call(DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}) + + +def setup(hass, config): + """Set up input booleans.""" + if not isinstance(config.get(DOMAIN), dict): + _LOGGER.error('Expected %s config to be a dictionary', DOMAIN) + return False + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if object_id != slugify(object_id): + _LOGGER.warning("Found invalid key for boolean input: %s. " + "Use %s instead", object_id, slugify(object_id)) + continue + if not cfg: + cfg = {} + + name = cfg.get(CONF_NAME) + state = cfg.get(CONF_INITIAL, False) + icon = cfg.get(CONF_ICON) + + entities.append(InputBoolean(object_id, name, state, icon)) + + if not entities: + return False + + component.add_entities(entities) + + def toggle_service(service): + """Handle a calls to the input boolean services.""" + target_inputs = component.extract_from_service(service) + + for input_b in target_inputs: + if service.service == SERVICE_TURN_ON: + input_b.turn_on() + else: + input_b.turn_off() + + hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service) + hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service) + + return True + + +class InputBoolean(ToggleEntity): + """Represent a boolean input within Home Assistant.""" + + def __init__(self, object_id, name, state, icon): + """Initialize a boolean input.""" + self.entity_id = ENTITY_ID_FORMAT.format(object_id) + self._name = name + self._state = state + self._icon = icon + + @property + def should_poll(self): + """If entitiy should be polled.""" + return False + + @property + def name(self): + """Name of the boolean input.""" + return self._name + + @property + def icon(self): + """Icon to be used for this entity.""" + return self._icon + + @property + def is_on(self): + """True if entity is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the entity on.""" + self._state = True + self.update_ha_state() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + self._state = False + self.update_ha_state() diff --git a/tests/components/test_input_boolean.py b/tests/components/test_input_boolean.py new file mode 100644 index 00000000000..a7366d91fd3 --- /dev/null +++ b/tests/components/test_input_boolean.py @@ -0,0 +1,95 @@ +""" +tests.components.test_input_boolean +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests input_boolean component. +""" +# pylint: disable=too-many-public-methods,protected-access +import unittest + +from homeassistant.components import input_boolean +from homeassistant.const import ( + STATE_ON, STATE_OFF, ATTR_ICON, ATTR_FRIENDLY_NAME) + +from tests.common import get_test_home_assistant + + +class TestInputBoolean(unittest.TestCase): + """ Test the input boolean module. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_config(self): + """Test config.""" + self.assertFalse(input_boolean.setup(self.hass, { + 'input_boolean': { + 'test 1': None, + } + })) + + self.assertFalse(input_boolean.setup(self.hass, { + 'input_boolean': { + } + })) + + def test_methods(self): + """ Test is_on, turn_on, turn_off methods. """ + self.assertTrue(input_boolean.setup(self.hass, { + 'input_boolean': { + 'test_1': None, + } + })) + entity_id = 'input_boolean.test_1' + + self.assertFalse( + input_boolean.is_on(self.hass, entity_id)) + + input_boolean.turn_on(self.hass, entity_id) + + self.hass.pool.block_till_done() + + self.assertTrue( + input_boolean.is_on(self.hass, entity_id)) + + input_boolean.turn_off(self.hass, entity_id) + + self.hass.pool.block_till_done() + + self.assertFalse( + input_boolean.is_on(self.hass, entity_id)) + + def test_config_options(self): + count_start = len(self.hass.states.entity_ids()) + + self.assertTrue(input_boolean.setup(self.hass, { + 'input_boolean': { + 'test_1': None, + 'test_2': { + 'name': 'Hello World', + 'icon': 'work', + 'initial': True, + }, + }, + })) + + self.assertEqual(count_start + 2, len(self.hass.states.entity_ids())) + + state_1 = self.hass.states.get('input_boolean.test_1') + state_2 = self.hass.states.get('input_boolean.test_2') + + self.assertIsNotNone(state_1) + self.assertIsNotNone(state_2) + + self.assertEqual(STATE_OFF, state_1.state) + self.assertNotIn(ATTR_ICON, state_1.attributes) + self.assertNotIn(ATTR_FRIENDLY_NAME, state_1.attributes) + + self.assertEqual(STATE_ON, state_2.state) + self.assertEqual('Hello World', + state_2.attributes.get(ATTR_FRIENDLY_NAME)) + self.assertEqual('work', state_2.attributes.get(ATTR_ICON)) From 0a711922efd202e7472d40b48e5f3e0226374762 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jan 2016 23:19:08 -0800 Subject: [PATCH 244/267] Remove unnecessary instance variable --- homeassistant/components/script.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 3e13db66699..238bea7dd4c 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -81,7 +81,7 @@ def setup(hass, config): object_id) continue alias = cfg.get(CONF_ALIAS, object_id) - script = Script(hass, object_id, alias, cfg[CONF_SEQUENCE]) + script = Script(object_id, alias, cfg[CONF_SEQUENCE]) component.add_entities((script,)) hass.services.register(DOMAIN, object_id, service_handler) @@ -106,8 +106,7 @@ def setup(hass, config): class Script(ToggleEntity): """ Represents a script. """ # pylint: disable=too-many-instance-attributes - def __init__(self, hass, object_id, name, sequence): - self.hass = hass + def __init__(self, object_id, name, sequence): self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name self.sequence = sequence From 6b899ddc1db3b59bb2c26ae22c172b75a352d0bf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jan 2016 23:25:25 -0800 Subject: [PATCH 245/267] 100% test coverage for input_boolean --- homeassistant/components/input_boolean.py | 4 ++-- tests/components/test_input_boolean.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 27216782ba1..f86fffcaeb6 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -65,8 +65,6 @@ def setup(hass, config): if not entities: return False - component.add_entities(entities) - def toggle_service(service): """Handle a calls to the input boolean services.""" target_inputs = component.extract_from_service(service) @@ -80,6 +78,8 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_TURN_OFF, toggle_service) hass.services.register(DOMAIN, SERVICE_TURN_ON, toggle_service) + component.add_entities(entities) + return True diff --git a/tests/components/test_input_boolean.py b/tests/components/test_input_boolean.py index a7366d91fd3..ace91018ccd 100644 --- a/tests/components/test_input_boolean.py +++ b/tests/components/test_input_boolean.py @@ -26,14 +26,18 @@ class TestInputBoolean(unittest.TestCase): def test_config(self): """Test config.""" + self.assertFalse(input_boolean.setup(self.hass, { + 'input_boolean': None + })) + self.assertFalse(input_boolean.setup(self.hass, { 'input_boolean': { - 'test 1': None, } })) self.assertFalse(input_boolean.setup(self.hass, { 'input_boolean': { + 'name with space': None } })) From 5ee17ffc585884224136539496d53ab84e3ee890 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jan 2016 23:47:55 -0800 Subject: [PATCH 246/267] Update frontend --- homeassistant/components/frontend/version.py | 2 +- homeassistant/components/frontend/www_static/frontend.html | 4 ++-- .../components/frontend/www_static/home-assistant-polymer | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index e0eaf447f0a..b8a31e418ca 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 = "ab1affef682040e0a7752b98b8ba7f52" +VERSION = "1003c31441ec44b3db84b49980f736a7" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 7cef9a04419..1816b922342 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -5103,7 +5103,7 @@ case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return border-radius: 50%; }