From 892573e53ed2db6d619730fd5f25c74bd817fd4a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 23 Jun 2015 12:34:55 +0200 Subject: [PATCH 001/111] remove unused stuff and update the names (same as in owm sensor) --- homeassistant/components/sensor/forecast.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 98e088d5139..bcc5cd3b050 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.forecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Forecast.io service. Configuration: @@ -113,10 +112,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-few-public-methods class ForeCastSensor(Entity): - """ Implements an OpenWeatherMap sensor. """ + """ Implements an Forecast.io sensor. """ def __init__(self, weather_data, sensor_type, unit): - self.client_name = 'Forecast' + self.client_name = 'Weather' self._name = SENSOR_TYPES[sensor_type][0] self.forecast_client = weather_data self._unit = unit @@ -127,7 +126,7 @@ class ForeCastSensor(Entity): @property def name(self): - return '{} - {}'.format(self.client_name, self._name) + return '{} {}'.format(self.client_name, self._name) @property def state(self): @@ -149,10 +148,6 @@ class ForeCastSensor(Entity): try: if self.type == 'summary': self._state = data.summary - # elif self.type == 'sunrise_time': - # self._state = data.sunriseTime - # elif self.type == 'sunset_time': - # self._state = data.sunsetTime elif self.type == 'precip_intensity': if data.precipIntensity == 0: self._state = 'None' From db5060b32374c24cbe6e41b7405a16166fde0ecf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 23 Jun 2015 12:34:55 +0200 Subject: [PATCH 002/111] remove unused stuff and update the names (same as in owm sensor) --- homeassistant/components/sensor/forecast.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 98e088d5139..bcc5cd3b050 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.forecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Forecast.io service. Configuration: @@ -113,10 +112,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-few-public-methods class ForeCastSensor(Entity): - """ Implements an OpenWeatherMap sensor. """ + """ Implements an Forecast.io sensor. """ def __init__(self, weather_data, sensor_type, unit): - self.client_name = 'Forecast' + self.client_name = 'Weather' self._name = SENSOR_TYPES[sensor_type][0] self.forecast_client = weather_data self._unit = unit @@ -127,7 +126,7 @@ class ForeCastSensor(Entity): @property def name(self): - return '{} - {}'.format(self.client_name, self._name) + return '{} {}'.format(self.client_name, self._name) @property def state(self): @@ -149,10 +148,6 @@ class ForeCastSensor(Entity): try: if self.type == 'summary': self._state = data.summary - # elif self.type == 'sunrise_time': - # self._state = data.sunriseTime - # elif self.type == 'sunset_time': - # self._state = data.sunsetTime elif self.type == 'precip_intensity': if data.precipIntensity == 0: self._state = 'None' From a34742040c5622216c01797307cd8db2a710b0fe Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 23 Jun 2015 12:34:55 +0200 Subject: [PATCH 003/111] remove unused stuff and update the names (same as in owm sensor) --- homeassistant/components/sensor/forecast.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 98e088d5139..bcc5cd3b050 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.forecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Forecast.io service. Configuration: @@ -113,10 +112,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-few-public-methods class ForeCastSensor(Entity): - """ Implements an OpenWeatherMap sensor. """ + """ Implements an Forecast.io sensor. """ def __init__(self, weather_data, sensor_type, unit): - self.client_name = 'Forecast' + self.client_name = 'Weather' self._name = SENSOR_TYPES[sensor_type][0] self.forecast_client = weather_data self._unit = unit @@ -127,7 +126,7 @@ class ForeCastSensor(Entity): @property def name(self): - return '{} - {}'.format(self.client_name, self._name) + return '{} {}'.format(self.client_name, self._name) @property def state(self): @@ -149,10 +148,6 @@ class ForeCastSensor(Entity): try: if self.type == 'summary': self._state = data.summary - # elif self.type == 'sunrise_time': - # self._state = data.sunriseTime - # elif self.type == 'sunset_time': - # self._state = data.sunsetTime elif self.type == 'precip_intensity': if data.precipIntensity == 0: self._state = 'None' From 4edf53899d7fce057e8c85f2105cacfc34844098 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jul 2015 23:21:01 -0700 Subject: [PATCH 004/111] Port PyMySensors from external to requirements.txt --- .gitmodules | 3 --- homeassistant/components/sensor/mysensors.py | 28 +++++++++++--------- homeassistant/external/pymysensors | 1 - requirements.txt | 4 +-- 4 files changed, 17 insertions(+), 19 deletions(-) delete mode 160000 homeassistant/external/pymysensors diff --git a/.gitmodules b/.gitmodules index 174bba680f0..4ecccece153 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,9 +16,6 @@ [submodule "homeassistant/external/nzbclients"] path = homeassistant/external/nzbclients url = https://github.com/jamespcole/home-assistant-nzb-clients.git -[submodule "homeassistant/external/pymysensors"] - path = homeassistant/external/pymysensors - url = https://github.com/theolind/pymysensors [submodule "homeassistant/components/frontend/www_static/home-assistant-polymer"] path = homeassistant/components/frontend/www_static/home-assistant-polymer url = https://github.com/balloob/home-assistant-polymer.git diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 4f3c2610c5a..ad9649f9966 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -21,9 +21,6 @@ Port of your connection to your MySensors device. """ import logging -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.pymysensors.mysensors.mysensors as mysensors -import homeassistant.external.pymysensors.mysensors.const as const from homeassistant.helpers.entity import Entity from homeassistant.const import ( @@ -39,12 +36,16 @@ ATTR_NODE_ID = "node_id" ATTR_CHILD_ID = "child_id" _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pyserial>=2.7'] +REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/master.zip' + '#egg=pymysensors-0.1'] def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup the mysensors platform. """ + import mysensors.mysensors as mysensors + import mysensors.const as const + devices = {} # keep track of devices added to HA # Just assume celcius means that the user wants metric for now. # It may make more sense to make this a global config option in the future. @@ -69,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id) node[child_id][value_type] = \ MySensorsNodeValue( - nid, child_id, name, value_type, is_metric) + nid, child_id, name, value_type, is_metric, const) new_devices.append(node[child_id][value_type]) else: node[child_id][value_type].update_sensor( @@ -102,8 +103,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MySensorsNodeValue(Entity): """ Represents the value of a MySensors child node. """ - # pylint: disable=too-many-arguments - def __init__(self, node_id, child_id, name, value_type, metric): + # pylint: disable=too-many-arguments, too-many-instance-attributes + def __init__(self, node_id, child_id, name, value_type, metric, const): self._name = name self.node_id = node_id self.child_id = child_id @@ -111,6 +112,7 @@ class MySensorsNodeValue(Entity): self.value_type = value_type self.metric = metric self._value = '' + self.const = const @property def should_poll(self): @@ -130,11 +132,11 @@ class MySensorsNodeValue(Entity): @property def unit_of_measurement(self): """ Unit of measurement of this entity. """ - if self.value_type == const.SetReq.V_TEMP: + if self.value_type == self.const.SetReq.V_TEMP: return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT - elif self.value_type == const.SetReq.V_HUM or \ - self.value_type == const.SetReq.V_DIMMER or \ - self.value_type == const.SetReq.V_LIGHT_LEVEL: + elif self.value_type == self.const.SetReq.V_HUM or \ + self.value_type == self.const.SetReq.V_DIMMER or \ + self.value_type == self.const.SetReq.V_LIGHT_LEVEL: return '%' return None @@ -150,8 +152,8 @@ class MySensorsNodeValue(Entity): def update_sensor(self, value, battery_level): """ Update a sensor with the latest value from the controller. """ _LOGGER.info("%s value = %s", self._name, value) - if self.value_type == const.SetReq.V_TRIPPED or \ - self.value_type == const.SetReq.V_ARMED: + if self.value_type == self.const.SetReq.V_TRIPPED or \ + self.value_type == self.const.SetReq.V_ARMED: self._value = STATE_ON if int(value) == 1 else STATE_OFF else: self._value = value diff --git a/homeassistant/external/pymysensors b/homeassistant/external/pymysensors deleted file mode 160000 index cd5ef892eee..00000000000 --- a/homeassistant/external/pymysensors +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cd5ef892eeec0ad027727f7e8f757e7f2927da97 diff --git a/requirements.txt b/requirements.txt index c7a569fad33..f9d11975e97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -77,5 +77,5 @@ python-forecastio>=1.3.3 # Firmata Bindings (*.arduino) PyMata==2.07a -# Mysensors serial gateway -pyserial>=2.7 +# Mysensors +https://github.com/theolind/pymysensors/archive/master.zip#egg=pymysensors-0.1 From 43cc3624eeab5a53c48decb127cbe829e75e6722 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 19 Jul 2015 23:44:32 -0700 Subject: [PATCH 005/111] Port PyNetgear from external to requirements.txt --- .gitmodules | 3 --- .../components/device_tracker/netgear.py | 17 +++-------------- homeassistant/external/pynetgear | 1 - requirements.txt | 3 +++ 4 files changed, 6 insertions(+), 18 deletions(-) delete mode 160000 homeassistant/external/pynetgear diff --git a/.gitmodules b/.gitmodules index 4ecccece153..b4ab8e17c0a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "homeassistant/external/pynetgear"] - path = homeassistant/external/pynetgear - url = https://github.com/balloob/pynetgear.git [submodule "homeassistant/external/pywemo"] path = homeassistant/external/pywemo url = https://github.com/balloob/pywemo.git diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 102bc78ff47..3fe11f99fe6 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -43,6 +43,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pynetgear>=0.1'] def get_scanner(hass, config): @@ -64,22 +65,10 @@ class NetgearDeviceScanner(object): """ This class queries a Netgear wireless router using the SOAP-API. """ def __init__(self, host, username, password): + import pynetgear + self.last_results = [] - try: - # Pylint does not play nice if not every folders has an __init__.py - # pylint: disable=no-name-in-module, import-error - import homeassistant.external.pynetgear.pynetgear as pynetgear - except ImportError: - _LOGGER.exception( - ("Failed to import pynetgear. " - "Did you maybe not run `git submodule init` " - "and `git submodule update`?")) - - self.success_init = False - - return - self._api = pynetgear.Netgear(host, username, password) self.lock = threading.Lock() diff --git a/homeassistant/external/pynetgear b/homeassistant/external/pynetgear deleted file mode 160000 index e946ecf7926..00000000000 --- a/homeassistant/external/pynetgear +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e946ecf7926b9b2adaa1e3127a9738201a1b1fc7 diff --git a/requirements.txt b/requirements.txt index f9d11975e97..f70d755d5d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -79,3 +79,6 @@ PyMata==2.07a # Mysensors https://github.com/theolind/pymysensors/archive/master.zip#egg=pymysensors-0.1 + +# Netgear (device_tracker.netgear) +pynetgear>=0.1 From 3efb1e4ac9a647922e6eacea9eac32823480aadc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Jul 2015 00:07:01 -0700 Subject: [PATCH 006/111] Port netdisco from external to requirements.txt --- .gitmodules | 3 --- homeassistant/components/discovery.py | 26 ++++++++----------- homeassistant/components/light/__init__.py | 2 +- .../components/media_player/__init__.py | 2 +- homeassistant/components/switch/__init__.py | 2 +- homeassistant/external/netdisco | 1 - requirements.txt | 6 ++--- 7 files changed, 17 insertions(+), 25 deletions(-) delete mode 160000 homeassistant/external/netdisco diff --git a/.gitmodules b/.gitmodules index b4ab8e17c0a..f33d9430f54 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "homeassistant/external/pywemo"] path = homeassistant/external/pywemo url = https://github.com/balloob/pywemo.git -[submodule "homeassistant/external/netdisco"] - path = homeassistant/external/netdisco - url = https://github.com/balloob/netdisco.git [submodule "homeassistant/external/noop"] path = homeassistant/external/noop url = https://github.com/balloob/noop.git diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 63c9a0af74f..0aa7312bfd7 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -12,9 +12,6 @@ loaded before the EVENT_PLATFORM_DISCOVERED is fired. import logging import threading -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.netdisco.netdisco.const as services - from homeassistant import bootstrap from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_PLATFORM_DISCOVERED, @@ -22,14 +19,20 @@ from homeassistant.const import ( DOMAIN = "discovery" DEPENDENCIES = [] -REQUIREMENTS = ['zeroconf>=0.16.0'] +REQUIREMENTS = ['netdisco>=0.1'] SCAN_INTERVAL = 300 # seconds +# Next 3 lines for now a mirror from netdisco.const +# Should setup a mapping netdisco.const -> own constants +SERVICE_WEMO = 'belkin_wemo' +SERVICE_HUE = 'philips_hue' +SERVICE_CAST = 'google_cast' + SERVICE_HANDLERS = { - services.BELKIN_WEMO: "switch", - services.GOOGLE_CAST: "media_player", - services.PHILIPS_HUE: "light", + SERVICE_WEMO: "switch", + SERVICE_CAST: "media_player", + SERVICE_HUE: "light", } @@ -56,14 +59,7 @@ def setup(hass, config): """ Starts a discovery service. """ logger = logging.getLogger(__name__) - try: - from homeassistant.external.netdisco.netdisco.service import \ - DiscoveryService - except ImportError: - logger.exception( - "Unable to import netdisco. " - "Did you install all the zeroconf dependency?") - return False + from netdisco.service import DiscoveryService # Disable zeroconf logging, it spams logging.getLogger('zeroconf').setLevel(logging.CRITICAL) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 58c3c3cd8f2..35e80cc143d 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -99,7 +99,7 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" DISCOVERY_PLATFORMS = { wink.DISCOVER_LIGHTS: 'wink', isy994.DISCOVER_LIGHTS: 'isy994', - discovery.services.PHILIPS_HUE: 'hue', + discovery.SERVICE_HUE: 'hue', } PROP_TO_ATTR = { diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 0cb4608ba1b..8cd3353bea8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -24,7 +24,7 @@ SCAN_INTERVAL = 30 ENTITY_ID_FORMAT = DOMAIN + '.{}' DISCOVERY_PLATFORMS = { - discovery.services.GOOGLE_CAST: 'cast', + discovery.SERVICE_CAST: 'cast', } SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 95457b66f5f..b141c36f7b5 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -29,7 +29,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) # Maps discovered services to their platforms DISCOVERY_PLATFORMS = { - discovery.services.BELKIN_WEMO: 'wemo', + discovery.SERVICE_WEMO: 'wemo', wink.DISCOVER_SWITCHES: 'wink', isy994.DISCOVER_SWITCHES: 'isy994', } diff --git a/homeassistant/external/netdisco b/homeassistant/external/netdisco deleted file mode 160000 index b2cad7c2b95..00000000000 --- a/homeassistant/external/netdisco +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b2cad7c2b959efa8eee9b5ac62d87232bf0b5176 diff --git a/requirements.txt b/requirements.txt index f70d755d5d3..e601614bb96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,9 +5,6 @@ pytz>=2015.2 # Optional, needed for specific components -# Discovery platform (discovery) -zeroconf>=0.16.0 - # Sun (sun) astral>=0.8.1 @@ -82,3 +79,6 @@ https://github.com/theolind/pymysensors/archive/master.zip#egg=pymysensors-0.1 # Netgear (device_tracker.netgear) pynetgear>=0.1 + +# Netdisco (discovery) +netdisco>=0.1 From 2f622053a67cb868b1ea5217c86c469bd73b3d74 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Jul 2015 00:08:00 -0700 Subject: [PATCH 007/111] Port PyWemo from external to requirements.txt --- .gitmodules | 3 --- homeassistant/components/switch/wemo.py | 13 ++----------- homeassistant/external/pywemo | 1 - requirements.txt | 3 +++ 4 files changed, 5 insertions(+), 15 deletions(-) delete mode 160000 homeassistant/external/pywemo diff --git a/.gitmodules b/.gitmodules index f33d9430f54..a627e522d8f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "homeassistant/external/pywemo"] - path = homeassistant/external/pywemo - url = https://github.com/balloob/pywemo.git [submodule "homeassistant/external/noop"] path = homeassistant/external/noop url = https://github.com/balloob/noop.git diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index eb55e0662b7..33f5f03799b 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -12,17 +12,8 @@ from homeassistant.components.switch import SwitchDevice # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return WeMo switches. """ - try: - # pylint: disable=no-name-in-module, import-error - import homeassistant.external.pywemo.pywemo as pywemo - import homeassistant.external.pywemo.pywemo.discovery as discovery - except ImportError: - logging.getLogger(__name__).exception(( - "Failed to import pywemo. " - "Did you maybe not run `git submodule init` " - "and `git submodule update`?")) - - return + import pywemo + import pywemo.discovery as discovery if discovery_info is not None: device = discovery.device_from_description(discovery_info) diff --git a/homeassistant/external/pywemo b/homeassistant/external/pywemo deleted file mode 160000 index ca94e41faa4..00000000000 --- a/homeassistant/external/pywemo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ca94e41faa48c783f600a2efd550c6b7dae01b0d diff --git a/requirements.txt b/requirements.txt index e601614bb96..e910ffba95f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -82,3 +82,6 @@ pynetgear>=0.1 # Netdisco (discovery) netdisco>=0.1 + +# Wemo (switch.wemo) +pywemo>=0.1 From 40b2acb472e2ef623e66c16c7e8c0b38050f23a1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Jul 2015 00:41:57 -0700 Subject: [PATCH 008/111] Port wink from external to requirements.txt --- homeassistant/components/light/wink.py | 7 +- homeassistant/components/sensor/wink.py | 5 +- homeassistant/components/switch/wink.py | 5 +- homeassistant/components/wink.py | 6 +- homeassistant/external/wink/pywink.py | 408 ------------------------ requirements.txt | 3 + 6 files changed, 13 insertions(+), 421 deletions(-) delete mode 100644 homeassistant/external/wink/pywink.py diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index dc7b7041611..c4a47ca7da1 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -1,9 +1,6 @@ -""" Support for Hue lights. """ +""" Support for Wink lights. """ import logging -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.wink.pywink as pywink - from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN @@ -11,6 +8,8 @@ from homeassistant.const import CONF_ACCESS_TOKEN def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Wink lights. """ + import pywink + token = config.get(CONF_ACCESS_TOKEN) if not pywink.is_token_set() and token is None: diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index ff61f02d041..1d52550aa37 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -1,15 +1,14 @@ """ Support for Wink sensors. """ import logging -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.wink.pywink as pywink - from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Wink platform. """ + import pywink + if discovery_info is None: token = config.get(CONF_ACCESS_TOKEN) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index add56d222b1..792de50a855 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -6,15 +6,14 @@ Support for Wink switches. """ import logging -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.wink.pywink as pywink - from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Wink platform. """ + import pywink + if discovery_info is None: token = config.get(CONF_ACCESS_TOKEN) diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index dd210bc2b7f..d56a244b84c 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -6,9 +6,6 @@ Connects to a Wink hub and loads relevant components to control its devices. """ import logging -# pylint: disable=no-name-in-module, import-error -import homeassistant.external.wink.pywink as pywink - from homeassistant import bootstrap from homeassistant.loader import get_component from homeassistant.helpers import validate_config @@ -19,6 +16,8 @@ from homeassistant.const import ( DOMAIN = "wink" DEPENDENCIES = [] +REQUIREMENTS = ['https://github.com/balloob/python-wink/archive/master.zip' + '#pywink>=0.1'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" @@ -32,6 +31,7 @@ def setup(hass, config): if not validate_config(config, {DOMAIN: [CONF_ACCESS_TOKEN]}, logger): return False + import pywink pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN]) # Load components for the devices in the Wink that we support diff --git a/homeassistant/external/wink/pywink.py b/homeassistant/external/wink/pywink.py deleted file mode 100644 index cbe50701707..00000000000 --- a/homeassistant/external/wink/pywink.py +++ /dev/null @@ -1,408 +0,0 @@ -__author__ = 'JOHNMCL' - -import json -import time - -import requests - -baseUrl = "https://winkapi.quirky.com" - -headers = {} - - -class wink_sensor_pod(object): - """ represents a wink.py sensor - json_obj holds the json stat at init (and if there is a refresh it's updated - it's the native format for this objects methods - and looks like so: -{ - "data": { - "last_event": { - "brightness_occurred_at": None, - "loudness_occurred_at": None, - "vibration_occurred_at": None - }, - "model_name": "Tripper", - "capabilities": { - "sensor_types": [ - { - "field": "opened", - "type": "boolean" - }, - { - "field": "battery", - "type": "percentage" - } - ] - }, - "manufacturer_device_model": "quirky_ge_tripper", - "location": "", - "radio_type": "zigbee", - "manufacturer_device_id": None, - "gang_id": None, - "sensor_pod_id": "37614", - "subscription": { - }, - "units": { - }, - "upc_id": "184", - "hidden_at": None, - "last_reading": { - "battery_voltage_threshold_2": 0, - "opened": False, - "battery_alarm_mask": 0, - "opened_updated_at": 1421697092.7347496, - "battery_voltage_min_threshold_updated_at": 1421697092.7347229, - "battery_voltage_min_threshold": 0, - "connection": None, - "battery_voltage": 25, - "battery_voltage_threshold_1": 25, - "connection_updated_at": None, - "battery_voltage_threshold_3": 0, - "battery_voltage_updated_at": 1421697092.7347066, - "battery_voltage_threshold_1_updated_at": 1421697092.7347302, - "battery_voltage_threshold_3_updated_at": 1421697092.7347434, - "battery_voltage_threshold_2_updated_at": 1421697092.7347374, - "battery": 1.0, - "battery_updated_at": 1421697092.7347553, - "battery_alarm_mask_updated_at": 1421697092.734716 - }, - "triggers": [ - ], - "name": "MasterBathroom", - "lat_lng": [ - 37.550773, - -122.279182 - ], - "uuid": "a2cb868a-dda3-4211-ab73-fc08087aeed7", - "locale": "en_us", - "device_manufacturer": "quirky_ge", - "created_at": 1421523277, - "local_id": "2", - "hub_id": "88264" - }, -} - - """ - def __init__(self, aJSonObj, objectprefix="sensor_pods"): - self.jsonState = aJSonObj - self.objectprefix = objectprefix - - def __str__(self): - return "%s %s %s" % (self.name(), self.deviceId(), self.state()) - - def __repr__(self): - return "" % (self.name(), self.deviceId(), self.state()) - - @property - def _last_reading(self): - return self.jsonState.get('last_reading') or {} - - def name(self): - return self.jsonState.get('name', "Unknown Name") - - def state(self): - return self._last_reading.get('opened', False) - - def deviceId(self): - return self.jsonState.get('sensor_pod_id', self.name()) - - def refresh_state_at_hub(self): - """ - Tell hub to query latest status from device and upload to Wink. - PS: Not sure if this even works.. - """ - urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId()) - requests.get(urlString, headers=headers) - - def updateState(self): - """ Update state with latest info from Wink API. """ - urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) - arequest = requests.get(urlString, headers=headers) - self._updateStateFromResponse(arequest.json()) - - def _updateStateFromResponse(self, response_json): - """ - :param response_json: the json obj returned from query - :return: - """ - self.jsonState = response_json.get('data') - -class wink_binary_switch(object): - """ represents a wink.py switch - json_obj holds the json stat at init (and if there is a refresh it's updated - it's the native format for this objects methods - and looks like so: - -{ - "data": { - "binary_switch_id": "4153", - "name": "Garage door indicator", - "locale": "en_us", - "units": {}, - "created_at": 1411614982, - "hidden_at": null, - "capabilities": {}, - "subscription": {}, - "triggers": [], - "desired_state": { - "powered": false - }, - "manufacturer_device_model": "leviton_dzs15", - "manufacturer_device_id": null, - "device_manufacturer": "leviton", - "model_name": "Switch", - "upc_id": "94", - "gang_id": null, - "hub_id": "11780", - "local_id": "9", - "radio_type": "zwave", - "last_reading": { - "powered": false, - "powered_updated_at": 1411614983.6153464, - "powering_mode": null, - "powering_mode_updated_at": null, - "consumption": null, - "consumption_updated_at": null, - "cost": null, - "cost_updated_at": null, - "budget_percentage": null, - "budget_percentage_updated_at": null, - "budget_velocity": null, - "budget_velocity_updated_at": null, - "summation_delivered": null, - "summation_delivered_updated_at": null, - "sum_delivered_multiplier": null, - "sum_delivered_multiplier_updated_at": null, - "sum_delivered_divisor": null, - "sum_delivered_divisor_updated_at": null, - "sum_delivered_formatting": null, - "sum_delivered_formatting_updated_at": null, - "sum_unit_of_measure": null, - "sum_unit_of_measure_updated_at": null, - "desired_powered": false, - "desired_powered_updated_at": 1417893563.7567682, - "desired_powering_mode": null, - "desired_powering_mode_updated_at": null - }, - "current_budget": null, - "lat_lng": [ - 38.429996, - -122.653721 - ], - "location": "", - "order": 0 - }, - "errors": [], - "pagination": {} -} - - """ - def __init__(self, aJSonObj, objectprefix="binary_switches"): - self.jsonState = aJSonObj - self.objectprefix = objectprefix - # Tuple (desired state, time) - self._last_call = (0, None) - - def __str__(self): - return "%s %s %s" % (self.name(), self.deviceId(), self.state()) - - def __repr__(self): - return "" % (self.name(), self.deviceId(), self.state()) - - @property - def _last_reading(self): - return self.jsonState.get('last_reading') or {} - - def name(self): - return self.jsonState.get('name', "Unknown Name") - - def state(self): - # Optimistic approach to setState: - # Within 15 seconds of a call to setState we assume it worked. - if self._recent_state_set(): - return self._last_call[1] - - return self._last_reading.get('powered', False) - - def deviceId(self): - return self.jsonState.get('binary_switch_id', self.name()) - - def setState(self, state): - """ - :param state: a boolean of true (on) or false ('off') - :return: nothing - """ - urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) - values = {"desired_state": {"powered": state}} - arequest = requests.put(urlString, data=json.dumps(values), headers=headers) - self._updateStateFromResponse(arequest.json()) - - self._last_call = (time.time(), state) - - def refresh_state_at_hub(self): - """ - Tell hub to query latest status from device and upload to Wink. - PS: Not sure if this even works.. - """ - urlString = baseUrl + "/%s/%s/refresh" % (self.objectprefix, self.deviceId()) - requests.get(urlString, headers=headers) - - def updateState(self): - """ Update state with latest info from Wink API. """ - urlString = baseUrl + "/%s/%s" % (self.objectprefix, self.deviceId()) - arequest = requests.get(urlString, headers=headers) - self._updateStateFromResponse(arequest.json()) - - def wait_till_desired_reached(self): - """ Wait till desired state reached. Max 10s. """ - if self._recent_state_set(): - return - - # self.refresh_state_at_hub() - tries = 1 - - while True: - self.updateState() - last_read = self._last_reading - - if last_read.get('desired_powered') == last_read.get('powered') \ - or tries == 5: - break - - time.sleep(2) - - tries += 1 - self.updateState() - last_read = self._last_reading - - def _updateStateFromResponse(self, response_json): - """ - :param response_json: the json obj returned from query - :return: - """ - self.jsonState = response_json.get('data') - - def _recent_state_set(self): - return time.time() - self._last_call[0] < 15 - - -class wink_bulb(wink_binary_switch): - """ represents a wink.py bulb - json_obj holds the json stat at init (and if there is a refresh it's updated - it's the native format for this objects methods - and looks like so: - - "light_bulb_id": "33990", - "name": "downstaurs lamp", - "locale": "en_us", - "units":{}, - "created_at": 1410925804, - "hidden_at": null, - "capabilities":{}, - "subscription":{}, - "triggers":[], - "desired_state":{"powered": true, "brightness": 1}, - "manufacturer_device_model": "lutron_p_pkg1_w_wh_d", - "manufacturer_device_id": null, - "device_manufacturer": "lutron", - "model_name": "Caseta Wireless Dimmer & Pico", - "upc_id": "3", - "hub_id": "11780", - "local_id": "8", - "radio_type": "lutron", - "linked_service_id": null, - "last_reading":{ - "brightness": 1, - "brightness_updated_at": 1417823487.490747, - "connection": true, - "connection_updated_at": 1417823487.4907365, - "powered": true, - "powered_updated_at": 1417823487.4907532, - "desired_powered": true, - "desired_powered_updated_at": 1417823485.054675, - "desired_brightness": 1, - "desired_brightness_updated_at": 1417409293.2591703 - }, - "lat_lng":[38.429962, -122.653715], - "location": "", - "order": 0 - - """ - jsonState = {} - - def __init__(self, ajsonobj): - super().__init__(ajsonobj, "light_bulbs") - - def deviceId(self): - return self.jsonState.get('light_bulb_id', self.name()) - - def brightness(self): - return self._last_reading.get('brightness') - - def setState(self, state, brightness=None): - """ - :param state: a boolean of true (on) or false ('off') - :return: nothing - """ - urlString = baseUrl + "/light_bulbs/%s" % self.deviceId() - values = { - "desired_state": { - "powered": state - } - } - - if brightness is not None: - values["desired_state"]["brightness"] = brightness - - urlString = baseUrl + "/light_bulbs/%s" % self.deviceId() - arequest = requests.put(urlString, data=json.dumps(values), headers=headers) - self._updateStateFromResponse(arequest.json()) - - self._last_call = (time.time(), state) - - def __repr__(self): - return "" % ( - self.name(), self.deviceId(), self.state()) - - -def get_devices(filter, constructor): - arequestUrl = baseUrl + "/users/me/wink_devices" - j = requests.get(arequestUrl, headers=headers).json() - - items = j.get('data') - - devices = [] - for item in items: - id = item.get(filter) - if (id is not None and item.get("hidden_at") is None): - devices.append(constructor(item)) - - return devices - -def get_bulbs(): - return get_devices('light_bulb_id', wink_bulb) - -def get_switches(): - return get_devices('binary_switch_id', wink_binary_switch) - -def get_sensors(): - return get_devices('sensor_pod_id', wink_sensor_pod) - -def is_token_set(): - """ Returns if an auth token has been set. """ - return bool(headers) - - -def set_bearer_token(token): - global headers - - headers = { - "Content-Type": "application/json", - "Authorization": "Bearer {}".format(token) - } - -if __name__ == "__main__": - sw = get_bulbs() - lamp = sw[3] - lamp.setState(False) diff --git a/requirements.txt b/requirements.txt index e910ffba95f..e9365a63811 100644 --- a/requirements.txt +++ b/requirements.txt @@ -85,3 +85,6 @@ netdisco>=0.1 # Wemo (switch.wemo) pywemo>=0.1 + +# Wink (*.wink) +https://github.com/balloob/python-wink/archive/master.zip#pywink>=0.1 From 6631ebfdfa5a735f14058be8e76f57af807a555a Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Mon, 20 Jul 2015 20:16:54 -0700 Subject: [PATCH 009/111] - Added git submodule @ https://github.com/rkabadi/pyedimax - Added edimax.py module to interface with Edimax SP-1101W and SP-2101W --- .gitmodules | 3 ++ homeassistant/components/switch/edimax.py | 52 +++++++++++++++++++++++ homeassistant/external/pyedimax | 1 + 3 files changed, 56 insertions(+) create mode 100644 homeassistant/components/switch/edimax.py create mode 160000 homeassistant/external/pyedimax diff --git a/.gitmodules b/.gitmodules index ca0b1f024b8..5d994cf6ceb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "homeassistant/external/pymysensors"] path = homeassistant/external/pymysensors url = https://github.com/theolind/pymysensors +[submodule "homeassistant/external/pyedimax"] + path = homeassistant/external/pyedimax + url = https://github.com/rkabadi/pyedimax diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py new file mode 100644 index 00000000000..615b52ea80c --- /dev/null +++ b/homeassistant/components/switch/edimax.py @@ -0,0 +1,52 @@ +""" +homeassistant.components.switch.edimax +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support for Edimax switches. +""" +import logging + +from homeassistant.components.switch import SwitchDevice + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return Edimax Smart Plugs. """ + try: + # pylint: disable=no-name-in-module, import-error + from homeassistant.external.pyedimax.smartplug import SmartPlug + except ImportError: + logging.getLogger(__name__).exception(( + "Failed to import pyedimax. " + "Did you maybe not run `git submodule init` " + "and `git submodule update`?")) + + return + + + add_devices_callback([ + SmartPlugSwitch(SmartPlug( + host = config.get('host'), + auth=( + config.get('user', 'admin'), + config.get('password', '1234')))) + ]) + + +class SmartPlugSwitch(SwitchDevice): + """ Represents a Edimax Smart Plug switch within Home Assistant. """ + def __init__(self, smartplug): + self.smartplug = smartplug + + @property + def is_on(self): + """ True if switch is on. """ + return self.smartplug.get_state() + + def turn_on(self, **kwargs): + """ Turns the switch on. """ + self.smartplug.state = 'ON' + + def turn_off(self): + """ Turns the switch off. """ + self.smartplug.state = 'OFF' \ No newline at end of file diff --git a/homeassistant/external/pyedimax b/homeassistant/external/pyedimax new file mode 160000 index 00000000000..3815f3bd99f --- /dev/null +++ b/homeassistant/external/pyedimax @@ -0,0 +1 @@ +Subproject commit 3815f3bd99fb9dcd4d9e5e6fc58626f5873e43db From fac194f66cf35c256c234c9e26a8bdeee69919c0 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Mon, 20 Jul 2015 23:27:25 -0700 Subject: [PATCH 010/111] - Added for smartplug - Added error check for host param in config.yaml - Fixed SmartPlugSwitch is_on method - Edimax smartplug works now! --- homeassistant/components/switch/edimax.py | 26 +++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 615b52ea80c..3754b452a2d 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -7,7 +7,7 @@ Support for Edimax switches. import logging from homeassistant.components.switch import SwitchDevice - +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -23,14 +23,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): return + host = config.get(CONF_HOST) + auth=(config.get(CONF_USERNAME, 'admin'), + config.get(CONF_PASSWORD, '1234')) - add_devices_callback([ - SmartPlugSwitch(SmartPlug( - host = config.get('host'), - auth=( - config.get('user', 'admin'), - config.get('password', '1234')))) - ]) + if not host: + logging.getLogger(__name__).error('Missing config variable %s', CONF_HOST) + return False + + + add_devices_callback([SmartPlugSwitch(SmartPlug(host, auth))]) class SmartPlugSwitch(SwitchDevice): @@ -38,10 +40,16 @@ class SmartPlugSwitch(SwitchDevice): def __init__(self, smartplug): self.smartplug = smartplug + @property + def name(self): + """ Returns the name of the Smart Plug, if any. """ + #TODO: dynamically get name from device using requests + return 'Edimax Smart Plug' + @property def is_on(self): """ True if switch is on. """ - return self.smartplug.get_state() + return self.smartplug.state == 'ON' def turn_on(self, **kwargs): """ Turns the switch on. """ From cbb390a918f62f2bdef6a8cceda327f0024bb45e Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 23 Jul 2015 18:13:46 +0200 Subject: [PATCH 011/111] Custom min/max temperature for thermostat --- .../components/thermostat/__init__.py | 20 ++++++++++++++++++- .../components/thermostat/heat_control.py | 20 ++++++++++--------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 08940b977c9..dffa7dfcc26 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity_component import EntityComponent import homeassistant.util as util from homeassistant.helpers.entity import Entity from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF) + ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, TEMP_CELCIUS) DOMAIN = "thermostat" DEPENDENCIES = [] @@ -24,6 +24,8 @@ SERVICE_SET_TEMPERATURE = "set_temperature" ATTR_CURRENT_TEMPERATURE = "current_temperature" ATTR_AWAY_MODE = "away_mode" +ATTR_MAX_TEMP = "max_temp" +ATTR_MIN_TEMP = "min_temp" _LOGGER = logging.getLogger(__name__) @@ -131,6 +133,22 @@ class ThermostatDevice(Entity): if device_attr is not None: data.update(device_attr) + if hasattr(self, ATTR_MIN_TEMP): + min_temp = self.hass.config.temperature( + getattr(self, ATTR_MIN_TEMP), self.unit_of_measurement)[0] + else: + min_temp = self.hass.config.temperature( + 7, TEMP_CELCIUS)[0] + data[ATTR_MIN_TEMP] = min_temp + + if hasattr(self, ATTR_MAX_TEMP): + max_temp = self.hass.config.temperature( + getattr(self, ATTR_MAX_TEMP), self.unit_of_measurement)[0] + else: + max_temp = self.hass.config.temperature( + 35, TEMP_CELCIUS)[0] + data[ATTR_MAX_TEMP] = max_temp + return data @property diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index d21245dae3a..273f1173da8 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -90,16 +90,18 @@ class HeatControl(ThermostatDevice): self.target_sensor_entity_id = config.get("target_sensor") self.time_temp = [] - for time_temp in list(config.get("time_temp").split(",")): - time, temp = time_temp.split(':') - time_start, time_end = time.split('-') - start_time = datetime.datetime.time(datetime.datetime. - strptime(time_start, '%H%M')) - end_time = datetime.datetime.time(datetime.datetime. - strptime(time_end, '%H%M')) - self.time_temp.append((start_time, end_time, float(temp))) + if config.get("time_temp"): + for time_temp in list(config.get("time_temp").split(",")): + time, temp = time_temp.split(':') + time_start, time_end = time.split('-') + start_time = datetime.datetime.time( + datetime.datetime.strptime(time_start, '%H%M')) + end_time = datetime.datetime.time( + datetime.datetime.strptime(time_end, '%H%M')) + self.time_temp.append((start_time, end_time, float(temp))) self.min_temp = float(config.get("min_temp")) + self.max_temp = float(config.get("max_temp")) self._manual_sat_temp = None self._away = False @@ -178,7 +180,7 @@ class HeatControl(ThermostatDevice): if not self._heater_manual_changed: pass else: - self.set_temperature(100) + self.set_temperature(self.max_temp) self._heater_manual_changed = True From 44ce756cbae80b5f672a3ded64aae7e9eb4c91c8 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 23 Jul 2015 19:36:05 +0200 Subject: [PATCH 012/111] Support for rfxtrx sensors --- homeassistant/components/sensor/rfxtrx.py | 119 ++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 homeassistant/components/sensor/rfxtrx.py diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py new file mode 100644 index 00000000000..879e3326eb8 --- /dev/null +++ b/homeassistant/components/sensor/rfxtrx.py @@ -0,0 +1,119 @@ +""" +homeassistant.components.sensor.rfxtrx +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Shows sensor values from rfxtrx sensors. + +Possible config keys: + +path to rfxtrx device +device=/dev/serial/by-id/usb-rfxtrx_RFXtrx433_A1Y0NJGR-if00-port0 + +id of the sensor: Name the sensor with ID +135=Outside + +only_named: Only show the named sensors +only_named=1 + +datatype_mask: mask to determine which sensor values to show based on + +datatype_mask=1 # only show temperature +datatype_mask=127 # show all sensor values +""" +import logging +from collections import OrderedDict + +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, TEMP_CELCIUS) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip' + '#RFXtrx>=0.15'] + +DATA_TYPES = OrderedDict([ + ('Temperature', TEMP_CELCIUS), + ('Humidity', '%'), + ('Forecast', ''), + ('Barometer', ''), + ('Wind direction', ''), + ('Humidity status', ''), + ('Humidity status numeric', ''), + ('Forecast numeric', ''), + ('Rain rate', ''), + ('Rain total', ''), + ('Wind average speed', ''), + ('Wind gust', ''), + ('Chill', ''), + ('Battery numeric', ATTR_BATTERY_LEVEL), + ('Rssi numeric', '')]) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Setup the rfxtrx platform. """ + logger = logging.getLogger(__name__) + + devices = {} # keep track of devices added to HA + + def sensor_update(event): + """ Callback for sensor updates from the MySensors gateway. """ + if event.device.id_string in devices: + devices[event.device.id_string].event = event + else: + logger.info("adding new devices: %s", event.device.type_string) + new_device = RfxtrxSensor(event) + devices[event.device.id_string] = new_device + add_devices([new_device]) + try: + import RFXtrx as rfxtrx + except ImportError: + logger.exception( + "Failed to import rfxtrx") + return False + + device = config.get("device", True) + rfxtrx.Core(device, sensor_update) + + +class RfxtrxSensor(Entity): + """ Represents a Vera Sensor. """ + + def __init__(self, event): + self.event = event + + self._unit_of_measurement = None + self._data_type = None + for data_type in DATA_TYPES: + if data_type in self.event.values: + self._unit_of_measurement = DATA_TYPES[data_type] + self._data_type = data_type + break + + id_string = int(event.device.id_string.replace(":", ""), 16) + self._name = "{} {} ({})".format(self._data_type, + self.event.device.type_string, + id_string) + + def __str__(self): + return self._name + + @property + def state(self): + if self._data_type: + return self.event.values[self._data_type] + return None + + @property + def name(self): + """ Get the mame of the sensor. """ + return self._name + + @property + def state_attributes(self): + attr = super().state_attributes + for data_type in DATA_TYPES: + if data_type in self.event.values: + attr[data_type] = self.event.values[data_type] + return attr + + @property + def unit_of_measurement(self): + return self._unit_of_measurement From f44acc9b0e695e96507c21aefa84325c7d792a67 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 23 Jul 2015 19:42:20 +0200 Subject: [PATCH 013/111] requirements file --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index c7a569fad33..06aea3fa518 100644 --- a/requirements.txt +++ b/requirements.txt @@ -79,3 +79,6 @@ PyMata==2.07a # Mysensors serial gateway pyserial>=2.7 + +#Rfxtrx sensor +https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip From 8f99ebf27e676390c2ee919c8b8c5b8d20462934 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 23 Jul 2015 19:47:45 +0200 Subject: [PATCH 014/111] Documentation of rfxtrx sensor --- homeassistant/components/sensor/rfxtrx.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 879e3326eb8..c2950c0d9c2 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -4,20 +4,14 @@ homeassistant.components.sensor.rfxtrx Shows sensor values from rfxtrx sensors. Possible config keys: +device="path to rfxtrx device" -path to rfxtrx device -device=/dev/serial/by-id/usb-rfxtrx_RFXtrx433_A1Y0NJGR-if00-port0 +Example: -id of the sensor: Name the sensor with ID -135=Outside +sensor 2: + platform: rfxtrx + device : /dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0 -only_named: Only show the named sensors -only_named=1 - -datatype_mask: mask to determine which sensor values to show based on - -datatype_mask=1 # only show temperature -datatype_mask=127 # show all sensor values """ import logging from collections import OrderedDict From b54c58235f49f93fc3245a351a29ba636b79ea91 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 23 Jul 2015 19:50:26 +0200 Subject: [PATCH 015/111] Documentation of rfxtrx sensor --- homeassistant/components/sensor/rfxtrx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index c2950c0d9c2..12e6afde3d1 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -8,7 +8,7 @@ device="path to rfxtrx device" Example: -sensor 2: +sensor 2: platform: rfxtrx device : /dev/serial/by-id/usb-RFXCOM_RFXtrx433_A1Y0NJGR-if00-port0 From be937a795aaf2ea144c4cf8d8fb7004834f31333 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 23 Jul 2015 22:15:17 +0200 Subject: [PATCH 016/111] Min max temp for thermostat --- .../components/thermostat/__init__.py | 27 +++++++++---------- .../components/thermostat/heat_control.py | 15 +++++++++-- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index dffa7dfcc26..40e392709f2 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -133,21 +133,8 @@ class ThermostatDevice(Entity): if device_attr is not None: data.update(device_attr) - if hasattr(self, ATTR_MIN_TEMP): - min_temp = self.hass.config.temperature( - getattr(self, ATTR_MIN_TEMP), self.unit_of_measurement)[0] - else: - min_temp = self.hass.config.temperature( - 7, TEMP_CELCIUS)[0] - data[ATTR_MIN_TEMP] = min_temp - - if hasattr(self, ATTR_MAX_TEMP): - max_temp = self.hass.config.temperature( - getattr(self, ATTR_MAX_TEMP), self.unit_of_measurement)[0] - else: - max_temp = self.hass.config.temperature( - 35, TEMP_CELCIUS)[0] - data[ATTR_MAX_TEMP] = max_temp + data[ATTR_MIN_TEMP] = self.min_temp + data[ATTR_MAX_TEMP] = self.max_temp return data @@ -180,3 +167,13 @@ class ThermostatDevice(Entity): def turn_away_mode_off(self): """ Turns away mode off. """ pass + + @property + def min_temp(self): + """ Return minimum temperature. """ + return self.hass.config.temperature(7, TEMP_CELCIUS)[0] + + @property + def max_temp(self): + """ Return maxmum temperature. """ + return self.hass.config.temperature(35, TEMP_CELCIUS)[0] diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 273f1173da8..54e7cc3e3a5 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -62,6 +62,7 @@ import logging import datetime import homeassistant.components as core +import homeassistant.util as util from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import TEMP_CELCIUS, STATE_ON, STATE_OFF @@ -100,8 +101,8 @@ class HeatControl(ThermostatDevice): datetime.datetime.strptime(time_end, '%H%M')) self.time_temp.append((start_time, end_time, float(temp))) - self.min_temp = float(config.get("min_temp")) - self.max_temp = float(config.get("max_temp")) + self._min_temp = util.convert(config.get("min_temp"), float, 0) + self._max_temp = util.convert(config.get("max_temp"), float, 100) self._manual_sat_temp = None self._away = False @@ -196,3 +197,13 @@ class HeatControl(ThermostatDevice): def turn_away_mode_off(self): """ Turns away mode off. """ self._away = False + + @property + def min_temp(self): + """ Return minimum temperature. """ + return self._min_temp + + @property + def max_temp(self): + """ Return maxmum temperature. """ + return self._max_temp From 22c72060cf99654d77eb358979d399ddd8355171 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Thu, 23 Jul 2015 22:24:48 +0200 Subject: [PATCH 017/111] Make only_named: 0 work --- homeassistant/components/sensor/tellstick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tellstick.py b/homeassistant/components/sensor/tellstick.py index 5720b65a669..f80a3c19f12 100644 --- a/homeassistant/components/sensor/tellstick.py +++ b/homeassistant/components/sensor/tellstick.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: sensor_name = config[ts_sensor.id] except KeyError: - if 'only_named' in config: + if util.convert(config.get('only_named'), bool, False): continue sensor_name = str(ts_sensor.id) From 445aaeb700ae091dcb6792fd813f84d1f4f305c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 24 Jul 2015 03:28:21 -0700 Subject: [PATCH 018/111] New compiled version of frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 96 ++++++++++++++----- .../www_static/home-assistant-polymer | 2 +- 3 files changed, 73 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 4a67ed0fc0e..6f30746f137 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "4f94fd4404583fbf27cc899c024d26ff" +VERSION = "ccfe7497d635ab4df3e6943b05adbd9b" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 023347483cb..c8cabd1b0a3 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,6 @@ \ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 576c04efb49..5a3fcc970b3 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 576c04efb49a8a5f7f35734458ffc93f874dd68d +Subproject commit 5a3fcc970b30d640e6a370b6f20904a745f69659 From 1489af0ecaa5b16ca5951482a50d5136ebad5faf Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Fri, 24 Jul 2015 12:35:03 +0200 Subject: [PATCH 019/111] updated rfxcom sensor --- homeassistant/components/sensor/rfxtrx.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 12e6afde3d1..1b2a4643449 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -16,8 +16,7 @@ sensor 2: import logging from collections import OrderedDict -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, TEMP_CELCIUS) +from homeassistant.const import (TEMP_CELCIUS) from homeassistant.helpers.entity import Entity REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip' @@ -37,7 +36,7 @@ DATA_TYPES = OrderedDict([ ('Wind average speed', ''), ('Wind gust', ''), ('Chill', ''), - ('Battery numeric', ATTR_BATTERY_LEVEL), + ('Battery numeric', '%'), ('Rssi numeric', '')]) @@ -102,11 +101,7 @@ class RfxtrxSensor(Entity): @property def state_attributes(self): - attr = super().state_attributes - for data_type in DATA_TYPES: - if data_type in self.event.values: - attr[data_type] = self.event.values[data_type] - return attr + return self.event.values @property def unit_of_measurement(self): From 3658c579129151980765886464054cf23aa7d214 Mon Sep 17 00:00:00 2001 From: Daniel Hoyer Iversen Date: Fri, 24 Jul 2015 13:06:15 +0200 Subject: [PATCH 020/111] updated rfxcom sensor --- homeassistant/components/sensor/rfxtrx.py | 32 ++++++++--------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 1b2a4643449..b9762995ea5 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -25,36 +25,26 @@ REQUIREMENTS = ['https://github.com/Danielhiversen/pyRFXtrx/archive/master.zip' DATA_TYPES = OrderedDict([ ('Temperature', TEMP_CELCIUS), ('Humidity', '%'), - ('Forecast', ''), ('Barometer', ''), ('Wind direction', ''), - ('Humidity status', ''), - ('Humidity status numeric', ''), - ('Forecast numeric', ''), - ('Rain rate', ''), - ('Rain total', ''), - ('Wind average speed', ''), - ('Wind gust', ''), - ('Chill', ''), - ('Battery numeric', '%'), - ('Rssi numeric', '')]) + ('Rain rate', '')]) def setup_platform(hass, config, add_devices, discovery_info=None): """ Setup the rfxtrx platform. """ logger = logging.getLogger(__name__) - devices = {} # keep track of devices added to HA + sensors = {} # keep track of sensors added to HA def sensor_update(event): - """ Callback for sensor updates from the MySensors gateway. """ - if event.device.id_string in devices: - devices[event.device.id_string].event = event + """ Callback for sensor updates from the RFXtrx gateway. """ + if event.device.id_string in sensors: + sensors[event.device.id_string].event = event else: - logger.info("adding new devices: %s", event.device.type_string) - new_device = RfxtrxSensor(event) - devices[event.device.id_string] = new_device - add_devices([new_device]) + logger.info("adding new sensor: %s", event.device.type_string) + new_sensor = RfxtrxSensor(event) + sensors[event.device.id_string] = new_sensor + add_devices([new_sensor]) try: import RFXtrx as rfxtrx except ImportError: @@ -62,12 +52,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "Failed to import rfxtrx") return False - device = config.get("device", True) + device = config.get("device", "") rfxtrx.Core(device, sensor_update) class RfxtrxSensor(Entity): - """ Represents a Vera Sensor. """ + """ Represents a Rfxtrx Sensor. """ def __init__(self, event): self.event = event From 6a7e28cc8565eec9a540cb0d8ea61f75e4372131 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sat, 25 Jul 2015 18:46:47 -0700 Subject: [PATCH 021/111] - Added support for getting power on SP2101W devices (returns None on SP1101W) --- .coveragerc | 1 + .../www_static/polymer/home-assistant-js | 2 +- homeassistant/components/switch/edimax.py | 27 +++++++++++++++---- homeassistant/external/netdisco | 2 +- homeassistant/external/pyedimax | 2 +- homeassistant/external/pymysensors | 2 +- homeassistant/external/pynetgear | 2 +- homeassistant/external/pywemo | 2 +- 8 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index 39a3dee22bf..a2ad399f1a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -53,6 +53,7 @@ omit = homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py + homeassistant/components/sensor/edimax.py homeassistant/components/switch/hikvisioncam.py homeassistant/components/switch/wemo.py homeassistant/components/thermostat/nest.py diff --git a/homeassistant/components/frontend/www_static/polymer/home-assistant-js b/homeassistant/components/frontend/www_static/polymer/home-assistant-js index 94d8682c1e7..232302b2f58 160000 --- a/homeassistant/components/frontend/www_static/polymer/home-assistant-js +++ b/homeassistant/components/frontend/www_static/polymer/home-assistant-js @@ -1 +1 @@ -Subproject commit 94d8682c1e7679ae744e8419896d5d7b0bdd16cc +Subproject commit 232302b2f589fa216b6531e65dae5dafd851f6f0 diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 3754b452a2d..1b8ec4e0a4a 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -9,6 +9,7 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Edimax Smart Plugs. """ @@ -24,14 +25,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): return host = config.get(CONF_HOST) - auth=(config.get(CONF_USERNAME, 'admin'), - config.get(CONF_PASSWORD, '1234')) + auth = (config.get(CONF_USERNAME, 'admin'), + config.get(CONF_PASSWORD, '1234')) if not host: - logging.getLogger(__name__).error('Missing config variable %s', CONF_HOST) + logging.getLogger(__name__).error( + 'Missing config variable %s', CONF_HOST) return False - add_devices_callback([SmartPlugSwitch(SmartPlug(host, auth))]) @@ -46,6 +47,22 @@ class SmartPlugSwitch(SwitchDevice): #TODO: dynamically get name from device using requests return 'Edimax Smart Plug' + @property + def current_power_mwh(self): + """ Current power usage in mwh. """ + try: + return float(self.smartplug.now_power) / 1000000.0 + except ValueError: + return None + + @property + def today_power_mw(self): + """ Today total power usage in mw. """ + try: + return float(self.smartplug.now_energy_day) / 1000.0 + except ValueError: + return None + @property def is_on(self): """ True if switch is on. """ @@ -57,4 +74,4 @@ class SmartPlugSwitch(SwitchDevice): def turn_off(self): """ Turns the switch off. """ - self.smartplug.state = 'OFF' \ No newline at end of file + self.smartplug.state = 'OFF' diff --git a/homeassistant/external/netdisco b/homeassistant/external/netdisco index b2cad7c2b95..0e2a4d4e3ec 160000 --- a/homeassistant/external/netdisco +++ b/homeassistant/external/netdisco @@ -1 +1 @@ -Subproject commit b2cad7c2b959efa8eee9b5ac62d87232bf0b5176 +Subproject commit 0e2a4d4e3eccc0895872d1046ef748b05d26ba90 diff --git a/homeassistant/external/pyedimax b/homeassistant/external/pyedimax index 3815f3bd99f..674ada04c42 160000 --- a/homeassistant/external/pyedimax +++ b/homeassistant/external/pyedimax @@ -1 +1 @@ -Subproject commit 3815f3bd99fb9dcd4d9e5e6fc58626f5873e43db +Subproject commit 674ada04c42da5c1103205293a078be73f661fd6 diff --git a/homeassistant/external/pymysensors b/homeassistant/external/pymysensors index cd5ef892eee..7fb5c0ef877 160000 --- a/homeassistant/external/pymysensors +++ b/homeassistant/external/pymysensors @@ -1 +1 @@ -Subproject commit cd5ef892eeec0ad027727f7e8f757e7f2927da97 +Subproject commit 7fb5c0ef877c285d5d98ca0c0c6cbee552164d34 diff --git a/homeassistant/external/pynetgear b/homeassistant/external/pynetgear index e946ecf7926..8863fdd3565 160000 --- a/homeassistant/external/pynetgear +++ b/homeassistant/external/pynetgear @@ -1 +1 @@ -Subproject commit e946ecf7926b9b2adaa1e3127a9738201a1b1fc7 +Subproject commit 8863fdd356556bc82e6d236ad2bc662e7d091ff0 diff --git a/homeassistant/external/pywemo b/homeassistant/external/pywemo index ca94e41faa4..eef7dae12a0 160000 --- a/homeassistant/external/pywemo +++ b/homeassistant/external/pywemo @@ -1 +1 @@ -Subproject commit ca94e41faa48c783f600a2efd550c6b7dae01b0d +Subproject commit eef7dae12a073db7b8ac58340bf1cd6a1fea78c6 From c659be7e17249597f60fc90b85dd3c846b0c8c35 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 25 Jul 2015 23:45:49 -0700 Subject: [PATCH 022/111] Sun component will work now without internet --- homeassistant/components/sun.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index fd2cfa46b72..af4a93825ac 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -21,6 +21,7 @@ The sun event need to have the type 'sun', which service to call, which event """ import logging from datetime import timedelta +import urllib import homeassistant.util as util import homeassistant.util.dt as dt_util @@ -129,8 +130,13 @@ def setup(hass, config): if elevation is None: google = GoogleGeocoder() - google._get_elevation(location) # pylint: disable=protected-access - _LOGGER.info('Retrieved elevation from Google: %s', location.elevation) + 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 sun = Sun(hass, location) sun.point_in_time_listener(dt_util.utcnow()) From bb0ace3a615b799c27b442619dba4a3c62dcadda Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sat, 25 Jul 2015 23:59:48 -0700 Subject: [PATCH 023/111] - Reverted submodule updates --- homeassistant/components/switch/edimax.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 1b8ec4e0a4a..426c016b59e 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class SmartPlugSwitch(SwitchDevice): - """ Represents a Edimax Smart Plug switch within Home Assistant. """ + """ Represents an Edimax Smart Plug switch within Home Assistant. """ def __init__(self, smartplug): self.smartplug = smartplug From 613c0122c0768f8133b8f6a9c979359d6f88d185 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sun, 26 Jul 2015 00:08:57 -0700 Subject: [PATCH 024/111] - Reverted submodule updates. This is the 2nd attempt since the first one did not work --- .../components/frontend/www_static/polymer/home-assistant-js | 2 +- homeassistant/external/netdisco | 2 +- homeassistant/external/pymysensors | 2 +- homeassistant/external/pynetgear | 2 +- homeassistant/external/pywemo | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/www_static/polymer/home-assistant-js b/homeassistant/components/frontend/www_static/polymer/home-assistant-js index 232302b2f58..94d8682c1e7 160000 --- a/homeassistant/components/frontend/www_static/polymer/home-assistant-js +++ b/homeassistant/components/frontend/www_static/polymer/home-assistant-js @@ -1 +1 @@ -Subproject commit 232302b2f589fa216b6531e65dae5dafd851f6f0 +Subproject commit 94d8682c1e7679ae744e8419896d5d7b0bdd16cc diff --git a/homeassistant/external/netdisco b/homeassistant/external/netdisco index 0e2a4d4e3ec..b2cad7c2b95 160000 --- a/homeassistant/external/netdisco +++ b/homeassistant/external/netdisco @@ -1 +1 @@ -Subproject commit 0e2a4d4e3eccc0895872d1046ef748b05d26ba90 +Subproject commit b2cad7c2b959efa8eee9b5ac62d87232bf0b5176 diff --git a/homeassistant/external/pymysensors b/homeassistant/external/pymysensors index 7fb5c0ef877..cd5ef892eee 160000 --- a/homeassistant/external/pymysensors +++ b/homeassistant/external/pymysensors @@ -1 +1 @@ -Subproject commit 7fb5c0ef877c285d5d98ca0c0c6cbee552164d34 +Subproject commit cd5ef892eeec0ad027727f7e8f757e7f2927da97 diff --git a/homeassistant/external/pynetgear b/homeassistant/external/pynetgear index 8863fdd3565..e946ecf7926 160000 --- a/homeassistant/external/pynetgear +++ b/homeassistant/external/pynetgear @@ -1 +1 @@ -Subproject commit 8863fdd356556bc82e6d236ad2bc662e7d091ff0 +Subproject commit e946ecf7926b9b2adaa1e3127a9738201a1b1fc7 diff --git a/homeassistant/external/pywemo b/homeassistant/external/pywemo index eef7dae12a0..ca94e41faa4 160000 --- a/homeassistant/external/pywemo +++ b/homeassistant/external/pywemo @@ -1 +1 @@ -Subproject commit eef7dae12a073db7b8ac58340bf1cd6a1fea78c6 +Subproject commit ca94e41faa48c783f600a2efd550c6b7dae01b0d From fed36d2cd035b527d0ebb398e017bbd2d09e15c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 Jul 2015 00:14:55 -0700 Subject: [PATCH 025/111] Better error reporting remote classes --- homeassistant/components/http.py | 12 ++++++++---- homeassistant/remote.py | 5 +++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index c1c0899c9ef..cb8e87490f3 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -119,7 +119,6 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config=None): """ Sets up the HTTP API and debug interface. """ - if config is None or DOMAIN not in config: config = {DOMAIN: {}} @@ -139,9 +138,14 @@ def setup(hass, config=None): sessions_enabled = config[DOMAIN].get(CONF_SESSIONS_ENABLED, True) - server = HomeAssistantHTTPServer( - (server_host, server_port), RequestHandler, hass, api_password, - development, no_password_set, sessions_enabled) + try: + server = HomeAssistantHTTPServer( + (server_host, server_port), RequestHandler, hass, api_password, + development, no_password_set, sessions_enabled) + except OSError: + # Happens if address already in use + _LOGGER.exception("Error setting up HTTP server") + return False hass.bus.listen_once( ha.EVENT_HOMEASSISTANT_START, diff --git a/homeassistant/remote.py b/homeassistant/remote.py index bd576f50e48..3decfa3ce3e 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -120,8 +120,9 @@ class HomeAssistant(ha.HomeAssistant): def start(self): # Ensure a local API exists to connect with remote if self.config.api is None: - bootstrap.setup_component(self, 'http') - bootstrap.setup_component(self, 'api') + if not bootstrap.setup_component(self, 'api'): + raise ha.HomeAssistantError( + 'Unable to setup local API to receive events') ha.Timer(self) From 0c56fde5a930c27c0a36ec2a5c3a909de70542b1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 Jul 2015 10:17:01 +0200 Subject: [PATCH 026/111] Reorg tests folder --- tests/{helpers.py => common.py} | 0 tests/components/__init__.py | 0 tests/{test_component_api.py => components/test_api.py} | 0 .../test_configurator.py} | 0 tests/{test_component_demo.py => components/test_demo.py} | 2 +- .../test_device_sun_light_trigger.py} | 2 +- .../test_device_tracker.py} | 2 +- .../{test_component_frontend.py => components/test_frontend.py} | 0 tests/{test_component_group.py => components/test_group.py} | 0 tests/{test_component_history.py => components/test_history.py} | 2 +- tests/{test_component_core.py => components/test_init.py} | 0 tests/{test_component_light.py => components/test_light.py} | 2 +- tests/{test_component_logbook.py => components/test_logbook.py} | 2 +- .../test_media_player.py} | 2 +- .../{test_component_recorder.py => components/test_recorder.py} | 2 +- tests/{test_component_sun.py => components/test_sun.py} | 0 tests/{test_component_switch.py => components/test_switch.py} | 2 +- tests/config/custom_components/light/test.py | 2 +- tests/config/custom_components/switch/test.py | 2 +- tests/helpers/__init__.py | 0 tests/{test_helper_entity.py => helpers/test_entity.py} | 0 tests/{test_helpers.py => helpers/test_init.py} | 2 +- tests/test_config.py | 2 +- tests/{test_core.py => test_init.py} | 0 tests/test_loader.py | 2 +- tests/util/__init__.py | 0 tests/{test_util_color.py => util/test_color.py} | 0 tests/{test_util_dt.py => util/test_dt.py} | 0 tests/{test_util.py => util/test_init.py} | 0 29 files changed, 14 insertions(+), 14 deletions(-) rename tests/{helpers.py => common.py} (100%) create mode 100644 tests/components/__init__.py rename tests/{test_component_api.py => components/test_api.py} (100%) rename tests/{test_component_configurator.py => components/test_configurator.py} (100%) rename tests/{test_component_demo.py => components/test_demo.py} (96%) rename tests/{test_component_device_sun_light_trigger.py => components/test_device_sun_light_trigger.py} (99%) rename tests/{test_component_device_tracker.py => components/test_device_tracker.py} (99%) rename tests/{test_component_frontend.py => components/test_frontend.py} (100%) rename tests/{test_component_group.py => components/test_group.py} (100%) rename tests/{test_component_history.py => components/test_history.py} (99%) rename tests/{test_component_core.py => components/test_init.py} (100%) rename tests/{test_component_light.py => components/test_light.py} (99%) rename tests/{test_component_logbook.py => components/test_logbook.py} (97%) rename tests/{test_component_media_player.py => components/test_media_player.py} (98%) rename tests/{test_component_recorder.py => components/test_recorder.py} (97%) rename tests/{test_component_sun.py => components/test_sun.py} (100%) rename tests/{test_component_switch.py => components/test_switch.py} (98%) create mode 100644 tests/helpers/__init__.py rename tests/{test_helper_entity.py => helpers/test_entity.py} (100%) rename tests/{test_helpers.py => helpers/test_init.py} (97%) rename tests/{test_core.py => test_init.py} (100%) create mode 100644 tests/util/__init__.py rename tests/{test_util_color.py => util/test_color.py} (100%) rename tests/{test_util_dt.py => util/test_dt.py} (100%) rename tests/{test_util.py => util/test_init.py} (100%) diff --git a/tests/helpers.py b/tests/common.py similarity index 100% rename from tests/helpers.py rename to tests/common.py diff --git a/tests/components/__init__.py b/tests/components/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_component_api.py b/tests/components/test_api.py similarity index 100% rename from tests/test_component_api.py rename to tests/components/test_api.py diff --git a/tests/test_component_configurator.py b/tests/components/test_configurator.py similarity index 100% rename from tests/test_component_configurator.py rename to tests/components/test_configurator.py diff --git a/tests/test_component_demo.py b/tests/components/test_demo.py similarity index 96% rename from tests/test_component_demo.py rename to tests/components/test_demo.py index d7a64167622..628c7e84bfe 100644 --- a/tests/test_component_demo.py +++ b/tests/components/test_demo.py @@ -9,7 +9,7 @@ import unittest import homeassistant as ha import homeassistant.components.demo as demo -from helpers import mock_http_component +from common import mock_http_component class TestDemo(unittest.TestCase): diff --git a/tests/test_component_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py similarity index 99% rename from tests/test_component_device_sun_light_trigger.py rename to tests/components/test_device_sun_light_trigger.py index 05452a830ec..16b89468201 100644 --- a/tests/test_component_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -14,7 +14,7 @@ from homeassistant.components import ( device_tracker, light, sun, device_sun_light_trigger) -from helpers import ( +from common import ( get_test_home_assistant, ensure_sun_risen, ensure_sun_set, trigger_device_tracker_scan) diff --git a/tests/test_component_device_tracker.py b/tests/components/test_device_tracker.py similarity index 99% rename from tests/test_component_device_tracker.py rename to tests/components/test_device_tracker.py index 143c28c9cdb..1d595df3f5a 100644 --- a/tests/test_component_device_tracker.py +++ b/tests/components/test_device_tracker.py @@ -18,7 +18,7 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME) import homeassistant.components.device_tracker as device_tracker -from helpers import get_test_home_assistant +from common import get_test_home_assistant def setUpModule(): # pylint: disable=invalid-name diff --git a/tests/test_component_frontend.py b/tests/components/test_frontend.py similarity index 100% rename from tests/test_component_frontend.py rename to tests/components/test_frontend.py diff --git a/tests/test_component_group.py b/tests/components/test_group.py similarity index 100% rename from tests/test_component_group.py rename to tests/components/test_group.py diff --git a/tests/test_component_history.py b/tests/components/test_history.py similarity index 99% rename from tests/test_component_history.py rename to tests/components/test_history.py index 7ad657f54b5..1d0d787b95d 100644 --- a/tests/test_component_history.py +++ b/tests/components/test_history.py @@ -13,7 +13,7 @@ import homeassistant as ha import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder -from helpers import ( +from common import ( mock_http_component, mock_state_change_event, get_test_home_assistant) diff --git a/tests/test_component_core.py b/tests/components/test_init.py similarity index 100% rename from tests/test_component_core.py rename to tests/components/test_init.py diff --git a/tests/test_component_light.py b/tests/components/test_light.py similarity index 99% rename from tests/test_component_light.py rename to tests/components/test_light.py index 07f8e8e14c9..5142823e8ef 100644 --- a/tests/test_component_light.py +++ b/tests/components/test_light.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components.light as light -from helpers import mock_service, get_test_home_assistant +from common import mock_service, get_test_home_assistant class TestLight(unittest.TestCase): diff --git a/tests/test_component_logbook.py b/tests/components/test_logbook.py similarity index 97% rename from tests/test_component_logbook.py rename to tests/components/test_logbook.py index 33b7b5f915b..2a426d0fcdc 100644 --- a/tests/test_component_logbook.py +++ b/tests/components/test_logbook.py @@ -14,7 +14,7 @@ from homeassistant.const import ( import homeassistant.util.dt as dt_util from homeassistant.components import logbook -from helpers import get_test_home_assistant, mock_http_component +from common import get_test_home_assistant, mock_http_component class TestComponentHistory(unittest.TestCase): diff --git a/tests/test_component_media_player.py b/tests/components/test_media_player.py similarity index 98% rename from tests/test_component_media_player.py rename to tests/components/test_media_player.py index b7f0b847e80..d79ad89609f 100644 --- a/tests/test_component_media_player.py +++ b/tests/components/test_media_player.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) import homeassistant.components.media_player as media_player -from helpers import mock_service +from common import mock_service def setUpModule(): # pylint: disable=invalid-name diff --git a/tests/test_component_recorder.py b/tests/components/test_recorder.py similarity index 97% rename from tests/test_component_recorder.py rename to tests/components/test_recorder.py index 68c63b637d0..eb90f2331d9 100644 --- a/tests/test_component_recorder.py +++ b/tests/components/test_recorder.py @@ -11,7 +11,7 @@ import os from homeassistant.const import MATCH_ALL from homeassistant.components import recorder -from helpers import get_test_home_assistant +from common import get_test_home_assistant class TestRecorder(unittest.TestCase): diff --git a/tests/test_component_sun.py b/tests/components/test_sun.py similarity index 100% rename from tests/test_component_sun.py rename to tests/components/test_sun.py diff --git a/tests/test_component_switch.py b/tests/components/test_switch.py similarity index 98% rename from tests/test_component_switch.py rename to tests/components/test_switch.py index cbc161be853..273c5987be2 100644 --- a/tests/test_component_switch.py +++ b/tests/components/test_switch.py @@ -11,7 +11,7 @@ import homeassistant.loader as loader from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM import homeassistant.components.switch as switch -from helpers import get_test_home_assistant +from common import get_test_home_assistant class TestSwitch(unittest.TestCase): diff --git a/tests/config/custom_components/light/test.py b/tests/config/custom_components/light/test.py index f7f355c4b30..1512d080b05 100644 --- a/tests/config/custom_components/light/test.py +++ b/tests/config/custom_components/light/test.py @@ -7,7 +7,7 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.helpers import MockToggleDevice +from tests.common import MockToggleDevice DEVICES = [] diff --git a/tests/config/custom_components/switch/test.py b/tests/config/custom_components/switch/test.py index 178faf7fcdc..bb95154a94b 100644 --- a/tests/config/custom_components/switch/test.py +++ b/tests/config/custom_components/switch/test.py @@ -7,7 +7,7 @@ Provides a mock switch platform. Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.helpers import MockToggleDevice +from tests.common import MockToggleDevice DEVICES = [] diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_helper_entity.py b/tests/helpers/test_entity.py similarity index 100% rename from tests/test_helper_entity.py rename to tests/helpers/test_entity.py diff --git a/tests/test_helpers.py b/tests/helpers/test_init.py similarity index 97% rename from tests/test_helpers.py rename to tests/helpers/test_init.py index adc4b0d0788..9257dd634ef 100644 --- a/tests/test_helpers.py +++ b/tests/helpers/test_init.py @@ -7,7 +7,7 @@ Tests component helpers. # pylint: disable=protected-access,too-many-public-methods import unittest -from helpers import get_test_home_assistant +from common import get_test_home_assistant import homeassistant as ha import homeassistant.loader as loader diff --git a/tests/test_config.py b/tests/test_config.py index 0ea18eead82..368f660eeb7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) -from helpers import get_test_config_dir +from common import get_test_config_dir CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) diff --git a/tests/test_core.py b/tests/test_init.py similarity index 100% rename from tests/test_core.py rename to tests/test_init.py diff --git a/tests/test_loader.py b/tests/test_loader.py index dd80587b247..03bd7e7419c 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -10,7 +10,7 @@ import unittest import homeassistant.loader as loader import homeassistant.components.http as http -from helpers import get_test_home_assistant, MockModule +from common import get_test_home_assistant, MockModule class TestLoader(unittest.TestCase): diff --git a/tests/util/__init__.py b/tests/util/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/test_util_color.py b/tests/util/test_color.py similarity index 100% rename from tests/test_util_color.py rename to tests/util/test_color.py diff --git a/tests/test_util_dt.py b/tests/util/test_dt.py similarity index 100% rename from tests/test_util_dt.py rename to tests/util/test_dt.py diff --git a/tests/test_util.py b/tests/util/test_init.py similarity index 100% rename from tests/test_util.py rename to tests/util/test_init.py From e0468f8b8e8667f05888a1ed218440d3ee2b9d31 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 26 Jul 2015 10:45:49 +0200 Subject: [PATCH 027/111] Extract helpers.event from core + misc cleanup --- homeassistant/__init__.py | 538 +++++++++++---------------------- homeassistant/helpers/event.py | 161 ++++++++++ homeassistant/helpers/state.py | 11 +- homeassistant/remote.py | 2 +- tests/helpers/test_event.py | 126 ++++++++ 5 files changed, 477 insertions(+), 361 deletions(-) create mode 100644 homeassistant/helpers/event.py create mode 100644 tests/helpers/test_event.py diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index 09069924e6b..c62128453e7 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -13,6 +13,7 @@ import threading import enum import re import functools as ft +from collections import namedtuple from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, @@ -41,6 +42,9 @@ ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$") _LOGGER = logging.getLogger(__name__) +# Temporary addition to proxy deprecated methods +_MockHA = namedtuple("MockHomeAssistant", ['bus']) + class HomeAssistant(object): """ Core class to route all communication to right components. """ @@ -52,39 +56,12 @@ class HomeAssistant(object): self.states = StateMachine(self.bus) self.config = Config() - @property - def components(self): - """ DEPRECATED 3/21/2015. Use hass.config.components """ - _LOGGER.warning( - 'hass.components is deprecated. Use hass.config.components') - return self.config.components - - @property - def local_api(self): - """ DEPRECATED 3/21/2015. Use hass.config.api """ - _LOGGER.warning( - 'hass.local_api is deprecated. Use hass.config.api') - return self.config.api - - @property - def config_dir(self): - """ DEPRECATED 3/18/2015. Use hass.config.config_dir """ - _LOGGER.warning( - 'hass.config_dir is deprecated. Use hass.config.config_dir') - return self.config.config_dir - - def get_config_path(self, path): - """ DEPRECATED 3/18/2015. Use hass.config.path """ - _LOGGER.warning( - 'hass.get_config_path is deprecated. Use hass.config.path') - return self.config.path(path) - def start(self): """ Start home assistant. """ _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) - Timer(self) + create_timer(self) self.bus.fire(EVENT_HOMEASSISTANT_START) @@ -105,98 +82,6 @@ class HomeAssistant(object): self.stop() - def track_point_in_time(self, action, point_in_time): - """ - Adds a listener that fires once after a spefic point in time. - """ - utc_point_in_time = date_util.as_utc(point_in_time) - - @ft.wraps(action) - def utc_converter(utc_now): - """ Converts passed in UTC now to local now. """ - action(date_util.as_local(utc_now)) - - self.track_point_in_utc_time(utc_converter, utc_point_in_time) - - def track_point_in_utc_time(self, action, point_in_time): - """ - Adds a listener that fires once after a specific point in UTC time. - """ - - @ft.wraps(action) - def point_in_time_listener(event): - """ Listens for matching time_changed events. """ - now = event.data[ATTR_NOW] - - if now >= point_in_time and \ - not hasattr(point_in_time_listener, 'run'): - - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. This will make - # sure the second time it does nothing. - point_in_time_listener.run = True - - self.bus.remove_listener(EVENT_TIME_CHANGED, - point_in_time_listener) - - action(now) - - self.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) - return point_in_time_listener - - # pylint: disable=too-many-arguments - def track_utc_time_change(self, action, - year=None, month=None, day=None, - hour=None, minute=None, second=None): - """ Adds a listener that will fire if time matches a pattern. """ - self.track_time_change( - action, year, month, day, hour, minute, second, utc=True) - - # pylint: disable=too-many-arguments - def track_time_change(self, action, - year=None, month=None, day=None, - hour=None, minute=None, second=None, utc=False): - """ Adds a listener that will fire if UTC time matches a pattern. """ - - # We do not have to wrap the function with time pattern matching logic - # if no pattern given - if any((val is not None for val in - (year, month, day, hour, minute, second))): - - pmp = _process_match_param - year, month, day = pmp(year), pmp(month), pmp(day) - hour, minute, second = pmp(hour), pmp(minute), pmp(second) - - @ft.wraps(action) - def time_listener(event): - """ Listens for matching time_changed events. """ - now = event.data[ATTR_NOW] - - if not utc: - now = date_util.as_local(now) - - mat = _matcher - - if mat(now.year, year) and \ - mat(now.month, month) and \ - mat(now.day, day) and \ - mat(now.hour, hour) and \ - mat(now.minute, minute) and \ - mat(now.second, second): - - action(now) - - else: - @ft.wraps(action) - def time_listener(event): - """ Fires every time event that comes in. """ - action(event.data[ATTR_NOW]) - - self.bus.listen(EVENT_TIME_CHANGED, time_listener) - return time_listener - def stop(self): """ Stops Home Assistant and shuts down all threads. """ _LOGGER.info("Stopping") @@ -208,76 +93,45 @@ class HomeAssistant(object): self.pool.stop() - def get_entity_ids(self, domain_filter=None): - """ - Returns known entity ids. - - THIS METHOD IS DEPRECATED. Use hass.states.entity_ids - """ + def track_point_in_time(self, action, point_in_time): + """Deprecated method to track point in time.""" _LOGGER.warning( - "hass.get_entiy_ids is deprecated. Use hass.states.entity_ids") + 'hass.track_point_in_time is deprecated. ' + 'Please use homeassistant.helpers.event.track_point_in_time') + import homeassistant.helpers.event as helper + helper.track_point_in_time(self, action, point_in_time) - return self.states.entity_ids(domain_filter) - - def listen_once_event(self, event_type, listener): - """ Listen once for event of a specific type. - - To listen to all events specify the constant ``MATCH_ALL`` - as event_type. - - Note: at the moment it is impossible to remove a one time listener. - - THIS METHOD IS DEPRECATED. Please use hass.events.listen_once. - """ + def track_point_in_utc_time(self, action, point_in_time): + """Deprecated method to track point in UTC time.""" _LOGGER.warning( - "hass.listen_once_event is deprecated. Use hass.bus.listen_once") + 'hass.track_point_in_utc_time is deprecated. ' + 'Please use homeassistant.helpers.event.track_point_in_utc_time') + import homeassistant.helpers.event as helper + helper.track_point_in_utc_time(self, action, point_in_time) - self.bus.listen_once(event_type, listener) + def track_utc_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None): + """Deprecated method to track UTC time change.""" + # pylint: disable=too-many-arguments + _LOGGER.warning( + 'hass.track_utc_time_change is deprecated. ' + 'Please use homeassistant.helpers.event.track_utc_time_change') + import homeassistant.helpers.event as helper + helper.track_utc_time_change(self, action, year, month, day, hour, + minute, second) - def track_state_change(self, entity_ids, action, - from_state=None, to_state=None): - """ - Track specific state changes. - entity_ids, from_state and to_state can be string or list. - Use list to match multiple. - - THIS METHOD IS DEPRECATED. Use hass.states.track_change - """ - _LOGGER.warning(( - "hass.track_state_change is deprecated. " - "Use hass.states.track_change")) - - self.states.track_change(entity_ids, action, from_state, to_state) - - def call_service(self, domain, service, service_data=None): - """ - Fires event to call specified service. - - THIS METHOD IS DEPRECATED. Use hass.services.call - """ - _LOGGER.warning(( - "hass.services.call is deprecated. " - "Use hass.services.call")) - - self.services.call(domain, service, service_data) - - -def _process_match_param(parameter): - """ Wraps parameter in a list if it is not one and returns it. """ - if parameter is None or parameter == MATCH_ALL: - return MATCH_ALL - elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): - return (parameter,) - else: - return tuple(parameter) - - -def _matcher(subject, pattern): - """ Returns True if subject matches the pattern. - - Pattern is either a list of allowed subjects or a `MATCH_ALL`. - """ - return MATCH_ALL == pattern or subject in pattern + def track_time_change(self, action, + year=None, month=None, day=None, + hour=None, minute=None, second=None, utc=False): + """Deprecated method to track time change.""" + # pylint: disable=too-many-arguments + _LOGGER.warning( + 'hass.track_time_change is deprecated. ' + 'Please use homeassistant.helpers.event.track_time_change') + import homeassistant.helpers.event as helper + helper.track_time_change(self, action, year, month, day, hour, + minute, second) class JobPriority(util.OrderedEnum): @@ -305,33 +159,6 @@ class JobPriority(util.OrderedEnum): return JobPriority.EVENT_DEFAULT -def create_worker_pool(): - """ Creates a worker pool to be used. """ - - def job_handler(job): - """ Called whenever a job is available to do. """ - try: - func, arg = job - func(arg) - except Exception: # pylint: disable=broad-except - # Catch any exception our service/event_listener might throw - # We do not want to crash our ThreadPool - _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. """ - - _LOGGER.warning( - "WorkerPool:All %d threads are busy and %d jobs pending", - worker_count, pending_jobs_count) - - for start, job in current_jobs: - _LOGGER.warning("WorkerPool:Current job from %s: %s", - date_util.datetime_to_local_str(start), job) - - return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback) - - class EventOrigin(enum.Enum): """ Distinguish between origin of event. """ # pylint: disable=no-init,too-few-public-methods @@ -446,25 +273,28 @@ class EventBus(object): To listen to all events specify the constant ``MATCH_ALL`` as event_type. - Note: at the moment it is impossible to remove a one time listener. + Returns registered listener that can be used with remove_listener. """ @ft.wraps(listener) def onetime_listener(event): """ Removes listener from eventbus and then fires listener. """ - if not hasattr(onetime_listener, 'run'): - # Set variable so that we will never run twice. - # Because the event bus might have to wait till a thread comes - # available to execute this listener it might occur that the - # listener gets lined up twice to be executed. - # This will make sure the second time it does nothing. - onetime_listener.run = True + if hasattr(onetime_listener, 'run'): + return + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. + # This will make sure the second time it does nothing. + onetime_listener.run = True - self.remove_listener(event_type, onetime_listener) + self.remove_listener(event_type, onetime_listener) - listener(event) + listener(event) self.listen(event_type, onetime_listener) + return onetime_listener + def remove_listener(self, event_type, listener): """ Removes a listener of a specific event_type. """ with self._lock: @@ -596,18 +426,19 @@ class StateMachine(object): def entity_ids(self, domain_filter=None): """ List of entity ids that are being tracked. """ - if domain_filter is not None: - domain_filter = domain_filter.lower() - - return [state.entity_id for key, state - in self._states.items() - if util.split_entity_id(key)[0] == domain_filter] - else: + if domain_filter is None: return list(self._states.keys()) + domain_filter = domain_filter.lower() + + return [state.entity_id for key, state + in self._states.items() + if util.split_entity_id(key)[0] == domain_filter] + def all(self): """ Returns a list of all states. """ - return [state.copy() for state in self._states.values()] + with self._lock: + return [state.copy() for state in self._states.values()] def get(self, entity_id): """ Returns the state of the specified entity. """ @@ -616,16 +447,6 @@ class StateMachine(object): # Make a copy so people won't mutate the state return state.copy() if state else None - def get_since(self, point_in_time): - """ - Returns all states that have been changed since point_in_time. - """ - point_in_time = date_util.strip_microseconds(point_in_time) - - with self._lock: - return [state for state in self._states.values() - if state.last_updated >= point_in_time] - def is_state(self, entity_id, state): """ Returns True if entity exists and is specified state. """ entity_id = entity_id.lower() @@ -661,59 +482,32 @@ class StateMachine(object): same_state = is_existing and old_state.state == new_state same_attr = is_existing and old_state.attributes == attributes + if same_state and same_attr: + return + # If state did not exist or is different, set it - if not (same_state and same_attr): - last_changed = old_state.last_changed if same_state else None + last_changed = old_state.last_changed if same_state else None - state = State(entity_id, new_state, attributes, last_changed) - self._states[entity_id] = state + state = State(entity_id, new_state, attributes, last_changed) + self._states[entity_id] = state - event_data = {'entity_id': entity_id, 'new_state': state} + event_data = {'entity_id': entity_id, 'new_state': state} - if old_state: - event_data['old_state'] = old_state + if old_state: + event_data['old_state'] = old_state - self._bus.fire(EVENT_STATE_CHANGED, event_data) + self._bus.fire(EVENT_STATE_CHANGED, event_data) def track_change(self, entity_ids, action, from_state=None, to_state=None): """ - Track specific state changes. - entity_ids, from_state and to_state can be string or list. - Use list to match multiple. - - Returns the listener that listens on the bus for EVENT_STATE_CHANGED. - Pass the return value into hass.bus.remove_listener to remove it. + DEPRECATED """ - from_state = _process_match_param(from_state) - to_state = _process_match_param(to_state) - - # Ensure it is a lowercase list with entity ids we want to match on - if isinstance(entity_ids, str): - entity_ids = (entity_ids.lower(),) - else: - entity_ids = tuple(entity_id.lower() for entity_id in entity_ids) - - @ft.wraps(action) - def state_listener(event): - """ The listener that listens for specific state changes. """ - if event.data['entity_id'] not in entity_ids: - return - - if 'old_state' in event.data: - old_state = event.data['old_state'].state - else: - old_state = None - - if _matcher(old_state, from_state) and \ - _matcher(event.data['new_state'].state, to_state): - - action(event.data['entity_id'], - event.data.get('old_state'), - event.data['new_state']) - - self._bus.listen(EVENT_STATE_CHANGED, state_listener) - - return state_listener + _LOGGER.warning( + 'hass.states.track_change is deprecated. ' + 'Use homeassistant.helpers.event.track_state_change instead.') + import homeassistant.helpers.event as helper + helper.track_state_change(_MockHA(self._bus), entity_ids, action, + from_state, to_state) # pylint: disable=too-few-public-methods @@ -826,15 +620,16 @@ class ServiceRegistry(object): domain = service_data.pop(ATTR_DOMAIN, None) service = service_data.pop(ATTR_SERVICE, None) - with self._lock: - if domain in self._services and service in self._services[domain]: - service_call = ServiceCall(domain, service, service_data) + if not self.has_service(domain, service): + return - # Add a job to the pool that calls _execute_service - self._pool.add_job(JobPriority.EVENT_SERVICE, - (self._execute_service, - (self._services[domain][service], - service_call))) + service_handler = self._services[domain][service] + service_call = ServiceCall(domain, service, service_data) + + # Add a job to the pool that calls _execute_service + self._pool.add_job(JobPriority.EVENT_SERVICE, + (self._execute_service, + (service_handler, service_call))) def _execute_service(self, service_and_call): """ Executes a service and fires a SERVICE_EXECUTED event. """ @@ -843,9 +638,8 @@ class ServiceRegistry(object): service(call) self._bus.fire( - EVENT_SERVICE_EXECUTED, { - ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID] - }) + EVENT_SERVICE_EXECUTED, + {ATTR_SERVICE_CALL_ID: call.data[ATTR_SERVICE_CALL_ID]}) def _generate_unique_id(self): """ Generates a unique service call id. """ @@ -853,70 +647,6 @@ class ServiceRegistry(object): return "{}-{}".format(id(self), self._cur_id) -class Timer(threading.Thread): - """ Timer will sent out an event every TIMER_INTERVAL seconds. """ - - def __init__(self, hass, interval=None): - threading.Thread.__init__(self) - - self.daemon = True - self.hass = hass - self.interval = interval or TIMER_INTERVAL - self._stop_event = threading.Event() - - # 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. - assert 60 % self.interval == 0, "60 % TIMER_INTERVAL should be 0!" - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, - lambda event: self.start()) - - def run(self): - """ Start the timer. """ - - self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: self._stop_event.set()) - - _LOGGER.info("Timer:starting") - - last_fired_on_second = -1 - - calc_now = date_util.utcnow - interval = self.interval - - while not self._stop_event.isSet(): - now = calc_now() - - # First check checks if we are not on a second matching the - # timer interval. Second check checks if we did not already fire - # this interval. - if now.second % interval or \ - now.second == last_fired_on_second: - - # Sleep till it is the next time that we have to fire an event. - # Aim for halfway through the second that fits TIMER_INTERVAL. - # If TIMER_INTERVAL is 10 fire at .5, 10.5, 20.5, etc seconds. - # This will yield the best results because time.sleep() is not - # 100% accurate because of non-realtime OS's - slp_seconds = interval - now.second % interval + \ - .5 - now.microsecond/1000000.0 - - time.sleep(slp_seconds) - - now = calc_now() - - last_fired_on_second = now.second - - # Event might have been set while sleeping - if not self._stop_event.isSet(): - try: - self.hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) - except HomeAssistantError: - # HA raises error if firing event after it has shut down - break - - class Config(object): """ Configuration settings for Home Assistant. """ @@ -986,3 +716,93 @@ class InvalidEntityFormatError(HomeAssistantError): class NoEntitySpecifiedError(HomeAssistantError): """ When no entity is specified. """ pass + + +def create_timer(hass, interval=TIMER_INTERVAL): + """ Creates a timer. Timer 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. + assert 60 % interval == 0, "60 % TIMER_INTERVAL should be 0!" + + def timer(): + """Send an EVENT_TIME_CHANGED on interval.""" + stop_event = threading.Event() + + def stop_timer(event): + """Stop the timer.""" + stop_event.set() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_timer) + + _LOGGER.info("Timer:starting") + + last_fired_on_second = -1 + + calc_now = date_util.utcnow + + while not stop_event.isSet(): + now = calc_now() + + # First check checks if we are not on a second matching the + # timer interval. Second check checks if we did not already fire + # this interval. + if now.second % interval or \ + now.second == last_fired_on_second: + + # Sleep till it is the next time that we have to fire an event. + # Aim for halfway through the second that fits TIMER_INTERVAL. + # If TIMER_INTERVAL is 10 fire at .5, 10.5, 20.5, etc seconds. + # This will yield the best results because time.sleep() is not + # 100% accurate because of non-realtime OS's + slp_seconds = interval - now.second % interval + \ + .5 - now.microsecond/1000000.0 + + time.sleep(slp_seconds) + + now = calc_now() + + last_fired_on_second = now.second + + # Event might have been set while sleeping + if not stop_event.isSet(): + try: + hass.bus.fire(EVENT_TIME_CHANGED, {ATTR_NOW: now}) + except HomeAssistantError: + # HA raises error if firing event after it has shut down + break + + def start_timer(event): + """Start the timer.""" + thread = threading.Thread(target=timer) + thread.daemon = True + thread.start() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_timer) + + +def create_worker_pool(): + """ Creates a worker pool to be used. """ + + def job_handler(job): + """ Called whenever a job is available to do. """ + try: + func, arg = job + func(arg) + except Exception: # pylint: disable=broad-except + # Catch any exception our service/event_listener might throw + # We do not want to crash our ThreadPool + _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. """ + + _LOGGER.warning( + "WorkerPool:All %d threads are busy and %d jobs pending", + worker_count, pending_jobs_count) + + for start, job in current_jobs: + _LOGGER.warning("WorkerPool:Current job from %s: %s", + date_util.datetime_to_local_str(start), job) + + return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py new file mode 100644 index 00000000000..194cacdc19d --- /dev/null +++ b/homeassistant/helpers/event.py @@ -0,0 +1,161 @@ +""" +Helpers for listening to events +""" +import functools as ft + +from ..util import dt as dt_util +from ..const import ( + ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) + + +def track_state_change(hass, entity_ids, action, from_state=None, + to_state=None): + """ + Track specific state changes. + entity_ids, from_state and to_state can be string or list. + Use list to match multiple. + + Returns the listener that listens on the bus for EVENT_STATE_CHANGED. + Pass the return value into hass.bus.remove_listener to remove it. + """ + from_state = _process_match_param(from_state) + to_state = _process_match_param(to_state) + + # Ensure it is a lowercase list with entity ids we want to match on + if isinstance(entity_ids, str): + entity_ids = (entity_ids.lower(),) + else: + entity_ids = tuple(entity_id.lower() for entity_id in entity_ids) + + @ft.wraps(action) + def state_change_listener(event): + """ The listener that listens for specific state changes. """ + if event.data['entity_id'] not in entity_ids: + return + + if 'old_state' in event.data: + old_state = event.data['old_state'].state + else: + old_state = None + + if _matcher(old_state, from_state) and \ + _matcher(event.data['new_state'].state, to_state): + + action(event.data['entity_id'], + event.data.get('old_state'), + event.data['new_state']) + + hass.bus.listen(EVENT_STATE_CHANGED, state_change_listener) + + return state_change_listener + + +def track_point_in_time(hass, action, point_in_time): + """ + Adds a listener that fires once after a spefic point in time. + """ + utc_point_in_time = dt_util.as_utc(point_in_time) + + @ft.wraps(action) + def utc_converter(utc_now): + """ Converts passed in UTC now to local now. """ + action(dt_util.as_local(utc_now)) + + return track_point_in_utc_time(hass, utc_converter, utc_point_in_time) + + +def track_point_in_utc_time(hass, action, point_in_time): + """ + Adds a listener that fires once after a specific point in UTC time. + """ + + @ft.wraps(action) + def point_in_time_listener(event): + """ Listens for matching time_changed events. """ + now = event.data[ATTR_NOW] + + if now >= point_in_time and \ + not hasattr(point_in_time_listener, 'run'): + + # Set variable so that we will never run twice. + # Because the event bus might have to wait till a thread comes + # available to execute this listener it might occur that the + # listener gets lined up twice to be executed. This will make + # sure the second time it does nothing. + point_in_time_listener.run = True + + hass.bus.remove_listener(EVENT_TIME_CHANGED, + point_in_time_listener) + + action(now) + + hass.bus.listen(EVENT_TIME_CHANGED, point_in_time_listener) + return point_in_time_listener + + +# pylint: disable=too-many-arguments +def track_utc_time_change(hass, action, year=None, month=None, day=None, + hour=None, minute=None, second=None, local=False): + """ Adds a listener that will fire if time matches a pattern. """ + # We do not have to wrap the function with time pattern matching logic + # if no pattern given + if all(val is None for val in (year, month, day, hour, minute, second)): + @ft.wraps(action) + def time_change_listener(event): + """ Fires every time event that comes in. """ + action(event.data[ATTR_NOW]) + + hass.bus.listen(EVENT_TIME_CHANGED, time_change_listener) + return time_change_listener + + pmp = _process_match_param + year, month, day = pmp(year), pmp(month), pmp(day) + hour, minute, second = pmp(hour), pmp(minute), pmp(second) + + @ft.wraps(action) + def pattern_time_change_listener(event): + """ Listens for matching time_changed events. """ + now = event.data[ATTR_NOW] + + if local: + now = dt_util.as_local(now) + + mat = _matcher + + if mat(now.year, year) and \ + mat(now.month, month) and \ + mat(now.day, day) and \ + mat(now.hour, hour) and \ + mat(now.minute, minute) and \ + mat(now.second, second): + + action(now) + + hass.bus.listen(EVENT_TIME_CHANGED, pattern_time_change_listener) + return pattern_time_change_listener + + +# pylint: disable=too-many-arguments +def track_time_change(hass, action, year=None, month=None, day=None, + hour=None, minute=None, second=None): + """ Adds a listener that will fire if UTC time matches a pattern. """ + track_utc_time_change(hass, action, year, month, day, hour, minute, second, + local=True) + + +def _process_match_param(parameter): + """ Wraps parameter in a tuple if it is not one and returns it. """ + if parameter is None or parameter == MATCH_ALL: + return MATCH_ALL + elif isinstance(parameter, str) or not hasattr(parameter, '__iter__'): + return (parameter,) + else: + return tuple(parameter) + + +def _matcher(subject, pattern): + """ Returns True if subject matches the pattern. + + Pattern is either a tuple of allowed subjects or a `MATCH_ALL`. + """ + return MATCH_ALL == pattern or subject in pattern diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 18c68808e94..66e9a448d8e 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -30,7 +30,16 @@ class TrackStates(object): return self.states def __exit__(self, exc_type, exc_value, traceback): - self.states.extend(self.hass.states.get_since(self.now)) + self.states.extend(get_changed_since(self.hass.states.all(), self.now)) + + +def get_changed_since(states, utc_point_in_time): + """ + Returns all states that have been changed since utc_point_in_time. + """ + point_in_time = dt_util.strip_microseconds(utc_point_in_time) + + return [state for state in states if state.last_updated >= point_in_time] def reproduce_state(hass, states, blocking=False): diff --git a/homeassistant/remote.py b/homeassistant/remote.py index 3decfa3ce3e..5a0a828bb21 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -124,7 +124,7 @@ class HomeAssistant(ha.HomeAssistant): raise ha.HomeAssistantError( 'Unable to setup local API to receive events') - ha.Timer(self) + ha.create_timer(self) self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py new file mode 100644 index 00000000000..c6fdf3e276a --- /dev/null +++ b/tests/helpers/test_event.py @@ -0,0 +1,126 @@ +""" +tests.helpers.event_test +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests event helpers. +""" +# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=too-few-public-methods +import unittest +from datetime import datetime + +import homeassistant as ha +from homeassistant.helpers.event import * + + +class TestEventHelpers(unittest.TestCase): + """ + Tests the Home Assistant event helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = ha.HomeAssistant() + self.hass.states.set("light.Bowl", "on") + self.hass.states.set("switch.AC", "off") + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_track_point_in_time(self): + """ Test track point in time. """ + before_birthday = datetime(1985, 7, 9, 12, 0, 0) + birthday_paulus = datetime(1986, 7, 9, 12, 0, 0) + after_birthday = datetime(1987, 7, 9, 12, 0, 0) + + runs = [] + + track_point_in_utc_time( + self.hass, lambda x: runs.append(1), birthday_paulus) + + self._send_time_changed(before_birthday) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + + self._send_time_changed(birthday_paulus) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + + # A point in time tracker will only fire once, this should do nothing + self._send_time_changed(birthday_paulus) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + + track_point_in_utc_time( + self.hass, lambda x: runs.append(1), birthday_paulus) + + self._send_time_changed(after_birthday) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + + def test_track_time_change(self): + """ Test tracking time change. """ + wildcard_runs = [] + specific_runs = [] + + track_time_change(self.hass, lambda x: wildcard_runs.append(1)) + track_time_change( + self.hass, lambda x: specific_runs.append(1), second=[0, 30]) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15)) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) + self.hass.pool.block_till_done() + self.assertEqual(2, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + + def test_track_state_change(self): + """ Test states.track_change. """ + # 2 lists to track how often our callbacks get called + specific_runs = [] + wildcard_runs = [] + + track_state_change( + self.hass, 'light.Bowl', lambda a, b, c: specific_runs.append(1), + 'on', 'off') + + track_state_change( + self.hass, 'light.Bowl', lambda a, b, c: wildcard_runs.append(1), + ha.MATCH_ALL, ha.MATCH_ALL) + + # Set same state should not trigger a state change/listener + self.hass.states.set('light.Bowl', 'on') + self.hass.pool.block_till_done() + self.assertEqual(0, len(specific_runs)) + self.assertEqual(0, len(wildcard_runs)) + + # State change off -> on + self.hass.states.set('light.Bowl', 'off') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(1, len(wildcard_runs)) + + # State change off -> off + self.hass.states.set('light.Bowl', 'off', {"some_attr": 1}) + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(2, len(wildcard_runs)) + + # State change off -> on + self.hass.states.set('light.Bowl', 'on') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(3, len(wildcard_runs)) + + def _send_time_changed(self, now): + """ Send a time changed event. """ + self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) From 4845c1290c4306afdfd95108639a26ba1d09ac5f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 23 Jun 2015 12:34:55 +0200 Subject: [PATCH 028/111] remove unused stuff and update the names (same as in owm sensor) --- homeassistant/components/sensor/forecast.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index abd3cdadb73..613dd35d640 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -1,7 +1,6 @@ """ homeassistant.components.sensor.forecast ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Forecast.io service. Configuration: @@ -121,10 +120,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-few-public-methods class ForeCastSensor(Entity): - """ Implements an OpenWeatherMap sensor. """ + """ Implements an Forecast.io sensor. """ def __init__(self, weather_data, sensor_type, unit): - self.client_name = 'Forecast' + self.client_name = 'Weather' self._name = SENSOR_TYPES[sensor_type][0] self.forecast_client = weather_data self._unit = unit @@ -135,7 +134,7 @@ class ForeCastSensor(Entity): @property def name(self): - return '{} - {}'.format(self.client_name, self._name) + return '{} {}'.format(self.client_name, self._name) @property def state(self): @@ -157,10 +156,6 @@ class ForeCastSensor(Entity): try: if self.type == 'summary': self._state = data.summary - # elif self.type == 'sunrise_time': - # self._state = data.sunriseTime - # elif self.type == 'sunset_time': - # self._state = data.sunsetTime elif self.type == 'precip_intensity': if data.precipIntensity == 0: self._state = 'None' From c1b428489fb5cf959ddeb10374ff2866bf263832 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 27 Jul 2015 18:58:32 +0200 Subject: [PATCH 029/111] fix requirement --- homeassistant/components/sensor/openweathermap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 22720748034..f4635cd13ca 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -48,7 +48,7 @@ from homeassistant.util import Throttle from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pywm>=2.2.1'] +REQUIREMENTS = ['pyowm>=2.2.1'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'weather': ['Condition', ''], From f6811e858ac80f47a71394931ff9e95d4b092ee4 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Wed, 29 Jul 2015 00:24:42 -0700 Subject: [PATCH 030/111] - Removed https://github.com/rkabadi/pyedimax as submodule - Added https://github.com/rkabadi/pyedimax to requirements - Modified edimax.py to import pyedimax from python3 default packages --- .gitmodules | 3 --- homeassistant/components/switch/edimax.py | 6 ++---- homeassistant/external/pyedimax | 1 - requirements.txt | 3 +++ 4 files changed, 5 insertions(+), 8 deletions(-) delete mode 160000 homeassistant/external/pyedimax diff --git a/.gitmodules b/.gitmodules index 5d994cf6ceb..ca0b1f024b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,6 +22,3 @@ [submodule "homeassistant/external/pymysensors"] path = homeassistant/external/pymysensors url = https://github.com/theolind/pymysensors -[submodule "homeassistant/external/pyedimax"] - path = homeassistant/external/pyedimax - url = https://github.com/rkabadi/pyedimax diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 426c016b59e..e988ffec0f0 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -15,12 +15,10 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Edimax Smart Plugs. """ try: # pylint: disable=no-name-in-module, import-error - from homeassistant.external.pyedimax.smartplug import SmartPlug + from pyedimax.smartplug import SmartPlug except ImportError: logging.getLogger(__name__).exception(( - "Failed to import pyedimax. " - "Did you maybe not run `git submodule init` " - "and `git submodule update`?")) + "Failed to import pyedimax. ")) return diff --git a/homeassistant/external/pyedimax b/homeassistant/external/pyedimax deleted file mode 160000 index 674ada04c42..00000000000 --- a/homeassistant/external/pyedimax +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 674ada04c42da5c1103205293a078be73f661fd6 diff --git a/requirements.txt b/requirements.txt index 83780721c2c..d77fe9b3b89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -79,3 +79,6 @@ PyMata==2.07a # Mysensors serial gateway pyserial>=2.7 + +# PyEdimax +git+https://github.com/rkabadi/pyedimax.git \ No newline at end of file From f82b63483a1e460a9838bd43735de72184757217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Esp=C3=ADndola?= Date: Wed, 29 Jul 2015 14:04:32 -0300 Subject: [PATCH 031/111] Modbus coil support --- homeassistant/components/sensor/modbus.py | 64 ++++++++++------- homeassistant/components/switch/modbus.py | 83 +++++++++++++++-------- 2 files changed, 94 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index 90593875a54..b7b122d8d66 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -18,6 +18,9 @@ sensor: name: My boolean sensor 2: name: My other boolean sensor + coils: + 0: + name: My coil switch VARIABLES: @@ -25,6 +28,7 @@ VARIABLES: - "unit" = unit to attach to value (optional, ignored for boolean sensors) - "registers" contains a list of relevant registers to read from it can contain a "bits" section, listing relevant bits + - "coils" contains a list of relevant coils to read from - each named register will create an integer sensor - each named bit will create a boolean sensor @@ -49,21 +53,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("No slave number provided for serial Modbus") return False registers = config.get("registers") - for regnum, register in registers.items(): - if register.get("name"): - sensors.append(ModbusSensor(register.get("name"), + if registers: + for regnum, register in registers.items(): + if register.get("name"): + sensors.append(ModbusSensor(register.get("name"), + slave, + regnum, + None, + register.get("unit"))) + if register.get("bits"): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + sensors.append(ModbusSensor(bit.get("name"), + slave, + regnum, + bitnum)) + coils = config.get("coils") + if coils: + for coilnum, coil in coils.items(): + sensors.append(ModbusSensor(coil.get("name"), slave, - regnum, - None, - register.get("unit"))) - if register.get("bits"): - bits = register.get("bits") - for bitnum, bit in bits.items(): - if bit.get("name"): - sensors.append(ModbusSensor(bit.get("name"), - slave, - regnum, - bitnum)) + coilnum, + coil=True)) + add_devices(sensors) @@ -71,13 +84,14 @@ class ModbusSensor(Entity): # pylint: disable=too-many-arguments """ Represents a Modbus Sensor """ - def __init__(self, name, slave, register, bit=None, unit=None): + def __init__(self, name, slave, register, bit=None, unit=None, coil=False): self._name = name self.slave = int(slave) if slave else 1 self.register = int(register) self.bit = int(bit) if bit else None self._value = None self._unit = unit + self._coil = coil def __str__(self): return "%s: %s" % (self.name, self.state) @@ -124,13 +138,17 @@ class ModbusSensor(Entity): return attr def update(self): - result = modbus.NETWORK.read_holding_registers(unit=self.slave, + if self._coil: + result = modbus.NETWORK.read_coils(self.register, 1) + self._value = result.bits[0] + else: + result = modbus.NETWORK.read_holding_registers(unit=self.slave, address=self.register, count=1) - val = 0 - for i, res in enumerate(result.registers): - val += res * (2**(i*16)) - if self.bit: - self._value = val & (0x0001 << self.bit) - else: - self._value = val + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + if self.bit: + self._value = val & (0x0001 << self.bit) + else: + self._value = val diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index 6513ba71f4a..41c45597de4 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -18,12 +18,16 @@ sensor: name: My switch 2: name: My other switch + coils: + 0: + name: My coil switch VARIABLES: - "slave" = slave number (ignored and can be omitted if not serial Modbus) - "registers" contains a list of relevant registers to read from - it must contain a "bits" section, listing relevant bits + - "coils" contains a list of relevant coils to read from/write to - each named bit will create a switch """ @@ -44,28 +48,38 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("No slave number provided for serial Modbus") return False registers = config.get("registers") - for regnum, register in registers.items(): - bits = register.get("bits") - for bitnum, bit in bits.items(): - if bit.get("name"): - switches.append(ModbusSwitch(bit.get("name"), - slave, - regnum, - bitnum)) + if registers: + for regnum, register in registers.items(): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + switches.append(ModbusSwitch(bit.get("name"), + slave, + regnum, + bitnum)) + coils = config.get("coils") + if coils: + for coilnum, coil in coils.items(): + switches.append(ModbusSwitch(coil.get("name"), + slave, + coilnum, + 0, + coil=True)) add_devices(switches) class ModbusSwitch(ToggleEntity): """ Represents a Modbus switch. """ - def __init__(self, name, slave, register, bit): + def __init__(self, name, slave, register, bit, coil=False): self._name = name self.slave = int(slave) if slave else 1 self.register = int(register) self.bit = int(bit) + self._coil = coil self._is_on = None self.register_value = None - + def __str__(self): return "%s: %s" % (self.name, self.state) @@ -98,27 +112,36 @@ class ModbusSwitch(ToggleEntity): return attr def turn_on(self, **kwargs): - if self.register_value is None: - self.update() - val = self.register_value | (0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + if self._coil: + modbus.NETWORK.write_coil(self.register, True) + else: + if self.register_value is None: + self.update() + val = self.register_value | (0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) def turn_off(self, **kwargs): - if self.register_value is None: - self.update() - val = self.register_value & ~(0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + if self._coil: + r = modbus.NETWORK.write_coil(self.register, False) + else: + if self.register_value is None: + self.update() + val = self.register_value & ~(0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) def update(self): - result = modbus.NETWORK.read_holding_registers(unit=self.slave, - address=self.register, - count=1) - val = 0 - for i, res in enumerate(result.registers): - val += res * (2**(i*16)) - self.register_value = val - self._is_on = (val & (0x0001 << self.bit) > 0) + if self._coil: + result = modbus.NETWORK.read_coils(self.register, 1) + self.register_value = result.bits[0] + self._is_on = self.register_value + else: + result = modbus.NETWORK.read_holding_registers(unit=self.slave, address=self.register, count=1) + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + self.register_value = val + self._is_on = (val & (0x0001 << self.bit) > 0) From 6a239bf18a7e010b09e7606fd09afc03a06f1691 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Thu, 30 Jul 2015 00:10:16 -0700 Subject: [PATCH 032/111] Used validate_config to ensure 'host' parameter in edimax config. Added name option to edimax config --- homeassistant/components/switch/edimax.py | 45 ++++++++++++++--------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index e988ffec0f0..58cc757a1f8 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -6,9 +6,17 @@ Support for Edimax switches. """ import logging -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.components.switch import SwitchDevice, DOMAIN +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_NAME +# constants +DEFAULT_USERNAME = 'admin' +DEFAULT_PASSWORD = '1234' +DEVICE_DEFAULT_NAME = 'Edimax Smart Plug' + +# setup logger +_LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -17,33 +25,34 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): # pylint: disable=no-name-in-module, import-error from pyedimax.smartplug import SmartPlug except ImportError: - logging.getLogger(__name__).exception(( - "Failed to import pyedimax. ")) - - return - - host = config.get(CONF_HOST) - auth = (config.get(CONF_USERNAME, 'admin'), - config.get(CONF_PASSWORD, '1234')) - - if not host: - logging.getLogger(__name__).error( - 'Missing config variable %s', CONF_HOST) + _LOGGER.error('Failed to import pyedimax') return False - add_devices_callback([SmartPlugSwitch(SmartPlug(host, auth))]) + # pylint: disable=global-statement + # check for required values in configuration file + if not validate_config({DOMAIN: config}, + {DOMAIN: [CONF_HOST]}, + _LOGGER): + return False + + host = config.get(CONF_HOST) + auth = (config.get(CONF_USERNAME, DEFAULT_USERNAME), + config.get(CONF_PASSWORD, DEFAULT_PASSWORD)) + name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) + + add_devices_callback([SmartPlugSwitch(SmartPlug(host, auth), name)]) class SmartPlugSwitch(SwitchDevice): """ Represents an Edimax Smart Plug switch within Home Assistant. """ - def __init__(self, smartplug): + def __init__(self, smartplug, name): self.smartplug = smartplug + self._name = name @property def name(self): """ Returns the name of the Smart Plug, if any. """ - #TODO: dynamically get name from device using requests - return 'Edimax Smart Plug' + return self._name @property def current_power_mwh(self): From f351ab9544d50dc39b806e3a31c05bc17f76b2a9 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Thu, 30 Jul 2015 00:37:11 -0700 Subject: [PATCH 033/111] Updated branch to avoid conflicts in requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index f0f49e5a248..b4adce86d22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,3 +91,6 @@ pywemo>=0.1 # Wink (*.wink) https://github.com/balloob/python-wink/archive/master.zip#pywink>=0.1 + +# PyEdimax +https://github.com/rkabadi/pyedimax/archive/master.zip \ No newline at end of file From ed0164843a2f0f71797d49431a4ee03c8a3c21d5 Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Thu, 30 Jul 2015 11:30:31 +0200 Subject: [PATCH 034/111] Added support for ASUSWRT based routers --- .coveragerc | 1 + README.md | 2 +- .../components/device_tracker/asuswrt.py | 165 ++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/device_tracker/asuswrt.py diff --git a/.coveragerc b/.coveragerc index 39a3dee22bf..7c21d3a6ed5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -26,6 +26,7 @@ omit = homeassistant/components/*/vera.py homeassistant/components/browser.py + homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py diff --git a/README.md b/README.md index 18a01345741..11d1e3ffd85 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Check out [the website](https://home-assistant.io) for installation instructions Examples of devices it can interface it: - * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/) + * Monitoring connected devices to a wireless router: [OpenWrt](https://openwrt.org/), [Tomato](http://www.polarcloud.com/tomato), [Netgear](http://netgear.com), [DD-WRT](http://www.dd-wrt.com/site/index), [TPLink](http://www.tp-link.us/), [ASUSWRT](http://event.asus.com/2013/nw/ASUSWRT/) * [Philips Hue](http://meethue.com) lights, [WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/) switches, and [Tellstick](http://www.telldus.se/products/tellstick) devices and sensors * [Google Chromecasts](http://www.google.com/intl/en/chrome/devices/chromecast), [Music Player Daemon](http://www.musicpd.org/) and [Kodi (XBMC)](http://kodi.tv/) * Support for [ISY994](https://www.universal-devices.com/residential/isy994i-series/) (Insteon and X10 devices), [Z-Wave](http://www.z-wave.com/), [Nest Thermostats](https://nest.com/), and [Modbus](http://www.modbus.org/) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py new file mode 100644 index 00000000000..e38e410fe54 --- /dev/null +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -0,0 +1,165 @@ +""" +homeassistant.components.device_tracker.asuswrt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Device tracker platform that supports scanning a ASUSWRT router for device +presence. + +This device tracker needs telnet to be enabled on the router + +Configuration: + +To use the ASUSWRT tracker you will need to add something like the following +to your config/configuration.yaml + +device_tracker: + platform: asuswrt + host: YOUR_ROUTER_IP + username: YOUR_ADMIN_USERNAME + password: YOUR_ADMIN_PASSWORD + +Variables: + +host +*Required +The IP address of your router, e.g. 192.168.1.1. + +username +*Required +The username of an user with administrative privileges, usually 'admin'. + +password +*Required +The password for your given admin account. +""" +import logging +from datetime import timedelta +import re +import threading +import telnetlib + +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers import validate_config +from homeassistant.util import Throttle +from homeassistant.components.device_tracker import DOMAIN + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + +_LEASES_REGEX = re.compile( + r'\w+\s' + + r'(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' + + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' + + r'(?P([^\s]+))') + +_IP_NEIGH_REGEX = re.compile( + r'(?P([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' + + r'\w+\s' + + r'\w+\s' + + r'(\w+\s(?P(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s' + + r'(?P(\w+))') + + +def get_scanner(hass, config): + """ Validates config and returns a DD-WRT scanner. """ + if not validate_config(config, + {DOMAIN: [CONF_HOST, CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return None + + scanner = AsusWrtDeviceScanner(config[DOMAIN]) + + return scanner if scanner.success_init else None + + +class AsusWrtDeviceScanner(object): + """ This class queries a router running ASUSWRT firmware + for connected devices. Adapted from DD-WRT scanner. + """ + + def __init__(self, config): + self.host = config[CONF_HOST] + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + self.lock = threading.Lock() + + self.last_results = {} + + # Test the router is accessible + data = self.get_asuswrt_data() + self.success_init = data is not None + + def scan_devices(self): + """ Scans for new devices and return a + list containing found device ids. """ + + self._update_info() + return [client['mac'] for client in self.last_results] + + def get_device_name(self, device): + """ Returns the name of the given device or None if we don't know. """ + if not self.last_results: + return None + for client in self.last_results: + if client['mac'] == device: + return client['host'] + return None + + @Throttle(MIN_TIME_BETWEEN_SCANS) + def _update_info(self): + """ Ensures the information from the ASUSWRT router is up to date. + Returns boolean if scanning successful. """ + if not self.success_init: + return False + + with self.lock: + _LOGGER.info("Checking ARP") + data = self.get_asuswrt_data() + if not data: + return False + + active_clients = [client for client in data.values() if + client['status'] == 'REACHABLE' or + client['status'] == 'DELAY' or + client['status'] == 'STALE'] + self.last_results = active_clients + return True + + def get_asuswrt_data(self): + """ Retrieve data from ASUSWRT and return parsed result. """ + try: + telnet = telnetlib.Telnet(self.host) + telnet.read_until(b'login: ') + telnet.write((self.username + '\n').encode('ascii')) + telnet.read_until(b'Password: ') + telnet.write((self.password + '\n').encode('ascii')) + prompt_string = telnet.read_until(b'#').split(b'\n')[-1] + telnet.write('ip neigh\n'.encode('ascii')) + neighbors = telnet.read_until(prompt_string).split(b'\n')[1:-1] + telnet.write('cat /var/lib/misc/dnsmasq.leases\n'.encode('ascii')) + leases_result = telnet.read_until(prompt_string).split(b'\n')[1:-1] + telnet.write('exit\n'.encode('ascii')) + except EOFError: + _LOGGER.exception("Unexpected response from router") + return + except ConnectionRefusedError: + _LOGGER.exception("Connection refused by router, is telnet enabled?") + return + + devices = {} + for lease in leases_result: + match = _LEASES_REGEX.search(lease.decode('utf-8')) + devices[match.group('ip')] = { + 'ip': match.group('ip'), + 'mac': match.group('mac').upper(), + 'host': match.group('host') + } + + for neighbor in neighbors: + match = _IP_NEIGH_REGEX.search(neighbor.decode('utf-8')) + if match.group('ip') in devices: + devices[match.group('ip')]['status'] = match.group('status') + return devices From ffde7e183e81031de7fd6fabbc5f65b137ac205b Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Thu, 30 Jul 2015 21:05:00 -0700 Subject: [PATCH 035/111] Fixed flake8 violations --- homeassistant/components/switch/edimax.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 58cc757a1f8..ab461f4de07 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -8,7 +8,8 @@ import logging from homeassistant.helpers import validate_config from homeassistant.components.switch import SwitchDevice, DOMAIN -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD,\ + CONF_NAME # constants DEFAULT_USERNAME = 'admin' @@ -18,6 +19,7 @@ DEVICE_DEFAULT_NAME = 'Edimax Smart Plug' # setup logger _LOGGER = logging.getLogger(__name__) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Edimax Smart Plugs. """ From 6873504cc0524e1bf471b2eac552d73b786bf97b Mon Sep 17 00:00:00 2001 From: jamespcole Date: Sat, 1 Aug 2015 06:45:41 +1000 Subject: [PATCH 036/111] Fixed linting errors --- homeassistant/components/notify/slack.py | 94 ++++++++++++++++++++++++ requirements.txt | 3 + 2 files changed, 97 insertions(+) create mode 100644 homeassistant/components/notify/slack.py diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py new file mode 100644 index 00000000000..adf22fa5bea --- /dev/null +++ b/homeassistant/components/notify/slack.py @@ -0,0 +1,94 @@ +""" +homeassistant.components.notify.slack +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Slack platform for notify component. + +Configuration: + +To use the Slack notifier you will need to add something like the following +to your config/configuration.yaml + +notify: + platform: slack + api_key: ABCDEFGHJKLMNOPQRSTUVXYZ + default_channel: '#general' + +Variables: + +api_key +*Required +The slack API token to use for sending slack messages. +You can get your slack API token here https://api.slack.com/web?sudo=1 + +default_channel +*Required +The default channel to post to if no channel is explicitly specified when +sending the notification message. + +""" +import logging + +from homeassistant.helpers import validate_config +from homeassistant.components.notify import ( + DOMAIN, BaseNotificationService) +from homeassistant.const import CONF_API_KEY + +REQUIREMENTS = ['slacker>=0.6.8'] +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-variable +def get_service(hass, config): + """ Get the slack notification service. """ + + if not validate_config(config, + {DOMAIN: ['default_channel', CONF_API_KEY]}, + _LOGGER): + return None + + try: + # pylint: disable=no-name-in-module, unused-variable + from slacker import Error as SlackError + + except ImportError: + _LOGGER.exception( + "Unable to import slacker. " + "Did you maybe not install the 'slacker.py' package?") + + return None + + try: + api_token = config[DOMAIN].get(CONF_API_KEY) + + return SlackNotificationService( + config[DOMAIN]['default_channel'], + api_token) + + except SlackError as ex: + _LOGGER.error( + "Slack authentication failed") + _LOGGER.exception(ex) + + +# pylint: disable=too-few-public-methods +class SlackNotificationService(BaseNotificationService): + """ Implements notification service for Slack. """ + + def __init__(self, default_channel, api_token): + from slacker import Slacker + self._default_channel = default_channel + self._api_token = api_token + self.slack = Slacker(self._api_token) + self.slack.auth.test() + + def send_message(self, message="", **kwargs): + """ Send a message to a user. """ + + from slacker import Error as SlackError + channel = kwargs.get('channel', self._default_channel) + try: + self.slack.chat.post_message(channel, message) + except SlackError as ex: + _LOGGER.exception("Could not send slack notification") + _LOGGER.exception(ex) diff --git a/requirements.txt b/requirements.txt index f0f49e5a248..763db9c2539 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,3 +91,6 @@ pywemo>=0.1 # Wink (*.wink) https://github.com/balloob/python-wink/archive/master.zip#pywink>=0.1 + +# Slack notifier +slacker>=0.6.8 \ No newline at end of file From 1b3a45aba9aba980c7d31a9003e9885e271c7a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Esp=C3=ADndola?= Date: Wed, 29 Jul 2015 14:04:32 -0300 Subject: [PATCH 037/111] Modbus coil support --- homeassistant/components/sensor/modbus.py | 72 ++++++++++-------- homeassistant/components/switch/modbus.py | 91 ++++++++++++++--------- 2 files changed, 100 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/sensor/modbus.py b/homeassistant/components/sensor/modbus.py index 90593875a54..7893ee5935e 100644 --- a/homeassistant/components/sensor/modbus.py +++ b/homeassistant/components/sensor/modbus.py @@ -18,6 +18,9 @@ sensor: name: My boolean sensor 2: name: My other boolean sensor + coils: + 0: + name: My coil switch VARIABLES: @@ -25,6 +28,7 @@ VARIABLES: - "unit" = unit to attach to value (optional, ignored for boolean sensors) - "registers" contains a list of relevant registers to read from it can contain a "bits" section, listing relevant bits + - "coils" contains a list of relevant coils to read from - each named register will create an integer sensor - each named bit will create a boolean sensor @@ -49,21 +53,30 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("No slave number provided for serial Modbus") return False registers = config.get("registers") - for regnum, register in registers.items(): - if register.get("name"): - sensors.append(ModbusSensor(register.get("name"), + if registers: + for regnum, register in registers.items(): + if register.get("name"): + sensors.append(ModbusSensor(register.get("name"), + slave, + regnum, + None, + register.get("unit"))) + if register.get("bits"): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + sensors.append(ModbusSensor(bit.get("name"), + slave, + regnum, + bitnum)) + coils = config.get("coils") + if coils: + for coilnum, coil in coils.items(): + sensors.append(ModbusSensor(coil.get("name"), slave, - regnum, - None, - register.get("unit"))) - if register.get("bits"): - bits = register.get("bits") - for bitnum, bit in bits.items(): - if bit.get("name"): - sensors.append(ModbusSensor(bit.get("name"), - slave, - regnum, - bitnum)) + coilnum, + coil=True)) + add_devices(sensors) @@ -71,13 +84,14 @@ class ModbusSensor(Entity): # pylint: disable=too-many-arguments """ Represents a Modbus Sensor """ - def __init__(self, name, slave, register, bit=None, unit=None): + def __init__(self, name, slave, register, bit=None, unit=None, coil=False): self._name = name self.slave = int(slave) if slave else 1 self.register = int(register) self.bit = int(bit) if bit else None self._value = None self._unit = unit + self._coil = coil def __str__(self): return "%s: %s" % (self.name, self.state) @@ -118,19 +132,19 @@ class ModbusSensor(Entity): else: return self._unit - @property - def state_attributes(self): - attr = super().state_attributes - return attr - def update(self): - result = modbus.NETWORK.read_holding_registers(unit=self.slave, - address=self.register, - count=1) - val = 0 - for i, res in enumerate(result.registers): - val += res * (2**(i*16)) - if self.bit: - self._value = val & (0x0001 << self.bit) + """ Update the state of the sensor. """ + if self._coil: + result = modbus.NETWORK.read_coils(self.register, 1) + self._value = result.bits[0] else: - self._value = val + result = modbus.NETWORK.read_holding_registers( + unit=self.slave, address=self.register, + count=1) + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + if self.bit: + self._value = val & (0x0001 << self.bit) + else: + self._value = val diff --git a/homeassistant/components/switch/modbus.py b/homeassistant/components/switch/modbus.py index 6513ba71f4a..21a4e0087c2 100644 --- a/homeassistant/components/switch/modbus.py +++ b/homeassistant/components/switch/modbus.py @@ -18,12 +18,16 @@ sensor: name: My switch 2: name: My other switch + coils: + 0: + name: My coil switch VARIABLES: - "slave" = slave number (ignored and can be omitted if not serial Modbus) - "registers" contains a list of relevant registers to read from - it must contain a "bits" section, listing relevant bits + - "coils" contains a list of relevant coils to read from/write to - each named bit will create a switch """ @@ -44,25 +48,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("No slave number provided for serial Modbus") return False registers = config.get("registers") - for regnum, register in registers.items(): - bits = register.get("bits") - for bitnum, bit in bits.items(): - if bit.get("name"): - switches.append(ModbusSwitch(bit.get("name"), - slave, - regnum, - bitnum)) + if registers: + for regnum, register in registers.items(): + bits = register.get("bits") + for bitnum, bit in bits.items(): + if bit.get("name"): + switches.append(ModbusSwitch(bit.get("name"), + slave, + regnum, + bitnum)) + coils = config.get("coils") + if coils: + for coilnum, coil in coils.items(): + switches.append(ModbusSwitch(coil.get("name"), + slave, + coilnum, + 0, + coil=True)) add_devices(switches) class ModbusSwitch(ToggleEntity): """ Represents a Modbus switch. """ - def __init__(self, name, slave, register, bit): + def __init__(self, name, slave, register, bit, coil=False): self._name = name self.slave = int(slave) if slave else 1 self.register = int(register) self.bit = int(bit) + self._coil = coil self._is_on = None self.register_value = None @@ -92,33 +106,42 @@ class ModbusSwitch(ToggleEntity): """ Get the name of the switch. """ return self._name - @property - def state_attributes(self): - attr = super().state_attributes - return attr - def turn_on(self, **kwargs): - if self.register_value is None: - self.update() - val = self.register_value | (0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + """ Set switch on. """ + if self._coil: + modbus.NETWORK.write_coil(self.register, True) + else: + if self.register_value is None: + self.update() + val = self.register_value | (0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) def turn_off(self, **kwargs): - if self.register_value is None: - self.update() - val = self.register_value & ~(0x0001 << self.bit) - modbus.NETWORK.write_register(unit=self.slave, - address=self.register, - value=val) + """ Set switch off. """ + if self._coil: + modbus.NETWORK.write_coil(self.register, False) + else: + if self.register_value is None: + self.update() + val = self.register_value & ~(0x0001 << self.bit) + modbus.NETWORK.write_register(unit=self.slave, + address=self.register, + value=val) def update(self): - result = modbus.NETWORK.read_holding_registers(unit=self.slave, - address=self.register, - count=1) - val = 0 - for i, res in enumerate(result.registers): - val += res * (2**(i*16)) - self.register_value = val - self._is_on = (val & (0x0001 << self.bit) > 0) + """ Update the state of the switch. """ + if self._coil: + result = modbus.NETWORK.read_coils(self.register, 1) + self.register_value = result.bits[0] + self._is_on = self.register_value + else: + result = modbus.NETWORK.read_holding_registers( + unit=self.slave, address=self.register, + count=1) + val = 0 + for i, res in enumerate(result.registers): + val += res * (2**(i*16)) + self.register_value = val + self._is_on = (val & (0x0001 << self.bit) > 0) From 3c08a5ee6ebff7f8fee61cc559065864e301a1b0 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sat, 1 Aug 2015 12:20:29 -0700 Subject: [PATCH 038/111] Added support for temper temperature sensors --- homeassistant/components/sensor/temper.py | 50 +++++++++++++++++++++++ requirements.txt | 3 ++ 2 files changed, 53 insertions(+) create mode 100644 homeassistant/components/sensor/temper.py diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py new file mode 100644 index 00000000000..1888ba05d3c --- /dev/null +++ b/homeassistant/components/sensor/temper.py @@ -0,0 +1,50 @@ +""" +homeassistant.components.sensor.temper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support for getting temperature from TEMPer devices +""" + +import logging +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """ Find and return Temper sensors. """ + try: + # pylint: disable=no-name-in-module, import-error + from temperusb.temper import TemperHandler + except ImportError: + _LOGGER.error('Failed to import temperusb') + return False + + temp_unit = hass.config.temperature_unit + temper_devices = TemperHandler().get_devices() + add_devices_callback([TemperSensor(dev, temp_unit) for dev in temper_devices]) + + +class TemperSensor(Entity): + def __init__(self, temper_device, temp_unit): + self.temper_device = temper_device + self.temp_unit = temp_unit + self.current_value = None + + @property + def state(self): + """ Returns the state of the entity. """ + return self.current_value + + @property + def unit_of_measurement(self): + """ Unit of measurement of this entity, if any. """ + return self.temp_unit + + def update(self): + """ Retrieve latest state. """ + try: + self.current_value = self.temper_device.get_temperature() + except Exception: + _LOGGER.error('Failed to get temperature due to insufficient permissions') diff --git a/requirements.txt b/requirements.txt index c7a569fad33..a16eadeaeb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -79,3 +79,6 @@ PyMata==2.07a # Mysensors serial gateway pyserial>=2.7 + +# Temper sensors +https://github.com/rkabadi/temper-python/archive/master.zip From 0e153183d4b760d1124aaf2f4361130388d9ad15 Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sat, 1 Aug 2015 12:46:28 -0700 Subject: [PATCH 039/111] Added name for temper, added to coveragerc, --- .coveragerc | 1 + homeassistant/components/sensor/temper.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.coveragerc b/.coveragerc index 39a3dee22bf..ac312bad8c2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -51,6 +51,7 @@ omit = homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/swiss_public_transport.py homeassistant/components/sensor/systemmonitor.py + homeassistant/components/sensor/temper.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py homeassistant/components/switch/hikvisioncam.py diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index 1888ba05d3c..b99ee407400 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -7,6 +7,7 @@ Support for getting temperature from TEMPer devices import logging from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME _LOGGER = logging.getLogger(__name__) @@ -22,15 +23,23 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): return False temp_unit = hass.config.temperature_unit + name = config.get(CONF_NAME, DEVICE_DEFAULT_NAME) temper_devices = TemperHandler().get_devices() - add_devices_callback([TemperSensor(dev, temp_unit) for dev in temper_devices]) + add_devices_callback([TemperSensor(dev, temp_unit, name + '_' + str(idx)) + for idx, dev in enumerate(temper_devices)]) class TemperSensor(Entity): - def __init__(self, temper_device, temp_unit): + def __init__(self, temper_device, temp_unit, name): self.temper_device = temper_device self.temp_unit = temp_unit self.current_value = None + self._name = name + + @property + def name(self): + """ Returns the name of the temperature sensor. """ + return self._name @property def state(self): @@ -47,4 +56,5 @@ class TemperSensor(Entity): try: self.current_value = self.temper_device.get_temperature() except Exception: - _LOGGER.error('Failed to get temperature due to insufficient permissions') + _LOGGER.error('Failed to get temperature due to insufficient ' + 'permissions. Try running with "sudo"') From c248d5455e7f21971527df8ab4637e3f8189882c Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sun, 2 Aug 2015 18:01:31 -0700 Subject: [PATCH 040/111] Added REQUIREMENTS lilst to edimax.py --- homeassistant/components/switch/edimax.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index ab461f4de07..171183900df 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -15,6 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD,\ DEFAULT_USERNAME = 'admin' DEFAULT_PASSWORD = '1234' DEVICE_DEFAULT_NAME = 'Edimax Smart Plug' +REQUIREMENTS = ['https://github.com/rkabadi/pyedimax/archive/master.zip'] # setup logger _LOGGER = logging.getLogger(__name__) From e6aabb9706fde49dd5f6417c522c0bdf728e763c Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sun, 2 Aug 2015 18:51:13 -0700 Subject: [PATCH 041/111] Fixed flake8 violations --- homeassistant/components/sensor/temper.py | 3 ++- requirements.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index b99ee407400..ab95ab70793 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -30,6 +30,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class TemperSensor(Entity): + """ Represents an Temper temperature sensor within Home Assistant. """ def __init__(self, temper_device, temp_unit, name): self.temper_device = temper_device self.temp_unit = temp_unit @@ -55,6 +56,6 @@ class TemperSensor(Entity): """ Retrieve latest state. """ try: self.current_value = self.temper_device.get_temperature() - except Exception: + except IOError: _LOGGER.error('Failed to get temperature due to insufficient ' 'permissions. Try running with "sudo"') diff --git a/requirements.txt b/requirements.txt index f0f49e5a248..7d8e7839d68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -91,3 +91,6 @@ pywemo>=0.1 # Wink (*.wink) https://github.com/balloob/python-wink/archive/master.zip#pywink>=0.1 + +# Temper sensors +https://github.com/rkabadi/temper-python/archive/master.zip \ No newline at end of file From 6c6ae9cb1af9c49375ee8df751fa52f7a654d88e Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sun, 2 Aug 2015 18:55:30 -0700 Subject: [PATCH 042/111] Added REQUIREMENTS list to temper.py --- homeassistant/components/sensor/temper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index ab95ab70793..c881ce801ce 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['https://github.com/rkabadi/temper-python/archive/master.zip'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): From 65d32c742559cf9b52eed3989fa12af2d42a154f Mon Sep 17 00:00:00 2001 From: Rohit Kabadi Date: Sun, 2 Aug 2015 18:58:30 -0700 Subject: [PATCH 043/111] Added blank line to temper.py --- homeassistant/components/sensor/temper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/temper.py b/homeassistant/components/sensor/temper.py index c881ce801ce..d52164fd0e1 100644 --- a/homeassistant/components/sensor/temper.py +++ b/homeassistant/components/sensor/temper.py @@ -13,6 +13,7 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['https://github.com/rkabadi/temper-python/archive/master.zip'] + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return Temper sensors. """ From 7870e9a5e23b5a35b1514089af21befd234f4416 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Aug 2015 17:05:33 +0200 Subject: [PATCH 044/111] Minor cleanup core --- homeassistant/__init__.py | 14 +++++---- homeassistant/bootstrap.py | 31 +++++++++---------- homeassistant/components/http.py | 12 +++++--- homeassistant/config.py | 11 +++---- homeassistant/helpers/entity_component.py | 16 ---------- homeassistant/loader.py | 37 +++++++++++------------ homeassistant/util/__init__.py | 7 ++--- 7 files changed, 55 insertions(+), 73 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index c62128453e7..bfbb944b23f 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -33,7 +33,7 @@ TIMER_INTERVAL = 1 # seconds SERVICE_CALL_LIMIT = 10 # seconds # Define number of MINIMUM worker threads. -# During bootstrap of HA (see bootstrap.from_config_dict()) worker threads +# During bootstrap of HA (see bootstrap._setup_component()) worker threads # will be added for each component that polls devices. MIN_WORKER_THREAD = 2 @@ -42,7 +42,7 @@ ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$") _LOGGER = logging.getLogger(__name__) -# Temporary addition to proxy deprecated methods +# Temporary to support deprecated methods _MockHA = namedtuple("MockHomeAssistant", ['bus']) @@ -62,7 +62,6 @@ class HomeAssistant(object): "Starting Home Assistant (%d threads)", self.pool.worker_count) create_timer(self) - self.bus.fire(EVENT_HOMEASSISTANT_START) def block_till_stopped(self): @@ -70,13 +69,16 @@ class HomeAssistant(object): will block until called. """ request_shutdown = threading.Event() - self.services.register(DOMAIN, SERVICE_HOMEASSISTANT_STOP, - lambda service: request_shutdown.set()) + def stop_homeassistant(service): + """ Stops Home Assistant. """ + request_shutdown.set() + + self.services.register( + DOMAIN, SERVICE_HOMEASSISTANT_STOP, stop_homeassistant) while not request_shutdown.isSet(): try: time.sleep(1) - except KeyboardInterrupt: break diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 2da2f4fb7b5..663ed611de4 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -83,33 +83,30 @@ def _setup_component(hass, domain, config): _LOGGER.error( 'Not initializing %s because not all dependencies loaded: %s', domain, ", ".join(missing_deps)) - return False if not _handle_requirements(component, domain): return False try: - if component.setup(hass, config): - hass.config.components.append(component.DOMAIN) - - # Assumption: if a component does not depend on groups - # it communicates with devices - if group.DOMAIN not in component.DEPENDENCIES: - hass.pool.add_worker() - - hass.bus.fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}) - - return True - - else: + if not component.setup(hass, config): _LOGGER.error('component %s failed to initialize', domain) - + return False except Exception: # pylint: disable=broad-except _LOGGER.exception('Error during setup of component %s', domain) + return False - return False + hass.config.components.append(component.DOMAIN) + + # Assumption: if a component does not depend on groups + # it communicates with devices + if group.DOMAIN not in component.DEPENDENCIES: + hass.pool.add_worker() + + hass.bus.fire( + EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN}) + + return True def prepare_setup_platform(hass, config, domain, platform_name): diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index cb8e87490f3..fcd17356885 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -187,10 +187,12 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): _LOGGER.info("running http in development mode") def start(self): - """ Starts the server. """ - self.hass.bus.listen_once( - ha.EVENT_HOMEASSISTANT_STOP, - lambda event: self.shutdown()) + """ Starts the HTTP server. """ + def stop_http(event): + """ Stops the HTTP server. """ + self.shutdown() + + self.hass.bus.listen_once(ha.EVENT_HOMEASSISTANT_STOP, stop_http) _LOGGER.info( "Starting web interface at http://%s:%d", *self.server_address) @@ -203,7 +205,7 @@ class HomeAssistantHTTPServer(ThreadingMixIn, HTTPServer): self.serve_forever() def register_path(self, method, url, callback, require_auth=True): - """ Regitsters a path wit the server. """ + """ Registers a path wit the server. """ self.paths.append((method, url, callback, require_auth)) diff --git a/homeassistant/config.py b/homeassistant/config.py index ec177776d8f..629001b972f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -120,17 +120,16 @@ def load_yaml_config_file(config_path): import yaml def parse(fname): - """ Actually parse the file. """ + """ Parse a YAML file. """ try: with open(fname) as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict - conf_dict = yaml.load(conf_file) or {} + return yaml.load(conf_file) or {} except yaml.YAMLError: - _LOGGER.exception('Error reading YAML configuration file %s', - fname) - raise HomeAssistantError() - return conf_dict + error = 'Error reading YAML configuration file {}'.format(fname) + _LOGGER.exception(error) + raise HomeAssistantError(error) def yaml_include(loader, node): """ diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 381584fabb8..0a120afa2b3 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -135,22 +135,6 @@ class EntityComponent(object): self.hass, platform_config, self.add_entities, discovery_info) self.hass.config.components.append(platform_name) - - except AttributeError: - # AttributeError if setup_platform does not exist - # Support old deprecated method for now - 3/1/2015 - if hasattr(platform, 'get_devices'): - self.logger.warning( - 'Please upgrade %s to return new entities using ' - 'setup_platform. See %s/demo.py for an example.', - platform_name, self.domain) - self.add_entities( - platform.get_devices(self.hass, platform_config)) - - else: - self.logger.exception( - 'Error while setting up platform %s', platform_type) - except Exception: # pylint: disable=broad-except self.logger.exception( 'Error while setting up platform %s', platform_type) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index d38e0df4465..b0eb8d287db 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -61,11 +61,10 @@ def prepare(hass): # python components. If this assumption is not true, HA won't break, # just might output more errors. for fil in os.listdir(custom_path): - if os.path.isdir(os.path.join(custom_path, fil)): - if fil != '__pycache__': - AVAILABLE_COMPONENTS.append( - 'custom_components.{}'.format(fil)) - + if fil == '__pycache__': + continue + elif os.path.isdir(os.path.join(custom_path, fil)): + AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) else: # For files we will strip out .py extension AVAILABLE_COMPONENTS.append( @@ -195,24 +194,24 @@ def _load_order_component(comp_name, load_order, loading): for dependency in component.DEPENDENCIES: # Check not already loaded - if dependency not in load_order: - # If we are already loading it, we have a circular dependency - if dependency in loading: - _LOGGER.error('Circular dependency detected: %s -> %s', - comp_name, dependency) + if dependency in load_order: + continue - return OrderedSet() + # If we are already loading it, we have a circular dependency + if dependency in loading: + _LOGGER.error('Circular dependency detected: %s -> %s', + comp_name, dependency) + return OrderedSet() - dep_load_order = _load_order_component( - dependency, load_order, loading) + dep_load_order = _load_order_component(dependency, load_order, loading) - # length == 0 means error loading dependency or children - if len(dep_load_order) == 0: - _LOGGER.error('Error loading %s dependency: %s', - comp_name, dependency) - return OrderedSet() + # length == 0 means error loading dependency or children + if len(dep_load_order) == 0: + _LOGGER.error('Error loading %s dependency: %s', + comp_name, dependency) + return OrderedSet() - load_order.update(dep_load_order) + load_order.update(dep_load_order) load_order.add(comp_name) loading.remove(comp_name) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 90476088d25..a75d9837de6 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -94,13 +94,12 @@ def get_local_ip(): # Use Google Public DNS server to determine own IP sock.connect(('8.8.8.8', 80)) - ip_addr = sock.getsockname()[0] - sock.close() - - return ip_addr + return sock.getsockname()[0] except socket.error: return socket.gethostbyname(socket.gethostname()) + finally: + sock.close() # Taken from http://stackoverflow.com/a/23728630 From 382c1de981266019665ad5b695177cbff96396ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Aug 2015 17:08:13 +0200 Subject: [PATCH 045/111] Built-in components no longer use deprecated methods --- homeassistant/components/automation/time.py | 6 +++--- homeassistant/components/device_sun_light_trigger.py | 7 ++++--- homeassistant/components/device_tracker/__init__.py | 3 ++- homeassistant/components/scheduler/time.py | 3 ++- homeassistant/components/script.py | 5 +++-- homeassistant/components/sun.py | 8 +++++--- homeassistant/helpers/entity_component.py | 5 +++-- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 7e38960534d..77bd40a7a41 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -5,6 +5,7 @@ homeassistant.components.automation.time Offers time listening automation rules. """ from homeassistant.util import convert +from homeassistant.helpers.event import track_time_change CONF_HOURS = "time_hours" CONF_MINUTES = "time_minutes" @@ -21,8 +22,7 @@ def register(hass, config, action): """ Listens for time changes and calls action. """ action() - hass.track_time_change( - time_automation_listener, - hour=hours, minute=minutes, second=seconds) + track_time_change(hass, time_automation_listener, + hour=hours, minute=minutes, second=seconds) return True diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index c53fff0e4f3..7c3be8894c4 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -8,6 +8,7 @@ the state of the sun and devices. import logging from datetime import timedelta +from homeassistant.helpers.event import track_point_in_time import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME from . import light, sun, device_tracker, group @@ -91,9 +92,9 @@ def setup(hass, config): if start_point: for index, light_id in enumerate(light_ids): - hass.track_point_in_time(turn_on(light_id), - (start_point + - index * LIGHT_TRANSITION_TIME)) + track_point_in_time( + hass, turn_on(light_id), + (start_point + index * LIGHT_TRANSITION_TIME)) # Track every time sun rises so we can schedule a time-based # pre-sun set event diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 611136aac5b..452480b12c9 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers import validate_config import homeassistant.util as util import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_utc_time_change from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, CONF_PLATFORM, DEVICE_DEFAULT_NAME) @@ -134,7 +135,7 @@ class DeviceTracker(object): seconds = range(0, 60, seconds) _LOGGER.info("Device tracker interval second=%s", seconds) - hass.track_utc_time_change(update_device_state, second=seconds) + track_utc_time_change(hass, update_device_state, second=seconds) hass.services.register(DOMAIN, SERVICE_DEVICE_TRACKER_RELOAD, diff --git a/homeassistant/components/scheduler/time.py b/homeassistant/components/scheduler/time.py index 793f33d0502..9fec19fbe57 100644 --- a/homeassistant/components/scheduler/time.py +++ b/homeassistant/components/scheduler/time.py @@ -17,6 +17,7 @@ from datetime import timedelta import logging import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_point_in_time from homeassistant.components.scheduler import ServiceEventListener _LOGGER = logging.getLogger(__name__) @@ -62,7 +63,7 @@ class TimeEventListener(ServiceEventListener): """ Call the execute method """ self.execute(hass) - hass.track_point_in_time(execute, next_time) + track_point_in_time(hass, execute, next_time) _LOGGER.info( 'TimeEventListener scheduled for %s, will call service %s.%s', diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 97e12c47a46..788eb8af96d 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -10,6 +10,7 @@ from datetime import timedelta import homeassistant.util.dt as date_util import threading +from homeassistant.helpers.event import track_point_in_time from homeassistant.util import split_entity_id from homeassistant.const import ( STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, EVENT_TIME_CHANGED) @@ -111,8 +112,8 @@ class Script(object): elif CONF_DELAY in action: delay = timedelta(**action[CONF_DELAY]) point_in_time = date_util.now() + delay - self.listener = self.hass.track_point_in_time( - self, point_in_time) + self.listener = track_point_in_time( + self.hass, self, point_in_time) return False return True diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index af4a93825ac..507c4a2b63b 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -25,6 +25,8 @@ import urllib import homeassistant.util as util import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import ( + track_point_in_utc_time, track_point_in_time) from homeassistant.helpers.entity import Entity from homeassistant.components.scheduler import ServiceEventListener @@ -209,8 +211,8 @@ class Sun(Entity): self.update_ha_state() # Schedule next update at next_change+1 second so sun state has changed - self.hass.track_point_in_utc_time( - self.point_in_time_listener, + track_point_in_utc_time( + self.hass, self.point_in_time_listener, self.next_change + timedelta(seconds=1)) @@ -272,7 +274,7 @@ class SunEventListener(ServiceEventListener): """ Call the execute method. """ self.execute(hass) - hass.track_point_in_time(execute, next_time) + track_point_in_time(hass, execute, next_time) return next_time diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 0a120afa2b3..80084178fe0 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,6 +7,7 @@ Provides helpers for components that manage entities. from homeassistant.bootstrap import prepare_setup_platform from homeassistant.helpers import ( generate_entity_id, config_per_platform, extract_entity_ids) +from homeassistant.helpers.event import track_utc_time_change from homeassistant.components import group, discovery from homeassistant.const import ATTR_ENTITY_ID @@ -115,8 +116,8 @@ class EntityComponent(object): self.is_polling = True - self.hass.track_time_change( - self._update_entity_states, + track_utc_time_change( + self.hass, self._update_entity_states, second=range(0, 60, self.scan_interval)) def _setup_platform(self, platform_type, platform_config, From 4096a672519822bd53dce1eb0b0f51882bee340d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Aug 2015 17:42:28 +0200 Subject: [PATCH 046/111] Built-in component cleanup --- homeassistant/components/light/__init__.py | 131 +++++++++--------- .../components/media_player/__init__.py | 46 +++--- homeassistant/components/switch/__init__.py | 4 - 3 files changed, 85 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 58c3c3cd8f2..fe1991c7ed1 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -166,26 +166,25 @@ def setup(hass, config): profiles = {} for profile_path in profile_paths: + if not os.path.isfile(profile_path): + continue + with open(profile_path) as inp: + reader = csv.reader(inp) - if os.path.isfile(profile_path): - with open(profile_path) as inp: - reader = csv.reader(inp) + # Skip the header + next(reader, None) - # Skip the header - next(reader, None) + try: + for profile_id, color_x, color_y, brightness in reader: + profiles[profile_id] = (float(color_x), float(color_y), + int(brightness)) + except ValueError: + # ValueError if not 4 values per row + # ValueError if convert to float/int failed + _LOGGER.error( + "Error parsing light profiles from %s", profile_path) - try: - for profile_id, color_x, color_y, brightness in reader: - profiles[profile_id] = (float(color_x), float(color_y), - int(brightness)) - - except ValueError: - # ValueError if not 4 values per row - # ValueError if convert to float/int failed - _LOGGER.error( - "Error parsing light profiles from %s", profile_path) - - return False + return False def handle_light_service(service): """ Hande a turn light on or off service call. """ @@ -206,66 +205,70 @@ def setup(hass, config): for light in target_lights: light.turn_off(**params) - else: - # Processing extra data for turn light on request + if light.should_poll: + for light in target_lights: + light.update_ha_state(True) + return - # We process the profile first so that we get the desired - # behavior that extra service data attributes overwrite - # profile values - profile = profiles.get(dat.get(ATTR_PROFILE)) + # Processing extra data for turn light on request - if profile: - *params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile + # We process the profile first so that we get the desired + # behavior that extra service data attributes overwrite + # profile values + profile = profiles.get(dat.get(ATTR_PROFILE)) - if ATTR_BRIGHTNESS in dat: - # We pass in the old value as the default parameter if parsing - # of the new one goes wrong. - params[ATTR_BRIGHTNESS] = util.convert( - dat.get(ATTR_BRIGHTNESS), int, params.get(ATTR_BRIGHTNESS)) + if profile: + *params[ATTR_XY_COLOR], params[ATTR_BRIGHTNESS] = profile - if ATTR_XY_COLOR in dat: - try: - # xy_color should be a list containing 2 floats - xycolor = dat.get(ATTR_XY_COLOR) + if ATTR_BRIGHTNESS in dat: + # We pass in the old value as the default parameter if parsing + # of the new one goes wrong. + params[ATTR_BRIGHTNESS] = util.convert( + dat.get(ATTR_BRIGHTNESS), int, params.get(ATTR_BRIGHTNESS)) - # Without this check, a xycolor with value '99' would work - if not isinstance(xycolor, str): - params[ATTR_XY_COLOR] = [float(val) for val in xycolor] + if ATTR_XY_COLOR in dat: + try: + # xy_color should be a list containing 2 floats + xycolor = dat.get(ATTR_XY_COLOR) - except (TypeError, ValueError): - # TypeError if xy_color is not iterable - # ValueError if value could not be converted to float - pass + # Without this check, a xycolor with value '99' would work + if not isinstance(xycolor, str): + params[ATTR_XY_COLOR] = [float(val) for val in xycolor] - if ATTR_RGB_COLOR in dat: - try: - # rgb_color should be a list containing 3 ints - rgb_color = dat.get(ATTR_RGB_COLOR) + except (TypeError, ValueError): + # TypeError if xy_color is not iterable + # ValueError if value could not be converted to float + pass - if len(rgb_color) == 3: - params[ATTR_XY_COLOR] = \ - color_util.color_RGB_to_xy(int(rgb_color[0]), - int(rgb_color[1]), - int(rgb_color[2])) + if ATTR_RGB_COLOR in dat: + try: + # rgb_color should be a list containing 3 ints + rgb_color = dat.get(ATTR_RGB_COLOR) - except (TypeError, ValueError): - # TypeError if rgb_color is not iterable - # ValueError if not all values can be converted to int - pass + if len(rgb_color) == 3: + params[ATTR_XY_COLOR] = \ + color_util.color_RGB_to_xy(int(rgb_color[0]), + int(rgb_color[1]), + int(rgb_color[2])) - if ATTR_FLASH in dat: - if dat[ATTR_FLASH] == FLASH_SHORT: - params[ATTR_FLASH] = FLASH_SHORT + except (TypeError, ValueError): + # TypeError if rgb_color is not iterable + # ValueError if not all values can be converted to int + pass - elif dat[ATTR_FLASH] == FLASH_LONG: - params[ATTR_FLASH] = FLASH_LONG + if ATTR_FLASH in dat: + if dat[ATTR_FLASH] == FLASH_SHORT: + params[ATTR_FLASH] = FLASH_SHORT - if ATTR_EFFECT in dat: - if dat[ATTR_EFFECT] == EFFECT_COLORLOOP: - params[ATTR_EFFECT] = EFFECT_COLORLOOP + elif dat[ATTR_FLASH] == FLASH_LONG: + params[ATTR_FLASH] = FLASH_LONG - for light in target_lights: - light.turn_on(**params) + if ATTR_EFFECT in dat: + if dat[ATTR_EFFECT] == EFFECT_COLORLOOP: + params[ATTR_EFFECT] = EFFECT_COLORLOOP + + for light in target_lights: + light.turn_on(**params) for light in target_lights: if light.should_poll: diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 0cb4608ba1b..d4b55807ab2 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -98,9 +98,7 @@ ATTR_TO_PROPERTY = [ def is_on(hass, entity_id=None): """ Returns true if specified media player entity_id is on. Will check all media player if no entity_id specified. """ - entity_ids = [entity_id] if entity_id else hass.states.entity_ids(DOMAIN) - return any(not hass.states.is_state(entity_id, STATE_OFF) for entity_id in entity_ids) @@ -108,28 +106,24 @@ def is_on(hass, entity_id=None): def turn_on(hass, entity_id=None): """ Will turn on specified media player or all. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None): """ Will turn off specified media player or all. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) def volume_up(hass, entity_id=None): """ Send the media player the command for volume up. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) def volume_down(hass, entity_id=None): """ Send the media player the command for volume down. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) @@ -156,35 +150,30 @@ def set_volume_level(hass, volume, entity_id=None): def media_play_pause(hass, entity_id=None): """ Send the media player the command for play/pause. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) def media_play(hass, entity_id=None): """ Send the media player the command for play/pause. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) def media_pause(hass, entity_id=None): """ Send the media player the command for play/pause. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) def media_next_track(hass, entity_id=None): """ Send the media player the command for next track. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) def media_previous_track(hass, entity_id=None): """ Send the media player the command for prev track. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) @@ -262,29 +251,30 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_MEDIA_SEEK, media_seek_service) - def play_youtube_video_service(service, media_id): + def play_youtube_video_service(service, media_id=None): """ Plays specified media_id on the media player. """ - target_players = component.extract_from_service(service) + if media_id is None: + service.data.get('video') - if media_id: - for player in target_players: - player.play_youtube(media_id) + if media_id is None: + return - if player.should_poll: - player.update_ha_state(True) + for player in component.extract_from_service(service): + player.play_youtube(media_id) - hass.services.register(DOMAIN, "start_fireplace", - lambda service: - play_youtube_video_service(service, "eyU3bRy2x44")) + if player.should_poll: + player.update_ha_state(True) - hass.services.register(DOMAIN, "start_epic_sax", - lambda service: - play_youtube_video_service(service, "kxopViU98Xo")) + hass.services.register( + DOMAIN, "start_fireplace", + lambda service: play_youtube_video_service(service, "eyU3bRy2x44")) - hass.services.register(DOMAIN, SERVICE_YOUTUBE_VIDEO, - lambda service: - play_youtube_video_service( - service, service.data.get('video'))) + hass.services.register( + DOMAIN, "start_epic_sax", + lambda service: play_youtube_video_service(service, "kxopViU98Xo")) + + hass.services.register( + DOMAIN, SERVICE_YOUTUBE_VIDEO, play_youtube_video_service) return True diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 95457b66f5f..e2772aed8fd 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -45,21 +45,18 @@ _LOGGER = logging.getLogger(__name__) def is_on(hass, entity_id=None): """ Returns if the switch is on based on the statemachine. """ entity_id = entity_id or ENTITY_ID_ALL_SWITCHES - return hass.states.is_state(entity_id, STATE_ON) def turn_on(hass, entity_id=None): """ Turns all or specified switch on. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_ON, data) def turn_off(hass, entity_id=None): """ Turns all or specified switch off. """ data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) @@ -84,7 +81,6 @@ def setup(hass, config): switch.update_ha_state(True) hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_switch_service) - hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service) return True From 76f63ee262c3a4c55d86f3b9397fb5199792655d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 3 Aug 2015 17:57:12 +0200 Subject: [PATCH 047/111] Clean up test code --- tests/common.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/tests/common.py b/tests/common.py index 65fd0659108..bad0723530b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -52,30 +52,28 @@ def mock_service(hass, domain, service): return calls +def fire_time_changed(hass, time): + hass.bus.fire(EVENT_TIME_CHANGED, {'now': time}) + + def trigger_device_tracker_scan(hass): """ Triggers the device tracker to scan. """ - hass.bus.fire( - EVENT_TIME_CHANGED, - {'now': - dt_util.utcnow().replace(second=0) + timedelta(hours=1)}) + fire_time_changed( + hass, dt_util.utcnow().replace(second=0) + timedelta(hours=1)) def ensure_sun_risen(hass): """ Trigger sun to rise if below horizon. """ - if not sun.is_on(hass): - hass.bus.fire( - EVENT_TIME_CHANGED, - {'now': - sun.next_rising_utc(hass) + timedelta(seconds=10)}) + if sun.is_on(hass): + return + fire_time_changed(hass, sun.next_rising_utc(hass) + timedelta(seconds=10)) def ensure_sun_set(hass): """ Trigger sun to set if above horizon. """ - if sun.is_on(hass): - hass.bus.fire( - EVENT_TIME_CHANGED, - {'now': - sun.next_setting_utc(hass) + timedelta(seconds=10)}) + if not sun.is_on(hass): + return + fire_time_changed(hass, sun.next_setting_utc(hass) + timedelta(seconds=10)) def mock_state_change_event(hass, new_state, old_state=None): From bed30a5307f3da8b5c13455df4623f7d53cf51ff Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Tue, 4 Aug 2015 17:22:56 +0200 Subject: [PATCH 048/111] added support for logitech squeezebox --- .../components/media_player/squeezebox.py | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 homeassistant/components/media_player/squeezebox.py diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py new file mode 100644 index 00000000000..4d88d226f9d --- /dev/null +++ b/homeassistant/components/media_player/squeezebox.py @@ -0,0 +1,273 @@ +""" +homeassistant.components.media_player.squeezebox +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Provides an interface to the Logitech SqueezeBox API + +Configuration: + +To use SqueezeBox add something like this to your configuration: + +media_player: + platform: squeezebox + name: SqueezeBox + server: 192.168.1.21 + player: Player1 + port: 9090 + user: user + password: password + +Variables: + +name +*Optional +The name of the device + +server +*Required +The address of the Logitech Media Server + +player +*Required +The unique name of the player + +port +*Optional +Telnet port to Logitech Media Server, default 9090 + +user +*Optional +User, if password protection is enabled + +password +*Optional +Password, if password protection is enabled +""" + +import logging +import telnetlib +import urllib.parse + +from homeassistant.components.media_player import ( + MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_MUSIC) +from homeassistant.const import ( + STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ + SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the squeezebox platform. """ + add_devices([ + SqueezeBoxDevice( + config.get('name', 'SqueezeBox'), + config.get('server'), + config.get('player'), + config.get('port', '9090'), + config.get('user', None), + config.get('password', None) + )]) + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-many-public-methods +class SqueezeBoxDevice(MediaPlayerDevice): + """ Represents a SqueezeBox device. """ + + def __init__(self, name, server, player, port, user, password): + super(SqueezeBoxDevice, self).__init__() + self._name = name + self._server = server + self._player = player + self._port = port + self._user = user + self._password = password + self._status = {} + self.update() + self._http_port = self._query('pref', 'httpport', '?') + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + if ('power' in self._status and self._status['power'] == '0'): + return STATE_OFF + if('mode' in self._status): + if self._status['mode'] == 'pause': + return STATE_PAUSED + if self._status['mode'] == 'play': + return STATE_PLAYING + if self._status['mode'] == 'stop': + return STATE_IDLE + return STATE_UNKNOWN + + def update(self): + if(self._user and self._password): + self._query('login', self._user, self._password) + self._get_status() + + def _query(self, *parameters): + """ Send request and await response from server """ + telnet = telnetlib.Telnet(self._server, self._port) + message = '{}\n'.format(' '.join(parameters)) + telnet.write(message.encode('UTF-8')) + response = telnet.read_until(b'\n', timeout=3)\ + .decode('UTF-8')\ + .split(' ')[-1]\ + .strip() + telnet.write(b'exit\n') + return urllib.parse.unquote(response) + + def _get_status(self): + """ request status and parse result """ + # (title) : Song title + # Requested Information + # a (artist): Artist name 'artist' + # d (duration): Song duration in seconds 'duration' + # K (artwork_url): URL to remote artwork + tags = 'adK' + new_status = {} + telnet = telnetlib.Telnet(self._server, self._port) + telnet.write('{player} status - 1 tags:{tags}\n'.format( + player=self._player, + tags=tags + ).encode('UTF-8')) + response = telnet.read_until(b'\n', timeout=3)\ + .decode('UTF-8')\ + .split(' ') + telnet.write(b'exit\n') + for item in response: + parts = urllib.parse.unquote(item).partition(':') + new_status[parts[0]] = parts[2] + self._status = new_status + + @property + def volume_level(self): + """ Volume level of the media player (0..1). """ + if('mixer volume' in self._status): + return int(self._status['mixer volume']) / 100.0 + + @property + def is_volume_muted(self): + if('mixer volume' in self._status): + return int(self._status['mixer volume']) < 0 + + @property + def media_content_id(self): + """ Content ID of current playing media. """ + if('current_title' in self._status): + return self._status['current_title'] + + @property + def media_content_type(self): + """ Content type of current playing media. """ + return MEDIA_TYPE_MUSIC + + @property + def media_duration(self): + """ Duration of current playing media in seconds. """ + if('duration' in self._status): + return int(float(self._status['duration'])) + + @property + def media_image_url(self): + """ Image url of current playing media. """ + if('artwork_url' in self._status): + return self._status['artwork_url'] + return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\ + .format( + server=self._server, + port=self._http_port, + player=self._player) + + @property + def media_title(self): + """ Title of current playing media. """ + if('artist' in self._status and 'title' in self._status): + return '{artist} - {title}'.format( + artist=self._status['artist'], + title=self._status['title'] + ) + if('current_title' in self._status): + return self._status['current_title'] + + @property + def supported_media_commands(self): + """ Flags of media commands that are supported. """ + return SUPPORT_SQUEEZEBOX + + def turn_off(self): + """ turn_off media player. """ + self._query(self._player, 'power', '0') + self.update_ha_state() + + def volume_up(self): + """ volume_up media player. """ + self._query(self._player, 'mixer', 'volume', '+5') + self.update_ha_state() + + def volume_down(self): + """ volume_down media player. """ + self._query(self._player, 'mixer', 'volume', '-5') + self.update_ha_state() + + def set_volume_level(self, volume): + """ set volume level, range 0..1. """ + volume_percent = str(int(volume*100)) + self._query(self._player, 'mixer', 'volume', volume_percent) + self.update_ha_state() + + def mute_volume(self, mute): + """ mute (true) or unmute (false) media player. """ + mute_numeric = '1' if mute else '0' + self._query(self._player, 'mixer', 'muting', mute_numeric) + self.update_ha_state() + + def media_play_pause(self): + """ media_play_pause media player. """ + self._query(self._player, 'pause') + self.update_ha_state() + + def media_play(self): + """ media_play media player. """ + self._query(self._player, 'play') + self.update_ha_state() + + def media_pause(self): + """ media_pause media player. """ + self._query(self._player, 'pause', '0') + self.update_ha_state() + + def media_next_track(self): + """ Send next track command. """ + self._query(self._player, 'playlist', 'index', '+1') + self.update_ha_state() + + def media_previous_track(self): + """ Send next track command. """ + self._query(self._player, 'playlist', 'index', '-1') + self.update_ha_state() + + def media_seek(self, position): + """ Send seek command. """ + self._query(self._player, 'time', position) + self.update_ha_state() + + def turn_on(self): + """ turn the media player on. """ + self._query(self._player, 'power', '1') + self.update_ha_state() + + def play_youtube(self, media_id): + """ Plays a YouTube media. """ + raise NotImplementedError() From e47ac9658776c8a6d40917741e38e3d270890ef7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Aug 2015 18:13:35 +0200 Subject: [PATCH 049/111] Remove more deprecated method calls --- homeassistant/components/automation/state.py | 5 +++-- .../components/device_sun_light_trigger.py | 14 +++++++------- homeassistant/components/group.py | 5 +++-- homeassistant/components/scene.py | 5 +++-- homeassistant/components/simple_alarm.py | 9 +++++---- .../components/thermostat/heat_control.py | 13 +++++++------ 6 files changed, 28 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index c8adfe95bbe..ba96debf9ac 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,6 +6,7 @@ Offers state listening automation rules. """ import logging +from homeassistant.helpers.event import track_state_change from homeassistant.const import MATCH_ALL @@ -30,7 +31,7 @@ def register(hass, config, action): """ Listens for state changes and calls action. """ action() - hass.states.track_change( - entity_id, state_automation_listener, from_state, to_state) + track_state_change( + hass, entity_id, state_automation_listener, from_state, to_state) return True diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index 7c3be8894c4..67da9e26a82 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -8,7 +8,7 @@ the state of the sun and devices. import logging from datetime import timedelta -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.event import track_point_in_time, track_state_change import homeassistant.util.dt as dt_util from homeassistant.const import STATE_HOME, STATE_NOT_HOME from . import light, sun, device_tracker, group @@ -98,8 +98,8 @@ def setup(hass, config): # Track every time sun rises so we can schedule a time-based # pre-sun set event - hass.states.track_change(sun.ENTITY_ID, schedule_light_on_sun_rise, - sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) + track_state_change(hass, sun.ENTITY_ID, schedule_light_on_sun_rise, + sun.STATE_BELOW_HORIZON, sun.STATE_ABOVE_HORIZON) # If the sun is already above horizon # schedule the time-based pre-sun set event @@ -158,13 +158,13 @@ def setup(hass, config): light.turn_off(hass, light_ids) # Track home coming of each device - hass.states.track_change( - device_entity_ids, check_light_on_dev_state_change, + track_state_change( + hass, device_entity_ids, check_light_on_dev_state_change, STATE_NOT_HOME, STATE_HOME) # Track when all devices are gone to shut down lights - hass.states.track_change( - device_group, check_light_on_dev_state_change, + track_state_change( + hass, device_group, check_light_on_dev_state_change, STATE_HOME, STATE_NOT_HOME) return True diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 20931f2b363..ac5e8cd4116 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -7,6 +7,7 @@ Provides functionality to group devices that can be turned on or off. import homeassistant as ha from homeassistant.helpers import generate_entity_id +from homeassistant.helpers.event import track_state_change from homeassistant.helpers.entity import Entity import homeassistant.util as util from homeassistant.const import ( @@ -162,8 +163,8 @@ class Group(Entity): def start(self): """ Starts the tracking. """ - self.hass.states.track_change( - self.tracking, self._state_changed_listener) + track_state_change( + self.hass, self.tracking, self._state_changed_listener) def stop(self): """ Unregisters the group from Home Assistant. """ diff --git a/homeassistant/components/scene.py b/homeassistant/components/scene.py index 979b3281300..a748e17ec5d 100644 --- a/homeassistant/components/scene.py +++ b/homeassistant/components/scene.py @@ -19,6 +19,7 @@ import logging from collections import namedtuple from homeassistant import State +from homeassistant.helpers.event import track_state_change from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import reproduce_state @@ -104,8 +105,8 @@ class Scene(ToggleEntity): self.prev_states = None self.ignore_updates = False - self.hass.states.track_change( - self.entity_ids, self.entity_state_changed) + track_state_change( + self.hass, self.entity_ids, self.entity_state_changed) self.update() diff --git a/homeassistant/components/simple_alarm.py b/homeassistant/components/simple_alarm.py index 4f2dbd768d5..46cb52fa950 100644 --- a/homeassistant/components/simple_alarm.py +++ b/homeassistant/components/simple_alarm.py @@ -9,6 +9,7 @@ Provides a simple alarm feature: import logging import homeassistant.loader as loader +from homeassistant.helpers.event import track_state_change from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME DOMAIN = "simple_alarm" @@ -83,8 +84,8 @@ def setup(hass, config): if not device_tracker.is_on(hass): unknown_alarm() - hass.states.track_change( - light.ENTITY_ID_ALL_LIGHTS, + track_state_change( + hass, light.ENTITY_ID_ALL_LIGHTS, unknown_alarm_if_lights_on, STATE_OFF, STATE_ON) def ring_known_alarm(entity_id, old_state, new_state): @@ -93,8 +94,8 @@ def setup(hass, config): known_alarm() # Track home coming of each device - hass.states.track_change( - hass.states.entity_ids(device_tracker.DOMAIN), + track_state_change( + hass, hass.states.entity_ids(device_tracker.DOMAIN), ring_known_alarm, STATE_NOT_HOME, STATE_HOME) return True diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index d21245dae3a..6b10a1b46f3 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -63,6 +63,7 @@ import datetime import homeassistant.components as core from homeassistant.components.thermostat import ThermostatDevice +from homeassistant.helpers.event import track_state_change from homeassistant.const import TEMP_CELCIUS, STATE_ON, STATE_OFF TOL_TEMP = 0.3 @@ -105,12 +106,12 @@ class HeatControl(ThermostatDevice): self._away = False self._heater_manual_changed = True - hass.states.track_change(self.heater_entity_id, - self._heater_turned_on, - STATE_OFF, STATE_ON) - hass.states.track_change(self.heater_entity_id, - self._heater_turned_off, - STATE_ON, STATE_OFF) + track_state_change(hass, self.heater_entity_id, + self._heater_turned_on, + STATE_OFF, STATE_ON) + track_state_change(hass, self.heater_entity_id, + self._heater_turned_off, + STATE_ON, STATE_OFF) @property def name(self): From 14023a15e6f8a7f3b8374cea16fb6911cb080df5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Aug 2015 18:13:55 +0200 Subject: [PATCH 050/111] Minor code cleanup --- homeassistant/__init__.py | 24 ++++++++-------------- homeassistant/components/light/__init__.py | 4 ++-- homeassistant/helpers/event.py | 2 ++ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index bfbb944b23f..f5129fd1c95 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -598,23 +598,15 @@ class ServiceRegistry(object): if call.data[ATTR_SERVICE_CALL_ID] == call_id: executed_event.set() - self._bus.remove_listener( - EVENT_SERVICE_EXECUTED, service_executed) - self._bus.listen(EVENT_SERVICE_EXECUTED, service_executed) self._bus.fire(EVENT_CALL_SERVICE, event_data) if blocking: - # wait will return False if event not set after our limit has - # passed. If not set, clean up the listener - if not executed_event.wait(SERVICE_CALL_LIMIT): - self._bus.remove_listener( - EVENT_SERVICE_EXECUTED, service_executed) - - return False - - return True + success = executed_event.wait(SERVICE_CALL_LIMIT) + self._bus.remove_listener( + EVENT_SERVICE_EXECUTED, service_executed) + return success def _event_to_service_call(self, event): """ Calls a service from an event. """ @@ -675,8 +667,8 @@ class Config(object): def temperature(self, value, unit): """ Converts temperature to user preferred unit if set. """ - if not (unit and self.temperature_unit and - unit != self.temperature_unit): + if not (unit in (TEMP_CELCIUS, TEMP_FAHRENHEIT) and + self.temperature_unit and unit != self.temperature_unit): return value, unit try: @@ -783,7 +775,7 @@ def create_timer(hass, interval=TIMER_INTERVAL): hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_timer) -def create_worker_pool(): +def create_worker_pool(worker_count=MIN_WORKER_THREAD): """ Creates a worker pool to be used. """ def job_handler(job): @@ -807,4 +799,4 @@ def create_worker_pool(): _LOGGER.warning("WorkerPool:Current job from %s: %s", date_util.datetime_to_local_str(start), job) - return util.ThreadPool(job_handler, MIN_WORKER_THREAD, busy_callback) + return util.ThreadPool(job_handler, worker_count, busy_callback) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fe1991c7ed1..3d1408b49bf 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -205,8 +205,8 @@ def setup(hass, config): for light in target_lights: light.turn_off(**params) - if light.should_poll: - for light in target_lights: + for light in target_lights: + if light.should_poll: light.update_ha_state(True) return diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 194cacdc19d..60377fd1f5d 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -68,6 +68,8 @@ def track_point_in_utc_time(hass, action, point_in_time): """ Adds a listener that fires once after a specific point in UTC time. """ + # Ensure point_in_time is UTC + point_in_time = dt_util.as_utc(point_in_time) @ft.wraps(action) def point_in_time_listener(event): From df3ee6005a0dffd7c7e71455bb907ce7a86f44f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Aug 2015 18:15:22 +0200 Subject: [PATCH 051/111] Nicer test imports unittest changes import path so old style worked but is confusing --- tests/components/test_demo.py | 2 +- tests/components/test_device_sun_light_trigger.py | 2 +- tests/components/test_device_tracker.py | 2 +- tests/components/test_history.py | 2 +- tests/components/test_light.py | 2 +- tests/components/test_logbook.py | 2 +- tests/components/test_media_player.py | 2 +- tests/components/test_recorder.py | 2 +- tests/components/test_switch.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/components/test_demo.py b/tests/components/test_demo.py index 628c7e84bfe..9e697fb0c74 100644 --- a/tests/components/test_demo.py +++ b/tests/components/test_demo.py @@ -9,7 +9,7 @@ import unittest import homeassistant as ha import homeassistant.components.demo as demo -from common import mock_http_component +from tests.common import mock_http_component class TestDemo(unittest.TestCase): diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 16b89468201..05b2bf11bea 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -14,7 +14,7 @@ from homeassistant.components import ( device_tracker, light, sun, device_sun_light_trigger) -from common import ( +from tests.common import ( get_test_home_assistant, ensure_sun_risen, ensure_sun_set, trigger_device_tracker_scan) diff --git a/tests/components/test_device_tracker.py b/tests/components/test_device_tracker.py index 1d595df3f5a..bbc647987c9 100644 --- a/tests/components/test_device_tracker.py +++ b/tests/components/test_device_tracker.py @@ -18,7 +18,7 @@ from homeassistant.const import ( DEVICE_DEFAULT_NAME) import homeassistant.components.device_tracker as device_tracker -from common import get_test_home_assistant +from tests.common import get_test_home_assistant def setUpModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 1d0d787b95d..675e2d022d9 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -13,7 +13,7 @@ import homeassistant as ha import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder -from common import ( +from tests.common import ( mock_http_component, mock_state_change_event, get_test_home_assistant) diff --git a/tests/components/test_light.py b/tests/components/test_light.py index 5142823e8ef..e56dcbb02ad 100644 --- a/tests/components/test_light.py +++ b/tests/components/test_light.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, SERVICE_TURN_OFF) import homeassistant.components.light as light -from common import mock_service, get_test_home_assistant +from tests.common import mock_service, get_test_home_assistant class TestLight(unittest.TestCase): diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 2a426d0fcdc..cd415590d2d 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -14,7 +14,7 @@ from homeassistant.const import ( import homeassistant.util.dt as dt_util from homeassistant.components import logbook -from common import get_test_home_assistant, mock_http_component +from tests.common import get_test_home_assistant, mock_http_component class TestComponentHistory(unittest.TestCase): diff --git a/tests/components/test_media_player.py b/tests/components/test_media_player.py index d79ad89609f..f3f0128af3e 100644 --- a/tests/components/test_media_player.py +++ b/tests/components/test_media_player.py @@ -15,7 +15,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) import homeassistant.components.media_player as media_player -from common import mock_service +from tests.common import mock_service def setUpModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_recorder.py b/tests/components/test_recorder.py index eb90f2331d9..26e5fdfd6b7 100644 --- a/tests/components/test_recorder.py +++ b/tests/components/test_recorder.py @@ -11,7 +11,7 @@ import os from homeassistant.const import MATCH_ALL from homeassistant.components import recorder -from common import get_test_home_assistant +from tests.common import get_test_home_assistant class TestRecorder(unittest.TestCase): diff --git a/tests/components/test_switch.py b/tests/components/test_switch.py index 273c5987be2..642c7f45aa9 100644 --- a/tests/components/test_switch.py +++ b/tests/components/test_switch.py @@ -11,7 +11,7 @@ import homeassistant.loader as loader from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM import homeassistant.components.switch as switch -from common import get_test_home_assistant +from tests.common import get_test_home_assistant class TestSwitch(unittest.TestCase): From 2075de3d81bc7f0a8e690f5d4d463bb02e00750d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Aug 2015 18:16:10 +0200 Subject: [PATCH 052/111] Extended test_init tests to cover all --- tests/helpers/test_event.py | 12 +- tests/test_init.py | 291 ++++++++++++++++++++++++++++++++---- 2 files changed, 266 insertions(+), 37 deletions(-) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index c6fdf3e276a..69338cf431b 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -30,9 +30,9 @@ class TestEventHelpers(unittest.TestCase): def test_track_point_in_time(self): """ Test track point in time. """ - before_birthday = datetime(1985, 7, 9, 12, 0, 0) - birthday_paulus = datetime(1986, 7, 9, 12, 0, 0) - after_birthday = datetime(1987, 7, 9, 12, 0, 0) + before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) runs = [] @@ -52,7 +52,7 @@ class TestEventHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(runs)) - track_point_in_utc_time( + track_point_in_time( self.hass, lambda x: runs.append(1), birthday_paulus) self._send_time_changed(after_birthday) @@ -65,7 +65,7 @@ class TestEventHelpers(unittest.TestCase): specific_runs = [] track_time_change(self.hass, lambda x: wildcard_runs.append(1)) - track_time_change( + track_utc_time_change( self.hass, lambda x: specific_runs.append(1), second=[0, 30]) self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) @@ -84,7 +84,7 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(3, len(wildcard_runs)) def test_track_state_change(self): - """ Test states.track_change. """ + """ Test track_state_change. """ # 2 lists to track how often our callbacks get called specific_runs = [] wildcard_runs = [] diff --git a/tests/test_init.py b/tests/test_init.py index 58052fe43f0..4d484be580e 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -8,18 +8,27 @@ Provides tests to verify that Home Assistant core works. # pylint: disable=too-few-public-methods import os import unittest +import unittest.mock as mock import time import threading from datetime import datetime +import pytz + import homeassistant as ha +import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import track_state_change +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + ATTR_FRIENDLY_NAME, TEMP_CELCIUS, + TEMP_FAHRENHEIT) + +PST = pytz.timezone('America/Los_Angeles') class TestHomeAssistant(unittest.TestCase): """ Tests the Home Assistant core classes. - Currently only includes tests to test cases that do not - get tested in the API integration tests. """ def setUp(self): # pylint: disable=invalid-name @@ -36,13 +45,13 @@ class TestHomeAssistant(unittest.TestCase): # Already stopped after the block till stopped test pass - def test_get_config_path(self): - """ Test get_config_path method. """ - self.assertEqual(os.path.join(os.getcwd(), "config"), - self.hass.config.config_dir) - - self.assertEqual(os.path.join(os.getcwd(), "config", "test.conf"), - self.hass.config.path("test.conf")) + def test_start(self): + calls = [] + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, + lambda event: calls.append(1)) + self.hass.start() + self.hass.pool.block_till_done() + self.assertEqual(1, len(calls)) def test_block_till_stoped(self): """ Test if we can block till stop service is called. """ @@ -51,28 +60,48 @@ class TestHomeAssistant(unittest.TestCase): self.assertFalse(blocking_thread.is_alive()) blocking_thread.start() - # Python will now give attention to the other thread - time.sleep(1) + + # Threads are unpredictable, try 20 times if we're ready + wait_loops = 0 + while not blocking_thread.is_alive() and wait_loops < 20: + wait_loops += 1 + time.sleep(0.05) self.assertTrue(blocking_thread.is_alive()) self.hass.services.call(ha.DOMAIN, ha.SERVICE_HOMEASSISTANT_STOP) self.hass.pool.block_till_done() - # hass.block_till_stopped checks every second if it should quit - # we have to wait worst case 1 second + # Threads are unpredictable, try 20 times if we're ready wait_loops = 0 - while blocking_thread.is_alive() and wait_loops < 50: + while blocking_thread.is_alive() and wait_loops < 20: wait_loops += 1 - time.sleep(0.1) + time.sleep(0.05) self.assertFalse(blocking_thread.is_alive()) + def test_stopping_with_keyboardinterrupt(self): + calls = [] + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, + lambda event: calls.append(1)) + + def raise_keyboardinterrupt(length): + # We don't want to patch the sleep of the timer. + if length == 1: + raise KeyboardInterrupt + + self.hass.start() + + with mock.patch('time.sleep', raise_keyboardinterrupt): + self.hass.block_till_stopped() + + self.assertEqual(1, len(calls)) + def test_track_point_in_time(self): """ Test track point in time. """ - before_birthday = datetime(1985, 7, 9, 12, 0, 0) - birthday_paulus = datetime(1986, 7, 9, 12, 0, 0) - after_birthday = datetime(1987, 7, 9, 12, 0, 0) + before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) runs = [] @@ -92,7 +121,7 @@ class TestHomeAssistant(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(runs)) - self.hass.track_point_in_utc_time( + self.hass.track_point_in_time( lambda x: runs.append(1), birthday_paulus) self._send_time_changed(after_birthday) @@ -105,7 +134,7 @@ class TestHomeAssistant(unittest.TestCase): specific_runs = [] self.hass.track_time_change(lambda x: wildcard_runs.append(1)) - self.hass.track_time_change( + self.hass.track_utc_time_change( lambda x: specific_runs.append(1), second=[0, 30]) self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) @@ -130,6 +159,16 @@ class TestHomeAssistant(unittest.TestCase): class TestEvent(unittest.TestCase): """ Test Event class. """ + def test_eq(self): + now = dt_util.utcnow() + data = {'some': 'attr'} + event1, event2 = [ + ha.Event('some_type', data, time_fired=now) + for _ in range(2) + ] + + self.assertEqual(event1, event2) + def test_repr(self): """ Test that repr method works. #MoreCoverage """ self.assertEqual( @@ -142,13 +181,27 @@ class TestEvent(unittest.TestCase): {"beer": "nice"}, ha.EventOrigin.remote))) + def test_as_dict(self): + event_type = 'some_type' + now = dt_util.utcnow() + data = {'some': 'attr'} + + event = ha.Event(event_type, data, ha.EventOrigin.local, now) + expected = { + 'event_type': event_type, + 'data': data, + 'origin': 'LOCAL', + 'time_fired': dt_util.datetime_to_str(now), + } + self.assertEqual(expected, event.as_dict()) + class TestEventBus(unittest.TestCase): """ Test EventBus methods. """ def setUp(self): # pylint: disable=invalid-name """ things to be run when tests are started. """ - self.bus = ha.EventBus() + self.bus = ha.EventBus(ha.create_worker_pool(0)) self.bus.listen('test_event', lambda x: len) def tearDown(self): # pylint: disable=invalid-name @@ -157,6 +210,7 @@ class TestEventBus(unittest.TestCase): def test_add_remove_listener(self): """ Test remove_listener method. """ + self.bus._pool.add_worker() old_count = len(self.bus.listeners) listener = lambda x: len @@ -182,11 +236,10 @@ class TestEventBus(unittest.TestCase): self.bus.listen_once('test_event', lambda x: runs.append(1)) self.bus.fire('test_event') - self.bus._pool.block_till_done() - self.assertEqual(1, len(runs)) - # Second time it should not increase runs self.bus.fire('test_event') + + self.bus._pool.add_worker() self.bus._pool.block_till_done() self.assertEqual(1, len(runs)) @@ -200,6 +253,37 @@ class TestState(unittest.TestCase): ha.InvalidEntityFormatError, ha.State, 'invalid_entity_format', 'test_state') + def test_domain(self): + state = ha.State('some_domain.hello', 'world') + self.assertEqual('some_domain', state.domain) + + def test_object_id(self): + state = ha.State('domain.hello', 'world') + self.assertEqual('hello', state.object_id) + + def test_name_if_no_friendly_name_attr(self): + state = ha.State('domain.hello_world', 'world') + self.assertEqual('hello world', state.name) + + def test_name_if_friendly_name_attr(self): + name = 'Some Unique Name' + state = ha.State('domain.hello_world', 'world', + {ATTR_FRIENDLY_NAME: name}) + self.assertEqual(name, state.name) + + def test_copy(self): + state = ha.State('domain.hello', 'world', {'some': 'attr'}) + self.assertEqual(state, state.copy()) + + def test_dict_conversion(self): + state = ha.State('domain.hello', 'world', {'some': 'attr'}) + self.assertEqual(state, ha.State.from_dict(state.as_dict())) + + def test_dict_conversion_with_wrong_data(self): + self.assertIsNone(ha.State.from_dict(None)) + self.assertIsNone(ha.State.from_dict({'state': 'yes'})) + self.assertIsNone(ha.State.from_dict({'entity_id': 'yes'})) + def test_repr(self): """ Test state.repr """ self.assertEqual("", @@ -218,14 +302,15 @@ class TestStateMachine(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ things to be run when tests are started. """ - self.bus = ha.EventBus() + self.pool = ha.create_worker_pool(0) + self.bus = ha.EventBus(self.pool) self.states = ha.StateMachine(self.bus) self.states.set("light.Bowl", "on") self.states.set("switch.AC", "off") def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.bus._pool.stop() + self.pool.stop() def test_is_state(self): """ Test is_state method. """ @@ -244,6 +329,10 @@ class TestStateMachine(unittest.TestCase): self.assertEqual(1, len(ent_ids)) self.assertTrue('light.bowl' in ent_ids) + def test_all(self): + states = sorted(state.entity_id for state in self.states.all()) + self.assertEqual(['light.bowl', 'switch.ac'], states) + def test_remove(self): """ Test remove method. """ self.assertTrue('light.bowl' in self.states.entity_ids()) @@ -255,6 +344,8 @@ class TestStateMachine(unittest.TestCase): def test_track_change(self): """ Test states.track_change. """ + self.pool.add_worker() + # 2 lists to track how often our callbacks got called specific_runs = [] wildcard_runs = [] @@ -291,10 +382,11 @@ class TestStateMachine(unittest.TestCase): self.assertEqual(3, len(wildcard_runs)) def test_case_insensitivty(self): + self.pool.add_worker() runs = [] - self.states.track_change( - 'light.BoWl', lambda a, b, c: runs.append(1), + track_state_change( + ha._MockHA(self.bus), 'light.BoWl', lambda a, b, c: runs.append(1), ha.MATCH_ALL, ha.MATCH_ALL) self.states.set('light.BOWL', 'off') @@ -332,16 +424,153 @@ class TestServiceRegistry(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """ things to be run when tests are started. """ - self.pool = ha.create_worker_pool() + self.pool = ha.create_worker_pool(0) self.bus = ha.EventBus(self.pool) self.services = ha.ServiceRegistry(self.bus, self.pool) - self.services.register("test_domain", "test_service", lambda x: len) + self.services.register("test_domain", "test_service", lambda x: None) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ - self.pool.stop() + if self.pool.worker_count: + self.pool.stop() def test_has_service(self): """ Test has_service method. """ self.assertTrue( self.services.has_service("test_domain", "test_service")) + self.assertFalse( + self.services.has_service("test_domain", "non_existing")) + self.assertFalse( + self.services.has_service("non_existing", "test_service")) + + def test_services(self): + expected = { + 'test_domain': ['test_service'] + } + self.assertEqual(expected, self.services.services) + + def test_call_with_blocking_done_in_time(self): + self.pool.add_worker() + self.pool.add_worker() + calls = [] + self.services.register("test_domain", "register_calls", + lambda x: calls.append(1)) + + self.assertTrue( + self.services.call('test_domain', 'register_calls', blocking=True)) + self.assertEqual(1, len(calls)) + + def test_call_with_blocking_not_done_in_time(self): + calls = [] + self.services.register("test_domain", "register_calls", + lambda x: calls.append(1)) + + orig_limit = ha.SERVICE_CALL_LIMIT + ha.SERVICE_CALL_LIMIT = 0.01 + self.assertFalse( + self.services.call('test_domain', 'register_calls', blocking=True)) + self.assertEqual(0, len(calls)) + ha.SERVICE_CALL_LIMIT = orig_limit + + def test_call_non_existing_with_blocking(self): + self.pool.add_worker() + self.pool.add_worker() + orig_limit = ha.SERVICE_CALL_LIMIT + ha.SERVICE_CALL_LIMIT = 0.01 + self.assertFalse( + self.services.call('test_domain', 'i_do_not_exist', blocking=True)) + ha.SERVICE_CALL_LIMIT = orig_limit + + +class TestConfig(unittest.TestCase): + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.config = ha.Config() + + def test_config_dir_set_correct(self): + """ Test config dir set correct. """ + self.assertEqual(os.path.join(os.getcwd(), "config"), + self.config.config_dir) + + def test_path_with_file(self): + """ Test get_config_path method. """ + self.assertEqual(os.path.join(os.getcwd(), "config", "test.conf"), + self.config.path("test.conf")) + + def test_path_with_dir_and_file(self): + """ Test get_config_path method. """ + self.assertEqual( + os.path.join(os.getcwd(), "config", "dir", "test.conf"), + self.config.path("dir", "test.conf")) + + def test_temperature_not_convert_if_no_preference(self): + """ No unit conversion to happen if no preference. """ + self.assertEqual( + (25, TEMP_CELCIUS), + self.config.temperature(25, TEMP_CELCIUS)) + self.assertEqual( + (80, TEMP_FAHRENHEIT), + self.config.temperature(80, TEMP_FAHRENHEIT)) + + def test_temperature_not_convert_if_invalid_value(self): + """ No unit conversion to happen if no preference. """ + self.config.temperature_unit = TEMP_FAHRENHEIT + self.assertEqual( + ('25a', TEMP_CELCIUS), + self.config.temperature('25a', TEMP_CELCIUS)) + + def test_temperature_not_convert_if_invalid_unit(self): + """ No unit conversion to happen if no preference. """ + self.assertEqual( + (25, 'Invalid unit'), + self.config.temperature(25, 'Invalid unit')) + + def test_temperature_to_convert_to_celcius(self): + self.config.temperature_unit = TEMP_CELCIUS + + self.assertEqual( + (25, TEMP_CELCIUS), + self.config.temperature(25, TEMP_CELCIUS)) + self.assertEqual( + (26.7, TEMP_CELCIUS), + self.config.temperature(80, TEMP_FAHRENHEIT)) + + def test_temperature_to_convert_to_fahrenheit(self): + self.config.temperature_unit = TEMP_FAHRENHEIT + + self.assertEqual( + (77, TEMP_FAHRENHEIT), + self.config.temperature(25, TEMP_CELCIUS)) + self.assertEqual( + (80, TEMP_FAHRENHEIT), + self.config.temperature(80, TEMP_FAHRENHEIT)) + + def test_as_dict(self): + expected = { + 'latitude': None, + 'longitude': None, + 'temperature_unit': None, + 'location_name': None, + 'time_zone': 'UTC', + 'components': [], + } + + self.assertEqual(expected, self.config.as_dict()) + + +class TestWorkerPool(unittest.TestCase): + def test_exception_during_job(self): + pool = ha.create_worker_pool(1) + + def malicious_job(_): + raise Exception("Test breaking worker pool") + + calls = [] + + def register_call(_): + calls.append(1) + + pool.add_job(ha.JobPriority.EVENT_DEFAULT, (malicious_job, None)) + pool.add_job(ha.JobPriority.EVENT_DEFAULT, (register_call, None)) + pool.block_till_done() + self.assertEqual(1, len(calls)) From 4284a3f5dc3e0e8ff5907eb1655e82549a74719e Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Tue, 4 Aug 2015 19:35:53 +0200 Subject: [PATCH 053/111] Fixed pylint conventions --- .../components/media_player/squeezebox.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 4d88d226f9d..70e74f3e895 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -80,6 +80,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SqueezeBoxDevice(MediaPlayerDevice): """ Represents a SqueezeBox device. """ + # pylint: disable=too-many-arguments def __init__(self, name, server, player, port, user, password): super(SqueezeBoxDevice, self).__init__() self._name = name @@ -100,9 +101,9 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def state(self): """ Returns the state of the device. """ - if ('power' in self._status and self._status['power'] == '0'): + if 'power' in self._status and self._status['power'] == '0': return STATE_OFF - if('mode' in self._status): + if 'mode' in self._status: if self._status['mode'] == 'pause': return STATE_PAUSED if self._status['mode'] == 'play': @@ -112,7 +113,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): return STATE_UNKNOWN def update(self): - if(self._user and self._password): + if self._user and self._password: self._query('login', self._user, self._password) self._get_status() @@ -154,18 +155,18 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def volume_level(self): """ Volume level of the media player (0..1). """ - if('mixer volume' in self._status): + if 'mixer volume' in self._status: return int(self._status['mixer volume']) / 100.0 @property def is_volume_muted(self): - if('mixer volume' in self._status): + if 'mixer volume' in self._status: return int(self._status['mixer volume']) < 0 @property def media_content_id(self): """ Content ID of current playing media. """ - if('current_title' in self._status): + if 'current_title' in self._status: return self._status['current_title'] @property @@ -176,13 +177,13 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def media_duration(self): """ Duration of current playing media in seconds. """ - if('duration' in self._status): + if 'duration' in self._status: return int(float(self._status['duration'])) @property def media_image_url(self): """ Image url of current playing media. """ - if('artwork_url' in self._status): + if 'artwork_url' in self._status: return self._status['artwork_url'] return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\ .format( @@ -193,12 +194,12 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def media_title(self): """ Title of current playing media. """ - if('artist' in self._status and 'title' in self._status): + if 'artist' in self._status and 'title' in self._status: return '{artist} - {title}'.format( artist=self._status['artist'], title=self._status['title'] ) - if('current_title' in self._status): + if 'current_title' in self._status: return self._status['current_title'] @property From e6c09f7413b76137361d178c97f2408b9649c0a6 Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Tue, 4 Aug 2015 20:08:48 +0200 Subject: [PATCH 054/111] Fixed bug with password protected LMS --- homeassistant/components/media_player/squeezebox.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 70e74f3e895..496e0170dc9 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -113,13 +113,16 @@ class SqueezeBoxDevice(MediaPlayerDevice): return STATE_UNKNOWN def update(self): - if self._user and self._password: - self._query('login', self._user, self._password) self._get_status() def _query(self, *parameters): """ Send request and await response from server """ telnet = telnetlib.Telnet(self._server, self._port) + if self._user and self._password: + telnet.write('login {user} {password}\n'.format( + user=self._user, + password=self._password).encode('UTF-8')) + telnet.read_until(b'\n', timeout=3) message = '{}\n'.format(' '.join(parameters)) telnet.write(message.encode('UTF-8')) response = telnet.read_until(b'\n', timeout=3)\ From 30e24296c44aa68ccc1b934240f176a929d7a29a Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Tue, 4 Aug 2015 20:30:01 +0200 Subject: [PATCH 055/111] Fixed flake8 --- homeassistant/components/media_player/squeezebox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 496e0170dc9..70eab958000 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -120,7 +120,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): telnet = telnetlib.Telnet(self._server, self._port) if self._user and self._password: telnet.write('login {user} {password}\n'.format( - user=self._user, + user=self._user, password=self._password).encode('UTF-8')) telnet.read_until(b'\n', timeout=3) message = '{}\n'.format(' '.join(parameters)) From d2b5f429fea6f0e423e6333116ada0cc315cb6c1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Aug 2015 16:21:09 -0400 Subject: [PATCH 056/111] Remove deprecated code --- homeassistant/__init__.py | 12 +++--- homeassistant/bootstrap.py | 15 ++++--- homeassistant/components/group.py | 4 -- homeassistant/components/process.py | 52 ----------------------- homeassistant/helpers/__init__.py | 4 -- homeassistant/helpers/device.py | 10 ----- homeassistant/helpers/device_component.py | 10 ----- homeassistant/util/__init__.py | 6 +-- tests/components/test_group.py | 3 +- 9 files changed, 17 insertions(+), 99 deletions(-) delete mode 100644 homeassistant/components/process.py delete mode 100644 homeassistant/helpers/device.py delete mode 100644 homeassistant/helpers/device_component.py diff --git a/homeassistant/__init__.py b/homeassistant/__init__.py index f5129fd1c95..bf21dc0a15d 100644 --- a/homeassistant/__init__.py +++ b/homeassistant/__init__.py @@ -96,7 +96,7 @@ class HomeAssistant(object): self.pool.stop() def track_point_in_time(self, action, point_in_time): - """Deprecated method to track point in time.""" + """Deprecated method as of 8/4/2015 to track point in time.""" _LOGGER.warning( 'hass.track_point_in_time is deprecated. ' 'Please use homeassistant.helpers.event.track_point_in_time') @@ -104,7 +104,7 @@ class HomeAssistant(object): helper.track_point_in_time(self, action, point_in_time) def track_point_in_utc_time(self, action, point_in_time): - """Deprecated method to track point in UTC time.""" + """Deprecated method as of 8/4/2015 to track point in UTC time.""" _LOGGER.warning( 'hass.track_point_in_utc_time is deprecated. ' 'Please use homeassistant.helpers.event.track_point_in_utc_time') @@ -114,7 +114,7 @@ class HomeAssistant(object): def track_utc_time_change(self, action, year=None, month=None, day=None, hour=None, minute=None, second=None): - """Deprecated method to track UTC time change.""" + """Deprecated method as of 8/4/2015 to track UTC time change.""" # pylint: disable=too-many-arguments _LOGGER.warning( 'hass.track_utc_time_change is deprecated. ' @@ -126,7 +126,7 @@ class HomeAssistant(object): def track_time_change(self, action, year=None, month=None, day=None, hour=None, minute=None, second=None, utc=False): - """Deprecated method to track time change.""" + """Deprecated method as of 8/4/2015 to track time change.""" # pylint: disable=too-many-arguments _LOGGER.warning( 'hass.track_time_change is deprecated. ' @@ -183,7 +183,7 @@ class Event(object): self.event_type = event_type self.data = data or {} self.origin = origin - self.time_fired = util.strip_microseconds( + self.time_fired = date_util.strip_microseconds( time_fired or date_util.utcnow()) def as_dict(self): @@ -502,7 +502,7 @@ class StateMachine(object): def track_change(self, entity_ids, action, from_state=None, to_state=None): """ - DEPRECATED + DEPRECATED AS OF 8/4/2015 """ _LOGGER.warning( 'hass.states.track_change is deprecated. ' diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 663ed611de4..514c1adce57 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -63,12 +63,15 @@ def setup_component(hass, domain, config=None): def _handle_requirements(component, name): """ Installs requirements for component. """ - if hasattr(component, 'REQUIREMENTS'): - for req in component.REQUIREMENTS: - if not pkg_util.install_package(req): - _LOGGER.error('Not initializing %s because could not install ' - 'dependency %s', name, req) - return False + if not hasattr(component, 'REQUIREMENTS'): + return True + + for req in component.REQUIREMENTS: + if not pkg_util.install_package(req): + _LOGGER.error('Not initializing %s because could not install ' + 'dependency %s', name, req) + return False + return True diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index ac5e8cd4116..7c24c505add 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -103,10 +103,6 @@ def get_entity_ids(hass, entity_id, domain_filter=None): def setup(hass, config): """ Sets up all groups found definded in the configuration. """ for name, entity_ids in config.get(DOMAIN, {}).items(): - # Support old deprecated method - 2/28/2015 - if isinstance(entity_ids, str): - entity_ids = entity_ids.split(",") - setup_group(hass, name, entity_ids) return True diff --git a/homeassistant/components/process.py b/homeassistant/components/process.py deleted file mode 100644 index 21343aa977b..00000000000 --- a/homeassistant/components/process.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -homeassistant.components.process -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Provides functionality to watch for specific processes running -on the host machine. - -Author: Markus Stenberg -""" -import logging -import os - -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.util as util - -DOMAIN = 'process' -DEPENDENCIES = [] -ENTITY_ID_FORMAT = DOMAIN + '.{}' - -PS_STRING = 'ps awx' - - -def setup(hass, config): - """ Sets up a check if specified processes are running. - - processes: dict mapping entity id to substring to search for - in process list. - """ - - # Deprecated as of 3/7/2015 - logging.getLogger(__name__).warning( - "This component has been deprecated and will be removed in the future." - " Please use sensor.systemmonitor with the process type") - - entities = {ENTITY_ID_FORMAT.format(util.slugify(pname)): pstring - for pname, pstring in config[DOMAIN].items()} - - def update_process_states(time): - """ Check ps for currently running processes and update states. """ - with os.popen(PS_STRING, 'r') as psfile: - lines = list(psfile) - - for entity_id, pstring in entities.items(): - state = STATE_ON if any(pstring in l for l in lines) else STATE_OFF - - hass.states.set(entity_id, state) - - update_process_states(None) - - hass.track_time_change(update_process_states, second=[0, 30]) - - return True diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 086cddc35e2..286eed4654e 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -6,10 +6,6 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME) from homeassistant.util import ensure_unique_string, slugify -# Deprecated 3/5/2015 - Moved to homeassistant.helpers.entity -# pylint: disable=unused-import -from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa - def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): """ Generate a unique entity ID based on given entity IDs or used ids. """ diff --git a/homeassistant/helpers/device.py b/homeassistant/helpers/device.py deleted file mode 100644 index 4c713693c43..00000000000 --- a/homeassistant/helpers/device.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Deprecated since 3/21/2015 - please use helpers.entity -""" -import logging - -# pylint: disable=unused-import -from .entity import Entity as Device, ToggleEntity as ToggleDevice # noqa - -logging.getLogger(__name__).warning( - 'This file is deprecated. Please use helpers.entity') diff --git a/homeassistant/helpers/device_component.py b/homeassistant/helpers/device_component.py deleted file mode 100644 index 248297a9694..00000000000 --- a/homeassistant/helpers/device_component.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Deprecated since 3/21/2015 - please use helpers.entity_component -""" -import logging - -# pylint: disable=unused-import -from .entity_component import EntityComponent as DeviceComponent # noqa - -logging.getLogger(__name__).warning( - 'This file is deprecated. Please use helpers.entity_component') diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index a75d9837de6..2e399384e63 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -16,11 +16,7 @@ import random import string from functools import wraps -# DEPRECATED AS OF 4/27/2015 - moved to homeassistant.util.dt package -# pylint: disable=unused-import -from .dt import ( # noqa - datetime_to_str, str_to_datetime, strip_microseconds, - datetime_to_local_str, utcnow) +from .dt import datetime_to_local_str, utcnow RE_SANITIZE_FILENAME = re.compile(r'(~|\.\.|/|\\)') diff --git a/tests/components/test_group.py b/tests/components/test_group.py index a476efdeea0..d1e62b02cdb 100644 --- a/tests/components/test_group.py +++ b/tests/components/test_group.py @@ -199,8 +199,7 @@ class TestComponentsGroup(unittest.TestCase): self.hass, { group.DOMAIN: { - 'second_group': ','.join((self.group_entity_id, - 'light.Bowl')) + 'second_group': (self.group_entity_id, 'light.Bowl') } })) From aa20b9492721f25ee9446058c9d9a263183b34a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Aug 2015 16:21:25 -0400 Subject: [PATCH 057/111] Remove support for old home-assistant.conf file --- homeassistant/config.py | 45 +++++++---------------------------------- tests/test_config.py | 35 ++------------------------------ 2 files changed, 9 insertions(+), 71 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 629001b972f..3b2dd1ce740 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -13,14 +13,11 @@ from homeassistant.const import ( CONF_TIME_ZONE) import homeassistant.util.location as loc_util - _LOGGER = logging.getLogger(__name__) - YAML_CONFIG_FILE = 'configuration.yaml' -CONF_CONFIG_FILE = 'home-assistant.conf' -DEFAULT_CONFIG = [ +DEFAULT_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' 'running'), @@ -30,9 +27,9 @@ DEFAULT_CONFIG = [ (CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celcius, F for Fahrenheit'), (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), -] -DEFAULT_COMPONENTS = [ - 'discovery', 'frontend', 'conversation', 'history', 'logbook', 'sun'] +) +DEFAULT_COMPONENTS = ( + 'discovery', 'frontend', 'conversation', 'history', 'logbook', 'sun') def ensure_config_exists(config_dir, detect_location=True): @@ -95,24 +92,14 @@ def create_default_config(config_dir, detect_location=True): def find_config_file(config_dir): """ Looks in given directory for supported config files. """ - for filename in (YAML_CONFIG_FILE, CONF_CONFIG_FILE): - config_path = os.path.join(config_dir, filename) + config_path = os.path.join(config_dir, YAML_CONFIG_FILE) - if os.path.isfile(config_path): - return config_path - - return None + return config_path if os.path.isfile(config_path) else None def load_config_file(config_path): """ Loads given config file. """ - config_ext = os.path.splitext(config_path)[1] - - if config_ext == '.yaml': - return load_yaml_config_file(config_path) - - elif config_ext == '.conf': - return load_conf_config_file(config_path) + return load_yaml_config_file(config_path) def load_yaml_config_file(config_path): @@ -152,21 +139,3 @@ def load_yaml_config_file(config_path): raise HomeAssistantError() return conf_dict - - -def load_conf_config_file(config_path): - """ Parse the old style conf configuration. """ - import configparser - - config_dict = {} - - config = configparser.ConfigParser() - config.read(config_path) - - for section in config.sections(): - config_dict[section] = {} - - for key, val in config.items(section): - config_dict[section][key] = val - - return config_dict diff --git a/tests/test_config.py b/tests/test_config.py index 368f660eeb7..235549f6cef 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -20,7 +20,6 @@ from common import get_test_config_dir CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) -CONF_PATH = os.path.join(CONFIG_DIR, config_util.CONF_CONFIG_FILE) def create_file(path): @@ -51,9 +50,8 @@ class TestConfig(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """ Clean up. """ - for path in (YAML_PATH, CONF_PATH): - if os.path.isfile(path): - os.remove(path) + if os.path.isfile(YAML_PATH): + os.remove(YAML_PATH) def test_create_default_config(self): """ Test creationg of default config. """ @@ -69,21 +67,6 @@ class TestConfig(unittest.TestCase): self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR)) - def test_find_config_file_conf(self): - """ Test if it finds the old CONF config file. """ - - create_file(CONF_PATH) - - self.assertEqual(CONF_PATH, config_util.find_config_file(CONFIG_DIR)) - - def test_find_config_file_prefers_yaml_over_conf(self): - """ Test if find config prefers YAML over CONF if both exist. """ - - create_file(YAML_PATH) - create_file(CONF_PATH) - - self.assertEqual(YAML_PATH, config_util.find_config_file(CONFIG_DIR)) - def test_ensure_config_exists_creates_config(self): """ Test that calling ensure_config_exists creates a new config file if none exists. """ @@ -135,20 +118,6 @@ class TestConfig(unittest.TestCase): self.assertEqual({'hello': 'world'}, config_util.load_config_file(YAML_PATH)) - def test_load_config_loads_conf_config(self): - """ Test correct YAML config loading. """ - create_file(CONF_PATH) - - self.assertEqual({}, config_util.load_config_file(CONF_PATH)) - - def test_conf_config_file(self): - """ Test correct CONF config loading. """ - with open(CONF_PATH, 'w') as f: - f.write('[ha]\ntime_zone=America/Los_Angeles') - - self.assertEqual({'ha': {'time_zone': 'America/Los_Angeles'}}, - config_util.load_conf_config_file(CONF_PATH)) - def test_create_default_config_detect_location(self): """ Test that detect location sets the correct config keys. """ with mock.patch('homeassistant.util.location.detect_location_info', From 4f9fa7f8ad7c5b7aaac3a92681f191c972d51712 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Aug 2015 16:33:35 -0400 Subject: [PATCH 058/111] Update coveragerc --- .coveragerc | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.coveragerc b/.coveragerc index 39a3dee22bf..15c76376eef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,30 +10,36 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/isy994.py + homeassistant/components/*/isy994.py + + homeassistant/components/modbus.py + homeassistant/components/*/modbus.py + homeassistant/components/wink.py homeassistant/components/*/wink.py homeassistant/components/zwave.py homeassistant/components/*/zwave.py - homeassistant/components/modbus.py - homeassistant/components/*/modbus.py - - homeassistant/components/isy994.py - homeassistant/components/*/isy994.py - homeassistant/components/*/tellstick.py homeassistant/components/*/vera.py homeassistant/components/browser.py + homeassistant/components/camera/* homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py homeassistant/components/device_tracker/nmap_tracker.py homeassistant/components/device_tracker/tomato.py + homeassistant/components/device_tracker/tplink.py + homeassistant/components/discovery.py + homeassistant/components/downloader.py homeassistant/components/keyboard.py homeassistant/components/light/hue.py + homeassistant/components/light/limitlessled.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/kodi.py homeassistant/components/media_player/mpd.py homeassistant/components/notify/file.py homeassistant/components/notify/instapush.py @@ -53,7 +59,9 @@ omit = homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/transmission.py + homeassistant/components/switch/command_switch.py homeassistant/components/switch/hikvisioncam.py + homeassistant/components/switch/transmission.py homeassistant/components/switch/wemo.py homeassistant/components/thermostat/nest.py From 52ec4ac1d8509f6450b1d9c1bccd72e300510cd9 Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Wed, 5 Aug 2015 10:22:03 +0200 Subject: [PATCH 059/111] flake8 and pylint --- homeassistant/components/device_tracker/asuswrt.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index e38e410fe54..e7126d7df91 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -62,6 +62,7 @@ _IP_NEIGH_REGEX = re.compile( r'(?P(\w+))') +# pylint: disable=unused-argument def get_scanner(hass, config): """ Validates config and returns a DD-WRT scanner. """ if not validate_config(config, @@ -146,7 +147,8 @@ class AsusWrtDeviceScanner(object): _LOGGER.exception("Unexpected response from router") return except ConnectionRefusedError: - _LOGGER.exception("Connection refused by router, is telnet enabled?") + _LOGGER.exception("Connection refused by router," + + " is telnet enabled?") return devices = {} From db2cbf33c3172c889fcc70e95efb39bdd8d7e0f1 Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Wed, 5 Aug 2015 13:49:45 +0200 Subject: [PATCH 060/111] Added support for multiple players --- .../components/media_player/squeezebox.py | 221 ++++++++++-------- 1 file changed, 129 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 70eab958000..fa05e304c29 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -10,34 +10,24 @@ To use SqueezeBox add something like this to your configuration: media_player: platform: squeezebox - name: SqueezeBox - server: 192.168.1.21 - player: Player1 + host: 192.168.1.21 port: 9090 - user: user + username: user password: password Variables: -name -*Optional -The name of the device - -server +host *Required -The address of the Logitech Media Server - -player -*Required -The unique name of the player +The host name or address of the Logitech Media Server port *Optional Telnet port to Logitech Media Server, default 9090 -user +usermame *Optional -User, if password protection is enabled +Username, if password protection is enabled password *Optional @@ -51,8 +41,10 @@ import urllib.parse from homeassistant.components.media_player import ( MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, - MEDIA_TYPE_MUSIC) + MEDIA_TYPE_MUSIC, DOMAIN) + from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) _LOGGER = logging.getLogger(__name__) @@ -64,15 +56,104 @@ SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the squeezebox platform. """ - add_devices([ - SqueezeBoxDevice( - config.get('name', 'SqueezeBox'), - config.get('server'), - config.get('player'), - config.get('port', '9090'), - config.get('user', None), - config.get('password', None) - )]) + if not config.get(CONF_HOST): + _LOGGER.error( + "Missing required configuration items in {}: {}".format( + DOMAIN, CONF_HOST)) + return False + + lms = LogitechMediaServer( + config.get(CONF_HOST), + config.get('port', '9090'), + config.get(CONF_USERNAME), + config.get(CONF_PASSWORD)) + + if not lms.init_success: + return False + + add_devices(lms.create_players()) + + return True + + +class LogitechMediaServer(object): + def __init__(self, host, port, username, password): + self.host = host + self.port = port + self._username = username + self._password = password + self.http_port = self._get_http_port() + self.init_success = True if self.http_port else False + + def _get_http_port(self): + """ Get http port from media server, it is used to get cover art """ + http_port = None + try: + http_port = self.query('pref', 'httpport', '?') + if not http_port: + _LOGGER.error( + "Unable to read data from server {}:{}".format( + self.host, + self.port)) + return + return http_port + except ConnectionError as ex: + _LOGGER.error( + "Failed to connect to server {}:{} - {}".format( + self.host, + self.port, + ex)) + return + + def create_players(self): + """ Create a list of SqueezeBoxDevices connected to the LMS """ + players = [] + count = self.query('player', 'count', '?') + for index in range(0, int(count)): + player_id = self.query('player', 'id', str(index), '?') + player = SqueezeBoxDevice(self, player_id) + players.append(player) + return players + + def query(self, *parameters): + """ Send request and await response from server """ + telnet = telnetlib.Telnet(self.host, self.port) + if self._username and self._password: + telnet.write('login {username} {password}\n'.format( + username=self._username, + password=self._password).encode('UTF-8')) + telnet.read_until(b'\n', timeout=3) + message = '{}\n'.format(' '.join(parameters)) + telnet.write(message.encode('UTF-8')) + response = telnet.read_until(b'\n', timeout=3)\ + .decode('UTF-8')\ + .split(' ')[-1]\ + .strip() + telnet.write(b'exit\n') + return urllib.parse.unquote(response) + + def get_player_status(self, player): + """ Get ithe status of a player """ + # (title) : Song title + # Requested Information + # a (artist): Artist name 'artist' + # d (duration): Song duration in seconds 'duration' + # K (artwork_url): URL to remote artwork + tags = 'adK' + new_status = {} + telnet = telnetlib.Telnet(self.host, self.port) + telnet.write('{player} status - 1 tags:{tags}\n'.format( + player=player, + tags=tags + ).encode('UTF-8')) + response = telnet.read_until(b'\n', timeout=3)\ + .decode('UTF-8')\ + .split(' ') + telnet.write(b'exit\n') + for item in response: + parts = urllib.parse.unquote(item).partition(':') + new_status[parts[0]] = parts[2] + return new_status # pylint: disable=too-many-instance-attributes @@ -81,17 +162,12 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ Represents a SqueezeBox device. """ # pylint: disable=too-many-arguments - def __init__(self, name, server, player, port, user, password): + def __init__(self, lms, player_id): super(SqueezeBoxDevice, self).__init__() - self._name = name - self._server = server - self._player = player - self._port = port - self._user = user - self._password = password - self._status = {} - self.update() - self._http_port = self._query('pref', 'httpport', '?') + self._lms = lms + self._id = player_id + self._name = self._lms.query(self._id, 'name', '?') + self._status = self._lms.get_player_status(self._id) @property def name(self): @@ -113,47 +189,8 @@ class SqueezeBoxDevice(MediaPlayerDevice): return STATE_UNKNOWN def update(self): - self._get_status() - - def _query(self, *parameters): - """ Send request and await response from server """ - telnet = telnetlib.Telnet(self._server, self._port) - if self._user and self._password: - telnet.write('login {user} {password}\n'.format( - user=self._user, - password=self._password).encode('UTF-8')) - telnet.read_until(b'\n', timeout=3) - message = '{}\n'.format(' '.join(parameters)) - telnet.write(message.encode('UTF-8')) - response = telnet.read_until(b'\n', timeout=3)\ - .decode('UTF-8')\ - .split(' ')[-1]\ - .strip() - telnet.write(b'exit\n') - return urllib.parse.unquote(response) - - def _get_status(self): - """ request status and parse result """ - # (title) : Song title - # Requested Information - # a (artist): Artist name 'artist' - # d (duration): Song duration in seconds 'duration' - # K (artwork_url): URL to remote artwork - tags = 'adK' - new_status = {} - telnet = telnetlib.Telnet(self._server, self._port) - telnet.write('{player} status - 1 tags:{tags}\n'.format( - player=self._player, - tags=tags - ).encode('UTF-8')) - response = telnet.read_until(b'\n', timeout=3)\ - .decode('UTF-8')\ - .split(' ') - telnet.write(b'exit\n') - for item in response: - parts = urllib.parse.unquote(item).partition(':') - new_status[parts[0]] = parts[2] - self._status = new_status + """ Retrieve latest state. """ + self._status = self._lms.get_player_status(self._name) @property def volume_level(self): @@ -190,9 +227,9 @@ class SqueezeBoxDevice(MediaPlayerDevice): return self._status['artwork_url'] return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\ .format( - server=self._server, - port=self._http_port, - player=self._player) + server=self._lms.host, + port=self._lms.http_port, + player=self._id) @property def media_title(self): @@ -212,64 +249,64 @@ class SqueezeBoxDevice(MediaPlayerDevice): def turn_off(self): """ turn_off media player. """ - self._query(self._player, 'power', '0') + self._lms.query(self._id, 'power', '0') self.update_ha_state() def volume_up(self): """ volume_up media player. """ - self._query(self._player, 'mixer', 'volume', '+5') + self._lms.query(self._id, 'mixer', 'volume', '+5') self.update_ha_state() def volume_down(self): """ volume_down media player. """ - self._query(self._player, 'mixer', 'volume', '-5') + self._lms.query(self._id, 'mixer', 'volume', '-5') self.update_ha_state() def set_volume_level(self, volume): """ set volume level, range 0..1. """ volume_percent = str(int(volume*100)) - self._query(self._player, 'mixer', 'volume', volume_percent) + self._lms.query(self._id, 'mixer', 'volume', volume_percent) self.update_ha_state() def mute_volume(self, mute): """ mute (true) or unmute (false) media player. """ mute_numeric = '1' if mute else '0' - self._query(self._player, 'mixer', 'muting', mute_numeric) + self._lms.query(self._id, 'mixer', 'muting', mute_numeric) self.update_ha_state() def media_play_pause(self): """ media_play_pause media player. """ - self._query(self._player, 'pause') + self._lms.query(self._id, 'pause') self.update_ha_state() def media_play(self): """ media_play media player. """ - self._query(self._player, 'play') + self._lms.query(self._id, 'play') self.update_ha_state() def media_pause(self): """ media_pause media player. """ - self._query(self._player, 'pause', '0') + self._lms.query(self._id, 'pause', '0') self.update_ha_state() def media_next_track(self): """ Send next track command. """ - self._query(self._player, 'playlist', 'index', '+1') + self._lms.query(self._id, 'playlist', 'index', '+1') self.update_ha_state() def media_previous_track(self): """ Send next track command. """ - self._query(self._player, 'playlist', 'index', '-1') + self._lms.query(self._id, 'playlist', 'index', '-1') self.update_ha_state() def media_seek(self, position): """ Send seek command. """ - self._query(self._player, 'time', position) + self._lms.query(self._id, 'time', position) self.update_ha_state() def turn_on(self): """ turn the media player on. """ - self._query(self._player, 'power', '1') + self._lms.query(self._id, 'power', '1') self.update_ha_state() def play_youtube(self, media_id): From eb83621fce2cb88e6773a26a928a536f5490a237 Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Wed, 5 Aug 2015 20:02:39 +0200 Subject: [PATCH 061/111] fixing pylint issues --- .../components/media_player/squeezebox.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index fa05e304c29..16f1b3ef523 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -58,8 +58,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the squeezebox platform. """ if not config.get(CONF_HOST): _LOGGER.error( - "Missing required configuration items in {}: {}".format( - DOMAIN, CONF_HOST)) + "Missing required configuration items in %s: %s", + DOMAIN, CONF_HOST) return False lms = LogitechMediaServer( @@ -77,6 +77,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LogitechMediaServer(object): + """ Represents a Logitech media server. """ + def __init__(self, host, port, username, password): self.host = host self.port = port @@ -92,17 +94,17 @@ class LogitechMediaServer(object): http_port = self.query('pref', 'httpport', '?') if not http_port: _LOGGER.error( - "Unable to read data from server {}:{}".format( + "Unable to read data from server %s:%s", self.host, - self.port)) + self.port) return return http_port except ConnectionError as ex: _LOGGER.error( - "Failed to connect to server {}:{} - {}".format( + "Failed to connect to server %s:%s - %s", self.host, self.port, - ex)) + ex) return def create_players(self): From 03f93063f84d4086d4cb2e503088082032ee7345 Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Wed, 5 Aug 2015 20:09:20 +0200 Subject: [PATCH 062/111] fixed flake8 issues --- .../components/media_player/squeezebox.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 16f1b3ef523..95715b1e555 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -59,7 +59,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if not config.get(CONF_HOST): _LOGGER.error( "Missing required configuration items in %s: %s", - DOMAIN, CONF_HOST) + DOMAIN, + CONF_HOST) return False lms = LogitechMediaServer( @@ -78,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class LogitechMediaServer(object): """ Represents a Logitech media server. """ - + def __init__(self, host, port, username, password): self.host = host self.port = port @@ -95,16 +96,16 @@ class LogitechMediaServer(object): if not http_port: _LOGGER.error( "Unable to read data from server %s:%s", - self.host, - self.port) + self.host, + self.port) return return http_port except ConnectionError as ex: _LOGGER.error( "Failed to connect to server %s:%s - %s", - self.host, - self.port, - ex) + self.host, + self.port, + ex) return def create_players(self): From 450b510d084cdd9a1c9e8a6693f4429e93f6f590 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Aug 2015 11:55:59 -0700 Subject: [PATCH 063/111] Fix sensor.forecastio to treat Fahrenheit wrong Fixes #245 --- homeassistant/components/sensor/forecast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 613dd35d640..6222ad4d664 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -215,5 +215,6 @@ class ForeCastData(object): forecast = forecastio.load_forecast(self._api_key, self.latitude, - self.longitude) + self.longitude, + units='si') self.data = forecast.currently() From 393e88e7324428f4c1b8b5f29a4a6081e2d636f2 Mon Sep 17 00:00:00 2001 From: Per Sandstrom Date: Wed, 5 Aug 2015 22:25:03 +0200 Subject: [PATCH 064/111] add to .coveragerc --- .coveragerc | 1 + homeassistant/components/media_player/squeezebox.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 39a3dee22bf..386480fa36b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -35,6 +35,7 @@ omit = homeassistant/components/light/hue.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/mpd.py + homeassistant/components/media_player/squeezebox.py homeassistant/components/notify/file.py homeassistant/components/notify/instapush.py homeassistant/components/notify/nma.py diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 95715b1e555..911f8b0faab 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -204,7 +204,8 @@ class SqueezeBoxDevice(MediaPlayerDevice): @property def is_volume_muted(self): if 'mixer volume' in self._status: - return int(self._status['mixer volume']) < 0 + _LOGGER.info(self._status['mixer volume']) + return self._status['mixer volume'].startswith('-') @property def media_content_id(self): From ac19ac8b83ba14ad11179e6f4fae499c79554adf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 5 Aug 2015 21:20:28 -0700 Subject: [PATCH 065/111] Update frontend dependencies --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 388 +++++++++++------- .../www_static/home-assistant-polymer | 2 +- .../www_static/webcomponents-lite.min.js | 7 +- 4 files changed, 245 insertions(+), 154 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 6f30746f137..4e4be1af065 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "ccfe7497d635ab4df3e6943b05adbd9b" +VERSION = "25837aadc266393928ddbc44bc806763" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index c8cabd1b0a3..1589d181df5 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1,6 +1,6 @@ -