From afcf3eaac3796b685516c8aab738a73e69bb3d42 Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Sun, 29 Mar 2015 14:38:10 +0200 Subject: [PATCH 001/229] - add ds18S20 1-Wire sensor support - gitignore emacs backup files --- .gitignore | 5 +- homeassistant/components/sensor/onewire.py | 93 ++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensor/onewire.py diff --git a/.gitignore b/.gitignore index a82763e1b6d..c7e72545bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -62,4 +62,7 @@ nosetests.xml # Mr Developer .mr.developer.cfg .project -.pydevproject \ No newline at end of file +.pydevproject + +# Hide emacs backups +*~ diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py new file mode 100644 index 00000000000..384a792296e --- /dev/null +++ b/homeassistant/components/sensor/onewire.py @@ -0,0 +1,93 @@ +""" Support for DS18B20 One Wire Sensors""" +from homeassistant.helpers.entity import Entity +from homeassistant.const import TEMP_CELCIUS, TEMP_FAHRENHEIT +from glob import glob +import os +import time +import logging + + +BASE_DIR = '/sys/bus/w1/devices/' +DEVICE_FOLDERS = glob(os.path.join(BASE_DIR, '28*')) +SENSOR_IDS = [os.path.split(device_folder)[1] for device_folder in DEVICE_FOLDERS] +DEVICE_FILES = [os.path.join(device_folder, 'w1_slave') for device_folder in DEVICE_FOLDERS] + +_LOGGER = logging.getLogger(__name__) + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the one wire Sensors""" + # TODO check if kernel modules are loaded + + # TODO implment config fore the name, but also default solution + if DEVICE_FILES == []: + _LOGGER.error('No onewire sensor found') + return + + devs = [] + names = [] + try: + ## only one name given + if type(config['names']) == str: + names = [names] + + ## map names and sensors in given order + elif type(config['names']) == list: + names = config['names'] + + ## map names with ids + elif type(config['names']) == dict: + for sensor_id in SENSOR_IDS: + names.append(config['names'][sensor_id]) + + except KeyError: + ## use id as name + if not config['names']: + for sensor_id in SENSOR_IDS: + names.append(sensor_id) + + for device_file, name in zip(DEVICE_FILES, names): + devs.append(OneWire(name, device_file, TEMP_CELCIUS)) + add_devices(devs) + +class OneWire(Entity): + """ A Dallas 1 Wire Sensor""" + + def __init__(self, name, device_file, unit_of_measurement): + self._name = name + self._device_file = device_file + self._unit_of_measurement = unit_of_measurement + + def _read_temp_raw(self): + f = open(self._device_file, 'r') + lines = f.readlines() + f.close() + return lines + + @property + def should_poll(self): + return True + + @property + def name(self): + return self._name + + @property + def state(self): + """ return temperature in unit_of_measurement""" + lines = self._read_temp_raw() + while lines[0].strip()[-3:] != 'YES': + time.sleep(0.2) + lines = self._read_temp_raw() + equals_pos = lines[1].find('t=') + if equals_pos != -1: + temp_string = lines[1][equals_pos+2:] + temp = float(temp_string) / 1000.0 + if self._unit_of_measurement == TEMP_FAHRENHEIT: + temp = temp * 9.0 / 5.0 + 32.0 + return temp + + @property + def unit_of_measurement(self): + return self._unit_of_measurement + \ No newline at end of file From 0ed608abffdf349ebc9e74efd1ea7e0af395c8af Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Tue, 7 Apr 2015 21:10:16 +0200 Subject: [PATCH 002/229] little bug --- homeassistant/components/sensor/onewire.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 384a792296e..c3e0b5e3e9c 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -29,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: ## only one name given if type(config['names']) == str: - names = [names] + names = config[names] ## map names and sensors in given order elif type(config['names']) == list: From b3683887147dd3031b8dc844d355d6e3e16b62c3 Mon Sep 17 00:00:00 2001 From: deisi Date: Wed, 16 Sep 2015 08:49:12 +0200 Subject: [PATCH 003/229] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c7e72545bf6..65c584d143a 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,5 @@ nosetests.xml # Hide emacs backups *~ +*# +*.orig From b6f954e082c171721ef177bd89d2218838274fdf Mon Sep 17 00:00:00 2001 From: deisi Date: Wed, 16 Sep 2015 10:18:11 +0200 Subject: [PATCH 004/229] Changed handling of config file I tried to implement your suggesteions for the default handlig of the device names. I think this way, everything you wanted is in. --- homeassistant/components/sensor/onewire.py | 39 +++++++++------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index c3e0b5e3e9c..bb2a0438a75 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -17,35 +17,26 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the one wire Sensors""" - # TODO check if kernel modules are loaded - # TODO implment config fore the name, but also default solution if DEVICE_FILES == []: - _LOGGER.error('No onewire sensor found') + _LOGGER.error('No onewire sensor found. Check if dtoverlay=w1-gpio,gpiopin=4 is in your /boot/config.txt and the correct gpiopin number is set.') return devs = [] - names = [] - try: - ## only one name given - if type(config['names']) == str: - names = config[names] - - ## map names and sensors in given order - elif type(config['names']) == list: - names = config['names'] + names = SENSOR_IDS - ## map names with ids - elif type(config['names']) == dict: - for sensor_id in SENSOR_IDS: - names.append(config['names'][sensor_id]) - - except KeyError: - ## use id as name - if not config['names']: - for sensor_id in SENSOR_IDS: - names.append(sensor_id) - + for key in config.keys(): + if key=="names": + ## only one name given + if isinstance(config['names'], str): + names = [config['names']] + ## map names and sensors in given order + elif isinstance(config['names'], list): + names = config['names'] + ## map names to ids. + elif isinstance(config['names'], dict): + names = [config['names'].get(sensor_id, sensor_id) for sensor_id in SENSOR_IDS] + for device_file, name in zip(DEVICE_FILES, names): devs.append(OneWire(name, device_file, TEMP_CELCIUS)) add_devices(devs) @@ -90,4 +81,4 @@ class OneWire(Entity): @property def unit_of_measurement(self): return self._unit_of_measurement - \ No newline at end of file + From ce501ae627e0afa9b5629b4a22ea78a87de01b8d Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Wed, 16 Sep 2015 14:17:41 +0200 Subject: [PATCH 005/229] Improved onewire configuration --- homeassistant/components/sensor/onewire.py | 39 +++++++++------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index c3e0b5e3e9c..bb2a0438a75 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -17,35 +17,26 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the one wire Sensors""" - # TODO check if kernel modules are loaded - # TODO implment config fore the name, but also default solution if DEVICE_FILES == []: - _LOGGER.error('No onewire sensor found') + _LOGGER.error('No onewire sensor found. Check if dtoverlay=w1-gpio,gpiopin=4 is in your /boot/config.txt and the correct gpiopin number is set.') return devs = [] - names = [] - try: - ## only one name given - if type(config['names']) == str: - names = config[names] - - ## map names and sensors in given order - elif type(config['names']) == list: - names = config['names'] + names = SENSOR_IDS - ## map names with ids - elif type(config['names']) == dict: - for sensor_id in SENSOR_IDS: - names.append(config['names'][sensor_id]) - - except KeyError: - ## use id as name - if not config['names']: - for sensor_id in SENSOR_IDS: - names.append(sensor_id) - + for key in config.keys(): + if key=="names": + ## only one name given + if isinstance(config['names'], str): + names = [config['names']] + ## map names and sensors in given order + elif isinstance(config['names'], list): + names = config['names'] + ## map names to ids. + elif isinstance(config['names'], dict): + names = [config['names'].get(sensor_id, sensor_id) for sensor_id in SENSOR_IDS] + for device_file, name in zip(DEVICE_FILES, names): devs.append(OneWire(name, device_file, TEMP_CELCIUS)) add_devices(devs) @@ -90,4 +81,4 @@ class OneWire(Entity): @property def unit_of_measurement(self): return self._unit_of_measurement - \ No newline at end of file + From 3027b4a5a85b4a0bd19eb9fc84fd62cbf759f5f8 Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Tue, 22 Sep 2015 12:32:45 +0200 Subject: [PATCH 006/229] respect pylint suggestions --- homeassistant/components/sensor/onewire.py | 35 +++++++++++----------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index bb2a0438a75..60f30715073 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -19,24 +19,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the one wire Sensors""" if DEVICE_FILES == []: - _LOGGER.error('No onewire sensor found. Check if dtoverlay=w1-gpio,gpiopin=4 is in your /boot/config.txt and the correct gpiopin number is set.') + _LOGGER.error('No onewire sensor found. Check if + dtoverlay=w1-gpio,gpiopin=4 is in your /boot/config.txt + and the correct gpiopin number is set.') return devs = [] names = SENSOR_IDS for key in config.keys(): - if key=="names": - ## only one name given - if isinstance(config['names'], str): - names = [config['names']] - ## map names and sensors in given order - elif isinstance(config['names'], list): - names = config['names'] - ## map names to ids. - elif isinstance(config['names'], dict): - names = [config['names'].get(sensor_id, sensor_id) for sensor_id in SENSOR_IDS] - + if key == "names": + ## only one name given + if isinstance(config['names'], str): + names = [config['names']] + ## map names and sensors in given order + elif isinstance(config['names'], list): + names = config['names'] + ## map names to ids. + elif isinstance(config['names'], dict): + names = [config['names'].get(sensor_id, sensor_id) for sensor_id in SENSOR_IDS] for device_file, name in zip(DEVICE_FILES, names): devs.append(OneWire(name, device_file, TEMP_CELCIUS)) add_devices(devs) @@ -50,9 +51,10 @@ class OneWire(Entity): self._unit_of_measurement = unit_of_measurement def _read_temp_raw(self): - f = open(self._device_file, 'r') - lines = f.readlines() - f.close() + """ read the temperature as it is returned by the sensor""" + ds_device_file = open(self._device_file, 'r') + lines = ds_device_file.readlines() + ds_device_file.close() return lines @property @@ -74,11 +76,10 @@ class OneWire(Entity): if equals_pos != -1: temp_string = lines[1][equals_pos+2:] temp = float(temp_string) / 1000.0 - if self._unit_of_measurement == TEMP_FAHRENHEIT: + if self._unit_of_measurement == TEMP_FAHRENHEIT: temp = temp * 9.0 / 5.0 + 32.0 return temp @property def unit_of_measurement(self): return self._unit_of_measurement - From d475e5362bb2e563ef7e77321ae1654bb63116df Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Tue, 22 Sep 2015 12:53:44 +0200 Subject: [PATCH 007/229] respect flake8 errors --- homeassistant/components/sensor/onewire.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 60f30715073..a85e25fc1ad 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -9,18 +9,22 @@ import logging BASE_DIR = '/sys/bus/w1/devices/' DEVICE_FOLDERS = glob(os.path.join(BASE_DIR, '28*')) -SENSOR_IDS = [os.path.split(device_folder)[1] for device_folder in DEVICE_FOLDERS] -DEVICE_FILES = [os.path.join(device_folder, 'w1_slave') for device_folder in DEVICE_FOLDERS] +SENSOR_IDS = [] +DEVICE_FILES = [] +for device_folder in DEVICE_FOLDERS: + SENSOR_IDS.append(os.path.split(device_folder)[1]) + DEVICE_FILES.append(os.path.join(device_folder, 'w1_slave')) _LOGGER = logging.getLogger(__name__) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the one wire Sensors""" if DEVICE_FILES == []: - _LOGGER.error('No onewire sensor found. Check if - dtoverlay=w1-gpio,gpiopin=4 is in your /boot/config.txt + _LOGGER.error('No onewire sensor found. Check if + dtoverlay=w1-gpio,gpiopin=4 is in your /boot/config.txt and the correct gpiopin number is set.') return @@ -29,19 +33,20 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for key in config.keys(): if key == "names": - ## only one name given + # only one name given if isinstance(config['names'], str): names = [config['names']] - ## map names and sensors in given order + # map names and sensors in given order elif isinstance(config['names'], list): names = config['names'] - ## map names to ids. + # map names to ids. elif isinstance(config['names'], dict): names = [config['names'].get(sensor_id, sensor_id) for sensor_id in SENSOR_IDS] for device_file, name in zip(DEVICE_FILES, names): devs.append(OneWire(name, device_file, TEMP_CELCIUS)) add_devices(devs) + class OneWire(Entity): """ A Dallas 1 Wire Sensor""" From ef76047ba2224e5b6f3f2ca4334bc2558e65a15b Mon Sep 17 00:00:00 2001 From: Malte Deiseroth Date: Tue, 22 Sep 2015 13:27:08 +0200 Subject: [PATCH 008/229] new try to add everythin from the stylecheckers --- homeassistant/components/sensor/onewire.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index a85e25fc1ad..06fdbcef141 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -23,9 +23,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the one wire Sensors""" if DEVICE_FILES == []: - _LOGGER.error('No onewire sensor found. Check if - dtoverlay=w1-gpio,gpiopin=4 is in your /boot/config.txt - and the correct gpiopin number is set.') + _LOGGER.error('No onewire sensor found.') + _LOGGER.error('Check if dtoverlay=w1-gpio,gpiopin=4.') + _LOGGER.error('is in your /boot/config.txt and') + _LOGGER.error('the correct gpiopin number is set.') return devs = [] @@ -41,12 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): names = config['names'] # map names to ids. elif isinstance(config['names'], dict): - names = [config['names'].get(sensor_id, sensor_id) for sensor_id in SENSOR_IDS] + names = [] + for sensor_id in SENSOR_IDS: + names.append(config['names'].get(sensor_id, sensor_id)) for device_file, name in zip(DEVICE_FILES, names): devs.append(OneWire(name, device_file, TEMP_CELCIUS)) add_devices(devs) - + class OneWire(Entity): """ A Dallas 1 Wire Sensor""" From c8961fcf99ce134a56a605b295122bd5a09b79d0 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Wed, 13 Jan 2016 11:52:42 +0100 Subject: [PATCH 009/229] Create command_sensor.py This adds a binary command_sensor --- .../binary_sensor/command_sensor.py | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 homeassistant/components/binary_sensor/command_sensor.py diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py new file mode 100644 index 00000000000..1f86a86ff14 --- /dev/null +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -0,0 +1,101 @@ +""" +homeassistant.components.sensor.command_sensor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows to configure custom shell commands to turn a value for a sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.command_sensor/ +""" +import logging +import subprocess +from datetime import timedelta + +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.util import template, Throttle + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Command Sensor" +DEFAULT_PAYLOAD_ON = 'ON' +DEFAULT_PAYLOAD_OFF = 'OFF' + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Add the Command Sensor. """ + + if config.get('command') is None: + _LOGGER.error('Missing required variable: "command"') + return False + + data = CommandSensorData(config.get('command')) + + add_devices([CommandBinarySensor( + hass, + data, + config.get('name', DEFAULT_NAME), + config.get('payload_on', DEFAULT_PAYLOAD_ON), + config.get('payload_off', DEFAULT_PAYLOAD_OFF), + config.get(CONF_VALUE_TEMPLATE) + )]) + + +# pylint: disable=too-many-arguments +class CommandBinarySensor(BinarySensorDevice): + """ Represents a binary sensor that is returning a value of a shell commands. """ + def __init__(self, hass, data, name, payload_on, payload_off, value_template): + self._hass = hass + self.data = data + self._name = name + self._state = False + self._payload_on = payload_on + self._payload_off = payload_off + self._value_template = value_template + self.update() + + @property + def name(self): + """ The name of the sensor. """ + return self._name + + @property + def is_on(self): + """ True if the binary sensor is on. """ + return self._state + + def update(self): + """ Gets the latest data and updates the state. """ + self.data.update() + value = self.data.value + + if self._value_template is not None: + value = template.render_with_possible_json_value( + self._hass, self.value_template, value, False) + if value == self._payload_on: + self._state = True + elif value == self._payload_off: + self._state = False + + +# pylint: disable=too-few-public-methods +class CommandSensorData(object): + """ Class for handling the data retrieval. """ + + def __init__(self, command): + self.command = command + self.value = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data with a shell command. """ + _LOGGER.info('Running command: %s', self.command) + + try: + return_value = subprocess.check_output(self.command, shell=True) + self.value = return_value.strip().decode('utf-8') + except subprocess.CalledProcessError: + _LOGGER.error('Command failed: %s', self.command) From e5919c1bfe3d89d30275018f9c823763af05eb36 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Wed, 13 Jan 2016 12:19:20 +0100 Subject: [PATCH 010/229] Update command_sensor.py --- homeassistant/components/binary_sensor/command_sensor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py index 1f86a86ff14..dd365fec468 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -1,10 +1,7 @@ """ -homeassistant.components.sensor.command_sensor +homeassistant.components.binary_sensor.command_sensor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allows to configure custom shell commands to turn a value for a sensor. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.command_sensor/ +Allows to configure custom shell commands to turn a value into a logical value for a binary sensor. """ import logging import subprocess @@ -16,7 +13,7 @@ from homeassistant.util import template, Throttle _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Command Sensor" +DEFAULT_NAME = "Binary Command Sensor" DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' From c2e8646aed697a8263e4ec8b4e543f55af78ff2e Mon Sep 17 00:00:00 2001 From: Bart274 Date: Wed, 13 Jan 2016 12:20:37 +0100 Subject: [PATCH 011/229] Update .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 272ace975c4..4b7dbcf856c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -41,6 +41,7 @@ omit = homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py + homeassistant/components/binary_sensor/command_sensor.py homeassistant/components/browser.py homeassistant/components/camera/* homeassistant/components/device_tracker/actiontec.py From 0e6a60b0866f46e2e4d7d5922c4421790cd296f5 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Wed, 13 Jan 2016 21:05:47 -0700 Subject: [PATCH 012/229] Add the nest sensor for tracking data from nest --- homeassistant/components/nest.py | 45 ++++++++ homeassistant/components/sensor/nest.py | 116 ++++++++++++++++++++ homeassistant/components/thermostat/nest.py | 29 +---- 3 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/nest.py create mode 100644 homeassistant/components/sensor/nest.py diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py new file mode 100644 index 00000000000..af71774b591 --- /dev/null +++ b/homeassistant/components/nest.py @@ -0,0 +1,45 @@ +""" +homeassistant.components.thermostat.nest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Adds support for Nest thermostats. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/thermostat.nest/ +""" +import logging + +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) + +REQUIREMENTS = ['python-nest==2.6.0'] +DOMAIN = 'nest' + +NEST = None + + +# pylint: disable=unused-argument +def setup(hass, config): + """ Sets up the nest thermostat. """ + global NEST + + logger = logging.getLogger(__name__) + print("nest config", config[DOMAIN]) + username = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + + if username is None or password is None: + logger.error("Missing required configuration items %s or %s", + CONF_USERNAME, CONF_PASSWORD) + return + + try: + import nest + except ImportError: + logger.exception( + "Error while importing dependency nest. " + "Did you maybe not install the python-nest dependency?") + + return + + NEST = nest.Nest(username, password) + + return True diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py new file mode 100644 index 00000000000..6f850a30b72 --- /dev/null +++ b/homeassistant/components/sensor/nest.py @@ -0,0 +1,116 @@ +""" +homeassistant.components.sensor.nest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Nest Thermostat Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.nest/ +""" +from homeassistant.helpers.entity import Entity +from homeassistant.const import (STATE_ON, STATE_OFF, TEMP_CELCIUS) +from homeassistant.helpers.temperature import convert + +import homeassistant.components.nest as nest +import logging +import socket + +DEPENDENCIES = ['nest'] +SENSOR_TYPES = ['humidity', + 'mode', + 'last_ip', + 'local_ip', + 'last_connection', + 'battery_level'] + +BINARY_TYPES = ['fan', + 'hvac_ac_state', + 'hvac_aux_heater_state', + 'hvac_heat_x2_state', + 'hvac_heat_x3_state', + 'hvac_alt_heat_state', + 'hvac_alt_heat_x2_state', + 'hvac_emer_heat_state', + 'online'] + +SENSOR_UNITS = {'humidity': '%', 'battery_level': '%'} + +SENSOR_TEMP_TYPES = ['temperature', + 'target', + 'away_temperature[0]', + 'away_temperature[1]'] + +def setup_platform(hass, config, add_devices, discovery_info=None): + logger = logging.getLogger(__name__) + try: + for structure in nest.NEST.structures: + for device in structure.devices: + for variable in config['monitored_conditions']: + if variable in SENSOR_TYPES: + add_devices([NestSensor(structure, device, variable)]) + elif variable in BINARY_TYPES: + add_devices([NestBinarySensor(structure, device, variable)]) + elif variable in SENSOR_TEMP_TYPES: + add_devices([NestTempSensor(structure, device, variable)]) + else: + logger.error('Nest sensor type: "%s" does not exist', variable) + except socket.error: + logger.error( + "Connection error logging into the nest web service." + ) + +class NestSensor(Entity): + """ Represents a Nest sensor. """ + + def __init__(self, structure, device, variable): + self.structure = structure + self.device = device + self.variable = variable + + @property + def name(self): + """ Returns the name of the nest, if any. """ + location = self.device.where + name = self.device.name + if location is None: + return name + ' ' + self.variable + else: + if name == '': + return location.capitalize() + ' ' + self.variable + else: + return location.capitalize() + '(' + name + ')' + self.variable + @property + def state(self): + """ Returns the state of the sensor. """ + return getattr(self.device, self.variable) + + @property + def unit_of_measurement(self): + return SENSOR_UNITS.get(self.variable, None) + +class NestTempSensor(NestSensor): + """ Represents a Nest Temperature sensor. """ + + @property + def unit_of_measurement(self): + return self.hass.config.temperature_unit + + @property + def state(self): + temp = getattr(self.device, self.variable) + if temp is None: + return None + + value = convert(temp, TEMP_CELCIUS, + self.hass.config.temperature_unit) + + return round(value, 1) + +class NestBinarySensor(NestSensor): + """ Represents a Nst Binary sensor. """ + + @property + def state(self): + if getattr(self.device, self.variable): + return STATE_ON + else: + return STATE_OFF diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index f38935b726e..5530f021755 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -11,38 +11,18 @@ import logging from homeassistant.components.thermostat import (ThermostatDevice, STATE_COOL, STATE_IDLE, STATE_HEAT) -from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) +from homeassistant.const import (TEMP_CELCIUS) +import homeassistant.components.nest as nest -REQUIREMENTS = ['python-nest==2.6.0'] +DEPENDENCIES = ['nest'] - -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """ Sets up the nest thermostat. """ logger = logging.getLogger(__name__) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - if username is None or password is None: - logger.error("Missing required configuration items %s or %s", - CONF_USERNAME, CONF_PASSWORD) - return - - try: - import nest - except ImportError: - logger.exception( - "Error while importing dependency nest. " - "Did you maybe not install the python-nest dependency?") - - return - - napi = nest.Nest(username, password) try: add_devices([ NestThermostat(structure, device) - for structure in napi.structures + for structure in nest.NEST.structures for device in structure.devices ]) except socket.error: @@ -50,7 +30,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "Connection error logging into the nest web service." ) - class NestThermostat(ThermostatDevice): """ Represents a Nest thermostat. """ From d17aa103b4ea2dc67f4c766d14c8cabedc2a794e Mon Sep 17 00:00:00 2001 From: Bart274 Date: Thu, 14 Jan 2016 10:13:57 +0100 Subject: [PATCH 013/229] Update command_sensor.py --- homeassistant/components/binary_sensor/command_sensor.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py index dd365fec468..5e59818ee22 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -1,7 +1,8 @@ """ homeassistant.components.binary_sensor.command_sensor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allows to configure custom shell commands to turn a value into a logical value for a binary sensor. +Allows to configure custom shell commands to turn a value +into a logical value for a binary sensor. """ import logging import subprocess @@ -43,8 +44,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments class CommandBinarySensor(BinarySensorDevice): - """ Represents a binary sensor that is returning a value of a shell commands. """ - def __init__(self, hass, data, name, payload_on, payload_off, value_template): + """ Represents a binary sensor that is returning + a value of a shell commands. """ + def __init__(self, hass, data, name, payload_on, + payload_off, value_template): self._hass = hass self.data = data self._name = name From d0d375d433ce4ef07cd14c5c8a9b0180e8cae886 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Thu, 14 Jan 2016 10:27:11 +0100 Subject: [PATCH 014/229] Update command_sensor.py --- homeassistant/components/binary_sensor/command_sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py index 5e59818ee22..025b2dfe0ba 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -1,7 +1,7 @@ """ homeassistant.components.binary_sensor.command_sensor ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Allows to configure custom shell commands to turn a value +Allows to configure custom shell commands to turn a value into a logical value for a binary sensor. """ import logging @@ -44,9 +44,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-arguments class CommandBinarySensor(BinarySensorDevice): - """ Represents a binary sensor that is returning + """ Represents a binary sensor that is returning a value of a shell commands. """ - def __init__(self, hass, data, name, payload_on, + def __init__(self, hass, data, name, payload_on, payload_off, value_template): self._hass = hass self.data = data @@ -74,7 +74,7 @@ class CommandBinarySensor(BinarySensorDevice): if self._value_template is not None: value = template.render_with_possible_json_value( - self._hass, self.value_template, value, False) + self._hass, self._value_template, value, False) if value == self._payload_on: self._state = True elif value == self._payload_off: From 7b993da0de09917a3bf654e240ca2e163e5bf35e Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Thu, 14 Jan 2016 10:48:24 -0700 Subject: [PATCH 015/229] address PR comments for Nest Sensor --- .coveragerc | 2 + .../components/binary_sensor/nest.py | 50 +++++++++++++++++++ homeassistant/components/sensor/nest.py | 24 +-------- requirements_all.txt | 6 +-- 4 files changed, 56 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/binary_sensor/nest.py diff --git a/.coveragerc b/.coveragerc index ea9f302fbb1..69e83f26adb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -37,6 +37,7 @@ omit = homeassistant/components/*/rfxtrx.py homeassistant/components/binary_sensor/arest.py + homeassistant/components/binary_sensor/nest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py homeassistant/components/camera/* @@ -93,6 +94,7 @@ omit = homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py homeassistant/components/sensor/mysensors.py + homeassistant/components/sensor/nest.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py homeassistant/components/sensor/rpi_gpio.py diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py new file mode 100644 index 00000000000..def04f520ed --- /dev/null +++ b/homeassistant/components/binary_sensor/nest.py @@ -0,0 +1,50 @@ +""" +homeassistant.components.binary.nest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Nest Thermostat Binary Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.nest/ +""" +import logging +import socket +import homeassistant.components.nest as nest + +from homeassistant.components.sensor.nest import NestSensor +from homeassistant.const import (STATE_ON, STATE_OFF) + + +BINARY_TYPES = ['fan', + 'hvac_ac_state', + 'hvac_aux_heater_state', + 'hvac_heat_x2_state', + 'hvac_heat_x3_state', + 'hvac_alt_heat_state', + 'hvac_alt_heat_x2_state', + 'hvac_emer_heat_state', + 'online'] + +def setup_platform(hass, config, add_devices, discovery_info=None): + logger = logging.getLogger(__name__) + try: + for structure in nest.NEST.structures: + for device in structure.devices: + for variable in config['monitored_conditions']: + if variable in BINARY_TYPES: + add_devices([NestBinarySensor(structure, device, variable)]) + else: + logger.error('Nest sensor type: "%s" does not exist', variable) + except socket.error: + logger.error( + "Connection error logging into the nest web service." + ) + +class NestBinarySensor(NestSensor): + """ Represents a Nst Binary sensor. """ + + @property + def state(self): + if getattr(self.device, self.variable): + return STATE_ON + else: + return STATE_OFF diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 6f850a30b72..3e0f09b1a41 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -7,7 +7,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.nest/ """ from homeassistant.helpers.entity import Entity -from homeassistant.const import (STATE_ON, STATE_OFF, TEMP_CELCIUS) +from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.temperature import convert import homeassistant.components.nest as nest @@ -22,16 +22,6 @@ SENSOR_TYPES = ['humidity', 'last_connection', 'battery_level'] -BINARY_TYPES = ['fan', - 'hvac_ac_state', - 'hvac_aux_heater_state', - 'hvac_heat_x2_state', - 'hvac_heat_x3_state', - 'hvac_alt_heat_state', - 'hvac_alt_heat_x2_state', - 'hvac_emer_heat_state', - 'online'] - SENSOR_UNITS = {'humidity': '%', 'battery_level': '%'} SENSOR_TEMP_TYPES = ['temperature', @@ -47,8 +37,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for variable in config['monitored_conditions']: if variable in SENSOR_TYPES: add_devices([NestSensor(structure, device, variable)]) - elif variable in BINARY_TYPES: - add_devices([NestBinarySensor(structure, device, variable)]) elif variable in SENSOR_TEMP_TYPES: add_devices([NestTempSensor(structure, device, variable)]) else: @@ -104,13 +92,3 @@ class NestTempSensor(NestSensor): self.hass.config.temperature_unit) return round(value, 1) - -class NestBinarySensor(NestSensor): - """ Represents a Nst Binary sensor. """ - - @property - def state(self): - if getattr(self.device, self.variable): - return STATE_ON - else: - return STATE_OFF diff --git a/requirements_all.txt b/requirements_all.txt index 7aca45c6069..eb30dae0203 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,6 +89,9 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # homeassistant.components.mqtt paho-mqtt==1.1 +# homeassistant.components.nest +python-nest==2.6.0 + # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 @@ -184,9 +187,6 @@ heatmiserV3==0.9.1 # homeassistant.components.thermostat.honeywell evohomeclient==0.2.4 -# homeassistant.components.thermostat.nest -python-nest==2.6.0 - # homeassistant.components.thermostat.radiotherm radiotherm==1.2 From 65a3bf232537f19f111533e6b9c5925050d9bbfc Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Thu, 14 Jan 2016 11:01:53 -0700 Subject: [PATCH 016/229] fix merge error --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e4d94039068..97e26f8d33a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ Constants used by Home Assistant components. """ -__version__ = "0.10.1" +__version__ = "0.11.0.dev0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' From b8c8c71b7828cc2420c0236a7882e1c09f3c11b9 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Thu, 14 Jan 2016 11:28:28 -0700 Subject: [PATCH 017/229] run requirements_all again for nest sensor --- requirements_all.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 0547e7b89a5..097fc743b33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,12 +89,12 @@ https://github.com/bashwork/pymodbus/archive/d7fc4f1cc975631e0a9011390e8017f64b6 # homeassistant.components.mqtt paho-mqtt==1.1 -# homeassistant.components.nest -python-nest==2.6.0 - # homeassistant.components.mysensors https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4 +# homeassistant.components.nest +python-nest==2.6.0 + # homeassistant.components.notify.free_mobile freesms==0.1.0 From a39148dd38d68f5698d8334e4131f318e59d33ce Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Thu, 14 Jan 2016 11:37:17 -0700 Subject: [PATCH 018/229] fix pylint errors for Nest Sensor --- homeassistant/components/binary_sensor/nest.py | 1 + homeassistant/components/sensor/nest.py | 9 +++++---- homeassistant/components/thermostat/nest.py | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index def04f520ed..99bb5e045fd 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -25,6 +25,7 @@ BINARY_TYPES = ['fan', 'online'] def setup_platform(hass, config, add_devices, discovery_info=None): + "Setup nest binary sensors from config file" logger = logging.getLogger(__name__) try: for structure in nest.NEST.structures: diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 3e0f09b1a41..86cd38f2b39 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -6,14 +6,14 @@ Support for Nest Thermostat Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.nest/ """ +import logging +import socket +import homeassistant.components.nest as nest + from homeassistant.helpers.entity import Entity from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.temperature import convert -import homeassistant.components.nest as nest -import logging -import socket - DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', 'mode', @@ -30,6 +30,7 @@ SENSOR_TEMP_TYPES = ['temperature', 'away_temperature[1]'] def setup_platform(hass, config, add_devices, discovery_info=None): + "Setup Nest Sensor from config file" logger = logging.getLogger(__name__) try: for structure in nest.NEST.structures: diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 5530f021755..24f9153c50e 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -17,6 +17,7 @@ import homeassistant.components.nest as nest DEPENDENCIES = ['nest'] def setup_platform(hass, config, add_devices, discovery_info=None): + "Setup nest thermostat" logger = logging.getLogger(__name__) try: From 313cbda0aa281b69ad74eb5ca365033736ca95c0 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Thu, 14 Jan 2016 14:17:28 -0700 Subject: [PATCH 019/229] fix multiple PR issues --- .../components/binary_sensor/nest.py | 19 ++++++++++--------- homeassistant/components/nest.py | 10 +--------- homeassistant/components/sensor/nest.py | 19 ++++++++++++++----- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 99bb5e045fd..d980205c33c 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -10,8 +10,7 @@ import logging import socket import homeassistant.components.nest as nest -from homeassistant.components.sensor.nest import NestSensor -from homeassistant.const import (STATE_ON, STATE_OFF) +from homeassistant.components.sensor.nest import NestSensor BINARY_TYPES = ['fan', @@ -26,15 +25,20 @@ BINARY_TYPES = ['fan', def setup_platform(hass, config, add_devices, discovery_info=None): "Setup nest binary sensors from config file" + logger = logging.getLogger(__name__) try: for structure in nest.NEST.structures: for device in structure.devices: for variable in config['monitored_conditions']: if variable in BINARY_TYPES: - add_devices([NestBinarySensor(structure, device, variable)]) + add_devices([NestBinarySensor( + structure, + device, + variable)]) else: - logger.error('Nest sensor type: "%s" does not exist', variable) + logger.error('Nest sensor type: "%s" does not exist', + variable) except socket.error: logger.error( "Connection error logging into the nest web service." @@ -44,8 +48,5 @@ class NestBinarySensor(NestSensor): """ Represents a Nst Binary sensor. """ @property - def state(self): - if getattr(self.device, self.variable): - return STATE_ON - else: - return STATE_OFF + def is_on(self): + return bool(getattr(self.device, self.variable)) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index af71774b591..1b1d940595a 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -22,7 +22,6 @@ def setup(hass, config): global NEST logger = logging.getLogger(__name__) - print("nest config", config[DOMAIN]) username = config[DOMAIN].get(CONF_USERNAME) password = config[DOMAIN].get(CONF_PASSWORD) @@ -31,14 +30,7 @@ def setup(hass, config): CONF_USERNAME, CONF_PASSWORD) return - try: - import nest - except ImportError: - logger.exception( - "Error while importing dependency nest. " - "Did you maybe not install the python-nest dependency?") - - return + import nest NEST = nest.Nest(username, password) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 86cd38f2b39..f900d2eba93 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -31,6 +31,7 @@ SENSOR_TEMP_TYPES = ['temperature', def setup_platform(hass, config, add_devices, discovery_info=None): "Setup Nest Sensor from config file" + logger = logging.getLogger(__name__) try: for structure in nest.NEST.structures: @@ -39,14 +40,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if variable in SENSOR_TYPES: add_devices([NestSensor(structure, device, variable)]) elif variable in SENSOR_TEMP_TYPES: - add_devices([NestTempSensor(structure, device, variable)]) + add_devices([NestTempSensor(structure, + device, + variable)]) else: - logger.error('Nest sensor type: "%s" does not exist', variable) + logger.error('Nest sensor type: "%s" does not exist', + variable) except socket.error: logger.error( "Connection error logging into the nest web service." ) + class NestSensor(Entity): """ Represents a Nest sensor. """ @@ -58,18 +63,22 @@ class NestSensor(Entity): @property def name(self): """ Returns the name of the nest, if any. """ + location = self.device.where name = self.device.name if location is None: - return name + ' ' + self.variable + return "{} {}".format(name, self.variable) else: if name == '': - return location.capitalize() + ' ' + self.variable + return "{} {}".format(location.capitalize(), self.variable) else: - return location.capitalize() + '(' + name + ')' + self.variable + return "{}({}){}".format(location.capitalize(), + name, + self.variable) @property def state(self): """ Returns the state of the sensor. """ + return getattr(self.device, self.variable) @property From 9210c57c2d55637dd15e843d74b4d382ff18d14a Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Thu, 14 Jan 2016 14:19:35 -0700 Subject: [PATCH 020/229] Fix lint errors and PR comments --- .coveragerc | 6 +++--- homeassistant/components/binary_sensor/nest.py | 7 +++++-- homeassistant/components/sensor/nest.py | 11 +++++------ homeassistant/components/thermostat/nest.py | 3 +++ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.coveragerc b/.coveragerc index 44263c15572..b927fd28255 100644 --- a/.coveragerc +++ b/.coveragerc @@ -39,8 +39,10 @@ omit = homeassistant/components/mysensors.py homeassistant/components/*/mysensors.py + homeassistant/components/nest.py + homeassistant/components/*/nest.py + homeassistant/components/binary_sensor/arest.py - homeassistant/components/binary_sensor/nest.py homeassistant/components/binary_sensor/rest.py homeassistant/components/browser.py homeassistant/components/camera/* @@ -97,7 +99,6 @@ omit = homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/forecast.py homeassistant/components/sensor/glances.py - homeassistant/components/sensor/nest.py homeassistant/components/sensor/netatmo.py homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/rest.py @@ -123,7 +124,6 @@ omit = homeassistant/components/thermostat/heatmiser.py homeassistant/components/thermostat/homematic.py homeassistant/components/thermostat/honeywell.py - homeassistant/components/thermostat/nest.py homeassistant/components/thermostat/radiotherm.py diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index d980205c33c..421a39e51f5 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -23,6 +23,7 @@ BINARY_TYPES = ['fan', 'hvac_emer_heat_state', 'online'] + def setup_platform(hass, config, add_devices, discovery_info=None): "Setup nest binary sensors from config file" @@ -32,8 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in structure.devices: for variable in config['monitored_conditions']: if variable in BINARY_TYPES: - add_devices([NestBinarySensor( - structure, + add_devices([NestBinarySensor(structure, device, variable)]) else: @@ -44,9 +44,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "Connection error logging into the nest web service." ) + class NestBinarySensor(NestSensor): """ Represents a Nst Binary sensor. """ @property def is_on(self): + "Returns is the binary sensor is on or off" + return bool(getattr(self.device, self.variable)) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index f900d2eba93..ed8052b23d8 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -12,7 +12,6 @@ import homeassistant.components.nest as nest from homeassistant.helpers.entity import Entity from homeassistant.const import TEMP_CELCIUS -from homeassistant.helpers.temperature import convert DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', @@ -29,6 +28,7 @@ SENSOR_TEMP_TYPES = ['temperature', 'away_temperature[0]', 'away_temperature[1]'] + def setup_platform(hass, config, add_devices, discovery_info=None): "Setup Nest Sensor from config file" @@ -75,6 +75,7 @@ class NestSensor(Entity): return "{}({}){}".format(location.capitalize(), name, self.variable) + @property def state(self): """ Returns the state of the sensor. """ @@ -85,12 +86,13 @@ class NestSensor(Entity): def unit_of_measurement(self): return SENSOR_UNITS.get(self.variable, None) + class NestTempSensor(NestSensor): """ Represents a Nest Temperature sensor. """ @property def unit_of_measurement(self): - return self.hass.config.temperature_unit + return TEMP_CELCIUS @property def state(self): @@ -98,7 +100,4 @@ class NestTempSensor(NestSensor): if temp is None: return None - value = convert(temp, TEMP_CELCIUS, - self.hass.config.temperature_unit) - - return round(value, 1) + return round(temp, 1) diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 24f9153c50e..423a3195976 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -16,8 +16,10 @@ import homeassistant.components.nest as nest DEPENDENCIES = ['nest'] + def setup_platform(hass, config, add_devices, discovery_info=None): "Setup nest thermostat" + logger = logging.getLogger(__name__) try: @@ -31,6 +33,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "Connection error logging into the nest web service." ) + class NestThermostat(ThermostatDevice): """ Represents a Nest thermostat. """ From 41acc8fa4326672c5eb4f327589e1071247f5260 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Fri, 15 Jan 2016 08:59:11 +0100 Subject: [PATCH 021/229] Update .coveragerc This shouldn't be excluded according to @balloob because it's a pure python component --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 4b7dbcf856c..272ace975c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -41,7 +41,6 @@ omit = homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/rest.py - homeassistant/components/binary_sensor/command_sensor.py homeassistant/components/browser.py homeassistant/components/camera/* homeassistant/components/device_tracker/actiontec.py From d40e889d3b03918cea48e9a373aaa7cbe4799ad3 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Fri, 15 Jan 2016 09:01:58 +0100 Subject: [PATCH 022/229] Update command_sensor.py Importing CommandSensorData from the command_sensor in sensors in order not to duplicate code --- .../binary_sensor/command_sensor.py | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py index 025b2dfe0ba..e6b5116a60f 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -10,6 +10,7 @@ from datetime import timedelta from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.component.sensor.command_sensor import CommandSensorData from homeassistant.util import template, Throttle _LOGGER = logging.getLogger(__name__) @@ -79,23 +80,3 @@ class CommandBinarySensor(BinarySensorDevice): self._state = True elif value == self._payload_off: self._state = False - - -# pylint: disable=too-few-public-methods -class CommandSensorData(object): - """ Class for handling the data retrieval. """ - - def __init__(self, command): - self.command = command - self.value = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data with a shell command. """ - _LOGGER.info('Running command: %s', self.command) - - try: - return_value = subprocess.check_output(self.command, shell=True) - self.value = return_value.strip().decode('utf-8') - except subprocess.CalledProcessError: - _LOGGER.error('Command failed: %s', self.command) From 9617288bd5677b86eb2890b2c7f86693e616a2c2 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Fri, 15 Jan 2016 08:16:33 -0700 Subject: [PATCH 023/229] multiple inheritance for nest binary sensor --- homeassistant/components/binary_sensor/nest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 421a39e51f5..e1414de5648 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -11,6 +11,7 @@ import socket import homeassistant.components.nest as nest from homeassistant.components.sensor.nest import NestSensor +from homeassistant.components.binary_sensor import BinarySensorDevice BINARY_TYPES = ['fan', @@ -45,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) -class NestBinarySensor(NestSensor): +class NestBinarySensor(NestSensor, BinarySensorDevice): """ Represents a Nst Binary sensor. """ @property From 342a819fd499cdd79ebd481a8b4026118f346356 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sat, 16 Jan 2016 10:45:05 -0500 Subject: [PATCH 024/229] Added toggle service Added a toggle service to the homeassistant, light, switch, and media_player domains. --- homeassistant/components/__init__.py | 11 +++++++- homeassistant/components/light/__init__.py | 26 +++++++++++++++++-- homeassistant/components/light/services.yaml | 12 +++++++++ .../components/media_player/__init__.py | 18 +++++++++++-- homeassistant/components/switch/__init__.py | 13 +++++++++- homeassistant/const.py | 1 + homeassistant/helpers/entity.py | 7 +++++ 7 files changed, 82 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 10e18216ea0..cfc8acb133e 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -20,7 +20,7 @@ import homeassistant.util as util from homeassistant.helpers import extract_entity_ids from homeassistant.loader import get_component from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) _LOGGER = logging.getLogger(__name__) @@ -68,6 +68,14 @@ def turn_off(hass, entity_id=None, **service_data): hass.services.call(ha.DOMAIN, SERVICE_TURN_OFF, service_data) +def toggle(hass, entity_id=None, **service_data): + """ Toggles specified entity. """ + if entity_id is not None: + service_data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(ha.DOMAIN, SERVICE_TOGGLE, service_data) + + def setup(hass, config): """ Setup general services related to homeassistant. """ @@ -105,5 +113,6 @@ def setup(hass, config): hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) + hass.services.register(ha.DOMAIN, SERVICE_TOGGLE, handle_turn_service) return True diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 93321b5fd10..b1017900b17 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -13,7 +13,8 @@ import csv from homeassistant.components import group, discovery, wink, isy994, zwave from homeassistant.config import load_yaml_config_file from homeassistant.const import ( - STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, + ATTR_ENTITY_ID) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent import homeassistant.util as util @@ -114,6 +115,18 @@ def turn_off(hass, entity_id=None, transition=None): hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) +def toggle(hass, entity_id=None, transition=None): + """ Toggles all or specified light. """ + data = { + key: value for key, value in [ + (ATTR_ENTITY_ID, entity_id), + (ATTR_TRANSITION, transition), + ] if value is not None + } + + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + # pylint: disable=too-many-branches, too-many-locals, too-many-statements def setup(hass, config): """ Exposes light control via statemachine and services. """ @@ -165,9 +178,15 @@ def setup(hass, config): if transition is not None: params[ATTR_TRANSITION] = transition + service_fun = None if service.service == SERVICE_TURN_OFF: + service_fun = 'turn_off' + elif service.service == SERVICE_TOGGLE: + service_fun = 'toggle' + + if service_fun: for light in target_lights: - light.turn_off(**params) + getattr(light, service_fun)(**params) for light in target_lights: if light.should_poll: @@ -249,6 +268,9 @@ def setup(hass, config): hass.services.register(DOMAIN, SERVICE_TURN_OFF, handle_light_service, descriptions.get(SERVICE_TURN_OFF)) + hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_light_service, + descriptions.get(SERVICE_TOGGLE)) + return True diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9908737b7b1..8ad2ea97a6b 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -55,3 +55,15 @@ turn_off: transition: description: Duration in seconds it takes to get to next state example: 60 + +toggle: + description: Toggles a light + + fields: + entity_id: + description: Name(s) of entities to toggle + example: 'light.kitchen' + + transition: + description: Duration in seconds it takes to get to next state + example: 60 diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 1b6b9fbfa44..58256c9b8fd 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -14,10 +14,10 @@ from homeassistant.config import load_yaml_config_file from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.const import ( - STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, + STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, - SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_MUTE, SERVICE_TOGGLE, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK) @@ -79,6 +79,7 @@ YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' SERVICE_TO_METHOD = { SERVICE_TURN_ON: 'turn_on', SERVICE_TURN_OFF: 'turn_off', + SERVICE_TOGGLE: 'toggle', SERVICE_VOLUME_UP: 'volume_up', SERVICE_VOLUME_DOWN: 'volume_down', SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause', @@ -131,6 +132,12 @@ def turn_off(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) +def toggle(hass, entity_id=None): + """ Will toggle specified media player or all. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_TOGGLE, 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 {} @@ -532,6 +539,13 @@ class MediaPlayerDevice(Entity): """ Boolean if play media command supported. """ return bool(self.supported_media_commands & SUPPORT_PLAY_MEDIA) + def toggle(self): + """ Toggles the power on the media player. """ + if self.state in [STATE_OFF, STATE_IDLE]: + self.turn_on() + else: + self.turn_off() + def volume_up(self): """ volume_up media player. """ if self.volume_level < 1: diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index a05a673c3dd..8fb930b6a59 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -15,7 +15,8 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( - STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, + ATTR_ENTITY_ID) from homeassistant.components import ( group, discovery, wink, isy994, verisure, zwave, tellduslive, mysensors) @@ -71,6 +72,12 @@ def turn_off(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) +def toggle(hass, entity_id=None): + """ Toggle all or specified switch. """ + data = {ATTR_ENTITY_ID: entity_id} if entity_id else None + hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + + def setup(hass, config): """ Track states and offer events for switches. """ component = EntityComponent( @@ -85,6 +92,8 @@ def setup(hass, config): for switch in target_switches: if service.service == SERVICE_TURN_ON: switch.turn_on() + elif service.service == SERVICE_TOGGLE: + switch.toggle() else: switch.turn_off() @@ -97,6 +106,8 @@ def setup(hass, config): descriptions.get(SERVICE_TURN_OFF)) hass.services.register(DOMAIN, SERVICE_TURN_ON, handle_switch_service, descriptions.get(SERVICE_TURN_ON)) + hass.services.register(DOMAIN, SERVICE_TOGGLE, handle_switch_service, + descriptions.get(SERVICE_TOGGLE)) return True diff --git a/homeassistant/const.py b/homeassistant/const.py index 97e26f8d33a..c493b9d056e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -126,6 +126,7 @@ SERVICE_HOMEASSISTANT_STOP = "stop" SERVICE_TURN_ON = 'turn_on' SERVICE_TURN_OFF = 'turn_off' +SERVICE_TOGGLE = 'toggle' SERVICE_VOLUME_UP = "volume_up" SERVICE_VOLUME_DOWN = "volume_down" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fd2611889c9..576a107ee7b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -174,3 +174,10 @@ class ToggleEntity(Entity): def turn_off(self, **kwargs): """ Turn the entity off. """ pass + + def toggle(self, **kwargs): + """ Toggle the entity off. """ + if self.state == STATE_ON: + self.turn_off(**kwargs) + else: + self.turn_on(**kwargs) From 9f61369156c6bef4904d7f56b07f33f2e596865f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 16 Jan 2016 11:23:55 -0800 Subject: [PATCH 025/229] Version bump to 0.12.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7a0d0dff9be..cb367af362a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """ Constants used by Home Assistant components. """ -__version__ = "0.11.0" +__version__ = "0.12.0.dev0" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' From dd35551047c1f634092e5ff390660b539cce1f6c Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Sat, 16 Jan 2016 12:47:08 -0700 Subject: [PATCH 026/229] fix order of inhertiance --- homeassistant/components/binary_sensor/nest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index e1414de5648..1150e9cc682 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) -class NestBinarySensor(NestSensor, BinarySensorDevice): +class NestBinarySensor(BinarySensorDevice, NestSensor): """ Represents a Nst Binary sensor. """ @property From 4dbd84ead0c0055c7fdee1a40171dba5da550162 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Sat, 16 Jan 2016 12:53:42 -0700 Subject: [PATCH 027/229] make sure everything inherits from NesSensor --- homeassistant/components/sensor/nest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 578c80e3c0f..fa2a245411c 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -89,7 +89,7 @@ class NestBasicSensor(NestSensor): return SENSOR_UNITS.get(self.variable, None) -class NestTempSensor(NestBasicSensor): +class NestTempSensor(NestSensor): """ Represents a Nest Temperature sensor. """ @property From 4ca4941c8265e7a880d03f4e0c3c97fca783358a Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Sat, 16 Jan 2016 12:56:38 -0700 Subject: [PATCH 028/229] fix pylint errors in sensor/nest --- homeassistant/components/sensor/nest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index fa2a245411c..040469d2e13 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -38,7 +38,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in structure.devices: for variable in config['monitored_conditions']: if variable in SENSOR_TYPES: - add_devices([NestBasicSensor(structure, device, variable)]) + add_devices([NestBasicSensor(structure, + device, + variable)]) elif variable in SENSOR_TEMP_TYPES: add_devices([NestTempSensor(structure, device, @@ -78,6 +80,8 @@ class NestSensor(Entity): class NestBasicSensor(NestSensor): + """ Represents a basic Nest sensor with state. """ + @property def state(self): """ Returns the state of the sensor. """ From 0624445627adaa2f016b203796f2e65ac81dd15a Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 17 Jan 2016 16:42:18 -0500 Subject: [PATCH 029/229] Added tests for toggle service. 1) Added tests to toggle service. 2) Removed color_util import in light tests. It was not being used. --- tests/components/light/test_init.py | 40 ++++++++++++++++++++-- tests/components/media_player/test_init.py | 4 ++- tests/components/test_init.py | 14 +++++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 2b2830503f4..fcbe89ee27b 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -9,10 +9,9 @@ import unittest import os import homeassistant.loader as loader -import homeassistant.util.color as color_util from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ON, STATE_OFF, CONF_PLATFORM, - SERVICE_TURN_ON, SERVICE_TURN_OFF) + SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) import homeassistant.components.light as light from tests.common import mock_service, get_test_home_assistant @@ -94,6 +93,23 @@ class TestLight(unittest.TestCase): self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION]) + # Test toggle + toggle_calls = mock_service( + self.hass, light.DOMAIN, SERVICE_TOGGLE) + + light.toggle( + self.hass, entity_id='entity_id_val', transition='transition_val') + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(toggle_calls)) + call = toggle_calls[-1] + + self.assertEqual(light.DOMAIN, call.domain) + self.assertEqual(SERVICE_TOGGLE, call.service) + self.assertEqual('entity_id_val', call.data[ATTR_ENTITY_ID]) + self.assertEqual('transition_val', call.data[light.ATTR_TRANSITION]) + def test_services(self): """ Test the provided services. """ platform = loader.get_component('light.test') @@ -109,7 +125,7 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev2.entity_id)) self.assertFalse(light.is_on(self.hass, dev3.entity_id)) - # Test basic turn_on, turn_off services + # Test basic turn_on, turn_off, toggle services light.turn_off(self.hass, entity_id=dev1.entity_id) light.turn_on(self.hass, entity_id=dev2.entity_id) @@ -136,6 +152,24 @@ class TestLight(unittest.TestCase): self.assertFalse(light.is_on(self.hass, dev2.entity_id)) self.assertFalse(light.is_on(self.hass, dev3.entity_id)) + # toggle all lights + light.toggle(self.hass) + + self.hass.pool.block_till_done() + + self.assertTrue(light.is_on(self.hass, dev1.entity_id)) + self.assertTrue(light.is_on(self.hass, dev2.entity_id)) + self.assertTrue(light.is_on(self.hass, dev3.entity_id)) + + # toggle all lights + light.toggle(self.hass) + + self.hass.pool.block_till_done() + + self.assertFalse(light.is_on(self.hass, dev1.entity_id)) + self.assertFalse(light.is_on(self.hass, dev2.entity_id)) + self.assertFalse(light.is_on(self.hass, dev3.entity_id)) + # Ensure all attributes process correctly light.turn_on(self.hass, dev1.entity_id, transition=10, brightness=20) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 211626ea3fb..a0a7ebc9567 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -12,7 +12,8 @@ from homeassistant.const import ( STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, ATTR_ENTITY_ID) + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_TOGGLE, + ATTR_ENTITY_ID) import homeassistant.components.media_player as media_player from tests.common import mock_service @@ -45,6 +46,7 @@ class TestMediaPlayer(unittest.TestCase): services = { SERVICE_TURN_ON: media_player.turn_on, SERVICE_TURN_OFF: media_player.turn_off, + SERVICE_TOGGLE: media_player.toggle, SERVICE_VOLUME_UP: media_player.volume_up, SERVICE_VOLUME_DOWN: media_player.volume_down, SERVICE_MEDIA_PLAY_PAUSE: media_player.media_play_pause, diff --git a/tests/components/test_init.py b/tests/components/test_init.py index cb170a5c24b..723149b19c6 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -10,7 +10,7 @@ from unittest.mock import patch import homeassistant.core as ha from homeassistant.const import ( - STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) + STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) import homeassistant.components as comps from tests.common import get_test_home_assistant @@ -61,6 +61,18 @@ class TestComponentsCore(unittest.TestCase): self.assertEqual(1, len(runs)) + def test_toggle(self): + """ Test toggle method. """ + runs = [] + self.hass.services.register( + 'light', SERVICE_TOGGLE, lambda x: runs.append(1)) + + comps.toggle(self.hass, 'light.Bowl') + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(runs)) + @patch('homeassistant.core.ServiceRegistry.call') def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): self.hass.services.register('light', SERVICE_TURN_ON, lambda x: x) From 85aa4fdd2e2a82ca23f258cc4d0df940891b2081 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 17 Jan 2016 16:59:22 -0500 Subject: [PATCH 030/229] Revised entity toggle to use is_on The toggle function in the Entity ABC was using state == STATE_ON to determine whether the entity was on. This was revised to use the is_on property instead. --- homeassistant/helpers/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 576a107ee7b..09a3c59370a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -177,7 +177,7 @@ class ToggleEntity(Entity): def toggle(self, **kwargs): """ Toggle the entity off. """ - if self.state == STATE_ON: + if self.is_on: self.turn_off(**kwargs) else: self.turn_on(**kwargs) From 6dc2501116fd7e89b45f002eaf00c26c955e2d66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jan 2016 16:29:41 -0800 Subject: [PATCH 031/229] ps - fix RPi GPIO imports --- homeassistant/components/rpi_gpio.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py index 3d0a068f8ca..bb0d7b841be 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio.py @@ -8,10 +8,6 @@ https://home-assistant.io/components/rpi_gpio/ """ import logging -try: - import RPi.GPIO as GPIO -except ImportError: - GPIO = None from homeassistant.const import (EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['RPi.GPIO==0.6.1'] @@ -22,9 +18,7 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=no-member def setup(hass, config): """ Sets up the Raspberry PI GPIO component. """ - if GPIO is None: - _LOGGER.error('RPi.GPIO not available. rpi_gpio ports ignored.') - return False + import RPi.GPIO as GPIO def cleanup_gpio(event): """ Stuff to do before stop home assistant. """ @@ -41,27 +35,32 @@ def setup(hass, config): def setup_output(port): """ Setup a GPIO as output. """ + import RPi.GPIO as GPIO GPIO.setup(port, GPIO.OUT) def setup_input(port, pull_mode): """ Setup a GPIO as input. """ + import RPi.GPIO as GPIO GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == 'DOWN' else GPIO.PUD_UP) def write_output(port, value): """ Write a value to a GPIO. """ + import RPi.GPIO as GPIO GPIO.output(port, value) def read_input(port): """ Read a value from a GPIO. """ + import RPi.GPIO as GPIO return GPIO.input(port) def edge_detect(port, event_callback, bounce): """ Adds detection for RISING and FALLING events. """ + import RPi.GPIO as GPIO GPIO.add_event_detect( port, GPIO.BOTH, From 06b4fcc2cf8ffdd93f666eac917c0d8caa1c78a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jan 2016 16:36:25 -0800 Subject: [PATCH 032/229] Fix lint errors --- homeassistant/components/rpi_gpio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rpi_gpio.py b/homeassistant/components/rpi_gpio.py index bb0d7b841be..8d75bf4a366 100644 --- a/homeassistant/components/rpi_gpio.py +++ b/homeassistant/components/rpi_gpio.py @@ -6,7 +6,7 @@ Allows to control the GPIO pins of a Raspberry Pi. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/rpi_gpio/ """ - +# pylint: disable=import-error import logging from homeassistant.const import (EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) From a9c6f8c1d9f204f98b90455ca63c6ddbaa814af0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jan 2016 17:50:20 -0800 Subject: [PATCH 033/229] Enforce entity attribute types --- homeassistant/components/sensor/bitcoin.py | 16 ++++++++-------- homeassistant/components/sensor/dht.py | 2 +- homeassistant/components/sensor/ecobee.py | 2 +- homeassistant/components/sensor/efergy.py | 4 ++-- homeassistant/components/sensor/forecast.py | 6 +++--- homeassistant/components/sensor/glances.py | 10 +++++----- .../components/sensor/openweathermap.py | 6 +++--- homeassistant/components/sensor/sabnzbd.py | 2 +- homeassistant/components/sensor/transmission.py | 2 +- homeassistant/components/sensor/verisure.py | 4 ++-- homeassistant/components/sensor/yr.py | 2 +- homeassistant/helpers/entity.py | 15 ++++++++------- 12 files changed, 36 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index ca921f4fca7..dc11f7038c2 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -17,24 +17,24 @@ REQUIREMENTS = ['blockchain==1.1.2'] _LOGGER = logging.getLogger(__name__) OPTION_TYPES = { 'wallet': ['Wallet balance', 'BTC'], - 'exchangerate': ['Exchange rate (1 BTC)', ''], + 'exchangerate': ['Exchange rate (1 BTC)', None], 'trade_volume_btc': ['Trade volume', 'BTC'], 'miners_revenue_usd': ['Miners revenue', 'USD'], 'btc_mined': ['Mined', 'BTC'], 'trade_volume_usd': ['Trade volume', 'USD'], - 'difficulty': ['Difficulty', ''], + 'difficulty': ['Difficulty', None], 'minutes_between_blocks': ['Time between Blocks', 'min'], - 'number_of_transactions': ['No. of Transactions', ''], + 'number_of_transactions': ['No. of Transactions', None], 'hash_rate': ['Hash rate', 'PH/s'], - 'timestamp': ['Timestamp', ''], - 'mined_blocks': ['Minded Blocks', ''], - 'blocks_size': ['Block size', ''], + 'timestamp': ['Timestamp', None], + 'mined_blocks': ['Minded Blocks', None], + 'blocks_size': ['Block size', None], 'total_fees_btc': ['Total fees', 'BTC'], 'total_btc_sent': ['Total sent', 'BTC'], 'estimated_btc_sent': ['Estimated sent', 'BTC'], 'total_btc': ['Total', 'BTC'], - 'total_blocks': ['Total Blocks', ''], - 'next_retarget': ['Next retarget', ''], + 'total_blocks': ['Total Blocks', None], + 'next_retarget': ['Next retarget', None], 'estimated_transaction_volume_usd': ['Est. Transaction volume', 'USD'], 'miners_revenue_btc': ['Miners revenue', 'BTC'], 'market_price_usd': ['Market price', 'USD'] diff --git a/homeassistant/components/sensor/dht.py b/homeassistant/components/sensor/dht.py index 0e39ef7382f..4f11ba7734f 100644 --- a/homeassistant/components/sensor/dht.py +++ b/homeassistant/components/sensor/dht.py @@ -20,7 +20,7 @@ REQUIREMENTS = ['http://github.com/mala-zaba/Adafruit_Python_DHT/archive/' _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'temperature': ['Temperature', ''], + 'temperature': ['Temperature', None], 'humidity': ['Humidity', '%'] } # Return cached results if last scan was less then this time ago diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index 02a2575d88b..91892f63617 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -36,7 +36,7 @@ DEPENDENCIES = ['ecobee'] SENSOR_TYPES = { 'temperature': ['Temperature', TEMP_FAHRENHEIT], 'humidity': ['Humidity', '%'], - 'occupancy': ['Occupancy', ''] + 'occupancy': ['Occupancy', None] } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/efergy.py b/homeassistant/components/sensor/efergy.py index 447903a714e..8eba0149408 100644 --- a/homeassistant/components/sensor/efergy.py +++ b/homeassistant/components/sensor/efergy.py @@ -16,8 +16,8 @@ _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://engage.efergy.com/mobile_proxy/' SENSOR_TYPES = { 'instant_readings': ['Energy Usage', 'kW'], - 'budget': ['Energy Budget', ''], - 'cost': ['Energy Cost', ''], + 'budget': ['Energy Budget', None], + 'cost': ['Energy Cost', None], } diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index 42fbe26b9cf..8cbc332678e 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -19,13 +19,13 @@ _LOGGER = logging.getLogger(__name__) # Sensor types are defined like so: # Name, si unit, us unit, ca unit, uk unit, uk2 unit SENSOR_TYPES = { - 'summary': ['Summary', '', '', '', '', ''], - 'icon': ['Icon', '', '', '', '', ''], + 'summary': ['Summary', None, None, None, None, None], + 'icon': ['Icon', None, None, None, None, None], 'nearest_storm_distance': ['Nearest Storm Distance', 'km', 'm', 'km', 'km', 'm'], 'nearest_storm_bearing': ['Nearest Storm Bearing', '°', '°', '°', '°', '°'], - 'precip_type': ['Precip', '', '', '', '', ''], + 'precip_type': ['Precip', None, None, None, None, None], 'precip_intensity': ['Precip Intensity', 'mm', 'in', 'mm', 'mm', 'mm'], 'precip_probability': ['Precip Probability', '%', '%', '%', '%', '%'], 'temperature': ['Temperature', '°C', '°F', '°C', '°C', '°C'], diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index c2bd96c8eea..eb38e3df265 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -31,11 +31,11 @@ SENSOR_TYPES = { 'swap_use_percent': ['Swap Use', '%'], 'swap_use': ['Swap Use', 'GiB'], 'swap_free': ['Swap Free', 'GiB'], - 'processor_load': ['CPU Load', ''], - 'process_running': ['Running', ''], - 'process_total': ['Total', ''], - 'process_thread': ['Thread', ''], - 'process_sleeping': ['Sleeping', ''] + 'processor_load': ['CPU Load', None], + 'process_running': ['Running', None], + 'process_total': ['Total', None], + 'process_thread': ['Thread', None], + 'process_sleeping': ['Sleeping', None] } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index 84784a19546..a5509904264 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -16,8 +16,8 @@ from homeassistant.helpers.entity import Entity REQUIREMENTS = ['pyowm==2.3.0'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'weather': ['Condition', ''], - 'temperature': ['Temperature', ''], + 'weather': ['Condition', None], + 'temperature': ['Temperature', None], 'wind_speed': ['Wind speed', 'm/s'], 'humidity': ['Humidity', '%'], 'pressure': ['Pressure', 'mbar'], @@ -71,7 +71,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pass if forecast == 1: - SENSOR_TYPES['forecast'] = ['Forecast', ''] + SENSOR_TYPES['forecast'] = ['Forecast', None] dev.append(OpenWeatherMapSensor(data, 'forecast', unit)) add_devices(dev) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 98d76a302dd..6b42453b5d3 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -17,7 +17,7 @@ REQUIREMENTS = ['https://github.com/jamespcole/home-assistant-nzb-clients/' '#python-sabnzbd==0.1'] SENSOR_TYPES = { - 'current_status': ['Status', ''], + 'current_status': ['Status', None], 'speed': ['Speed', 'MB/s'], 'queue_size': ['Queue', 'MB'], 'queue_remaining': ['Left', 'MB'], diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 62afdd39bf4..26062cbba4d 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity import Entity REQUIREMENTS = ['transmissionrpc==0.11'] SENSOR_TYPES = { - 'current_status': ['Status', ''], + 'current_status': ['Status', None], 'download_speed': ['Down Speed', 'MB/s'], 'upload_speed': ['Up Speed', 'MB/s'] } diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index e7c6a30b558..dec678677b4 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -67,7 +67,7 @@ class VerisureThermometer(Entity): return TEMP_CELCIUS # can verisure report in fahrenheit? def update(self): - ''' update sensor ''' + """ update sensor """ verisure.update_climate() @@ -96,5 +96,5 @@ class VerisureHygrometer(Entity): return "%" def update(self): - ''' update sensor ''' + """ update sensor """ verisure.update_climate() diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 24f565feb48..08abffb758d 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -21,7 +21,7 @@ REQUIREMENTS = ['xmltodict'] # Sensor types are defined like so: SENSOR_TYPES = { - 'symbol': ['Symbol', ''], + 'symbol': ['Symbol', None], 'precipitation': ['Condition', 'mm'], 'temperature': ['Temperature', '°C'], 'windSpeed': ['Wind speed', 'm/s'], diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fd2611889c9..980504d1829 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -101,17 +101,18 @@ class Entity(object): state = str(self.state) attr = self.state_attributes or {} - if ATTR_FRIENDLY_NAME not in attr and self.name: - attr[ATTR_FRIENDLY_NAME] = self.name + if ATTR_FRIENDLY_NAME not in attr and self.name is not None: + attr[ATTR_FRIENDLY_NAME] = str(self.name) - if ATTR_UNIT_OF_MEASUREMENT not in attr and self.unit_of_measurement: - attr[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement + if ATTR_UNIT_OF_MEASUREMENT not in attr and \ + self.unit_of_measurement is not None: + attr[ATTR_UNIT_OF_MEASUREMENT] = str(self.unit_of_measurement) - if ATTR_ICON not in attr and self.icon: - attr[ATTR_ICON] = self.icon + if ATTR_ICON not in attr and self.icon is not None: + attr[ATTR_ICON] = str(self.icon) if self.hidden: - attr[ATTR_HIDDEN] = self.hidden + attr[ATTR_HIDDEN] = bool(self.hidden) # overwrite properties that have been set in the config file attr.update(_OVERWRITE.get(self.entity_id, {})) From 8fbb5858743dc4f356bdff3f0126899aad9c3aa8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jan 2016 21:39:25 -0800 Subject: [PATCH 034/229] Fix MQTT reconnecting --- homeassistant/components/mqtt/__init__.py | 193 +++++++++++----------- tests/components/test_mqtt.py | 74 ++++++--- 2 files changed, 150 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 86dce3d511b..c26f03a24f5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -51,7 +51,7 @@ MAX_RECONNECT_WAIT = 300 # seconds def publish(hass, topic, payload, qos=None, retain=None): - """ Send an MQTT message. """ + """Publish message to an MQTT topic.""" data = { ATTR_TOPIC: topic, ATTR_PAYLOAD: payload, @@ -66,9 +66,9 @@ def publish(hass, topic, payload, qos=None, retain=None): def subscribe(hass, topic, callback, qos=DEFAULT_QOS): - """ Subscribe to a topic. """ + """Subscribe to an MQTT topic.""" def mqtt_topic_subscriber(event): - """ Match subscribed MQTT topic. """ + """Match subscribed MQTT topic.""" if _match_topic(topic, event.data[ATTR_TOPIC]): callback(event.data[ATTR_TOPIC], event.data[ATTR_PAYLOAD], event.data[ATTR_QOS]) @@ -78,8 +78,7 @@ def subscribe(hass, topic, callback, qos=DEFAULT_QOS): def setup(hass, config): - """ Get the MQTT protocol service. """ - + """Start the MQTT protocol service.""" if not validate_config(config, {DOMAIN: ['broker']}, _LOGGER): return False @@ -110,16 +109,16 @@ def setup(hass, config): return False def stop_mqtt(event): - """ Stop MQTT component. """ + """Stop MQTT component.""" MQTT_CLIENT.stop() def start_mqtt(event): - """ Launch MQTT component when Home Assistant starts up. """ + """Launch MQTT component when Home Assistant starts up.""" MQTT_CLIENT.start() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_mqtt) def publish_service(call): - """ Handle MQTT publish service calls. """ + """Handle MQTT publish service calls.""" msg_topic = call.data.get(ATTR_TOPIC) payload = call.data.get(ATTR_PAYLOAD) qos = call.data.get(ATTR_QOS, DEFAULT_QOS) @@ -137,148 +136,156 @@ def setup(hass, config): # pylint: disable=too-many-arguments class MQTT(object): - """ Implements messaging service for MQTT. """ + """Home Assistant MQTT client.""" + def __init__(self, hass, broker, port, client_id, keepalive, username, password, certificate): + """Initialize Home Assistant MQTT client.""" import paho.mqtt.client as mqtt - self.userdata = { - 'hass': hass, - 'topics': {}, - 'progress': {}, - } + self.hass = hass + self.topics = {} + self.progress = {} if client_id is None: self._mqttc = mqtt.Client(protocol=mqtt.MQTTv311) else: self._mqttc = mqtt.Client(client_id, protocol=mqtt.MQTTv311) - self._mqttc.user_data_set(self.userdata) - if username is not None: self._mqttc.username_pw_set(username, password) if certificate is not None: self._mqttc.tls_set(certificate) - self._mqttc.on_subscribe = _mqtt_on_subscribe - self._mqttc.on_unsubscribe = _mqtt_on_unsubscribe - self._mqttc.on_connect = _mqtt_on_connect - self._mqttc.on_disconnect = _mqtt_on_disconnect - self._mqttc.on_message = _mqtt_on_message + self._mqttc.on_subscribe = self._mqtt_on_subscribe + self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe + self._mqttc.on_connect = self._mqtt_on_connect + self._mqttc.on_disconnect = self._mqtt_on_disconnect + self._mqttc.on_message = self._mqtt_on_message self._mqttc.connect(broker, port, keepalive) def publish(self, topic, payload, qos, retain): - """ Publish a MQTT message. """ + """Publish a MQTT message.""" self._mqttc.publish(topic, payload, qos, retain) def start(self): - """ Run the MQTT client. """ + """Run the MQTT client.""" self._mqttc.loop_start() def stop(self): - """ Stop the MQTT client. """ + """Stop the MQTT client.""" + self._mqttc.disconnect() self._mqttc.loop_stop() def subscribe(self, topic, qos): - """ Subscribe to a topic. """ - if topic in self.userdata['topics']: + """Subscribe to a topic.""" + assert isinstance(topic, str) + + if topic in self.topics: return result, mid = self._mqttc.subscribe(topic, qos) _raise_on_error(result) - self.userdata['progress'][mid] = topic - self.userdata['topics'][topic] = None + self.progress[mid] = topic + self.topics[topic] = None def unsubscribe(self, topic): - """ Unsubscribe from topic. """ + """Unsubscribe from topic.""" result, mid = self._mqttc.unsubscribe(topic) _raise_on_error(result) - self.userdata['progress'][mid] = topic + self.progress[mid] = topic + def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code): + """On connect callback. -def _mqtt_on_message(mqttc, userdata, msg): - """ Message callback """ - userdata['hass'].bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { - ATTR_TOPIC: msg.topic, - ATTR_QOS: msg.qos, - ATTR_PAYLOAD: msg.payload.decode('utf-8'), - }) + Resubscribe to all topics we were subscribed to. + """ + if result_code != 0: + _LOGGER.error('Unable to connect to the MQTT broker: %s', { + 1: 'Incorrect protocol version', + 2: 'Invalid client identifier', + 3: 'Server unavailable', + 4: 'Bad username or password', + 5: 'Not authorised' + }.get(result_code, 'Unknown reason')) + self._mqttc.disconnect() + return + old_topics = self.topics -def _mqtt_on_connect(mqttc, userdata, flags, result_code): - """ On connect, resubscribe to all topics we were subscribed to. """ - if result_code != 0: - _LOGGER.error('Unable to connect to the MQTT broker: %s', { - 1: 'Incorrect protocol version', - 2: 'Invalid client identifier', - 3: 'Server unavailable', - 4: 'Bad username or password', - 5: 'Not authorised' - }.get(result_code, 'Unknown reason')) - mqttc.disconnect() - return + self.topics = {key: value for key, value in self.topics.items() + if value is None} - old_topics = userdata['topics'] + for topic, qos in old_topics.items(): + # qos is None if we were in process of subscribing + if qos is not None: + self.subscribe(topic, qos) - userdata['topics'] = {} - userdata['progress'] = {} + def _mqtt_on_subscribe(self, _mqttc, _userdata, mid, granted_qos): + """Subscribe successful callback.""" + topic = self.progress.pop(mid, None) + if topic is None: + return + self.topics[topic] = granted_qos[0] - for topic, qos in old_topics.items(): - # qos is None if we were in process of subscribing - if qos is not None: - mqttc.subscribe(topic, qos) + def _mqtt_on_message(self, _mqttc, _userdata, msg): + """Message received callback.""" + self.hass.bus.fire(EVENT_MQTT_MESSAGE_RECEIVED, { + ATTR_TOPIC: msg.topic, + ATTR_QOS: msg.qos, + ATTR_PAYLOAD: msg.payload.decode('utf-8'), + }) + def _mqtt_on_unsubscribe(self, _mqttc, _userdata, mid, granted_qos): + """Unsubscribe successful callback.""" + topic = self.progress.pop(mid, None) + if topic is None: + return + self.topics.pop(topic, None) -def _mqtt_on_subscribe(mqttc, userdata, mid, granted_qos): - """ Called when subscribe successful. """ - topic = userdata['progress'].pop(mid, None) - if topic is None: - return - userdata['topics'][topic] = granted_qos + def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code): + """Disconnected callback.""" + self.progress = {} + self.topics = {key: value for key, value in self.topics.items() + if value is not None} + # Remove None values from topic list + for key in list(self.topics): + if self.topics[key] is None: + self.topics.pop(key) -def _mqtt_on_unsubscribe(mqttc, userdata, mid, granted_qos): - """ Called when subscribe successful. """ - topic = userdata['progress'].pop(mid, None) - if topic is None: - return - userdata['topics'].pop(topic, None) + # When disconnected because of calling disconnect() + if result_code == 0: + return + tries = 0 + wait_time = 0 -def _mqtt_on_disconnect(mqttc, userdata, result_code): - """ Called when being disconnected. """ - # When disconnected because of calling disconnect() - if result_code == 0: - return + while True: + try: + if self._mqttc.reconnect() == 0: + _LOGGER.info('Successfully reconnected to the MQTT server') + break + except socket.error: + pass - tries = 0 - wait_time = 0 - - while True: - try: - if mqttc.reconnect() == 0: - _LOGGER.info('Successfully reconnected to the MQTT server') - break - except socket.error: - pass - - wait_time = min(2**tries, MAX_RECONNECT_WAIT) - _LOGGER.warning( - 'Disconnected from MQTT (%s). Trying to reconnect in %ss', - result_code, wait_time) - # It is ok to sleep here as we are in the MQTT thread. - time.sleep(wait_time) - tries += 1 + wait_time = min(2**tries, MAX_RECONNECT_WAIT) + _LOGGER.warning( + 'Disconnected from MQTT (%s). Trying to reconnect in %ss', + result_code, wait_time) + # It is ok to sleep here as we are in the MQTT thread. + time.sleep(wait_time) + tries += 1 def _raise_on_error(result): - """ Raise error if error result. """ + """Raise error if error result.""" if result != 0: raise HomeAssistantError('Error talking to MQTT: {}'.format(result)) def _match_topic(subscription, topic): - """ Returns if topic matches subscription. """ + """Test if topic matches subscription.""" if subscription.endswith('#'): return (subscription[:-2] == topic or topic.startswith(subscription[:-1])) diff --git a/tests/components/test_mqtt.py b/tests/components/test_mqtt.py index 47a5ac7b4e1..40e473a3572 100644 --- a/tests/components/test_mqtt.py +++ b/tests/components/test_mqtt.py @@ -144,8 +144,15 @@ class TestMQTTCallbacks(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name self.hass = get_test_home_assistant(1) - mock_mqtt_component(self.hass) - self.calls = [] + # mock_mqtt_component(self.hass) + + with mock.patch('paho.mqtt.client.Client'): + mqtt.setup(self.hass, { + mqtt.DOMAIN: { + mqtt.CONF_BROKER: 'mock-broker', + } + }) + self.hass.config.components.append(mqtt.DOMAIN) def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ @@ -162,7 +169,7 @@ class TestMQTTCallbacks(unittest.TestCase): MQTTMessage = namedtuple('MQTTMessage', ['topic', 'qos', 'payload']) message = MQTTMessage('test_topic', 1, 'Hello World!'.encode('utf-8')) - mqtt._mqtt_on_message(None, {'hass': self.hass}, message) + mqtt.MQTT_CLIENT._mqtt_on_message(None, {'hass': self.hass}, message) self.hass.pool.block_till_done() self.assertEqual(1, len(calls)) @@ -173,36 +180,55 @@ class TestMQTTCallbacks(unittest.TestCase): def test_mqtt_failed_connection_results_in_disconnect(self): for result_code in range(1, 6): - mqttc = mock.MagicMock() - mqtt._mqtt_on_connect(mqttc, {'topics': {}}, 0, result_code) - self.assertTrue(mqttc.disconnect.called) + mqtt.MQTT_CLIENT._mqttc = mock.MagicMock() + mqtt.MQTT_CLIENT._mqtt_on_connect(None, {'topics': {}}, 0, + result_code) + self.assertTrue(mqtt.MQTT_CLIENT._mqttc.disconnect.called) def test_mqtt_subscribes_topics_on_connect(self): - prev_topics = { - 'topic/test': 1, - 'home/sensor': 2, - 'still/pending': None - } - mqttc = mock.MagicMock() - mqtt._mqtt_on_connect(mqttc, {'topics': prev_topics}, 0, 0) - self.assertFalse(mqttc.disconnect.called) + from collections import OrderedDict + prev_topics = OrderedDict() + prev_topics['topic/test'] = 1, + prev_topics['home/sensor'] = 2, + prev_topics['still/pending'] = None + + mqtt.MQTT_CLIENT.topics = prev_topics + mqtt.MQTT_CLIENT.progress = {1: 'still/pending'} + # Return values for subscribe calls (rc, mid) + mqtt.MQTT_CLIENT._mqttc.subscribe.side_effect = ((0, 2), (0, 3)) + mqtt.MQTT_CLIENT._mqtt_on_connect(None, None, 0, 0) + self.assertFalse(mqtt.MQTT_CLIENT._mqttc.disconnect.called) expected = [(topic, qos) for topic, qos in prev_topics.items() if qos is not None] - self.assertEqual(expected, [call[1] for call - in mqttc.subscribe.mock_calls]) + self.assertEqual( + expected, + [call[1] for call in mqtt.MQTT_CLIENT._mqttc.subscribe.mock_calls]) + self.assertEqual({ + 1: 'still/pending', + 2: 'topic/test', + 3: 'home/sensor', + }, mqtt.MQTT_CLIENT.progress) def test_mqtt_disconnect_tries_no_reconnect_on_stop(self): - mqttc = mock.MagicMock() - mqtt._mqtt_on_disconnect(mqttc, {}, 0) - self.assertFalse(mqttc.reconnect.called) + mqtt.MQTT_CLIENT._mqtt_on_disconnect(None, None, 0) + self.assertFalse(mqtt.MQTT_CLIENT._mqttc.reconnect.called) @mock.patch('homeassistant.components.mqtt.time.sleep') def test_mqtt_disconnect_tries_reconnect(self, mock_sleep): - mqttc = mock.MagicMock() - mqttc.reconnect.side_effect = [1, 1, 1, 0] - mqtt._mqtt_on_disconnect(mqttc, {}, 1) - self.assertTrue(mqttc.reconnect.called) - self.assertEqual(4, len(mqttc.reconnect.mock_calls)) + mqtt.MQTT_CLIENT.topics = { + 'test/topic': 1, + 'test/progress': None + } + mqtt.MQTT_CLIENT.progress = { + 1: 'test/progress' + } + mqtt.MQTT_CLIENT._mqttc.reconnect.side_effect = [1, 1, 1, 0] + mqtt.MQTT_CLIENT._mqtt_on_disconnect(None, None, 1) + self.assertTrue(mqtt.MQTT_CLIENT._mqttc.reconnect.called) + self.assertEqual(4, len(mqtt.MQTT_CLIENT._mqttc.reconnect.mock_calls)) self.assertEqual([1, 2, 4], [call[1][0] for call in mock_sleep.mock_calls]) + + self.assertEqual({'test/topic': 1}, mqtt.MQTT_CLIENT.topics) + self.assertEqual({}, mqtt.MQTT_CLIENT.progress) From a80917f53012aafb03b7889810740f545d788e61 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Mon, 18 Jan 2016 09:24:38 +0100 Subject: [PATCH 035/229] Update command_sensor.py --- homeassistant/components/binary_sensor/command_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py index e6b5116a60f..11c8af141bd 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -10,7 +10,7 @@ from datetime import timedelta from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.component.sensor.command_sensor import CommandSensorData +from homeassistant.components.sensor.command_sensor import CommandSensorData from homeassistant.util import template, Throttle _LOGGER = logging.getLogger(__name__) From 3b423900623fc1fc801f638ba4bb94b04ba40506 Mon Sep 17 00:00:00 2001 From: Bart274 Date: Mon, 18 Jan 2016 13:48:09 +0100 Subject: [PATCH 036/229] Update command_sensor.py --- homeassistant/components/binary_sensor/command_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_sensor.py b/homeassistant/components/binary_sensor/command_sensor.py index 11c8af141bd..8798e457e71 100644 --- a/homeassistant/components/binary_sensor/command_sensor.py +++ b/homeassistant/components/binary_sensor/command_sensor.py @@ -5,13 +5,12 @@ Allows to configure custom shell commands to turn a value into a logical value for a binary sensor. """ import logging -import subprocess from datetime import timedelta from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.sensor.command_sensor import CommandSensorData -from homeassistant.util import template, Throttle +from homeassistant.util import template _LOGGER = logging.getLogger(__name__) From 291910d74e3975d5ba2b50a2d88ec9eb35061471 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 18 Jan 2016 18:10:32 +0000 Subject: [PATCH 037/229] Add LIFX bulb support --- homeassistant/components/light/lifx.py | 648 +++++++++++++++++++++++++ 1 file changed, 648 insertions(+) create mode 100644 homeassistant/components/light/lifx.py diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py new file mode 100644 index 00000000000..911262b75c9 --- /dev/null +++ b/homeassistant/components/light/lifx.py @@ -0,0 +1,648 @@ +""" +homeassistant.components.light.lifx +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +LiFX platform that implements lights + +Configuration: + +light: + # platform name + platform: lifx + # optional server address + # only needed if using more than one network interface + # (omit if you are unsure) + server: 192.168.1.3 + # optional broadcast address, set to reach all LiFX bulbs + # (omit if you are unsure) + broadcast: 192.168.1.255 + +""" +# pylint: disable=missing-docstring + +import logging +import threading +import time +import queue +import socket +import io +import struct +import ipaddress +import colorsys + +from struct import pack +from enum import IntEnum +from homeassistant.helpers.event import track_time_change +from homeassistant.components.light import \ + (Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = [] +REQUIREMENTS = [] + +CONF_SERVER = "server" # server address configuration item +CONF_BROADCAST = "broadcast" # broadcast address configuration item +RETRIES = 10 # number of packet send retries +DELAY = 0.05 # delay between retries +UDP_PORT = 56700 # udp port for listening socket +UDP_IP = "0.0.0.0" # address for listening socket +MAX_ACK_AGE = 1 # maximum ACK age in seconds +BUFFERSIZE = 1024 # socket buffer size +SHORT_MAX = 65535 # short int maximum +BYTE_MAX = 255 # byte maximum +SEQUENCE_BASE = 1 # packet sequence base +SEQUENCE_COUNT = 255 # packet sequence count + +HUE_MIN = 0 +HUE_MAX = 360 +SATURATION_MIN = 0 +SATURATION_MAX = 255 +BRIGHTNESS_MIN = 0 +BRIGHTNESS_MAX = 65535 +TEMP_MIN = 2500 +TEMP_MAX = 9000 +TEMP_MIN_HASS = 154 +TEMP_MAX_HASS = 500 + + +class PayloadType(IntEnum): + """ LIFX message payload types. """ + GETSERVICE = 2 + STATESERVICE = 3 + GETHOSTINFO = 12 + STATEHOSTINFO = 13 + GETHOSTFIRMWARE = 14 + STATEHOSTFIRMWARE = 15 + GETWIFIINFO = 16 + STATEWIFIINFO = 17 + GETWIFIFIRMWARE = 18 + STATEWIFIFIRMWARE = 19 + GETPOWER1 = 20 + SETPOWER1 = 21 + STATEPOWER1 = 22 + GETLABEL = 23 + SETLABEL = 24 + STATELABEL = 25 + GETVERSION = 32 + STATEVERSION = 33 + GETINFO = 34 + STATEINFO = 35 + ACKNOWLEDGEMENT = 45 + GETLOCATION = 48 + STATELOCATION = 50 + GETGROUP = 51 + STATEGROUP = 53 + ECHOREQUEST = 58 + ECHORESPONSE = 59 + GET = 101 + SETCOLOR = 102 + STATE = 107 + GETPOWER2 = 116 + SETPOWER2 = 117 + STATEPOWER2 = 118 + + +class Power(IntEnum): + """ LIFX power settings. """ + BULB_ON = 65535 + BULB_OFF = 0 + + +def gen_header(sequence, payloadtype): + """ Create LIFX packet header. """ + protocol = bytearray.fromhex("00 34") + source = bytearray.fromhex("42 52 4b 52") + target = bytearray.fromhex("00 00 00 00 00 00 00 00") + reserved1 = bytearray.fromhex("00 00 00 00 00 00") + sequence = pack("B", 3) + reserved2 = bytearray.fromhex("00 00 00 00 00 00 00 00") + packet_type = pack(" Date: Mon, 18 Jan 2016 18:27:46 +0000 Subject: [PATCH 038/229] Remove use of warn() --- homeassistant/components/light/lifx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 911262b75c9..5e43d50eabf 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -523,7 +523,7 @@ class LIFXLight(Light): break if not ack: - _LOGGER.warn("Packet %d not ACK'd", seq) + _LOGGER.warning("Packet %d not ACK'd", seq) # pylint: disable=broad-except except Exception as exc: From 3d23cd10fc96a504e13167b399c886138e67ea8f Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 18 Jan 2016 18:30:09 +0000 Subject: [PATCH 039/229] Attempt to fix ungrouped-imports pylint error --- homeassistant/components/light/lifx.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 5e43d50eabf..c516b273d58 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -25,10 +25,9 @@ import time import queue import socket import io -import struct import ipaddress import colorsys - +import struct from struct import pack from enum import IntEnum from homeassistant.helpers.event import track_time_change From d43101f22a045b58b37cbdaedae19185d9614b53 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 18 Jan 2016 19:41:41 +0100 Subject: [PATCH 040/229] fix issue where sensors and switches were duplicated because of component getting initialized twice. closes #913 --- homeassistant/components/sensor/tellduslive.py | 3 ++- homeassistant/components/switch/tellduslive.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index ae05ce47e19..364b790ce6f 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -18,7 +18,6 @@ from homeassistant.components import tellduslive ATTR_LAST_UPDATED = "time_last_updated" _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['tellduslive'] SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_HUMIDITY = "humidity" @@ -43,6 +42,8 @@ SENSOR_TYPES = { def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up Tellstick sensors. """ + if discovery_info is None: + return sensors = tellduslive.NETWORK.get_sensors() devices = [] diff --git a/homeassistant/components/switch/tellduslive.py b/homeassistant/components/switch/tellduslive.py index d515dcb50a2..b6c7af3ce12 100644 --- a/homeassistant/components/switch/tellduslive.py +++ b/homeassistant/components/switch/tellduslive.py @@ -15,11 +15,12 @@ from homeassistant.components import tellduslive from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['tellduslive'] def setup_platform(hass, config, add_devices, discovery_info=None): """ Find and return Tellstick switches. """ + if discovery_info is None: + return switches = tellduslive.NETWORK.get_switches() add_devices([TelldusLiveSwitch(switch["name"], switch["id"]) From f97ba263c4b9df19d80eb84650d51ee7fabc9121 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jan 2016 09:00:40 -0800 Subject: [PATCH 041/229] Allow forcing MQTT protocol v3.1 --- homeassistant/components/mqtt/__init__.py | 36 ++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c26f03a24f5..2701ccad314 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -24,11 +24,6 @@ DOMAIN = "mqtt" MQTT_CLIENT = None -DEFAULT_PORT = 1883 -DEFAULT_KEEPALIVE = 60 -DEFAULT_QOS = 0 -DEFAULT_RETAIN = False - SERVICE_PUBLISH = 'publish' EVENT_MQTT_MESSAGE_RECEIVED = 'mqtt_message_received' @@ -41,6 +36,16 @@ CONF_KEEPALIVE = 'keepalive' CONF_USERNAME = 'username' CONF_PASSWORD = 'password' CONF_CERTIFICATE = 'certificate' +CONF_PROTOCOL = 'protocol' + +PROTOCOL_31 = '3.1' +PROTOCOL_311 = '3.1.1' + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_QOS = 0 +DEFAULT_RETAIN = False +DEFAULT_PROTOCOL = PROTOCOL_311 ATTR_TOPIC = 'topic' ATTR_PAYLOAD = 'payload' @@ -91,6 +96,12 @@ def setup(hass, config): username = util.convert(conf.get(CONF_USERNAME), str) password = util.convert(conf.get(CONF_PASSWORD), str) certificate = util.convert(conf.get(CONF_CERTIFICATE), str) + protocol = util.convert(conf.get(CONF_PROTOCOL), str, DEFAULT_PROTOCOL) + + if protocol not in (PROTOCOL_31, PROTOCOL_311): + _LOGGER.error('Invalid protocol specified: %s. Allowed values: %s, %s', + protocol, PROTOCOL_31, PROTOCOL_311) + return False # For cloudmqtt.com, secured connection, auto fill in certificate if certificate is None and 19999 < port < 30000 and \ @@ -101,7 +112,7 @@ def setup(hass, config): global MQTT_CLIENT try: MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username, - password, certificate) + password, certificate, protocol) except socket.error: _LOGGER.exception("Can't connect to the broker. " "Please check your settings and the broker " @@ -139,7 +150,7 @@ class MQTT(object): """Home Assistant MQTT client.""" def __init__(self, hass, broker, port, client_id, keepalive, username, - password, certificate): + password, certificate, protocol): """Initialize Home Assistant MQTT client.""" import paho.mqtt.client as mqtt @@ -147,10 +158,15 @@ class MQTT(object): self.topics = {} self.progress = {} - if client_id is None: - self._mqttc = mqtt.Client(protocol=mqtt.MQTTv311) + if protocol == PROTOCOL_31: + proto = mqtt.MQTTv31 else: - self._mqttc = mqtt.Client(client_id, protocol=mqtt.MQTTv311) + proto = mqtt.MQTTv311 + + if client_id is None: + self._mqttc = mqtt.Client(protocol=proto) + else: + self._mqttc = mqtt.Client(client_id, protocol=proto) if username is not None: self._mqttc.username_pw_set(username, password) From 9249dc6dd38866f7397b6cad0cf1169bc6637a9f Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Tue, 19 Jan 2016 19:26:40 +0100 Subject: [PATCH 042/229] Fix missing binary sensor types * Add missing binary sensor types to sensor/mysensors. * Remove unneeded pylint disable. --- homeassistant/components/mysensors.py | 2 -- homeassistant/components/sensor/mysensors.py | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 7fb1a7cb1d7..97c2329656b 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -72,8 +72,6 @@ DISCOVERY_COMPONENTS = [ def setup(hass, config): """Setup the MySensors component.""" - # pylint: disable=too-many-locals - if not validate_config(config, {DOMAIN: [CONF_GATEWAYS]}, _LOGGER): diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 3562af1949d..a3d5da11cbb 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -33,6 +33,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Define the S_TYPES and V_TYPES that the platform should handle as # states. s_types = [ + gateway.const.Presentation.S_DOOR, + gateway.const.Presentation.S_MOTION, + gateway.const.Presentation.S_SMOKE, gateway.const.Presentation.S_TEMP, gateway.const.Presentation.S_HUM, gateway.const.Presentation.S_BARO, @@ -59,6 +62,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): s_types.extend([ gateway.const.Presentation.S_COLOR_SENSOR, gateway.const.Presentation.S_MULTIMETER, + gateway.const.Presentation.S_SPRINKLER, + gateway.const.Presentation.S_WATER_LEAK, + gateway.const.Presentation.S_SOUND, + gateway.const.Presentation.S_VIBRATION, + gateway.const.Presentation.S_MOISTURE, ]) not_v_types.extend([gateway.const.SetReq.V_STATUS, ]) v_types = [member for member in gateway.const.SetReq From 54f65ae87d9d0c8bb4d61d429dc323debf831b70 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 20 Jan 2016 10:57:39 +0100 Subject: [PATCH 043/229] 1) artwork_url might be a relative url (such as /imageproxy). in that case, join it with the base url. note: urllib.parse.urljoin will handle case when the artwork url is absolute. 2) artwork would not be replaced in the user interface because the url did not change between tracks (http://.../cover.jpg). solved by appending internal hash of the media title to the url to force reload --- .../components/media_player/squeezebox.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 4fd13e8da42..871afe3c0bb 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -201,11 +201,18 @@ class SqueezeBoxDevice(MediaPlayerDevice): 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._lms.host, - port=self._lms.http_port, - player=self._id) + media_url = self._status['artwork_url'] + else: + media_url = ('/music/current/cover.jpg?' + 'player={player}&' + 'nocache={nocache}').format( + player=self._id, + nocache=hash(self.media_title)) + + base_url = 'http://{server}:{port}/'.format(server=self._lms.host, + port=self._lms.http_port) + + return urllib.parse.urljoin(base_url, media_url) @property def media_title(self): From 6c5ceaf6863963486883521e749fa5f79c21b840 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 20 Jan 2016 11:06:08 +0000 Subject: [PATCH 044/229] Remove sensor state from switch and wemo --- homeassistant/components/switch/__init__.py | 7 --- homeassistant/components/switch/wemo.py | 49 ++++++++++++--------- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 8fb930b6a59..944d16cf40b 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -30,7 +30,6 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_TODAY_MWH = "today_mwh" ATTR_CURRENT_POWER_MWH = "current_power_mwh" -ATTR_SENSOR_STATE = "sensor_state" MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -48,7 +47,6 @@ DISCOVERY_PLATFORMS = { PROP_TO_ATTR = { 'current_power_mwh': ATTR_CURRENT_POWER_MWH, 'today_power_mw': ATTR_TODAY_MWH, - 'sensor_state': ATTR_SENSOR_STATE } _LOGGER = logging.getLogger(__name__) @@ -131,11 +129,6 @@ class SwitchDevice(ToggleEntity): """ Is the device in standby. """ return None - @property - def sensor_state(self): - """ Is the sensor on or off. """ - return None - @property def device_state_attributes(self): """ Returns device specific state attributes. """ diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index ed56305542d..c3b6d434b73 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -17,6 +17,11 @@ _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None +ATTR_SENSOR_STATE = "sensor_state" +ATTR_SWITCH_MODE = "switch_mode" + +MAKER_SWITCH_MOMENTARY = "momentary" +MAKER_SWITCH_TOGGLE = "toggle" # pylint: disable=unused-argument, too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -88,6 +93,28 @@ class WemoSwitch(SwitchDevice): """ Returns the name of the switch if any. """ return self.wemo.name + @property + def state_attributes(self): + attr = super().state_attributes or {} + + if self.maker_params: + # Is the maker sensor on or off. + if self.maker_params['hassensor']: + # Note a state of 1 matches the WeMo app 'not triggered'! + if self.maker_params['sensorstate'] == '1': + attr[ATTR_SENSOR_STATE] = STATE_OFF + else: + attr[ATTR_SENSOR_STATE] = STATE_ON + + # Is the maker switch configured as toggle(0) or momentary (1). + if self.maker_params['switchmode']: + if self.maker_params['switchmode'] == '1': + attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY + else: + attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE + + return attr + @property def state(self): """ Returns the state. """ @@ -122,28 +149,6 @@ class WemoSwitch(SwitchDevice): else: return True - @property - def sensor_state(self): - """ Is the sensor on or off. """ - if self.maker_params and self.has_sensor: - # Note a state of 1 matches the WeMo app 'not triggered'! - if self.maker_params['sensorstate']: - return STATE_OFF - else: - return STATE_ON - - @property - def switch_mode(self): - """ Is the switch configured as toggle(0) or momentary (1). """ - if self.maker_params: - return self.maker_params['switchmode'] - - @property - def has_sensor(self): - """ Is the sensor present? """ - if self.maker_params: - return self.maker_params['hassensor'] - @property def is_on(self): """ True if switch is on. """ From f4c3ac2a628b8d5b048facb455ff309c2daee37b Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 20 Jan 2016 11:19:00 +0000 Subject: [PATCH 045/229] Tidy --- homeassistant/components/switch/wemo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index c3b6d434b73..f476cdd980b 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -109,9 +109,9 @@ class WemoSwitch(SwitchDevice): # Is the maker switch configured as toggle(0) or momentary (1). if self.maker_params['switchmode']: if self.maker_params['switchmode'] == '1': - attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY + attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY else: - attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE + attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE return attr From 0de9229d75491ee64663820da44bacc3b3e8c6a7 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 20 Jan 2016 11:25:20 +0000 Subject: [PATCH 046/229] More tidying. --- homeassistant/components/switch/wemo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index f476cdd980b..b44a21279ba 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -23,6 +23,7 @@ ATTR_SWITCH_MODE = "switch_mode" MAKER_SWITCH_MOMENTARY = "momentary" MAKER_SWITCH_TOGGLE = "toggle" + # pylint: disable=unused-argument, too-many-function-args def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Find and return WeMo switches. """ From 58ef69b95db8056f12752ea125bc6b59b17ebb9a Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 20 Jan 2016 16:31:51 +0100 Subject: [PATCH 047/229] less hacky way of getting unique cover art --- homeassistant/components/media_player/squeezebox.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 871afe3c0bb..0494b29fedb 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -203,14 +203,12 @@ class SqueezeBoxDevice(MediaPlayerDevice): if 'artwork_url' in self._status: media_url = self._status['artwork_url'] else: - media_url = ('/music/current/cover.jpg?' - 'player={player}&' - 'nocache={nocache}').format( - player=self._id, - nocache=hash(self.media_title)) + media_url = ('/music/{track_id}/cover.jpg').format( + track_id=self._status["id"]) - base_url = 'http://{server}:{port}/'.format(server=self._lms.host, - port=self._lms.http_port) + base_url = 'http://{server}:{port}/'.format( + server=self._lms.host, + port=self._lms.http_port) return urllib.parse.urljoin(base_url, media_url) From 7ad5b3a17b02ca96f8872ee3470845082dae0b9c Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 20 Jan 2016 19:13:29 +0000 Subject: [PATCH 048/229] Fix bug related to maker_param types --- homeassistant/components/switch/wemo.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index b44a21279ba..72631244ea1 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -97,22 +97,21 @@ class WemoSwitch(SwitchDevice): @property def state_attributes(self): attr = super().state_attributes or {} - if self.maker_params: # Is the maker sensor on or off. + if self.entity_id == 'switch.hi_fi_systemline_sensor': if self.maker_params['hassensor']: # Note a state of 1 matches the WeMo app 'not triggered'! - if self.maker_params['sensorstate'] == '1': + if self.maker_params['sensorstate']: attr[ATTR_SENSOR_STATE] = STATE_OFF else: attr[ATTR_SENSOR_STATE] = STATE_ON # Is the maker switch configured as toggle(0) or momentary (1). if self.maker_params['switchmode']: - if self.maker_params['switchmode'] == '1': - attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY - else: - attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE + attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY + else: + attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE return attr From d2d421ca8ffbcbb015a7f2d3c1cd18e5ae576da4 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 20 Jan 2016 20:02:03 +0000 Subject: [PATCH 049/229] Remove ghost debug code. --- homeassistant/components/switch/wemo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 72631244ea1..d828e23464c 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -99,7 +99,6 @@ class WemoSwitch(SwitchDevice): attr = super().state_attributes or {} if self.maker_params: # Is the maker sensor on or off. - if self.entity_id == 'switch.hi_fi_systemline_sensor': if self.maker_params['hassensor']: # Note a state of 1 matches the WeMo app 'not triggered'! if self.maker_params['sensorstate']: From 543190dfb0b467127841b5960d2c1deaf9eff9ea Mon Sep 17 00:00:00 2001 From: Robert Marklund Date: Tue, 19 Jan 2016 19:30:45 +0100 Subject: [PATCH 050/229] sonos: add hosts and interface_addr to sonos config Config can now specify one or more hosts to connect and also a interface_addr to multicast on if multiple interfaces exists. Signed-off-by: Robert Marklund --- homeassistant/components/media_player/sonos.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 71c0c2aeb75..35c5e360bf0 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -38,12 +38,23 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the Sonos platform. """ import soco + import socket if discovery_info: add_devices([SonosDevice(hass, soco.SoCo(discovery_info))]) return True - players = soco.discover() + players = None + hosts = config.get('hosts', None) + if hosts: + players = [] + for host in hosts.split(","): + host = socket.gethostbyname(host) + players.append(soco.SoCo(host)) + + if not players: + players = soco.discover(interface_addr=config.get('interface_addr', + None)) if not players: _LOGGER.warning('No Sonos speakers found.') From 89aa3cbc62a947b3623380f7d1fe631bdf070b98 Mon Sep 17 00:00:00 2001 From: Robert Marklund Date: Wed, 20 Jan 2016 22:43:35 +0100 Subject: [PATCH 051/229] influxdb: fix the need of admin to run Use select statment to show if db exits instead of 'SHOW DATABASES' which cant be run by a non admin user. See https://github.com/influxdata/influxdb/issues/4785 for more info. Also influxdb dont like empty writes('') so ignore state changes of that kind, this happens on startup of home assistant. Signed-off-by: Robert Marklund --- homeassistant/components/influxdb.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 7cbba00afbb..4e9182816da 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -51,14 +51,11 @@ def setup(hass, config): try: influx = InfluxDBClient(host=host, port=port, username=username, password=password, database=database) - databases = [i['name'] for i in influx.get_list_database()] - except exceptions.InfluxDBClientError: - _LOGGER.error("Database host is not accessible. " - "Please check your entries in the configuration file.") - return False - - if database not in databases: - _LOGGER.error("Database %s doesn't exist", database) + influx.query("select * from /.*/ LIMIT 1;") + except exceptions.InfluxDBClientError as exc: + _LOGGER.error("Database host is not accessible due to '%s', please " + "check your entries in the configuration file and that" + " the database exists and is READ/WRITE.", exc) return False def influx_event_listener(event): @@ -76,6 +73,8 @@ def setup(hass, config): _state = 0 else: _state = state.state + if _state == '': + return try: _state = float(_state) except ValueError: @@ -100,7 +99,7 @@ def setup(hass, config): try: influx.write_points(json_body) except exceptions.InfluxDBClientError: - _LOGGER.exception('Error saving event to InfluxDB') + _LOGGER.exception('Error saving event "%s" to InfluxDB', json_body) hass.bus.listen(EVENT_STATE_CHANGED, influx_event_listener) From f6017a17b25a27c489cda09ba44f3afac27eae76 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Thu, 21 Jan 2016 02:02:32 +0100 Subject: [PATCH 052/229] Add Twitter as a notification platform --- .coveragerc | 1 + homeassistant/components/notify/twitter.py | 59 ++++++++++++++++++++++ requirements_all.txt | 3 ++ 3 files changed, 63 insertions(+) create mode 100644 homeassistant/components/notify/twitter.py diff --git a/.coveragerc b/.coveragerc index 57c3d89d647..9242d5e3745 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,7 @@ omit = homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py + homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py new file mode 100644 index 00000000000..7a9941ce7df --- /dev/null +++ b/homeassistant/components/notify/twitter.py @@ -0,0 +1,59 @@ +""" +homeassistant.components.notify.free_mobile +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Twitter platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.twitter/ +""" +import logging +from homeassistant.helpers import validate_config +from homeassistant.components.notify import ( + DOMAIN, BaseNotificationService) +from homeassistant.const import CONF_ACCESS_TOKEN + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['TwitterAPI==2.3.6'] + +CONF_CONSUMER_KEY = "consumer_key" +CONF_CONSUMER_SECRET = "consumer_secret" +CONF_ACCESS_TOKEN_SECRET = "access_token_secret" + + +def get_service(hass, config): + """ Get the Twitter notification service. """ + + if not validate_config({DOMAIN: config}, + {DOMAIN: [CONF_CONSUMER_KEY, CONF_CONSUMER_SECRET, + CONF_ACCESS_TOKEN, + CONF_ACCESS_TOKEN_SECRET]}, + _LOGGER): + return None + + return TwitterNotificationService(config[CONF_CONSUMER_KEY], + config[CONF_CONSUMER_SECRET], + config[CONF_ACCESS_TOKEN], + config[CONF_ACCESS_TOKEN_SECRET]) + + +# pylint: disable=too-few-public-methods +class TwitterNotificationService(BaseNotificationService): + """ Implements notification service for the Twitter service. """ + + def __init__(self, consumer_key, consumer_secret, access_token_key, + access_token_secret): + from TwitterAPI import TwitterAPI + self.api = TwitterAPI(consumer_key, consumer_secret, access_token_key, + access_token_secret) + + def send_message(self, message="", **kwargs): + """ Tweet some message. """ + resp = self.api.request('statuses/update', {'status': message}) + if resp.status_code != 200: + import json + obj = json.loads(resp.text) + error_message = obj['errors'][0]['message'] + error_code = obj['errors'][0]['code'] + _LOGGER.error("Error %s : %s (Code %s)", resp.status_code, + error_message, + error_code) diff --git a/requirements_all.txt b/requirements_all.txt index b6d0af08ee0..0a05e573699 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -116,6 +116,9 @@ slacker==0.6.8 # homeassistant.components.notify.telegram python-telegram-bot==2.8.7 +# homeassistant.components.notify.twitter +TwitterAPI==2.3.6 + # homeassistant.components.notify.xmpp sleekxmpp==1.3.1 From f67747456f78f7238e6b7170be148faf752e587a Mon Sep 17 00:00:00 2001 From: hydreliox Date: Thu, 21 Jan 2016 02:14:11 +0100 Subject: [PATCH 053/229] Clean some errors --- .coveragerc | 2 +- homeassistant/components/notify/twitter.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 9242d5e3745..1eb46d9b37c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,7 +94,7 @@ omit = homeassistant/components/notify/smtp.py homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py - homeassistant/components/notify/twitter.py + homeassistant/components/notify/twitter.py homeassistant/components/notify/xmpp.py homeassistant/components/sensor/arest.py homeassistant/components/sensor/bitcoin.py diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 7a9941ce7df..d38faba7990 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -1,5 +1,5 @@ """ -homeassistant.components.notify.free_mobile +homeassistant.components.notify.twitter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Twitter platform for notify component. From c3a1193ef9b01529e9f92495fb1c3ae58d822c89 Mon Sep 17 00:00:00 2001 From: "zmrowicki@hotmail.com" Date: Wed, 20 Jan 2016 19:36:33 -0800 Subject: [PATCH 054/229] Fix #863 - Added a check for good pull from OWM and return if the object is None --- homeassistant/components/sensor/openweathermap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sensor/openweathermap.py b/homeassistant/components/sensor/openweathermap.py index a5509904264..4caba2dd81e 100644 --- a/homeassistant/components/sensor/openweathermap.py +++ b/homeassistant/components/sensor/openweathermap.py @@ -165,6 +165,10 @@ class WeatherData(object): def update(self): """ Gets the latest data from OpenWeatherMap. """ obs = self.owm.weather_at_coords(self.latitude, self.longitude) + if obs is None: + _LOGGER.warning('Failed to fetch data from OWM') + return + self.data = obs.get_weather() if self.forecast == 1: From e66d15b71d397258b2c886276e21182d20b4e8e3 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 21 Jan 2016 16:31:23 +0000 Subject: [PATCH 055/229] First drafy of sensor.template. --- homeassistant/components/sensor/template.py | 142 ++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 homeassistant/components/sensor/template.py diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py new file mode 100644 index 00000000000..78714f135d0 --- /dev/null +++ b/homeassistant/components/sensor/template.py @@ -0,0 +1,142 @@ +""" +homeassistant.components.sensor.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Allows the creation of a sensor that breaks out state_attributes +from other entities. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.template/ +""" +import logging + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_state_change +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, ATTR_UNIT_OF_MEASUREMENT) + +from homeassistant.util import template + +_LOGGER = logging.getLogger(__name__) + +CONF_SENSORS = 'sensors' +CONF_ENTITIES = 'entities' + +DOT = '.' +QUOTED_DOT = '__________' + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the sensors. """ + + sensors = [] + if config.get(CONF_SENSORS) is None: + _LOGGER.error("Missing configuration data for sensor platfoprm") + return False + + for device in config[CONF_SENSORS]: + device_config = config[CONF_SENSORS].get(device) + if device_config is None: + _LOGGER.error("Missing configuration data for sensor %s", device) + continue + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) + state_template = device_config.get(CONF_VALUE_TEMPLATE) + if state_template is None: + _LOGGER.error( + "Missing %s for sensor %s", CONF_VALUE_TEMPLATE, device) + continue + dependencies = device_config.get(CONF_ENTITIES, []) + sensors.append( + SensorTemplate( + hass, + device, + friendly_name, + unit_of_measurement, + state_template, + dependencies) + ) + if sensors is None: + _LOGGER.error("No sensors added.") + return False + add_devices(sensors) + return True + + +class SensorTemplate(Entity): + """ Represents a Template Sensor. """ + + # pylint: disable=too-many-arguments + def __init__(self, + hass, + entity_name, + friendly_name, + unit_of_measurement, + state_template, + dependencies): + self.hass = hass + # self.entity_id = entity_name + self._name = entity_name + self._friendly_name = friendly_name + self._unit_of_measurement = unit_of_measurement + self._template = state_template + self._entities = dependencies + self._state = '' + + # Quote entity names in template. So replace sun.sun with sunQUOTEsun + # the template engine uses dots! + for entity in self._entities: + self._template = self._template.replace( + entity, entity.replace(DOT, QUOTED_DOT)) + + def _update_callback(_entity_id, _old_state, _new_state): + """ Called when the target device changes state. """ + self.update_ha_state(True) + + for target in dependencies: + track_state_change(hass, target, _update_callback) + self.update() + + @property + def name(self): + """ Returns the name of the device. """ + return self._name + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + @property + def unit_of_measurement(self): + """ Returns the unit_of_measurement of the device. """ + return self._unit_of_measurement + + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + + @property + def state_attributes(self): + attr = {} + + if self._friendly_name: + attr[ATTR_FRIENDLY_NAME] = self._friendly_name + + return attr + + def update(self): + self._state = self._renderer() + + def _renderer(self): + """Render sensor value.""" + render_dictionary = {} + for entity in self._entities: + hass_entity = self.hass.states.get(entity) + if hass_entity is None: + continue + key = entity.replace(DOT, QUOTED_DOT) + render_dictionary[key] = hass_entity + + return template.render(self.hass, self._template, render_dictionary) From a6f37c032bced633d2098f1e8d6033e31307f637 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 21 Jan 2016 17:35:33 +0000 Subject: [PATCH 056/229] Revise to not need dependencies (or quoting)! --- homeassistant/components/sensor/template.py | 38 ++++----------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 78714f135d0..437cc7b936e 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -10,7 +10,7 @@ https://home-assistant.io/components/sensor.template/ import logging from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_state_change +from homeassistant.core import EVENT_STATE_CHANGED from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, ATTR_UNIT_OF_MEASUREMENT) @@ -19,10 +19,6 @@ from homeassistant.util import template _LOGGER = logging.getLogger(__name__) CONF_SENSORS = 'sensors' -CONF_ENTITIES = 'entities' - -DOT = '.' -QUOTED_DOT = '__________' # pylint: disable=unused-argument @@ -46,15 +42,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error( "Missing %s for sensor %s", CONF_VALUE_TEMPLATE, device) continue - dependencies = device_config.get(CONF_ENTITIES, []) sensors.append( SensorTemplate( hass, device, friendly_name, unit_of_measurement, - state_template, - dependencies) + state_template) ) if sensors is None: _LOGGER.error("No sensors added.") @@ -72,30 +66,20 @@ class SensorTemplate(Entity): entity_name, friendly_name, unit_of_measurement, - state_template, - dependencies): + state_template): + self.hass = hass - # self.entity_id = entity_name self._name = entity_name self._friendly_name = friendly_name self._unit_of_measurement = unit_of_measurement self._template = state_template - self._entities = dependencies self._state = '' - # Quote entity names in template. So replace sun.sun with sunQUOTEsun - # the template engine uses dots! - for entity in self._entities: - self._template = self._template.replace( - entity, entity.replace(DOT, QUOTED_DOT)) - - def _update_callback(_entity_id, _old_state, _new_state): + def _update_callback(_event): """ Called when the target device changes state. """ self.update_ha_state(True) - for target in dependencies: - track_state_change(hass, target, _update_callback) - self.update() + self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback) @property def name(self): @@ -131,12 +115,4 @@ class SensorTemplate(Entity): def _renderer(self): """Render sensor value.""" - render_dictionary = {} - for entity in self._entities: - hass_entity = self.hass.states.get(entity) - if hass_entity is None: - continue - key = entity.replace(DOT, QUOTED_DOT) - render_dictionary[key] = hass_entity - - return template.render(self.hass, self._template, render_dictionary) + return template.render(self.hass, self._template) From 92afcae9bed81c3afb6ed58b8f7ef38324c795ac Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 21 Jan 2016 18:31:44 +0000 Subject: [PATCH 057/229] Add test. --- tests/components/sensor/test_template.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/components/sensor/test_template.py diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py new file mode 100644 index 00000000000..15f1be56928 --- /dev/null +++ b/tests/components/sensor/test_template.py @@ -0,0 +1,49 @@ +""" +tests.components.sensor.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests template sensor. +""" +from unittest.mock import patch + +import pytest + +import homeassistant.core as ha +import homeassistant.components.sensor as sensor + + +@pytest.mark.usefixtures('betamax_session') +class TestSensorYr: + """ Test the Yr sensor. """ + + def setup_method(self, method): + self.hass = ha.HomeAssistant() + self.hass.config.latitude = 32.87336 + self.hass.config.longitude = 117.22743 + + def teardown_method(self, method): + """ Stop down stuff we started. """ + self.hass.stop() + + def test_template(self, betamax_session): + with patch('homeassistant.components.sensor.yr.requests.Session', + return_value=betamax_session): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': + '{{ states.sensor.test_state.state }}' + } + } + } + }) + + state = self.hass.states.get('sensor.test_template_sensor') + assert state.state == '' + + self.hass.states.set('sensor.test_state', 'Works') + self.hass.pool.block_till_done() + state = self.hass.states.get('sensor.test_template_sensor') + assert state.state == 'Works' From 0bdd29357213970bc6b4299b16eac6d2552e6f51 Mon Sep 17 00:00:00 2001 From: Robert Marklund Date: Thu, 21 Jan 2016 20:49:17 +0100 Subject: [PATCH 058/229] kodi: remove traceback on state check If the computer is sleeping that are running xbmc then the traceback is printed in the log all the time. Keep the traceback in debug mode of the module if needed. Signed-off-by: Robert Marklund --- homeassistant/components/media_player/kodi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 6fe6be554c6..867255a43c4 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -72,7 +72,8 @@ class KodiDevice(MediaPlayerDevice): try: return self._server.Player.GetActivePlayers() except jsonrpc_requests.jsonrpc.TransportError: - _LOGGER.exception('Unable to fetch kodi data') + _LOGGER.warning('Unable to fetch kodi data') + _LOGGER.debug('Unable to fetch kodi data', exc_info=True) return None @property From f19a46dcfe787133b1130349a0cb7b26fe86d596 Mon Sep 17 00:00:00 2001 From: Robert Marklund Date: Thu, 21 Jan 2016 22:04:18 +0100 Subject: [PATCH 059/229] yr.no: fix some bugs in the component Someone forgot some in:s and () Also added windGust sensor. Signed-off-by: Robert Marklund --- homeassistant/components/sensor/yr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 08abffb758d..66d334cb17d 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -25,6 +25,7 @@ SENSOR_TYPES = { 'precipitation': ['Condition', 'mm'], 'temperature': ['Temperature', '°C'], 'windSpeed': ['Wind speed', 'm/s'], + 'windGust': ['Wind gust', 'm/s'], 'pressure': ['Pressure', 'mbar'], 'windDirection': ['Wind direction', '°'], 'humidity': ['Humidity', '%'], @@ -143,11 +144,11 @@ class YrSensor(Entity): elif self.type == 'symbol' and valid_from < now: self._state = loc_data[self.type]['@number'] break - elif self.type == ('temperature', 'pressure', 'humidity', + elif self.type in ('temperature', 'pressure', 'humidity', 'dewpointTemperature'): self._state = loc_data[self.type]['@value'] break - elif self.type == 'windSpeed': + elif self.type in ('windSpeed', 'windGust'): self._state = loc_data[self.type]['@mps'] break elif self.type == 'windDirection': From 71dbd10b3998f9eeacfbb8bb48f9031dfe0974c5 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 21 Jan 2016 17:18:52 -0500 Subject: [PATCH 060/229] let port be optional --- homeassistant/components/media_player/itunes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 5d08a7e95d4..7bd5f9b928e 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -40,7 +40,10 @@ class Itunes(object): @property def _base_url(self): """ Returns the base url for endpoints. """ - return self.host + ":" + str(self.port) + if self.port: + return self.host + ":" + str(self.port) + else: + return self.host def _request(self, method, path, params=None): """ Makes the actual request and returns the parsed response. """ From 682e3460e068438d98aeddd7d1184e1c950f0c6b Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 21 Jan 2016 17:45:30 -0500 Subject: [PATCH 061/229] use custom material icons to represent speaker state --- .../components/media_player/itunes.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 7bd5f9b928e..94ca5166ef6 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -383,6 +383,14 @@ class AirPlayDevice(MediaPlayerDevice): """ Returns the name of the device. """ return self.device_name + @property + def icon(self): + """ Icon to use in the frontend, if any. """ + if self.selected is True: + return "mdi:volume-high" + else: + return "mdi:volume-off" + @property def state(self): """ Returns the state of the device. """ @@ -408,23 +416,6 @@ class AirPlayDevice(MediaPlayerDevice): """ Flags of media commands that are supported. """ return SUPPORT_AIRPLAY - @property - def device_state_attributes(self): - """ Return the state attributes. """ - state_attr = {} - state_attr[ATTR_SUPPORTED_MEDIA_COMMANDS] = SUPPORT_AIRPLAY - - if self.state == STATE_OFF: - state_attr[ATTR_ENTITY_PICTURE] = \ - ('https://cloud.githubusercontent.com/assets/260/9833073' - '/6eb5c906-5958-11e5-9b4a-472cdf36be16.png') - else: - state_attr[ATTR_ENTITY_PICTURE] = \ - ('https://cloud.githubusercontent.com/assets/260/9833072' - '/6eb13cce-5958-11e5-996f-e2aaefbc9a24.png') - - return state_attr - def set_volume_level(self, volume): """ set volume level, range 0..1. """ volume = int(volume * 100) From a3b8122707abf00c7d99dcc53f70d4c5c8db0e8d Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Thu, 21 Jan 2016 17:54:26 -0500 Subject: [PATCH 062/229] not using these --- homeassistant/components/media_player/itunes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/itunes.py b/homeassistant/components/media_player/itunes.py index 94ca5166ef6..1f51885d731 100644 --- a/homeassistant/components/media_player/itunes.py +++ b/homeassistant/components/media_player/itunes.py @@ -14,8 +14,7 @@ from homeassistant.components.media_player import ( MediaPlayerDevice, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA, - ATTR_ENTITY_PICTURE, ATTR_SUPPORTED_MEDIA_COMMANDS) + SUPPORT_TURN_OFF, SUPPORT_PLAY_MEDIA) from homeassistant.const import ( STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_ON) From c615272c06e27ee78424eaf649570be3a3c2e012 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 21 Jan 2016 23:17:19 +0000 Subject: [PATCH 063/229] Tidy. --- homeassistant/components/sensor/template.py | 36 ++++++++------------- tests/components/sensor/test_template.py | 23 ++++++------- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 437cc7b936e..ecd6f092097 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -12,9 +12,13 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.core import EVENT_STATE_CHANGED from homeassistant.const import ( - ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, ATTR_UNIT_OF_MEASUREMENT) + STATE_UNKNOWN, + ATTR_FRIENDLY_NAME, + CONF_VALUE_TEMPLATE, + ATTR_UNIT_OF_MEASUREMENT) from homeassistant.util import template +from homeassistant.exceptions import TemplateError _LOGGER = logging.getLogger(__name__) @@ -30,9 +34,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Missing configuration data for sensor platfoprm") return False - for device in config[CONF_SENSORS]: - device_config = config[CONF_SENSORS].get(device) - if device_config is None: + for device, device_config in config[CONF_SENSORS].items(): + if not isinstance(device_config, dict): _LOGGER.error("Missing configuration data for sensor %s", device) continue friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) @@ -45,7 +48,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.append( SensorTemplate( hass, - device, friendly_name, unit_of_measurement, state_template) @@ -63,17 +65,15 @@ class SensorTemplate(Entity): # pylint: disable=too-many-arguments def __init__(self, hass, - entity_name, friendly_name, unit_of_measurement, state_template): self.hass = hass - self._name = entity_name - self._friendly_name = friendly_name + self._name = friendly_name self._unit_of_measurement = unit_of_measurement self._template = state_template - self._state = '' + self.update() def _update_callback(_event): """ Called when the target device changes state. """ @@ -101,18 +101,8 @@ class SensorTemplate(Entity): """ Tells Home Assistant not to poll this entity. """ return False - @property - def state_attributes(self): - attr = {} - - if self._friendly_name: - attr[ATTR_FRIENDLY_NAME] = self._friendly_name - - return attr - def update(self): - self._state = self._renderer() - - def _renderer(self): - """Render sensor value.""" - return template.render(self.hass, self._template) + try: + self._state = template.render(self.hass, self._template) + except TemplateError: + self._state = STATE_UNKNOWN diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index 15f1be56928..d2aff600a89 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -12,33 +12,28 @@ import homeassistant.core as ha import homeassistant.components.sensor as sensor -@pytest.mark.usefixtures('betamax_session') class TestSensorYr: """ Test the Yr sensor. """ def setup_method(self, method): self.hass = ha.HomeAssistant() - self.hass.config.latitude = 32.87336 - self.hass.config.longitude = 117.22743 def teardown_method(self, method): """ Stop down stuff we started. """ self.hass.stop() def test_template(self, betamax_session): - with patch('homeassistant.components.sensor.yr.requests.Session', - return_value=betamax_session): - assert sensor.setup(self.hass, { - 'sensor': { - 'platform': 'template', - 'sensors': { - 'test_template_sensor': { - 'value_template': - '{{ states.sensor.test_state.state }}' - } + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': + '{{ states.sensor.test_state.state }}' } } - }) + } + }) state = self.hass.states.get('sensor.test_template_sensor') assert state.state == '' From 56ce3e5f5ac44a1820c93ec11d946ab60ccc3350 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 21 Jan 2016 11:12:00 -0500 Subject: [PATCH 064/229] Wink siren support --- homeassistant/components/light/wink.py | 2 +- homeassistant/components/lock/wink.py | 2 +- homeassistant/components/sensor/wink.py | 2 +- homeassistant/components/switch/wink.py | 4 +++- homeassistant/components/wink.py | 3 ++- requirements_all.txt | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 294f092c050..f9cdfc432f9 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -12,7 +12,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.4.1'] +REQUIREMENTS = ['python-wink==0.4.2'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 6c457c722f9..a203a59bf69 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -11,7 +11,7 @@ import logging from homeassistant.components.lock import LockDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.4.1'] +REQUIREMENTS = ['python-wink==0.4.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 8acc99f2d3d..f57f8fdc4cf 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -11,7 +11,7 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_ACCESS_TOKEN, STATE_OPEN, STATE_CLOSED -REQUIREMENTS = ['python-wink==0.4.1'] +REQUIREMENTS = ['python-wink==0.4.2'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index e01065c3cd4..88df1a158ef 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -11,7 +11,7 @@ import logging from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.4.1'] +REQUIREMENTS = ['python-wink==0.4.2'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -32,3 +32,5 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkToggleDevice(switch) for switch in pywink.get_switches()) add_devices(WinkToggleDevice(switch) for switch in pywink.get_powerstrip_outlets()) + add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens()) + diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index a719c5613a4..29a7a5537d1 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ATTR_SERVICE, ATTR_DISCOVERED, ATTR_FRIENDLY_NAME) DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.4.1'] +REQUIREMENTS = ['python-wink==0.4.2'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" @@ -38,6 +38,7 @@ def setup(hass, config): for component_name, func_exists, discovery_type in ( ('light', pywink.get_bulbs, DISCOVER_LIGHTS), ('switch', lambda: pywink.get_switches or + pywink.get_sirens or pywink.get_powerstrip_outlets, DISCOVER_SWITCHES), ('sensor', lambda: pywink.get_sensors or pywink.get_eggtrays, DISCOVER_SENSORS), diff --git a/requirements_all.txt b/requirements_all.txt index 0a05e573699..45c57425eac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -69,7 +69,7 @@ pyvera==0.2.7 # homeassistant.components.lock.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.4.1 +python-wink==0.4.2 # homeassistant.components.media_player.cast pychromecast==0.6.14 From c95b03f2408b75ebea2a4eb08ede1c0a9f535abb Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Thu, 21 Jan 2016 11:19:42 -0500 Subject: [PATCH 065/229] Fixed style --- homeassistant/components/switch/wink.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 88df1a158ef..7858029e388 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -33,4 +33,3 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkToggleDevice(switch) for switch in pywink.get_powerstrip_outlets()) add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens()) - From a91163877f35db5e456fa4b66c1f048ced947ba9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jan 2016 22:47:25 -0800 Subject: [PATCH 066/229] Script: use config service helper --- homeassistant/components/script.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 238bea7dd4c..52dfdc49281 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -15,6 +15,7 @@ import threading from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.service import call_from_config from homeassistant.util import slugify, split_entity_id import homeassistant.util.dt as date_util from homeassistant.const import ( @@ -30,7 +31,8 @@ STATE_NOT_RUNNING = 'Not Running' CONF_ALIAS = "alias" CONF_SERVICE = "service" CONF_SERVICE_OLD = "execute_service" -CONF_SERVICE_DATA = "service_data" +CONF_SERVICE_DATA = "data" +CONF_SERVICE_DATA_OLD = "service_data" CONF_SEQUENCE = "sequence" CONF_EVENT = "event" CONF_EVENT_DATA = "event_data" @@ -194,13 +196,17 @@ class Script(ToggleEntity): def _call_service(self, action): """ Calls the service specified in the action. """ - conf_service = action.get(CONF_SERVICE, action.get(CONF_SERVICE_OLD)) - self._last_action = action.get(CONF_ALIAS, conf_service) + # Backwards compatibility + if CONF_SERVICE not in action and CONF_SERVICE_OLD in action: + action[CONF_SERVICE] = action[CONF_SERVICE_OLD] + + if CONF_SERVICE_DATA not in action and CONF_SERVICE_DATA_OLD in action: + action[CONF_SERVICE_DATA] = action[CONF_SERVICE_DATA_OLD] + + self._last_action = action.get(CONF_ALIAS, action[CONF_SERVICE]) _LOGGER.info("Executing script %s step %s", self._name, self._last_action) - domain, service = split_entity_id(conf_service) - data = action.get(CONF_SERVICE_DATA, {}) - self.hass.services.call(domain, service, data, True) + call_from_config(self.hass, action, True) def _fire_event(self, action): """ Fires an event. """ From 6bbbbd9e1730eb7c4f52c9000601c8ba198226db Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jan 2016 23:07:43 -0800 Subject: [PATCH 067/229] Update mdi icons --- homeassistant/components/frontend/mdi_version.py | 2 +- homeassistant/components/frontend/www_static/mdi.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontend/mdi_version.py b/homeassistant/components/frontend/mdi_version.py index a8106ecd77e..4fa9a33b78c 100644 --- a/homeassistant/components/frontend/mdi_version.py +++ b/homeassistant/components/frontend/mdi_version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by update_mdi script """ -VERSION = "7d76081c37634d36af21f5cc1ca79408" +VERSION = "a2605736c8d959d50c4bcbba1e6a6aa5" diff --git a/homeassistant/components/frontend/www_static/mdi.html b/homeassistant/components/frontend/www_static/mdi.html index 13b003806b3..71e48096e6c 100644 --- a/homeassistant/components/frontend/www_static/mdi.html +++ b/homeassistant/components/frontend/www_static/mdi.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From d59b98ee2b003fba5d75ee3f9dfa0c4e3347e3a0 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 19 Jan 2016 08:10:17 -0500 Subject: [PATCH 068/229] Added Insteon Support --- .coveragerc | 3 + .gitignore | 4 + homeassistant/components/insteon.py | 93 ++++++++++++++++++++++ homeassistant/components/light/__init__.py | 4 +- homeassistant/components/light/insteon.py | 16 ++++ requirements_all.txt | 3 + 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/insteon.py create mode 100644 homeassistant/components/light/insteon.py diff --git a/.coveragerc b/.coveragerc index 1eb46d9b37c..a2cf15f021a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,9 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py + homeassistant/components/insteon.py + homeassistant/components/*/insteon.py + homeassistant/components/isy994.py homeassistant/components/*/isy994.py diff --git a/.gitignore b/.gitignore index 3ee71808ab1..90562a0f909 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,7 @@ nosetests.xml # venv stuff pyvenv.cfg pip-selfcheck.json + +# vimmy stuff +*.swp +*.swo diff --git a/homeassistant/components/insteon.py b/homeassistant/components/insteon.py new file mode 100644 index 00000000000..76ca2d303a7 --- /dev/null +++ b/homeassistant/components/insteon.py @@ -0,0 +1,93 @@ +""" +homeassistant.components.light +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Insteon Hub. +""" + +import logging +import homeassistant.bootstrap as bootstrap +from homeassistant.helpers import validate_config +from homeassistant.loader import get_component +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, ATTR_DISCOVERED, + ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED) + +# The domain of your component. Should be equal to the name of your component +DOMAIN = "insteon" + +# List of component names (string) your component depends upon +REQUIREMENTS = [ + 'insteon_hub==0.4.5' +] + +API_KEY = "3eb14d15-a486-4d9e-99af-179d0e9417c11444718937.80636061" +INSTEON = None + +DISCOVER_LIGHTS = "insteon.lights" + +_LOGGER = logging.getLogger(__name__) + + +def setup(hass, config): + """ + Setup Insteon Hub component. + This will automatically import associated lights. + """ + if not validate_config( + config, + {DOMAIN: [CONF_USERNAME, CONF_PASSWORD]}, + _LOGGER): + return False + + import insteon + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + global INSTEON + INSTEON = insteon.Insteon(username, password, API_KEY) + + comp_name = 'light' + discovery = DISCOVER_LIGHTS + component = get_component(comp_name) + bootstrap.setup_component(hass, component.DOMAIN, config) + hass.bus.fire( + EVENT_PLATFORM_DISCOVERED, + {ATTR_SERVICE: discovery, ATTR_DISCOVERED: {}}) + return True + + +class InsteonToggleDevice(ToggleEntity): + """ Abstract Class for an Insteon node. """ + + def __init__(self, node): + self.node = node + self._value = 0 + + @property + def name(self): + """ Returns the name of the node. """ + return self.node.DeviceName + + @property + def unique_id(self): + """ Returns the id of this insteon node. """ + return self.node.DeviceID + + def update(self): + """ Update state of the sensor. """ + resp = self.node.send_command('get_status', wait=True) + try: + self._value = resp['response']['level'] + except KeyError: + pass + + @property + def is_on(self): + """ Returns boolean response if the node is on. """ + return self._value != 0 + + def turn_on(self, **kwargs): + self.node.send_command('on') + + def turn_off(self, **kwargs): + self.node.send_command('off') diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b1017900b17..47bfd68345f 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -10,7 +10,8 @@ import logging import os import csv -from homeassistant.components import group, discovery, wink, isy994, zwave +from homeassistant.components import ( + group, discovery, wink, isy994, zwave, insteon) from homeassistant.config import load_yaml_config_file from homeassistant.const import ( STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, @@ -59,6 +60,7 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" # Maps discovered services to their platforms DISCOVERY_PLATFORMS = { wink.DISCOVER_LIGHTS: 'wink', + insteon.DISCOVER_LIGHTS: 'insteon', isy994.DISCOVER_LIGHTS: 'isy994', discovery.SERVICE_HUE: 'hue', zwave.DISCOVER_LIGHTS: 'zwave', diff --git a/homeassistant/components/light/insteon.py b/homeassistant/components/light/insteon.py new file mode 100644 index 00000000000..07c4d19b202 --- /dev/null +++ b/homeassistant/components/light/insteon.py @@ -0,0 +1,16 @@ +""" +homeassistant.components.light.insteon +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Support for Insteon Hub lights. +""" + +from homeassistant.components.insteon import (INSTEON, InsteonToggleDevice) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Sets up the Insteon Hub light platform. """ + devs = [] + for device in INSTEON.devices: + if device.DeviceCategory == "Switched Lighting Control": + devs.append(InsteonToggleDevice(device)) + add_devices(devs) diff --git a/requirements_all.txt b/requirements_all.txt index 45c57425eac..087e22afd2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -39,6 +39,9 @@ pyfttt==0.3 # homeassistant.components.influxdb influxdb==2.10.0 +# homeassistant.components.insteon +insteon_hub==0.4.5 + # homeassistant.components.isy994 PyISY==1.0.5 From bb97af1504bb791c77a6d923415e92dec9bd9c22 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Jan 2016 23:53:57 -0800 Subject: [PATCH 069/229] Allow passive zones --- homeassistant/components/zone.py | 36 +++++---- tests/components/test_zone.py | 126 +++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 tests/components/test_zone.py diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index da0341129f7..86e2cf5062e 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -24,6 +24,9 @@ DEFAULT_NAME = 'Unnamed zone' ATTR_RADIUS = 'radius' DEFAULT_RADIUS = 100 +ATTR_PASSIVE = 'passive' +DEFAULT_PASSIVE = False + ICON_HOME = 'mdi:home' @@ -37,6 +40,9 @@ def active_zone(hass, latitude, longitude, radius=0): closest = None for zone in zones: + if zone.attributes.get(ATTR_PASSIVE): + continue + zone_dist = distance( latitude, longitude, zone.attributes[ATTR_LATITUDE], zone.attributes[ATTR_LONGITUDE]) @@ -78,13 +84,14 @@ def setup(hass, config): longitude = entry.get(ATTR_LONGITUDE) radius = entry.get(ATTR_RADIUS, DEFAULT_RADIUS) icon = entry.get(ATTR_ICON) + passive = entry.get(ATTR_PASSIVE, DEFAULT_PASSIVE) if None in (latitude, longitude): logging.getLogger(__name__).error( 'Each zone needs a latitude and longitude.') continue - zone = Zone(hass, name, latitude, longitude, radius, icon) + zone = Zone(hass, name, latitude, longitude, radius, icon, passive) zone.entity_id = generate_entity_id(ENTITY_ID_FORMAT, name, entities) zone.update_ha_state() @@ -92,7 +99,7 @@ def setup(hass, config): if ENTITY_ID_HOME not in entities: zone = Zone(hass, hass.config.location_name, hass.config.latitude, - hass.config.longitude, DEFAULT_RADIUS, ICON_HOME) + hass.config.longitude, DEFAULT_RADIUS, ICON_HOME, False) zone.entity_id = ENTITY_ID_HOME zone.update_ha_state() @@ -101,17 +108,15 @@ def setup(hass, config): class Zone(Entity): """ Represents a Zone in Home Assistant. """ - # pylint: disable=too-many-arguments - def __init__(self, hass, name, latitude, longitude, radius, icon): + # pylint: disable=too-many-arguments, too-many-instance-attributes + def __init__(self, hass, name, latitude, longitude, radius, icon, passive): self.hass = hass self._name = name - self.latitude = latitude - self.longitude = longitude - self.radius = radius + self._latitude = latitude + self._longitude = longitude + self._radius = radius self._icon = icon - - def should_poll(self): - return False + self._passive = passive @property def name(self): @@ -128,9 +133,12 @@ class Zone(Entity): @property def state_attributes(self): - return { + data = { ATTR_HIDDEN: True, - ATTR_LATITUDE: self.latitude, - ATTR_LONGITUDE: self.longitude, - ATTR_RADIUS: self.radius, + ATTR_LATITUDE: self._latitude, + ATTR_LONGITUDE: self._longitude, + ATTR_RADIUS: self._radius, } + if self._passive: + data[ATTR_PASSIVE] = self._passive + return data diff --git a/tests/components/test_zone.py b/tests/components/test_zone.py new file mode 100644 index 00000000000..7275767d7be --- /dev/null +++ b/tests/components/test_zone.py @@ -0,0 +1,126 @@ +""" +tests.components.automation.test_location +±±±~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests location automation. +""" +import unittest + +from homeassistant.components import zone + +from tests.common import get_test_home_assistant + + +class TestAutomationZone(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_setup(self): + info = { + 'name': 'Test Zone', + 'latitude': 32.880837, + 'longitude': -117.237561, + 'radius': 250, + 'passive': True + } + assert zone.setup(self.hass, { + 'zone': info + }) + + state = self.hass.states.get('zone.test_zone') + assert info['name'] == state.name + assert info['latitude'] == state.attributes['latitude'] + assert info['longitude'] == state.attributes['longitude'] + assert info['radius'] == state.attributes['radius'] + assert info['passive'] == state.attributes['passive'] + + def test_active_zone_skips_passive_zones(self): + assert zone.setup(self.hass, { + 'zone': [ + { + 'name': 'Passive Zone', + 'latitude': 32.880600, + 'longitude': -117.237561, + 'radius': 250, + 'passive': True + }, + ] + }) + + active = zone.active_zone(self.hass, 32.880600, -117.237561) + assert active is None + + assert zone.setup(self.hass, { + 'zone': [ + { + 'name': 'Active Zone', + 'latitude': 32.880800, + 'longitude': -117.237561, + 'radius': 500, + }, + ] + }) + + active = zone.active_zone(self.hass, 32.880700, -117.237561) + assert 'zone.active_zone' == active.entity_id + + def test_active_zone_prefers_smaller_zone_if_same_distance(self): + latitude = 32.880600 + longitude = -117.237561 + assert zone.setup(self.hass, { + 'zone': [ + { + 'name': 'Small Zone', + 'latitude': latitude, + 'longitude': longitude, + 'radius': 250, + }, + { + 'name': 'Big Zone', + 'latitude': latitude, + 'longitude': longitude, + 'radius': 500, + }, + ] + }) + + active = zone.active_zone(self.hass, latitude, longitude) + assert 'zone.small_zone' == active.entity_id + + assert zone.setup(self.hass, { + 'zone': [ + { + 'name': 'Smallest Zone', + 'latitude': latitude, + 'longitude': longitude, + 'radius': 50, + }, + ] + }) + + active = zone.active_zone(self.hass, latitude, longitude) + assert 'zone.smallest_zone' == active.entity_id + + def test_in_zone_works_for_passive_zones(self): + latitude = 32.880600 + longitude = -117.237561 + assert zone.setup(self.hass, { + 'zone': [ + { + 'name': 'Passive Zone', + 'latitude': latitude, + 'longitude': longitude, + 'radius': 250, + 'passive': True + }, + ] + }) + + assert zone.in_zone(self.hass.states.get('zone.passive_zone'), + latitude, longitude) From b1f7b5c6d7c4e0ebf38557bbef301f8574c4e1cd Mon Sep 17 00:00:00 2001 From: pavoni Date: Fri, 22 Jan 2016 09:37:20 +0000 Subject: [PATCH 070/229] Tidy, add test for ValueException logic. --- tests/components/sensor/test_template.py | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index d2aff600a89..b4f6ab41c97 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -12,8 +12,8 @@ import homeassistant.core as ha import homeassistant.components.sensor as sensor -class TestSensorYr: - """ Test the Yr sensor. """ +class TestTemplateSensor: + """ Test the Template sensor. """ def setup_method(self, method): self.hass = ha.HomeAssistant() @@ -22,14 +22,14 @@ class TestSensorYr: """ Stop down stuff we started. """ self.hass.stop() - def test_template(self, betamax_session): + def test_template(self): assert sensor.setup(self.hass, { 'sensor': { 'platform': 'template', 'sensors': { 'test_template_sensor': { 'value_template': - '{{ states.sensor.test_state.state }}' + "{{ states.sensor.test_state.state }}" } } } @@ -42,3 +42,22 @@ class TestSensorYr: self.hass.pool.block_till_done() state = self.hass.states.get('sensor.test_template_sensor') assert state.state == 'Works' + + def test_template_syntax_error(self): + assert sensor.setup(self.hass, { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test_template_sensor': { + 'value_template': + "{% if rubbish %}" + } + } + } + }) + + + self.hass.states.set('sensor.test_state', 'Works') + self.hass.pool.block_till_done() + state = self.hass.states.get('sensor.test_template_sensor') + assert state.state == 'unknown' From 87a9fd825230bdbbd88e3305280cdb4118e9bb0a Mon Sep 17 00:00:00 2001 From: pavoni Date: Fri, 22 Jan 2016 11:30:04 +0000 Subject: [PATCH 071/229] Handle race condition on startup. --- homeassistant/components/sensor/template.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index ecd6f092097..eeb764e70a5 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -77,7 +77,10 @@ class SensorTemplate(Entity): def _update_callback(_event): """ Called when the target device changes state. """ - self.update_ha_state(True) + # This can be called before the entity is properly + # initialised, so check before updating state, + if self.entity_id: + self.update_ha_state(True) self.hass.bus.listen(EVENT_STATE_CHANGED, _update_callback) From 143b08d661562f8cd0512ec5893766e412073798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Kut=C3=BD?= <6du1ro.n@gmail.com> Date: Thu, 21 Jan 2016 18:08:39 +0100 Subject: [PATCH 072/229] Add statsd component. --- .coveragerc | 1 + homeassistant/components/statsd.py | 89 ++++++++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 93 insertions(+) create mode 100644 homeassistant/components/statsd.py diff --git a/.coveragerc b/.coveragerc index a2cf15f021a..b7a8e3ef1e3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -72,6 +72,7 @@ omit = homeassistant/components/discovery.py homeassistant/components/downloader.py homeassistant/components/ifttt.py + homeassistant/components/statsd.py homeassistant/components/influxdb.py homeassistant/components/keyboard.py homeassistant/components/light/blinksticklight.py diff --git a/homeassistant/components/statsd.py b/homeassistant/components/statsd.py new file mode 100644 index 00000000000..79bab00be0b --- /dev/null +++ b/homeassistant/components/statsd.py @@ -0,0 +1,89 @@ +""" +homeassistant.components.statsd +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +StatsD component which allows you to send data to many backends. + +For list supported backends visit this link: +https://github.com/etsy/statsd/blob/master/docs/backend.md + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/statsd/ +""" +import logging +import homeassistant.util as util +from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF, + STATE_UNLOCKED, STATE_LOCKED, STATE_UNKNOWN) +from homeassistant.components.sun import (STATE_ABOVE_HORIZON, + STATE_BELOW_HORIZON) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "statsd" +DEPENDENCIES = [] + +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8125 +DEFAULT_PREFIX = 'hass' +DEFAULT_RATE = 1 + +REQUIREMENTS = ['python-statsd==1.7.2'] + +CONF_HOST = 'host' +CONF_PORT = 'port' +CONF_PREFIX = 'prefix' +CONF_RATE = 'rate' + + +def setup(hass, config): + """ Setup the StatsD component. """ + + from statsd.compat import NUM_TYPES + import statsd + + conf = config[DOMAIN] + + host = conf[CONF_HOST] + port = util.convert(conf.get(CONF_PORT), int, DEFAULT_PORT) + sample_rate = util.convert(conf.get(CONF_RATE), int, DEFAULT_RATE) + prefix = util.convert(conf.get(CONF_PREFIX), str, DEFAULT_PREFIX) + + statsd_connection = statsd.Connection( + host=host, + port=port, + sample_rate=sample_rate, + disabled=False + ) + + meter = statsd.Gauge(prefix, statsd_connection) + + def statsd_event_listener(event): + """ Listen for new messages on the bus and sends them to StatsD. """ + + state = event.data.get('new_state') + + if state is None: + return + + if state.state in (STATE_ON, STATE_LOCKED, STATE_ABOVE_HORIZON): + _state = 1 + elif state.state in (STATE_OFF, STATE_UNLOCKED, STATE_UNKNOWN, + STATE_BELOW_HORIZON): + _state = 0 + else: + _state = state.state + if _state == '': + return + try: + _state = float(_state) + except ValueError: + pass + + if not isinstance(_state, NUM_TYPES): + return + + _LOGGER.debug('Sending %s.%s', state.entity_id, _state) + meter.send(state.entity_id, _state) + + hass.bus.listen(EVENT_STATE_CHANGED, statsd_event_listener) + + return True diff --git a/requirements_all.txt b/requirements_all.txt index 087e22afd2f..2241aaecdda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -177,6 +177,9 @@ python-twitch==1.2.0 # homeassistant.components.sensor.yr xmltodict +# homeassistant.components.statsd +python-statsd==1.7.2 + # homeassistant.components.sun astral==0.8.1 From ad62591f434a750fa4edee40292ba6e698cc497a Mon Sep 17 00:00:00 2001 From: pavoni Date: Fri, 22 Jan 2016 16:30:02 +0000 Subject: [PATCH 073/229] Change error state to be 'error' rather than 'unknown', trace error. --- homeassistant/components/sensor/template.py | 8 +++++--- tests/components/sensor/test_template.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index eeb764e70a5..a75d0a3e656 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -12,7 +12,6 @@ import logging from homeassistant.helpers.entity import Entity from homeassistant.core import EVENT_STATE_CHANGED from homeassistant.const import ( - STATE_UNKNOWN, ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, ATTR_UNIT_OF_MEASUREMENT) @@ -24,6 +23,8 @@ _LOGGER = logging.getLogger(__name__) CONF_SENSORS = 'sensors' +STATE_ERROR = 'error' + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -107,5 +108,6 @@ class SensorTemplate(Entity): def update(self): try: self._state = template.render(self.hass, self._template) - except TemplateError: - self._state = STATE_UNKNOWN + except TemplateError as ex: + self._state = STATE_ERROR + _LOGGER.error(ex) diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index b4f6ab41c97..513117a8a9e 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -60,4 +60,4 @@ class TestTemplateSensor: self.hass.states.set('sensor.test_state', 'Works') self.hass.pool.block_till_done() state = self.hass.states.get('sensor.test_template_sensor') - assert state.state == 'unknown' + assert state.state == 'error' From 613f8d0bd2167ec33129b411c0d761028dd5c495 Mon Sep 17 00:00:00 2001 From: hydreliox Date: Fri, 22 Jan 2016 21:15:07 +0100 Subject: [PATCH 074/229] Add icons to NetAtmo sensors --- homeassistant/components/sensor/netatmo.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index d1830cd9811..0375fcf4d74 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -23,11 +23,11 @@ REQUIREMENTS = [ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_CELCIUS], - 'co2': ['CO2', 'ppm'], - 'pressure': ['Pressure', 'mbar'], - 'noise': ['Noise', 'dB'], - 'humidity': ['Humidity', '%'] + 'temperature': ['Temperature', TEMP_CELCIUS, 'mdi:thermometer'], + 'co2': ['CO2', 'ppm', 'mdi:cloud'], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], + 'noise': ['Noise', 'dB', 'mdi:volume-high'], + 'humidity': ['Humidity', '%', 'mdi:water-percent'] } CONF_SECRET_KEY = 'secret_key' @@ -104,6 +104,10 @@ class NetAtmoSensor(Entity): def name(self): return self._name + @property + def icon(self): + return SENSOR_TYPES[self.type][2] + @property def state(self): """ Returns the state of the device. """ From 0e7088ce3bc2557a8e36faba6c56b8350e402b1b Mon Sep 17 00:00:00 2001 From: Robert Marklund Date: Sat, 23 Jan 2016 01:29:34 +0100 Subject: [PATCH 075/229] smtp: make smtp component thread safe Also fix so it does not require ip, port, username and password when using local smtp server. Add debug config. Signed-off-by: Robert Marklund --- homeassistant/components/notify/smtp.py | 66 ++++++++++++++----------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 5e848634bb9..833fffb9f99 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -21,37 +21,38 @@ def get_service(hass, config): """ Get the mail notification service. """ if not validate_config({DOMAIN: config}, - {DOMAIN: ['server', 'port', 'sender', 'username', - 'password', 'recipient']}, + {DOMAIN: ['recipient']}, _LOGGER): return None - smtp_server = config['server'] - port = int(config['port']) - username = config['username'] - password = config['password'] - starttls = int(config['starttls']) + smtp_server = config.get('server', 'localhost') + port = int(config.get('port', '25')) + username = config.get('username', None) + password = config.get('password', None) + starttls = int(config.get('starttls', 0)) + debug = config.get('debug', 0) server = None try: - server = smtplib.SMTP(smtp_server, port) + server = smtplib.SMTP(smtp_server, port, timeout=5) + server.set_debuglevel(debug) server.ehlo() if starttls == 1: server.starttls() server.ehlo() + if username and password: + try: + server.login(username, password) - try: - server.login(username, password) - - except (smtplib.SMTPException, smtplib.SMTPSenderRefused): - _LOGGER.exception("Please check your settings.") - - return None + except (smtplib.SMTPException, smtplib.SMTPSenderRefused): + _LOGGER.exception("Please check your settings.") + return None except smtplib.socket.gaierror: _LOGGER.exception( - "SMTP server not found. " - "Please check the IP address or hostname of your SMTP server.") + "SMTP server not found (%s:%s). " + "Please check the IP address or hostname of your SMTP server.", + smtp_server, port) return None @@ -68,7 +69,7 @@ def get_service(hass, config): return MailNotificationService( smtp_server, port, config['sender'], starttls, username, password, - config['recipient']) + config['recipient'], debug) # pylint: disable=too-few-public-methods, too-many-instance-attributes @@ -77,7 +78,7 @@ class MailNotificationService(BaseNotificationService): # pylint: disable=too-many-arguments def __init__(self, server, port, sender, starttls, username, - password, recipient): + password, recipient, debug): self._server = server self._port = port self._sender = sender @@ -85,24 +86,26 @@ class MailNotificationService(BaseNotificationService): self.username = username self.password = password self.recipient = recipient + self.debug = debug self.tries = 2 - self.mail = None - - self.connect() def connect(self): """ Connect/Authenticate to SMTP Server """ - self.mail = smtplib.SMTP(self._server, self._port) - self.mail.ehlo_or_helo_if_needed() + mail = smtplib.SMTP(self._server, self._port, timeout=5) + mail.set_debuglevel(self.debug) + mail.ehlo_or_helo_if_needed() if self.starttls == 1: - self.mail.starttls() - self.mail.ehlo() - self.mail.login(self.username, self.password) + mail.starttls() + mail.ehlo() + if self.username and self.password: + mail.login(self.username, self.password) + return mail def send_message(self, message="", **kwargs): """ Send a message to a user. """ + mail = self.connect() subject = kwargs.get(ATTR_TITLE) msg = MIMEText(message) @@ -113,10 +116,13 @@ class MailNotificationService(BaseNotificationService): for _ in range(self.tries): try: - self.mail.sendmail(self._sender, self.recipient, - msg.as_string()) + mail.sendmail(self._sender, self.recipient, + msg.as_string()) break except smtplib.SMTPException: _LOGGER.warning('SMTPException sending mail: ' 'retrying connection') - self.connect() + mail.quit() + mail = self.connect() + + mail.quit() From a63edcf505259ed7902b228f78003871af35e07f Mon Sep 17 00:00:00 2001 From: turbokongen Date: Sat, 23 Jan 2016 11:18:11 +0100 Subject: [PATCH 076/229] Added support for Dimming with lights in Rfxtrx module --- homeassistant/components/light/rfxtrx.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 22bd2575242..d4c6133397c 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -9,7 +9,7 @@ https://home-assistant.io/components/light.rfxtrx/ import logging import homeassistant.components.rfxtrx as rfxtrx -from homeassistant.components.light import Light +from homeassistant.components.light import Light, ATTR_BRIGHTNESS from homeassistant.util import slugify from homeassistant.const import ATTR_ENTITY_ID @@ -112,6 +112,7 @@ class RfxtrxLight(Light): self._event = event self._state = datas[ATTR_STATE] self._should_fire_event = datas[ATTR_FIREEVENT] + self._brightness = 0 @property def should_poll(self): @@ -133,12 +134,27 @@ class RfxtrxLight(Light): """ True if light is on. """ return self._state + @property + def brightness(self): + """ Brightness of this light between 0..255. """ + return self._brightness + def turn_on(self, **kwargs): """ Turn the light on. """ + brightness = kwargs.get(ATTR_BRIGHTNESS) + + if brightness is None: + """ Brightness in rfxtrx is defined as level and values supported are 0-100 """ + self._brightness = 100 + else: + """ Brightness in rfxtrx is defined as level and values supported are 0-100 so we need to scale the set value (0-255)""" + self._brightness = ((brightness + 4) * 100 // 255 -1) if hasattr(self, '_event') and self._event: - self._event.device.send_on(rfxtrx.RFXOBJECT.transport) + self._event.device.send_on(rfxtrx.RFXOBJECT.transport, self._brightness) + """ Reverse earlier calculation to make dimmer slider stay at correct point in HA frontend """ + self._brightness = (self._brightness * 255 // 100) self._state = True self.update_ha_state() @@ -147,6 +163,7 @@ class RfxtrxLight(Light): if hasattr(self, '_event') and self._event: self._event.device.send_off(rfxtrx.RFXOBJECT.transport) - + + self._brightness = 0 self._state = False self.update_ha_state() From d3c6c892a8d09da01523a9696a0e678b60cdc3b9 Mon Sep 17 00:00:00 2001 From: turbokongen Date: Sat, 23 Jan 2016 11:50:09 +0100 Subject: [PATCH 077/229] Small intendation fix --- homeassistant/components/light/rfxtrx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index d4c6133397c..f5231be78ad 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -134,7 +134,7 @@ class RfxtrxLight(Light): """ True if light is on. """ return self._state - @property + @property def brightness(self): """ Brightness of this light between 0..255. """ return self._brightness From 43e2b58f2091c28fa48fd6d1291c2dc1575b026e Mon Sep 17 00:00:00 2001 From: turbokongen Date: Sat, 23 Jan 2016 12:00:03 +0100 Subject: [PATCH 078/229] Fixing of various test errors --- homeassistant/components/light/rfxtrx.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index f5231be78ad..f886457ffd7 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -144,16 +144,13 @@ class RfxtrxLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS) if brightness is None: - """ Brightness in rfxtrx is defined as level and values supported are 0-100 """ self._brightness = 100 else: - """ Brightness in rfxtrx is defined as level and values supported are 0-100 so we need to scale the set value (0-255)""" - self._brightness = ((brightness + 4) * 100 // 255 -1) + self._brightness = ((brightness + 4) * 100 // 255 - 1) if hasattr(self, '_event') and self._event: self._event.device.send_on(rfxtrx.RFXOBJECT.transport, self._brightness) - """ Reverse earlier calculation to make dimmer slider stay at correct point in HA frontend """ self._brightness = (self._brightness * 255 // 100) self._state = True self.update_ha_state() From e30915eb2c5edf1ce9084fcd0fab5416b589b883 Mon Sep 17 00:00:00 2001 From: turbokongen Date: Sat, 23 Jan 2016 12:08:21 +0100 Subject: [PATCH 079/229] flake8 complaint fix --- homeassistant/components/light/rfxtrx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index f886457ffd7..fd05a37a303 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -160,7 +160,7 @@ class RfxtrxLight(Light): if hasattr(self, '_event') and self._event: self._event.device.send_off(rfxtrx.RFXOBJECT.transport) - + self._brightness = 0 self._state = False self.update_ha_state() From 6d527842dd0024d14afe19f5a86b315b9494d527 Mon Sep 17 00:00:00 2001 From: turbokongen Date: Sat, 23 Jan 2016 12:14:00 +0100 Subject: [PATCH 080/229] Another flake8 fix too long line --- homeassistant/components/light/rfxtrx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index fd05a37a303..64389a0fa59 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -149,7 +149,8 @@ class RfxtrxLight(Light): self._brightness = ((brightness + 4) * 100 // 255 - 1) if hasattr(self, '_event') and self._event: - self._event.device.send_on(rfxtrx.RFXOBJECT.transport, self._brightness) + self._event.device.send_on(rfxtrx.RFXOBJECT.transport, + self._brightness) self._brightness = (self._brightness * 255 // 100) self._state = True From 837e7affa7ba922710d0939fc800bc6333ae4887 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 Jan 2016 17:48:14 +0100 Subject: [PATCH 081/229] only query artwork by track_id if id is available (7.7 vs 7.9 version issue?) --- homeassistant/components/media_player/squeezebox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 0494b29fedb..69b15251144 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -202,9 +202,12 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ Image url of current playing media. """ if 'artwork_url' in self._status: media_url = self._status['artwork_url'] - else: + elif "id" in self._status: media_url = ('/music/{track_id}/cover.jpg').format( track_id=self._status["id"]) + else: + media_url = ('/music/current/cover.jpg?player={player}').format( + player=self.id) base_url = 'http://{server}:{port}/'.format( server=self._lms.host, From b3beb9f3c90c3356fb9b05a35c6fb2df88cdd509 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 Jan 2016 18:08:54 +0100 Subject: [PATCH 082/229] style --- 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 69b15251144..e06f1d3800b 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -202,7 +202,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ Image url of current playing media. """ if 'artwork_url' in self._status: media_url = self._status['artwork_url'] - elif "id" in self._status: + elif 'id' in self._status: media_url = ('/music/{track_id}/cover.jpg').format( track_id=self._status["id"]) else: From 492c4b7f00b2cb004e4b4f28c58479c36cc348b2 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 Jan 2016 18:14:03 +0100 Subject: [PATCH 083/229] style --- 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 e06f1d3800b..c0267489f6a 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -204,7 +204,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): media_url = self._status['artwork_url'] elif 'id' in self._status: media_url = ('/music/{track_id}/cover.jpg').format( - track_id=self._status["id"]) + track_id=self._status['id']) else: media_url = ('/music/current/cover.jpg?player={player}').format( player=self.id) From ec2b433733507566eff510570c20cdc1735ef42a Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 Jan 2016 18:55:43 +0100 Subject: [PATCH 084/229] should be _id --- 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 c0267489f6a..05cbb683a52 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -207,7 +207,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): track_id=self._status['id']) else: media_url = ('/music/current/cover.jpg?player={player}').format( - player=self.id) + player=self._id) base_url = 'http://{server}:{port}/'.format( server=self._lms.host, From 90ca6a09987496f83ef1c7513da896913c841614 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Sat, 23 Jan 2016 16:06:50 -0500 Subject: [PATCH 085/229] fix typo in log message The plex component logs an htts url, which is confusing to people, as they think something is broken, when it is not. Closes #959 --- homeassistant/components/media_player/plex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 52dd399cedf..feb1d282551 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -112,7 +112,7 @@ def setup_plexserver(host, token, hass, add_devices_callback): {host: {'token': token}}): _LOGGER.error('failed to save config file') - _LOGGER.info('Connected to: htts://%s', host) + _LOGGER.info('Connected to: http://%s', host) plex_clients = {} plex_sessions = {} From 17f5a466d9df082ab7b7cb0ab2eed114b4fdbe4e Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 23 Jan 2016 22:14:57 +0000 Subject: [PATCH 086/229] Separate LIFX code and HA component --- homeassistant/components/light/lifx.py | 568 ++++--------------------- 1 file changed, 73 insertions(+), 495 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index c516b273d58..4db691781b0 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -1,7 +1,8 @@ + """ homeassistant.components.light.lifx ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -LiFX platform that implements lights +LIFX platform that implements lights Configuration: @@ -12,525 +13,129 @@ light: # only needed if using more than one network interface # (omit if you are unsure) server: 192.168.1.3 - # optional broadcast address, set to reach all LiFX bulbs + # optional broadcast address, set to reach all LIFX bulbs # (omit if you are unsure) broadcast: 192.168.1.255 """ # pylint: disable=missing-docstring +import liffylights import logging -import threading -import time -import queue -import socket -import io -import ipaddress import colorsys -import struct -from struct import pack -from enum import IntEnum from homeassistant.helpers.event import track_time_change from homeassistant.components.light import \ (Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP) _LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['liffylights==0.1'] DEPENDENCIES = [] -REQUIREMENTS = [] CONF_SERVER = "server" # server address configuration item CONF_BROADCAST = "broadcast" # broadcast address configuration item -RETRIES = 10 # number of packet send retries -DELAY = 0.05 # delay between retries -UDP_PORT = 56700 # udp port for listening socket -UDP_IP = "0.0.0.0" # address for listening socket -MAX_ACK_AGE = 1 # maximum ACK age in seconds -BUFFERSIZE = 1024 # socket buffer size SHORT_MAX = 65535 # short int maximum BYTE_MAX = 255 # byte maximum -SEQUENCE_BASE = 1 # packet sequence base -SEQUENCE_COUNT = 255 # packet sequence count +TEMP_MIN = 2500 # lifx minimum temperature +TEMP_MAX = 9000 # lifx maximum temperature +TEMP_MIN_HASS = 154 # home assistant minimum temperature +TEMP_MAX_HASS = 500 # home assistant maximum temperature -HUE_MIN = 0 -HUE_MAX = 360 -SATURATION_MIN = 0 -SATURATION_MAX = 255 -BRIGHTNESS_MIN = 0 -BRIGHTNESS_MAX = 65535 -TEMP_MIN = 2500 -TEMP_MAX = 9000 -TEMP_MIN_HASS = 154 -TEMP_MAX_HASS = 500 +class lifx_api(): + def __init__(self, add_devices_callback, + server_addr=None, broadcast_addr=None): + self._devices = [] -class PayloadType(IntEnum): - """ LIFX message payload types. """ - GETSERVICE = 2 - STATESERVICE = 3 - GETHOSTINFO = 12 - STATEHOSTINFO = 13 - GETHOSTFIRMWARE = 14 - STATEHOSTFIRMWARE = 15 - GETWIFIINFO = 16 - STATEWIFIINFO = 17 - GETWIFIFIRMWARE = 18 - STATEWIFIFIRMWARE = 19 - GETPOWER1 = 20 - SETPOWER1 = 21 - STATEPOWER1 = 22 - GETLABEL = 23 - SETLABEL = 24 - STATELABEL = 25 - GETVERSION = 32 - STATEVERSION = 33 - GETINFO = 34 - STATEINFO = 35 - ACKNOWLEDGEMENT = 45 - GETLOCATION = 48 - STATELOCATION = 50 - GETGROUP = 51 - STATEGROUP = 53 - ECHOREQUEST = 58 - ECHORESPONSE = 59 - GET = 101 - SETCOLOR = 102 - STATE = 107 - GETPOWER2 = 116 - SETPOWER2 = 117 - STATEPOWER2 = 118 + self._add_devices_callback = add_devices_callback + self._liffylights = liffylights(self.on_device, + self.on_power, + self.on_color, + server_addr, + broadcast_addr) -class Power(IntEnum): - """ LIFX power settings. """ - BULB_ON = 65535 - BULB_OFF = 0 + def find_bulb(self, ipaddr): + bulb = None + for device in self._devices: + if device.ipaddr == ipaddr: + bulb = device + break + return bulb + def on_device(self, ipaddr, name, power, hue, sat, bri, kel): + bulb = self.find_bulb(ipaddr) -def gen_header(sequence, payloadtype): - """ Create LIFX packet header. """ - protocol = bytearray.fromhex("00 34") - source = bytearray.fromhex("42 52 4b 52") - target = bytearray.fromhex("00 00 00 00 00 00 00 00") - reserved1 = bytearray.fromhex("00 00 00 00 00 00") - sequence = pack("B", 3) - reserved2 = bytearray.fromhex("00 00 00 00 00 00 00 00") - packet_type = pack(" Date: Sat, 23 Jan 2016 22:23:46 +0000 Subject: [PATCH 087/229] Bump version liffylights --- homeassistant/components/light/lifx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 4db691781b0..bb8d2e348fc 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -29,7 +29,7 @@ from homeassistant.components.light import \ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['liffylights==0.1'] +REQUIREMENTS = ['liffylights==0.3'] DEPENDENCIES = [] CONF_SERVER = "server" # server address configuration item From fbd68b6f89af1d8022b1f00d66a651472251bb3d Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sat, 23 Jan 2016 19:39:59 -0500 Subject: [PATCH 088/229] Created automation decorator prototype Created an initial iteration of an Automation decorator. --- .../components/automation/__init__.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 9c464f6954e..839cc71c37f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,7 +6,11 @@ Allows to setup simple automation rules via the config file. For more details about this component, please refer to the documentation at https://home-assistant.io/components/automation/ """ +from datetime import datetime +import functools +import inspect import logging +import yaml from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM @@ -31,6 +35,8 @@ CONDITION_TYPE_OR = 'or' DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND +CUSTOM_AUTOMATIONS = [] + _LOGGER = logging.getLogger(__name__) @@ -63,6 +69,115 @@ def setup(hass, config): return True +def activate(hass, config, domain): + """ Activate the automations for specified domain """ + for auto_rule in CUSTOM_AUTOMATIONS: + if auto_rule.domain == domain: + try: + success = auto_rule.activate(hass, config) + except Exception: + _LOGGER.exception('Error activating automation %s', + auto_rule.alias) + success = True + + if not success: + _LOGGER.error('Error activating automation %s', + auto_rule.alias) + + +class Automation(object): + """ Decorator for automation functions """ + + hass = None + + def __init__(self, action): + # store action and config + self.action = action + self.config = yaml.load(inspect.getdoc(action)) + self._activated = False + self._last_run = None + self._running = 0 + + # register the automation + module = inspect.getmodule(action) + self._domain = module.DOMAIN + CUSTOM_AUTOMATIONS.append(self) + + functools.update_wrapper(self, action) + + def __call__(self): + """ Call the action """ + if not self.activated: + return + + self._running += 1 + + _LOGGER.info('Executing %s', self.alias) + logbook.log_entry(self.hass, self.alias, 'has been triggered', DOMAIN) + + try: + self.action(self) + except Exception: + _LOGGER.exception('Error running Python automation: %s', + self.alias) + else: + self._last_run = datetime.now() + + self._running -= 1 + + @property + def alias(self): + """ The alias for the function """ + if CONF_ALIAS in self.config: + return self.config[CONF_ALIAS] + return None + + @property + def domain(self): + """ The domain to which this automation belongs """ + return self._domain + + @property + def is_running(self): + """ Boolean if the automation is running """ + return self._running > 0 + + @property + def num_running(self): + """ Integer of how many instances of the automation are running """ + return self._running + + @property + def activated(self): + """ Boolean indicating if the automation has been activated """ + return self._activated + + @property + def last_run(self): + """ Datetime object of the last automation completion """ + return self._last_run + + def activate(self, hass, config): + """ Activates the automation with HASS """ + self.hass = hass + + if self.activated: + return True + + if CONF_CONDITION in self.config or CONF_CONDITION_TYPE in self.config: + action = _process_if(hass, config, self.config, self.action) + + if action is None: + return False + self.action = action + + _process_trigger(hass, config, self.config.get(CONF_TRIGGER, []), + self.alias, self) + + self._activated = True + return True + + def _setup_automation(hass, config_block, name, config): """ Setup one instance of automation """ From 711f2da496f33ac93b4c54c42e41c47304f18feb Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 00:43:50 +0000 Subject: [PATCH 089/229] Update liffylights version --- homeassistant/components/light/lifx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index bb8d2e348fc..5c1ca44968d 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -20,7 +20,7 @@ light: """ # pylint: disable=missing-docstring -import liffylights +from liffylights import liffylights import logging import colorsys from homeassistant.helpers.event import track_time_change @@ -29,7 +29,7 @@ from homeassistant.components.light import \ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['liffylights==0.3'] +REQUIREMENTS = ['liffylights==0.4'] DEPENDENCIES = [] CONF_SERVER = "server" # server address configuration item From 99286391e18f0fead7a01f195842ffd61f4c96ca Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 01:00:02 +0000 Subject: [PATCH 090/229] Fixes for lint --- homeassistant/components/light/lifx.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 5c1ca44968d..09c0c829dad 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -42,7 +42,7 @@ TEMP_MIN_HASS = 154 # home assistant minimum temperature TEMP_MAX_HASS = 500 # home assistant maximum temperature -class lifx_api(): +class LIFX(): def __init__(self, add_devices_callback, server_addr=None, broadcast_addr=None): self._devices = [] @@ -63,6 +63,7 @@ class lifx_api(): break return bulb + # pylint: disable=too-many-arguments def on_device(self, ipaddr, name, power, hue, sat, bri, kel): bulb = self.find_bulb(ipaddr) @@ -72,6 +73,7 @@ class lifx_api(): self._devices.append(bulb) self._add_devices_callback([bulb]) + # pylint: disable=too-many-arguments def on_color(self, ipaddr, hue, sat, bri, kel): bulb = self.find_bulb(ipaddr) @@ -86,15 +88,13 @@ class lifx_api(): bulb.set_power(power) bulb.update_ha_state() + # pylint: disable=unused-argument def poll(self, now): self.probe() def probe(self, address=None): self._liffylights.probe(address) - def state(self, power): - return "on" if power else "off" - # pylint: disable=unused-argument def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -102,7 +102,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): server_addr = config.get(CONF_SERVER, None) broadcast_addr = config.get(CONF_BROADCAST, None) - lifx_library = lifx_api(add_devices_callback, server_addr, broadcast_addr) + lifx_library = LIFX(add_devices_callback, server_addr, broadcast_addr) # register our poll service track_time_change(hass, lifx_library.poll, second=10) @@ -125,9 +125,9 @@ def convert_rgb_to_hsv(rgb): class LIFXLight(Light): """ Provides LIFX light. """ # pylint: disable=too-many-arguments - def __init__(self, liffylights, ipaddr, name, power, hue, + def __init__(self, liffy, ipaddr, name, power, hue, saturation, brightness, kelvin): - self._liffylights = liffylights + self._liffylights = liffy self._ip = ipaddr self.set_name(name) self.set_power(power) From 6d2bca0fd108535f03fb70dfb8486b10535f627d Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 01:13:51 +0000 Subject: [PATCH 091/229] Import 3rd party library inside method --- homeassistant/components/light/lifx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 09c0c829dad..afe2196b0e0 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -20,7 +20,6 @@ light: """ # pylint: disable=missing-docstring -from liffylights import liffylights import logging import colorsys from homeassistant.helpers.event import track_time_change @@ -45,6 +44,8 @@ TEMP_MAX_HASS = 500 # home assistant maximum temperature class LIFX(): def __init__(self, add_devices_callback, server_addr=None, broadcast_addr=None): + from liffylights import liffylights + self._devices = [] self._add_devices_callback = add_devices_callback From 6cb6cbfefd83927ffee1be8f3dc3a09a84637aca Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 01:18:18 +0000 Subject: [PATCH 092/229] Update requirements --- requirements_all.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index b6d0af08ee0..d48e0190425 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -51,6 +51,9 @@ blinkstick==1.1.7 # homeassistant.components.light.hue phue==0.8 +# homeassistant.components.light.lifx +liffylights==0.4 + # homeassistant.components.light.limitlessled limitlessled==1.0.0 @@ -95,9 +98,6 @@ paho-mqtt==1.1 # homeassistant.components.mysensors https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4 -# homeassistant.components.nest -python-nest==2.6.0 - # homeassistant.components.notify.free_mobile freesms==0.1.0 @@ -195,6 +195,9 @@ heatmiserV3==0.9.1 # homeassistant.components.thermostat.honeywell evohomeclient==0.2.4 +# homeassistant.components.thermostat.nest +python-nest==2.6.0 + # homeassistant.components.thermostat.proliphix proliphix==0.1.0 From 90c392e270ee8356b3fb5badf01bfea4e430a8a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Jan 2016 17:29:40 -0800 Subject: [PATCH 093/229] Upgrade PyChromecast version --- homeassistant/components/media_player/cast.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index c0717edc860..c08a61826ef 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -20,7 +20,7 @@ from homeassistant.components.media_player import ( SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) -REQUIREMENTS = ['pychromecast==0.6.14'] +REQUIREMENTS = ['pychromecast==0.7.1'] CONF_IGNORE_CEC = 'ignore_cec' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ diff --git a/requirements_all.txt b/requirements_all.txt index 2241aaecdda..9c9c60564e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -75,7 +75,7 @@ pyvera==0.2.7 python-wink==0.4.2 # homeassistant.components.media_player.cast -pychromecast==0.6.14 +pychromecast==0.7.1 # homeassistant.components.media_player.kodi jsonrpc-requests==0.1 From 706bbeae16345696572d874e45924e38ad42f720 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 02:17:52 +0000 Subject: [PATCH 094/229] Add to .coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 57c3d89d647..64b9e985454 100644 --- a/.coveragerc +++ b/.coveragerc @@ -74,6 +74,7 @@ omit = homeassistant/components/light/blinksticklight.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py + homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/media_player/cast.py homeassistant/components/media_player/denon.py From de08f0afaa13e32dcdb86ea4aab9e57069e10b26 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Jan 2016 22:37:15 -0800 Subject: [PATCH 095/229] Load YAML config into an ordered dict --- homeassistant/bootstrap.py | 2 +- homeassistant/config.py | 34 ++-------------------- homeassistant/core.py | 7 ++--- homeassistant/helpers/entity.py | 9 ++++++ homeassistant/util/yaml.py | 50 +++++++++++++++++++++++++++++++++ tests/test_config.py | 13 +++++---- 6 files changed, 71 insertions(+), 44 deletions(-) create mode 100644 homeassistant/util/yaml.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b704fc082ac..64134f7bc9b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -223,7 +223,7 @@ def from_config_file(config_path, hass=None, verbose=False, daemon=False, enable_logging(hass, verbose, daemon, log_rotate_days) - config_dict = config_util.load_config_file(config_path) + config_dict = config_util.load_yaml_config_file(config_path) return from_config_dict(config_dict, hass, enable_log=False, skip_pip=skip_pip) diff --git a/homeassistant/config.py b/homeassistant/config.py index 3d17fce5e17..b6d60f873cb 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_TIME_ZONE) import homeassistant.util.location as loc_util +from homeassistant.util.yaml import load_yaml _LOGGER = logging.getLogger(__name__) @@ -113,40 +114,9 @@ def find_config_file(config_dir): return config_path if os.path.isfile(config_path) else None -def load_config_file(config_path): - """ Loads given config file. """ - return load_yaml_config_file(config_path) - - def load_yaml_config_file(config_path): """ Parse a YAML configuration file. """ - import yaml - - def parse(fname): - """ Parse a YAML file. """ - try: - with open(fname, encoding='utf-8') as conf_file: - # If configuration file is empty YAML returns None - # We convert that to an empty dict - return yaml.load(conf_file) or {} - except yaml.YAMLError: - error = 'Error reading YAML configuration file {}'.format(fname) - _LOGGER.exception(error) - raise HomeAssistantError(error) - - def yaml_include(loader, node): - """ - Loads another YAML file and embeds it using the !include tag. - - Example: - device_tracker: !include device_tracker.yaml - """ - fname = os.path.join(os.path.dirname(loader.name), node.value) - return parse(fname) - - yaml.add_constructor('!include', yaml_include) - - conf_dict = parse(config_path) + conf_dict = load_yaml(config_path) if not isinstance(conf_dict, dict): _LOGGER.error( diff --git a/homeassistant/core.py b/homeassistant/core.py index 55ceddb37c7..79fc0cfeb36 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -11,7 +11,6 @@ import logging import signal import threading import enum -import re import functools as ft from collections import namedtuple @@ -26,6 +25,7 @@ from homeassistant.exceptions import ( import homeassistant.util as util import homeassistant.util.dt as dt_util import homeassistant.util.location as location +from homeassistant.helpers.entity import valid_entity_id import homeassistant.helpers.temperature as temp_helper from homeassistant.config import get_default_config_dir @@ -42,9 +42,6 @@ SERVICE_CALL_LIMIT = 10 # seconds # will be added for each component that polls devices. MIN_WORKER_THREAD = 2 -# Pattern for validating entity IDs (format: .) -ENTITY_ID_PATTERN = re.compile(r"^(?P\w+)\.(?P\w+)$") - _LOGGER = logging.getLogger(__name__) # Temporary to support deprecated methods @@ -339,7 +336,7 @@ class State(object): def __init__(self, entity_id, state, attributes=None, last_changed=None, last_updated=None): """Initialize a new state.""" - if not ENTITY_ID_PATTERN.match(entity_id): + if not valid_entity_id(entity_id): raise InvalidEntityFormatError(( "Invalid entity id encountered: {}. " "Format should be .").format(entity_id)) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 86a723f8bd1..e700a316667 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -6,6 +6,7 @@ Provides ABC for entities in HA. """ from collections import defaultdict +import re from homeassistant.exceptions import NoEntitySpecifiedError @@ -17,6 +18,14 @@ from homeassistant.const import ( # Dict mapping entity_id to a boolean that overwrites the hidden property _OVERWRITE = defaultdict(dict) +# Pattern for validating entity IDs (format: .) +ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") + + +def valid_entity_id(entity_id): + """Test if an entity ID is a valid format.""" + return ENTITY_ID_PATTERN.match(entity_id) is not None + class Entity(object): """ ABC for Home Assistant entities. """ diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py new file mode 100644 index 00000000000..26d7c6c316e --- /dev/null +++ b/homeassistant/util/yaml.py @@ -0,0 +1,50 @@ +""" +YAML utility functions. +""" +from collections import OrderedDict +import logging +import os + +import yaml + +from homeassistant.exceptions import HomeAssistantError + + +_LOGGER = logging.getLogger(__name__) + + +def load_yaml(fname): + """Load a YAML file.""" + try: + with open(fname, encoding='utf-8') as conf_file: + # If configuration file is empty YAML returns None + # We convert that to an empty dict + return yaml.load(conf_file) or {} + except yaml.YAMLError: + error = 'Error reading YAML configuration file {}'.format(fname) + _LOGGER.exception(error) + raise HomeAssistantError(error) + + +def _include_yaml(loader, node): + """ + Loads another YAML file and embeds it using the !include tag. + + Example: + device_tracker: !include device_tracker.yaml + """ + fname = os.path.join(os.path.dirname(loader.name), node.value) + return load_yaml(fname) + + +def _ordered_dict(loader, node): + """ + Loads YAML mappings into an ordered dict to preserve key order. + """ + loader.flatten_mapping(node) + return OrderedDict(loader.construct_pairs(node)) + + +yaml.add_constructor('!include', _include_yaml) +yaml.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, + _ordered_dict) diff --git a/tests/test_config.py b/tests/test_config.py index 65c93f9f333..781fc51731f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -94,13 +94,14 @@ class TestConfig(unittest.TestCase): with self.assertRaises(HomeAssistantError): config_util.load_yaml_config_file(YAML_PATH) - def test_load_config_loads_yaml_config(self): - """ Test correct YAML config loading. """ + def test_load_yaml_config_preserves_key_order(self): with open(YAML_PATH, 'w') as f: - f.write('hello: world') + f.write('hello: 0\n') + f.write('world: 1\n') - self.assertEqual({'hello': 'world'}, - config_util.load_config_file(YAML_PATH)) + self.assertEqual( + [('hello', 0), ('world', 1)], + list(config_util.load_yaml_config_file(YAML_PATH).items())) @mock.patch('homeassistant.util.location.detect_location_info', mock_detect_location_info) @@ -109,7 +110,7 @@ class TestConfig(unittest.TestCase): """ Test that detect location sets the correct config keys. """ config_util.ensure_config_exists(CONFIG_DIR) - config = config_util.load_config_file(YAML_PATH) + config = config_util.load_yaml_config_file(YAML_PATH) self.assertIn(DOMAIN, config) From bc19ef66bf1ff869be0c3332c5248894069cd4f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Jan 2016 22:49:49 -0800 Subject: [PATCH 096/229] Move split_entity_id to helpers --- homeassistant/components/__init__.py | 6 +++--- homeassistant/components/group.py | 7 +++---- homeassistant/components/logbook.py | 5 ++--- homeassistant/components/script.py | 4 ++-- homeassistant/core.py | 6 +++--- homeassistant/helpers/entity.py | 5 +++++ homeassistant/helpers/service.py | 2 +- homeassistant/util/__init__.py | 5 ----- tests/helpers/test_entity.py | 5 +++++ tests/util/test_init.py | 5 ----- 10 files changed, 24 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index cfc8acb133e..3c38e908b16 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -16,8 +16,8 @@ import itertools as it import logging import homeassistant.core as ha -import homeassistant.util as util from homeassistant.helpers import extract_entity_ids +from homeassistant.helpers.entity import split_entity_id from homeassistant.loader import get_component from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) @@ -36,7 +36,7 @@ def is_on(hass, entity_id=None): entity_ids = hass.states.entity_ids() for entity_id in entity_ids: - domain = util.split_entity_id(entity_id)[0] + domain = split_entity_id(entity_id)[0] module = get_component(domain) @@ -92,7 +92,7 @@ def setup(hass, config): # Group entity_ids by domain. groupby requires sorted data. by_domain = it.groupby(sorted(entity_ids), - lambda item: util.split_entity_id(item)[0]) + lambda item: split_entity_id(item)[0]) for domain, ent_ids in by_domain: # We want to block for all calls and only return when all calls diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 52ffe824e42..78729d1d3ba 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -9,8 +9,7 @@ https://home-assistant.io/components/group/ import homeassistant.core 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.helpers.entity import Entity, split_entity_id from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED, @@ -62,7 +61,7 @@ def expand_entity_ids(hass, entity_ids): try: # If entity_id points at a group, expand it - domain, _ = util.split_entity_id(entity_id) + domain, _ = split_entity_id(entity_id) if domain == DOMAIN: found_ids.extend( @@ -75,7 +74,7 @@ def expand_entity_ids(hass, entity_ids): found_ids.append(entity_id) except AttributeError: - # Raised by util.split_entity_id if entity_id is not a string + # Raised by split_entity_id if entity_id is not a string pass return found_ids diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 16159404dec..98d02af5eb0 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -14,10 +14,9 @@ from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_NOT_HOME, STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, HTTP_BAD_REQUEST) -from homeassistant import util import homeassistant.util.dt as dt_util from homeassistant.components import recorder, sun - +from homeassistant.helpers.entity import split_entity_id DOMAIN = "logbook" DEPENDENCIES = ['recorder', 'http'] @@ -209,7 +208,7 @@ def humanify(events): entity_id = event.data.get(ATTR_ENTITY_ID) if domain is None and entity_id is not None: try: - domain = util.split_entity_id(str(entity_id))[0] + domain = split_entity_id(str(entity_id))[0] except IndexError: pass diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 52dfdc49281..167ddb55957 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -13,10 +13,10 @@ from itertools import islice import threading from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity import ToggleEntity, split_entity_id from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.service import call_from_config -from homeassistant.util import slugify, split_entity_id +from homeassistant.util import slugify import homeassistant.util.dt as date_util from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_TIME_CHANGED, STATE_ON, SERVICE_TURN_ON, diff --git a/homeassistant/core.py b/homeassistant/core.py index 79fc0cfeb36..853d09020ce 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -25,7 +25,7 @@ from homeassistant.exceptions import ( import homeassistant.util as util import homeassistant.util.dt as dt_util import homeassistant.util.location as location -from homeassistant.helpers.entity import valid_entity_id +from homeassistant.helpers.entity import valid_entity_id, split_entity_id import homeassistant.helpers.temperature as temp_helper from homeassistant.config import get_default_config_dir @@ -357,12 +357,12 @@ class State(object): @property def domain(self): """Domain of this state.""" - return util.split_entity_id(self.entity_id)[0] + return split_entity_id(self.entity_id)[0] @property def object_id(self): """Object id of this state.""" - return util.split_entity_id(self.entity_id)[1] + return split_entity_id(self.entity_id)[1] @property def name(self): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index e700a316667..a4f964f40b9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -22,6 +22,11 @@ _OVERWRITE = defaultdict(dict) ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") +def split_entity_id(entity_id): + """ Splits a state entity_id into domain, object_id. """ + return entity_id.split(".", 1) + + def valid_entity_id(entity_id): """Test if an entity ID is a valid format.""" return ENTITY_ID_PATTERN.match(entity_id) is not None diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 15cfe9b97c6..941227d79cd 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,8 +1,8 @@ """Service calling related helpers.""" import logging -from homeassistant.util import split_entity_id from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers.entity import split_entity_id CONF_SERVICE = 'service' CONF_SERVICE_ENTITY_ID = 'entity_id' diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index ada6d150188..b2b4b037384 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -41,11 +41,6 @@ def slugify(text): return RE_SLUGIFY.sub("", text) -def split_entity_id(entity_id): - """ Splits a state entity_id into domain, object_id. """ - return entity_id.split(".", 1) - - def repr_helper(inp): """ Helps creating a more readable string representation of objects. """ if isinstance(inp, dict): diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index cdca36a9701..8845bb622dc 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -42,3 +42,8 @@ class TestHelpersEntity(unittest.TestCase): state = self.hass.states.get(self.entity.entity_id) self.assertTrue(state.attributes.get(ATTR_HIDDEN)) + + def test_split_entity_id(self): + """ Test split_entity_id. """ + self.assertEqual(['domain', 'object_id'], + entity.split_entity_id('domain.object_id')) diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 2e520ac4980..f0a3eb8a109 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -36,11 +36,6 @@ class TestUtil(unittest.TestCase): self.assertEqual("test_more", util.slugify("Test More")) self.assertEqual("test_more", util.slugify("Test_(More)")) - def test_split_entity_id(self): - """ Test split_entity_id. """ - self.assertEqual(['domain', 'object_id'], - util.split_entity_id('domain.object_id')) - def test_repr_helper(self): """ Test repr_helper. """ self.assertEqual("A", util.repr_helper("A")) From de79a46d43328e99e5ca2b70c945b06bfa918bb4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Jan 2016 22:57:14 -0800 Subject: [PATCH 097/229] Move extract_entity_id to service helpers --- homeassistant/components/__init__.py | 2 +- homeassistant/helpers/__init__.py | 20 ---------------- homeassistant/helpers/entity_component.py | 4 ++-- homeassistant/helpers/service.py | 20 ++++++++++++++++ tests/helpers/test_init.py | 29 +++-------------------- tests/helpers/test_service.py | 24 ++++++++++++++++++- 6 files changed, 49 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 3c38e908b16..0d82e1d2882 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -16,8 +16,8 @@ import itertools as it import logging import homeassistant.core as ha -from homeassistant.helpers import extract_entity_ids from homeassistant.helpers.entity import split_entity_id +from homeassistant.helpers.service import extract_entity_ids from homeassistant.loader import get_component from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 95dfe7dd65e..ab5f3df6563 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -3,7 +3,6 @@ Helper methods for components within Home Assistant. """ import re -from homeassistant.loader import get_component from homeassistant.const import ( ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME) from homeassistant.util import ensure_unique_string, slugify @@ -22,25 +21,6 @@ def generate_entity_id(entity_id_format, name, current_ids=None, hass=None): entity_id_format.format(slugify(name.lower())), current_ids) -def extract_entity_ids(hass, service): - """ - Helper method to extract a list of entity ids from a service call. - Will convert group entity ids to the entity ids it represents. - """ - if not (service.data and ATTR_ENTITY_ID in service.data): - return [] - - group = get_component('group') - - # Entity ID attr can be a list or a string - service_ent_id = service.data[ATTR_ENTITY_ID] - - if isinstance(service_ent_id, str): - return group.expand_entity_ids(hass, [service_ent_id]) - - return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)] - - def validate_config(config, items, logger): """ Validates if all items are available in the configuration. diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 4cf44737f90..a6b2ab4c886 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,9 +7,9 @@ Provides helpers for components that manage entities. from threading import Lock from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.helpers import ( - generate_entity_id, config_per_platform, extract_entity_ids) +from homeassistant.helpers import generate_entity_id, config_per_platform from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.service import extract_entity_ids from homeassistant.components import group, discovery from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 941227d79cd..a1ba45a491f 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -3,6 +3,7 @@ import logging from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers.entity import split_entity_id +from homeassistant.loader import get_component CONF_SERVICE = 'service' CONF_SERVICE_ENTITY_ID = 'entity_id' @@ -41,3 +42,22 @@ def call_from_config(hass, config, blocking=False): service_data[ATTR_ENTITY_ID] = entity_id hass.services.call(domain, service, service_data, blocking) + + +def extract_entity_ids(hass, service): + """ + Helper method to extract a list of entity ids from a service call. + Will convert group entity ids to the entity ids it represents. + """ + if not (service.data and ATTR_ENTITY_ID in service.data): + return [] + + group = get_component('group') + + # Entity ID attr can be a list or a string + service_ent_id = service.data[ATTR_ENTITY_ID] + + if isinstance(service_ent_id, str): + return group.expand_entity_ids(hass, [service_ent_id]) + + return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)] diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py index 5899ef3a943..9d8afeb2b6d 100644 --- a/tests/helpers/test_init.py +++ b/tests/helpers/test_init.py @@ -7,45 +7,22 @@ Tests component helpers. # pylint: disable=protected-access,too-many-public-methods import unittest -import homeassistant.core as ha -from homeassistant import loader, helpers -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID +from homeassistant import helpers from tests.common import get_test_home_assistant -class TestComponentsCore(unittest.TestCase): - """ Tests homeassistant.components module. """ +class TestHelpers(unittest.TestCase): + """ Tests homeassistant.helpers module. """ def setUp(self): # pylint: disable=invalid-name """ Init needed objects. """ self.hass = get_test_home_assistant() - self.hass.states.set('light.Bowl', STATE_ON) - self.hass.states.set('light.Ceiling', STATE_OFF) - self.hass.states.set('light.Kitchen', STATE_OFF) - - loader.get_component('group').setup_group( - self.hass, 'test', ['light.Ceiling', 'light.Kitchen']) - def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ self.hass.stop() - def test_extract_entity_ids(self): - """ Test extract_entity_ids method. """ - call = ha.ServiceCall('light', 'turn_on', - {ATTR_ENTITY_ID: 'light.Bowl'}) - - self.assertEqual(['light.bowl'], - helpers.extract_entity_ids(self.hass, call)) - - call = ha.ServiceCall('light', 'turn_on', - {ATTR_ENTITY_ID: 'group.test'}) - - self.assertEqual(['light.ceiling', 'light.kitchen'], - helpers.extract_entity_ids(self.hass, call)) - def test_extract_domain_configs(self): config = { 'zone': None, diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index aa2cab07d0d..659ab6e3ace 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -7,7 +7,8 @@ Test service helpers. import unittest from unittest.mock import patch -from homeassistant.const import SERVICE_TURN_ON +from homeassistant import core as ha, loader +from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import service from tests.common import get_test_home_assistant, mock_service @@ -66,3 +67,24 @@ class TestServiceHelpers(unittest.TestCase): 'service': 'invalid' }) self.assertEqual(3, mock_log.call_count) + + def test_extract_entity_ids(self): + """ Test extract_entity_ids method. """ + self.hass.states.set('light.Bowl', STATE_ON) + self.hass.states.set('light.Ceiling', STATE_OFF) + self.hass.states.set('light.Kitchen', STATE_OFF) + + loader.get_component('group').setup_group( + self.hass, 'test', ['light.Ceiling', 'light.Kitchen']) + + call = ha.ServiceCall('light', 'turn_on', + {ATTR_ENTITY_ID: 'light.Bowl'}) + + self.assertEqual(['light.bowl'], + service.extract_entity_ids(self.hass, call)) + + call = ha.ServiceCall('light', 'turn_on', + {ATTR_ENTITY_ID: 'group.test'}) + + self.assertEqual(['light.ceiling', 'light.kitchen'], + service.extract_entity_ids(self.hass, call)) From 53484e46a31a8b8908aef4094553c211f36490f9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Jan 2016 23:00:46 -0800 Subject: [PATCH 098/229] Move generate_entity_id to entity helpers --- homeassistant/components/configurator.py | 2 +- homeassistant/components/group.py | 4 ++-- homeassistant/components/zone.py | 4 ++-- homeassistant/helpers/__init__.py | 17 +---------------- homeassistant/helpers/entity.py | 14 ++++++++++++++ homeassistant/helpers/entity_component.py | 3 ++- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/configurator.py b/homeassistant/components/configurator.py index 515daffc71c..6fb584635f9 100644 --- a/homeassistant/components/configurator.py +++ b/homeassistant/components/configurator.py @@ -11,7 +11,7 @@ the user has submitted configuration information. """ import logging -from homeassistant.helpers import generate_entity_id +from homeassistant.helpers.entity import generate_entity_id from homeassistant.const import EVENT_TIME_CHANGED DOMAIN = "configurator" diff --git a/homeassistant/components/group.py b/homeassistant/components/group.py index 78729d1d3ba..5d19f313323 100644 --- a/homeassistant/components/group.py +++ b/homeassistant/components/group.py @@ -7,9 +7,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/group/ """ import homeassistant.core as ha -from homeassistant.helpers import generate_entity_id from homeassistant.helpers.event import track_state_change -from homeassistant.helpers.entity import Entity, split_entity_id +from homeassistant.helpers.entity import ( + Entity, split_entity_id, generate_entity_id) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, STATE_OPEN, STATE_CLOSED, diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone.py index 86e2cf5062e..3dcc3dc0f07 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone.py @@ -10,8 +10,8 @@ import logging from homeassistant.const import ( ATTR_HIDDEN, ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME) -from homeassistant.helpers import extract_domain_configs, generate_entity_id -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import extract_domain_configs +from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.util.location import distance DOMAIN = "zone" diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index ab5f3df6563..ec7996ff6df 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -3,22 +3,7 @@ Helper methods for components within Home Assistant. """ import re -from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_PLATFORM, DEVICE_DEFAULT_NAME) -from homeassistant.util import ensure_unique_string, slugify - - -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. """ - name = name.lower() or DEVICE_DEFAULT_NAME.lower() - if current_ids is None: - if hass is None: - raise RuntimeError("Missing required parameter currentids or hass") - - current_ids = hass.states.entity_ids() - - return ensure_unique_string( - entity_id_format.format(slugify(name.lower())), current_ids) +from homeassistant.const import CONF_PLATFORM def validate_config(config, items, logger): diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a4f964f40b9..ab5707a0121 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -9,6 +9,7 @@ from collections import defaultdict import re from homeassistant.exceptions import NoEntitySpecifiedError +from homeassistant.util import ensure_unique_string, slugify from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON, @@ -22,6 +23,19 @@ _OVERWRITE = defaultdict(dict) ENTITY_ID_PATTERN = re.compile(r"^(\w+)\.(\w+)$") +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. """ + name = name.lower() or DEVICE_DEFAULT_NAME.lower() + if current_ids is None: + if hass is None: + raise RuntimeError("Missing required parameter currentids or hass") + + current_ids = hass.states.entity_ids() + + return ensure_unique_string( + entity_id_format.format(slugify(name.lower())), current_ids) + + def split_entity_id(entity_id): """ Splits a state entity_id into domain, object_id. """ return entity_id.split(".", 1) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a6b2ab4c886..0450a788809 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -7,7 +7,8 @@ Provides helpers for components that manage entities. from threading import Lock from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.helpers import generate_entity_id, config_per_platform +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_utc_time_change from homeassistant.helpers.service import extract_entity_ids from homeassistant.components import group, discovery From 1842e5909e44a225db55ac745d2fb76b798846a6 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 24 Jan 2016 09:57:40 +0100 Subject: [PATCH 099/229] Updated the entity names for all z-wave devices. Z-Wave have a unique node id, this is now added to the entity name. This is a change will break old configs. The Z-Wave entity_id will have to be updated in configurations. The reason for this change is that the old behavior created duplicate entity_ids if you have several similar devices. The order devices is included is not consistent so when the service was restarted the entity ids could change and the configuration for entities will be invalid. There was also inconsistencies between the naming for sensors and switches. (cherry picked from commit 33a78df) --- homeassistant/components/light/zwave.py | 5 +++-- homeassistant/components/sensor/zwave.py | 2 +- homeassistant/components/switch/zwave.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 02664ed896c..0dac1f37b37 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -94,9 +94,10 @@ class ZwaveDimmer(Light): @property def name(self): """ Returns the name of the device if any. """ - name = self._node.name or "{}".format(self._node.product_name) + name = self._node.name or "{} {}".format( + self._node.manufacturer_name, self._node.product_name) - return "{}".format(name or self._value.label) + return "{} {} {}".format(name, self._node.node_id, self._value.label) @property def brightness(self): diff --git a/homeassistant/components/sensor/zwave.py b/homeassistant/components/sensor/zwave.py index 869f4dbe810..f1d3e00e69f 100644 --- a/homeassistant/components/sensor/zwave.py +++ b/homeassistant/components/sensor/zwave.py @@ -107,7 +107,7 @@ class ZWaveSensor(Entity): name = self._node.name or "{} {}".format( self._node.manufacturer_name, self._node.product_name) - return "{} {}".format(name, self._value.label) + return "{} {} {}".format(name, self._node.node_id, self._value.label) @property def state(self): diff --git a/homeassistant/components/switch/zwave.py b/homeassistant/components/switch/zwave.py index f4777340445..ab5c0bcd9f0 100644 --- a/homeassistant/components/switch/zwave.py +++ b/homeassistant/components/switch/zwave.py @@ -58,9 +58,10 @@ class ZwaveSwitch(SwitchDevice): @property def name(self): """ Returns the name of the device if any. """ - name = self._node.name or "{}".format(self._node.product_name) + name = self._node.name or "{} {}".format( + self._node.manufacturer_name, self._node.product_name) - return "{}".format(name or self._value.label) + return "{} {} {}".format(name, self._node.node_id, self._value.label) @property def is_on(self): From 9f6a1c75fa1d4e05671e778a95704b73a92480a9 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 10:01:23 +0000 Subject: [PATCH 100/229] Fix wrongly generated requirements --- requirements_all.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index d48e0190425..5473f9536cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -101,6 +101,9 @@ https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5 # homeassistant.components.notify.free_mobile freesms==0.1.0 +# homeassistant.components.nest +python-nest==2.6.0 + # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 @@ -195,9 +198,6 @@ heatmiserV3==0.9.1 # homeassistant.components.thermostat.honeywell evohomeclient==0.2.4 -# homeassistant.components.thermostat.nest -python-nest==2.6.0 - # homeassistant.components.thermostat.proliphix proliphix==0.1.0 From 2411d1f2c849cae1e56a7174b5f8f07c7290263c Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 10:07:56 +0000 Subject: [PATCH 101/229] Fix wrongly generated requirements --- requirements_all.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index 5473f9536cb..c7e9369cbf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -98,12 +98,12 @@ paho-mqtt==1.1 # homeassistant.components.mysensors https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4 -# homeassistant.components.notify.free_mobile -freesms==0.1.0 - # homeassistant.components.nest python-nest==2.6.0 +# homeassistant.components.notify.free_mobile +freesms==0.1.0 + # homeassistant.components.notify.pushbullet pushbullet.py==0.9.0 From 6df67d2852354fa574360ad94559f56887f34f1e Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sun, 24 Jan 2016 16:37:38 +0100 Subject: [PATCH 102/229] Send correct command to pyrfxtrx Although it seems to work with send_on, it throws an logged error. Using correct command against pyrfxtrx removes this error. --- homeassistant/components/light/rfxtrx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 64389a0fa59..0ee6d35d5f7 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -149,7 +149,7 @@ class RfxtrxLight(Light): self._brightness = ((brightness + 4) * 100 // 255 - 1) if hasattr(self, '_event') and self._event: - self._event.device.send_on(rfxtrx.RFXOBJECT.transport, + self._event.device.send_dim(rfxtrx.RFXOBJECT.transport, self._brightness) self._brightness = (self._brightness * 255 // 100) From f6f3f542289b2276a3066204647e836937e93b90 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Sun, 24 Jan 2016 16:43:24 +0100 Subject: [PATCH 103/229] flake8 complaint fix --- homeassistant/components/light/rfxtrx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/rfxtrx.py b/homeassistant/components/light/rfxtrx.py index 0ee6d35d5f7..f96a9f7b1fa 100644 --- a/homeassistant/components/light/rfxtrx.py +++ b/homeassistant/components/light/rfxtrx.py @@ -150,7 +150,7 @@ class RfxtrxLight(Light): if hasattr(self, '_event') and self._event: self._event.device.send_dim(rfxtrx.RFXOBJECT.transport, - self._brightness) + self._brightness) self._brightness = (self._brightness * 255 // 100) self._state = True From dc5d652d31118eb02d57955bcc80ee6aa59c1049 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 24 Jan 2016 09:43:06 -0800 Subject: [PATCH 104/229] Update version pynetgear --- homeassistant/components/device_tracker/netgear.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index ab1eccba769..233622e076e 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear==0.3.1'] +REQUIREMENTS = ['pynetgear==0.3.2'] def get_scanner(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index 9c9c60564e5..f8fe64d1c72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -19,7 +19,7 @@ fuzzywuzzy==0.8.0 pyicloud==0.7.2 # homeassistant.components.device_tracker.netgear -pynetgear==0.3.1 +pynetgear==0.3.2 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.4.3 From afa4fc4ef59cccc75b40173b811c0f0ebe51111e Mon Sep 17 00:00:00 2001 From: Michael Auchter Date: Sun, 24 Jan 2016 12:02:23 -0600 Subject: [PATCH 105/229] thermostat: split up services --- .../components/thermostat/__init__.py | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 7610070b1f0..dce742b71a8 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -75,51 +75,54 @@ def setup(hass, config): SCAN_INTERVAL, DISCOVERY_PLATFORMS) component.setup(config) - def thermostat_service(service): - """ Handles calls to the services. """ - - # Convert the entity ids to valid light ids - target_thermostats = component.extract_from_service(service) - - if service.service == SERVICE_SET_AWAY_MODE: - away_mode = service.data.get(ATTR_AWAY_MODE) - - if away_mode is None: - _LOGGER.error( - "Received call to %s without attribute %s", - SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) - - elif away_mode: - for thermostat in target_thermostats: - thermostat.turn_away_mode_on() - else: - for thermostat in target_thermostats: - thermostat.turn_away_mode_off() - - elif service.service == SERVICE_SET_TEMPERATURE: - temperature = util.convert( - service.data.get(ATTR_TEMPERATURE), float) - - if temperature is None: - return - - for thermostat in target_thermostats: - thermostat.set_temperature(convert( - temperature, hass.config.temperature_unit, - thermostat.unit_of_measurement)) - - for thermostat in target_thermostats: - thermostat.update_ha_state(True) - descriptions = load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) - hass.services.register( - DOMAIN, SERVICE_SET_AWAY_MODE, thermostat_service, - descriptions.get(SERVICE_SET_AWAY_MODE)) + def away_mode_set_service(service): + """ Set away mode on target thermostats """ + + target_thermostats = component.extract_from_service(service) + + away_mode = service.data.get(ATTR_AWAY_MODE) + + if away_mode is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) + return + + for thermostat in target_thermostats: + if away_mode: + thermostat.turn_away_mode_on() + else: + thermostat.turn_away_mode_off() + + thermostat.update_ha_state(True) hass.services.register( - DOMAIN, SERVICE_SET_TEMPERATURE, thermostat_service, + DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, + descriptions.get(SERVICE_SET_AWAY_MODE)) + + def temperature_set_service(service): + """ Set temperature on the target thermostats """ + + target_thermostats = component.extract_from_service(service) + + temperature = util.convert( + service.data.get(ATTR_TEMPERATURE), float) + + if temperature is None: + return + + for thermostat in target_thermostats: + thermostat.set_temperature(convert( + temperature, hass.config.temperature_unit, + thermostat.unit_of_measurement)) + + thermostat.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, descriptions.get(SERVICE_SET_TEMPERATURE)) return True From a0ed469aa204aecc60bc61165d78588d82effc3f Mon Sep 17 00:00:00 2001 From: Michael Auchter Date: Mon, 11 Jan 2016 19:08:41 -0600 Subject: [PATCH 106/229] thermostat: move fan attribute up to thermostat --- homeassistant/components/thermostat/__init__.py | 13 +++++++++++++ homeassistant/components/thermostat/nest.py | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index dce742b71a8..17b9a2daca6 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -34,6 +34,7 @@ STATE_IDLE = "idle" ATTR_CURRENT_TEMPERATURE = "current_temperature" ATTR_AWAY_MODE = "away_mode" +ATTR_FAN = "fan" ATTR_MAX_TEMP = "max_temp" ATTR_MIN_TEMP = "min_temp" ATTR_TEMPERATURE_LOW = "target_temp_low" @@ -167,6 +168,10 @@ class ThermostatDevice(Entity): if is_away is not None: data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF + is_fan_on = self.is_fan_on + if is_fan_on is not None: + data[ATTR_FAN] = STATE_ON if is_fan_on else STATE_OFF + device_attr = self.device_state_attributes if device_attr is not None: @@ -212,6 +217,14 @@ class ThermostatDevice(Entity): """ return None + @property + def is_fan_on(self): + """ + Returns if the fan is on + Return None if not available. + """ + return None + def set_temperate(self, temperature): """ Set new target temperature. """ pass diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 423a3195976..88a7761cb28 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -66,7 +66,6 @@ class NestThermostat(ThermostatDevice): return { "humidity": self.device.humidity, "target_humidity": self.device.target_humidity, - "fan": self.device.fan, "mode": self.device.mode } @@ -143,6 +142,11 @@ class NestThermostat(ThermostatDevice): """ Turns away off. """ self.structure.away = False + @property + def is_fan_on(self): + """ Returns whether the fan is on """ + return self.device.fan + @property def min_temp(self): """ Identifies min_temp in Nest API or defaults if not available. """ From df94c909f7db9c7948dc097ad0ee52169cb41bbb Mon Sep 17 00:00:00 2001 From: Michael Auchter Date: Mon, 11 Jan 2016 19:23:24 -0600 Subject: [PATCH 107/229] thermostat: add service to control fan mode --- .../components/thermostat/__init__.py | 47 +++++++++++++++++++ .../components/thermostat/services.yaml | 12 +++++ 2 files changed, 59 insertions(+) diff --git a/homeassistant/components/thermostat/__init__.py b/homeassistant/components/thermostat/__init__.py index 17b9a2daca6..d92a71ba1f6 100644 --- a/homeassistant/components/thermostat/__init__.py +++ b/homeassistant/components/thermostat/__init__.py @@ -27,6 +27,7 @@ SCAN_INTERVAL = 60 SERVICE_SET_AWAY_MODE = "set_away_mode" SERVICE_SET_TEMPERATURE = "set_temperature" +SERVICE_SET_FAN_MODE = "set_fan_mode" STATE_HEAT = "heat" STATE_COOL = "cool" @@ -70,6 +71,19 @@ def set_temperature(hass, temperature, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data) +def set_fan_mode(hass, fan_mode, entity_id=None): + """ Turn all or specified thermostat fan mode on. """ + data = { + ATTR_FAN: fan_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) + + +# pylint: disable=too-many-branches def setup(hass, config): """ Setup thermostats. """ component = EntityComponent(_LOGGER, DOMAIN, hass, @@ -126,6 +140,31 @@ def setup(hass, config): DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, descriptions.get(SERVICE_SET_TEMPERATURE)) + def fan_mode_set_service(service): + """ Set fan mode on target thermostats """ + + target_thermostats = component.extract_from_service(service) + + fan_mode = service.data.get(ATTR_FAN) + + if fan_mode is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_FAN_MODE, ATTR_FAN) + return + + for thermostat in target_thermostats: + if fan_mode: + thermostat.turn_fan_on() + else: + thermostat.turn_fan_off() + + thermostat.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, + descriptions.get(SERVICE_SET_FAN_MODE)) + return True @@ -237,6 +276,14 @@ class ThermostatDevice(Entity): """ Turns away mode off. """ pass + def turn_fan_on(self): + """ Turns fan on. """ + pass + + def turn_fan_off(self): + """ Turns fan off. """ + pass + @property def min_temp(self): """ Return minimum temperature. """ diff --git a/homeassistant/components/thermostat/services.yaml b/homeassistant/components/thermostat/services.yaml index 0d4f4726204..3592dfce75d 100644 --- a/homeassistant/components/thermostat/services.yaml +++ b/homeassistant/components/thermostat/services.yaml @@ -22,3 +22,15 @@ set_temperature: temperature: description: New target temperature for thermostat example: 25 + +set_fan_mode: + description: Turn fan on/off for a thermostat + + fields: + entity_id: + description: Name(s) of entities to change + example: 'thermostat.nest' + + fan: + description: New value of fan mode + example: true From 881c82c2df42628413c7a55825e5d79d726fe879 Mon Sep 17 00:00:00 2001 From: Michael Auchter Date: Mon, 11 Jan 2016 19:24:27 -0600 Subject: [PATCH 108/229] nest: implement fan control --- homeassistant/components/thermostat/nest.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/thermostat/nest.py b/homeassistant/components/thermostat/nest.py index 88a7761cb28..e0e1f74cdbc 100644 --- a/homeassistant/components/thermostat/nest.py +++ b/homeassistant/components/thermostat/nest.py @@ -147,6 +147,14 @@ class NestThermostat(ThermostatDevice): """ Returns whether the fan is on """ return self.device.fan + def turn_fan_on(self): + """ Turns fan on """ + self.device.fan = True + + def turn_fan_off(self): + """ Turns fan off """ + self.device.fan = False + @property def min_temp(self): """ Identifies min_temp in Nest API or defaults if not available. """ From a65d0f05496d837dbfb8aea88f6da3d12d7b2ac5 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 14:44:48 -0500 Subject: [PATCH 109/229] Reverting Automation decorator in favor of a new approach. --- .../components/automation/__init__.py | 115 ------------------ 1 file changed, 115 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 839cc71c37f..9c464f6954e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,11 +6,7 @@ Allows to setup simple automation rules via the config file. For more details about this component, please refer to the documentation at https://home-assistant.io/components/automation/ """ -from datetime import datetime -import functools -import inspect import logging -import yaml from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM @@ -35,8 +31,6 @@ CONDITION_TYPE_OR = 'or' DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND -CUSTOM_AUTOMATIONS = [] - _LOGGER = logging.getLogger(__name__) @@ -69,115 +63,6 @@ def setup(hass, config): return True -def activate(hass, config, domain): - """ Activate the automations for specified domain """ - for auto_rule in CUSTOM_AUTOMATIONS: - if auto_rule.domain == domain: - try: - success = auto_rule.activate(hass, config) - except Exception: - _LOGGER.exception('Error activating automation %s', - auto_rule.alias) - success = True - - if not success: - _LOGGER.error('Error activating automation %s', - auto_rule.alias) - - -class Automation(object): - """ Decorator for automation functions """ - - hass = None - - def __init__(self, action): - # store action and config - self.action = action - self.config = yaml.load(inspect.getdoc(action)) - self._activated = False - self._last_run = None - self._running = 0 - - # register the automation - module = inspect.getmodule(action) - self._domain = module.DOMAIN - CUSTOM_AUTOMATIONS.append(self) - - functools.update_wrapper(self, action) - - def __call__(self): - """ Call the action """ - if not self.activated: - return - - self._running += 1 - - _LOGGER.info('Executing %s', self.alias) - logbook.log_entry(self.hass, self.alias, 'has been triggered', DOMAIN) - - try: - self.action(self) - except Exception: - _LOGGER.exception('Error running Python automation: %s', - self.alias) - else: - self._last_run = datetime.now() - - self._running -= 1 - - @property - def alias(self): - """ The alias for the function """ - if CONF_ALIAS in self.config: - return self.config[CONF_ALIAS] - return None - - @property - def domain(self): - """ The domain to which this automation belongs """ - return self._domain - - @property - def is_running(self): - """ Boolean if the automation is running """ - return self._running > 0 - - @property - def num_running(self): - """ Integer of how many instances of the automation are running """ - return self._running - - @property - def activated(self): - """ Boolean indicating if the automation has been activated """ - return self._activated - - @property - def last_run(self): - """ Datetime object of the last automation completion """ - return self._last_run - - def activate(self, hass, config): - """ Activates the automation with HASS """ - self.hass = hass - - if self.activated: - return True - - if CONF_CONDITION in self.config or CONF_CONDITION_TYPE in self.config: - action = _process_if(hass, config, self.config, self.action) - - if action is None: - return False - self.action = action - - _process_trigger(hass, config, self.config.get(CONF_TRIGGER, []), - self.alias, self) - - self._activated = True - return True - - def _setup_automation(hass, config_block, name, config): """ Setup one instance of automation """ From 81dd1515ae8d3c18b7f1d48b4874d95b3c6e3eec Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 15:07:09 -0500 Subject: [PATCH 110/229] Moved sunrise/sunset tracking to helpers The automation component contained some very handy and generic functions for tracking sunset and sunrise. This was moved to helpers/event.py. --- homeassistant/components/automation/sun.py | 44 ++------------------ homeassistant/helpers/event.py | 48 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 0616c0a48e6..6abb59eede6 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -10,7 +10,7 @@ import logging from datetime import timedelta from homeassistant.components import sun -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import track_sunrise, track_sunset import homeassistant.util.dt as dt_util DEPENDENCIES = ['sun'] @@ -47,9 +47,9 @@ def trigger(hass, config, action): # Do something to call action if event == EVENT_SUNRISE: - trigger_sunrise(hass, action, offset) + track_sunrise(hass, action, offset) else: - trigger_sunset(hass, action, offset) + track_sunset(hass, action, offset) return True @@ -125,44 +125,6 @@ def if_action(hass, config): return time_if -def trigger_sunrise(hass, action, offset): - """ Trigger action at next sun rise. """ - def next_rise(): - """ Returns next sunrise. """ - next_time = sun.next_rising_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - - def sunrise_automation_listener(now): - """ Called when it's time for action. """ - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) - action() - - track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) - - -def trigger_sunset(hass, action, offset): - """ Trigger action at next sun set. """ - def next_set(): - """ Returns next sunrise. """ - next_time = sun.next_setting_utc(hass) + offset - - while next_time < dt_util.utcnow(): - next_time = next_time + timedelta(days=1) - - return next_time - - def sunset_automation_listener(now): - """ Called when it's time for action. """ - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) - action() - - track_point_in_utc_time(hass, sunset_automation_listener, next_set()) - - def _parse_offset(raw_offset): if raw_offset is None: return timedelta(0) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 3934a6c52ef..e8c9d0048b0 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,11 +1,13 @@ """ Helpers for listening to events """ +from datetime import timedelta import functools as ft from ..util import dt as dt_util from ..const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) +from homeassistant.components import sun def track_state_change(hass, entity_ids, action, from_state=None, @@ -95,6 +97,52 @@ def track_point_in_utc_time(hass, action, point_in_time): return point_in_time_listener +def track_sunrise(hass, action, offset=None): + """ + Adds a listener that will fire a specified offset from sunrise daily. + """ + offset = offset or timedelta() + + def next_rise(): + """ Returns next sunrise. """ + next_time = sun.next_rising_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunrise_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + action() + + track_point_in_utc_time(hass, sunrise_automation_listener, next_rise()) + + +def track_sunset(hass, action, offset=None): + """ + Adds a listener that will fire a specified offset from sunset daily. + """ + offset = offset or timedelta() + + def next_set(): + """ Returns next sunrise. """ + next_time = sun.next_setting_utc(hass) + offset + + while next_time < dt_util.utcnow(): + next_time = next_time + timedelta(days=1) + + return next_time + + def sunset_automation_listener(now): + """ Called when it's time for action. """ + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + action() + + track_point_in_utc_time(hass, sunset_automation_listener, next_set()) + + # 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): From 0f937cad7452e1b606dddd4f25666572dbf43b97 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 15:28:09 -0500 Subject: [PATCH 111/229] Initial pass at event decorators Created event decorators for custom components. Decorators were created for the events: track_state_change, track_sunrise, track_sunset, and track_time_change. --- homeassistant/bootstrap.py | 4 + homeassistant/helpers/event_decorators.py | 146 ++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 homeassistant/helpers/event_decorators.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b704fc082ac..e78d70fd11a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,6 +24,7 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group +from homeassistnat.helpers import event_decorators from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -203,6 +204,9 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, for domain in loader.load_order_components(components): _setup_component(hass, domain, config) + # activate event decorators + event_decorators.activate(hass) + return hass diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py new file mode 100644 index 00000000000..35b1247cf60 --- /dev/null +++ b/homeassistant/helpers/event_decorators.py @@ -0,0 +1,146 @@ +""" Event Decorators for custom components """ + +from datetime import datetime +import functools +import inspect +import logging + +from homeassistant.helpers import event +from homeassistant.components import logbook + +REGISTERED_DECORATORS = [] +_LOGGER = logging.getLogger(__name__) + + +def track_state_change(entity_ids, from_state=None, to_state=None): + """ Decorator factory to track state changes for entity id """ + + def track_state_change_decorator(action): + """ Decorator to track state changes """ + return Automation(action, event.track_state_change, + {"entity_ids": entity_ids, "from_state": from_state, + "to_state": to_state}) + + return track_state_change_decorator + + +def track_sunrise(offset=None): + """ Decorator factory to track sunrise events """ + + def track_sunrise_decorator(action): + """ Decorator to track sunrise events """ + return Automation(action, event.track_sunrise, {"offset": offset}) + + return track_sunrise_decorator + + +def track_sunset(offset=None): + """ Decorator factory to track sunset events """ + + def track_sunset_decorator(action): + """ Decorator to track sunset events """ + return Automation(action, event.track_sunset, {"offset": offset}) + + return track_sunset_decorator + + +# pylint: disable=too-many-arguments +def track_time_change(year=None, month=None, day=None, hour=None, minute=None, + second=None): + """ Decorator factory to track time changes """ + + def track_time_change_decorator(action): + """ Decorator to track time changes """ + return Automation(action, event.track_time_change, + {"year": year, "month": month, "day": day, + "hour": hour, "minute": minute, "second": second}) + + return track_time_change_decorator + + +def activate(hass): + """ Activate all event decorators """ + Automation.hass = hass + + return all([rule.activate() for rule in REGISTERED_DECORATORS]) + + +class Automation(object): + """ Base Decorator for automation functions """ + + hass = None + + def __init__(self, action, event, event_args): + # store action and config + self.action = action + self._event = (event, event_args) + self._activated = False + self._last_run = None + self._running = 0 + module = inspect.getmodule(action) + self._domain = module.DOMAIN + + REGISTERED_DECORATORS.append(self) + + functools.update_wrapper(self, action) + + def __call__(self, *args, **kwargs): + """ Call the action """ + if not self.activated: + return + + self._running += 1 + + _LOGGER.info('Executing %s', self.alias) + logbook.log_entry(self.hass, self.alias, 'has been triggered', + self._domain) + + try: + self.action(*args, **kwargs) + except Exception: + _LOGGER.exception('Error running Python automation: %s', + self.alias) + else: + self._last_run = datetime.now() + + self._running -= 1 + + @property + def alias(self): + """ The name of the action """ + return self.action.__name__ + + @property + def domain(self): + """ The domain to which this automation belongs """ + return self._domain + + @property + def is_running(self): + """ Boolean if the automation is running """ + return self._running > 0 + + @property + def num_running(self): + """ Integer of how many instances of the automation are running """ + return self._running + + @property + def activated(self): + """ Boolean indicating if the automation has been activated """ + return self._activated + + @property + def last_run(self): + """ Datetime object of the last automation completion """ + return self._last_run + + def activate(self): + """ Activates the automation with HASS """ + if self.activated: + return True + + self._event[0](hass=self.hass, action=self.action, **self._event[1]) + + self._activated = True + return True From 02e634c6a2df7a03dbdb6edcf00003d0df85f20c Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 15:55:47 -0500 Subject: [PATCH 112/229] Fixed bugs to allow HA to boot again 1) helpers/event should not import the sun component unless it is requested. This prevents circular import. 2) fixed import typo in bootstrap 2) bootstrap cannot import event_decorators until it is needed because this leads to a circular import. --- homeassistant/bootstrap.py | 2 +- homeassistant/helpers/event.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e78d70fd11a..aa649c400a0 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,6 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group -from homeassistnat.helpers import event_decorators from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -205,6 +204,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, _setup_component(hass, domain, config) # activate event decorators + from homeassistant.helpers import event_decorators event_decorators.activate(hass) return hass diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e8c9d0048b0..42725b8eea9 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -7,7 +7,6 @@ import functools as ft from ..util import dt as dt_util from ..const import ( ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) -from homeassistant.components import sun def track_state_change(hass, entity_ids, action, from_state=None, @@ -101,6 +100,7 @@ def track_sunrise(hass, action, offset=None): """ Adds a listener that will fire a specified offset from sunrise daily. """ + from homeassistant.components import sun offset = offset or timedelta() def next_rise(): @@ -124,6 +124,7 @@ def track_sunset(hass, action, offset=None): """ Adds a listener that will fire a specified offset from sunset daily. """ + from homeassistant.components import sun offset = offset or timedelta() def next_set(): From ef92940ffb4980dc490bd91799299718c7b34a63 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 16:45:35 -0500 Subject: [PATCH 113/229] A few lint fixes to event decorators. --- homeassistant/helpers/event_decorators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index 35b1247cf60..fbf979eaf47 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -70,10 +70,10 @@ class Automation(object): hass = None - def __init__(self, action, event, event_args): + def __init__(self, action, event_fun, event_args): # store action and config self.action = action - self._event = (event, event_args) + self._event = (event_fun, event_args) self._activated = False self._last_run = None self._running = 0 @@ -86,6 +86,7 @@ class Automation(object): def __call__(self, *args, **kwargs): """ Call the action """ + # pylint: disable=broad-except if not self.activated: return From 40dbeb0b60aaafc6909c477626851094659a4f4e Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 17:46:05 -0500 Subject: [PATCH 114/229] Another revision on event decorators This revision of event decorators removes much of the complexity. The decorated functions are no longer wrapped with a class that tracks last_run, etc. Bootstrap now gives hass to the event_decorators module before initializing components so the decorators no longer require activation. --- homeassistant/bootstrap.py | 8 +- homeassistant/helpers/event_decorators.py | 116 ++-------------------- 2 files changed, 15 insertions(+), 109 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index aa649c400a0..132178361e0 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,6 +24,7 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group +from homeassistant.helpers import event_decorators from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -199,14 +200,13 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, _LOGGER.info('Home Assistant core initialized') + # give event decorators access to HASS + event_decorators.HASS = hass + # Setup the components for domain in loader.load_order_components(components): _setup_component(hass, domain, config) - # activate event decorators - from homeassistant.helpers import event_decorators - event_decorators.activate(hass) - return hass diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index fbf979eaf47..e48cf4bf88d 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -1,15 +1,8 @@ """ Event Decorators for custom components """ -from datetime import datetime -import functools -import inspect -import logging - from homeassistant.helpers import event -from homeassistant.components import logbook -REGISTERED_DECORATORS = [] -_LOGGER = logging.getLogger(__name__) +HASS = None def track_state_change(entity_ids, from_state=None, to_state=None): @@ -17,9 +10,9 @@ def track_state_change(entity_ids, from_state=None, to_state=None): def track_state_change_decorator(action): """ Decorator to track state changes """ - return Automation(action, event.track_state_change, - {"entity_ids": entity_ids, "from_state": from_state, - "to_state": to_state}) + event.track_state_change(HASS, entity_ids, action, + from_state, to_state) + return action return track_state_change_decorator @@ -29,7 +22,8 @@ def track_sunrise(offset=None): def track_sunrise_decorator(action): """ Decorator to track sunrise events """ - return Automation(action, event.track_sunrise, {"offset": offset}) + event.track_sunrise(HASS, action, offset) + return action return track_sunrise_decorator @@ -39,7 +33,8 @@ def track_sunset(offset=None): def track_sunset_decorator(action): """ Decorator to track sunset events """ - return Automation(action, event.track_sunset, {"offset": offset}) + event.track_sunset(HASS, action, offset) + return action return track_sunset_decorator @@ -51,97 +46,8 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, def track_time_change_decorator(action): """ Decorator to track time changes """ - return Automation(action, event.track_time_change, - {"year": year, "month": month, "day": day, - "hour": hour, "minute": minute, "second": second}) + event.track_time_change(HASS, action, year, month, day, hour, + minute, second) + return action return track_time_change_decorator - - -def activate(hass): - """ Activate all event decorators """ - Automation.hass = hass - - return all([rule.activate() for rule in REGISTERED_DECORATORS]) - - -class Automation(object): - """ Base Decorator for automation functions """ - - hass = None - - def __init__(self, action, event_fun, event_args): - # store action and config - self.action = action - self._event = (event_fun, event_args) - self._activated = False - self._last_run = None - self._running = 0 - module = inspect.getmodule(action) - self._domain = module.DOMAIN - - REGISTERED_DECORATORS.append(self) - - functools.update_wrapper(self, action) - - def __call__(self, *args, **kwargs): - """ Call the action """ - # pylint: disable=broad-except - if not self.activated: - return - - self._running += 1 - - _LOGGER.info('Executing %s', self.alias) - logbook.log_entry(self.hass, self.alias, 'has been triggered', - self._domain) - - try: - self.action(*args, **kwargs) - except Exception: - _LOGGER.exception('Error running Python automation: %s', - self.alias) - else: - self._last_run = datetime.now() - - self._running -= 1 - - @property - def alias(self): - """ The name of the action """ - return self.action.__name__ - - @property - def domain(self): - """ The domain to which this automation belongs """ - return self._domain - - @property - def is_running(self): - """ Boolean if the automation is running """ - return self._running > 0 - - @property - def num_running(self): - """ Integer of how many instances of the automation are running """ - return self._running - - @property - def activated(self): - """ Boolean indicating if the automation has been activated """ - return self._activated - - @property - def last_run(self): - """ Datetime object of the last automation completion """ - return self._last_run - - def activate(self): - """ Activates the automation with HASS """ - if self.activated: - return True - - self._event[0](hass=self.hass, action=self.action, **self._event[1]) - - self._activated = True - return True From 57725136c0413f156b093ee0049752ed3f30cd25 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 19:52:22 -0500 Subject: [PATCH 115/229] Many updates regarding event decorators 1. Added HASS to the arguments for callbacks that are created with event decorators. 2. Added a service decorator. 3. Updated example.py in the example config to use the event decorators. --- config/custom_components/example.py | 145 +++++++++++----------- homeassistant/helpers/event_decorators.py | 35 +++++- 2 files changed, 104 insertions(+), 76 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index ee7f18f437a..dc29d4b1967 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -29,9 +29,12 @@ import time import logging from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF -import homeassistant.loader as loader from homeassistant.helpers import validate_config +from homeassistant.helpers.event_decorators import \ + track_state_change, track_time_change, service import homeassistant.components as core +from homeassistant.components import device_tracker +from homeassistant.components import light # The domain of your component. Should be equal to the name of your component DOMAIN = "example" @@ -39,11 +42,14 @@ DOMAIN = "example" # List of component names (string) your component depends upon # We depend on group because group will be loaded after all the components that # initialize devices have been setup. -DEPENDENCIES = ['group'] +DEPENDENCIES = ['group', 'device_tracker', 'light'] # Configuration key for the entity id we are targetting CONF_TARGET = 'target' +# Variable for storing configuration parameters +CONFIG = {} + # Name of the service that we expose SERVICE_FLASH = 'flash' @@ -58,79 +64,76 @@ def setup(hass, config): if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): return False - target_id = config[DOMAIN][CONF_TARGET] + CONFIG['target_id'] = config[DOMAIN][CONF_TARGET] # Validate that the target entity id exists - if hass.states.get(target_id) is None: - _LOGGER.error("Target entity id %s does not exist", target_id) + if hass.states.get(config['target_id']) is None: + _LOGGER.error("Target entity id %s does not exist", + CONFIG['target_id']) # Tell the bootstrapper that we failed to initialize return False - # We will use the component helper methods to check the states. - device_tracker = loader.get_component('device_tracker') - light = loader.get_component('light') - - def track_devices(entity_id, old_state, new_state): - """ Called when the group.all devices change state. """ - - # If anyone comes home and the core is not on, turn it on. - if new_state.state == STATE_HOME and not core.is_on(hass, target_id): - - core.turn_on(hass, target_id) - - # If all people leave the house and the core is on, turn it off - elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id): - - core.turn_off(hass, target_id) - - # Register our track_devices method to receive state changes of the - # all tracked devices group. - hass.states.track_change( - device_tracker.ENTITY_ID_ALL_DEVICES, track_devices) - - def wake_up(now): - """ Turn it on in the morning if there are people home and - it is not already on. """ - - if device_tracker.is_on(hass) and not core.is_on(hass, target_id): - _LOGGER.info('People home at 7AM, turning it on') - core.turn_on(hass, target_id) - - # Register our wake_up service to be called at 7AM in the morning - hass.track_time_change(wake_up, hour=7, minute=0, second=0) - - def all_lights_off(entity_id, old_state, new_state): - """ If all lights turn off, turn off. """ - - if core.is_on(hass, target_id): - _LOGGER.info('All lights have been turned off, turning it off') - core.turn_off(hass, target_id) - - # Register our all_lights_off method to be called when all lights turn off - hass.states.track_change( - light.ENTITY_ID_ALL_LIGHTS, all_lights_off, STATE_ON, STATE_OFF) - - def flash_service(call): - """ Service that will turn the target off for 10 seconds - if on and vice versa. """ - - if core.is_on(hass, target_id): - core.turn_off(hass, target_id) - - time.sleep(10) - - core.turn_on(hass, target_id) - - else: - core.turn_on(hass, target_id) - - time.sleep(10) - - core.turn_off(hass, target_id) - - # Register our service with HASS. - hass.services.register(DOMAIN, SERVICE_FLASH, flash_service) - - # Tells the bootstrapper that the component was successfully initialized + # Tell the bootstrapper that we initialized successfully return True + + +@track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES) +def track_devices(hass, entity_id, old_state, new_state): + """ Called when the group.all devices change state. """ + target_id = CONFIG['target_id'] + + # If anyone comes home and the entity is not on, turn it on. + if new_state.state == STATE_HOME and not core.is_on(hass, target_id): + + core.turn_on(hass, target_id) + + # If all people leave the house and the entity is on, turn it off + elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id): + + core.turn_off(hass, target_id) + + +@track_time_change(hour=7, minute=0, second=0) +def wake_up(hass, now): + """ + Turn it on in the morning (7 AM) if there are people home and + it is not already on. + """ + target_id = CONFIG['target_id'] + + if device_tracker.is_on(hass) and not core.is_on(hass, target_id): + _LOGGER.info('People home at 7AM, turning it on') + core.turn_on(hass, target_id) + + +@track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF) +def all_lights_off(hass, entity_id, old_state, new_state): + """ If all lights turn off, turn off. """ + target_id = CONFIG['target_id'] + + if core.is_on(hass, target_id): + _LOGGER.info('All lights have been turned off, turning it off') + core.turn_off(hass, target_id) + + +@service(DOMAIN, SERVICE_FLASH) +def flash_service(hass, call): + """ + Service that will turn the target off for 10 seconds if on and vice versa. + """ + target_id = CONFIG['target_id'] + + if core.is_on(hass, target_id): + core.turn_off(hass, target_id) + + time.sleep(10) + + core.turn_on(hass, target_id) + + else: + core.turn_on(hass, target_id) + + time.sleep(10) + + core.turn_off(hass, target_id) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index e48cf4bf88d..0fcd002c169 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -1,16 +1,36 @@ """ Event Decorators for custom components """ +import functools + from homeassistant.helpers import event HASS = None +def _callback(action, *args, **kwargs): + """ adds HASS to callback arguments """ + action(HASS, *args, **kwargs) + + +def service(domain, service): + """ Decorator factory to register a service """ + + def register_service_decorator(action): + """ Decorator to register a service """ + HASS.services.register(domain, service, + functools.partial(_callback, action)) + return action + + return register_service_decorator + + def track_state_change(entity_ids, from_state=None, to_state=None): """ Decorator factory to track state changes for entity id """ def track_state_change_decorator(action): """ Decorator to track state changes """ - event.track_state_change(HASS, entity_ids, action, + event.track_state_change(HASS, entity_ids, + functools.partial(_callback, action), from_state, to_state) return action @@ -22,7 +42,9 @@ def track_sunrise(offset=None): def track_sunrise_decorator(action): """ Decorator to track sunrise events """ - event.track_sunrise(HASS, action, offset) + event.track_sunrise(HASS, + functools.partial(_callback, action), + action, offset) return action return track_sunrise_decorator @@ -33,7 +55,9 @@ def track_sunset(offset=None): def track_sunset_decorator(action): """ Decorator to track sunset events """ - event.track_sunset(HASS, action, offset) + event.track_sunset(HASS, + functools.partial(_callback, action), + offset) return action return track_sunset_decorator @@ -46,8 +70,9 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, def track_time_change_decorator(action): """ Decorator to track time changes """ - event.track_time_change(HASS, action, year, month, day, hour, - minute, second) + event.track_time_change(HASS, + functools.partial(_callback, action), + year, month, day, hour, minute, second) return action return track_time_change_decorator From 2fa98167c2d4d80f74207b1cfb137783a1b8872a Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 20:05:40 -0500 Subject: [PATCH 116/229] Updated example.py component Cleaned up example.py to better handle failed loads. --- config/custom_components/example.py | 52 +++++++++++++++++------------ 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index dc29d4b1967..3fb46d18792 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -48,7 +48,7 @@ DEPENDENCIES = ['group', 'device_tracker', 'light'] CONF_TARGET = 'target' # Variable for storing configuration parameters -CONFIG = {} +TARGET_ID = None # Name of the service that we expose SERVICE_FLASH = 'flash' @@ -59,19 +59,22 @@ _LOGGER = logging.getLogger(__name__) def setup(hass, config): """ Setup example component. """ + global TARGET_ID # Validate that all required config options are given if not validate_config(config, {DOMAIN: [CONF_TARGET]}, _LOGGER): return False - CONFIG['target_id'] = config[DOMAIN][CONF_TARGET] + TARGET_ID = config[DOMAIN][CONF_TARGET] # Validate that the target entity id exists - if hass.states.get(config['target_id']) is None: + if hass.states.get(TARGET_ID) is None: _LOGGER.error("Target entity id %s does not exist", - CONFIG['target_id']) + TARGET_ID) - # Tell the bootstrapper that we failed to initialize + # Tell the bootstrapper that we failed to initialize and clear the + # stored target id so our functions don't run. + TARGET_ID = None return False # Tell the bootstrapper that we initialized successfully @@ -81,17 +84,19 @@ def setup(hass, config): @track_state_change(device_tracker.ENTITY_ID_ALL_DEVICES) def track_devices(hass, entity_id, old_state, new_state): """ Called when the group.all devices change state. """ - target_id = CONFIG['target_id'] + # If the target id is not set, return + if not TARGET_ID: + return # If anyone comes home and the entity is not on, turn it on. - if new_state.state == STATE_HOME and not core.is_on(hass, target_id): + if new_state.state == STATE_HOME and not core.is_on(hass, TARGET_ID): - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) # If all people leave the house and the entity is on, turn it off - elif new_state.state == STATE_NOT_HOME and core.is_on(hass, target_id): + elif new_state.state == STATE_NOT_HOME and core.is_on(hass, TARGET_ID): - core.turn_off(hass, target_id) + core.turn_off(hass, TARGET_ID) @track_time_change(hour=7, minute=0, second=0) @@ -100,21 +105,23 @@ def wake_up(hass, now): Turn it on in the morning (7 AM) if there are people home and it is not already on. """ - target_id = CONFIG['target_id'] + if not TARGET_ID: + return - if device_tracker.is_on(hass) and not core.is_on(hass, target_id): + if device_tracker.is_on(hass) and not core.is_on(hass, TARGET_ID): _LOGGER.info('People home at 7AM, turning it on') - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) @track_state_change(light.ENTITY_ID_ALL_LIGHTS, STATE_ON, STATE_OFF) def all_lights_off(hass, entity_id, old_state, new_state): """ If all lights turn off, turn off. """ - target_id = CONFIG['target_id'] + if not TARGET_ID: + return - if core.is_on(hass, target_id): + if core.is_on(hass, TARGET_ID): _LOGGER.info('All lights have been turned off, turning it off') - core.turn_off(hass, target_id) + core.turn_off(hass, TARGET_ID) @service(DOMAIN, SERVICE_FLASH) @@ -122,18 +129,19 @@ def flash_service(hass, call): """ Service that will turn the target off for 10 seconds if on and vice versa. """ - target_id = CONFIG['target_id'] + if not TARGET_ID: + return - if core.is_on(hass, target_id): - core.turn_off(hass, target_id) + if core.is_on(hass, TARGET_ID): + core.turn_off(hass, TARGET_ID) time.sleep(10) - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) else: - core.turn_on(hass, target_id) + core.turn_on(hass, TARGET_ID) time.sleep(10) - core.turn_off(hass, target_id) + core.turn_off(hass, TARGET_ID) From 54b82ecd91e1dcc000eb020bbabb4a3aa2b0e1cc Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 21:06:15 -0500 Subject: [PATCH 117/229] Lint fixes and additions to event decorators 1. service decorator was overwriting the function name with one of its arguments. 2. Accidentally left an extra argument in track_sunrise. 3. Added track_utc_time_change decorator. --- homeassistant/helpers/event_decorators.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index 0fcd002c169..f7aee82631c 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -12,12 +12,12 @@ def _callback(action, *args, **kwargs): action(HASS, *args, **kwargs) -def service(domain, service): +def service(domain, service_name): """ Decorator factory to register a service """ def register_service_decorator(action): """ Decorator to register a service """ - HASS.services.register(domain, service, + HASS.services.register(domain, service_name, functools.partial(_callback, action)) return action @@ -44,7 +44,7 @@ def track_sunrise(offset=None): """ Decorator to track sunrise events """ event.track_sunrise(HASS, functools.partial(_callback, action), - action, offset) + offset) return action return track_sunrise_decorator @@ -76,3 +76,18 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, return action return track_time_change_decorator + + +# pylint: disable=too-many-arguments +def track_utc_time_change(year=None, month=None, day=None, hour=None, + minute=None, second=None): + """ Decorator factory to track time changes """ + + def track_utc_time_change_decorator(action): + """ Decorator to track time changes """ + event.track_utc_time_change(HASS, + functools.partial(_callback, action), + year, month, day, hour, minute, second) + return action + + return track_utc_time_change_decorator From f66aeb2e7332f6fb8062717cb56cbffa46d50e58 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 22:23:56 -0500 Subject: [PATCH 118/229] Added event helper tests 1. Added tests for all event decorators 2. Added tests for sunrise and sunset event helpers --- tests/helpers/test_event.py | 95 +++++++++++ tests/helpers/test_event_decorators.py | 211 +++++++++++++++++++++++++ 2 files changed, 306 insertions(+) create mode 100644 tests/helpers/test_event_decorators.py diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 89711e2584e..e12ca0c4124 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -9,8 +9,11 @@ Tests event helpers. import unittest from datetime import datetime +from astral import Astral + import homeassistant.core as ha from homeassistant.helpers.event import * +from homeassistant.components import sun class TestEventHelpers(unittest.TestCase): @@ -121,6 +124,98 @@ class TestEventHelpers(unittest.TestCase): self.assertEqual(1, len(specific_runs)) self.assertEqual(3, len(wildcard_runs)) + def test_track_sunrise(self): + """ Test track sunrise """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_rising = (astral.sunrise_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 + + # track sunrise + runs = [] + track_sunrise(self.hass, lambda: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + track_sunrise(self.hass, lambda: offset_runs.append(1), offset) + + # run tests + self._send_time_changed(next_rising - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_rising) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_rising + offset) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + self.assertEqual(1, len(offset_runs)) + + def test_track_sunset(self): + """ Test track sunset """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_setting = (astral.sunset_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 + + # track sunset + runs = [] + track_sunset(self.hass, lambda: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + track_sunset(self.hass, lambda: offset_runs.append(1), offset) + + # run tests + self._send_time_changed(next_setting - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_setting) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_setting + offset) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + self.assertEqual(1, len(offset_runs)) + def _send_time_changed(self, now): """ Send a time changed event. """ self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) diff --git a/tests/helpers/test_event_decorators.py b/tests/helpers/test_event_decorators.py new file mode 100644 index 00000000000..d246cf1844c --- /dev/null +++ b/tests/helpers/test_event_decorators.py @@ -0,0 +1,211 @@ +""" +tests.helpers.test_event_decorators +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests event decorator helpers. +""" +# pylint: disable=protected-access,too-many-public-methods +# pylint: disable=too-few-public-methods +import unittest +from datetime import datetime, timedelta + +from astral import Astral + +import homeassistant.core as ha +import homeassistant.util.dt as dt_util +from homeassistant.helpers import event_decorators +from homeassistant.helpers.event_decorators import ( + track_time_change, track_utc_time_change, track_state_change, service, + track_sunrise, track_sunset) +from homeassistant.components import sun + + +class TestEventDecoratorHelpers(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") + + event_decorators.HASS = self.hass + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_service(self): + """ Test service registration decorator. """ + runs = [] + + decor = service('test', 'test') + decor(lambda x, y: runs.append(1)) + + self.hass.services.call('test', 'test') + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + + def test_track_sunrise(self): + """ Test track sunrise decorator """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_rising = (astral.sunrise_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 + + # use decorator + runs = [] + decor = track_sunrise() + decor(lambda x: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + decor = track_sunrise(offset) + decor(lambda x: offset_runs.append(1)) + + # run tests + self._send_time_changed(next_rising - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_rising) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_rising + offset) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + self.assertEqual(1, len(offset_runs)) + + def test_track_sunset(self): + """ Test track sunset decorator """ + latitude = 32.87336 + longitude = 117.22743 + + # setup sun component + self.hass.config.latitude = latitude + self.hass.config.longitude = longitude + sun.setup(self.hass, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # get next sunrise/sunset + astral = Astral() + utc_now = dt_util.utcnow() + + mod = -1 + while True: + next_setting = (astral.sunset_utc(utc_now + + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 + + # use decorator + runs = [] + decor = track_sunset() + decor(lambda x: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + decor = track_sunset(offset) + decor(lambda x: offset_runs.append(1)) + + # run tests + self._send_time_changed(next_setting - offset) + self.hass.pool.block_till_done() + self.assertEqual(0, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_setting) + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + self.assertEqual(0, len(offset_runs)) + + self._send_time_changed(next_setting + offset) + self.hass.pool.block_till_done() + self.assertEqual(2, len(runs)) + self.assertEqual(1, len(offset_runs)) + + def test_track_time_change(self): + """ Test tracking time change. """ + wildcard_runs = [] + specific_runs = [] + + decor = track_time_change() + decor(lambda x, y: wildcard_runs.append(1)) + + decor = track_utc_time_change(second=[0, 30]) + decor(lambda x, y: specific_runs.append(1)) + + 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 track_state_change. """ + # 2 lists to track how often our callbacks get called + specific_runs = [] + wildcard_runs = [] + + decor = track_state_change('light.Bowl', 'on', 'off') + decor(lambda a, b, c, d: specific_runs.append(1)) + + decor = track_state_change('light.Bowl', ha.MATCH_ALL, ha.MATCH_ALL) + decor(lambda a, b, c, d: wildcard_runs.append(1)) + + # 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 d94db5388c42c70bb9e404fcf3595da839b80af7 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Jan 2016 03:32:55 +0000 Subject: [PATCH 119/229] Add preliminary support for transition time --- homeassistant/components/light/lifx.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index afe2196b0e0..8ff99403e05 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -24,7 +24,7 @@ import logging import colorsys from homeassistant.helpers.event import track_time_change from homeassistant.components.light import \ - (Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP) + (Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION) _LOGGER = logging.getLogger(__name__) @@ -172,6 +172,11 @@ class LIFXLight(Light): def turn_on(self, **kwargs): """ Turn the device on. """ + if ATTR_TRANSITION in kwargs: + fade = kwargs[ATTR_TRANSITION] * 1000 + else: + fade = 0 + if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) else: @@ -192,15 +197,21 @@ class LIFXLight(Light): else: kelvin = self._kel + _LOGGER.info("%s %d %d %d %d %d", self._ip, hue, saturation, brightness, kelvin, fade) if self._power == 0: - self._liffylights.set_power(self._ip, 65535) + self._liffylights.set_power(self._ip, 65535, 0) self._liffylights.set_color(self._ip, hue, saturation, - brightness, kelvin) + brightness, kelvin, fade) def turn_off(self, **kwargs): """ Turn the device off. """ - self._liffylights.set_power(self._ip, 0) + if ATTR_TRANSITION in kwargs: + fade = kwargs[ATTR_TRANSITION] * 1000 + else: + fade = 0 + + self._liffylights.set_power(self._ip, 0, fade) def set_name(self, name): """ Set name. """ From 74e844655641d6ad64a1e3440890af1339d5a07a Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Jan 2016 03:34:24 +0000 Subject: [PATCH 120/229] Bump version of liffylights --- homeassistant/components/light/lifx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 8ff99403e05..9c33ab3c8e4 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -28,7 +28,7 @@ from homeassistant.components.light import \ _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['liffylights==0.4'] +REQUIREMENTS = ['liffylights==0.5'] DEPENDENCIES = [] CONF_SERVER = "server" # server address configuration item From 50561ffe974d0c4034193d5e3baecb31d4eb86c5 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 25 Jan 2016 03:39:13 +0000 Subject: [PATCH 121/229] Fix long line --- homeassistant/components/light/lifx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 9c33ab3c8e4..64bdb946cf1 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -197,7 +197,8 @@ class LIFXLight(Light): else: kelvin = self._kel - _LOGGER.info("%s %d %d %d %d %d", self._ip, hue, saturation, brightness, kelvin, fade) + _LOGGER.debug("%s %d %d %d %d %d", + self._ip, hue, saturation, brightness, kelvin, fade) if self._power == 0: self._liffylights.set_power(self._ip, 65535, 0) From 5830da63b134fbc83de6e177ac1f19055d8ead2e Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 22:46:30 -0500 Subject: [PATCH 122/229] Moved service decorator to service helpers Moved the service decorator to the service helpers module and moved the associated tests. --- config/custom_components/example.py | 3 ++- homeassistant/bootstrap.py | 3 ++- homeassistant/helpers/service.py | 24 ++++++++++++++++++++++-- tests/helpers/test_event_decorators.py | 13 +------------ tests/helpers/test_service.py | 14 +++++++++++++- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/config/custom_components/example.py b/config/custom_components/example.py index 3fb46d18792..08b3f4c2a83 100644 --- a/config/custom_components/example.py +++ b/config/custom_components/example.py @@ -31,7 +31,8 @@ import logging from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_ON, STATE_OFF from homeassistant.helpers import validate_config from homeassistant.helpers.event_decorators import \ - track_state_change, track_time_change, service + track_state_change, track_time_change +from homeassistant.helpers.service import service import homeassistant.components as core from homeassistant.components import device_tracker from homeassistant.components import light diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 132178361e0..dbec25b99b6 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,7 @@ import homeassistant.config as config_util import homeassistant.loader as loader import homeassistant.components as core_components import homeassistant.components.group as group -from homeassistant.helpers import event_decorators +from homeassistant.helpers import event_decorators, service from homeassistant.helpers.entity import Entity from homeassistant.const import ( __version__, EVENT_COMPONENT_LOADED, CONF_LATITUDE, CONF_LONGITUDE, @@ -202,6 +202,7 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True, # give event decorators access to HASS event_decorators.HASS = hass + service.HASS = hass # Setup the components for domain in loader.load_order_components(components): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 15cfe9b97c6..952de383444 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -1,9 +1,12 @@ """Service calling related helpers.""" +import functools import logging from homeassistant.util import split_entity_id from homeassistant.const import ATTR_ENTITY_ID +HASS = None + CONF_SERVICE = 'service' CONF_SERVICE_ENTITY_ID = 'entity_id' CONF_SERVICE_DATA = 'data' @@ -11,6 +14,23 @@ CONF_SERVICE_DATA = 'data' _LOGGER = logging.getLogger(__name__) +def _callback(action, *args, **kwargs): + """ adds HASS to callback arguments """ + action(HASS, *args, **kwargs) + + +def service(domain, service_name): + """ Decorator factory to register a service """ + + def register_service_decorator(action): + """ Decorator to register a service """ + HASS.services.register(domain, service_name, + functools.partial(_callback, action)) + return action + + return register_service_decorator + + def call_from_config(hass, config, blocking=False): """Call a service based on a config hash.""" if not isinstance(config, dict) or CONF_SERVICE not in config: @@ -18,7 +38,7 @@ def call_from_config(hass, config, blocking=False): return try: - domain, service = split_entity_id(config[CONF_SERVICE]) + domain, service_name = split_entity_id(config[CONF_SERVICE]) except ValueError: _LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE]) return @@ -40,4 +60,4 @@ def call_from_config(hass, config, blocking=False): elif entity_id is not None: service_data[ATTR_ENTITY_ID] = entity_id - hass.services.call(domain, service, service_data, blocking) + hass.services.call(domain, service_name, service_data, blocking) diff --git a/tests/helpers/test_event_decorators.py b/tests/helpers/test_event_decorators.py index d246cf1844c..db836e372ae 100644 --- a/tests/helpers/test_event_decorators.py +++ b/tests/helpers/test_event_decorators.py @@ -15,7 +15,7 @@ import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.helpers import event_decorators from homeassistant.helpers.event_decorators import ( - track_time_change, track_utc_time_change, track_state_change, service, + track_time_change, track_utc_time_change, track_state_change, track_sunrise, track_sunset) from homeassistant.components import sun @@ -37,17 +37,6 @@ class TestEventDecoratorHelpers(unittest.TestCase): """ Stop down stuff we started. """ self.hass.stop() - def test_service(self): - """ Test service registration decorator. """ - runs = [] - - decor = service('test', 'test') - decor(lambda x, y: runs.append(1)) - - self.hass.services.call('test', 'test') - self.hass.pool.block_till_done() - self.assertEqual(1, len(runs)) - def test_track_sunrise(self): """ Test track sunrise decorator """ latitude = 32.87336 diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index aa2cab07d0d..d0bd1669f07 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -7,7 +7,6 @@ Test service helpers. import unittest from unittest.mock import patch -from homeassistant.const import SERVICE_TURN_ON from homeassistant.helpers import service from tests.common import get_test_home_assistant, mock_service @@ -23,10 +22,23 @@ class TestServiceHelpers(unittest.TestCase): self.hass = get_test_home_assistant() self.calls = mock_service(self.hass, 'test_domain', 'test_service') + service.HASS = self.hass + def tearDown(self): # pylint: disable=invalid-name """ Stop down stuff we started. """ self.hass.stop() + def test_service(self): + """ Test service registration decorator. """ + runs = [] + + decor = service.service('test', 'test') + decor(lambda x, y: runs.append(1)) + + self.hass.services.call('test', 'test') + self.hass.pool.block_till_done() + self.assertEqual(1, len(runs)) + def test_split_entity_string(self): service.call_from_config(self.hass, { 'service': 'test_domain.test_service', From 3b89102338bfd8c7576d5d37a97a51f4bc101c2d Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 23:00:43 -0500 Subject: [PATCH 123/229] Fixed lint issue from merge extract_entity_ids from the service helpers was overwriting the service decorator with one of its attributes. This was fixed. --- homeassistant/helpers/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ccab891eedb..6617d0e1514 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -64,18 +64,18 @@ def call_from_config(hass, config, blocking=False): hass.services.call(domain, service_name, service_data, blocking) -def extract_entity_ids(hass, service): +def extract_entity_ids(hass, service_call): """ Helper method to extract a list of entity ids from a service call. Will convert group entity ids to the entity ids it represents. """ - if not (service.data and ATTR_ENTITY_ID in service.data): + if not (service_call.data and ATTR_ENTITY_ID in service_call.data): return [] group = get_component('group') # Entity ID attr can be a list or a string - service_ent_id = service.data[ATTR_ENTITY_ID] + service_ent_id = service_call.data[ATTR_ENTITY_ID] if isinstance(service_ent_id, str): return group.expand_entity_ids(hass, [service_ent_id]) From bcdfc555e0e6da26d05df98a213f97af7787567a Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Sun, 24 Jan 2016 23:09:09 -0500 Subject: [PATCH 124/229] Removed service decorator from event decorators --- homeassistant/helpers/event_decorators.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index f7aee82631c..b1a1e1f0304 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -12,18 +12,6 @@ def _callback(action, *args, **kwargs): action(HASS, *args, **kwargs) -def service(domain, service_name): - """ Decorator factory to register a service """ - - def register_service_decorator(action): - """ Decorator to register a service """ - HASS.services.register(domain, service_name, - functools.partial(_callback, action)) - return action - - return register_service_decorator - - def track_state_change(entity_ids, from_state=None, to_state=None): """ Decorator factory to track state changes for entity id """ From 8406f8181172036047cdfbce4d6fab1d342e86b3 Mon Sep 17 00:00:00 2001 From: Ryan Kraus Date: Mon, 25 Jan 2016 00:14:16 -0500 Subject: [PATCH 125/229] Removed decorator callback The decorator callback was not actually necessary so it was removed and replaced with a partial function instead. --- homeassistant/helpers/event_decorators.py | 15 +++++---------- homeassistant/helpers/service.py | 7 +------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/event_decorators.py b/homeassistant/helpers/event_decorators.py index b1a1e1f0304..e98f912ef64 100644 --- a/homeassistant/helpers/event_decorators.py +++ b/homeassistant/helpers/event_decorators.py @@ -7,18 +7,13 @@ from homeassistant.helpers import event HASS = None -def _callback(action, *args, **kwargs): - """ adds HASS to callback arguments """ - action(HASS, *args, **kwargs) - - def track_state_change(entity_ids, from_state=None, to_state=None): """ Decorator factory to track state changes for entity id """ def track_state_change_decorator(action): """ Decorator to track state changes """ event.track_state_change(HASS, entity_ids, - functools.partial(_callback, action), + functools.partial(action, HASS), from_state, to_state) return action @@ -31,7 +26,7 @@ def track_sunrise(offset=None): def track_sunrise_decorator(action): """ Decorator to track sunrise events """ event.track_sunrise(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), offset) return action @@ -44,7 +39,7 @@ def track_sunset(offset=None): def track_sunset_decorator(action): """ Decorator to track sunset events """ event.track_sunset(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), offset) return action @@ -59,7 +54,7 @@ def track_time_change(year=None, month=None, day=None, hour=None, minute=None, def track_time_change_decorator(action): """ Decorator to track time changes """ event.track_time_change(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), year, month, day, hour, minute, second) return action @@ -74,7 +69,7 @@ def track_utc_time_change(year=None, month=None, day=None, hour=None, def track_utc_time_change_decorator(action): """ Decorator to track time changes """ event.track_utc_time_change(HASS, - functools.partial(_callback, action), + functools.partial(action, HASS), year, month, day, hour, minute, second) return action diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 6617d0e1514..2d198910408 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -15,18 +15,13 @@ CONF_SERVICE_DATA = 'data' _LOGGER = logging.getLogger(__name__) -def _callback(action, *args, **kwargs): - """ adds HASS to callback arguments """ - action(HASS, *args, **kwargs) - - def service(domain, service_name): """ Decorator factory to register a service """ def register_service_decorator(action): """ Decorator to register a service """ HASS.services.register(domain, service_name, - functools.partial(_callback, action)) + functools.partial(action, HASS)) return action return register_service_decorator From f6c53896e301c8588531d142a40f93a381852d9b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 24 Jan 2016 14:13:39 -0800 Subject: [PATCH 126/229] Allow groups to be used as views --- homeassistant/components/demo.py | 8 +- .../components/device_tracker/__init__.py | 2 +- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 200 +++++++++++++++--- .../www_static/home-assistant-polymer | 2 +- homeassistant/components/group.py | 60 ++++-- homeassistant/const.py | 1 + tests/components/test_group.py | 36 +++- tests/components/test_zone.py | 10 +- tests/helpers/test_service.py | 2 +- 10 files changed, 256 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/demo.py b/homeassistant/components/demo.py index 8b4b3fcce6c..348ba0f645b 100644 --- a/homeassistant/components/demo.py +++ b/homeassistant/components/demo.py @@ -62,10 +62,10 @@ def setup(hass, config): lights = sorted(hass.states.entity_ids('light')) switches = sorted(hass.states.entity_ids('switch')) media_players = sorted(hass.states.entity_ids('media_player')) - group.setup_group(hass, 'living room', [lights[2], lights[1], switches[0], - media_players[1]]) - group.setup_group(hass, 'bedroom', [lights[0], switches[1], - media_players[0]]) + group.Group(hass, 'living room', [lights[2], lights[1], switches[0], + media_players[1]]) + group.Group(hass, 'bedroom', [lights[0], switches[1], + media_players[0]]) # Setup scripts bootstrap.setup_component( diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 204d845084c..c5b4ccd1c16 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -229,7 +229,7 @@ class DeviceTracker(object): """ Initializes group for all tracked devices. """ entity_ids = (dev.entity_id for dev in self.devices.values() if dev.track) - self.group = group.setup_group( + self.group = group.Group( self.hass, GROUP_NAME_ALL_DEVICES, entity_ids, False) def update_stale(self, now): diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index b8a31e418ca..5220dee892e 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 = "1003c31441ec44b3db84b49980f736a7" +VERSION = "5acc1c32156966aef67ca45a1e677eae" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 1816b922342..0cdea3a6b02 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1437,7 +1437,7 @@ var n=this._rootDataHost;return n?n._scopeElementClass(t,e):void 0},stamp:functi left: 0; }; - }