From d9fc2a8bf6673c6aecf1fb617938878a0f214c7d Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Mon, 30 Nov 2015 17:00:28 +0100 Subject: [PATCH 001/117] 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 002/117] 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 003/117] 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 004/117] 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 005/117] 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 006/117] 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 007/117] 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 008/117] 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 009/117] 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 010/117] 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 011/117] 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 012/117] 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 013/117] 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 f071e3b4ac554524a0fa91aad5574ef0522d5cd8 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Fri, 18 Dec 2015 23:55:37 -0500 Subject: [PATCH 014/117] torque support --- .coveragerc | 1 + homeassistant/components/sensor/torque.py | 118 ++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 homeassistant/components/sensor/torque.py diff --git a/.coveragerc b/.coveragerc index d39169439d6..a1123c12be3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -101,6 +101,7 @@ omit = homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/temper.py homeassistant/components/sensor/time_date.py + homeassistant/components/sensor/torque.py homeassistant/components/sensor/transmission.py homeassistant/components/sensor/twitch.py homeassistant/components/sensor/worldclock.py diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py new file mode 100644 index 00000000000..11ccba6ca1f --- /dev/null +++ b/homeassistant/components/sensor/torque.py @@ -0,0 +1,118 @@ +""" +homeassistant.components.sensor.torque +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Get data from the Torque OBD application. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.torque/ +""" + +import re +from homeassistant.helpers.entity import Entity + + +DOMAIN = 'torque' +DEPENDENCIES = ['http'] +SENSOR_EMAIL_FIELD = 'eml' +DEFAULT_NAME = 'vehicle' +HTTP_RESPONSE = 'OK' +HTTP_VERB = 'GET' +ENTITY_NAME_FORMAT = '{0} {1}' + +API_PATH = r'/api/torque' +SENSOR_NAME_KEY = r'userFullName(\w+)' +SENSOR_UNIT_KEY = r'userUnit(\w+)' +SENSOR_VALUE_KEY = r'k(\w+)' + +PATH_MATCH = re.compile(API_PATH) +NAME_KEY = re.compile(SENSOR_NAME_KEY) +UNIT_KEY = re.compile(SENSOR_UNIT_KEY) +VALUE_KEY = re.compile(SENSOR_VALUE_KEY) + + +def decode(value): + """ Double-decode required. """ + return value.encode('raw_unicode_escape').decode('utf-8') + + +def convert_pid(value): + """ Convert pid from hex string to integer. """ + return int(value, 16) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Set up Torque platform. """ + + vehicle = config.get('name', DEFAULT_NAME) + email = config.get('email', None) + sensors = {} + + def _receive_data(handler, path_match, data): + """ Received data from Torque. """ + handler.write_json_message(HTTP_RESPONSE) + + if email is not None and email != data[SENSOR_EMAIL_FIELD]: + return + + names = {} + units = {} + for key in data: + is_name = NAME_KEY.match(key) + is_unit = UNIT_KEY.match(key) + is_value = VALUE_KEY.match(key) + + if is_name: + pid = convert_pid(is_name.group(1)) + names[pid] = decode(data[key]) + elif is_unit: + pid = convert_pid(is_unit.group(1)) + units[pid] = decode(data[key]) + elif is_value: + pid = convert_pid(is_value.group(1)) + if pid in sensors: + sensors[pid].on_update(data[key]) + + for pid in names: + if pid not in sensors: + sensors[pid] = TorqueSensor( + ENTITY_NAME_FORMAT.format(vehicle, names[pid]), + units.get(pid, None)) + add_devices([sensors[pid]]) + + hass.http.register_path(HTTP_VERB, PATH_MATCH, _receive_data) + return True + + +class TorqueSensor(Entity): + """ Represents a Torque sensor. """ + + def __init__(self, name, unit): + self._name = name + self._unit = unit + self._state = None + + @property + def name(self): + """ Returns the name of the sensor. """ + return self._name + + @property + def unit_of_measurement(self): + """ Returns the unit of measurement. """ + return self._unit + + @property + def state(self): + """ State of the sensor. """ + return self._state + + @property + def icon(self): + """ Sensor default icon. """ + return 'mdi:car' + + def on_update(self, value): + """ Receive an update. """ + self._state = value + self.update_ha_state() From 04316d9723db32dc6ac750c7edb8991dbc3e6cad Mon Sep 17 00:00:00 2001 From: happyleaves Date: Sun, 20 Dec 2015 11:20:40 -0500 Subject: [PATCH 015/117] http fixes --- homeassistant/components/sensor/torque.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/torque.py b/homeassistant/components/sensor/torque.py index 11ccba6ca1f..e123aa2d18c 100644 --- a/homeassistant/components/sensor/torque.py +++ b/homeassistant/components/sensor/torque.py @@ -8,6 +8,7 @@ https://home-assistant.io/components/sensor.torque/ """ import re +from homeassistant.const import HTTP_OK from homeassistant.helpers.entity import Entity @@ -15,16 +16,13 @@ DOMAIN = 'torque' DEPENDENCIES = ['http'] SENSOR_EMAIL_FIELD = 'eml' DEFAULT_NAME = 'vehicle' -HTTP_RESPONSE = 'OK' -HTTP_VERB = 'GET' ENTITY_NAME_FORMAT = '{0} {1}' -API_PATH = r'/api/torque' +API_PATH = '/api/torque' SENSOR_NAME_KEY = r'userFullName(\w+)' SENSOR_UNIT_KEY = r'userUnit(\w+)' SENSOR_VALUE_KEY = r'k(\w+)' -PATH_MATCH = re.compile(API_PATH) NAME_KEY = re.compile(SENSOR_NAME_KEY) UNIT_KEY = re.compile(SENSOR_UNIT_KEY) VALUE_KEY = re.compile(SENSOR_VALUE_KEY) @@ -50,7 +48,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): def _receive_data(handler, path_match, data): """ Received data from Torque. """ - handler.write_json_message(HTTP_RESPONSE) + handler.send_response(HTTP_OK) + handler.end_headers() if email is not None and email != data[SENSOR_EMAIL_FIELD]: return @@ -80,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): units.get(pid, None)) add_devices([sensors[pid]]) - hass.http.register_path(HTTP_VERB, PATH_MATCH, _receive_data) + hass.http.register_path('GET', API_PATH, _receive_data) return True From 901f63be0c35cfb631ffcda417a942c71e55c863 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Dec 2015 15:30:45 -0800 Subject: [PATCH 016/117] Version bump to 0.10 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 287ae7998d2..deda344f4da 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.dev0" +__version__ = "0.10.0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' From 35411cd57e52a3f0171451721c60371dd34c1975 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 20 Dec 2015 15:32:51 -0800 Subject: [PATCH 017/117] 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 018/117] 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 019/117] 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 020/117] 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 021/117] 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 022/117] 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 023/117] 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 024/117] 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 025/117] 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 026/117] 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 027/117] 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 028/117] 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 029/117] 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 030/117] 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 9876a2a0810e2730ff01ae41dc84efed9c0da7ca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Dec 2015 02:08:46 -0800 Subject: [PATCH 031/117] Fix Alexa bug if no value for slots --- homeassistant/components/alexa.py | 2 +- tests/components/test_alexa.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index c261cfd3f6a..0b06f3c9a79 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -116,7 +116,7 @@ class AlexaResponse(object): self.should_end_session = True if intent is not None and 'slots' in intent: self.variables = {key: value['value'] for key, value - in intent['slots'].items()} + in intent['slots'].items() if 'value' in value} else: self.variables = {} diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index 75aec2b087c..741cfff4bb8 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -149,6 +149,45 @@ class TestAlexa(unittest.TestCase): text = req.json().get('response', {}).get('outputSpeech', {}).get('text') self.assertEqual('You told us your sign is virgo.', text) + def test_intent_request_with_slots_but_no_value(self): + data = { + 'version': '1.0', + 'session': { + 'new': False, + 'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000', + 'application': { + 'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe' + }, + 'attributes': { + 'supportedHoroscopePeriods': { + 'daily': True, + 'weekly': False, + 'monthly': False + } + }, + 'user': { + 'userId': 'amzn1.account.AM3B00000000000000000000000' + } + }, + 'request': { + 'type': 'IntentRequest', + 'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000', + 'timestamp': '2015-05-13T12:34:56Z', + 'intent': { + 'name': 'GetZodiacHoroscopeIntent', + 'slots': { + 'ZodiacSign': { + 'name': 'ZodiacSign', + } + } + } + } + } + req = _req(data) + self.assertEqual(200, req.status_code) + text = req.json().get('response', {}).get('outputSpeech', {}).get('text') + self.assertEqual('You told us your sign is .', text) + def test_intent_request_without_slots(self): data = { 'version': '1.0', From 561a78bef3c40f23d898c3e428094198ef13e5e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 22 Dec 2015 02:19:55 -0800 Subject: [PATCH 032/117] Fix EntityComponent deadlock --- homeassistant/helpers/entity_component.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ec22181bf5a..4cf44737f90 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -113,12 +113,16 @@ class EntityComponent(object): def _update_entity_states(self, now): """ Update the states of all the entities. """ + with self.lock: + # We copy the entities because new entities might be detected + # during state update causing deadlocks. + entities = list(entity for entity in self.entities.values() + if entity.should_poll) + self.logger.info("Updating %s entities", self.domain) - with self.lock: - for entity in self.entities.values(): - if entity.should_poll: - entity.update_ha_state(True) + for entity in entities: + entity.update_ha_state(True) def _entity_discovered(self, service, info): """ Called when a entity is discovered. """ From edff53609fbad5e2601bd805657dbe4055837c88 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Tue, 22 Dec 2015 13:50:59 -0600 Subject: [PATCH 033/117] 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 034/117] 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 035/117] 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 036/117] 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 037/117] 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 038/117] 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 039/117] 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 040/117] 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 041/117] 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 042/117] 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 043/117] 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 2606e4d641d0f16e48fd4c2321adc6a918300e0e Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 24 Dec 2015 00:38:49 -0700 Subject: [PATCH 044/117] 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 045/117] 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 046/117] 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 047/117] 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 048/117] 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 049/117] 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 050/117] 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 051/117] 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 052/117] 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 053/117] 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 054/117] 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 055/117] 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 056/117] 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 057/117] 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 058/117] 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 059/117] 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 060/117] 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 061/117] 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 062/117] 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 063/117] 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 064/117] 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 065/117] 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 066/117] 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 067/117] 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 068/117] 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 069/117] 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 070/117] 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 071/117] 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 072/117] 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 073/117] 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 074/117] 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 075/117] 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 076/117] 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 077/117] 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 078/117] 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 079/117] 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 080/117] 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 081/117] 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 082/117] 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 083/117] 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 084/117] 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 085/117] 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 086/117] 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 087/117] 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 088/117] 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 089/117] 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 090/117] 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 091/117] 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 092/117] 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 093/117] 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 094/117] 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 095/117] 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 096/117] 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 097/117] 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 098/117] 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 099/117] 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 c23375a18b1793dfe0e9066763c85bf45cbf255c Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 30 Dec 2015 17:30:20 -0700 Subject: [PATCH 100/117] 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 101/117] 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 102/117] 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 103/117] 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 104/117] 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 105/117] 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 106/117] 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 107/117] 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 108/117] 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 109/117] 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 110/117] 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 111/117] 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 112/117] 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 113/117] 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 114/117] 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 115/117] 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 116/117] 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 a36ae4b24af3d4903203abe206aebfbc2734be2c Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Sat, 2 Jan 2016 01:01:11 -0500 Subject: [PATCH 117/117] 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():