From 291910d74e3975d5ba2b50a2d88ec9eb35061471 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 18 Jan 2016 18:10:32 +0000 Subject: [PATCH 01/68] 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 02/68] 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 03/68] 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 c2d72bbf09a465e22f36f040e2a80d65fd67559f Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 18 Jan 2016 19:41:41 +0100 Subject: [PATCH 04/68] 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 cbb74d50cea2585228ea789801fe02c260206856 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jan 2016 17:50:20 -0800 Subject: [PATCH 05/68] 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 1ceee2d6c5904216c974c914bf37bbf3ee689ef0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 17 Jan 2016 21:39:25 -0800 Subject: [PATCH 06/68] 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 4c0ff0e0d0d2872519a5d1f69b9eea662d76d671 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jan 2016 09:00:40 -0800 Subject: [PATCH 07/68] 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 4c4539caffa63d8de1605bc70e86f20fc0dcf8e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Jan 2016 09:12:03 -0800 Subject: [PATCH 08/68] Version bump to 0.11.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7a0d0dff9be..70957d63eca 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.11.1" # Can be used to specify a catch all when registering state or event listeners. MATCH_ALL = '*' From d4629a7efe510895bc453078323d101c5b2f159f Mon Sep 17 00:00:00 2001 From: MartinHjelmare Date: Tue, 19 Jan 2016 19:26:40 +0100 Subject: [PATCH 09/68] 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 837e7affa7ba922710d0939fc800bc6333ae4887 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 23 Jan 2016 17:48:14 +0100 Subject: [PATCH 10/68] 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 11/68] 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 12/68] 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 13/68] 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 17f5a466d9df082ab7b7cb0ab2eed114b4fdbe4e Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 23 Jan 2016 22:14:57 +0000 Subject: [PATCH 14/68] 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 15/68] 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 16/68] 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 17/68] 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 18/68] 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 19/68] 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 20/68] 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 706bbeae16345696572d874e45924e38ad42f720 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 02:17:52 +0000 Subject: [PATCH 21/68] 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 9f6a1c75fa1d4e05671e778a95704b73a92480a9 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 24 Jan 2016 10:01:23 +0000 Subject: [PATCH 22/68] 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 23/68] 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 24/68] 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 25/68] 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 26/68] 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 27/68] 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 28/68] 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 29/68] 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 30/68] 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 31/68] 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 32/68] 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 33/68] 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 34/68] 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 35/68] 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 36/68] 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 37/68] 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 38/68] 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 39/68] 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 40/68] 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 41/68] 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 42/68] 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 43/68] 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 44/68] 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 45/68] 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 46/68] 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 47/68] 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 48/68] 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; }; - }