From d5179b4bdc6f0a183c53257ff89eb4e3bfb33df1 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Mon, 21 Dec 2015 19:49:39 -0500 Subject: [PATCH 01/78] add statecmd to command_switch --- .../components/switch/command_switch.py | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 91171be3680..1882d73fc51 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -10,6 +10,8 @@ import logging import subprocess from homeassistant.components.switch import SwitchDevice +from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.util import template _LOGGER = logging.getLogger(__name__) @@ -22,22 +24,36 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): devices = [] for dev_name, properties in switches.items(): + if 'statecmd' in properties and CONF_VALUE_TEMPLATE not in properties: + _LOGGER.warn("Specify a %s when using statemcd", + CONF_VALUE_TEMPLATE) + continue devices.append( CommandSwitch( + hass, properties.get('name', dev_name), properties.get('oncmd', 'true'), - properties.get('offcmd', 'true'))) + properties.get('offcmd', 'true'), + properties.get('statecmd', False), + properties.get(CONF_VALUE_TEMPLATE, False))) add_devices_callback(devices) class CommandSwitch(SwitchDevice): """ Represents a switch that can be togggled using shell commands. """ - def __init__(self, name, command_on, command_off): + + # pylint: disable=too-many-arguments + def __init__(self, hass, name, command_on, command_off, + command_state, value_template): + + self._hass = hass self._name = name self._state = False self._command_on = command_on self._command_off = command_off + self._command_state = command_state + self._value_template = value_template @staticmethod def _switch(command): @@ -51,10 +67,21 @@ class CommandSwitch(SwitchDevice): return success + @staticmethod + def _query_state(command): + """ Execute state command. """ + _LOGGER.info('Running state command: %s', command) + + try: + return_value = subprocess.check_output(command, shell=True) + return return_value.strip().decode('utf-8') + except subprocess.CalledProcessError: + _LOGGER.error('Command failed: %s', command) + @property def should_poll(self): """ No polling needed. """ - return False + return True @property def name(self): @@ -66,14 +93,24 @@ class CommandSwitch(SwitchDevice): """ True if device is on. """ return self._state + def update(self): + """ Update device state. """ + if self._command_state and self._value_template: + payload = CommandSwitch._query_state(self._command_state) + payload = template.render_with_possible_json_value( + self._hass, self._value_template, payload) + self._state = (payload == "True") + def turn_on(self, **kwargs): """ Turn the device on. """ if CommandSwitch._switch(self._command_on): - self._state = True - self.update_ha_state() + if not self._command_state: + self._state = True + self.update_ha_state() def turn_off(self, **kwargs): """ Turn the device off. """ if CommandSwitch._switch(self._command_off): - self._state = False - self.update_ha_state() + if not self._command_state: + self._state = False + self.update_ha_state() From fba5becd909ed826fbe8b8368abbdf848b5efcfd Mon Sep 17 00:00:00 2001 From: happyleaves Date: Mon, 21 Dec 2015 20:18:00 -0500 Subject: [PATCH 02/78] warn->warning --- homeassistant/components/switch/command_switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 1882d73fc51..5af197193d3 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -25,8 +25,8 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): for dev_name, properties in switches.items(): if 'statecmd' in properties and CONF_VALUE_TEMPLATE not in properties: - _LOGGER.warn("Specify a %s when using statemcd", - CONF_VALUE_TEMPLATE) + _LOGGER.warning("Specify a %s when using statemcd", + CONF_VALUE_TEMPLATE) continue devices.append( CommandSwitch( From e9059a3ed9b6d311b79750eeb672b153a6c1bfe0 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Sun, 27 Dec 2015 22:49:55 -0500 Subject: [PATCH 03/78] added test; addressed comments --- .../components/switch/command_switch.py | 38 +++-- .../components/switch/test_command_switch.py | 158 ++++++++++++++++++ 2 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 tests/components/switch/test_command_switch.py diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index 5af197193d3..c36ca4e9ce9 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -24,10 +24,6 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): devices = [] for dev_name, properties in switches.items(): - if 'statecmd' in properties and CONF_VALUE_TEMPLATE not in properties: - _LOGGER.warning("Specify a %s when using statemcd", - CONF_VALUE_TEMPLATE) - continue devices.append( CommandSwitch( hass, @@ -68,8 +64,8 @@ class CommandSwitch(SwitchDevice): return success @staticmethod - def _query_state(command): - """ Execute state command. """ + def _query_state_value(command): + """ Execute state command for return value. """ _LOGGER.info('Running state command: %s', command) try: @@ -78,10 +74,16 @@ class CommandSwitch(SwitchDevice): except subprocess.CalledProcessError: _LOGGER.error('Command failed: %s', command) + @staticmethod + def _query_state_code(command): + """ Execute state command for return code. """ + _LOGGER.info('Running state command: %s', command) + return subprocess.call(command, shell=True) == 0 + @property def should_poll(self): - """ No polling needed. """ - return True + """ Only poll if we have statecmd. """ + return self._command_state is not None @property def name(self): @@ -93,13 +95,23 @@ class CommandSwitch(SwitchDevice): """ True if device is on. """ return self._state + def _query_state(self): + """ Query for state. """ + if not self._command_state: + _LOGGER.error('No state command specified') + return + if self._value_template: + return CommandSwitch._query_state_value(self._command_state) + return CommandSwitch._query_state_code(self._command_state) + def update(self): """ Update device state. """ - if self._command_state and self._value_template: - payload = CommandSwitch._query_state(self._command_state) - payload = template.render_with_possible_json_value( - self._hass, self._value_template, payload) - self._state = (payload == "True") + if self._command_state: + payload = str(self._query_state()) + if self._value_template: + payload = template.render_with_possible_json_value( + self._hass, self._value_template, payload) + self._state = (payload.lower() == "true") def turn_on(self, **kwargs): """ Turn the device on. """ diff --git a/tests/components/switch/test_command_switch.py b/tests/components/switch/test_command_switch.py new file mode 100644 index 00000000000..3684f78fff4 --- /dev/null +++ b/tests/components/switch/test_command_switch.py @@ -0,0 +1,158 @@ +""" +tests.components.switch.test_command_switch +~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests command switch. +""" +import json +import os +import tempfile +import unittest + +from homeassistant import core +from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.components.switch as switch + + +class TestCommandSwitch(unittest.TestCase): + """ Test the command switch. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = core.HomeAssistant() + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_state_none(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + 'value_template': '{{ value=="1" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + + def test_state_json_value(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + oncmd = json.dumps({'status': 'ok'}) + offcmd = json.dumps({'status': 'nope'}) + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo \'{}\' > {}'.format(oncmd, path), + 'offcmd': 'echo \'{}\' > {}'.format(offcmd, path), + 'value_template': '{{ value_json.status=="ok" }}' + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + def test_state_code(self): + with tempfile.TemporaryDirectory() as tempdirname: + path = os.path.join(tempdirname, 'switch_status') + test_switch = { + 'statecmd': 'cat {}'.format(path), + 'oncmd': 'echo 1 > {}'.format(path), + 'offcmd': 'echo 0 > {}'.format(path), + } + self.assertTrue(switch.setup(self.hass, { + 'switch': { + 'platform': 'command_switch', + 'switches': { + 'test': test_switch + } + } + })) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + + switch.turn_on(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) + + switch.turn_off(self.hass, 'switch.test') + self.hass.pool.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) From 6e2fb17f191047553d81c22d189d494d9d80faac Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Tue, 29 Dec 2015 17:52:05 +0100 Subject: [PATCH 04/78] Fix KeyError on 'title' when title is empty --- homeassistant/components/media_player/mpd.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index a61dac88150..9d48f1458eb 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -142,12 +142,15 @@ class MpdDevice(MediaPlayerDevice): def media_title(self): """ Title of current playing media. """ name = self.currentsong.get('name', None) - title = self.currentsong['title'] + title = self.currentsong.get('title', None) if name is None: return title else: - return '{}: {}'.format(name, title) + if title is None: + return name + else: + return '{}: {}'.format(name, title) @property def media_artist(self): From 56a2ffca1dcbc404c9a33eb8bab33f572934569c Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Tue, 29 Dec 2015 22:10:09 +0100 Subject: [PATCH 05/78] Changed if else statements. The following situations are handled now: - name and title can be None - name can be None - title can be None - name and title can contain data --- homeassistant/components/media_player/mpd.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 9d48f1458eb..c15982945a9 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -144,13 +144,14 @@ class MpdDevice(MediaPlayerDevice): name = self.currentsong.get('name', None) title = self.currentsong.get('title', None) - if name is None: + if name is None and title is None: + return "No information received from MPD" + elif name is None: return title + elif title is None: + return name else: - if title is None: - return name - else: - return '{}: {}'.format(name, title) + return '{}: {}'.format(name, title) @property def media_artist(self): From 41a36df80170b05b26f1077c13c6d4796cca4564 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 30 Dec 2015 11:54:21 +0000 Subject: [PATCH 06/78] Update pywemo version --- homeassistant/components/switch/wemo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index a343711ccc3..6057681c53c 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pywemo==0.3.4'] +REQUIREMENTS = ['pywemo==0.3.5'] _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None diff --git a/requirements_all.txt b/requirements_all.txt index a9ec467e8b1..cb623ce0920 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ hikvision==0.4 orvibo==1.1.0 # homeassistant.components.switch.wemo -pywemo==0.3.4 +pywemo==0.3.5 # homeassistant.components.tellduslive tellive-py==0.5.2 From 429904c437b38223274a95d4f2be4b113068a080 Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Wed, 30 Dec 2015 13:00:34 +0100 Subject: [PATCH 07/78] Returning None when name and title are both not available Removed trailing whitespaces --- homeassistant/components/media_player/mpd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index c15982945a9..285607360ac 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -145,11 +145,11 @@ class MpdDevice(MediaPlayerDevice): title = self.currentsong.get('title', None) if name is None and title is None: - return "No information received from MPD" + return "None" elif name is None: return title elif title is None: - return name + return name else: return '{}: {}'.format(name, title) From 913c5ab47c2e07a5b964a484c7ef1c67cf8c0365 Mon Sep 17 00:00:00 2001 From: Richard Arends Date: Wed, 30 Dec 2015 13:26:42 +0100 Subject: [PATCH 08/78] identing error... sorry --- homeassistant/components/media_player/mpd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 285607360ac..27b5aa3863c 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -145,7 +145,7 @@ class MpdDevice(MediaPlayerDevice): title = self.currentsong.get('title', None) if name is None and title is None: - return "None" + return "None" elif name is None: return title elif title is None: From b0734e613fc1849f78ab7431d2102fe343a90677 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Wed, 30 Dec 2015 13:36:47 -0500 Subject: [PATCH 09/78] Add support for deCONZ (Raspbee-GW hue-like API) - Doesn't support the none transition type, so don't send it --- homeassistant/components/light/hue.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 77672c9aaf5..29ee523dec4 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -121,10 +121,19 @@ def setup_bridge(host, hass, add_devices_callback): new_lights = [] + api_name = api.get('config').get('name') + if api_name == 'RaspBee-GW': + bridge_type = 'deconz' + _LOGGER.info("Found DeCONZ gateway (%s)", api_name) + else: + _LOGGER.info("Found Hue bridge (%s)", api_name) + bridge_type = 'hue' + for light_id, info in api_states.items(): if light_id not in lights: lights[light_id] = HueLight(int(light_id), info, - bridge, update_lights) + bridge, update_lights, + bridge_type=bridge_type) new_lights.append(lights[light_id]) else: lights[light_id].info = info @@ -163,11 +172,13 @@ def request_configuration(host, hass, add_devices_callback): class HueLight(Light): """ Represents a Hue light """ - def __init__(self, light_id, info, bridge, update_lights): + # pylint: disable=too-many-arguments + def __init__(self, light_id, info, bridge, update_lights, bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge self.update_lights = update_lights + self.bridge_type = bridge_type @property def unique_id(self): @@ -227,7 +238,7 @@ class HueLight(Light): command['alert'] = 'lselect' elif flash == FLASH_SHORT: command['alert'] = 'select' - else: + elif self.bridge_type == 'hue': command['alert'] = 'none' effect = kwargs.get(ATTR_EFFECT) @@ -237,7 +248,7 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - else: + elif self.bridge_type == 'hue': command['effect'] = 'none' self.bridge.set_light(self.light_id, command) From adfcfad48886298cfe949705969dc0e62699cf98 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 23 Dec 2015 03:52:52 -0700 Subject: [PATCH 10/78] Update locative functionality --- .../components/device_tracker/locative.py | 112 +++++++++++++----- 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 2d238992cc7..0ed97b6c4f8 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -6,12 +6,15 @@ Locative platform for the device tracker. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.locative/ """ +import logging +from functools import partial + from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR) + HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR, STATE_NOT_HOME) -DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) -_SEE = 0 +DEPENDENCIES = ['http', 'zone'] URL_API_LOCATIVE_ENDPOINT = "/api/locative" @@ -19,52 +22,97 @@ URL_API_LOCATIVE_ENDPOINT = "/api/locative" def setup_scanner(hass, config, see): """ Set up an endpoint for the Locative app. """ - # Use a global variable to keep setup_scanner compact when using a callback - global _SEE - _SEE = see - # POST would be semantically better, but that currently does not work # since Locative sends the data as key1=value1&key2=value2 # in the request body, while Home Assistant expects json there. hass.http.register_path( - 'GET', URL_API_LOCATIVE_ENDPOINT, _handle_get_api_locative) + 'GET', URL_API_LOCATIVE_ENDPOINT, + partial(_handle_get_api_locative, hass, see)) return True -def _handle_get_api_locative(handler, path_match, data): +# TODO: What happens with HA turns off? +def _handle_get_api_locative(hass, see, handler, path_match, data): """ Locative message received. """ - if not isinstance(data, dict): - handler.write_json_message( - "Error while parsing Locative message.", - HTTP_INTERNAL_SERVER_ERROR) - return - if 'latitude' not in data or 'longitude' not in data: - handler.write_json_message( - "Location not specified.", - HTTP_UNPROCESSABLE_ENTITY) - return - if 'device' not in data or 'id' not in data: - handler.write_json_message( - "Device id or location id not specified.", - HTTP_UNPROCESSABLE_ENTITY) + if not _check_data(handler, data): return + device = data['device'].replace('-', '') + location_name = data['id'] + direction = data['trigger'] + try: gps_coords = (float(data['latitude']), float(data['longitude'])) except ValueError: - # If invalid latitude / longitude format - handler.write_json_message( - "Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_json_message("Invalid latitude / longitude format.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Received invalid latitude / longitude format.") return - # entity id's in Home Assistant must be alphanumerical - device_uuid = data['device'] - device_entity_id = device_uuid.replace('-', '') + if direction == 'enter': + zones = [state for state in hass.states.entity_ids('zone')] + _LOGGER.info(zones) - _SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id']) + if "zone.{}".format(location_name.lower()) in zones: + see(dev_id=device, location_name=location_name) + handler.write_json_message("Set new location to {}".format(location_name)) + else: + see(dev_id=device, gps=gps_coords) + handler.write_json_message("Set new location to {}".format(gps_coords)) + + elif direction == 'exit': + current_zone = hass.states.get("{}.{}".format("device_tracker", device)).state + + if current_zone.lower() == location_name.lower(): + see(dev_id=device, location_name=STATE_NOT_HOME) + handler.write_json_message("Set new location to not home") + else: + # Ignore the message if it is telling us to exit a zone that we aren't + # currently in. This occurs when a zone is entered before the previous + # zone was exited. The enter message will be sent first, then the exit + # message will be sent second. + handler.write_json_message("Ignoring transition to {}".format(location_name)) + + else: + handler.write_json_message("Received unidentified message: {}".format(direction)) + _LOGGER.error("Received unidentified message from Locative: %s", + direction) + + +def _check_data(handler, data): + if not isinstance(data, dict): + handler.write_json_message("Error while parsing Locative message.", + HTTP_INTERNAL_SERVER_ERROR) + _LOGGER.error("Error while parsing Locative message: " + "data is not a dict.") + return False + + if 'latitude' not in data or 'longitude' not in data: + handler.write_json_message("Latitude and longitude not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Latitude and longitude not specified.") + return False + + if 'device' not in data: + handler.write_json_message("Device id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Device id not specified.") + return False + + if 'id' not in data: + handler.write_json_message("Location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Location id not specified.") + return False + + if 'trigger' not in data: + handler.write_json_message("Trigger is not specified.", + HTTP_UNPROCESSABLE_ENTITY) + _LOGGER.error("Trigger is not specified.") + return False + + return True - handler.write_json_message("Locative message processed") From 25e1432403f2afc9ca050fc530122f63a38c0fa7 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 30 Dec 2015 12:30:49 -0700 Subject: [PATCH 11/78] Fix style issues --- .../components/device_tracker/locative.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 0ed97b6c4f8..cb8e42fd1c4 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -33,7 +33,6 @@ def setup_scanner(hass, config, see): return True -# TODO: What happens with HA turns off? def _handle_get_api_locative(hass, see, handler, path_match, data): """ Locative message received. """ @@ -58,26 +57,31 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if "zone.{}".format(location_name.lower()) in zones: see(dev_id=device, location_name=location_name) - handler.write_json_message("Set new location to {}".format(location_name)) + handler.write_json_message( + "Set new location to {}".format(location_name)) else: see(dev_id=device, gps=gps_coords) - handler.write_json_message("Set new location to {}".format(gps_coords)) + handler.write_json_message( + "Set new location to {}".format(gps_coords)) elif direction == 'exit': - current_zone = hass.states.get("{}.{}".format("device_tracker", device)).state + current_zone = hass.states.get( + "{}.{}".format("device_tracker", device)).state if current_zone.lower() == location_name.lower(): see(dev_id=device, location_name=STATE_NOT_HOME) handler.write_json_message("Set new location to not home") else: - # Ignore the message if it is telling us to exit a zone that we aren't - # currently in. This occurs when a zone is entered before the previous - # zone was exited. The enter message will be sent first, then the exit - # message will be sent second. - handler.write_json_message("Ignoring transition to {}".format(location_name)) + # Ignore the message if it is telling us to exit a zone that we + # aren't currently in. This occurs when a zone is entered before + # the previous zone was exited. The enter message will be sent + # first, then the exit message will be sent second. + handler.write_json_message( + "Ignoring transition to {}".format(location_name)) else: - handler.write_json_message("Received unidentified message: {}".format(direction)) + handler.write_json_message( + "Received unidentified message: {}".format(direction)) _LOGGER.error("Received unidentified message from Locative: %s", direction) @@ -115,4 +119,3 @@ def _check_data(handler, data): return False return True - From ae0dbbcfa599c5670f8176d75dae03d63a466282 Mon Sep 17 00:00:00 2001 From: pavoni Date: Wed, 30 Dec 2015 19:44:02 +0000 Subject: [PATCH 12/78] Added support for event subscriptions --- homeassistant/components/light/vera.py | 17 ++++++++++--- homeassistant/components/switch/vera.py | 33 ++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 829d3cfccdb..23daba4991f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -14,6 +14,8 @@ from homeassistant.components.switch.vera import VeraSwitch from homeassistant.components.light import ATTR_BRIGHTNESS +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' '#python-vera==0.1.1'] @@ -36,10 +38,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device_data = config.get('device_data', {}) - controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: - devices = controller.get_devices([ + devices = vera_controller.get_devices([ 'Switch', 'On/Off Switch', 'Dimmable Switch']) @@ -54,7 +65,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): exclude = extra_data.get('exclude', False) if exclude is not True: - lights.append(VeraLight(device, extra_data)) + lights.append(VeraLight(device, vera_controller, extra_data)) add_devices_callback(lights) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 14983919c64..0df1c390929 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -13,7 +13,11 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME) + ATTR_BATTERY_LEVEL, + ATTR_TRIPPED, + ATTR_ARMED, + ATTR_LAST_TRIP_TIME, + EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' @@ -37,7 +41,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + devices = [] try: devices = vera_controller.get_devices([ @@ -53,7 +66,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_switches.append(VeraSwitch(device, extra_data)) + vera_switches.append( + VeraSwitch(device, vera_controller, extra_data)) return vera_switches @@ -66,9 +80,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSwitch(ToggleEntity): """ Represents a Vera Switch. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device self.extra_data = extra_data + self.controller = controller if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') else: @@ -77,6 +92,16 @@ class VeraSwitch(ToggleEntity): # for debouncing status check after command is sent self.last_command_send = 0 + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + @property def name(self): """ Get the mame of the switch. """ From 4e2d75a8f48cc7bfe3cc4de59508968488246dad Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Wed, 30 Dec 2015 16:59:22 -0500 Subject: [PATCH 13/78] fix style --- homeassistant/components/light/hue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 29ee523dec4..40875d8ea0e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -173,7 +173,8 @@ class HueLight(Light): """ Represents a Hue light """ # pylint: disable=too-many-arguments - def __init__(self, light_id, info, bridge, update_lights, bridge_type='hue'): + def __init__(self, light_id, info, bridge, update_lights, + bridge_type='hue'): self.light_id = light_id self.info = info self.bridge = bridge From c23375a18b1793dfe0e9066763c85bf45cbf255c Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 30 Dec 2015 17:30:20 -0700 Subject: [PATCH 14/78] Add case for test message --- homeassistant/components/device_tracker/locative.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index cb8e42fd1c4..72e458bc314 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -77,7 +77,12 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. handler.write_json_message( - "Ignoring transition to {}".format(location_name)) + "Ignoring transition from {}".format(location_name)) + + elif direction == 'test': + # In the app, a test message can be sent. Just return something to + # the user to let them know that it works. + handler.write_text("Received test message.") else: handler.write_json_message( From 7d41ce4e46c0f6a77218621619bd287c550a4665 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Wed, 30 Dec 2015 22:43:00 -0700 Subject: [PATCH 15/78] Switch from json messages to plain text messages --- .../components/device_tracker/locative.py | 34 +++++++++---------- homeassistant/components/http.py | 14 +++++++- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 72e458bc314..c635aa47858 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -46,8 +46,8 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): try: gps_coords = (float(data['latitude']), float(data['longitude'])) except ValueError: - handler.write_json_message("Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Invalid latitude / longitude format.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Received invalid latitude / longitude format.") return @@ -57,11 +57,11 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if "zone.{}".format(location_name.lower()) in zones: see(dev_id=device, location_name=location_name) - handler.write_json_message( + handler.write_text( "Set new location to {}".format(location_name)) else: see(dev_id=device, gps=gps_coords) - handler.write_json_message( + handler.write_text( "Set new location to {}".format(gps_coords)) elif direction == 'exit': @@ -70,13 +70,13 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if current_zone.lower() == location_name.lower(): see(dev_id=device, location_name=STATE_NOT_HOME) - handler.write_json_message("Set new location to not home") + handler.write_text("Set new location to not home") else: # Ignore the message if it is telling us to exit a zone that we # aren't currently in. This occurs when a zone is entered before # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. - handler.write_json_message( + handler.write_text( "Ignoring transition from {}".format(location_name)) elif direction == 'test': @@ -85,7 +85,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): handler.write_text("Received test message.") else: - handler.write_json_message( + handler.write_text( "Received unidentified message: {}".format(direction)) _LOGGER.error("Received unidentified message from Locative: %s", direction) @@ -93,33 +93,33 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): def _check_data(handler, data): if not isinstance(data, dict): - handler.write_json_message("Error while parsing Locative message.", - HTTP_INTERNAL_SERVER_ERROR) + handler.write_text("Error while parsing Locative message.", + HTTP_INTERNAL_SERVER_ERROR) _LOGGER.error("Error while parsing Locative message: " "data is not a dict.") return False if 'latitude' not in data or 'longitude' not in data: - handler.write_json_message("Latitude and longitude not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Latitude and longitude not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Latitude and longitude not specified.") return False if 'device' not in data: - handler.write_json_message("Device id not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Device id not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Device id not specified.") return False if 'id' not in data: - handler.write_json_message("Location id not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Location id not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Location id not specified.") return False if 'trigger' not in data: - handler.write_json_message("Trigger is not specified.", - HTTP_UNPROCESSABLE_ENTITY) + handler.write_text("Trigger is not specified.", + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Trigger is not specified.") return False diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 7a4e87de5a8..cd701c24bb6 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs import homeassistant.core as ha from homeassistant.const import ( - SERVER_PORT, CONTENT_TYPE_JSON, + SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING, HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH, HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED, @@ -293,6 +293,18 @@ class RequestHandler(SimpleHTTPRequestHandler): json.dumps(data, indent=4, sort_keys=True, cls=rem.JSONEncoder).encode("UTF-8")) + def write_text(self, message, status_code=HTTP_OK): + """ Helper method to return a text message to the caller. """ + self.send_response(status_code) + self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN) + + self.set_session_cookie_header() + + self.end_headers() + + if message is not None: + self.wfile.write(message.encode("UTF-8")) + def write_file(self, path, cache_headers=True): """ Returns a file to the user. """ try: From d82859b6ea7c790ef39cb2615ebd0d216b58d586 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 10:57:54 +0000 Subject: [PATCH 16/78] Turn off poll --- homeassistant/components/switch/vera.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 0df1c390929..52029a2c5ec 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -143,6 +143,11 @@ class VeraSwitch(ToggleEntity): self.vera_device.switch_off() self.is_on_status = False + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + @property def is_on(self): """ True if device is on. """ From a8bb75d0706030035aed9299e561382e6e3e873b Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 12:16:03 +0000 Subject: [PATCH 17/78] Update sensor with subscription code, change to use pyvera library --- homeassistant/components/light/vera.py | 4 +--- homeassistant/components/sensor/vera.py | 30 ++++++++++++++++++++----- homeassistant/components/switch/vera.py | 4 +--- requirements_all.txt | 2 +- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 23daba4991f..169fa442134 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,9 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 7fb72fd91b7..22fdffc8f1d 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,9 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -37,7 +35,16 @@ def get_devices(hass, config): device_data = config.get('device_data', {}) - vera_controller = veraApi.VeraController(base_url) + vera_controller, created = veraApi.init_controller(base_url) + + if created: + def stop_subscription(event): + """ Shutdown Vera subscriptions and subscription thread on exit""" + _LOGGER.info("Shutting down subscriptions.") + vera_controller.stop() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + categories = ['Temperature Sensor', 'Light Sensor', 'Sensor'] devices = [] try: @@ -53,7 +60,7 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, extra_data)) + vera_sensors.append(VeraSensor(device, controller, extra_data)) return vera_sensors @@ -66,8 +73,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class VeraSensor(Entity): """ Represents a Vera Sensor. """ - def __init__(self, vera_device, extra_data=None): + def __init__(self, vera_device, controller, extra_data=None): self.vera_device = vera_device + self.controller = controller self.extra_data = extra_data if self.extra_data and self.extra_data.get('name'): self._name = self.extra_data.get('name') @@ -76,6 +84,16 @@ class VeraSensor(Entity): self.current_value = '' self._temperature_units = None + self.controller.register(vera_device) + self.controller.on( + vera_device, self._update_callback) + + def _update_callback(self, _device): + """ Called by the vera device callback to update state. """ + _LOGGER.info( + 'Subscription update for %s', self.name) + self.update_ha_state(True) + def __str__(self): return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 52029a2c5ec..68a0a1d8871 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,9 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' - 'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' - '#python-vera==0.1.1'] +REQUIREMENTS = ['#pyvera==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a9ec467e8b1..387a7217f92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 +#pyvera==0.2.0 # homeassistant.components.wink # homeassistant.components.light.wink From 90ae5c6646fb9ff09f8c5dc09fa0f089a2208056 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 12:25:24 +0000 Subject: [PATCH 18/78] Add missed import, fix style error. --- homeassistant/components/sensor/vera.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 22fdffc8f1d..ccb71366df6 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -13,7 +13,7 @@ import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, - TEMP_CELCIUS, TEMP_FAHRENHEIT) + TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) REQUIREMENTS = ['#pyvera==0.2.0'] @@ -60,7 +60,8 @@ def get_devices(hass, config): exclude = extra_data.get('exclude', False) if exclude is not True: - vera_sensors.append(VeraSensor(device, controller, extra_data)) + vera_sensors.append( + VeraSensor(device, vera_controller, extra_data)) return vera_sensors From 5f89b34831e836f0b8ef60c7c264fde8b5fc41d8 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 16:09:05 +0000 Subject: [PATCH 19/78] Bump pyvera version --- homeassistant/components/light/vera.py | 2 +- homeassistant/components/sensor/vera.py | 2 +- homeassistant/components/switch/vera.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 169fa442134..2627b505ef8 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index ccb71366df6..836dfacf4f1 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 68a0a1d8871..7feea6c99c6 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.0'] +REQUIREMENTS = ['#pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 387a7217f92..156bc1657f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -#pyvera==0.2.0 +#pyvera==0.2.1 # homeassistant.components.wink # homeassistant.components.light.wink From 6773c35760e82940eaf2c5a6f27fcb39b9afbeb4 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 18:47:12 +0000 Subject: [PATCH 20/78] Bump pywemo version --- homeassistant/components/switch/wemo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 6057681c53c..ad21463ea17 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pywemo==0.3.5'] +REQUIREMENTS = ['pywemo==0.3.7'] _LOGGER = logging.getLogger(__name__) _WEMO_SUBSCRIPTION_REGISTRY = None diff --git a/requirements_all.txt b/requirements_all.txt index cb623ce0920..7aca45c6069 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ hikvision==0.4 orvibo==1.1.0 # homeassistant.components.switch.wemo -pywemo==0.3.5 +pywemo==0.3.7 # homeassistant.components.tellduslive tellive-py==0.5.2 From 55d1ad94ef8a34fb0c5e5b06e709e79a3be4d322 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 00:33:48 -0700 Subject: [PATCH 21/78] Add tests for Locative --- .coveragerc | 1 - .../device_tracker/test_locative.py | 246 ++++++++++++++++++ 2 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 tests/components/device_tracker/test_locative.py diff --git a/.coveragerc b/.coveragerc index 4b916a7fbcd..f5d1789a174 100644 --- a/.coveragerc +++ b/.coveragerc @@ -41,7 +41,6 @@ omit = homeassistant/components/device_tracker/asuswrt.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py - homeassistant/components/device_tracker/locative.py homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/luci.py homeassistant/components/device_tracker/netgear.py diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py new file mode 100644 index 00000000000..a6e5a431d36 --- /dev/null +++ b/tests/components/device_tracker/test_locative.py @@ -0,0 +1,246 @@ +""" +tests.components.device_tracker.locative +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests the locative device tracker component. +""" + +import unittest +from unittest.mock import patch + +import requests + +from homeassistant import bootstrap, const +import homeassistant.core as ha +import homeassistant.components.device_tracker as device_tracker +import homeassistant.components.http as http +import homeassistant.components.zone as zone + +SERVER_PORT = 8126 +HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) + +hass = None + + +def _url(data={}): + """ Helper method to generate urls. """ + data = "&".join(["{}={}".format(name, value) for name, value in data.items()]) + return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data) + + +@patch('homeassistant.components.http.util.get_local_ip', + return_value='127.0.0.1') +def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name + """ Initalizes a Home Assistant server. """ + global hass + + hass = ha.HomeAssistant() + + # Set up a couple of zones + bootstrap.setup_component(hass, zone.DOMAIN, { + zone.DOMAIN: [ + { + 'name': 'Home', + 'latitude': 41.7855, + 'longitude': -110.7367, + 'radius': 200 + }, + { + 'name': 'Work', + 'latitude': 41.5855, + 'longitude': -110.9367, + 'radius': 100 + } + ] + }) + + # Set up server + bootstrap.setup_component(hass, http.DOMAIN, { + http.DOMAIN: { + http.CONF_SERVER_PORT: SERVER_PORT + } + }) + + # Set up API + bootstrap.setup_component(hass, 'api') + + # Set up device tracker + bootstrap.setup_component(hass, device_tracker.DOMAIN, { + device_tracker.DOMAIN: { + 'platform': 'locative' + } + }) + + hass.start() + + +def tearDownModule(): # pylint: disable=invalid-name + """ Stops the Home Assistant server. """ + hass.stop() + + +class TestLocative(unittest.TestCase): + """ Test Locative """ + + def test_missing_data(self): + data = { + 'latitude': 1.0, + 'longitude': 1.1, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # No data + req = requests.get(_url({})) + self.assertEqual(422, req.status_code) + + # No latitude + copy = data.copy() + del copy['latitude'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No device + copy = data.copy() + del copy['device'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No location + copy = data.copy() + del copy['id'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # No trigger + copy = data.copy() + del copy['trigger'] + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # Bad longitude + copy = data.copy() + copy['longitude'] = 'hello world' + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + # Test message + copy = data.copy() + copy['trigger'] = 'test' + req = requests.get(_url(copy)) + self.assertEqual(200, req.status_code) + + # Unknown trigger + copy = data.copy() + copy['trigger'] = 'foobar' + req = requests.get(_url(copy)) + self.assertEqual(422, req.status_code) + + + def test_known_zone(self): + """ Test when there is a known zone """ + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter the Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + data['id'] = 'HOME' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') + + data['id'] = 'hOmE' + data['trigger'] = 'enter' + + # Enter Home again + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'home') + + + def test_unknown_zone(self): + """ Test when there is no known zone """ + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Foobar', + 'trigger': 'enter' + } + + # Enter Foobar + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'not_home') + self.assertEqual(state.attributes['latitude'], data['latitude']) + self.assertEqual(state.attributes['longitude'], data['longitude']) + + data['trigger'] = 'exit' + + # Exit Foobar + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'not_home') + self.assertEqual(state.attributes['latitude'], data['latitude']) + self.assertEqual(state.attributes['longitude'], data['longitude']) + + + def test_exit_after_enter(self): + """ Test when an exit message comes after an enter message """ + + data = { + 'latitude': 40.7855, + 'longitude': -111.7367, + 'device': '123', + 'id': 'Home', + 'trigger': 'enter' + } + + # Enter Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'home') + + data['id'] = 'Work' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') + + data['id'] = 'Home' + data['trigger'] = 'exit' + + # Exit Home + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + + state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) + self.assertEqual(state.state, 'work') + + print(req.text) + + From bdb6182921b0319aa347199393bd9f4494ab2c85 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 00:34:06 -0700 Subject: [PATCH 22/78] Changes to locative based on tests --- .../components/device_tracker/locative.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index c635aa47858..3263c424c74 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -40,7 +40,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): return device = data['device'].replace('-', '') - location_name = data['id'] + location_name = data['id'].lower() direction = data['trigger'] try: @@ -53,9 +53,8 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if direction == 'enter': zones = [state for state in hass.states.entity_ids('zone')] - _LOGGER.info(zones) - if "zone.{}".format(location_name.lower()) in zones: + if "zone.{}".format(location_name) in zones: see(dev_id=device, location_name=location_name) handler.write_text( "Set new location to {}".format(location_name)) @@ -68,7 +67,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): current_zone = hass.states.get( "{}.{}".format("device_tracker", device)).state - if current_zone.lower() == location_name.lower(): + if current_zone == location_name: see(dev_id=device, location_name=STATE_NOT_HOME) handler.write_text("Set new location to not home") else: @@ -77,7 +76,9 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. handler.write_text( - "Ignoring transition from {}".format(location_name)) + 'Ignoring exit from "{}". Already in "{}".'.format( + location_name, + current_zone.split('.')[-1])) elif direction == 'test': # In the app, a test message can be sent. Just return something to @@ -86,7 +87,8 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): else: handler.write_text( - "Received unidentified message: {}".format(direction)) + "Received unidentified message: {}".format(direction), + HTTP_UNPROCESSABLE_ENTITY) _LOGGER.error("Received unidentified message from Locative: %s", direction) From 1bcca8cba14f3dcac9efd41141b5141517c3be00 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 00:52:12 -0700 Subject: [PATCH 23/78] Fix problem with test --- tests/components/device_tracker/test_locative.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index a6e5a431d36..81c8152238e 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -78,11 +78,12 @@ def tearDownModule(): # pylint: disable=invalid-name """ Stops the Home Assistant server. """ hass.stop() - +# Stub out update_config or else Travis CI raises an exception +@patch('homeassistant.components.device_tracker.update_config') class TestLocative(unittest.TestCase): """ Test Locative """ - def test_missing_data(self): + def test_missing_data(self, update_config): data = { 'latitude': 1.0, 'longitude': 1.1, @@ -138,7 +139,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(422, req.status_code) - def test_known_zone(self): + def test_known_zone(self, update_config): """ Test when there is a known zone """ data = { 'latitude': 40.7855, @@ -173,7 +174,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(state_name, 'home') - def test_unknown_zone(self): + def test_unknown_zone(self, update_config): """ Test when there is no known zone """ data = { 'latitude': 40.7855, @@ -204,7 +205,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(state.attributes['longitude'], data['longitude']) - def test_exit_after_enter(self): + def test_exit_after_enter(self, update_config): """ Test when an exit message comes after an enter message """ data = { @@ -240,7 +241,3 @@ class TestLocative(unittest.TestCase): state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) self.assertEqual(state.state, 'work') - - print(req.text) - - From 5d953061e823c585a3b27ee182f45287304d27a0 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 01:43:18 -0700 Subject: [PATCH 24/78] Remove unnecessary error checking --- .../components/device_tracker/locative.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index 3263c424c74..f4fd72d0c5f 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -10,7 +10,7 @@ import logging from functools import partial from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR, STATE_NOT_HOME) + HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) _LOGGER = logging.getLogger(__name__) @@ -57,11 +57,11 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if "zone.{}".format(location_name) in zones: see(dev_id=device, location_name=location_name) handler.write_text( - "Set new location to {}".format(location_name)) + "Setting location to {}".format(location_name)) else: see(dev_id=device, gps=gps_coords) handler.write_text( - "Set new location to {}".format(gps_coords)) + "Setting location to {}".format(gps_coords)) elif direction == 'exit': current_zone = hass.states.get( @@ -69,14 +69,14 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): if current_zone == location_name: see(dev_id=device, location_name=STATE_NOT_HOME) - handler.write_text("Set new location to not home") + handler.write_text("Setting location to not home") else: # Ignore the message if it is telling us to exit a zone that we # aren't currently in. This occurs when a zone is entered before # the previous zone was exited. The enter message will be sent # first, then the exit message will be sent second. handler.write_text( - 'Ignoring exit from "{}". Already in "{}".'.format( + 'Ignoring exit from {} (already in {})'.format( location_name, current_zone.split('.')[-1])) @@ -94,13 +94,6 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): def _check_data(handler, data): - if not isinstance(data, dict): - handler.write_text("Error while parsing Locative message.", - HTTP_INTERNAL_SERVER_ERROR) - _LOGGER.error("Error while parsing Locative message: " - "data is not a dict.") - return False - if 'latitude' not in data or 'longitude' not in data: handler.write_text("Latitude and longitude not specified.", HTTP_UNPROCESSABLE_ENTITY) From f8e5df237bb98fabce2330d7a99b9b021f2e7139 Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 18:58:12 +0000 Subject: [PATCH 25/78] Remove '#'' from requirements --- homeassistant/components/light/vera.py | 2 +- homeassistant/components/sensor/vera.py | 2 +- homeassistant/components/switch/vera.py | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 2627b505ef8..9135323fb1f 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import EVENT_HOMEASSISTANT_STOP -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index 836dfacf4f1..db283c51633 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -15,7 +15,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/vera.py b/homeassistant/components/switch/vera.py index 7feea6c99c6..614b588f36f 100644 --- a/homeassistant/components/switch/vera.py +++ b/homeassistant/components/switch/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_LAST_TRIP_TIME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['#pyvera==0.2.1'] +REQUIREMENTS = ['pyvera==0.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 156bc1657f1..b32d49dcc74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -59,7 +59,7 @@ tellcore-py==1.1.2 # homeassistant.components.light.vera # homeassistant.components.sensor.vera # homeassistant.components.switch.vera -#pyvera==0.2.1 +pyvera==0.2.1 # homeassistant.components.wink # homeassistant.components.light.wink From 9e0946b207c2d2fb9d69c864424768cfd79d528a Mon Sep 17 00:00:00 2001 From: pavoni Date: Thu, 31 Dec 2015 19:15:21 +0000 Subject: [PATCH 26/78] Turn off polling for sensor too! --- homeassistant/components/sensor/vera.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index db283c51633..03b8d05d2f5 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -136,6 +136,11 @@ class VeraSensor(Entity): attr['Vera Device Id'] = self.vera_device.vera_device_id return attr + @property + def should_poll(self): + """ Tells Home Assistant not to poll this entity. """ + return False + def update(self): if self.vera_device.category == "Temperature Sensor": self.vera_device.refresh_value('CurrentTemperature') From ce152e9c94a5212f60f1c5560f8b7f36d9586eb5 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 12:02:50 -0700 Subject: [PATCH 27/78] Simplify logic --- .../components/device_tracker/locative.py | 32 +++------- .../device_tracker/test_locative.py | 62 ++++--------------- 2 files changed, 20 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/device_tracker/locative.py b/homeassistant/components/device_tracker/locative.py index f4fd72d0c5f..e7532d1075d 100644 --- a/homeassistant/components/device_tracker/locative.py +++ b/homeassistant/components/device_tracker/locative.py @@ -11,10 +11,11 @@ from functools import partial from homeassistant.const import ( HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME) +from homeassistant.components.device_tracker import DOMAIN _LOGGER = logging.getLogger(__name__) -DEPENDENCIES = ['http', 'zone'] +DEPENDENCIES = ['http'] URL_API_LOCATIVE_ENDPOINT = "/api/locative" @@ -43,31 +44,15 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): location_name = data['id'].lower() direction = data['trigger'] - try: - gps_coords = (float(data['latitude']), float(data['longitude'])) - except ValueError: - handler.write_text("Invalid latitude / longitude format.", - HTTP_UNPROCESSABLE_ENTITY) - _LOGGER.error("Received invalid latitude / longitude format.") - return - if direction == 'enter': - zones = [state for state in hass.states.entity_ids('zone')] - - if "zone.{}".format(location_name) in zones: - see(dev_id=device, location_name=location_name) - handler.write_text( - "Setting location to {}".format(location_name)) - else: - see(dev_id=device, gps=gps_coords) - handler.write_text( - "Setting location to {}".format(gps_coords)) + see(dev_id=device, location_name=location_name) + handler.write_text("Setting location to {}".format(location_name)) elif direction == 'exit': - current_zone = hass.states.get( - "{}.{}".format("device_tracker", device)).state + current_state = hass.states.get( + "{}.{}".format(DOMAIN, device)).state - if current_zone == location_name: + if current_state == location_name: see(dev_id=device, location_name=STATE_NOT_HOME) handler.write_text("Setting location to not home") else: @@ -77,8 +62,7 @@ def _handle_get_api_locative(hass, see, handler, path_match, data): # first, then the exit message will be sent second. handler.write_text( 'Ignoring exit from {} (already in {})'.format( - location_name, - current_zone.split('.')[-1])) + location_name, current_state)) elif direction == 'test': # In the app, a test message can be sent. Just return something to diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 81c8152238e..b86f24455de 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -36,24 +36,6 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name hass = ha.HomeAssistant() - # Set up a couple of zones - bootstrap.setup_component(hass, zone.DOMAIN, { - zone.DOMAIN: [ - { - 'name': 'Home', - 'latitude': 41.7855, - 'longitude': -110.7367, - 'radius': 200 - }, - { - 'name': 'Work', - 'latitude': 41.5855, - 'longitude': -110.9367, - 'radius': 100 - } - ] - }) - # Set up server bootstrap.setup_component(hass, http.DOMAIN, { http.DOMAIN: { @@ -120,12 +102,6 @@ class TestLocative(unittest.TestCase): req = requests.get(_url(copy)) self.assertEqual(422, req.status_code) - # Bad longitude - copy = data.copy() - copy['longitude'] = 'hello world' - req = requests.get(_url(copy)) - self.assertEqual(422, req.status_code) - # Test message copy = data.copy() copy['trigger'] = 'test' @@ -139,7 +115,7 @@ class TestLocative(unittest.TestCase): self.assertEqual(422, req.status_code) - def test_known_zone(self, update_config): + def test_enter_and_exit(self, update_config): """ Test when there is a known zone """ data = { 'latitude': 40.7855, @@ -173,36 +149,22 @@ class TestLocative(unittest.TestCase): state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state self.assertEqual(state_name, 'home') - - def test_unknown_zone(self, update_config): - """ Test when there is no known zone """ - data = { - 'latitude': 40.7855, - 'longitude': -111.7367, - 'device': '123', - 'id': 'Foobar', - 'trigger': 'enter' - } - - # Enter Foobar - req = requests.get(_url(data)) - self.assertEqual(200, req.status_code) - - state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) - self.assertEqual(state.state, 'not_home') - self.assertEqual(state.attributes['latitude'], data['latitude']) - self.assertEqual(state.attributes['longitude'], data['longitude']) - data['trigger'] = 'exit' - # Exit Foobar + # Exit Home req = requests.get(_url(data)) self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'not_home') - state = hass.states.get('{}.{}'.format('device_tracker', data['device'])) - self.assertEqual(state.state, 'not_home') - self.assertEqual(state.attributes['latitude'], data['latitude']) - self.assertEqual(state.attributes['longitude'], data['longitude']) + data['id'] = 'work' + data['trigger'] = 'enter' + + # Enter Work + req = requests.get(_url(data)) + self.assertEqual(200, req.status_code) + state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state + self.assertEqual(state_name, 'work') def test_exit_after_enter(self, update_config): From 394c87c40b23eeb50d293a31bb9655d0c32dda43 Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Thu, 31 Dec 2015 13:05:24 -0700 Subject: [PATCH 28/78] Remove unnecessary condition in write_text --- homeassistant/components/http.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index cd701c24bb6..b7f57b0157e 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -302,8 +302,7 @@ class RequestHandler(SimpleHTTPRequestHandler): self.end_headers() - if message is not None: - self.wfile.write(message.encode("UTF-8")) + self.wfile.write(message.encode("UTF-8")) def write_file(self, path, cache_headers=True): """ Returns a file to the user. """ From 11f32d050028c6cf396b71811e025b064926ff56 Mon Sep 17 00:00:00 2001 From: Andrew Thigpen Date: Thu, 31 Dec 2015 14:58:18 -0600 Subject: [PATCH 29/78] Add is_state_attr method. Returns True if the entity exists and has an attribute with the given name and value. --- homeassistant/core.py | 7 +++++++ homeassistant/util/template.py | 3 ++- tests/test_core.py | 12 ++++++++++++ tests/util/test_template.py | 8 ++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index e2650969eb0..55ceddb37c7 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -468,6 +468,13 @@ class StateMachine(object): return (entity_id in self._states and self._states[entity_id].state == state) + def is_state_attr(self, entity_id, name, value): + """Test if entity exists and has a state attribute set to value.""" + entity_id = entity_id.lower() + + return (entity_id in self._states and + self._states[entity_id].attributes.get(name, None) == value) + def remove(self, entity_id): """Remove the state of an entity. diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index d0a07507bdf..bc7431ebf6d 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs): try: return ENV.from_string(template, { 'states': AllStates(hass), - 'is_state': hass.states.is_state + 'is_state': hass.states.is_state, + 'is_state_attr': hass.states.is_state_attr }).render(kwargs).strip() except jinja2.TemplateError as err: raise TemplateError(err) diff --git a/tests/test_core.py b/tests/test_core.py index fee46fe2dd4..ca935e2d106 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -321,6 +321,18 @@ class TestStateMachine(unittest.TestCase): self.assertFalse(self.states.is_state('light.Bowl', 'off')) self.assertFalse(self.states.is_state('light.Non_existing', 'on')) + def test_is_state_attr(self): + """ Test is_state_attr method. """ + self.states.set("light.Bowl", "on", {"brightness": 100}) + self.assertTrue( + self.states.is_state_attr('light.Bowl', 'brightness', 100)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 200)) + self.assertFalse( + self.states.is_state_attr('light.Bowl', 'friendly_name', 'Bowl')) + self.assertFalse( + self.states.is_state_attr('light.Non_existing', 'brightness', 100)) + def test_entity_ids(self): """ Test get_entity_ids method. """ ent_ids = self.states.entity_ids() diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 1ecd7d5b894..844826f80d5 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -117,6 +117,14 @@ class TestUtilTemplate(unittest.TestCase): self.hass, '{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}')) + def test_is_state_attr(self): + self.hass.states.set('test.object', 'available', {'mode': 'on'}) + self.assertEqual( + 'yes', + template.render( + self.hass, + '{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}')) + def test_states_function(self): self.hass.states.set('test.object', 'available') self.assertEqual( From 9c85702c875b4934595de0b9e2625be2949bc728 Mon Sep 17 00:00:00 2001 From: happyleaves Date: Thu, 31 Dec 2015 18:39:40 -0500 Subject: [PATCH 30/78] combine ifs --- .../components/switch/command_switch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch/command_switch.py b/homeassistant/components/switch/command_switch.py index c36ca4e9ce9..a90ed61c3e2 100644 --- a/homeassistant/components/switch/command_switch.py +++ b/homeassistant/components/switch/command_switch.py @@ -115,14 +115,14 @@ class CommandSwitch(SwitchDevice): def turn_on(self, **kwargs): """ Turn the device on. """ - if CommandSwitch._switch(self._command_on): - if not self._command_state: - self._state = True - self.update_ha_state() + if (CommandSwitch._switch(self._command_on) and + not self._command_state): + self._state = True + self.update_ha_state() def turn_off(self, **kwargs): """ Turn the device off. """ - if CommandSwitch._switch(self._command_off): - if not self._command_state: - self._state = False - self.update_ha_state() + if (CommandSwitch._switch(self._command_off) and + not self._command_state): + self._state = False + self.update_ha_state() From c703c89dbd0fea0b10dbddcf5f02d587c467f1a3 Mon Sep 17 00:00:00 2001 From: sander Date: Fri, 1 Jan 2016 15:29:58 +0100 Subject: [PATCH 31/78] implement away mode --- .../components/thermostat/honeywell.py | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 4139c5d8aa7..15162a4d279 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -6,8 +6,9 @@ Adds support for Honeywell Round Connected and Honeywell Evohome thermostats. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.honeywell/ """ -import socket import logging +import socket + from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) @@ -15,6 +16,7 @@ REQUIREMENTS = ['evohomeclient==0.2.4'] _LOGGER = logging.getLogger(__name__) +CONF_AWAY_TEMP = "away_temperature" # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): @@ -23,20 +25,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - + try: + away_temp = float(config.get(CONF_AWAY_TEMP,16)) + except ValueError: + _LOGGER.error("value entered for item {} should convert to a number" + .format(CONF_AWAY_TEMP)) if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", CONF_USERNAME, CONF_PASSWORD) return False evo_api = EvohomeClient(username, password) + try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0)]) + add_devices([RoundThermostat(evo_api, zone['id'], i == 0,away_temp)]) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" + "Connection error logging into the honeywell evohome web service" ) return False @@ -44,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ - def __init__(self, device, zone_id, master): + def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None self._target_temperature = None @@ -52,6 +59,8 @@ class RoundThermostat(ThermostatDevice): self._id = zone_id self._master = master self._is_dhw = False + self._away_temp = away_temp + self._away = False self.update() @property @@ -80,6 +89,25 @@ class RoundThermostat(ThermostatDevice): """ Set new target temperature """ self.device.set_temperature(self._name, temperature) + @property + def is_away_mode_on(self): + """ Returns if away mode is on. """ + return self._away + + def turn_away_mode_on(self): + """ Turns away on. + Evohome does have a proprietary away mode, but it doesn't really work + the way it should. For example: If you set a temperature manually + it doesn't get overwritten when away mode is switched on. + """ + self._away = True + self.device.set_temperature(self._name, self._away_temp) + + def turn_away_mode_off(self): + """ Turns away off. """ + self._away = False + self.device.cancel_temp_override(self._name) + def update(self): try: # Only refresh if this is the "master" device, From a36ae4b24af3d4903203abe206aebfbc2734be2c Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Sat, 2 Jan 2016 01:01:11 -0500 Subject: [PATCH 32/78] Reduce chatiness --- homeassistant/components/light/hue.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 40875d8ea0e..61bff75bbd3 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -124,9 +124,7 @@ def setup_bridge(host, hass, add_devices_callback): api_name = api.get('config').get('name') if api_name == 'RaspBee-GW': bridge_type = 'deconz' - _LOGGER.info("Found DeCONZ gateway (%s)", api_name) else: - _LOGGER.info("Found Hue bridge (%s)", api_name) bridge_type = 'hue' for light_id, info in api_states.items(): From 8f2ca856c7bec90b55f788dc97ea09bd1c621259 Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 11:56:07 +0100 Subject: [PATCH 33/78] added return False --- homeassistant/components/thermostat/honeywell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 15162a4d279..de6d5ff6a56 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -18,6 +18,7 @@ _LOGGER = logging.getLogger(__name__) CONF_AWAY_TEMP = "away_temperature" + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """ Sets up the honeywel thermostat. """ @@ -26,10 +27,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) try: - away_temp = float(config.get(CONF_AWAY_TEMP,16)) + away_temp = float(config.get(CONF_AWAY_TEMP, 16)) except ValueError: _LOGGER.error("value entered for item {} should convert to a number" .format(CONF_AWAY_TEMP)) + return False if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", CONF_USERNAME, CONF_PASSWORD) @@ -40,7 +42,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0,away_temp)]) + add_devices([RoundThermostat(evo_api, zone['id'], i == 0, away_temp)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" From cdf2179b3e09c50e21d49314da5cea36ad7ecd4a Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 2 Jan 2016 10:54:26 -0600 Subject: [PATCH 34/78] Describe device tracker see service --- .../components/device_tracker/services.yaml | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index e69de29bb2d..63907f7457b 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -0,0 +1,33 @@ +# Describes the format for available device tracker services + +see: + description: Control tracked device + + fields: + mac: + description: MAC address of device + example: 'FF:FF:FF:FF:FF:FF' + + dev_id: + description: Id of device (find id in tracked_devices.yaml) + example: 'phonedave' + + host_name: + description: Hostname of device + example: 'Dave' + + location_name: + description: Name of location where device is located (not_home is away) + example: 'home' + + gps: + description: GPS coordinates where device is located (latitude, longitude) + example: '[51.509802, -0.086692]' + + gps_accuracy: + description: Accuracy of GPS coordinates + example: '80' + + battery: + description: Battery level of device + example: '100' From e9b2cf1600ff97d8ee3d069d15d24559ba0e9f05 Mon Sep 17 00:00:00 2001 From: Joseph Hughes Date: Fri, 1 Jan 2016 08:30:59 -0700 Subject: [PATCH 35/78] Ensure we send data to influx as float and not as a string value. --- homeassistant/components/influxdb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 2286dd2d659..7cbba00afbb 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -7,7 +7,6 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/influxdb/ """ import logging - import homeassistant.util as util from homeassistant.helpers import validate_config from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF, @@ -77,6 +76,10 @@ def setup(hass, config): _state = 0 else: _state = state.state + try: + _state = float(_state) + except ValueError: + pass measurement = state.attributes.get('unit_of_measurement', state.domain) From 5804dde0e979fe097e10c95fe785e69195902007 Mon Sep 17 00:00:00 2001 From: xifle Date: Sat, 2 Jan 2016 18:26:59 +0100 Subject: [PATCH 36/78] Enables the use of owntracks transition events By using the configuration option "use_events:yes" in the device_tracker section, only 'enter'/'leave' events are considered to calculate the state of a tracker device. The home zone is defined as the owntracks region 'home'. Other regions may also be defined, the name of the region is then used as state for the device. All owntracks regions, the 'Share' setting must be enabled in the app. --- .../components/device_tracker/owntracks.py | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index b98c3a1636c..a1818e60901 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,6 +1,6 @@ """ homeassistant.components.device_tracker.owntracks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OwnTracks platform for the device tracker. For more details about this platform, please refer to the documentation at @@ -10,14 +10,16 @@ import json import logging import homeassistant.components.mqtt as mqtt +from homeassistant.const import (STATE_HOME, STATE_NOT_HOME) DEPENDENCIES = ['mqtt'] +CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' - +EVENT_TOPIC = 'owntracks/+/+/event' def setup_scanner(hass, config, see): - """ Set up a OwnTracksks tracker. """ + """ Set up an OwnTracks tracker. """ def owntracks_location_update(topic, payload, qos): """ MQTT message received. """ @@ -47,7 +49,58 @@ def setup_scanner(hass, config, see): kwargs['battery'] = data['batt'] see(**kwargs) + + + def owntracks_event_update(topic, payload, qos): + """ MQTT event (geofences) received. """ - mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) + # Docs on available data: + # http://owntracks.org/booklet/tech/json/#_typetransition + try: + data = json.loads(payload) + except ValueError: + # If invalid JSON + logging.getLogger(__name__).error( + 'Unable to parse payload as JSON: %s', payload) + return + + if not isinstance(data, dict) or data.get('_type') != 'transition': + return + + + # check if in "home" fence or other zone + location = '' + if data['event'] == 'enter': + + if data['desc'] == 'home': + location = STATE_HOME + else: + location = data['desc'] + + elif data['event'] == 'leave': + location = STATE_NOT_HOME + else: + logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', data['event']) + return + + parts = topic.split('/') + kwargs = { + 'dev_id': '{}_{}'.format(parts[1], parts[2]), + 'host_name': parts[1], + 'gps': (data['lat'], data['lon']), + 'location_name': location, + } + if 'acc' in data: + kwargs['gps_accuracy'] = data['acc'] + + see(**kwargs) + + + use_events = config.get(CONF_TRANSITION_EVENTS) + + if use_events: + mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) + else: + mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) return True From 217ffc215ba346a1c9f87b92da6eb862e3d52de0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 10:27:11 -0800 Subject: [PATCH 37/78] Update PyNetgear version --- homeassistant/components/device_tracker/netgear.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 5d20e98e992..ab1eccba769 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pynetgear==0.3'] +REQUIREMENTS = ['pynetgear==0.3.1'] def get_scanner(hass, config): diff --git a/requirements_all.txt b/requirements_all.txt index 18d308f6d5c..997cbb567e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -16,7 +16,7 @@ fuzzywuzzy==0.8.0 pyicloud==0.7.2 # homeassistant.components.device_tracker.netgear -pynetgear==0.3 +pynetgear==0.3.1 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.4.3 From 39de92960d040da71ccfe3b716be54f2ef8ad39f Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:27:40 +0100 Subject: [PATCH 38/78] line too long change --- homeassistant/components/thermostat/honeywell.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index de6d5ff6a56..4d1a7fe708d 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -42,7 +42,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: zones = evo_api.temperatures(force_refresh=True) for i, zone in enumerate(zones): - add_devices([RoundThermostat(evo_api, zone['id'], i == 0, away_temp)]) + add_devices([RoundThermostat(evo_api, + zone['id'], + i == 0, + away_temp)]) except socket.error: _LOGGER.error( "Connection error logging into the honeywell evohome web service" From 8c7898ed054fc10c07f2c02bd093f54768be615b Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:53:25 +0100 Subject: [PATCH 39/78] pylinting.. --- homeassistant/components/thermostat/honeywell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 4d1a7fe708d..491fe57900e 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -48,7 +48,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): away_temp)]) except socket.error: _LOGGER.error( - "Connection error logging into the honeywell evohome web service" + "Connection error logging into the honeywell evohome web service" ) return False From 36f5caa214215a51b29870a60aa1fda4d7e333ba Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 20:59:45 +0100 Subject: [PATCH 40/78] more pylinting.. --- homeassistant/components/thermostat/honeywell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 491fe57900e..0b7479bd202 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -29,8 +29,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: away_temp = float(config.get(CONF_AWAY_TEMP, 16)) except ValueError: - _LOGGER.error("value entered for item {} should convert to a number" - .format(CONF_AWAY_TEMP)) + _LOGGER.error("value entered for item %s should convert to a number", + CONF_AWAY_TEMP) return False if username is None or password is None: _LOGGER.error("Missing required configuration items %s or %s", From 55c5d254d58e6902667ea712dc9eb0d820b258db Mon Sep 17 00:00:00 2001 From: sander Date: Sat, 2 Jan 2016 21:09:03 +0100 Subject: [PATCH 41/78] some more pylinting.. --- homeassistant/components/thermostat/honeywell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/thermostat/honeywell.py b/homeassistant/components/thermostat/honeywell.py index 0b7479bd202..5475e1ce306 100644 --- a/homeassistant/components/thermostat/honeywell.py +++ b/homeassistant/components/thermostat/honeywell.py @@ -56,6 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RoundThermostat(ThermostatDevice): """ Represents a Honeywell Round Connected thermostat. """ + # pylint: disable=too-many-instance-attributes def __init__(self, device, zone_id, master, away_temp): self.device = device self._current_temperature = None From 0361f37178bdc3a409718f5d73e254512925817a Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Sat, 2 Jan 2016 16:13:58 -0500 Subject: [PATCH 42/78] Support multiple hue hubs using a filename parameter. --- homeassistant/components/light/hue.py | 15 ++++++++------- homeassistant/const.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 61bff75bbd3..b828847ecb0 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -17,7 +17,7 @@ from urllib.parse import urlparse from homeassistant.loader import get_component import homeassistant.util as util import homeassistant.util.color as color_util -from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME +from homeassistant.const import CONF_HOST, CONF_FILENAME, DEVICE_DEFAULT_NAME from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, @@ -35,9 +35,9 @@ _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -def _find_host_from_config(hass): +def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): """ Attempt to detect host based on existing configuration. """ - path = hass.config.path(PHUE_CONFIG_FILE) + path = hass.config.path(filename) if not os.path.isfile(path): return None @@ -54,13 +54,14 @@ def _find_host_from_config(hass): def setup_platform(hass, config, add_devices_callback, discovery_info=None): """ Gets the Hue lights. """ + filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE) if discovery_info is not None: host = urlparse(discovery_info[1]).hostname else: host = config.get(CONF_HOST, None) if host is None: - host = _find_host_from_config(hass) + host = _find_host_from_config(hass, filename) if host is None: _LOGGER.error('No host found in configuration') @@ -70,17 +71,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): if host in _CONFIGURING: return - setup_bridge(host, hass, add_devices_callback) + setup_bridge(host, hass, add_devices_callback, filename) -def setup_bridge(host, hass, add_devices_callback): +def setup_bridge(host, hass, add_devices_callback, filename): """ Setup a phue bridge based on host parameter. """ import phue try: bridge = phue.Bridge( host, - config_file_path=hass.config.path(PHUE_CONFIG_FILE)) + config_file_path=hass.config.path(filename)) except ConnectionRefusedError: # Wrong host was given _LOGGER.exception("Error connecting to the Hue bridge at %s", host) diff --git a/homeassistant/const.py b/homeassistant/const.py index 82276d81b48..97e26f8d33a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,6 +24,7 @@ CONF_USERNAME = "username" CONF_PASSWORD = "password" CONF_API_KEY = "api_key" CONF_ACCESS_TOKEN = "access_token" +CONF_FILENAME = "filename" CONF_VALUE_TEMPLATE = "value_template" From 7edbb6aadc081475f521f6dfbc02dbb6f79ac3fb Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:21:04 +0100 Subject: [PATCH 43/78] Added rain sensor --- homeassistant/components/sensor/tellduslive.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 7cc49e3c611..dbf416787a2 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -22,10 +22,14 @@ DEPENDENCIES = ['tellduslive'] SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_RAINRATE = "rrate" +SENSOR_TYPE_RAINTOTAL = "rtot" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], } From 86047eceb1ce21f1d8ac7621396305792163d277 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:28:15 +0100 Subject: [PATCH 44/78] Added wind sensor --- homeassistant/components/sensor/tellduslive.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index dbf416787a2..022ae00a098 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -24,12 +24,18 @@ SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_RAINRATE = "rrate" SENSOR_TYPE_RAINTOTAL = "rtot" +SENSOR_TYPE_WINDDIRECTION = "wdir" +SENSOR_TYPE_WINDAVERAGE = "wavg" +SENSOR_TYPE_WINDGUST = "wgust" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], } From 4a421e25b08f453feba08ad78c4865c2abcc4790 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 13:29:33 -0800 Subject: [PATCH 45/78] Simplify Rest sensors --- .../components/binary_sensor/rest.py | 113 ++++-------------- homeassistant/components/sensor/rest.py | 80 +++---------- 2 files changed, 38 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 6cb6ede5e50..4d82d25e473 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.rest/ """ -from datetime import timedelta import logging -import requests from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.util import template, Throttle +from homeassistant.util import template +from homeassistant.components.sensor.rest import RestData from homeassistant.components.binary_sensor import BinarySensorDevice _LOGGER = logging.getLogger(__name__) @@ -19,60 +18,33 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_METHOD = 'GET' -# Return cached results if last scan was less then this time ago -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) - # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): - """ Get the REST binary sensor. """ - - use_get = False - use_post = False - + """Setup REST binary sensors.""" resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error("Response status is '%s'", response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// or https:// to your URL") - return False - except requests.exceptions.ConnectionError: - _LOGGER.error('No route to resource/endpoint: %s', resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestBinarySensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestBinarySensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments class RestBinarySensor(BinarySensorDevice): - """ Implements a REST binary sensor. """ + """REST binary sensor.""" def __init__(self, hass, rest, name, value_template): + """Initialize a REST binary sensor.""" self._hass = hass self.rest = rest self._name = name @@ -82,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice): @property def name(self): - """ The name of the binary sensor. """ + """Name of the binary sensor.""" return self._name @property def is_on(self): - """ True if the binary sensor is on. """ - if self.rest.data is False: + """Return if the binary sensor is on.""" + if self.rest.data is None: return False - else: - if self._value_template is not None: - self.rest.data = template.render_with_possible_json_value( - self._hass, self._value_template, self.rest.data, False) - return bool(int(self.rest.data)) + + if self._value_template is not None: + self.rest.data = template.render_with_possible_json_value( + self._hass, self._value_template, self.rest.data, False) + return bool(int(self.rest.data)) def update(self): - """ Gets the latest data from REST API and updates the state. """ + """Get the latest data from REST API and updates the state.""" self.rest.update() - - -# pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ - - def __init__(self, resource, verify_ssl): - self._resource = resource - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with GET method. """ - try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = False - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = False diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index f6a56d3a99e..fdbc1ab26e3 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -26,47 +26,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) # pylint: disable=unused-variable def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the REST sensor. """ - - use_get = False - use_post = False - resource = config.get('resource', None) method = config.get('method', DEFAULT_METHOD) payload = config.get('payload', None) verify_ssl = config.get('verify_ssl', True) - if method == 'GET': - use_get = True - elif method == 'POST': - use_post = True + rest = RestData(method, resource, payload, verify_ssl) + rest.update() - try: - if use_get: - response = requests.get(resource, timeout=10, verify=verify_ssl) - elif use_post: - response = requests.post(resource, data=payload, timeout=10, - verify=verify_ssl) - if not response.ok: - _LOGGER.error("Response status is '%s'", response.status_code) - return False - except requests.exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// or https:// to your URL") - return False - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", resource) + if rest.data is None: + _LOGGER.error('Unable to fetch Rest data') return False - if use_get: - rest = RestDataGet(resource, verify_ssl) - elif use_post: - rest = RestDataPost(resource, payload, verify_ssl) - - add_devices([RestSensor(hass, - rest, - config.get('name', DEFAULT_NAME), - config.get('unit_of_measurement'), - config.get(CONF_VALUE_TEMPLATE))]) + add_devices([RestSensor( + hass, rest, config.get('name', DEFAULT_NAME), + config.get('unit_of_measurement'), config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments @@ -112,11 +86,11 @@ class RestSensor(Entity): # pylint: disable=too-few-public-methods -class RestDataGet(object): - """ Class for handling the data retrieval with GET method. """ +class RestData(object): + """Class for handling the data retrieval.""" - def __init__(self, resource, verify_ssl): - self._resource = resource + def __init__(self, method, resource, data, verify_ssl): + self._request = requests.Request(method, resource, data=data).prepare() self._verify_ssl = verify_ssl self.data = None @@ -124,31 +98,11 @@ class RestDataGet(object): def update(self): """ Gets the latest data from REST service with GET method. """ try: - response = requests.get(self._resource, timeout=10, - verify=self._verify_ssl) + with requests.Session() as sess: + response = sess.send(self._request, timeout=10, + verify=self._verify_ssl) + self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) - self.data = None - - -# pylint: disable=too-few-public-methods -class RestDataPost(object): - """ Class for handling the data retrieval with POST method. """ - - def __init__(self, resource, payload, verify_ssl): - self._resource = resource - self._payload = payload - self._verify_ssl = verify_ssl - self.data = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """ Gets the latest data from REST service with POST method. """ - try: - response = requests.post(self._resource, data=self._payload, - timeout=10, verify=self._verify_ssl) - self.data = response.text - except requests.exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint: %s", self._resource) + except requests.exceptions.RequestException: + _LOGGER.error("Error fetching data: %s", self._request) self.data = None From 3abc78eef2c678fe6f0df27f89dfe3ceff997ae9 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sat, 2 Jan 2016 22:30:02 +0100 Subject: [PATCH 46/78] Added power sensor --- homeassistant/components/sensor/tellduslive.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 022ae00a098..a7bfca49b30 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -27,6 +27,7 @@ SENSOR_TYPE_RAINTOTAL = "rtot" SENSOR_TYPE_WINDDIRECTION = "wdir" SENSOR_TYPE_WINDAVERAGE = "wavg" SENSOR_TYPE_WINDGUST = "wgust" +SENSOR_TYPE_WATT = "watt" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], @@ -36,6 +37,7 @@ SENSOR_TYPES = { SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], + SENSOR_TYPE_WATT: ['Watt', 'W', ""], } From 305c87a9c9753d27f8f003e494009dab7188bdb7 Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 2 Jan 2016 16:01:58 -0600 Subject: [PATCH 47/78] Fix reference known_devices.yaml --- homeassistant/components/device_tracker/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 63907f7457b..dc573ae0275 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -9,7 +9,7 @@ see: example: 'FF:FF:FF:FF:FF:FF' dev_id: - description: Id of device (find id in tracked_devices.yaml) + description: Id of device (find id in known_devices.yaml) example: 'phonedave' host_name: From 7dc1499386dd8e45b58ff79a35b0c702baee782f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Jan 2016 14:23:12 -0800 Subject: [PATCH 48/78] Make CI erros more prominent --- .travis.yml | 4 +--- script/cibuild | 22 ++++++++++++++++++++++ script/test | 14 +++++++------- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4383d49f548..c01b0750360 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,7 @@ python: - 3.4 - 3.5 install: - # Validate requirements_all.txt on Python 3.4 - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi - - script/bootstrap_server + - "true" script: - script/cibuild matrix: diff --git a/script/cibuild b/script/cibuild index beb7b22693d..11d91415405 100755 --- a/script/cibuild +++ b/script/cibuild @@ -5,6 +5,28 @@ cd "$(dirname "$0")/.." +if [ "$TRAVIS_PYTHON_VERSION" = "3.5" ]; then + echo "Verifying requirements_all.txt..." + python3 setup.py -q develop 2> /dev/null + tput setaf 1 + script/gen_requirements_all.py validate + VERIFY_REQUIREMENTS_STATUS=$? + tput sgr0 +else + VERIFY_REQUIREMENTS_STATUS=0 +fi + +if [ "$VERIFY_REQUIREMENTS_STATUS" != "0" ]; then + exit $VERIFY_REQUIREMENTS_STATUS +fi + +script/bootstrap_server > /dev/null +DEP_INSTALL_STATUS=$? + +if [ "$DEP_INSTALL_STATUS" != "0" ]; then + exit $DEP_INSTALL_STATUS +fi + if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then NO_LINT=1 fi diff --git a/script/test b/script/test index 25873492001..6a78ce42d41 100755 --- a/script/test +++ b/script/test @@ -5,13 +5,6 @@ cd "$(dirname "$0")/.." -if [ "$NO_LINT" = "1" ]; then - LINT_STATUS=0 -else - script/lint - LINT_STATUS=$? -fi - echo "Running tests..." if [ "$1" = "coverage" ]; then @@ -22,6 +15,13 @@ else TEST_STATUS=$? fi +if [ "$NO_LINT" = "1" ]; then + LINT_STATUS=0 +else + script/lint + LINT_STATUS=$? +fi + if [ $LINT_STATUS -eq 0 ] then exit $TEST_STATUS From 4b96a7c8202470999a862940a337da412eda4a86 Mon Sep 17 00:00:00 2001 From: Ronny Eia Date: Sun, 3 Jan 2016 01:00:10 +0100 Subject: [PATCH 49/78] Untabified lines --- homeassistant/components/sensor/tellduslive.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index a7bfca49b30..ae05ce47e19 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -32,12 +32,12 @@ SENSOR_TYPE_WATT = "watt" SENSOR_TYPES = { SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], - SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], - SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], - SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], - SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], - SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], - SENSOR_TYPE_WATT: ['Watt', 'W', ""], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""], + SENSOR_TYPE_WATT: ['Watt', 'W', ""], } From c9ff0ab7eb0ef8e52089d1d16699f6235739f52b Mon Sep 17 00:00:00 2001 From: Philip Lundrigan Date: Sun, 3 Jan 2016 00:36:22 -0700 Subject: [PATCH 50/78] Fix for sun if condition --- homeassistant/components/automation/sun.py | 10 ++-- tests/components/automation/test_sun.py | 61 ++++++++++++++++++---- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 394dc904be1..064f6a0a16a 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -82,21 +82,21 @@ def if_action(hass, config): if before is None: before_func = lambda: None elif before == EVENT_SUNRISE: - before_func = lambda: sun.next_rising_utc(hass) + before_offset + before_func = lambda: sun.next_rising(hass) + before_offset else: - before_func = lambda: sun.next_setting_utc(hass) + before_offset + before_func = lambda: sun.next_setting(hass) + before_offset if after is None: after_func = lambda: None elif after == EVENT_SUNRISE: - after_func = lambda: sun.next_rising_utc(hass) + after_offset + after_func = lambda: sun.next_rising(hass) + after_offset else: - after_func = lambda: sun.next_setting_utc(hass) + after_offset + after_func = lambda: sun.next_setting(hass) + after_offset def time_if(): """ Validate time based if-condition """ - now = dt_util.utcnow() + now = dt_util.now() before = before_func() after = after_func() diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 26ecc26c72a..db4782cfd46 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -162,14 +162,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -197,14 +197,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -233,14 +233,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -269,14 +269,14 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() @@ -306,21 +306,60 @@ class TestAutomationSun(unittest.TestCase): }) now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) - with patch('homeassistant.components.automation.sun.dt_util.utcnow', + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_action_after_different_tz(self): + import pytz + + self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, { + sun.STATE_ATTR_NEXT_SETTING: '17:30:00 16-09-2015', + }) + + automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + 'condition': { + 'platform': 'sun', + 'after': 'sunset', + }, + 'action': { + 'service': 'test.automation' + } + } + }) + + # Before + now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', + return_value=now): + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + # After + now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain')) + with patch('homeassistant.components.automation.sun.dt_util.now', return_value=now): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() From f8b2570cb3a8df1349f60ffa6393f65aa97d9f82 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 02:32:09 -0800 Subject: [PATCH 51/78] Group entities when reproducing a state --- homeassistant/helpers/state.py | 35 +++++---- tests/helpers/test_state.py | 129 +++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 tests/helpers/test_state.py diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 24a37c5b5ea..019e7ce6ce9 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,9 +1,5 @@ -""" -homeassistant.helpers.state -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Helpers that help with state related things. -""" +"""Helpers that help with state related things.""" +from collections import defaultdict import logging from homeassistant.core import State @@ -25,32 +21,36 @@ class TrackStates(object): that have changed since the start time to the return list when with-block is exited. """ + def __init__(self, hass): + """Initialize a TrackStates block.""" self.hass = hass self.states = [] def __enter__(self): + """Record time from which to track changes.""" self.now = dt_util.utcnow() return self.states def __exit__(self, exc_type, exc_value, traceback): + """Add changes states to changes list.""" self.states.extend(get_changed_since(self.hass.states.all(), self.now)) def get_changed_since(states, utc_point_in_time): - """ - Returns all states that have been changed since utc_point_in_time. - """ + """List of states that have been changed since utc_point_in_time.""" point_in_time = dt_util.strip_microseconds(utc_point_in_time) return [state for state in states if state.last_updated >= point_in_time] def reproduce_state(hass, states, blocking=False): - """ Takes in a state and will try to have the entity reproduce it. """ + """Reproduce given state.""" if isinstance(states, State): states = [states] + to_call = defaultdict(list) + for state in states: current_state = hass.states.get(state.entity_id) @@ -76,7 +76,16 @@ def reproduce_state(hass, states, blocking=False): state) continue - service_data = dict(state.attributes) - service_data[ATTR_ENTITY_ID] = state.entity_id + if state.domain == 'group': + service_domain = 'homeassistant' + else: + service_domain = state.domain - hass.services.call(state.domain, service, service_data, blocking) + # We group service calls for entities by service call + key = (service_domain, service, tuple(state.attributes.items())) + to_call[key].append(state.entity_id) + + for (service_domain, service, service_data), entity_ids in to_call.items(): + data = dict(service_data) + data[ATTR_ENTITY_ID] = entity_ids + hass.services.call(service_domain, service, data, blocking) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py new file mode 100644 index 00000000000..32924b1d6d5 --- /dev/null +++ b/tests/helpers/test_state.py @@ -0,0 +1,129 @@ +""" +tests.helpers.test_state +~~~~~~~~~~~~~~~~~~~~~~~~ + +Test state helpers. +""" +from datetime import timedelta +import unittest +from unittest.mock import patch + +import homeassistant.core as ha +import homeassistant.components as core_components +from homeassistant.const import SERVICE_TURN_ON +from homeassistant.util import dt as dt_util +from homeassistant.helpers import state + +from tests.common import get_test_home_assistant, mock_service + + +class TestStateHelpers(unittest.TestCase): + """ + Tests the Home Assistant event helpers. + """ + + def setUp(self): # pylint: disable=invalid-name + """ things to be run when tests are started. """ + self.hass = get_test_home_assistant() + core_components.setup(self.hass, {}) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_get_changed_since(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow', return_value=point1): + self.hass.states.set('light.test', 'on') + state1 = self.hass.states.get('light.test') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point2): + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + with patch('homeassistant.core.dt_util.utcnow', return_value=point3): + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + [state2, state3], + state.get_changed_since([state1, state2, state3], point2)) + + def test_track_states(self): + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) + + with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow: + mock_utcnow.return_value = point2 + + with state.TrackStates(self.hass) as states: + mock_utcnow.return_value = point1 + self.hass.states.set('light.test', 'on') + + mock_utcnow.return_value = point2 + self.hass.states.set('light.test2', 'on') + state2 = self.hass.states.get('light.test2') + + mock_utcnow.return_value = point3 + self.hass.states.set('light.test3', 'on') + state3 = self.hass.states.get('light.test3') + + self.assertEqual( + sorted([state2, state3], key=lambda state: state.entity_id), + sorted(states, key=lambda state: state.entity_id)) + + def test_reproduce_state_with_turn_on(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + state.reproduce_state(self.hass, ha.State('light.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test'], last_call.data.get('entity_id')) + + def test_reproduce_state_with_group(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('group.test', 'off', { + 'entity_id': ['light.test1', 'light.test2']}) + + state.reproduce_state(self.hass, ha.State('group.test', 'on')) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + + def test_reproduce_state_group_states_with_same_domain_and_data(self): + light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test1', 'off') + self.hass.states.set('light.test2', 'off') + + state.reproduce_state(self.hass, [ + ha.State('light.test1', 'on', {'brightness': 95}), + ha.State('light.test2', 'on', {'brightness': 95})]) + + self.hass.pool.block_till_done() + + self.assertEqual(1, len(light_calls)) + last_call = light_calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(['light.test1', 'light.test2'], + last_call.data.get('entity_id')) + self.assertEqual(95, last_call.data.get('brightness')) From 82904c59ce4abaa7b47048f551611c8ceab067f9 Mon Sep 17 00:00:00 2001 From: xifle Date: Sun, 3 Jan 2016 17:12:11 +0100 Subject: [PATCH 52/78] Fixed code style --- .../components/device_tracker/owntracks.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index a1818e60901..c20b50e7e8c 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -49,8 +49,8 @@ def setup_scanner(hass, config, see): kwargs['battery'] = data['batt'] see(**kwargs) - - + + def owntracks_event_update(topic, payload, qos): """ MQTT event (geofences) received. """ @@ -67,20 +67,21 @@ def setup_scanner(hass, config, see): if not isinstance(data, dict) or data.get('_type') != 'transition': return - + # check if in "home" fence or other zone location = '' if data['event'] == 'enter': - + if data['desc'] == 'home': location = STATE_HOME else: location = data['desc'] - + elif data['event'] == 'leave': location = STATE_NOT_HOME else: - logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', data['event']) + logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', + data['event']) return parts = topic.split('/') @@ -94,10 +95,10 @@ def setup_scanner(hass, config, see): kwargs['gps_accuracy'] = data['acc'] see(**kwargs) - - + + use_events = config.get(CONF_TRANSITION_EVENTS) - + if use_events: mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1) else: From d244d3b59958da19697548987574d411cd992acb Mon Sep 17 00:00:00 2001 From: xifle Date: Sun, 3 Jan 2016 17:42:49 +0100 Subject: [PATCH 53/78] Fixed flake8 style errors --- homeassistant/components/device_tracker/owntracks.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/owntracks.py b/homeassistant/components/device_tracker/owntracks.py index c20b50e7e8c..e81952eb770 100644 --- a/homeassistant/components/device_tracker/owntracks.py +++ b/homeassistant/components/device_tracker/owntracks.py @@ -1,6 +1,6 @@ """ homeassistant.components.device_tracker.owntracks -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OwnTracks platform for the device tracker. For more details about this platform, please refer to the documentation at @@ -18,6 +18,7 @@ CONF_TRANSITION_EVENTS = 'use_events' LOCATION_TOPIC = 'owntracks/+/+' EVENT_TOPIC = 'owntracks/+/+/event' + def setup_scanner(hass, config, see): """ Set up an OwnTracks tracker. """ @@ -50,7 +51,6 @@ def setup_scanner(hass, config, see): see(**kwargs) - def owntracks_event_update(topic, payload, qos): """ MQTT event (geofences) received. """ @@ -67,7 +67,6 @@ def setup_scanner(hass, config, see): if not isinstance(data, dict) or data.get('_type') != 'transition': return - # check if in "home" fence or other zone location = '' if data['event'] == 'enter': @@ -80,8 +79,9 @@ def setup_scanner(hass, config, see): elif data['event'] == 'leave': location = STATE_NOT_HOME else: - logging.getLogger(__name__).error('Misformatted mqtt msgs, _type=transition, event=%s', - data['event']) + logging.getLogger(__name__).error( + 'Misformatted mqtt msgs, _type=transition, event=%s', + data['event']) return parts = topic.split('/') @@ -96,7 +96,6 @@ def setup_scanner(hass, config, see): see(**kwargs) - use_events = config.get(CONF_TRANSITION_EVENTS) if use_events: From 736183e6f5c09f08a5ae202832cb03a25a54198d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 11:27:30 -0800 Subject: [PATCH 54/78] Fix bug in reproduce_state with complex state attributes --- homeassistant/helpers/state.py | 7 +++++-- tests/helpers/test_state.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 019e7ce6ce9..c8f6f05661a 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,5 +1,6 @@ """Helpers that help with state related things.""" from collections import defaultdict +import json import logging from homeassistant.core import State @@ -82,10 +83,12 @@ def reproduce_state(hass, states, blocking=False): service_domain = state.domain # We group service calls for entities by service call - key = (service_domain, service, tuple(state.attributes.items())) + # json used to create a hashable version of dict with maybe lists in it + key = (service_domain, service, + json.dumps(state.attributes, sort_keys=True)) to_call[key].append(state.entity_id) for (service_domain, service, service_data), entity_ids in to_call.items(): - data = dict(service_data) + data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids hass.services.call(service_domain, service, data, blocking) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 32924b1d6d5..f4e28330f7a 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -91,6 +91,25 @@ class TestStateHelpers(unittest.TestCase): self.assertEqual(SERVICE_TURN_ON, last_call.service) self.assertEqual(['light.test'], last_call.data.get('entity_id')) + def test_reproduce_state_with_complex_service_data(self): + calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + + self.hass.states.set('light.test', 'off') + + complex_data = ['hello', {'11': '22'}] + + state.reproduce_state(self.hass, ha.State('light.test', 'on', { + 'complex': complex_data + })) + + self.hass.pool.block_till_done() + + self.assertTrue(len(calls) > 0) + last_call = calls[-1] + self.assertEqual('light', last_call.domain) + self.assertEqual(SERVICE_TURN_ON, last_call.service) + self.assertEqual(complex_data, last_call.data.get('complex')) + def test_reproduce_state_with_group(self): light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) From 08aabd18add15ec8a12477b2b80d0ebe4de671d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jan 2016 11:44:26 -0800 Subject: [PATCH 55/78] New version frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 26 +++++++++---------- .../www_static/home-assistant-polymer | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 64845a350ca..2ded702dc6b 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -1,2 +1,2 @@ """ DO NOT MODIFY. Auto-generated by build_frontend script """ -VERSION = "be08c5a3ce12040bbdba2db83cb1a568" +VERSION = "72a8220d0db0f7f3702228cd556b8c40" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 8df0a4724a0..edc9635dbf4 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -2134,7 +2134,7 @@ case"touchend":return this.addPointerListenerEnd(t,e,i,n);case"touchmove":return }