From 623b023ac07a261d8f94f4d9809d8d4fd0332b90 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Apr 2016 18:44:27 -0700 Subject: [PATCH 001/101] Version bump to 0.19.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7c32c8e886b..77e540cd76f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" -__version__ = "0.18.0" +__version__ = "0.19.0.dev0" REQUIRED_PYTHON_VER = (3, 4) PLATFORM_FORMAT = '{}.{}' From 4745282a95f5304f8e2c4caf4721502a414b6cdf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Apr 2016 19:00:12 -0700 Subject: [PATCH 002/101] Update underline of header README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 69856586be0..de07e635fd4 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| -=========================================================================================================== +================================================================================================================== Home Assistant is a home automation platform running on Python 3. The goal of Home Assistant is to be able to track and control all devices at From dfa37511adcffe7129e409034c7e65962ca4d058 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Thu, 21 Apr 2016 16:51:31 +0200 Subject: [PATCH 003/101] Improve recognition for zwave thermostats --- homeassistant/components/zwave.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 0c6eba45106..24f269847dd 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -53,7 +53,7 @@ COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_METER = 50 COMMAND_CLASS_BATTERY = 128 COMMAND_CLASS_ALARM = 113 # 0x71 -COMMAND_CLASS_THERMOSTAT_MODE = 64 # 0x40 +COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43 GENRE_WHATEVER = None GENRE_USER = "User" @@ -91,7 +91,7 @@ DISCOVERY_COMPONENTS = [ GENRE_USER), ('thermostat', DISCOVER_THERMOSTATS, - [COMMAND_CLASS_THERMOSTAT_MODE], + [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, GENRE_WHATEVER), ] From 43a94995c28e7af04fe974b49dcf6ea9aa8e426f Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 21 Apr 2016 10:56:19 -0400 Subject: [PATCH 004/101] Update unifi version New unifi version has a fix that will allow it to install correctly --- homeassistant/components/device_tracker/unifi.py | 3 ++- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index fbfaef83e81..3bff25edc5e 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -12,7 +12,8 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import validate_config # Unifi package doesn't list urllib3 as a requirement -REQUIREMENTS = ['urllib3', 'unifi==1.2.4'] +REQUIREMENTS = ['urllib3', 'unifi==1.2.5'] + _LOGGER = logging.getLogger(__name__) CONF_PORT = 'port' diff --git a/requirements_all.txt b/requirements_all.txt index 8b3caf538d1..297bc01c127 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -316,7 +316,7 @@ transmissionrpc==0.11 uber_rides==0.2.1 # homeassistant.components.device_tracker.unifi -unifi==1.2.4 +unifi==1.2.5 # homeassistant.components.device_tracker.unifi urllib3 From d3fb69783d1f599756bf38f1aec3686710866a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 21 Apr 2016 16:57:28 +0200 Subject: [PATCH 005/101] Tellstick config validation --- homeassistant/components/light/tellstick.py | 4 ++++ homeassistant/components/switch/tellstick.py | 4 ++++ homeassistant/components/tellstick.py | 12 ++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/tellstick.py b/homeassistant/components/light/tellstick.py index 3571bd9737d..67d8c243ebc 100644 --- a/homeassistant/components/light/tellstick.py +++ b/homeassistant/components/light/tellstick.py @@ -4,12 +4,16 @@ Support for Tellstick lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.tellstick/ """ +import voluptuous as vol + from homeassistant.components import tellstick from homeassistant.components.light import ATTR_BRIGHTNESS, Light from homeassistant.components.tellstick import (DEFAULT_SIGNAL_REPETITIONS, ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG) +PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/switch/tellstick.py b/homeassistant/components/switch/tellstick.py index 8014244e828..a0cc4294b23 100644 --- a/homeassistant/components/switch/tellstick.py +++ b/homeassistant/components/switch/tellstick.py @@ -4,11 +4,15 @@ Support for Tellstick switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.tellstick/ """ +import voluptuous as vol + from homeassistant.components import tellstick from homeassistant.components.tellstick import (ATTR_DISCOVER_DEVICES, ATTR_DISCOVER_CONFIG) from homeassistant.helpers.entity import ToggleEntity +PLATFORM_SCHEMA = vol.Schema({vol.Required("platform"): tellstick.DOMAIN}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/tellstick.py b/homeassistant/components/tellstick.py index 04927d9e652..8bb9d6a53f0 100644 --- a/homeassistant/components/tellstick.py +++ b/homeassistant/components/tellstick.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/Tellstick/ """ import logging import threading +import voluptuous as vol from homeassistant import bootstrap from homeassistant.const import ( @@ -39,6 +40,14 @@ TELLSTICK_LOCK = threading.Lock() # Used from entities that register callback listeners TELLCORE_REGISTRY = None +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(ATTR_SIGNAL_REPETITIONS, + default=DEFAULT_SIGNAL_REPETITIONS): + vol.Coerce(int), + }), +}, extra=vol.ALLOW_EXTRA) + def _discover(hass, config, found_devices, component_name): """Setup and send the discovery event.""" @@ -52,8 +61,7 @@ def _discover(hass, config, found_devices, component_name): bootstrap.setup_component(hass, component.DOMAIN, config) - signal_repetitions = config[DOMAIN].get( - ATTR_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS) + signal_repetitions = config[DOMAIN].get(ATTR_SIGNAL_REPETITIONS) hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {ATTR_SERVICE: DISCOVERY_TYPES[component_name], From 3318c55c6539100ac53c45b8d6f9c9e0d81c6be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 21 Apr 2016 16:59:35 +0200 Subject: [PATCH 006/101] Heat control config validation * heat control configuration validation * fix heat contol test --- .../components/thermostat/heat_control.py | 25 ++++++++++++------- .../thermostat/test_heat_control.py | 17 +++++++++---- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/thermostat/heat_control.py b/homeassistant/components/thermostat/heat_control.py index 12472b61d02..64f95c2e517 100644 --- a/homeassistant/components/thermostat/heat_control.py +++ b/homeassistant/components/thermostat/heat_control.py @@ -5,7 +5,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/thermostat.heat_control/ """ import logging +import voluptuous as vol +import homeassistant.helpers.config_validation as cv import homeassistant.util as util from homeassistant.components import switch from homeassistant.components.thermostat import ( @@ -28,21 +30,26 @@ CONF_TARGET_TEMP = 'target_temp' _LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = vol.Schema({ + vol.Required("platform"): "heat_control", + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HEATER): cv.entity_id, + vol.Required(CONF_SENSOR): cv.entity_id, + vol.Optional(CONF_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_TARGET_TEMP): vol.Coerce(float), +}) + # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the heat control thermostat.""" - name = config.get(CONF_NAME, DEFAULT_NAME) + name = config.get(CONF_NAME) heater_entity_id = config.get(CONF_HEATER) sensor_entity_id = config.get(CONF_SENSOR) - min_temp = util.convert(config.get(CONF_MIN_TEMP), float, None) - max_temp = util.convert(config.get(CONF_MAX_TEMP), float, None) - target_temp = util.convert(config.get(CONF_TARGET_TEMP), float, None) - - if None in (heater_entity_id, sensor_entity_id): - _LOGGER.error('Missing required key %s or %s', CONF_HEATER, - CONF_SENSOR) - return False + min_temp = config.get(CONF_MIN_TEMP) + max_temp = config.get(CONF_MAX_TEMP) + target_temp = config.get(CONF_TARGET_TEMP) add_devices([HeatControl(hass, name, heater_entity_id, sensor_entity_id, min_temp, max_temp, target_temp)]) diff --git a/tests/components/thermostat/test_heat_control.py b/tests/components/thermostat/test_heat_control.py index 7ef23be85bb..ca3572d1710 100644 --- a/tests/components/thermostat/test_heat_control.py +++ b/tests/components/thermostat/test_heat_control.py @@ -1,7 +1,7 @@ """The tests for the heat control thermostat.""" import unittest -from unittest import mock +from homeassistant.bootstrap import _setup_component from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, SERVICE_TURN_OFF, @@ -11,7 +11,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.components import thermostat -import homeassistant.components.thermostat.heat_control as heat_control from tests.common import get_test_home_assistant @@ -41,9 +40,17 @@ class TestSetupThermostatHeatControl(unittest.TestCase): 'name': 'test', 'target_sensor': ENT_SENSOR } - add_devices = mock.MagicMock() - result = heat_control.setup_platform(self.hass, config, add_devices) - self.assertEqual(False, result) + self.assertFalse(_setup_component(self.hass, 'thermostat', { + 'thermostat': config})) + + def test_valid_conf(self): + """Test set up heat_control with valid config values.""" + self.assertTrue(_setup_component(self.hass, 'thermostat', + {'thermostat': { + 'platform': 'heat_control', + 'name': 'test', + 'heater': ENT_SWITCH, + 'target_sensor': ENT_SENSOR}})) def test_setup_with_sensor(self): """Test set up heat_control with sensor to trigger update at init.""" From 4acb12168979b77785b0b7c9e8b19bf5f8acdcbf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 12:22:19 -0700 Subject: [PATCH 007/101] Allow variables in service.call_from_config --- homeassistant/helpers/config_validation.py | 8 +-- homeassistant/helpers/service.py | 76 ++++++++++------------ tests/helpers/test_service.py | 22 +++++++ 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 923d071231a..51684e5f1cd 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -231,8 +231,8 @@ EVENT_SCHEMA = vol.Schema({ SERVICE_SCHEMA = vol.All(vol.Schema({ vol.Exclusive('service', 'service name'): service, - vol.Exclusive('service_template', 'service name'): string, - vol.Exclusive('data', 'service data'): dict, - vol.Exclusive('data_template', 'service data'): {match_all: template}, - 'entity_id': entity_ids, + vol.Exclusive('service_template', 'service name'): template, + vol.Optional('data'): dict, + vol.Optional('data_template'): {match_all: template}, + vol.Optional('entity_id'): entity_ids, }), has_at_least_one_key('service', 'service_template')) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 8f366352532..8b89d856c50 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -2,9 +2,13 @@ import functools import logging +import voluptuous as vol + from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import TemplateError from homeassistant.helpers import template from homeassistant.loader import get_component +import homeassistant.helpers.config_validation as cv HASS = None @@ -28,47 +32,38 @@ def service(domain, service_name): return register_service_decorator -def call_from_config(hass, config, blocking=False): +def call_from_config(hass, config, blocking=False, variables=None): """Call a service based on a config hash.""" - validation_error = validate_service_call(config) - if validation_error: - _LOGGER.error(validation_error) - return - - domain_service = ( - config[CONF_SERVICE] - if CONF_SERVICE in config - else template.render(hass, config[CONF_SERVICE_TEMPLATE])) - try: - domain, service_name = domain_service.split('.', 1) - except ValueError: - _LOGGER.error('Invalid service specified: %s', domain_service) + config = cv.SERVICE_SCHEMA(config) + except vol.Invalid as ex: + _LOGGER.error("Invalid config for calling service: %s", ex) return - service_data = config.get(CONF_SERVICE_DATA) - - if service_data is None: - service_data = {} - elif isinstance(service_data, dict): - service_data = dict(service_data) + if CONF_SERVICE in config: + domain_service = config[CONF_SERVICE] else: - _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) - service_data = {} + try: + domain_service = template.render( + hass, config[CONF_SERVICE_TEMPLATE], variables) + domain_service = cv.service(domain_service) + except TemplateError as ex: + _LOGGER.error('Error rendering service name template: %s', ex) + return + except vol.Invalid as ex: + _LOGGER.error('Template rendered invalid service: %s', + domain_service) + return - service_data_template = config.get(CONF_SERVICE_DATA_TEMPLATE) - if service_data_template and isinstance(service_data_template, dict): - for key, value in service_data_template.items(): - service_data[key] = template.render(hass, value) - elif service_data_template: - _LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA) + domain, service_name = domain_service.split('.', 1) + service_data = dict(config.get(CONF_SERVICE_DATA, {})) - entity_id = config.get(CONF_SERVICE_ENTITY_ID) - if isinstance(entity_id, str): - service_data[ATTR_ENTITY_ID] = [ent.strip() for ent in - entity_id.split(",")] - elif entity_id is not None: - service_data[ATTR_ENTITY_ID] = entity_id + if CONF_SERVICE_DATA_TEMPLATE in config: + for key, value in config[CONF_SERVICE_DATA_TEMPLATE].items(): + service_data[key] = template.render(hass, value, variables) + + if CONF_SERVICE_ENTITY_ID in config: + service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] hass.services.call(domain, service_name, service_data, blocking) @@ -98,11 +93,8 @@ def validate_service_call(config): Helper method to validate that a configuration is a valid service call. Returns None if validation succeeds, else an error description """ - if not isinstance(config, dict): - return 'Invalid configuration {}'.format(config) - if CONF_SERVICE not in config and CONF_SERVICE_TEMPLATE not in config: - return 'Missing key {} or {}: {}'.format( - CONF_SERVICE, - CONF_SERVICE_TEMPLATE, - config) - return None + try: + cv.SERVICE_SCHEMA(config) + return None + except vol.Invalid as ex: + return str(ex) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index be224b51ff0..59ba1781ab2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import patch +import homeassistant.components # noqa - to prevent circular import from homeassistant import core as ha, loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import service @@ -53,6 +54,27 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual('goodbye', runs[0].data['hello']) + def test_passing_variables_to_templates(self): + config = { + 'service_template': '{{ var_service }}', + 'entity_id': 'hello.world', + 'data_template': { + 'hello': '{{ var_data }}', + }, + } + runs = [] + + decor = service.service('test_domain', 'test_service') + decor(lambda x, y: runs.append(y)) + + service.call_from_config(self.hass, config, variables={ + 'var_service': 'test_domain.test_service', + 'var_data': 'goodbye', + }) + self.hass.pool.block_till_done() + + self.assertEqual('goodbye', runs[0].data['hello']) + def test_split_entity_string(self): """Test splitting of entity string.""" service.call_from_config(self.hass, { From c4913a87e42df6057842dc2071149770d10ff370 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 12:27:23 -0700 Subject: [PATCH 008/101] Alexa: Expose intent variables to service calls --- homeassistant/components/alexa.py | 2 +- tests/components/test_alexa.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 806d6874a8d..bb9e1816a68 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -91,7 +91,7 @@ def _handle_alexa(handler, path_match, data): card['content']) if action is not None: - call_from_config(handler.server.hass, action, True) + call_from_config(handler.server.hass, action, True, response.variables) handler.write_json(response.as_dict()) diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index b004ab642ed..03fa5c2d33c 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -71,8 +71,8 @@ def setUpModule(): # pylint: disable=invalid-name }, 'action': { 'service': 'test.alexa', - 'data': { - 'hello': 1 + 'data_template': { + 'hello': '{{ ZodiacSign }}' }, 'entity_id': 'switch.test', } @@ -278,6 +278,12 @@ class TestAlexa(unittest.TestCase): 'timestamp': '2015-05-13T12:34:56Z', 'intent': { 'name': 'CallServiceIntent', + 'slots': { + 'ZodiacSign': { + 'name': 'ZodiacSign', + 'value': 'virgo', + } + } } } } @@ -289,7 +295,7 @@ class TestAlexa(unittest.TestCase): self.assertEqual('test', call.domain) self.assertEqual('alexa', call.service) self.assertEqual(['switch.test'], call.data.get('entity_id')) - self.assertEqual(1, call.data.get('hello')) + self.assertEqual('virgo', call.data.get('hello')) def test_session_ended_request(self): """Test the request for ending the session.""" From 4e568f8b99e45d1642887d4770708ea28a33aa18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 13:59:42 -0700 Subject: [PATCH 009/101] Automation: Add trigger context and expose to action --- .../components/automation/__init__.py | 20 ++++----- homeassistant/components/automation/event.py | 7 ++- homeassistant/components/automation/mqtt.py | 9 +++- .../components/automation/numeric_state.py | 33 +++++++++++--- homeassistant/components/automation/state.py | 43 ++++++++++++------- homeassistant/components/automation/sun.py | 16 +++++-- .../components/automation/template.py | 23 +++++++--- homeassistant/components/automation/time.py | 9 +++- homeassistant/components/automation/zone.py | 23 +++++++--- homeassistant/helpers/event.py | 7 ++- tests/components/automation/test_init.py | 7 ++- tests/components/automation/test_mqtt.py | 10 ++++- .../automation/test_numeric_state.py | 17 +++++++- tests/components/automation/test_state.py | 14 +++++- tests/components/automation/test_sun.py | 6 +++ tests/components/automation/test_template.py | 13 +++++- tests/components/automation/test_time.py | 7 ++- tests/components/automation/test_zone.py | 10 +++++ tests/helpers/test_event.py | 24 ++++++++++- tests/helpers/test_service.py | 3 +- 20 files changed, 232 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 7e13ae3ed75..8cbaf35a5c4 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -122,12 +122,11 @@ def _setup_automation(hass, config_block, name, config): def _get_action(hass, config, name): """Return an action based on a configuration.""" - def action(): + def action(variables=None): """Action to be executed.""" _LOGGER.info('Executing %s', name) logbook.log_entry(hass, name, 'has been triggered', DOMAIN) - - call_from_config(hass, config) + call_from_config(hass, config, variables=variables) return action @@ -159,24 +158,21 @@ def _process_if(hass, config, p_config, action): checks.append(check) if cond_type == CONDITION_TYPE_AND: - def if_action(): + def if_action(variables=None): """AND all conditions.""" - if all(check() for check in checks): - action() + if all(check(variables) for check in checks): + action(variables) else: - def if_action(): + def if_action(variables=None): """OR all conditions.""" - if any(check() for check in checks): - action() + if any(check(variables) for check in checks): + action(variables) return if_action def _process_trigger(hass, config, trigger_configs, name, action): """Setup the triggers.""" - if isinstance(trigger_configs, dict): - trigger_configs = [trigger_configs] - for conf in trigger_configs: platform = _resolve_platform(METHOD_TRIGGER, hass, config, conf.get(CONF_PLATFORM)) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 80dd6c29f6b..46b5b4ef10d 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -26,7 +26,12 @@ def trigger(hass, config, action): """Listen for events and calls the action when data matches.""" if not event_data or all(val == event.data.get(key) for key, val in event_data.items()): - action() + action({ + 'trigger': { + 'platform': 'event', + 'event': event, + }, + }) hass.bus.listen(event_type, handle_event) return True diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index db0c1be7c2a..e4a6b221e04 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -30,7 +30,14 @@ def trigger(hass, config, action): def mqtt_automation_listener(msg_topic, msg_payload, qos): """Listen for MQTT messages.""" if payload is None or payload == msg_payload: - action() + action({ + 'trigger': { + 'platform': 'mqtt', + 'topic': msg_topic, + 'payload': msg_payload, + 'qos': qos, + } + }) mqtt.subscribe(hass, topic, mqtt_automation_listener) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 74f5b3ba805..6ed2add0b25 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -18,12 +18,15 @@ CONF_ABOVE = "above" _LOGGER = logging.getLogger(__name__) -def _renderer(hass, value_template, state): +def _renderer(hass, value_template, state, variables=None): """Render the state value.""" if value_template is None: return state.state - return template.render(hass, value_template, {'state': state}) + variables = dict(variables or {}) + variables['state'] = state + + return template.render(hass, value_template, variables) def trigger(hass, config, action): @@ -50,9 +53,27 @@ def trigger(hass, config, action): def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" # Fire action if we go from outside range into range - if _in_range(above, below, renderer(to_s)) and \ - (from_s is None or not _in_range(above, below, renderer(from_s))): - action() + if to_s is None: + return + + variables = { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': entity_id, + 'below': below, + 'above': above, + } + } + to_s_value = renderer(to_s, variables) + from_s_value = None if from_s is None else renderer(from_s, variables) + if _in_range(above, below, to_s_value) and \ + (from_s is None or not _in_range(above, below, from_s_value)): + variables['trigger']['from_state'] = from_s + variables['trigger']['from_value'] = from_s_value + variables['trigger']['to_state'] = to_s + variables['trigger']['to_value'] = to_s_value + + action(variables) track_state_change( hass, entity_id, state_automation_listener) @@ -80,7 +101,7 @@ def if_action(hass, config): renderer = partial(_renderer, hass, value_template) - def if_numeric_state(): + def if_numeric_state(variables): """Test numeric state condition.""" state = hass.states.get(entity_id) return state is not None and _in_range(above, below, renderer(state)) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 742e6195949..802debbe63e 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -73,29 +73,42 @@ def trigger(hass, config, action): def state_automation_listener(entity, from_s, to_s): """Listen for state changes and calls action.""" + def call_action(): + """Call action with right context.""" + action({ + 'trigger': { + 'platform': 'state', + 'entity_id': entity, + 'from_state': from_s, + 'to_state': to_s, + 'for': time_delta, + } + }) + + if time_delta is None: + call_action() + return + def state_for_listener(now): """Fire on state changes after a delay and calls action.""" hass.bus.remove_listener( - EVENT_STATE_CHANGED, for_state_listener) - action() + EVENT_STATE_CHANGED, attached_state_for_cancel_listener) + call_action() def state_for_cancel_listener(entity, inner_from_s, inner_to_s): """Fire on changes and cancel for listener if changed.""" if inner_to_s == to_s: return - hass.bus.remove_listener(EVENT_TIME_CHANGED, for_time_listener) - hass.bus.remove_listener( - EVENT_STATE_CHANGED, for_state_listener) + hass.bus.remove_listener(EVENT_TIME_CHANGED, + attached_state_for_listener) + hass.bus.remove_listener(EVENT_STATE_CHANGED, + attached_state_for_cancel_listener) - if time_delta is not None: - target_tm = dt_util.utcnow() + time_delta - for_time_listener = track_point_in_time( - hass, state_for_listener, target_tm) - for_state_listener = track_state_change( - hass, entity_id, state_for_cancel_listener, - MATCH_ALL, MATCH_ALL) - else: - action() + attached_state_for_listener = track_point_in_time( + hass, state_for_listener, dt_util.utcnow() + time_delta) + + attached_state_for_cancel_listener = track_state_change( + hass, entity_id, state_for_cancel_listener) track_state_change( hass, entity_id, state_automation_listener, from_state, to_state) @@ -109,7 +122,7 @@ def if_action(hass, config): state = config.get(CONF_STATE) time_delta = get_time_config(config) - def if_state(): + def if_state(variables): """Test if condition.""" is_state = hass.states.is_state(entity_id, state) return (time_delta is None and is_state or diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 2a564a3b588..c9db88a83c2 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -55,11 +55,21 @@ def trigger(hass, config, action): event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) + def call_action(): + """Call action with right context.""" + action({ + 'trigger': { + 'platform': 'sun', + 'event': event, + 'offset': offset, + }, + }) + # Do something to call action if event == EVENT_SUNRISE: - track_sunrise(hass, action, offset) + track_sunrise(hass, call_action, offset) else: - track_sunset(hass, action, offset) + track_sunset(hass, call_action, offset) return True @@ -97,7 +107,7 @@ def if_action(hass, config): """Return time after sunset.""" return sun.next_setting(hass) + after_offset - def time_if(): + def time_if(variables): """Validate time based if-condition.""" now = dt_util.now() before = before_func() diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 02e8f30d209..66c20518c7e 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -9,9 +9,10 @@ import logging import voluptuous as vol from homeassistant.const import ( - CONF_VALUE_TEMPLATE, EVENT_STATE_CHANGED, CONF_PLATFORM) + CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template +from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv @@ -30,7 +31,7 @@ def trigger(hass, config, action): # Local variable to keep track of if the action has already been triggered already_triggered = False - def event_listener(event): + def state_changed_listener(entity_id, from_s, to_s): """Listen for state changes and calls action.""" nonlocal already_triggered template_result = _check_template(hass, value_template) @@ -38,11 +39,18 @@ def trigger(hass, config, action): # Check to see if template returns true if template_result and not already_triggered: already_triggered = True - action() + action({ + 'trigger': { + 'platform': 'template', + 'entity_id': entity_id, + 'from_state': from_s, + 'to_state': to_s, + }, + }) elif not template_result: already_triggered = False - hass.bus.listen(EVENT_STATE_CHANGED, event_listener) + track_state_change(hass, MATCH_ALL, state_changed_listener) return True @@ -50,13 +58,14 @@ def if_action(hass, config): """Wrap action method with state based condition.""" value_template = config.get(CONF_VALUE_TEMPLATE) - return lambda: _check_template(hass, value_template) + return lambda variables: _check_template(hass, value_template, + variables=variables) -def _check_template(hass, value_template): +def _check_template(hass, value_template, variables=None): """Check if result of template is true.""" try: - value = template.render(hass, value_template, {}) + value = template.render(hass, value_template, variables) except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 761ad73b826..879b0e113d9 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -41,7 +41,12 @@ def trigger(hass, config, action): def time_automation_listener(now): """Listen for time changes and calls action.""" - action() + action({ + 'trigger': { + 'platform': 'time', + 'now': now, + }, + }) track_time_change(hass, time_automation_listener, hour=hours, minute=minutes, second=seconds) @@ -73,7 +78,7 @@ def if_action(hass, config): _error_time(after, CONF_AFTER) return None - def time_if(): + def time_if(variables): """Validate time based if-condition.""" now = dt_util.now() if before is not None and now > now.replace(hour=before.hour, diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 66ea3c2d7c7..fd798f45549 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -48,13 +48,22 @@ def trigger(hass, config, action): to_s.attributes.get(ATTR_LONGITUDE)): return - from_match = _in_zone(hass, zone_entity_id, from_s) if from_s else None - to_match = _in_zone(hass, zone_entity_id, to_s) + zone_state = hass.states.get(zone_entity_id) + from_match = _in_zone(hass, zone_state, from_s) if from_s else None + to_match = _in_zone(hass, zone_state, to_s) # pylint: disable=too-many-boolean-expressions if event == EVENT_ENTER and not from_match and to_match or \ event == EVENT_LEAVE and from_match and not to_match: - action() + action({ + 'trigger': { + 'platform': 'zone', + 'entity_id': entity, + 'from_state': from_s, + 'to_state': to_s, + 'zone': zone_state, + }, + }) track_state_change( hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL) @@ -67,20 +76,20 @@ def if_action(hass, config): entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) - def if_in_zone(): + def if_in_zone(variables): """Test if condition.""" - return _in_zone(hass, zone_entity_id, hass.states.get(entity_id)) + zone_state = hass.states.get(zone_entity_id) + return _in_zone(hass, zone_state, hass.states.get(entity_id)) return if_in_zone -def _in_zone(hass, zone_entity_id, state): +def _in_zone(hass, zone_state, state): """Check if state is in zone.""" if not state or None in (state.attributes.get(ATTR_LATITUDE), state.attributes.get(ATTR_LONGITUDE)): return False - zone_state = hass.states.get(zone_entity_id) return zone_state and zone.in_zone( zone_state, state.attributes.get(ATTR_LATITUDE), state.attributes.get(ATTR_LONGITUDE), diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b6a08cc59d0..50a7b290cc8 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -21,7 +21,9 @@ def track_state_change(hass, entity_ids, action, from_state=None, to_state = _process_match_param(to_state) # Ensure it is a lowercase list with entity ids we want to match on - if isinstance(entity_ids, str): + if entity_ids == MATCH_ALL: + pass + elif isinstance(entity_ids, str): entity_ids = (entity_ids.lower(),) else: entity_ids = tuple(entity_id.lower() for entity_id in entity_ids) @@ -29,7 +31,8 @@ def track_state_change(hass, entity_ids, action, from_state=None, @ft.wraps(action) def state_change_listener(event): """The listener that listens for specific state changes.""" - if event.data['entity_id'] not in entity_ids: + if entity_ids != MATCH_ALL and \ + event.data['entity_id'] not in entity_ids: return if event.data['old_state'] is None: diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 355865e9e9c..f6d33c18071 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -51,7 +51,10 @@ class TestAutomation(unittest.TestCase): }, 'action': { 'service': 'test.automation', - 'data': {'some': 'data'} + 'data_template': { + 'some': '{{ trigger.platform }} - ' + '{{ trigger.event.event_type }}' + }, } } }) @@ -59,7 +62,7 @@ class TestAutomation(unittest.TestCase): self.hass.bus.fire('test_event') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) - self.assertEqual('data', self.calls[0].data['some']) + self.assertEqual('event - test_event', self.calls[0].data['some']) def test_service_specify_entity_id(self): """Test service data.""" diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 0fd2a9aef06..29d55b424f2 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -35,14 +35,20 @@ class TestAutomationMQTT(unittest.TestCase): 'topic': 'test-topic' }, 'action': { - 'service': 'test.automation' + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.platform }} - {{ trigger.topic }}' + ' - {{ trigger.payload }}' + }, } } }) - fire_mqtt_message(self.hass, 'test-topic', '') + fire_mqtt_message(self.hass, 'test-topic', 'test_payload') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + self.assertEqual('mqtt - test-topic - test_payload', + self.calls[0].data['some']) def test_if_fires_on_topic_and_payload_match(self): """Test if message is fired on topic and payload match.""" diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index ee29c0fb56f..37df19e38ed 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -437,15 +437,28 @@ class TestAutomationNumericState(unittest.TestCase): 'below': 10, }, 'action': { - 'service': 'test.automation' + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', 'below', 'above', + 'from_state.state', 'from_value', + 'to_state.state', 'to_value')) + }, } } }) # 9 is below 10 - self.hass.states.set('test.entity', 'entity', + self.hass.states.set('test.entity', 'test state 1', + {'test_attribute': '1.2'}) + self.hass.pool.block_till_done() + self.hass.states.set('test.entity', 'test state 2', {'test_attribute': '0.9'}) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + self.assertEqual( + 'numeric_state - test.entity - 10 - None - test state 1 - 12.0 - ' + 'test state 2 - 9.0', + self.calls[0].data['some']) def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(self): """"Test if not fired changed attributes.""" diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 2f688249834..4a6971124b6 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -31,6 +31,9 @@ class TestAutomationState(unittest.TestCase): def test_if_fires_on_entity_change(self): """Test for firing on entity change.""" + self.hass.states.set('test.entity', 'hello') + self.hass.pool.block_till_done() + assert _setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -38,7 +41,13 @@ class TestAutomationState(unittest.TestCase): 'entity_id': 'test.entity', }, 'action': { - 'service': 'test.automation' + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'for')) + }, } } }) @@ -46,6 +55,9 @@ class TestAutomationState(unittest.TestCase): self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + self.assertEqual( + 'state - test.entity - hello - world - None', + self.calls[0].data['some']) def test_if_fires_on_entity_change_with_from_filter(self): """Test for firing on entity change with filter.""" diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 738c171ce6c..1975dc8da44 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -105,6 +105,11 @@ class TestAutomationSun(unittest.TestCase): }, 'action': { 'service': 'test.automation', + 'data_template': { + 'some': + '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'event', 'offset')) + }, } } }) @@ -112,6 +117,7 @@ class TestAutomationSun(unittest.TestCase): fire_time_changed(self.hass, trigger_time) self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + self.assertEqual('sun - sunset - 0:30:00', self.calls[0].data['some']) def test_sunrise_trigger_with_offset(self): """Test the runrise trigger with offset.""" diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index bb46c7a262a..a643b731492 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -1,4 +1,4 @@ -"""The tests fr the Template automation.""" +"""The tests for the Template automation.""" import unittest from homeassistant.bootstrap import _setup_component @@ -226,7 +226,13 @@ class TestAutomationTemplate(unittest.TestCase): {%- endif -%}''', }, 'action': { - 'service': 'test.automation' + 'service': 'test.automation', + 'data_template': { + 'some': + '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', 'from_state.state', + 'to_state.state')) + }, } } }) @@ -234,6 +240,9 @@ class TestAutomationTemplate(unittest.TestCase): self.hass.states.set('test.entity', 'world') self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + self.assertEqual( + 'template - test.entity - hello - world', + self.calls[0].data['some']) def test_if_fires_on_no_change_with_template_advanced(self): """Test for firing on no change with template advanced.""" diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index 36f22a00148..0b19e9389e2 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -176,7 +176,11 @@ class TestAutomationTime(unittest.TestCase): 'after': '5:00:00', }, 'action': { - 'service': 'test.automation' + 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.platform }} - ' + '{{ trigger.now.hour }}' + }, } } }) @@ -186,6 +190,7 @@ class TestAutomationTime(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + self.assertEqual('time - 5', self.calls[0].data['some']) def test_if_not_working_if_no_values_in_conf_provided(self): """Test for failure if no configuration.""" diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index 87a22243760..24980b466bf 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -52,6 +52,13 @@ class TestAutomationZone(unittest.TestCase): }, 'action': { 'service': 'test.automation', + 'data_template': { + 'some': '{{ trigger.%s }}' % '}} - {{ trigger.'.join(( + 'platform', 'entity_id', + 'from_state.state', 'to_state.state', + 'zone.name')) + }, + } } }) @@ -63,6 +70,9 @@ class TestAutomationZone(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(1, len(self.calls)) + self.assertEqual( + 'zone - test.entity - hello - hello - test', + self.calls[0].data['some']) def test_if_not_fires_for_enter_on_zone_leave(self): """Test for not firing on zone leave.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 6d3a9cbb6a0..5d9f8d28e20 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from astral import Astral import homeassistant.core as ha +from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import ( track_point_in_utc_time, track_point_in_time, @@ -93,6 +94,7 @@ class TestEventHelpers(unittest.TestCase): # 2 lists to track how often our callbacks get called specific_runs = [] wildcard_runs = [] + wildercard_runs = [] track_state_change( self.hass, 'light.Bowl', lambda a, b, c: specific_runs.append(1), @@ -100,14 +102,18 @@ class TestEventHelpers(unittest.TestCase): track_state_change( self.hass, 'light.Bowl', - lambda _, old_s, new_s: wildcard_runs.append((old_s, new_s)), - ha.MATCH_ALL, ha.MATCH_ALL) + lambda _, old_s, new_s: wildcard_runs.append((old_s, new_s))) + + track_state_change( + self.hass, MATCH_ALL, + lambda _, old_s, new_s: wildercard_runs.append((old_s, new_s))) # Adding state to state machine self.hass.states.set("light.Bowl", "on") self.hass.pool.block_till_done() self.assertEqual(0, len(specific_runs)) self.assertEqual(1, len(wildcard_runs)) + self.assertEqual(1, len(wildercard_runs)) self.assertIsNone(wildcard_runs[-1][0]) self.assertIsNotNone(wildcard_runs[-1][1]) @@ -116,31 +122,45 @@ class TestEventHelpers(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(specific_runs)) self.assertEqual(1, len(wildcard_runs)) + self.assertEqual(1, len(wildercard_runs)) # State change off -> on self.hass.states.set('light.Bowl', 'off') self.hass.pool.block_till_done() self.assertEqual(1, len(specific_runs)) self.assertEqual(2, len(wildcard_runs)) + self.assertEqual(2, len(wildercard_runs)) # State change off -> off self.hass.states.set('light.Bowl', 'off', {"some_attr": 1}) self.hass.pool.block_till_done() self.assertEqual(1, len(specific_runs)) self.assertEqual(3, len(wildcard_runs)) + self.assertEqual(3, len(wildercard_runs)) # State change off -> on self.hass.states.set('light.Bowl', 'on') self.hass.pool.block_till_done() self.assertEqual(1, len(specific_runs)) self.assertEqual(4, len(wildcard_runs)) + self.assertEqual(4, len(wildercard_runs)) self.hass.states.remove('light.bowl') self.hass.pool.block_till_done() self.assertEqual(1, len(specific_runs)) self.assertEqual(5, len(wildcard_runs)) + self.assertEqual(5, len(wildercard_runs)) self.assertIsNotNone(wildcard_runs[-1][0]) self.assertIsNone(wildcard_runs[-1][1]) + self.assertIsNotNone(wildercard_runs[-1][0]) + self.assertIsNone(wildercard_runs[-1][1]) + + # Set state for different entity id + self.hass.states.set('switch.kitchen', 'on') + self.hass.pool.block_till_done() + self.assertEqual(1, len(specific_runs)) + self.assertEqual(5, len(wildcard_runs)) + self.assertEqual(6, len(wildercard_runs)) def test_track_sunrise(self): """Test track the sunrise.""" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 59ba1781ab2..c863a46ad3b 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -2,7 +2,8 @@ import unittest from unittest.mock import patch -import homeassistant.components # noqa - to prevent circular import +# To prevent circular import when running just this file +import homeassistant.components # noqa from homeassistant import core as ha, loader from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.helpers import service From 2333c0ca3b0fe5304da42fe4b723b8902f58a896 Mon Sep 17 00:00:00 2001 From: Dennis Karpienski Date: Fri, 22 Apr 2016 02:35:56 +0200 Subject: [PATCH 010/101] WebOS component fixes * fixed some exceptions * add requirements to notify * added optimistic state to power off * run requirements script --- homeassistant/components/media_player/webostv.py | 5 +++-- homeassistant/components/notify/webostv.py | 8 ++++++-- requirements_all.txt | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 38c3b8b2830..4d3bb701586 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -69,7 +69,7 @@ def setup_tv(host, hass, add_devices): _LOGGER.warning( 'Connected to LG WebOS TV at %s but not paired.', host) return - except ConnectionRefusedError: + except OSError: _LOGGER.error('Unable to connect to host %s.', host) return else: @@ -158,7 +158,7 @@ class LgWebOSDevice(MediaPlayerDevice): if source['appId'] == self._current_source_id: self._current_source = source['label'] - except ConnectionRefusedError: + except OSError: self._state = STATE_OFF @property @@ -208,6 +208,7 @@ class LgWebOSDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" + self._state = STATE_OFF self._client.power_off() def volume_up(self): diff --git a/homeassistant/components/notify/webostv.py b/homeassistant/components/notify/webostv.py index fb6cf02c9c5..34463dc6e45 100644 --- a/homeassistant/components/notify/webostv.py +++ b/homeassistant/components/notify/webostv.py @@ -10,6 +10,10 @@ from homeassistant.components.notify import (BaseNotificationService, DOMAIN) from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.helpers import validate_config +REQUIREMENTS = ['https://github.com/TheRealLink/pylgtv' + '/archive/v0.1.2.zip' + '#pylgtv==0.1.2'] + _LOGGER = logging.getLogger(__name__) @@ -35,7 +39,7 @@ def get_service(hass, config): except PyLGTVPairException: _LOGGER.error('Pairing failed.') return None - except ConnectionRefusedError: + except OSError: _LOGGER.error('Host unreachable.') return None @@ -58,5 +62,5 @@ class LgWebOSNotificationService(BaseNotificationService): self._client.send_message(message) except PyLGTVPairException: _LOGGER.error('Pairing failed.') - except ConnectionRefusedError: + except OSError: _LOGGER.error('Host unreachable.') diff --git a/requirements_all.txt b/requirements_all.txt index 297bc01c127..cdddc4550e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -80,6 +80,7 @@ https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a https://github.com/LinuxChristian/pyW215/archive/v0.1.1.zip#pyW215==0.1.1 # homeassistant.components.media_player.webostv +# homeassistant.components.notify.webostv https://github.com/TheRealLink/pylgtv/archive/v0.1.2.zip#pylgtv==0.1.2 # homeassistant.components.sensor.thinkingcleaner From f76d545a084e48f0cdaf59cd5937f9ae2eee691d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 15:52:20 -0700 Subject: [PATCH 011/101] Add script logic into helper. --- homeassistant/components/automation/state.py | 6 +- homeassistant/components/automation/sun.py | 6 +- homeassistant/components/script.py | 175 ++----------------- homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 81 ++++++--- homeassistant/helpers/script.py | 125 +++++++++++++ tests/components/test_script.py | 47 ----- tests/helpers/test_config_validation.py | 11 +- tests/helpers/test_service.py | 5 +- 9 files changed, 219 insertions(+), 238 deletions(-) create mode 100644 homeassistant/helpers/script.py diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 802debbe63e..3183dab0803 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -92,7 +92,7 @@ def trigger(hass, config, action): def state_for_listener(now): """Fire on state changes after a delay and calls action.""" hass.bus.remove_listener( - EVENT_STATE_CHANGED, attached_state_for_cancel_listener) + EVENT_STATE_CHANGED, attached_state_for_cancel) call_action() def state_for_cancel_listener(entity, inner_from_s, inner_to_s): @@ -102,12 +102,12 @@ def trigger(hass, config, action): hass.bus.remove_listener(EVENT_TIME_CHANGED, attached_state_for_listener) hass.bus.remove_listener(EVENT_STATE_CHANGED, - attached_state_for_cancel_listener) + attached_state_for_cancel) attached_state_for_listener = track_point_in_time( hass, state_for_listener, dt_util.utcnow() + time_delta) - attached_state_for_cancel_listener = track_state_change( + attached_state_for_cancel = track_state_change( hass, entity_id, state_for_cancel_listener) track_state_change( diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index c9db88a83c2..7de43d7f5e3 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -35,7 +35,7 @@ _SUN_EVENT = vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET)) TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'sun', vol.Required(CONF_EVENT): _SUN_EVENT, - vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_offset, + vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period, }) IF_ACTION_SCHEMA = vol.All( @@ -43,8 +43,8 @@ IF_ACTION_SCHEMA = vol.All( vol.Required(CONF_PLATFORM): 'sun', CONF_BEFORE: _SUN_EVENT, CONF_AFTER: _SUN_EVENT, - vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_offset, - vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_offset, + vol.Required(CONF_BEFORE_OFFSET, default=timedelta(0)): cv.time_period, + vol.Required(CONF_AFTER_OFFSET, default=timedelta(0)): cv.time_period, }), cv.has_at_least_one_key(CONF_BEFORE, CONF_AFTER), ) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index c19e614f19d..3557179c6eb 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -8,101 +8,33 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/script/ """ import logging -import threading -from datetime import timedelta -from itertools import islice import voluptuous as vol -import homeassistant.util.dt as date_util from homeassistant.const import ( - ATTR_ENTITY_ID, EVENT_TIME_CHANGED, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, STATE_ON) + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_TOGGLE, STATE_ON, CONF_ALIAS) from homeassistant.helpers.entity import ToggleEntity, split_entity_id from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.helpers.service import (call_from_config, - validate_service_call) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.script import Script + DOMAIN = "script" ENTITY_ID_FORMAT = DOMAIN + '.{}' DEPENDENCIES = ["group"] -STATE_NOT_RUNNING = 'Not Running' - -CONF_ALIAS = "alias" -CONF_SERVICE = "service" -CONF_SERVICE_DATA = "data" CONF_SEQUENCE = "sequence" -CONF_EVENT = "event" -CONF_EVENT_DATA = "event_data" -CONF_DELAY = "delay" ATTR_LAST_ACTION = 'last_action' ATTR_CAN_CANCEL = 'can_cancel' _LOGGER = logging.getLogger(__name__) -_ALIAS_VALIDATOR = vol.Schema(cv.string) - - -def _alias_stripper(validator): - """Strip alias from object for validation.""" - def validate(value): - """Validate without alias value.""" - value = value.copy() - alias = value.pop(CONF_ALIAS, None) - - if alias is not None: - alias = _ALIAS_VALIDATOR(alias) - - value = validator(value) - - if alias is not None: - value[CONF_ALIAS] = alias - - return value - - return validate - - -_TIMESPEC = vol.Schema({ - 'days': cv.positive_int, - 'hours': cv.positive_int, - 'minutes': cv.positive_int, - 'seconds': cv.positive_int, - 'milliseconds': cv.positive_int, -}) -_TIMESPEC_REQ = cv.has_at_least_one_key( - 'days', 'hours', 'minutes', 'seconds', 'milliseconds', -) - -_DELAY_SCHEMA = vol.Any( - vol.Schema({ - vol.Required(CONF_DELAY): vol.All(_TIMESPEC.extend({ - vol.Optional(CONF_ALIAS): cv.string - }), _TIMESPEC_REQ) - }), - # Alternative format in case people forgot to indent after 'delay:' - vol.All(_TIMESPEC.extend({ - vol.Required(CONF_DELAY): None, - vol.Optional(CONF_ALIAS): cv.string, - }), _TIMESPEC_REQ) -) - -_EVENT_SCHEMA = cv.EVENT_SCHEMA.extend({ - CONF_ALIAS: cv.string, -}) _SCRIPT_ENTRY_SCHEMA = vol.Schema({ CONF_ALIAS: cv.string, - vol.Required(CONF_SEQUENCE): vol.All(vol.Length(min=1), [vol.Any( - _EVENT_SCHEMA, - _DELAY_SCHEMA, - # Can't extend SERVICE_SCHEMA because it is an vol.All - _alias_stripper(cv.SERVICE_SCHEMA), - )]), + vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, }) CONFIG_SCHEMA = vol.Schema({ @@ -152,7 +84,7 @@ def setup(hass, config): for object_id, cfg in config[DOMAIN].items(): alias = cfg.get(CONF_ALIAS, object_id) - script = Script(object_id, alias, cfg[CONF_SEQUENCE]) + script = ScriptEntity(hass, object_id, alias, cfg[CONF_SEQUENCE]) component.add_entities((script,)) hass.services.register(DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA) @@ -183,21 +115,14 @@ def setup(hass, config): return True -class Script(ToggleEntity): - """Representation of a script.""" +class ScriptEntity(ToggleEntity): + """Representation of a script entity.""" # pylint: disable=too-many-instance-attributes - def __init__(self, object_id, name, sequence): + def __init__(self, hass, object_id, name, sequence): """Initialize the script.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self.sequence = sequence - self._lock = threading.Lock() - self._cur = -1 - self._last_action = None - self._listener = None - self._can_cancel = any(CONF_DELAY in action for action - in self.sequence) + self.script = Script(hass, sequence, name, self.update_ha_state) @property def should_poll(self): @@ -207,91 +132,27 @@ class Script(ToggleEntity): @property def name(self): """Return the name of the entity.""" - return self._name + return self.script.name @property def state_attributes(self): """Return the state attributes.""" attrs = {} - if self._can_cancel: - attrs[ATTR_CAN_CANCEL] = self._can_cancel - if self._last_action: - attrs[ATTR_LAST_ACTION] = self._last_action + if self.script.can_cancel: + attrs[ATTR_CAN_CANCEL] = self.script.can_cancel + if self.script.last_action: + attrs[ATTR_LAST_ACTION] = self.script.last_action return attrs @property def is_on(self): """Return true if script is on.""" - return self._cur != -1 + return self.script.is_running def turn_on(self, **kwargs): """Turn the entity on.""" - _LOGGER.info("Executing script %s", self._name) - with self._lock: - if self._cur == -1: - self._cur = 0 - - # Unregister callback if we were in a delay but turn on is called - # again. In that case we just continue execution. - self._remove_listener() - - for cur, action in islice(enumerate(self.sequence), self._cur, - None): - - if validate_service_call(action) is None: - self._call_service(action) - - elif CONF_EVENT in action: - self._fire_event(action) - - elif CONF_DELAY in action: - # Call ourselves in the future to continue work - def script_delay(now): - """Called after delay is done.""" - self._listener = None - self.turn_on() - - timespec = action[CONF_DELAY] or action.copy() - timespec.pop(CONF_DELAY, None) - delay = timedelta(**timespec) - self._listener = track_point_in_utc_time( - self.hass, script_delay, date_util.utcnow() + delay) - self._cur = cur + 1 - self.update_ha_state() - return - - self._cur = -1 - self._last_action = None - self.update_ha_state() + self.script.run() def turn_off(self, **kwargs): """Turn script off.""" - _LOGGER.info("Cancelled script %s", self._name) - with self._lock: - if self._cur == -1: - return - - self._cur = -1 - self.update_ha_state() - self._remove_listener() - - def _call_service(self, action): - """Call the service specified in the action.""" - self._last_action = action.get(CONF_ALIAS, 'call service') - _LOGGER.info("Executing script %s step %s", self._name, - self._last_action) - call_from_config(self.hass, action, True) - - def _fire_event(self, action): - """Fire an event.""" - self._last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) - _LOGGER.info("Executing script %s step %s", self._name, - self._last_action) - self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA)) - - def _remove_listener(self): - """Remove point in time listener, if any.""" - if self._listener: - self.hass.bus.remove_listener(EVENT_TIME_CHANGED, - self._listener) - self._listener = None + self.script.stop() diff --git a/homeassistant/const.py b/homeassistant/const.py index 77e540cd76f..b2971ab59f6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -13,6 +13,7 @@ MATCH_ALL = '*' DEVICE_DEFAULT_NAME = "Unnamed Device" # #### CONFIG #### +CONF_ALIAS = "alias" CONF_ICON = "icon" CONF_LATITUDE = "latitude" CONF_LONGITUDE = "longitude" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 51684e5f1cd..71e103f7dd3 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -6,7 +6,8 @@ import voluptuous as vol from homeassistant.loader import get_platform from homeassistant.const import ( - CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT) + CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_ALIAS) from homeassistant.helpers.entity import valid_entity_id import homeassistant.util.dt as dt_util from homeassistant.util import slugify @@ -23,6 +24,23 @@ longitude = vol.All(vol.Coerce(float), vol.Range(min=-180, max=180), msg='invalid longitude') +# Adapted from: +# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666 +def has_at_least_one_key(*keys): + """Validator that at least one key exists.""" + def validate(obj): + """Test keys exist in dict.""" + if not isinstance(obj, dict): + raise vol.Invalid('expected dictionary') + + for k in obj.keys(): + if k in keys: + return obj + raise vol.Invalid('must contain one of {}.'.format(', '.join(keys))) + + return validate + + def boolean(value): """Validate and coerce a boolean value.""" if isinstance(value, str): @@ -72,10 +90,24 @@ def icon(value): raise vol.Invalid('Icons should start with prefix "mdi:"') -def time_offset(value): +time_period_dict = vol.All( + dict, vol.Schema({ + 'days': vol.Coerce(int), + 'hours': vol.Coerce(int), + 'minutes': vol.Coerce(int), + 'seconds': vol.Coerce(int), + 'milliseconds': vol.Coerce(int), + }), + has_at_least_one_key('days', 'hours', 'minutes', + 'seconds', 'milliseconds'), + lambda value: timedelta(**value)) + + +def time_period_str(value): """Validate and transform time offset.""" if not isinstance(value, str): - raise vol.Invalid('offset should be a string') + raise vol.Invalid( + 'offset {} should be format HH:MM or HH:MM:SS'.format(value)) negative_offset = False if value.startswith('-'): @@ -107,6 +139,9 @@ def time_offset(value): return offset +time_period = vol.Any(time_period_str, timedelta, time_period_dict) + + def match_all(value): """Validator that matches all values.""" return value @@ -125,6 +160,13 @@ def platform_validator(domain): return validator +def positive_timedelta(value): + """Validate timedelta is positive.""" + if value < timedelta(0): + raise vol.Invalid('Time period should be positive') + return value + + def service(value): """Validate service.""" # Services use same format as entities so we can use same helper. @@ -200,23 +242,6 @@ def key_dependency(key, dependency): return validator -# Adapted from: -# https://github.com/alecthomas/voluptuous/issues/115#issuecomment-144464666 -def has_at_least_one_key(*keys): - """Validator that at least one key exists.""" - def validate(obj): - """Test keys exist in dict.""" - if not isinstance(obj, dict): - raise vol.Invalid('expected dictionary') - - for k in obj.keys(): - if k in keys: - return obj - raise vol.Invalid('must contain one of {}.'.format(', '.join(keys))) - - return validate - - # Schemas PLATFORM_SCHEMA = vol.Schema({ @@ -225,14 +250,28 @@ PLATFORM_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) EVENT_SCHEMA = vol.Schema({ + vol.Optional(CONF_ALIAS): string, vol.Required('event'): string, - 'event_data': dict + vol.Optional('event_data'): dict, }) SERVICE_SCHEMA = vol.All(vol.Schema({ + vol.Optional(CONF_ALIAS): string, vol.Exclusive('service', 'service name'): service, vol.Exclusive('service_template', 'service name'): template, vol.Optional('data'): dict, vol.Optional('data_template'): {match_all: template}, vol.Optional('entity_id'): entity_ids, }), has_at_least_one_key('service', 'service_template')) + +# ----- SCRIPT + +_DELAY_SCHEMA = vol.Schema({ + vol.Optional(CONF_ALIAS): string, + vol.Required("delay"): vol.All(time_period, positive_timedelta) +}) + +SCRIPT_SCHEMA = vol.All( + ensure_list, + [vol.Any(SERVICE_SCHEMA, _DELAY_SCHEMA, EVENT_SCHEMA)], +) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py new file mode 100644 index 00000000000..e4cf2f6756d --- /dev/null +++ b/homeassistant/helpers/script.py @@ -0,0 +1,125 @@ +"""Helpers to execute scripts.""" +import logging +import threading +from itertools import islice + +import homeassistant.util.dt as date_util +from homeassistant.const import EVENT_TIME_CHANGED +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers import service +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_ALIAS = "alias" +CONF_SERVICE = "service" +CONF_SERVICE_DATA = "data" +CONF_SEQUENCE = "sequence" +CONF_EVENT = "event" +CONF_EVENT_DATA = "event_data" +CONF_DELAY = "delay" + + +def call_from_config(hass, config): + """Call a script based on a config entry.""" + Script(hass, config).run() + + +class Script(): + """Representation of a script.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, hass, sequence, name=None, change_listener=None): + """Initialize the script.""" + self.hass = hass + self.sequence = cv.SCRIPT_SCHEMA(sequence) + self.name = name + self._change_listener = change_listener + self._cur = -1 + self.last_action = None + self.can_cancel = any(CONF_DELAY in action for action + in self.sequence) + self._lock = threading.Lock() + self._delay_listener = None + + @property + def is_running(self): + """Return true if script is on.""" + return self._cur != -1 + + def run(self): + """Run script.""" + with self._lock: + if self._cur == -1: + self._log('Running script') + self._cur = 0 + + # Unregister callback if we were in a delay but turn on is called + # again. In that case we just continue execution. + self._remove_listener() + + for cur, action in islice(enumerate(self.sequence), self._cur, + None): + + if CONF_DELAY in action: + # Call ourselves in the future to continue work + def script_delay(now): + """Called after delay is done.""" + self._delay_listener = None + self.run() + + self._delay_listener = track_point_in_utc_time( + self.hass, script_delay, + date_util.utcnow() + action[CONF_DELAY]) + self._cur = cur + 1 + if self._change_listener: + self._change_listener() + return + + elif service.validate_service_call(action) is None: + self._call_service(action) + + elif CONF_EVENT in action: + self._fire_event(action) + + self._cur = -1 + self.last_action = None + if self._change_listener: + self._change_listener() + + def stop(self): + """Stop running script.""" + with self._lock: + if self._cur == -1: + return + + self._cur = -1 + self._remove_listener() + if self._change_listener: + self._change_listener() + + def _call_service(self, action): + """Call the service specified in the action.""" + self.last_action = action.get(CONF_ALIAS, 'call service') + self._log("Executing step %s", self.last_action) + service.call_from_config(self.hass, action, True) + + def _fire_event(self, action): + """Fire an event.""" + self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) + self._log("Executing step %s", self.last_action) + self.hass.bus.fire(action[CONF_EVENT], action.get(CONF_EVENT_DATA)) + + def _remove_listener(self): + """Remove point in time listener, if any.""" + if self._delay_listener: + self.hass.bus.remove_listener(EVENT_TIME_CHANGED, + self._delay_listener) + self._delay_listener = None + + def _log(self, msg, *substitutes): + """Logger helper.""" + if self.name is not None: + msg = "Script {}: {}".format(self.name, msg, *substitutes) + + _LOGGER.info(msg) diff --git a/tests/components/test_script.py b/tests/components/test_script.py index 4f912dc77a0..f8b99533c18 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -34,13 +34,6 @@ class TestScript(unittest.TestCase): 'sequence': [{'event': 'bla'}] } }, - { - 'test': { - 'sequence': { - 'event': 'test_event' - } - } - }, { 'test': { 'sequence': { @@ -49,7 +42,6 @@ class TestScript(unittest.TestCase): } } }, - ): assert not _setup_component(self.hass, 'script', { 'script': value @@ -206,45 +198,6 @@ class TestScript(unittest.TestCase): self.assertEqual(2, len(calls)) - def test_alt_delay(self): - """Test alternative delay config format.""" - event = 'test_event' - calls = [] - - def record_event(event): - """Add recorded event to set.""" - calls.append(event) - - self.hass.bus.listen(event, record_event) - - assert _setup_component(self.hass, 'script', { - 'script': { - 'test': { - 'sequence': [{ - 'event': event, - }, { - 'delay': None, - 'seconds': 5 - }, { - 'event': event, - }] - } - } - }) - - script.turn_on(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - - self.assertTrue(script.is_on(self.hass, ENTITY_ID)) - self.assertEqual(1, len(calls)) - - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.pool.block_till_done() - - self.assertFalse(script.is_on(self.hass, ENTITY_ID)) - self.assertEqual(2, len(calls)) - def test_cancel_while_delay(self): """Test the cancelling while the delay is present.""" event = 'test_event' diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 3f4789eca4f..b73dc6d6f94 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -145,18 +145,19 @@ def test_icon(): schema('mdi:work') -def test_time_offset(): - """Test time_offset validation.""" - schema = vol.Schema(cv.time_offset) +def test_time_period(): + """Test time_period validation.""" + schema = vol.Schema(cv.time_period) for value in ( - None, '', 1234, 'hello:world', '12:', '12:34:56:78' + None, '', 1234, 'hello:world', '12:', '12:34:56:78', + {}, {'wrong_key': -10} ): with pytest.raises(vol.MultipleInvalid): schema(value) for value in ( - '8:20', '23:59', '-8:20', '-23:59:59', '-48:00' + '8:20', '23:59', '-8:20', '-23:59:59', '-48:00', {'minutes': 5} ): schema(value) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index c863a46ad3b..11ace1ab5d8 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -37,7 +37,7 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual(1, len(runs)) def test_template_service_call(self): - """ Test service call with tempating. """ + """Test service call with tempating.""" config = { 'service_template': '{{ \'test_domain.test_service\' }}', 'entity_id': 'hello.world', @@ -56,6 +56,7 @@ class TestServiceHelpers(unittest.TestCase): self.assertEqual('goodbye', runs[0].data['hello']) def test_passing_variables_to_templates(self): + """Test passing variables to templates.""" config = { 'service_template': '{{ var_service }}', 'entity_id': 'hello.world', @@ -141,7 +142,7 @@ class TestServiceHelpers(unittest.TestCase): service.extract_entity_ids(self.hass, call)) def test_validate_service_call(self): - """Test is_valid_service_call method""" + """Test is_valid_service_call method.""" self.assertNotEqual( service.validate_service_call( {}), From 09a771a026d12e5c6d32878c17a809f38ec2a41c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 21:29:20 -0400 Subject: [PATCH 012/101] Move script component tests to script helper tests --- tests/components/test_script.py | 199 +------------------------------- tests/helpers/test_script.py | 175 ++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 197 deletions(-) create mode 100644 tests/helpers/test_script.py diff --git a/tests/components/test_script.py b/tests/components/test_script.py index f8b99533c18..90165ada294 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -1,19 +1,17 @@ """The tests for the Script component.""" # pylint: disable=too-many-public-methods,protected-access -from datetime import timedelta import unittest from homeassistant.bootstrap import _setup_component from homeassistant.components import script -import homeassistant.util.dt as dt_util -from tests.common import fire_time_changed, get_test_home_assistant +from tests.common import get_test_home_assistant ENTITY_ID = 'script.test' -class TestScript(unittest.TestCase): +class TestScriptComponent(unittest.TestCase): """Test the Script component.""" def setUp(self): # pylint: disable=invalid-name @@ -49,199 +47,6 @@ class TestScript(unittest.TestCase): self.assertEqual(0, len(self.hass.states.entity_ids('script'))) - def test_firing_event(self): - """Test the firing of events.""" - event = 'test_event' - calls = [] - - def record_event(event): - """Add recorded event to set.""" - calls.append(event) - - self.hass.bus.listen(event, record_event) - - assert _setup_component(self.hass, 'script', { - 'script': { - 'test': { - 'alias': 'Test Script', - 'sequence': [{ - 'event': event, - 'event_data': { - 'hello': 'world' - } - }] - } - } - }) - - script.turn_on(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - - self.assertEqual(1, len(calls)) - self.assertEqual('world', calls[0].data.get('hello')) - self.assertIsNone( - self.hass.states.get(ENTITY_ID).attributes.get('can_cancel')) - - def test_calling_service(self): - """Test the calling of a service.""" - calls = [] - - def record_call(service): - """Add recorded event to set.""" - calls.append(service) - - self.hass.services.register('test', 'script', record_call) - - assert _setup_component(self.hass, 'script', { - 'script': { - 'test': { - 'sequence': [{ - 'service': 'test.script', - 'data': { - 'hello': 'world' - } - }] - } - } - }) - - script.turn_on(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - - self.assertEqual(1, len(calls)) - self.assertEqual('world', calls[0].data.get('hello')) - - def test_calling_service_template(self): - """Test the calling of a service.""" - calls = [] - - def record_call(service): - """Add recorded event to set.""" - calls.append(service) - - self.hass.services.register('test', 'script', record_call) - - assert _setup_component(self.hass, 'script', { - 'script': { - 'test': { - 'sequence': [{ - 'service_template': """ - {% if True %} - test.script - {% else %} - test.not_script - {% endif %}""", - 'data_template': { - 'hello': """ - {% if True %} - world - {% else %} - Not world - {% endif %} - """ - } - }] - } - } - }) - - script.turn_on(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - - self.assertEqual(1, len(calls)) - self.assertEqual('world', calls[0].data.get('hello')) - - def test_delay(self): - """Test the delay.""" - event = 'test_event' - calls = [] - - def record_event(event): - """Add recorded event to set.""" - calls.append(event) - - self.hass.bus.listen(event, record_event) - - assert _setup_component(self.hass, 'script', { - 'script': { - 'test': { - 'sequence': [{ - 'event': event - }, { - 'delay': { - 'seconds': 5 - } - }, { - 'event': event, - }] - } - } - }) - - script.turn_on(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - - self.assertTrue(script.is_on(self.hass, ENTITY_ID)) - self.assertTrue( - self.hass.states.get(ENTITY_ID).attributes.get('can_cancel')) - - self.assertEqual( - event, - self.hass.states.get(ENTITY_ID).attributes.get('last_action')) - self.assertEqual(1, len(calls)) - - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.pool.block_till_done() - - self.assertFalse(script.is_on(self.hass, ENTITY_ID)) - - self.assertEqual(2, len(calls)) - - def test_cancel_while_delay(self): - """Test the cancelling while the delay is present.""" - event = 'test_event' - calls = [] - - def record_event(event): - """Add recorded event to set.""" - calls.append(event) - - self.hass.bus.listen(event, record_event) - - assert _setup_component(self.hass, 'script', { - 'script': { - 'test': { - 'sequence': [{ - 'delay': { - 'seconds': 5 - } - }, { - 'event': event, - }] - } - } - }) - - script.turn_on(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - - self.assertTrue(script.is_on(self.hass, ENTITY_ID)) - - self.assertEqual(0, len(calls)) - - script.turn_off(self.hass, ENTITY_ID) - self.hass.pool.block_till_done() - self.assertFalse(script.is_on(self.hass, ENTITY_ID)) - - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.pool.block_till_done() - - self.assertFalse(script.is_on(self.hass, ENTITY_ID)) - - self.assertEqual(0, len(calls)) - def test_turn_on_service(self): """Verify that the turn_on service.""" event = 'test_event' diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py new file mode 100644 index 00000000000..90e438047bb --- /dev/null +++ b/tests/helpers/test_script.py @@ -0,0 +1,175 @@ +"""The tests for the Script component.""" +# pylint: disable=too-many-public-methods,protected-access +from datetime import timedelta +import unittest + +from homeassistant.bootstrap import _setup_component +import homeassistant.util.dt as dt_util +from homeassistant.helpers import script + +from tests.common import fire_time_changed, get_test_home_assistant + + +ENTITY_ID = 'script.test' + + +class TestScriptHelper(unittest.TestCase): + """Test the Script component.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_firing_event(self): + """Test the firing of events.""" + event = 'test_event' + calls = [] + + def record_event(event): + """Add recorded event to set.""" + calls.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, { + 'event': event, + 'event_data': { + 'hello': 'world' + } + }) + + script_obj.run() + + self.hass.pool.block_till_done() + + assert len(calls) == 1 + assert calls[0].data.get('hello') == 'world' + assert not script_obj.can_cancel + + def test_calling_service(self): + """Test the calling of a service.""" + calls = [] + + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + self.hass.services.register('test', 'script', record_call) + + script_obj = script.Script(self.hass, { + 'service': 'test.script', + 'data': { + 'hello': 'world' + } + }) + + script_obj.run() + self.hass.pool.block_till_done() + + assert len(calls) == 1 + assert calls[0].data.get('hello') == 'world' + + def test_calling_service_template(self): + """Test the calling of a service.""" + calls = [] + + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + self.hass.services.register('test', 'script', record_call) + + script_obj = script.Script(self.hass, { + 'service_template': """ + {% if True %} + test.script + {% else %} + test.not_script + {% endif %}""", + 'data_template': { + 'hello': """ + {% if True %} + world + {% else %} + Not world + {% endif %} + """ + } + }) + + script_obj.run() + + self.hass.pool.block_till_done() + + assert len(calls) == 1 + assert calls[0].data.get('hello') == 'world' + + def test_delay(self): + """Test the delay.""" + event = 'test_event' + events = [] + + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, [ + {'event': event}, + {'delay': {'seconds': 5}}, + {'event': event}]) + + script_obj.run() + + self.hass.pool.block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == event + assert len(events) == 1 + + future = dt_util.utcnow() + timedelta(seconds=5) + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + assert not script_obj.is_running + assert len(events) == 2 + + def test_cancel_while_delay(self): + """Test the cancelling while the delay is present.""" + event = 'test_event' + events = [] + + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, [ + {'delay': {'seconds': 5}}, + {'event': event}]) + + script_obj.run() + + self.hass.pool.block_till_done() + + assert script_obj.is_running + assert len(events) == 0 + + script_obj.stop() + + assert not script_obj.is_running + + # Make sure the script is really stopped. + future = dt_util.utcnow() + timedelta(seconds=5) + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + assert not script_obj.is_running + assert len(events) == 0 From 26863284b6245e9552ac9a7839cd279cc04ad393 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 21:42:20 -0400 Subject: [PATCH 013/101] Script helper: support variables --- homeassistant/helpers/script.py | 10 ++++---- tests/helpers/test_script.py | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e4cf2f6756d..0025ddd61df 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -47,7 +47,7 @@ class Script(): """Return true if script is on.""" return self._cur != -1 - def run(self): + def run(self, variables=None): """Run script.""" with self._lock: if self._cur == -1: @@ -66,7 +66,7 @@ class Script(): def script_delay(now): """Called after delay is done.""" self._delay_listener = None - self.run() + self.run(variables) self._delay_listener = track_point_in_utc_time( self.hass, script_delay, @@ -77,7 +77,7 @@ class Script(): return elif service.validate_service_call(action) is None: - self._call_service(action) + self._call_service(action, variables) elif CONF_EVENT in action: self._fire_event(action) @@ -98,11 +98,11 @@ class Script(): if self._change_listener: self._change_listener() - def _call_service(self, action): + def _call_service(self, action, variables): """Call the service specified in the action.""" self.last_action = action.get(CONF_ALIAS, 'call service') self._log("Executing step %s", self.last_action) - service.call_from_config(self.hass, action, True) + service.call_from_config(self.hass, action, True, variables) def _fire_event(self, action): """Fire an event.""" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 90e438047bb..492b62906df 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -173,3 +173,47 @@ class TestScriptHelper(unittest.TestCase): assert not script_obj.is_running assert len(events) == 0 + + def test_passing_variables_to_script(self): + """Test if we can pass variables to script.""" + calls = [] + + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + self.hass.services.register('test', 'script', record_call) + + script_obj = script.Script(self.hass, [ + { + 'service': 'test.script', + 'data_template': { + 'hello': '{{ greeting }}', + }, + }, + {'delay': {'seconds': 5}}, + { + 'service': 'test.script', + 'data_template': { + 'hello': '{{ greeting2 }}', + }, + }]) + + script_obj.run({ + 'greeting': 'world', + 'greeting2': 'universe', + }) + + self.hass.pool.block_till_done() + + assert script_obj.is_running + assert len(calls) == 1 + assert calls[-1].data['hello'] == 'world' + + future = dt_util.utcnow() + timedelta(seconds=5) + fire_time_changed(self.hass, future) + self.hass.pool.block_till_done() + + assert not script_obj.is_running + assert len(calls) == 2 + assert calls[-1].data['hello'] == 'universe' From b8e4db9161ca8d058e0d91b759c830f151374235 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 22:21:11 -0400 Subject: [PATCH 014/101] Script entities to allow passing in variables --- homeassistant/components/script.py | 17 ++++----- tests/components/test_script.py | 57 +++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/script.py b/homeassistant/components/script.py index 3557179c6eb..5f1e63f5d00 100644 --- a/homeassistant/components/script.py +++ b/homeassistant/components/script.py @@ -26,12 +26,12 @@ DEPENDENCIES = ["group"] CONF_SEQUENCE = "sequence" +ATTR_VARIABLES = 'variables' ATTR_LAST_ACTION = 'last_action' ATTR_CAN_CANCEL = 'can_cancel' _LOGGER = logging.getLogger(__name__) - _SCRIPT_ENTRY_SCHEMA = vol.Schema({ CONF_ALIAS: cv.string, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, @@ -41,9 +41,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(DOMAIN): {cv.slug: _SCRIPT_ENTRY_SCHEMA} }, extra=vol.ALLOW_EXTRA) -SCRIPT_SERVICE_SCHEMA = vol.Schema({}) +SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) SCRIPT_TURN_ONOFF_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_VARIABLES): dict, }) @@ -52,11 +53,11 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -def turn_on(hass, entity_id): +def turn_on(hass, entity_id, variables=None): """Turn script on.""" _, object_id = split_entity_id(entity_id) - hass.services.call(DOMAIN, object_id) + hass.services.call(DOMAIN, object_id, variables) def turn_off(hass, entity_id): @@ -80,7 +81,7 @@ def setup(hass, config): if script.is_on: _LOGGER.warning("Script %s already running.", entity_id) return - script.turn_on() + script.turn_on(variables=service.data) for object_id, cfg in config[DOMAIN].items(): alias = cfg.get(CONF_ALIAS, object_id) @@ -92,9 +93,9 @@ def setup(hass, config): def turn_on_service(service): """Call a service to turn script on.""" # We could turn on script directly here, but we only want to offer - # one way to do it. Otherwise no easy way to call invocations. + # one way to do it. Otherwise no easy way to detect invocations. for script in component.extract_from_service(service): - turn_on(hass, script.entity_id) + turn_on(hass, script.entity_id, service.data.get(ATTR_VARIABLES)) def turn_off_service(service): """Cancel a script.""" @@ -151,7 +152,7 @@ class ScriptEntity(ToggleEntity): def turn_on(self, **kwargs): """Turn the entity on.""" - self.script.run() + self.script.run(kwargs.get(ATTR_VARIABLES)) def turn_off(self, **kwargs): """Turn script off.""" diff --git a/tests/components/test_script.py b/tests/components/test_script.py index 90165ada294..30cf69d7922 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -50,11 +50,11 @@ class TestScriptComponent(unittest.TestCase): def test_turn_on_service(self): """Verify that the turn_on service.""" event = 'test_event' - calls = [] + events = [] def record_event(event): """Add recorded event to set.""" - calls.append(event) + events.append(event) self.hass.bus.listen(event, record_event) @@ -75,21 +75,21 @@ class TestScriptComponent(unittest.TestCase): script.turn_on(self.hass, ENTITY_ID) self.hass.pool.block_till_done() self.assertTrue(script.is_on(self.hass, ENTITY_ID)) - self.assertEqual(0, len(calls)) + self.assertEqual(0, len(events)) # Calling turn_on a second time should not advance the script script.turn_on(self.hass, ENTITY_ID) self.hass.pool.block_till_done() - self.assertEqual(0, len(calls)) + self.assertEqual(0, len(events)) def test_toggle_service(self): """Test the toggling of a service.""" event = 'test_event' - calls = [] + events = [] def record_event(event): """Add recorded event to set.""" - calls.append(event) + events.append(event) self.hass.bus.listen(event, record_event) @@ -110,9 +110,50 @@ class TestScriptComponent(unittest.TestCase): script.toggle(self.hass, ENTITY_ID) self.hass.pool.block_till_done() self.assertTrue(script.is_on(self.hass, ENTITY_ID)) - self.assertEqual(0, len(calls)) + self.assertEqual(0, len(events)) script.toggle(self.hass, ENTITY_ID) self.hass.pool.block_till_done() self.assertFalse(script.is_on(self.hass, ENTITY_ID)) - self.assertEqual(0, len(calls)) + self.assertEqual(0, len(events)) + + def test_passing_variables(self): + """Test different ways of passing in variables.""" + calls = [] + + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + self.hass.services.register('test', 'script', record_call) + + assert _setup_component(self.hass, 'script', { + 'script': { + 'test': { + 'sequence': { + 'service': 'test.script', + 'data_template': { + 'hello': '{{ greeting }}', + }, + }, + }, + }, + }) + + script.turn_on(self.hass, ENTITY_ID, { + 'greeting': 'world' + }) + + self.hass.pool.block_till_done() + + assert len(calls) == 1 + assert calls[-1].data['hello'] == 'world' + + self.hass.services.call('script', 'test', { + 'greeting': 'universe', + }) + + self.hass.pool.block_till_done() + + assert len(calls) == 2 + assert calls[-1].data['hello'] == 'universe' From 612a017bc6138b4b0b292111fb2e538889cd85a9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Apr 2016 22:36:14 -0400 Subject: [PATCH 015/101] Automation: Allow embedding script definition --- .../components/automation/__init__.py | 9 ++++--- tests/components/automation/test_init.py | 26 +++++++++++++++++++ tests/helpers/test_script.py | 1 - 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8cbaf35a5c4..3ba5596fb4d 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -11,8 +11,7 @@ import voluptuous as vol from homeassistant.bootstrap import prepare_setup_platform from homeassistant.const import CONF_PLATFORM from homeassistant.components import logbook -from homeassistant.helpers import extract_domain_configs -from homeassistant.helpers.service import call_from_config +from homeassistant.helpers import extract_domain_configs, script from homeassistant.loader import get_platform import homeassistant.helpers.config_validation as cv @@ -88,7 +87,7 @@ PLATFORM_SCHEMA = vol.Schema({ vol.Required(CONF_CONDITION_TYPE, default=DEFAULT_CONDITION_TYPE): vol.All(vol.Lower, vol.Any(CONDITION_TYPE_AND, CONDITION_TYPE_OR)), CONF_CONDITION: _CONDITION_SCHEMA, - vol.Required(CONF_ACTION): cv.SERVICE_SCHEMA, + vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, }) @@ -122,11 +121,13 @@ def _setup_automation(hass, config_block, name, config): def _get_action(hass, config, name): """Return an action based on a configuration.""" + script_obj = script.Script(hass, config, name) + def action(variables=None): """Action to be executed.""" _LOGGER.info('Executing %s', name) logbook.log_entry(hass, name, 'has been triggered', DOMAIN) - call_from_config(hass, config, variables=variables) + script_obj.run(variables) return action diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index f6d33c18071..8e06f524d0e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -316,3 +316,29 @@ class TestAutomation(unittest.TestCase): self.hass.bus.fire('test_event_2') self.hass.pool.block_till_done() self.assertEqual(2, len(self.calls)) + + def test_automation_calling_two_actions(self): + """Test if we can call two actions from automation definition.""" + self.assertTrue(_setup_component(self.hass, automation.DOMAIN, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'event', + 'event_type': 'test_event', + }, + + 'action': [{ + 'service': 'test.automation', + 'data': {'position': 0}, + }, { + 'service': 'test.automation', + 'data': {'position': 1}, + }], + } + })) + + self.hass.bus.fire('test_event') + self.hass.pool.block_till_done() + + assert len(self.calls) == 2 + assert self.calls[0].data['position'] == 0 + assert self.calls[1].data['position'] == 1 diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 492b62906df..47af833223f 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3,7 +3,6 @@ from datetime import timedelta import unittest -from homeassistant.bootstrap import _setup_component import homeassistant.util.dt as dt_util from homeassistant.helpers import script From 4a5411a957629b885c856b6f2e18981449a208f9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 22 Apr 2016 05:30:30 -0400 Subject: [PATCH 016/101] Allow calling scripts from Alexa --- homeassistant/components/alexa.py | 6 +++--- homeassistant/helpers/script.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index bb9e1816a68..d6178b4744c 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -8,8 +8,7 @@ import enum import logging from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY -from homeassistant.helpers.service import call_from_config -from homeassistant.helpers import template +from homeassistant.helpers import template, script DOMAIN = 'alexa' DEPENDENCIES = ['http'] @@ -91,7 +90,8 @@ def _handle_alexa(handler, path_match, data): card['content']) if action is not None: - call_from_config(handler.server.hass, action, True, response.variables) + script.call_from_config(handler.server.hass, action, + response.variables) handler.write_json(response.as_dict()) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0025ddd61df..6c938fd4032 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -20,9 +20,9 @@ CONF_EVENT_DATA = "event_data" CONF_DELAY = "delay" -def call_from_config(hass, config): +def call_from_config(hass, config, variables=None): """Call a script based on a config entry.""" - Script(hass, config).run() + Script(hass, config).run(variables) class Script(): From 1c6a2f87ebd75d69857590ec3918d65ee6468b81 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 22 Apr 2016 15:48:40 +0200 Subject: [PATCH 017/101] Add link to docs --- homeassistant/components/feedreader.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index b5fec104bbe..13ec66a890d 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -1,4 +1,9 @@ -"""RSS/Atom feed reader for Home Assistant.""" +""" +Support for RSS/Atom feed. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/feedreader/ +""" from datetime import datetime from logging import getLogger import voluptuous as vol From a4083bab1ab543c78296d56dac9d1fade4e50edd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Fri, 22 Apr 2016 20:02:54 +0200 Subject: [PATCH 018/101] new version and changed "key" --- homeassistant/components/switch/verisure.py | 2 +- homeassistant/components/verisure.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index 168c34c7d7a..75833c4016c 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hub.update_smartplugs() switches = [] switches.extend([ - VerisureSmartplug(value.id) + VerisureSmartplug(value.deviceLabel) for value in hub.smartplug_status.values()]) add_devices(switches) diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 57d64581bc1..963367eed7a 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -23,7 +23,7 @@ DISCOVER_SWITCHES = 'verisure.switches' DISCOVER_ALARMS = 'verisure.alarm_control_panel' DISCOVER_LOCKS = 'verisure.lock' -REQUIREMENTS = ['vsure==0.7.1'] +REQUIREMENTS = ['vsure==0.8.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 10549b3d715..40214215ce3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -313,7 +313,7 @@ urllib3 uvcclient==0.8 # homeassistant.components.verisure -vsure==0.7.1 +vsure==0.8.1 # homeassistant.components.switch.wake_on_lan wakeonlan==0.2.2 From cc42f2d8be34bde3390c526ae40f730cd240f965 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Apr 2016 06:30:34 +0200 Subject: [PATCH 019/101] Show error message frontend if can't find server (#1886) --- .../components/frontend/index.html.template | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/index.html.template b/homeassistant/components/frontend/index.html.template index e21d00e86bc..cedebe1bbb4 100644 --- a/homeassistant/components/frontend/index.html.template +++ b/homeassistant/components/frontend/index.html.template @@ -28,20 +28,55 @@ left: 0; right: 0; bottom: 0; - margin-bottom: 123px; + margin-bottom: 97px; + font-family: Roboto, sans-serif; + font-size: 0pt; + transition: font-size 2s; + } + + #ha-init-skeleton paper-spinner { + height: 28px; + } + + #ha-init-skeleton a { + color: #03A9F4; + text-decoration: none; + font-weight: bold; + } + + #ha-init-skeleton.error { + font-size: 16px; + } + + #ha-init-skeleton.error img, + #ha-init-skeleton.error paper-spinner { + display: none; } - + + -
+
+ + + Home Assistant had trouble
connecting to the server.

TRY AGAIN +
\ No newline at end of file + } \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index 11311809c1e..40027eaac68 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit 11311809c1eba0ed3b7e26d07a0fdb81b7959e3a +Subproject commit 40027eaac6858d563f89d10544a451bd758351c5 diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index ff572169ded..5cb7633b20f 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -1,5 +1 @@ -!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={exports:{},id:r,loaded:!1};return e[r].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([/*!*************************************!*\ - !*** ./src/service-worker/index.js ***! - \*************************************/ -function(e,t,n){"use strict";var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}]); -//# sourceMappingURL=service_worker.js.map \ No newline at end of file +!function(e){function t(r){if(n[r])return n[r].exports;var s=n[r]={i:r,l:!1,exports:{}};return e[r].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(t.s=192)}({192:function(e,t,n){var r="0.10",s="/",c=["/","/logbook","/history","/map","/devService","/devState","/devEvent","/devInfo","/states"],i=["/static/favicon-192x192.png"];self.addEventListener("install",function(e){e.waitUntil(caches.open(r).then(function(e){return e.addAll(i.concat(s))}))}),self.addEventListener("activate",function(e){}),self.addEventListener("message",function(e){}),self.addEventListener("fetch",function(e){var t=e.request.url.substr(e.request.url.indexOf("/",8));i.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(e.request)})),c.includes(t)&&e.respondWith(caches.open(r).then(function(t){return t.match(s).then(function(n){return n||fetch(e.request).then(function(e){return t.put(s,e.clone()),e})})}))})}}); \ No newline at end of file From cc202b886b28f9d9972824ec425c17cf4eaff74b Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Thu, 28 Apr 2016 17:33:33 +0200 Subject: [PATCH 056/101] ELIQ Online platform cleanup (#1942) --- homeassistant/components/sensor/eliqonline.py | 42 ++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 9c2c8242b68..0a82f5d587c 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -1,5 +1,5 @@ """ -Monitors home energy use for the eliq online service. +Monitors home energy use for the ELIQ Online service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.eliqonline/ @@ -12,17 +12,21 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['eliqonline==1.0.11'] -DEFAULT_NAME = "ELIQ Energy Usage" +REQUIREMENTS = ['eliqonline==1.0.12'] +DEFAULT_NAME = "ELIQ Online" +UNIT_OF_MEASUREMENT = "W" +ICON = "mdi:speedometer" +CONF_CHANNEL_ID = "channel_id" +SCAN_INTERVAL = 60 def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the Eliq sensor.""" + """Setup the ELIQ Online sensor.""" import eliqonline access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME, DEFAULT_NAME) - channel_id = config.get("channel_id") + channel_id = config.get(CONF_CHANNEL_ID) if access_token is None: _LOGGER.error( @@ -32,20 +36,27 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return False api = eliqonline.API(access_token) + + try: + _LOGGER.debug("Probing for access to ELIQ Online API") + api.get_data_now(channelid=channel_id) + except URLError: + _LOGGER.error("Could not access the ELIQ Online API. " + "Is the configuration valid?") + return False + add_devices([EliqSensor(api, channel_id, name)]) class EliqSensor(Entity): - """Implementation of an Eliq sensor.""" + """Implementation of an ELIQ Online sensor.""" def __init__(self, api, channel_id, name): """Initialize the sensor.""" self._name = name - self._unit_of_measurement = "W" self._state = STATE_UNKNOWN - - self.api = api - self.channel_id = channel_id + self._api = api + self._channel_id = channel_id self.update() @property @@ -56,12 +67,12 @@ class EliqSensor(Entity): @property def icon(self): """Return icon.""" - return "mdi:speedometer" + return ICON @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement + return UNIT_OF_MEASUREMENT @property def state(self): @@ -71,7 +82,8 @@ class EliqSensor(Entity): def update(self): """Get the latest data.""" try: - response = self.api.get_data_now(channelid=self.channel_id) + response = self._api.get_data_now(channelid=self._channel_id) self._state = int(response.power) - except (TypeError, URLError): - _LOGGER.error("Could not connect to the eliqonline servers") + _LOGGER.debug("Updated power from server %d W", self._state) + except URLError: + _LOGGER.error("Could not connect to the ELIQ Online API") diff --git a/requirements_all.txt b/requirements_all.txt index 54f1dd93c4b..d5948cd28aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ dnspython3==1.12.0 dweepy==0.2.0 # homeassistant.components.sensor.eliqonline -eliqonline==1.0.11 +eliqonline==1.0.12 # homeassistant.components.thermostat.honeywell evohomeclient==0.2.5 From f8340b94bc443cf56c0d50543927aea720d32658 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 28 Apr 2016 17:57:03 -0700 Subject: [PATCH 057/101] Fix variable name --- homeassistant/components/sensor/uber.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/uber.py b/homeassistant/components/sensor/uber.py index b29cca39c9b..611c0d44b3d 100644 --- a/homeassistant/components/sensor/uber.py +++ b/homeassistant/components/sensor/uber.py @@ -47,8 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): (product_id not in wanted_product_ids): continue dev.append(UberSensor('time', timeandpriceest, product_id, product)) - isMetered = (product['price_details']['estimate'] == "Metered") - if 'price_details' in product and isMetered is False: + is_metered = (product['price_details']['estimate'] == "Metered") + if 'price_details' in product and is_metered is False: dev.append(UberSensor('price', timeandpriceest, product_id, product)) add_devices(dev) From cfd7ca344ed7bfe534dce0c8c5a0a5638bf17e1b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 29 Apr 2016 07:49:46 +0200 Subject: [PATCH 058/101] Upgrade pushbullet.py to 0.10.0 (#1939) --- homeassistant/components/notify/pushbullet.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 1baee8e5287..20a6daebf05 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_API_KEY _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pushbullet.py==0.9.0'] +REQUIREMENTS = ['pushbullet.py==0.10.0'] # pylint: disable=unused-argument diff --git a/requirements_all.txt b/requirements_all.txt index d5948cd28aa..8550b0d7429 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -176,7 +176,7 @@ proliphix==0.1.0 psutil==4.1.0 # homeassistant.components.notify.pushbullet -pushbullet.py==0.9.0 +pushbullet.py==0.10.0 # homeassistant.components.notify.pushetta pushetta==1.0.15 From 069a4b1706b5d4cb7be94e25c38ceb233d2a4ad5 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 30 Apr 2016 15:27:59 +0200 Subject: [PATCH 059/101] Refactor mysensors component * Add MySensorsDeviceEntity class to hold the common attributes, properties and methods for mysensors entities. * Inherit from MySensorsDeviceEntity class in binary_sensor, light, sensor and switch mysensors platforms. * Remove not needed attribute and method for const in GatewayWrapper class. The const attribute is already set in the wrapped object. * Clean up state property for mysensors sensor entities. * Inherit from MySensorsLightRGB in MySensorsLightRGBW class. * Remove use of get_component in mysensors component and platforms. * Clean up update method in MySensorsDeviceEntity class. --- .../components/binary_sensor/mysensors.py | 104 +-------------- homeassistant/components/light/mysensors.py | 77 ++--------- homeassistant/components/mysensors.py | 120 +++++++++++++++--- homeassistant/components/sensor/mysensors.py | 105 +-------------- homeassistant/components/switch/mysensors.py | 100 +-------------- 5 files changed, 127 insertions(+), 379 deletions(-) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 3cc9798f288..d7b1a82188e 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -6,10 +6,10 @@ https://home-assistant.io/components/binary_sensor.mysensors/ """ import logging +from homeassistant.components import mysensors from homeassistant.components.binary_sensor import (SENSOR_CLASSES, BinarySensorDevice) -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON -from homeassistant.loader import get_component +from homeassistant.const import STATE_ON _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] @@ -22,8 +22,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - mysensors = get_component('mysensors') - for gateway in mysensors.GATEWAYS.values(): # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. @@ -48,81 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): map_sv_types, devices, add_devices, MySensorsBinarySensor)) -class MySensorsBinarySensor(BinarySensorDevice): - """Represent the value of a MySensors child node.""" - - # pylint: disable=too-many-arguments,too-many-instance-attributes - - def __init__( - self, gateway, node_id, child_id, name, value_type, child_type): - """ - Setup class attributes on instantiation. - - Args: - gateway (GatewayWrapper): Gateway object. - node_id (str): Id of node. - child_id (str): Id of child. - name (str): Entity name. - value_type (str): Value type of child. Value is entity state. - child_type (str): Child type of child. - - Attributes: - gateway (GatewayWrapper): Gateway object. - node_id (str): Id of node. - child_id (str): Id of child. - _name (str): Entity name. - value_type (str): Value type of child. Value is entity state. - child_type (str): Child type of child. - battery_level (int): Node battery level. - _values (dict): Child values. Non state values set as state attributes. - mysensors (module): Mysensors main component module. - """ - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - self.child_type = child_type - self.battery_level = 0 - self._values = {} - self.mysensors = get_component('mysensors') - - @property - def should_poll(self): - """Mysensor gateway pushes its state to HA.""" - return False - - @property - def name(self): - """The name of this entity.""" - return self._name - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - address = getattr(self.gateway, 'server_address', None) - if address: - device = '{}:{}'.format(address[0], address[1]) - else: - device = self.gateway.port - attr = { - self.mysensors.ATTR_DEVICE: device, - self.mysensors.ATTR_NODE_ID: self.node_id, - self.mysensors.ATTR_CHILD_ID: self.child_id, - ATTR_BATTERY_LEVEL: self.battery_level, - } - - set_req = self.gateway.const.SetReq - - for value_type, value in self._values.items(): - if value_type != self.value_type: - try: - attr[set_req(value_type).name] = value - except ValueError: - _LOGGER.error('value_type %s is not valid for mysensors ' - 'version %s', value_type, - self.gateway.version) - return attr +class MySensorsBinarySensor( + mysensors.MySensorsDeviceEntity, BinarySensorDevice): + """Represent the value of a MySensors Binary Sensor child node.""" @property def is_on(self): @@ -150,23 +76,3 @@ class MySensorsBinarySensor(BinarySensorDevice): }) if class_map.get(self.child_type) in SENSOR_CLASSES: return class_map.get(self.child_type) - - @property - def available(self): - """Return True if entity is available.""" - return self.value_type in self._values - - def update(self): - """Update the controller with the latest values from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - for value_type, value in child.values.items(): - _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) - if value_type == self.gateway.const.SetReq.V_TRIPPED: - self._values[value_type] = STATE_ON if int( - value) == 1 else STATE_OFF - else: - self._values[value_type] = value - - self.battery_level = node.battery_level diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 5a85b047be8..04c0942e1f6 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -6,10 +6,10 @@ https://home-assistant.io/components/light.mysensors/ """ import logging +from homeassistant.components import mysensors from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, Light) -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON -from homeassistant.loader import get_component +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list _LOGGER = logging.getLogger(__name__) @@ -25,8 +25,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - mysensors = get_component('mysensors') - for gateway in mysensors.GATEWAYS.values(): # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. @@ -52,35 +50,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): map_sv_types, devices, add_devices, device_class_map)) -class MySensorsLight(Light): - """Represent the value of a MySensors child node.""" +class MySensorsLight(mysensors.MySensorsDeviceEntity, Light): + """Represent the value of a MySensors Light child node.""" - # pylint: disable=too-many-arguments,too-many-instance-attributes - def __init__( - self, gateway, node_id, child_id, name, value_type, child_type): + def __init__(self, *args): """Setup instance attributes.""" - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - self.battery_level = 0 - self._values = {} + mysensors.MySensorsDeviceEntity.__init__(self, *args) self._state = None self._brightness = None self._rgb = None self._white = None - self.mysensors = get_component('mysensors') - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of this entity.""" - return self._name @property def brightness(self): @@ -97,29 +76,6 @@ class MySensorsLight(Light): """Return the white value in RGBW, value between 0..255.""" return self._white - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - address = getattr(self.gateway, 'server_address', None) - if address: - device = '{}:{}'.format(address[0], address[1]) - else: - device = self.gateway.port - attr = { - self.mysensors.ATTR_DEVICE: device, - self.mysensors.ATTR_NODE_ID: self.node_id, - self.mysensors.ATTR_CHILD_ID: self.child_id, - ATTR_BATTERY_LEVEL: self.battery_level, - } - for value_type, value in self._values.items(): - attr[self.gateway.const.SetReq(value_type).name] = value - return attr - - @property - def available(self): - """Return true if entity is available.""" - return self.value_type in self._values - @property def assumed_state(self): """Return true if unable to access real state of entity.""" @@ -319,28 +275,11 @@ class MySensorsLightRGB(MySensorsLight): self._update_rgb_or_w() -class MySensorsLightRGBW(MySensorsLight): - """RGBW child class to MySensorsLight.""" +class MySensorsLightRGBW(MySensorsLightRGB): + """RGBW child class to MySensorsLightRGB.""" def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) - - def turn_off(self, **kwargs): - """Turn the device off.""" - ret = self._turn_off_rgb_or_w() - ret = self._turn_off_dimmer( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - ret = self._turn_off_light( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - self._turn_off_main( - value_type=ret[ATTR_VALUE_TYPE], value=ret[ATTR_VALUE]) - - def update(self): - """Update the controller with the latest value from a sensor.""" - self._update_main() - self._update_light() - self._update_dimmer() - self._update_rgb_or_w() diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 204b2818de6..9d42316d382 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -8,10 +8,12 @@ import logging import socket import homeassistant.bootstrap as bootstrap -from homeassistant.const import (ATTR_DISCOVERED, ATTR_SERVICE, - CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, +from homeassistant.const import (ATTR_BATTERY_LEVEL, ATTR_DISCOVERED, + ATTR_SERVICE, CONF_OPTIMISTIC, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_PLATFORM_DISCOVERED, TEMP_CELSIUS) + EVENT_PLATFORM_DISCOVERED, STATE_OFF, + STATE_ON, TEMP_CELSIUS) from homeassistant.helpers import validate_config CONF_GATEWAYS = 'gateways' @@ -170,6 +172,8 @@ def pf_callback_factory(map_sv_types, devices, add_devices, entity_class): class GatewayWrapper(object): """Gateway wrapper class.""" + # pylint: disable=too-few-public-methods + def __init__(self, gateway, version, optimistic): """Setup class attributes on instantiation. @@ -182,14 +186,12 @@ class GatewayWrapper(object): _wrapped_gateway (mysensors.SerialGateway): Wrapped gateway. version (str): Version of mysensors API. platform_callbacks (list): Callback functions, one per platform. - const (module): Mysensors API constants. optimistic (bool): Send values to actuators without feedback state. __initialised (bool): True if GatewayWrapper is initialised. """ self._wrapped_gateway = gateway self.version = version self.platform_callbacks = [] - self.const = self.get_const() self.optimistic = optimistic self.__initialised = True @@ -197,9 +199,9 @@ class GatewayWrapper(object): """See if this object has attribute name.""" # Do not use hasattr, it goes into infinite recurrsion if name in self.__dict__: - # this object has it + # This object has the attribute. return getattr(self, name) - # proxy to the wrapped object + # The wrapped object has the attribute. return getattr(self._wrapped_gateway, name) def __setattr__(self, name, value): @@ -211,14 +213,6 @@ class GatewayWrapper(object): else: object.__setattr__(self._wrapped_gateway, name, value) - def get_const(self): - """Get mysensors API constants.""" - if self.version == '1.5': - import mysensors.const_15 as const - else: - import mysensors.const_14 as const - return const - def callback_factory(self): """Return a new callback function.""" def node_update(update_type, node_id): @@ -228,3 +222,99 @@ class GatewayWrapper(object): callback(self, node_id) return node_update + + +class MySensorsDeviceEntity(object): + """Represent a MySensors entity.""" + + # pylint: disable=too-many-arguments,too-many-instance-attributes + + def __init__( + self, gateway, node_id, child_id, name, value_type, child_type): + """ + Setup class attributes on instantiation. + + Args: + gateway (GatewayWrapper): Gateway object. + node_id (str): Id of node. + child_id (str): Id of child. + name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + child_type (str): Child type of child. + + Attributes: + gateway (GatewayWrapper): Gateway object. + node_id (str): Id of node. + child_id (str): Id of child. + _name (str): Entity name. + value_type (str): Value type of child. Value is entity state. + child_type (str): Child type of child. + battery_level (int): Node battery level. + _values (dict): Child values. Non state values set as state attributes. + mysensors (module): Mysensors main component module. + """ + self.gateway = gateway + self.node_id = node_id + self.child_id = child_id + self._name = name + self.value_type = value_type + self.child_type = child_type + self.battery_level = 0 + self._values = {} + + @property + def should_poll(self): + """Mysensor gateway pushes its state to HA.""" + return False + + @property + def name(self): + """The name of this entity.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + address = getattr(self.gateway, 'server_address', None) + if address: + device = '{}:{}'.format(address[0], address[1]) + else: + device = self.gateway.port + attr = { + ATTR_DEVICE: device, + ATTR_NODE_ID: self.node_id, + ATTR_CHILD_ID: self.child_id, + ATTR_BATTERY_LEVEL: self.battery_level, + } + + set_req = self.gateway.const.SetReq + + for value_type, value in self._values.items(): + try: + attr[set_req(value_type).name] = value + except ValueError: + _LOGGER.error('value_type %s is not valid for mysensors ' + 'version %s', value_type, + self.gateway.version) + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self.value_type in self._values + + def update(self): + """Update the controller with the latest value from a sensor.""" + node = self.gateway.sensors[self.node_id] + child = node.children[self.child_id] + self.battery_level = node.battery_level + set_req = self.gateway.const.SetReq + for value_type, value in child.values.items(): + _LOGGER.debug( + "%s: value_type %s, value = %s", self._name, value_type, value) + if value_type in (set_req.V_ARMED, set_req.V_LIGHT, + set_req.V_LOCK_STATUS, set_req.V_TRIPPED): + self._values[value_type] = ( + STATE_ON if int(value) == 1 else STATE_OFF) + else: + self._values[value_type] = value diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index ae959af3ac6..c1eaa913535 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -6,10 +6,9 @@ https://home-assistant.io/components/sensor.mysensors/ """ import logging -from homeassistant.const import (ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, - TEMP_CELSIUS, TEMP_FAHRENHEIT) +from homeassistant.components import mysensors +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] @@ -22,8 +21,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - mysensors = get_component('mysensors') - for gateway in mysensors.GATEWAYS.values(): # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. @@ -74,58 +71,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): map_sv_types, devices, add_devices, MySensorsSensor)) -class MySensorsSensor(Entity): - """Represent the value of a MySensors child node.""" - - # pylint: disable=too-many-arguments,too-many-instance-attributes - - def __init__( - self, gateway, node_id, child_id, name, value_type, child_type): - """Setup class attributes on instantiation. - - Args: - gateway (GatewayWrapper): Gateway object. - node_id (str): Id of node. - child_id (str): Id of child. - name (str): Entity name. - value_type (str): Value type of child. Value is entity state. - child_type (str): Child type of child. - - Attributes: - gateway (GatewayWrapper): Gateway object. - node_id (str): Id of node. - child_id (str): Id of child. - _name (str): Entity name. - value_type (str): Value type of child. Value is entity state. - battery_level (int): Node battery level. - _values (dict): Child values. Non state values set as state attributes. - mysensors (module): Mysensors main component module. - """ - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - self.battery_level = 0 - self._values = {} - self.mysensors = get_component('mysensors') - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of this entity.""" - return self._name +class MySensorsSensor(mysensors.MySensorsDeviceEntity, Entity): + """Represent the value of a MySensors Sensor child node.""" @property def state(self): """Return the state of the device.""" - if not self._values: - return '' - return self._values[self.value_type] + return self._values.get(self.value_type) @property def unit_of_measurement(self): @@ -153,50 +105,3 @@ class MySensorsSensor(Entity): set_req.V_UNIT_PREFIX] unit_map.update({set_req.V_PERCENTAGE: '%'}) return unit_map.get(self.value_type) - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - address = getattr(self.gateway, 'server_address', None) - if address: - device = '{}:{}'.format(address[0], address[1]) - else: - device = self.gateway.port - attr = { - self.mysensors.ATTR_DEVICE: device, - self.mysensors.ATTR_NODE_ID: self.node_id, - self.mysensors.ATTR_CHILD_ID: self.child_id, - ATTR_BATTERY_LEVEL: self.battery_level, - } - - set_req = self.gateway.const.SetReq - - for value_type, value in self._values.items(): - if value_type != self.value_type: - try: - attr[set_req(value_type).name] = value - except ValueError: - _LOGGER.error('value_type %s is not valid for mysensors ' - 'version %s', value_type, - self.gateway.version) - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self.value_type in self._values - - def update(self): - """Update the controller with the latest values from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - for value_type, value in child.values.items(): - _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) - if value_type == self.gateway.const.SetReq.V_TRIPPED: - self._values[value_type] = STATE_ON if int( - value) == 1 else STATE_OFF - else: - self._values[value_type] = value - - self.battery_level = node.battery_level diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 3101f68322d..25cf4945d97 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -6,9 +6,9 @@ https://home-assistant.io/components/switch.mysensors/ """ import logging +from homeassistant.components import mysensors from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON -from homeassistant.loader import get_component +from homeassistant.const import STATE_OFF, STATE_ON _LOGGER = logging.getLogger(__name__) DEPENDENCIES = [] @@ -21,8 +21,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is None: return - mysensors = get_component('mysensors') - for gateway in mysensors.GATEWAYS.values(): # Define the S_TYPES and V_TYPES that the platform should handle as # states. Map them in a dict of lists. @@ -51,77 +49,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): map_sv_types, devices, add_devices, MySensorsSwitch)) -class MySensorsSwitch(SwitchDevice): - """Representation of the value of a MySensors child node.""" - - # pylint: disable=too-many-arguments,too-many-instance-attributes - def __init__( - self, gateway, node_id, child_id, name, value_type, child_type): - """Setup class attributes on instantiation. - - Args: - gateway (GatewayWrapper): Gateway object. - node_id (str): Id of node. - child_id (str): Id of child. - name (str): Entity name. - value_type (str): Value type of child. Value is entity state. - child_type (str): Child type of child. - - Attributes: - gateway (GatewayWrapper): Gateway object - node_id (str): Id of node. - child_id (str): Id of child. - _name (str): Entity name. - value_type (str): Value type of child. Value is entity state. - battery_level (int): Node battery level. - _values (dict): Child values. Non state values set as state attributes. - mysensors (module): Mysensors main component module. - """ - self.gateway = gateway - self.node_id = node_id - self.child_id = child_id - self._name = name - self.value_type = value_type - self.battery_level = 0 - self._values = {} - self.mysensors = get_component('mysensors') - - @property - def should_poll(self): - """Mysensor gateway pushes its state to HA.""" - return False - - @property - def name(self): - """The name of this entity.""" - return self._name - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - address = getattr(self.gateway, 'server_address', None) - if address: - device = '{}:{}'.format(address[0], address[1]) - else: - device = self.gateway.port - attr = { - self.mysensors.ATTR_DEVICE: device, - self.mysensors.ATTR_NODE_ID: self.node_id, - self.mysensors.ATTR_CHILD_ID: self.child_id, - ATTR_BATTERY_LEVEL: self.battery_level, - } - - set_req = self.gateway.const.SetReq - - for value_type, value in self._values.items(): - if value_type != self.value_type: - try: - attr[set_req(value_type).name] = value - except ValueError: - _LOGGER.error('value_type %s is not valid for mysensors ' - 'version %s', value_type, - self.gateway.version) - return attr +class MySensorsSwitch(mysensors.MySensorsDeviceEntity, SwitchDevice): + """Representation of the value of a MySensors Switch child node.""" @property def is_on(self): @@ -148,28 +77,7 @@ class MySensorsSwitch(SwitchDevice): self._values[self.value_type] = STATE_OFF self.update_ha_state() - @property - def available(self): - """Return True if entity is available.""" - return self.value_type in self._values - @property def assumed_state(self): """Return True if unable to access real state of entity.""" return self.gateway.optimistic - - def update(self): - """Update the controller with the latest value from a sensor.""" - node = self.gateway.sensors[self.node_id] - child = node.children[self.child_id] - for value_type, value in child.values.items(): - _LOGGER.debug( - "%s: value_type %s, value = %s", self._name, value_type, value) - if value_type == self.gateway.const.SetReq.V_ARMED or \ - value_type == self.gateway.const.SetReq.V_LIGHT or \ - value_type == self.gateway.const.SetReq.V_LOCK_STATUS: - self._values[value_type] = ( - STATE_ON if int(value) == 1 else STATE_OFF) - else: - self._values[value_type] = value - self.battery_level = node.battery_level From 4710b38fad6756a2d072a71e41750f07ad7d18df Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 2 May 2016 08:05:53 +0200 Subject: [PATCH 060/101] Add support for sensor classes (#1950) --- .../components/binary_sensor/command_line.py | 20 ++++++++++++++++--- .../binary_sensor/test_command_line.py | 7 ++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 17e3fe9c9a7..1acefe973d2 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/binary_sensor.command/ import logging from datetime import timedelta -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import (BinarySensorDevice, + SENSOR_CLASSES) from homeassistant.components.sensor.command_line import CommandSensorData from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.helpers import template @@ -15,6 +16,7 @@ from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Binary Command Sensor" +DEFAULT_SENSOR_CLASS = None DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' @@ -29,28 +31,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error('Missing required variable: "command"') return False + sensor_class = config.get('sensor_class') + if sensor_class not in SENSOR_CLASSES: + _LOGGER.warning('Unknown sensor class: %s', sensor_class) + sensor_class = DEFAULT_SENSOR_CLASS + data = CommandSensorData(config.get('command')) add_devices([CommandBinarySensor( hass, data, config.get('name', DEFAULT_NAME), + sensor_class, config.get('payload_on', DEFAULT_PAYLOAD_ON), config.get('payload_off', DEFAULT_PAYLOAD_OFF), config.get(CONF_VALUE_TEMPLATE) )]) -# pylint: disable=too-many-arguments +# pylint: disable=too-many-arguments, too-many-instance-attributes class CommandBinarySensor(BinarySensorDevice): """Represent a command line binary sensor.""" - def __init__(self, hass, data, name, payload_on, + def __init__(self, hass, data, name, sensor_class, payload_on, payload_off, value_template): """Initialize the Command line binary sensor.""" self._hass = hass self.data = data self._name = name + self._sensor_class = sensor_class self._state = False self._payload_on = payload_on self._payload_off = payload_off @@ -67,6 +76,11 @@ class CommandBinarySensor(BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state + @ property + def sensor_class(self): + """Return the class of the binary sensor.""" + return self._sensor_class + def update(self): """Get the latest data and updates the state.""" self.data.update() diff --git a/tests/components/binary_sensor/test_command_line.py b/tests/components/binary_sensor/test_command_line.py index 4aae3079a07..758911db353 100644 --- a/tests/components/binary_sensor/test_command_line.py +++ b/tests/components/binary_sensor/test_command_line.py @@ -1,4 +1,4 @@ -"""The tests fr the Command line Binary sensor platform.""" +"""The tests for the Command line Binary sensor platform.""" import unittest from homeassistant.const import (STATE_ON, STATE_OFF) @@ -60,7 +60,8 @@ class TestCommandSensorBinarySensor(unittest.TestCase): data = command_line.CommandSensorData('echo 10') entity = command_line.CommandBinarySensor( - self.hass, data, 'test', '1.0', '0', '{{ value | multiply(0.1) }}') + self.hass, data, 'test', None, '1.0', '0', + '{{ value | multiply(0.1) }}') self.assertEqual(STATE_ON, entity.state) @@ -69,6 +70,6 @@ class TestCommandSensorBinarySensor(unittest.TestCase): data = command_line.CommandSensorData('echo 0') entity = command_line.CommandBinarySensor( - self.hass, data, 'test', '1', '0', None) + self.hass, data, 'test', None, '1', '0', None) self.assertEqual(STATE_OFF, entity.state) From b51561cd9bae86e1c06f1bc67810bac9cea8019e Mon Sep 17 00:00:00 2001 From: Brent Date: Mon, 2 May 2016 01:10:21 -0500 Subject: [PATCH 061/101] Fixed slack component bug with getting the channel when sending a message (#1954) --- homeassistant/components/notify/slack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index f010244795a..624683af020 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -51,7 +51,7 @@ class SlackNotificationService(BaseNotificationService): """Send a message to a user.""" import slacker - channel = kwargs.get('channel', self._default_channel) + channel = kwargs.get('target', self._default_channel) try: self.slack.chat.post_message(channel, message) except slacker.Error: From f6df5bc3908d078d2c7dec4f429e16b783cf5dbc Mon Sep 17 00:00:00 2001 From: Tim Harton Date: Mon, 2 May 2016 18:21:28 +1200 Subject: [PATCH 062/101] Mqtt client key auth (#1935) * Made changes so that the mqtt configuration can accept client keys and certs for auth. * Need to figure out how the broker_config works, it's failing tests * Fixed it so all tests passed and ssl feature works for all brokers which are't embedded * Bring into line with pep8 * Added config validation which has allowed me to make the code simpler --- homeassistant/components/mqtt/__init__.py | 35 +++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 52ed7d93847..8d34c153682 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -39,6 +39,9 @@ CONF_KEEPALIVE = 'keepalive' CONF_USERNAME = 'username' CONF_PASSWORD = 'password' CONF_CERTIFICATE = 'certificate' +CONF_CLIENT_KEY = 'client_key' +CONF_CLIENT_CERT = 'client_cert' +CONF_TLS_INSECURE = 'tls_insecure' CONF_PROTOCOL = 'protocol' CONF_STATE_TOPIC = 'state_topic' @@ -78,6 +81,9 @@ def valid_publish_topic(value): _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) _HBMQTT_CONFIG_SCHEMA = vol.Schema(dict) +CLIENT_KEY_AUTH_MSG = 'client_key and client_cert must both be present in ' \ + 'the mqtt broker config' + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_CLIENT_ID): cv.string, @@ -89,6 +95,11 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERTIFICATE): cv.isfile, + vol.Inclusive(CONF_CLIENT_KEY, 'client_key_auth', + msg=CLIENT_KEY_AUTH_MSG): cv.isfile, + vol.Inclusive(CONF_CLIENT_CERT, 'client_key_auth', + msg=CLIENT_KEY_AUTH_MSG): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All(cv.string, vol.In([PROTOCOL_31, PROTOCOL_311])), vol.Optional(CONF_EMBEDDED): _HBMQTT_CONFIG_SCHEMA, @@ -192,19 +203,26 @@ def setup(hass, config): broker_config = _setup_server(hass, config) + broker_in_conf = True if CONF_BROKER in conf else False + # Only auto config if no server config was passed in if broker_config and CONF_EMBEDDED not in conf: broker, port, username, password, certificate, protocol = broker_config + # Embedded broker doesn't have some ssl variables + client_key, client_cert, tls_insecure = None, None, None elif not broker_config and CONF_BROKER not in conf: _LOGGER.error('Unable to start broker and auto-configure MQTT.') return False - if CONF_BROKER in conf: + if broker_in_conf: broker = conf[CONF_BROKER] port = conf[CONF_PORT] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) certificate = conf.get(CONF_CERTIFICATE) + client_key = conf.get(CONF_CLIENT_KEY) + client_cert = conf.get(CONF_CLIENT_CERT) + tls_insecure = conf.get(CONF_TLS_INSECURE) protocol = conf[CONF_PROTOCOL] # For cloudmqtt.com, secured connection, auto fill in certificate @@ -215,8 +233,9 @@ def setup(hass, config): global MQTT_CLIENT try: - MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, username, - password, certificate, protocol) + MQTT_CLIENT = MQTT(hass, broker, port, client_id, keepalive, + username, password, certificate, client_key, + client_cert, tls_insecure, protocol) except socket.error: _LOGGER.exception("Can't connect to the broker. " "Please check your settings and the broker " @@ -267,7 +286,8 @@ class MQTT(object): """Home Assistant MQTT client.""" def __init__(self, hass, broker, port, client_id, keepalive, username, - password, certificate, protocol): + password, certificate, client_key, client_cert, + tls_insecure, protocol): """Initialize Home Assistant MQTT client.""" import paho.mqtt.client as mqtt @@ -287,8 +307,13 @@ class MQTT(object): if username is not None: self._mqttc.username_pw_set(username, password) + if certificate is not None: - self._mqttc.tls_set(certificate) + self._mqttc.tls_set(certificate, certfile=client_cert, + keyfile=client_key) + + if tls_insecure is not None: + self._mqttc.tls_insecure_set(tls_insecure) self._mqttc.on_subscribe = self._mqtt_on_subscribe self._mqttc.on_unsubscribe = self._mqtt_on_unsubscribe From a6a5e4fda2cb4cf47a3524a3439d133e4325bed2 Mon Sep 17 00:00:00 2001 From: Andrew LeCody Date: Mon, 2 May 2016 23:56:15 -0500 Subject: [PATCH 063/101] Forecast.io: Added minutely, hourly, and daily summary (#1943) * Forecast.io: Added minutely, hourly, and daily summary * Resolve pylint issue (Too many instance attributes) --- homeassistant/components/sensor/forecast.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/forecast.py b/homeassistant/components/sensor/forecast.py index f33c6ca719c..70d8be8deb2 100644 --- a/homeassistant/components/sensor/forecast.py +++ b/homeassistant/components/sensor/forecast.py @@ -18,6 +18,9 @@ _LOGGER = logging.getLogger(__name__) # Name, si unit, us unit, ca unit, uk unit, uk2 unit SENSOR_TYPES = { 'summary': ['Summary', None, None, None, None, None], + 'minutely_summary': ['Minutely Summary', None, None, None, None, None], + 'hourly_summary': ['Hourly Summary', None, None, None, None, None], + 'daily_summary': ['Daily Summary', None, None, None, None, None], 'icon': ['Icon', None, None, None, None, None], 'nearest_storm_distance': ['Nearest Storm Distance', 'km', 'm', 'km', 'km', 'm'], @@ -134,11 +137,20 @@ class ForeCastSensor(Entity): import forecastio self.forecast_client.update() - data = self.forecast_client.data + data = self.forecast_client.data.currently() + data_minutely = self.forecast_client.data.minutely() + data_hourly = self.forecast_client.data.hourly() + data_daily = self.forecast_client.data.daily() try: if self.type == 'summary': self._state = data.summary + elif self.type == 'minutely_summary': + self._state = data_minutely.summary + elif self.type == 'hourly_summary': + self._state = data_hourly.summary + elif self.type == 'daily_summary': + self._state = data_daily.summary elif self.type == 'icon': self._state = data.icon elif self.type == 'nearest_storm_distance': @@ -198,5 +210,5 @@ class ForeCastData(object): self.latitude, self.longitude, units=self.units) - self.data = forecast.currently() + self.data = forecast self.unit_system = forecast.json['flags']['units'] From 79653a672d6b3fffda1f85ef8e84d808e7f5d328 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2016 22:05:09 -0700 Subject: [PATCH 064/101] Script cleanup (#1963) --- .../components/automation/__init__.py | 16 ++++- homeassistant/components/automation/state.py | 4 +- homeassistant/components/automation/sun.py | 18 ++---- homeassistant/components/automation/time.py | 3 +- homeassistant/components/automation/zone.py | 9 ++- homeassistant/const.py | 63 +++++++++++-------- homeassistant/helpers/condition.py | 56 ++++++++++------- homeassistant/helpers/config_validation.py | 6 +- 8 files changed, 98 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 37806a6fdac..ef3a9b4a41d 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.bootstrap import prepare_setup_platform -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.components import logbook from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import extract_domain_configs, script, condition @@ -140,6 +140,12 @@ def _process_if(hass, config, p_config, action): """Process if checks.""" cond_type = p_config.get(CONF_CONDITION_TYPE, DEFAULT_CONDITION_TYPE).lower() + + # Deprecated since 0.19 - 5/5/2016 + if cond_type != DEFAULT_CONDITION_TYPE: + _LOGGER.warning('Using condition_type: %s is deprecated. Please use ' + '"condition: or" instead.') + if_configs = p_config.get(CONF_CONDITION) use_trigger = if_configs == CONDITION_USE_TRIGGER_VALUES @@ -148,6 +154,8 @@ def _process_if(hass, config, p_config, action): checks = [] for if_config in if_configs: + # Deprecated except for used by use_trigger_values + # since 0.19 - 5/5/2016 if CONF_PLATFORM in if_config: if not use_trigger: _LOGGER.warning("Please switch your condition configuration " @@ -155,6 +163,12 @@ def _process_if(hass, config, p_config, action): if_config = dict(if_config) if_config[CONF_CONDITION] = if_config.pop(CONF_PLATFORM) + # To support use_trigger_values with state trigger accepting + # multiple entity_ids to monitor. + if_entity_id = if_config.get(ATTR_ENTITY_ID) + if isinstance(if_entity_id, list) and len(if_entity_id) == 1: + if_config[ATTR_ENTITY_ID] = if_entity_id[0] + try: checks.append(condition.from_config(if_config)) except HomeAssistantError as ex: diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 82fcfdab341..03902c1d6e2 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -21,7 +21,7 @@ CONF_FOR = "for" TRIGGER_SCHEMA = vol.All( vol.Schema({ vol.Required(CONF_PLATFORM): 'state', - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): cv.entity_ids, # These are str on purpose. Want to catch YAML conversions CONF_FROM: str, CONF_TO: str, @@ -77,7 +77,7 @@ def trigger(hass, config, action): hass, state_for_listener, dt_util.utcnow() + time_delta) attached_state_for_cancel = track_state_change( - hass, entity_id, state_for_cancel_listener) + hass, entity, state_for_cancel_listener) track_state_change( hass, entity_id, state_automation_listener, from_state, to_state) diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 5a69970ff23..7666847575e 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -9,28 +9,18 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import ( + CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE) from homeassistant.helpers.event import track_sunrise, track_sunset import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['sun'] -CONF_OFFSET = 'offset' -CONF_EVENT = 'event' -CONF_BEFORE = "before" -CONF_BEFORE_OFFSET = "before_offset" -CONF_AFTER = "after" -CONF_AFTER_OFFSET = "after_offset" - -EVENT_SUNSET = 'sunset' -EVENT_SUNRISE = 'sunrise' - _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'sun', - vol.Required(CONF_EVENT): - vol.All(vol.Lower, vol.Any(EVENT_SUNRISE, EVENT_SUNSET)), + vol.Required(CONF_EVENT): cv.sun_event, vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period, }) @@ -51,7 +41,7 @@ def trigger(hass, config, action): }) # Do something to call action - if event == EVENT_SUNRISE: + if event == SUN_EVENT_SUNRISE: track_sunrise(hass, call_action, offset) else: track_sunset(hass, call_action, offset) diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 8ddeb2958d4..ca80536ea96 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -8,14 +8,13 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_AFTER, CONF_PLATFORM from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_time_change CONF_HOURS = "hours" CONF_MINUTES = "minutes" CONF_SECONDS = "seconds" -CONF_AFTER = "after" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index bf325c677cd..5578bf052c4 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -6,21 +6,19 @@ at https://home-assistant.io/components/automation/#zone-trigger """ import voluptuous as vol -from homeassistant.const import MATCH_ALL, CONF_PLATFORM +from homeassistant.const import ( + CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM) from homeassistant.helpers.event import track_state_change from homeassistant.helpers import ( condition, config_validation as cv, location) -CONF_ENTITY_ID = "entity_id" -CONF_ZONE = "zone" -CONF_EVENT = "event" EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER TRIGGER_SCHEMA = vol.Schema({ vol.Required(CONF_PLATFORM): 'zone', - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): cv.entity_ids, vol.Required(CONF_ZONE): cv.entity_id, vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(EVENT_ENTER, EVENT_LEAVE), @@ -56,6 +54,7 @@ def trigger(hass, config, action): 'from_state': from_s, 'to_state': to_s, 'zone': zone_state, + 'event': event, }, }) diff --git a/homeassistant/const.py b/homeassistant/const.py index 70aa0397ab3..525f0e5b304 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -14,35 +14,44 @@ DEVICE_DEFAULT_NAME = "Unnamed Device" WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] -# #### CONFIG #### -CONF_ALIAS = "alias" -CONF_ICON = "icon" -CONF_LATITUDE = "latitude" -CONF_LONGITUDE = "longitude" -CONF_ELEVATION = "elevation" -CONF_TEMPERATURE_UNIT = "temperature_unit" -CONF_NAME = "name" -CONF_TIME_ZONE = "time_zone" -CONF_CUSTOMIZE = "customize" +SUN_EVENT_SUNSET = 'sunset' +SUN_EVENT_SUNRISE = 'sunrise' -CONF_PLATFORM = "platform" -CONF_HOST = "host" -CONF_PORT = "port" -CONF_HOSTS = "hosts" -CONF_USERNAME = "username" -CONF_PASSWORD = "password" -CONF_API_KEY = "api_key" -CONF_ACCESS_TOKEN = "access_token" -CONF_FILENAME = "filename" -CONF_MONITORED_CONDITIONS = 'monitored_conditions' -CONF_OPTIMISTIC = 'optimistic' -CONF_ENTITY_ID = "entity_id" -CONF_ENTITY_NAMESPACE = "entity_namespace" -CONF_SCAN_INTERVAL = "scan_interval" -CONF_VALUE_TEMPLATE = "value_template" -CONF_CONDITION = 'condition' -CONF_BELOW = 'below' +# #### CONFIG #### CONF_ABOVE = 'above' +CONF_ACCESS_TOKEN = 'access_token' +CONF_AFTER = 'after' +CONF_ALIAS = 'alias' +CONF_API_KEY = 'api_key' +CONF_BEFORE = 'before' +CONF_BELOW = 'below' +CONF_CONDITION = 'condition' +CONF_CUSTOMIZE = 'customize' +CONF_ELEVATION = 'elevation' +CONF_ENTITY_ID = 'entity_id' +CONF_ENTITY_NAMESPACE = 'entity_namespace' +CONF_EVENT = 'event' +CONF_FILENAME = 'filename' +CONF_HOST = 'host' +CONF_HOSTS = 'hosts' +CONF_ICON = 'icon' +CONF_LATITUDE = 'latitude' +CONF_LONGITUDE = 'longitude' +CONF_MONITORED_CONDITIONS = 'monitored_conditions' +CONF_NAME = 'name' +CONF_OFFSET = 'offset' +CONF_OPTIMISTIC = 'optimistic' +CONF_PASSWORD = 'password' +CONF_PLATFORM = 'platform' +CONF_PORT = 'port' +CONF_SCAN_INTERVAL = 'scan_interval' +CONF_STATE = 'state' +CONF_TEMPERATURE_UNIT = 'temperature_unit' +CONF_TIME_ZONE = 'time_zone' +CONF_USERNAME = 'username' +CONF_VALUE_TEMPLATE = 'value_template' +CONF_WEEKDAY = 'weekday' +CONF_ZONE = 'zone' # #### EVENTS #### EVENT_HOMEASSISTANT_START = "homeassistant_start" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 571012bdbc8..4785612f114 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,20 +8,15 @@ from homeassistant.components import ( from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION, - WEEKDAYS) + WEEKDAYS, CONF_STATE, CONF_ZONE, CONF_BEFORE, + CONF_AFTER, CONF_WEEKDAY, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, + CONF_BELOW, CONF_ABOVE) from homeassistant.exceptions import TemplateError, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import render import homeassistant.util.dt as dt_util FROM_CONFIG_FORMAT = '{}_from_config' -CONF_BELOW = 'below' -CONF_ABOVE = 'above' -CONF_STATE = 'state' -CONF_ZONE = 'zone' - -EVENT_SUNRISE = 'sunrise' -EVENT_SUNSET = 'sunset' _LOGGER = logging.getLogger(__name__) @@ -47,7 +42,15 @@ def and_from_config(config, config_validation=True): def if_and_condition(hass, variables=None): """Test and condition.""" - return all(check(hass, variables) for check in checks) + for check in checks: + try: + if not check(hass, variables): + return False + except Exception as ex: # pylint: disable=broad-except + _LOGGER.warning('Error during and-condition: %s', ex) + return False + + return True return if_and_condition @@ -60,7 +63,14 @@ def or_from_config(config, config_validation=True): def if_or_condition(hass, variables=None): """Test and condition.""" - return any(check(hass, variables) for check in checks) + for check in checks: + try: + if check(hass, variables): + return True + except Exception as ex: # pylint: disable=broad-except + _LOGGER.warning('Error during or-condition: %s', ex) + + return False return if_or_condition @@ -92,8 +102,6 @@ def numeric_state(hass, entity, below=None, above=None, value_template=None, _LOGGER.warning("Value cannot be processed as a number: %s", value) return False - print(below, value, above) - if below is not None and value > below: return False @@ -157,20 +165,20 @@ def sun(hass, before=None, after=None, before_offset=None, after_offset=None): before_offset = before_offset or timedelta(0) after_offset = after_offset or timedelta(0) - if before == EVENT_SUNRISE and now > (sun_cmp.next_rising(hass) + - before_offset).time(): + if before == SUN_EVENT_SUNRISE and now > (sun_cmp.next_rising(hass) + + before_offset).time(): return False - elif before == EVENT_SUNSET and now > (sun_cmp.next_setting(hass) + - before_offset).time(): + elif before == SUN_EVENT_SUNSET and now > (sun_cmp.next_setting(hass) + + before_offset).time(): return False - if after == EVENT_SUNRISE and now < (sun_cmp.next_rising(hass) + - after_offset).time(): + if after == SUN_EVENT_SUNRISE and now < (sun_cmp.next_rising(hass) + + after_offset).time(): return False - elif after == EVENT_SUNSET and now < (sun_cmp.next_setting(hass) + - after_offset).time(): + elif after == SUN_EVENT_SUNSET and now < (sun_cmp.next_setting(hass) + + after_offset).time(): return False return True @@ -241,9 +249,9 @@ def time_from_config(config, config_validation=True): """Wrap action method with time based condition.""" if config_validation: config = cv.TIME_CONDITION_SCHEMA(config) - before = config.get('before') - after = config.get('after') - weekday = config.get('weekday') + before = config.get(CONF_BEFORE) + after = config.get(CONF_AFTER) + weekday = config.get(CONF_WEEKDAY) def time_if(hass, variables=None): """Validate time based if-condition.""" @@ -281,7 +289,7 @@ def zone_from_config(config, config_validation=True): if config_validation: config = cv.ZONE_CONDITION_SCHEMA(config) entity_id = config.get(CONF_ENTITY_ID) - zone_entity_id = config.get('zone') + zone_entity_id = config.get(CONF_ZONE) def if_in_zone(hass, variables=None): """Test if condition.""" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ca07c08db48..0c79949cdbd 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -8,7 +8,8 @@ from homeassistant.loader import get_platform from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, - CONF_CONDITION, CONF_BELOW, CONF_ABOVE) + CONF_CONDITION, CONF_BELOW, CONF_ABOVE, SUN_EVENT_SUNSET, + SUN_EVENT_SUNRISE) from homeassistant.helpers.entity import valid_entity_id import homeassistant.util.dt as dt_util from homeassistant.util import slugify @@ -25,6 +26,7 @@ latitude = vol.All(vol.Coerce(float), vol.Range(min=-90, max=90), msg='invalid latitude') longitude = vol.All(vol.Coerce(float), vol.Range(min=-180, max=180), msg='invalid longitude') +sun_event = vol.All(vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)) # Adapted from: @@ -298,7 +300,7 @@ STATE_CONDITION_SCHEMA = vol.All(vol.Schema({ SUN_CONDITION_SCHEMA = vol.All(vol.Schema({ vol.Required(CONF_CONDITION): 'sun', - vol.Optional('before'): vol.Any('sunset', 'sunrise'), + vol.Optional('before'): sun_event, vol.Optional('before_offset'): time_period, vol.Optional('after'): vol.All(vol.Lower, vol.Any('sunset', 'sunrise')), vol.Optional('after_offset'): time_period, From f2176e54ba3a1f31abdb15600d6f19db2e3e9e09 Mon Sep 17 00:00:00 2001 From: Kyle Hendricks Date: Tue, 3 May 2016 01:09:27 -0400 Subject: [PATCH 065/101] Add Pioneer AVR media_player support (#1968) --- .coveragerc | 1 + .../components/media_player/pioneer.py | 194 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 homeassistant/components/media_player/pioneer.py diff --git a/.coveragerc b/.coveragerc index babb263d730..173d6ab4865 100644 --- a/.coveragerc +++ b/.coveragerc @@ -110,6 +110,7 @@ omit = homeassistant/components/media_player/mpd.py homeassistant/components/media_player/onkyo.py homeassistant/components/media_player/panasonic_viera.py + homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/plex.py homeassistant/components/media_player/samsungtv.py homeassistant/components/media_player/snapcast.py diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py new file mode 100644 index 00000000000..9e987711b76 --- /dev/null +++ b/homeassistant/components/media_player/pioneer.py @@ -0,0 +1,194 @@ +""" +Support for Pioneer Network Receivers. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.pioneer/ +""" +import logging +import telnetlib + +from homeassistant.components.media_player import ( + DOMAIN, SUPPORT_PAUSE, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + MediaPlayerDevice) +from homeassistant.const import ( + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNKNOWN, + CONF_NAME) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_PIONEER = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE + +MAX_VOLUME = 185 +MAX_SOURCE_NUMBERS = 60 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Pioneer platform.""" + if not config.get(CONF_HOST): + _LOGGER.error( + "Missing required configuration items in %s: %s", + DOMAIN, + CONF_HOST) + return False + + pioneer = PioneerDevice( + config.get(CONF_NAME, "Pioneer AVR"), + config.get(CONF_HOST) + ) + if pioneer.update(): + add_devices([pioneer]) + return True + else: + return False + + +class PioneerDevice(MediaPlayerDevice): + """Representation of a Pioneer device.""" + + # pylint: disable=too-many-public-methods, abstract-method + # pylint: disable=too-many-instance-attributes + def __init__(self, name, host): + """Initialize the Pioneer device.""" + self._name = name + self._host = host + self._pwstate = "PWR1" + self._volume = 0 + self._muted = False + self._selected_source = '' + self._source_name_to_number = {} + self._source_number_to_name = {} + + @classmethod + def telnet_request(cls, telnet, command, expected_prefix): + """Execute `command` and return the response.""" + telnet.write(command.encode("ASCII") + b"\r") + + # The receiver will randomly send state change updates, make sure + # we get the response we are looking for + for _ in range(3): + result = telnet.read_until(b"\r\n", timeout=0.2).decode("ASCII") \ + .strip() + if result.startswith(expected_prefix): + return result + + return None + + def telnet_command(self, command): + """Establish a telnet connection and sends `command`.""" + telnet = telnetlib.Telnet(self._host) + telnet.write(command.encode("ASCII") + b"\r") + telnet.read_very_eager() # skip response + telnet.close() + + def update(self): + """Get the latest details from the device.""" + try: + telnet = telnetlib.Telnet(self._host) + except ConnectionRefusedError: + return False + + self._pwstate = self.telnet_request(telnet, "?P", "PWR") + + volume_str = self.telnet_request(telnet, "?V", "VOL") + self._volume = int(volume_str[3:]) / MAX_VOLUME if volume_str else None + + muted_value = self.telnet_request(telnet, "?M", "MUT") + self._muted = (muted_value == "MUT0") if muted_value else None + + # Build the source name dictionaries if necessary + if not self._source_name_to_number: + for i in range(MAX_SOURCE_NUMBERS): + result = self.telnet_request(telnet, + "?RGB" + str(i).zfill(2), + "RGB") + + if not result: + continue + + source_name = result[6:] + source_number = str(i).zfill(2) + + self._source_name_to_number[source_name] = source_number + self._source_number_to_name[source_number] = source_name + + source_number = self.telnet_request(telnet, "?F", "FN") + + if source_number: + self._selected_source = self._source_number_to_name \ + .get(source_number[2:]) + else: + self._selected_source = None + + telnet.close() + return True + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + if self._pwstate == "PWR1": + return STATE_OFF + if self._pwstate == "PWR0": + return STATE_ON + + return STATE_UNKNOWN + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_media_commands(self): + """Flag of media commands that are supported.""" + return SUPPORT_PIONEER + + @property + def source(self): + """Return the current input source.""" + return self._selected_source + + @property + def source_list(self): + """List of available input sources.""" + return list(self._source_name_to_number.keys()) + + def turn_off(self): + """Turn off media player.""" + self.telnet_command("PF") + + def volume_up(self): + """Volume up media player.""" + self.telnet_command("VU") + + def volume_down(self): + """Volume down media player.""" + self.telnet_command("VD") + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + # 60dB max + self.telnet_command(str(round(volume * MAX_VOLUME)).zfill(3) + "VL") + + def mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + self.telnet_command("MO" if mute else "MF") + + def turn_on(self): + """Turn the media player on.""" + self.telnet_command("PO") + + def select_source(self, source): + """Select input source.""" + self.telnet_command(self._source_name_to_number.get(source) + "FN") From 2a972b7fe3f622fa13aded63d98048038ea2f37b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2016 18:19:55 -0700 Subject: [PATCH 066/101] Fix script condition issue (Thanks @bart274) (#1972) --- homeassistant/helpers/script.py | 2 +- tests/helpers/test_script.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index de5951a3ad4..48a4bffc6a3 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -118,7 +118,7 @@ class Script(): def _check_condition(self, action, variables): """Test if condition is matching.""" self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION]) - check = condition.from_config(action)(self.hass, False) + check = condition.from_config(action)(self.hass, variables) self._log("Test condition {}: {}".format(self.last_action, check)) return check diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 38b9b467d00..3fcb144ac1f 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3,6 +3,8 @@ from datetime import timedelta import unittest +# Otherwise can't test just this file (import order issue) +import homeassistant.components # noqa import homeassistant.util.dt as dt_util from homeassistant.helpers import script @@ -233,9 +235,8 @@ class TestScriptHelper(unittest.TestCase): script_obj = script.Script(self.hass, [ {'event': event}, { - 'condition': 'state', - 'entity_id': 'test.entity', - 'state': 'hello', + 'condition': 'template', + 'value_template': '{{ states.test.entity.state == "hello" }}', }, {'event': event}, ]) From b2abe552a0aea2ed32fc800db1fde47c19b1c285 Mon Sep 17 00:00:00 2001 From: deisi Date: Wed, 4 May 2016 03:23:38 +0200 Subject: [PATCH 067/101] Added a switch to control beamers from acer (#1913) --- .coveragerc | 1 + .../components/switch/acer_projector.py | 145 ++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 149 insertions(+) create mode 100644 homeassistant/components/switch/acer_projector.py diff --git a/.coveragerc b/.coveragerc index 173d6ab4865..153d0182f62 100644 --- a/.coveragerc +++ b/.coveragerc @@ -165,6 +165,7 @@ omit = homeassistant/components/sensor/twitch.py homeassistant/components/sensor/uber.py homeassistant/components/sensor/worldclock.py + homeassistant/components/switch/acer_projector.py homeassistant/components/switch/arest.py homeassistant/components/switch/dlink.py homeassistant/components/switch/edimax.py diff --git a/homeassistant/components/switch/acer_projector.py b/homeassistant/components/switch/acer_projector.py new file mode 100644 index 00000000000..e6e1eb76b75 --- /dev/null +++ b/homeassistant/components/switch/acer_projector.py @@ -0,0 +1,145 @@ +""" +Use serial protocol of acer projector to obtain state of the projector. + +This component allows to control almost all projectors from acer using +their RS232 serial communication protocol. +""" + +import logging +import re + +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import (STATE_ON, STATE_OFF, STATE_UNKNOWN, + CONF_NAME, CONF_FILENAME) + +LAMP_HOURS = 'Lamp Hours' +INPUT_SOURCE = 'Input Source' +ECO_MODE = 'ECO Mode' +MODEL = 'Model' +LAMP = 'Lamp' + +# Commands known to the projector +CMD_DICT = {LAMP: '* 0 Lamp ?\r', + LAMP_HOURS: '* 0 Lamp\r', + INPUT_SOURCE: '* 0 Src ?\r', + ECO_MODE: '* 0 IR 052\r', + MODEL: '* 0 IR 035\r', + STATE_ON: '* 0 IR 001\r', + STATE_OFF: '* 0 IR 002\r'} + +_LOGGER = logging.getLogger(__name__) +REQUIREMENTS = ['pyserial<=3.0'] + +ICON = 'mdi:projector' + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Connect with serial port and return Acer Projector.""" + serial_port = config.get(CONF_FILENAME, None) + name = config.get(CONF_NAME, 'Projector') + timeout = config.get('timeout', 1) + write_timeout = config.get('write_timeout', 1) + + if not serial_port: + _LOGGER.error('Missing path of serial device') + return + + devices = [] + devices.append(AcerSwitch(serial_port, name, timeout, write_timeout)) + add_devices_callback(devices) + + +class AcerSwitch(SwitchDevice): + """Represents an Acer Projector as an switch.""" + + def __init__(self, serial_port, name='Projector', + timeout=1, write_timeout=1, **kwargs): + """Init of the Acer projector.""" + import serial + self.ser = serial.Serial(port=serial_port, timeout=timeout, + write_timeout=write_timeout, **kwargs) + self._serial_port = serial_port + self._name = name + self._state = STATE_UNKNOWN + self._attributes = { + LAMP_HOURS: STATE_UNKNOWN, + INPUT_SOURCE: STATE_UNKNOWN, + ECO_MODE: STATE_UNKNOWN, + } + self.update() + + def _write_read(self, msg): + """Write to the projector and read the return.""" + import serial + ret = "" + # Sometimes the projector won't answer for no reason, + # or the projector was disconnected during runtime. + # Thisway the projector can be reconnected and will still + # work + try: + if not self.ser.is_open: + self.ser.open() + msg = msg.encode('utf-8') + self.ser.write(msg) + # size is an experience value there is no real limit. + # AFAIK there is no limit and no end character so + # we will usually need to wait for timeout + ret = self.ser.read_until(size=20).decode('utf-8') + except serial.SerialException: + _LOGGER.error('Problem comunicating with %s', self._serial_port) + self.ser.close() + return ret + + def _write_read_format(self, msg): + """Write msg, obtain awnser and format output.""" + # awnsers are formated as ***\rawnser\r*** + awns = self._write_read(msg) + match = re.search(r'\r(.+)\r', awns) + if match: + return match.group(1) + return STATE_UNKNOWN + + @property + def name(self): + """Return name of the projector.""" + return self._name + + @property + def state(self): + """Return the current state of the projector.""" + return self._state + + @property + def state_attributes(self): + """Return state attributes.""" + return self._attributes + + def update(self): + """Get the latest state from the projector.""" + msg = CMD_DICT[LAMP] + awns = self._write_read_format(msg) + if awns == 'Lamp 1': + self._state = STATE_ON + elif awns == 'Lamp 0': + self._state = STATE_OFF + else: + self._state = STATE_UNKNOWN + + for key in self._attributes.keys(): + msg = CMD_DICT.get(key, None) + if msg: + awns = self._write_read_format(msg) + self._attributes[key] = awns + + def turn_on(self): + """Turn the projector on.""" + msg = CMD_DICT[STATE_ON] + self._write_read(msg) + self._state = STATE_ON + + def turn_off(self): + """Turn the projector off.""" + msg = CMD_DICT[STATE_OFF] + self._write_read(msg) + self._state = STATE_OFF diff --git a/requirements_all.txt b/requirements_all.txt index 8550b0d7429..082166b5a99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,6 +221,9 @@ pynx584==0.2 # homeassistant.components.sensor.openweathermap pyowm==2.3.1 +# homeassistant.components.switch.acer_projector +pyserial<=3.0 + # homeassistant.components.device_tracker.snmp pysnmp==4.2.5 From 1a59ba735f4f15f69d2adde0e55b794ba07b3004 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Wed, 4 May 2016 03:27:51 +0200 Subject: [PATCH 068/101] Initial HVAC component + Z-Wave platform (#1912) --- homeassistant/components/hvac/__init__.py | 491 +++++++++++++++++++ homeassistant/components/hvac/demo.py | 164 +++++++ homeassistant/components/hvac/services.yaml | 84 ++++ homeassistant/components/hvac/zwave.py | 228 +++++++++ homeassistant/components/thermostat/zwave.py | 30 +- homeassistant/components/zwave.py | 7 + homeassistant/helpers/state.py | 8 + tests/components/hvac/__init__.py | 1 + tests/components/hvac/test_demo.py | 166 +++++++ 9 files changed, 1178 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/hvac/__init__.py create mode 100644 homeassistant/components/hvac/demo.py create mode 100644 homeassistant/components/hvac/services.yaml create mode 100644 homeassistant/components/hvac/zwave.py create mode 100644 tests/components/hvac/__init__.py create mode 100644 tests/components/hvac/test_demo.py diff --git a/homeassistant/components/hvac/__init__.py b/homeassistant/components/hvac/__init__.py new file mode 100644 index 00000000000..d514db364b8 --- /dev/null +++ b/homeassistant/components/hvac/__init__.py @@ -0,0 +1,491 @@ +""" +Provides functionality to interact with hvacs. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/hvac/ +""" +import logging +import os + +from homeassistant.helpers.entity_component import EntityComponent + +from homeassistant.config import load_yaml_config_file +import homeassistant.util as util +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.temperature import convert +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.components import zwave +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_ON, STATE_OFF, STATE_UNKNOWN, + TEMP_CELCIUS) + +DOMAIN = "hvac" + +ENTITY_ID_FORMAT = DOMAIN + ".{}" +SCAN_INTERVAL = 60 + +SERVICE_SET_AWAY_MODE = "set_away_mode" +SERVICE_SET_AUX_HEAT = "set_aux_heat" +SERVICE_SET_TEMPERATURE = "set_temperature" +SERVICE_SET_FAN_MODE = "set_fan_mode" +SERVICE_SET_OPERATION_MODE = "set_operation_mode" +SERVICE_SET_SWING = "set_swing_mode" +SERVICE_SET_HUMIDITY = "set_humidity" + +STATE_HEAT = "heat" +STATE_COOL = "cool" +STATE_IDLE = "idle" +STATE_AUTO = "auto" +STATE_DRY = "dry" +STATE_FAN_ONLY = "fan_only" + +ATTR_CURRENT_TEMPERATURE = "current_temperature" +ATTR_CURRENT_HUMIDITY = "current_humidity" +ATTR_HUMIDITY = "humidity" +ATTR_AWAY_MODE = "away_mode" +ATTR_AUX_HEAT = "aux_heat" +ATTR_FAN = "fan" +ATTR_FAN_LIST = "fan_list" +ATTR_MAX_TEMP = "max_temp" +ATTR_MIN_TEMP = "min_temp" +ATTR_MAX_HUMIDITY = "max_humidity" +ATTR_MIN_HUMIDITY = "min_humidity" +ATTR_OPERATION = "operation_mode" +ATTR_OPERATION_LIST = "operation_list" +ATTR_SWING_MODE = "swing_mode" +ATTR_SWING_LIST = "swing_list" + +_LOGGER = logging.getLogger(__name__) + +DISCOVERY_PLATFORMS = { + zwave.DISCOVER_HVAC: 'zwave' +} + + +def set_away_mode(hass, away_mode, entity_id=None): + """Turn all or specified hvac away mode on.""" + data = { + ATTR_AWAY_MODE: away_mode + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) + + +def set_aux_heat(hass, aux_heat, entity_id=None): + """Turn all or specified hvac auxillary heater on.""" + data = { + ATTR_AUX_HEAT: aux_heat + } + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) + + +def set_temperature(hass, temperature, entity_id=None): + """Set new target temperature.""" + data = {ATTR_TEMPERATURE: temperature} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, data) + + +def set_humidity(hass, humidity, entity_id=None): + """Set new target humidity.""" + data = {ATTR_HUMIDITY: humidity} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) + + +def set_fan_mode(hass, fan, entity_id=None): + """Turn all or specified hvac fan mode on.""" + data = {ATTR_FAN: fan} + + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) + + +def set_operation_mode(hass, operation_mode, entity_id=None): + """Set new target operation mode.""" + data = {ATTR_OPERATION: operation_mode} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) + + +def set_swing_mode(hass, swing_mode, entity_id=None): + """Set new target swing mode.""" + data = {ATTR_SWING_MODE: swing_mode} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_SET_SWING, data) + + +# pylint: disable=too-many-branches +def setup(hass, config): + """Setup hvacs.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, + SCAN_INTERVAL, DISCOVERY_PLATFORMS) + component.setup(config) + + descriptions = load_yaml_config_file( + os.path.join(os.path.dirname(__file__), 'services.yaml')) + + def away_mode_set_service(service): + """Set away mode on target hvacs.""" + target_hvacs = component.extract_from_service(service) + + away_mode = service.data.get(ATTR_AWAY_MODE) + + if away_mode is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_AWAY_MODE, ATTR_AWAY_MODE) + return + + for hvac in target_hvacs: + if away_mode: + hvac.turn_away_mode_on() + else: + hvac.turn_away_mode_off() + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_AWAY_MODE, away_mode_set_service, + descriptions.get(SERVICE_SET_AWAY_MODE)) + + def aux_heat_set_service(service): + """Set auxillary heater on target hvacs.""" + target_hvacs = component.extract_from_service(service) + + aux_heat = service.data.get(ATTR_AUX_HEAT) + + if aux_heat is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT) + return + + for hvac in target_hvacs: + if aux_heat: + hvac.turn_aux_heat_on() + else: + hvac.turn_aux_heat_off() + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_AUX_HEAT, aux_heat_set_service, + descriptions.get(SERVICE_SET_AUX_HEAT)) + + def temperature_set_service(service): + """Set temperature on the target hvacs.""" + target_hvacs = component.extract_from_service(service) + + temperature = util.convert( + service.data.get(ATTR_TEMPERATURE), float) + + if temperature is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE) + return + + for hvac in target_hvacs: + hvac.set_temperature(convert( + temperature, hass.config.temperature_unit, + hvac.unit_of_measurement)) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_TEMPERATURE, temperature_set_service, + descriptions.get(SERVICE_SET_TEMPERATURE)) + + def humidity_set_service(service): + """Set humidity on the target hvacs.""" + target_hvacs = component.extract_from_service(service) + + humidity = service.data.get(ATTR_HUMIDITY) + + if humidity is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_HUMIDITY, ATTR_HUMIDITY) + return + + for hvac in target_hvacs: + hvac.set_humidity(humidity) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_HUMIDITY, humidity_set_service, + descriptions.get(SERVICE_SET_HUMIDITY)) + + def fan_mode_set_service(service): + """Set fan mode on target hvacs.""" + target_hvacs = component.extract_from_service(service) + + fan = service.data.get(ATTR_FAN) + + if fan is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_FAN_MODE, ATTR_FAN) + return + + for hvac in target_hvacs: + hvac.set_fan_mode(fan) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_FAN_MODE, fan_mode_set_service, + descriptions.get(SERVICE_SET_FAN_MODE)) + + def operation_set_service(service): + """Set operating mode on the target hvacs.""" + target_hvacs = component.extract_from_service(service) + + operation_mode = service.data.get(ATTR_OPERATION) + + if operation_mode is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_OPERATION_MODE, ATTR_OPERATION) + return + + for hvac in target_hvacs: + hvac.set_operation(operation_mode) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_OPERATION_MODE, operation_set_service, + descriptions.get(SERVICE_SET_OPERATION_MODE)) + + def swing_set_service(service): + """Set swing mode on the target hvacs.""" + target_hvacs = component.extract_from_service(service) + + swing_mode = service.data.get(ATTR_SWING_MODE) + + if swing_mode is None: + _LOGGER.error( + "Received call to %s without attribute %s", + SERVICE_SET_SWING, ATTR_SWING_MODE) + return + + for hvac in target_hvacs: + hvac.set_swing(swing_mode) + + if hvac.should_poll: + hvac.update_ha_state(True) + + hass.services.register( + DOMAIN, SERVICE_SET_SWING, swing_set_service, + descriptions.get(SERVICE_SET_SWING)) + return True + + +class HvacDevice(Entity): + """Representation of a hvac.""" + + # pylint: disable=too-many-public-methods,no-self-use + @property + def state(self): + """Return the current state.""" + return self.current_operation or STATE_UNKNOWN + + @property + def state_attributes(self): + """Return the optional state attributes.""" + data = { + ATTR_CURRENT_TEMPERATURE: + self._convert_for_display(self.current_temperature), + ATTR_MIN_TEMP: self._convert_for_display(self.min_temp), + ATTR_MAX_TEMP: self._convert_for_display(self.max_temp), + ATTR_TEMPERATURE: + self._convert_for_display(self.target_temperature), + ATTR_HUMIDITY: self.target_humidity, + ATTR_CURRENT_HUMIDITY: self.current_humidity, + ATTR_MIN_HUMIDITY: self.min_humidity, + ATTR_MAX_HUMIDITY: self.max_humidity, + ATTR_FAN_LIST: self.fan_list, + ATTR_OPERATION_LIST: self.operation_list, + ATTR_SWING_LIST: self.swing_list, + ATTR_OPERATION: self.current_operation, + ATTR_FAN: self.current_fan_mode, + ATTR_SWING_MODE: self.current_swing_mode, + + } + + is_away = self.is_away_mode_on + if is_away is not None: + data[ATTR_AWAY_MODE] = STATE_ON if is_away else STATE_OFF + + is_aux_heat = self.is_aux_heat_on + if is_aux_heat is not None: + data[ATTR_AUX_HEAT] = STATE_ON if is_aux_heat else STATE_OFF + + return data + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + raise NotImplementedError + + @property + def current_humidity(self): + """Return the current humidity.""" + return None + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + return None + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return None + + @property + def operation_list(self): + """List of available operation modes.""" + return None + + @property + def current_temperature(self): + """Return the current temperature.""" + return None + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + raise NotImplementedError + + @property + def is_away_mode_on(self): + """Return true if away mode is on.""" + return None + + @property + def is_aux_heat_on(self): + """Return true if away mode is on.""" + return None + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return None + + @property + def fan_list(self): + """List of available fan modes.""" + return None + + @property + def current_swing_mode(self): + """Return the fan setting.""" + return None + + @property + def swing_list(self): + """List of available swing modes.""" + return None + + def set_temperature(self, temperature): + """Set new target temperature.""" + pass + + def set_humidity(self, humidity): + """Set new target humidity.""" + pass + + def set_fan_mode(self, fan): + """Set new target fan mode.""" + pass + + def set_operation(self, operation_mode): + """Set new target operation mode.""" + pass + + def set_swing(self, swing_mode): + """Set new target swing operation.""" + pass + + def turn_away_mode_on(self): + """Turn away mode on.""" + pass + + def turn_away_mode_off(self): + """Turn away mode off.""" + pass + + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + pass + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + pass + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._convert_for_display(7) + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._convert_for_display(35) + + @property + def min_humidity(self): + """Return the minimum humidity.""" + return 30 + + @property + def max_humidity(self): + """Return the maximum humidity.""" + return 99 + + def _convert_for_display(self, temp): + """Convert temperature into preferred units for display purposes.""" + if temp is None: + return None + + value = convert(temp, self.unit_of_measurement, + self.hass.config.temperature_unit) + + if self.hass.config.temperature_unit is TEMP_CELCIUS: + decimal_count = 1 + else: + # Users of fahrenheit generally expect integer units. + decimal_count = 0 + + return round(value, decimal_count) diff --git a/homeassistant/components/hvac/demo.py b/homeassistant/components/hvac/demo.py new file mode 100644 index 00000000000..cb2f0c4b364 --- /dev/null +++ b/homeassistant/components/hvac/demo.py @@ -0,0 +1,164 @@ +""" +Demo platform that offers a fake hvac. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/demo/ +""" +from homeassistant.components.hvac import HvacDevice +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the Demo hvacs.""" + add_devices([ + DemoHvac("HeatPump", 68, TEMP_FAHRENHEIT, None, 77, "Auto Low", + None, None, "Auto", "Heat", None), + DemoHvac("Hvac", 21, TEMP_CELSIUS, True, 22, "On High", + 67, 54, "Off", "Cool", False), + ]) + + +# pylint: disable=too-many-arguments, too-many-public-methods +class DemoHvac(HvacDevice): + """Representation of a demo hvac.""" + + # pylint: disable=too-many-instance-attributes + def __init__(self, name, target_temperature, unit_of_measurement, + away, current_temperature, current_fan_mode, + target_humidity, current_humidity, current_swing_mode, + current_operation, aux): + """Initialize the hvac.""" + self._name = name + self._target_temperature = target_temperature + self._target_humidity = target_humidity + self._unit_of_measurement = unit_of_measurement + self._away = away + self._current_temperature = current_temperature + self._current_humidity = current_humidity + self._current_fan_mode = current_fan_mode + self._current_operation = current_operation + self._aux = aux + self._current_swing_mode = current_swing_mode + self._fan_list = ["On Low", "On High", "Auto Low", "Auto High", "Off"] + self._operation_list = ["Heat", "Cool", "Auto Changeover", "Off"] + self._swing_list = ["Auto", 1, 2, 3, "Off"] + + @property + def should_poll(self): + """Polling not needed for a demo hvac.""" + return False + + @property + def name(self): + """Return the name of the hvac.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._current_humidity + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + return self._target_humidity + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return self._current_operation + + @property + def operation_list(self): + """List of available operation modes.""" + return self._operation_list + + @property + def is_away_mode_on(self): + """Return if away mode is on.""" + return self._away + + @property + def is_aux_heat_on(self): + """Return true if away mode is on.""" + return self._aux + + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self._current_fan_mode + + @property + def fan_list(self): + """List of available fan modes.""" + return self._fan_list + + def set_temperature(self, temperature): + """Set new target temperature.""" + self._target_temperature = temperature + self.update_ha_state() + + def set_humidity(self, humidity): + """Set new target temperature.""" + self._target_humidity = humidity + self.update_ha_state() + + def set_swing(self, swing_mode): + """Set new target temperature.""" + self._current_swing_mode = swing_mode + self.update_ha_state() + + def set_fan_mode(self, fan): + """Set new target temperature.""" + self._current_fan_mode = fan + self.update_ha_state() + + def set_operation(self, operation_mode): + """Set new target temperature.""" + self._current_operation = operation_mode + self.update_ha_state() + + @property + def current_swing_mode(self): + """Return the swing setting.""" + return self._current_swing_mode + + @property + def swing_list(self): + """List of available swing modes.""" + return self._swing_list + + def turn_away_mode_on(self): + """Turn away mode on.""" + self._away = True + self.update_ha_state() + + def turn_away_mode_off(self): + """Turn away mode off.""" + self._away = False + self.update_ha_state() + + def turn_aux_heat_on(self): + """Turn away auxillary heater on.""" + self._aux = True + self.update_ha_state() + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + self._aux = False + self.update_ha_state() diff --git a/homeassistant/components/hvac/services.yaml b/homeassistant/components/hvac/services.yaml new file mode 100644 index 00000000000..5d9f7463399 --- /dev/null +++ b/homeassistant/components/hvac/services.yaml @@ -0,0 +1,84 @@ +set_aux_heat: + description: Turn auxillary heater on/off for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + aux_heat: + description: New value of axillary heater + example: true + +set_away_mode: + description: Turn away mode on/off for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + away_mode: + description: New value of away mode + example: true + +set_temperature: + description: Set target temperature of hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + temperature: + description: New target temperature for hvac + example: 25 + +set_humidity: + description: Set target humidity of hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.kitchen' + + humidity: + description: New target humidity for hvac + example: 60 + +set_fan_mode: + description: Set fan operation for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.nest' + + fan: + description: New value of fan mode + example: On Low + +set_operation_mode: + description: Set operation mode for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.nest' + + operation_mode: + description: New value of operation mode + example: Heat + + +set_swing_mode: + description: Set swing operation for hvac + + fields: + entity_id: + description: Name(s) of entities to change + example: 'hvac.nest' + + swing_mode: + description: New value of swing mode + example: 1 diff --git a/homeassistant/components/hvac/zwave.py b/homeassistant/components/hvac/zwave.py new file mode 100644 index 00000000000..f02a3e74f98 --- /dev/null +++ b/homeassistant/components/hvac/zwave.py @@ -0,0 +1,228 @@ +"""ZWave Hvac device.""" + +# Because we do not compile openzwave on CI +# pylint: disable=import-error +import logging +from homeassistant.components.hvac import DOMAIN +from homeassistant.components.hvac import HvacDevice +from homeassistant.components.zwave import ( + ATTR_NODE_ID, ATTR_VALUE_ID, ZWaveDeviceEntity) +from homeassistant.components import zwave +from homeassistant.const import (TEMP_FAHRENHEIT, TEMP_CELSIUS) + +_LOGGER = logging.getLogger(__name__) + +CONF_NAME = 'name' +DEFAULT_NAME = 'ZWave Hvac' + +REMOTEC = 0x5254 +REMOTEC_ZXT_120 = 0x8377 +REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0) + +WORKAROUND_ZXT_120 = 'zxt_120' + +DEVICE_MAPPINGS = { + REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_ZXT_120 +} + +ZXT_120_SET_TEMP = { + 'Heat': 1, + 'Cool': 2, + 'Dry Air': 8, + 'Auto Changeover': 10 +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the ZWave Hvac devices.""" + if discovery_info is None or zwave.NETWORK is None: + _LOGGER.debug("No discovery_info=%s or no NETWORK=%s", + discovery_info, zwave.NETWORK) + return + + node = zwave.NETWORK.nodes[discovery_info[ATTR_NODE_ID]] + value = node.values[discovery_info[ATTR_VALUE_ID]] + value.set_change_verified(False) + add_devices([ZWaveHvac(value)]) + _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", + discovery_info, zwave.NETWORK) + + +# pylint: disable=too-many-arguments +class ZWaveHvac(ZWaveDeviceEntity, HvacDevice): + """Represents a HeatControl hvac.""" + + # pylint: disable=too-many-public-methods, too-many-instance-attributes + def __init__(self, value): + """Initialize the zwave hvac.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + ZWaveDeviceEntity.__init__(self, value, DOMAIN) + self._node = value.node + self._target_temperature = None + self._current_temperature = None + self._current_operation = None + self._operation_list = None + self._current_operation_state = None + self._current_fan_mode = None + self._fan_list = None + self._current_swing_mode = None + self._swing_list = None + self._unit = None + self._zxt_120 = None + self.update_properties() + # register listener + dispatcher.connect( + self.value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + # Make sure that we have values for the key before converting to int + if (value.node.manufacturer_id.strip() and + value.node.product_id.strip()): + specific_sensor_key = (int(value.node.manufacturer_id, 16), + int(value.node.product_id, 16), + value.index) + + if specific_sensor_key in DEVICE_MAPPINGS: + if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_ZXT_120: + _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat as HVAC") + self._zxt_120 = 1 + + def value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.node == value.node: + self.update_properties() + self.update_ha_state(True) + _LOGGER.debug("Value changed on network %s", value) + + def update_properties(self): + """Callback on data change for the registered node/value pair.""" + # Set point + for value in self._node.get_values(class_id=0x43).values(): + if int(value.data) != 0: + self._target_temperature = int(value.data) + # Operation Mode + for value in self._node.get_values(class_id=0x40).values(): + self._current_operation = value.data + self._operation_list = list(value.data_items) + _LOGGER.debug("self._operation_list=%s", self._operation_list) + # Current Temp + for value in self._node.get_values(class_id=0x31).values(): + self._current_temperature = int(value.data) + self._unit = value.units + # Fan Mode + fan_class_id = 0x44 if self._zxt_120 else 0x42 + _LOGGER.debug("fan_class_id=%s", fan_class_id) + for value in self._node.get_values(class_id=fan_class_id).values(): + self._current_operation_state = value.data + self._fan_list = list(value.data_items) + _LOGGER.debug("self._fan_list=%s", self._fan_list) + _LOGGER.debug("self._current_operation_state=%s", + self._current_operation_state) + # Swing mode + if self._zxt_120 == 1: + for value in self._node.get_values(class_id=0x70).values(): + if value.command_class == 112 and value.index == 33: + self._current_swing_mode = value.data + self._swing_list = [0, 1] + _LOGGER.debug("self._swing_list=%s", self._swing_list) + + @property + def should_poll(self): + """No polling on ZWave.""" + return False + + @property + def current_fan_mode(self): + """Return the fan speed set.""" + return self._current_operation_state + + @property + def fan_list(self): + """List of available fan modes.""" + return self._fan_list + + @property + def current_swing_mode(self): + """Return the swing mode set.""" + return self._current_swing_mode + + @property + def swing_list(self): + """List of available swing modes.""" + return self._swing_list + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + unit = self._unit + if unit == 'C': + return TEMP_CELSIUS + elif unit == 'F': + return TEMP_FAHRENHEIT + else: + _LOGGER.exception("unit_of_measurement=%s is not valid", + unit) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def current_operation(self): + """Return the current operation mode.""" + return self._current_operation + + @property + def operation_list(self): + """List of available operation modes.""" + return self._operation_list + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + def set_temperature(self, temperature): + """Set new target temperature.""" + for value in self._node.get_values(class_id=0x43).values(): + if value.command_class != 67: + continue + if self._zxt_120: + # ZXT-120 does not support get setpoint + self._target_temperature = temperature + if ZXT_120_SET_TEMP.get(self._current_operation) \ + != value.index: + continue + # ZXT-120 responds only to whole int + value.data = int(round(temperature, 0)) + else: + value.data = int(temperature) + + def set_fan_mode(self, fan): + """Set new target fan mode.""" + for value in self._node.get_values(class_id=0x44).values(): + if value.command_class == 68 and value.index == 0: + value.data = bytes(fan, 'utf-8') + + def set_operation(self, operation_mode): + """Set new target operation mode.""" + for value in self._node.get_values(class_id=0x40).values(): + if value.command_class == 64 and value.index == 0: + value.data = bytes(operation_mode, 'utf-8') + + def set_swing(self, swing_mode): + """Set new target swing mode.""" + if self._zxt_120 == 1: + for value in self._node.get_values(class_id=0x70).values(): + if value.command_class == 112 and value.index == 33: + value.data = int(swing_mode) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._convert_for_display(19) + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._convert_for_display(30) diff --git a/homeassistant/components/thermostat/zwave.py b/homeassistant/components/thermostat/zwave.py index c5f00fbd457..ef72fc55c10 100644 --- a/homeassistant/components/thermostat/zwave.py +++ b/homeassistant/components/thermostat/zwave.py @@ -2,6 +2,7 @@ # Because we do not compile openzwave on CI # pylint: disable=import-error +import logging from homeassistant.components.thermostat import DOMAIN from homeassistant.components.thermostat import ( ThermostatDevice, @@ -9,19 +10,46 @@ from homeassistant.components.thermostat import ( from homeassistant.components import zwave from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS +_LOGGER = logging.getLogger(__name__) + CONF_NAME = 'name' DEFAULT_NAME = 'ZWave Thermostat' +REMOTEC = 0x5254 +REMOTEC_ZXT_120 = 0x8377 +REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120, 0) + +WORKAROUND_IGNORE = 'ignore' + +DEVICE_MAPPINGS = { + REMOTEC_ZXT_120_THERMOSTAT: WORKAROUND_IGNORE +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the ZWave thermostats.""" if discovery_info is None or zwave.NETWORK is None: + _LOGGER.debug("No discovery_info=%s or no NETWORK=%s", + discovery_info, zwave.NETWORK) return node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] value.set_change_verified(False) - add_devices([ZWaveThermostat(value)]) + # Make sure that we have values for the key before converting to int + if (value.node.manufacturer_id.strip() and + value.node.product_id.strip()): + specific_sensor_key = (int(value.node.manufacturer_id, 16), + int(value.node.product_id, 16), + value.index) + if specific_sensor_key in DEVICE_MAPPINGS: + if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_IGNORE: + _LOGGER.debug("Remotec ZXT-120 Zwave Thermostat, ignoring") + return + else: + add_devices([ZWaveThermostat(value)]) + _LOGGER.debug("discovery_info=%s and zwave.NETWORK=%s", + discovery_info, zwave.NETWORK) # pylint: disable=too-many-arguments diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 94e8b85df6b..1a15c1e3baf 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -39,6 +39,7 @@ DISCOVER_SWITCHES = "zwave.switch" DISCOVER_LIGHTS = "zwave.light" DISCOVER_BINARY_SENSORS = 'zwave.binary_sensor' DISCOVER_THERMOSTATS = 'zwave.thermostat' +DISCOVER_HVAC = 'zwave.hvac' EVENT_SCENE_ACTIVATED = "zwave.scene_activated" @@ -51,6 +52,7 @@ COMMAND_CLASS_METER = 50 COMMAND_CLASS_BATTERY = 128 COMMAND_CLASS_ALARM = 113 # 0x71 COMMAND_CLASS_THERMOSTAT_SETPOINT = 67 # 0x43 +COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 # 0x44 GENRE_WHATEVER = None GENRE_USER = "User" @@ -91,6 +93,11 @@ DISCOVERY_COMPONENTS = [ [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, GENRE_WHATEVER), + ('hvac', + DISCOVER_HVAC, + [COMMAND_CLASS_THERMOSTAT_FAN_MODE], + TYPE_WHATEVER, + GENRE_WHATEVER), ] diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 1a3421cecaf..b8585621913 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -15,6 +15,10 @@ from homeassistant.components.sun import ( from homeassistant.components.thermostat import ( ATTR_AWAY_MODE, ATTR_FAN, SERVICE_SET_AWAY_MODE, SERVICE_SET_FAN_MODE, SERVICE_SET_TEMPERATURE) +from homeassistant.components.hvac import ( + ATTR_HUMIDITY, ATTR_SWING_MODE, ATTR_OPERATION, ATTR_AUX_HEAT, + SERVICE_SET_HUMIDITY, SERVICE_SET_SWING, + SERVICE_SET_OPERATION_MODE, SERVICE_SET_AUX_HEAT) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, @@ -43,6 +47,10 @@ SERVICE_ATTRIBUTES = { SERVICE_SET_AWAY_MODE: [ATTR_AWAY_MODE], SERVICE_SET_FAN_MODE: [ATTR_FAN], SERVICE_SET_TEMPERATURE: [ATTR_TEMPERATURE], + SERVICE_SET_HUMIDITY: [ATTR_HUMIDITY], + SERVICE_SET_SWING: [ATTR_SWING_MODE], + SERVICE_SET_OPERATION_MODE: [ATTR_OPERATION], + SERVICE_SET_AUX_HEAT: [ATTR_AUX_HEAT], SERVICE_SELECT_SOURCE: [ATTR_INPUT_SOURCE], } diff --git a/tests/components/hvac/__init__.py b/tests/components/hvac/__init__.py new file mode 100644 index 00000000000..6a5696cfb62 --- /dev/null +++ b/tests/components/hvac/__init__.py @@ -0,0 +1 @@ +"""The tests for hvac component.""" diff --git a/tests/components/hvac/test_demo.py b/tests/components/hvac/test_demo.py new file mode 100644 index 00000000000..59a51d52011 --- /dev/null +++ b/tests/components/hvac/test_demo.py @@ -0,0 +1,166 @@ +"""The tests for the demo hvac.""" +import unittest + +from homeassistant.const import ( + TEMP_CELSIUS, +) +from homeassistant.components import hvac + +from tests.common import get_test_home_assistant + + +ENTITY_HVAC = 'hvac.hvac' + + +class TestDemoHvac(unittest.TestCase): + """Test the demo hvac.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.temperature_unit = TEMP_CELSIUS + self.assertTrue(hvac.setup(self.hass, {'hvac': { + 'platform': 'demo', + }})) + + def tearDown(self): # pylint: disable=invalid-name + """Stop down everything that was started.""" + self.hass.stop() + + def test_setup_params(self): + """Test the inititial parameters.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual(21, state.attributes.get('temperature')) + self.assertEqual('on', state.attributes.get('away_mode')) + self.assertEqual(22, state.attributes.get('current_temperature')) + self.assertEqual("On High", state.attributes.get('fan')) + self.assertEqual(67, state.attributes.get('humidity')) + self.assertEqual(54, state.attributes.get('current_humidity')) + self.assertEqual("Off", state.attributes.get('swing_mode')) + self.assertEqual("Cool", state.attributes.get('operation_mode')) + self.assertEqual('off', state.attributes.get('aux_heat')) + + def test_default_setup_params(self): + """Test the setup with default parameters.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual(7, state.attributes.get('min_temp')) + self.assertEqual(35, state.attributes.get('max_temp')) + self.assertEqual(30, state.attributes.get('min_humidity')) + self.assertEqual(99, state.attributes.get('max_humidity')) + + def test_set_target_temp_bad_attr(self): + """Test setting the target temperature without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual(21, state.attributes.get('temperature')) + hvac.set_temperature(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual(21, state.attributes.get('temperature')) + + def test_set_target_temp(self): + """Test the setting of the target temperature.""" + hvac.set_temperature(self.hass, 30, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual(30.0, state.attributes.get('temperature')) + + def test_set_target_humidity_bad_attr(self): + """Test setting the target humidity without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual(67, state.attributes.get('humidity')) + hvac.set_humidity(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual(67, state.attributes.get('humidity')) + + def test_set_target_humidity(self): + """Test the setting of the target humidity.""" + hvac.set_humidity(self.hass, 64, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual(64.0, state.attributes.get('humidity')) + + def test_set_fan_mode_bad_attr(self): + """Test setting fan mode without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual("On High", state.attributes.get('fan')) + hvac.set_fan_mode(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual("On High", state.attributes.get('fan')) + + def test_set_fan_mode(self): + """Test setting of new fan mode.""" + hvac.set_fan_mode(self.hass, "On Low", ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual("On Low", state.attributes.get('fan')) + + def test_set_swing_mode_bad_attr(self): + """Test setting swing mode without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual("Off", state.attributes.get('swing_mode')) + hvac.set_swing_mode(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual("Off", state.attributes.get('swing_mode')) + + def test_set_swing(self): + """Test setting of new swing mode.""" + hvac.set_swing_mode(self.hass, "Auto", ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual("Auto", state.attributes.get('swing_mode')) + + def test_set_operation_bad_attr(self): + """Test setting operation mode without required attribute.""" + self.assertEqual("Cool", self.hass.states.get(ENTITY_HVAC).state) + hvac.set_operation_mode(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual("Cool", self.hass.states.get(ENTITY_HVAC).state) + + def test_set_operation(self): + """Test setting of new operation mode.""" + hvac.set_operation_mode(self.hass, "Heat", ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual("Heat", self.hass.states.get(ENTITY_HVAC).state) + + def test_set_away_mode_bad_attr(self): + """Test setting the away mode without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('on', state.attributes.get('away_mode')) + hvac.set_away_mode(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual('on', state.attributes.get('away_mode')) + + def test_set_away_mode_on(self): + """Test setting the away mode on/true.""" + hvac.set_away_mode(self.hass, True, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('on', state.attributes.get('away_mode')) + + def test_set_away_mode_off(self): + """Test setting the away mode off/false.""" + hvac.set_away_mode(self.hass, False, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('off', state.attributes.get('away_mode')) + + def test_set_aux_heat_bad_attr(self): + """Test setting the auxillary heater without required attribute.""" + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('off', state.attributes.get('aux_heat')) + hvac.set_aux_heat(self.hass, None, ENTITY_HVAC) + self.hass.pool.block_till_done() + self.assertEqual('off', state.attributes.get('aux_heat')) + + def test_set_aux_heat_on(self): + """Test setting the axillary heater on/true.""" + hvac.set_aux_heat(self.hass, True, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('on', state.attributes.get('aux_heat')) + + def test_set_aux_heat_off(self): + """Test setting the auxillary heater off/false.""" + hvac.set_aux_heat(self.hass, False, ENTITY_HVAC) + self.hass.pool.block_till_done() + state = self.hass.states.get(ENTITY_HVAC) + self.assertEqual('off', state.attributes.get('aux_heat')) From 6d9254ce25521dd8dc9fcc561484289a1c0a7fdd Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 3 May 2016 21:35:11 -0400 Subject: [PATCH 069/101] Support for OctoPrint sensors (#1924) --- .coveragerc | 3 + .../components/binary_sensor/octoprint.py | 111 ++++++++++++++++ homeassistant/components/octoprint.py | 121 ++++++++++++++++++ homeassistant/components/sensor/octoprint.py | 118 +++++++++++++++++ 4 files changed, 353 insertions(+) create mode 100644 homeassistant/components/binary_sensor/octoprint.py create mode 100644 homeassistant/components/octoprint.py create mode 100644 homeassistant/components/sensor/octoprint.py diff --git a/.coveragerc b/.coveragerc index 153d0182f62..32cbdc38047 100644 --- a/.coveragerc +++ b/.coveragerc @@ -32,6 +32,9 @@ omit = homeassistant/components/nest.py homeassistant/components/*/nest.py + homeassistant/components/octoprint.py + homeassistant/components/*/octoprint.py + homeassistant/components/rpi_gpio.py homeassistant/components/*/rpi_gpio.py diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py new file mode 100644 index 00000000000..803416ce66c --- /dev/null +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -0,0 +1,111 @@ +""" +Support for monitoring OctoPrint binary sensors. + +Uses OctoPrint REST JSON API to query for monitored variables. +For more details about this component, please refer to the documentation at +http://docs.octoprint.org/en/master/api/ +""" + +import logging +import requests + +from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.loader import get_component + +DEPENDENCIES = ["octoprint"] + +SENSOR_TYPES = { + # API Endpoint, Group, Key, unit + "Printing": ["printer", "state", "printing", None], + "Printing Error": ["printer", "state", "error", None] +} + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the available OctoPrint binary sensors.""" + octoprint = get_component('octoprint') + name = config.get(CONF_NAME, "OctoPrint") + monitored_conditions = config.get("monitored_conditions", + SENSOR_TYPES.keys()) + + devices = [] + for octo_type in monitored_conditions: + if octo_type in SENSOR_TYPES: + new_sensor = OctoPrintBinarySensor(octoprint.OCTOPRINT, + octo_type, + SENSOR_TYPES[octo_type][2], + name, + SENSOR_TYPES[octo_type][3], + SENSOR_TYPES[octo_type][0], + SENSOR_TYPES[octo_type][1], + "flags") + devices.append(new_sensor) + else: + _LOGGER.error("Unknown OctoPrint sensor type: %s", octo_type) + add_devices(devices) + + +# pylint: disable=too-many-instance-attributes +class OctoPrintBinarySensor(BinarySensorDevice): + """Represents an OctoPrint binary sensor.""" + + # pylint: disable=too-many-arguments + def __init__(self, api, condition, sensor_type, sensor_name, + unit, endpoint, group, tool=None): + """Initialize a new OctoPrint sensor.""" + self.sensor_name = sensor_name + if tool is None: + self._name = sensor_name + ' ' + condition + else: + self._name = sensor_name + ' ' + condition + self.sensor_type = sensor_type + self.api = api + self._state = False + self._unit_of_measurement = unit + self.api_endpoint = endpoint + self.api_group = group + self.api_tool = tool + # Set initial state + self.update() + _LOGGER.debug("created OctoPrint binary sensor %r", self) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self.is_on + + @property + def is_on(self): + """Return true if binary sensor is on.""" + if self._state: + return STATE_ON + else: + return STATE_OFF + + @property + def sensor_class(self): + """Return the class of this sensor, from SENSOR_CLASSES.""" + return None + + def update(self): + """Update state of sensor.""" + try: + self._state = self.api.update(self.sensor_type, + self.api_endpoint, + self.api_group, + self.api_tool) + except requests.exceptions.ConnectionError: + # Error calling the api, already logged in api.update() + return + + if self._state is None: + _LOGGER.warning("unable to locate value for %s", self.sensor_type) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py new file mode 100644 index 00000000000..9d7f015a2f4 --- /dev/null +++ b/homeassistant/components/octoprint.py @@ -0,0 +1,121 @@ +""" +Support for monitoring OctoPrint 3D printers. + +Uses OctoPrint REST JSON API to query for monitored variables. +http://docs.octoprint.org/en/master/api/ +""" + +import logging + +import time +import requests + +from homeassistant.components import discovery +from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.helpers import validate_config + +DOMAIN = "octoprint" +OCTOPRINT = None + +_LOGGER = logging.getLogger(__name__) + +DISCOVER_SENSORS = 'octoprint.sensors' +DISCOVER_BINARY_SENSORS = 'octoprint.binary_sensor' + + +def setup(hass, config): + """Set up OctoPrint API.""" + if not validate_config(config, {DOMAIN: [CONF_API_KEY], + DOMAIN: [CONF_HOST]}, + _LOGGER): + return False + + base_url = config[DOMAIN][CONF_HOST] + "/api/" + api_key = config[DOMAIN][CONF_API_KEY] + + global OCTOPRINT + try: + OCTOPRINT = OctoPrintAPI(base_url, api_key) + OCTOPRINT.get("printer") + OCTOPRINT.get("job") + except requests.exceptions.RequestException as conn_err: + _LOGGER.error("Error setting up OctoPrint API: %r", conn_err) + return False + + for component, discovery_service in ( + ('sensor', DISCOVER_SENSORS), + ('binary_sensor', DISCOVER_BINARY_SENSORS)): + discovery.discover(hass, discovery_service, component=component, + hass_config=config) + + return True + + +class OctoPrintAPI(object): + """Simple json wrapper for OctoPrint's api.""" + + def __init__(self, api_url, key): + """Initialize OctoPrint API and set headers needed later.""" + self.api_url = api_url + self.headers = {'content-type': 'application/json', + 'X-Api-Key': key} + self.printer_last_reading = [{}, None] + self.job_last_reading = [{}, None] + + def get_tools(self): + """Get the dynamic list of tools that temperature is monitored on.""" + tools = self.printer_last_reading[0]['temperature'] + return tools.keys() + + def get(self, endpoint): + """Send a get request, and return the response as a dict.""" + now = time.time() + if endpoint == "job": + last_time = self.job_last_reading[1] + if last_time is not None: + if now - last_time < 30.0: + return self.job_last_reading[0] + elif endpoint == "printer": + last_time = self.printer_last_reading[1] + if last_time is not None: + if now - last_time < 30.0: + return self.printer_last_reading[0] + url = self.api_url + endpoint + try: + response = requests.get(url, + headers=self.headers, + timeout=30) + response.raise_for_status() + if endpoint == "job": + self.job_last_reading[0] = response.json() + self.job_last_reading[1] = time.time() + elif endpoint == "printer": + self.printer_last_reading[0] = response.json() + self.printer_last_reading[1] = time.time() + return response.json() + except requests.exceptions.ConnectionError as conn_exc: + _LOGGER.error("Failed to update OctoPrint status. Error: %s", + conn_exc) + raise + + def update(self, sensor_type, end_point, group, tool=None): + """Return the value for sensor_type from the provided endpoint.""" + try: + return get_value_from_json(self.get(end_point), sensor_type, + group, tool) + except requests.exceptions.ConnectionError: + raise + + +# pylint: disable=unused-variable +def get_value_from_json(json_dict, sensor_type, group, tool): + """Return the value for sensor_type from the provided json.""" + if group in json_dict: + if sensor_type in json_dict[group]: + if sensor_type == "target" and json_dict[sensor_type] is None: + return 0 + else: + return json_dict[group][sensor_type] + elif tool is not None: + if sensor_type in json_dict[group][tool]: + return json_dict[group][tool][sensor_type] diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py new file mode 100644 index 00000000000..3e47c4d51fe --- /dev/null +++ b/homeassistant/components/sensor/octoprint.py @@ -0,0 +1,118 @@ +""" +Support for monitoring OctoPrint sensors. + +Uses OctoPrint REST JSON API to query for monitored variables. +For more details about this component, please refer to the documentation at +http://docs.octoprint.org/en/master/api/ +""" + +import logging +import requests + +from homeassistant.const import TEMP_CELSIUS, CONF_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.loader import get_component + +DEPENDENCIES = ["octoprint"] + +SENSOR_TYPES = { + # API Endpoint, Group, Key, unit + "Temperatures": ["printer", "temperature", "*", TEMP_CELSIUS], + "Current State": ["printer", "state", "text", None], + "Job Percentage": ["job", "progress", "completion", "%"], +} + +_LOGGER = logging.getLogger(__name__) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the available OctoPrint sensors.""" + octoprint = get_component('octoprint') + name = config.get(CONF_NAME, "OctoPrint") + monitored_conditions = config.get("monitored_conditions", + SENSOR_TYPES.keys()) + + devices = [] + types = ["actual", "target"] + for octo_type in monitored_conditions: + if octo_type == "Temperatures": + for tool in octoprint.OCTOPRINT.get_tools(): + for temp_type in types: + new_sensor = OctoPrintSensor(octoprint.OCTOPRINT, + temp_type, + temp_type, + name, + SENSOR_TYPES[octo_type][3], + SENSOR_TYPES[octo_type][0], + SENSOR_TYPES[octo_type][1], + tool) + devices.append(new_sensor) + elif octo_type in SENSOR_TYPES: + new_sensor = OctoPrintSensor(octoprint.OCTOPRINT, + octo_type, + SENSOR_TYPES[octo_type][2], + name, + SENSOR_TYPES[octo_type][3], + SENSOR_TYPES[octo_type][0], + SENSOR_TYPES[octo_type][1]) + devices.append(new_sensor) + else: + _LOGGER.error("Unknown OctoPrint sensor type: %s", octo_type) + + add_devices(devices) + + +# pylint: disable=too-many-instance-attributes +class OctoPrintSensor(Entity): + """Represents an OctoPrint sensor.""" + + # pylint: disable=too-many-arguments + def __init__(self, api, condition, sensor_type, sensor_name, + unit, endpoint, group, tool=None): + """Initialize a new OctoPrint sensor.""" + self.sensor_name = sensor_name + if tool is None: + self._name = sensor_name + ' ' + condition + else: + self._name = sensor_name + ' ' + condition + ' ' + tool + ' temp' + self.sensor_type = sensor_type + self.api = api + self._state = None + self._unit_of_measurement = unit + self.api_endpoint = endpoint + self.api_group = group + self.api_tool = tool + # Set initial state + self.update() + _LOGGER.debug("created OctoPrint sensor %r", self) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + def update(self): + """Update state of sensor.""" + try: + self._state = self.api.update(self.sensor_type, + self.api_endpoint, + self.api_group, + self.api_tool) + except requests.exceptions.ConnectionError: + # Error calling the api, already logged in api.update() + return + + if self._state is None: + _LOGGER.warning("unable to locate value for %s", self.sensor_type) + return From 298b9d1f12b38dc565cb74e292f0a1360fe50dd0 Mon Sep 17 00:00:00 2001 From: Alexander Fortin Date: Wed, 4 May 2016 03:40:30 +0200 Subject: [PATCH 070/101] Limit number of processed entries by Feedreader (#1966) * process only last 20 available entries to avoid bombing event bus when parsing huge feeds * trigger first update only when HA has completed startup, allowing components to complete subscriptions to feedreader events * quote url in logs for better readability --- homeassistant/components/feedreader.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 6aab17fe011..6944d6ef58f 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/feedreader/ from datetime import datetime from logging import getLogger import voluptuous as vol +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.helpers.event import track_utc_time_change REQUIREMENTS = ['feedparser==5.2.1'] @@ -19,6 +20,7 @@ CONFIG_SCHEMA = vol.Schema({ 'urls': [vol.Url()], } }, extra=vol.ALLOW_EXTRA) +MAX_ENTRIES = 20 # pylint: disable=too-few-public-methods @@ -33,17 +35,17 @@ class FeedManager(object): self._firstrun = True # Initialize last entry timestamp as epoch time self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple() - _LOGGER.debug('Loading feed %s', self._url) - self._update() + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, + lambda _: self._update()) track_utc_time_change(hass, lambda now: self._update(), minute=0, second=0) def _log_no_entries(self): """Send no entries log at debug level.""" - _LOGGER.debug('No new entries in feed %s', self._url) + _LOGGER.debug('No new entries in feed "%s"', self._url) def _update(self): - """Update the feed and publish new entries in the event bus.""" + """Update the feed and publish new entries to the event bus.""" import feedparser _LOGGER.info('Fetching new data from feed "%s"', self._url) self._feed = feedparser.parse(self._url, @@ -52,16 +54,20 @@ class FeedManager(object): modified=None if not self._feed else self._feed.get('modified')) if not self._feed: - _LOGGER.error('Error fetching feed data from %s', self._url) + _LOGGER.error('Error fetching feed data from "%s"', self._url) else: if self._feed.bozo != 0: - _LOGGER.error('Error parsing feed %s', self._url) + _LOGGER.error('Error parsing feed "%s"', self._url) # Using etag and modified, if there's no new data available, # the entries list will be empty elif len(self._feed.entries) > 0: - _LOGGER.debug('%s entri(es) available in feed %s', + _LOGGER.debug('%s entri(es) available in feed "%s"', len(self._feed.entries), self._url) + if len(self._feed.entries) > MAX_ENTRIES: + _LOGGER.debug('Publishing only the first %s entries ' + 'in feed "%s"', MAX_ENTRIES, self._url) + self._feed.entries = self._feed.entries[0:MAX_ENTRIES] self._publish_new_entries() else: self._log_no_entries() @@ -91,7 +97,7 @@ class FeedManager(object): self._update_and_fire_entry(entry) new_entries = True else: - _LOGGER.debug('Entry %s already processed', entry.title) + _LOGGER.debug('Entry "%s" already processed', entry.title) if not new_entries: self._log_no_entries() self._firstrun = False From 34193de158b9ba0eee7b9eb372b6278ca615e6f3 Mon Sep 17 00:00:00 2001 From: Brent Date: Tue, 3 May 2016 20:41:14 -0500 Subject: [PATCH 071/101] Update yaml parser for handling environment variables (#1967) --- homeassistant/util/yaml.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 1c9ff8c1c16..8768de5d2f7 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -64,6 +64,17 @@ def _ordered_dict(loader, node): return OrderedDict(nodes) + +def _env_var_yaml(loader, node): + """Load environment variables and embed it into the configuration YAML.""" + if node.value in os.environ: + return os.environ[node.value] + else: + _LOGGER.error("Environment variable %s not defined.", node.value) + raise HomeAssistantError(node.value) + + yaml.SafeLoader.add_constructor('!include', _include_yaml) yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict) +yaml.SafeLoader.add_constructor('!env_var', _env_var_yaml) From 39e03eebcf0a4279edcf7f747368ed7b0f6951b5 Mon Sep 17 00:00:00 2001 From: devdelay Date: Mon, 2 May 2016 22:38:48 -0400 Subject: [PATCH 072/101] Add Z-Wave lock support --- homeassistant/components/lock/__init__.py | 5 +- homeassistant/components/lock/zwave.py | 64 +++++++++++++++++++++++ homeassistant/components/zwave.py | 8 ++- 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/lock/zwave.py diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 299c5fbb778..8b27929e816 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -18,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) -from homeassistant.components import (group, verisure, wink) +from homeassistant.components import (group, verisure, wink, zwave) DOMAIN = 'lock' SCAN_INTERVAL = 30 @@ -33,7 +33,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) # Maps discovered services to their platforms DISCOVERY_PLATFORMS = { wink.DISCOVER_LOCKS: 'wink', - verisure.DISCOVER_LOCKS: 'verisure' + verisure.DISCOVER_LOCKS: 'verisure', + zwave.DISCOVER_LOCKS: 'zwave', } LOCK_SERVICE_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py new file mode 100644 index 00000000000..9a3b24deb8a --- /dev/null +++ b/homeassistant/components/lock/zwave.py @@ -0,0 +1,64 @@ +""" +Zwave platform that handles simple door locks. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.zwave/ +""" +# Because we do not compile openzwave on CI +# pylint: disable=import-error +from homeassistant.components.lock import DOMAIN, LockDevice +from homeassistant.components import zwave + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Find and return Z-Wave switches.""" + if discovery_info is None or zwave.NETWORK is None: + return + + node = zwave.NETWORK.nodes[discovery_info[zwave.ATTR_NODE_ID]] + value = node.values[discovery_info[zwave.ATTR_VALUE_ID]] + + if value.command_class != zwave.COMMAND_CLASS_DOOR_LOCK: + return + if value.type != zwave.TYPE_BOOL: + return + if value.genre != zwave.GENRE_USER: + return + + value.set_change_verified(False) + add_devices([ZwaveLock(value)]) + + +class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): + """Representation of a Z-Wave switch.""" + + def __init__(self, value): + """Initialize the Z-Wave switch device.""" + from openzwave.network import ZWaveNetwork + from pydispatch import dispatcher + + zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) + + self._state = value.data + dispatcher.connect( + self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) + + def _value_changed(self, value): + """Called when a value has changed on the network.""" + if self._value.value_id == value.value_id: + self._state = value.data + self.update_ha_state() + + @property + def is_locked(self): + """Return true if device is locked.""" + return self._state + + def lock(self, **kwargs): + """Lock the device.""" + self._value.data = True + + def unlock(self, **kwargs): + """Unlock the device.""" + self._value.data = False diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 94e8b85df6b..dcc80126be7 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -39,11 +39,12 @@ DISCOVER_SWITCHES = "zwave.switch" DISCOVER_LIGHTS = "zwave.light" DISCOVER_BINARY_SENSORS = 'zwave.binary_sensor' DISCOVER_THERMOSTATS = 'zwave.thermostat' +DISCOVER_LOCKS = 'zwave.lock' EVENT_SCENE_ACTIVATED = "zwave.scene_activated" COMMAND_CLASS_SWITCH_MULTILEVEL = 38 - +COMMAND_CLASS_DOOR_LOCK = 98 COMMAND_CLASS_SWITCH_BINARY = 37 COMMAND_CLASS_SENSOR_BINARY = 48 COMMAND_CLASS_SENSOR_MULTILEVEL = 49 @@ -91,6 +92,11 @@ DISCOVERY_COMPONENTS = [ [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, GENRE_WHATEVER), + ('lock', + DISCOVER_LOCKS, + [COMMAND_CLASS_DOOR_LOCK], + TYPE_BOOL, + GENRE_USER), ] From 4850a65ed09d10afc1a8cd35c4288645f77a426e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Wed, 4 May 2016 03:53:11 +0200 Subject: [PATCH 073/101] add reconnect retry and longer timeouts for verisure (#1944) --- .../alarm_control_panel/verisure.py | 5 ++++ homeassistant/components/lock/verisure.py | 5 ++++ homeassistant/components/sensor/verisure.py | 15 +++++++++++ homeassistant/components/switch/verisure.py | 5 ++++ homeassistant/components/verisure.py | 25 +++++++++++-------- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 9bc25af3b69..4a49e0b6941 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -48,6 +48,11 @@ class VerisureAlarm(alarm.AlarmControlPanel): """Return the state of the device.""" return self._state + @property + def available(self): + """Return True if entity is available.""" + return hub.available + @property def code_format(self): """The code format as regex.""" diff --git a/homeassistant/components/lock/verisure.py b/homeassistant/components/lock/verisure.py index a905b6f2d19..08cfbc6202f 100644 --- a/homeassistant/components/lock/verisure.py +++ b/homeassistant/components/lock/verisure.py @@ -46,6 +46,11 @@ class VerisureDoorlock(LockDevice): """Return the state of the lock.""" return self._state + @property + def available(self): + """Return True if entity is available.""" + return hub.available + @property def code_format(self): """Return the required six digit code.""" diff --git a/homeassistant/components/sensor/verisure.py b/homeassistant/components/sensor/verisure.py index bd6f53c3e1c..4252c9d8b33 100644 --- a/homeassistant/components/sensor/verisure.py +++ b/homeassistant/components/sensor/verisure.py @@ -65,6 +65,11 @@ class VerisureThermometer(Entity): # Remove ° character return hub.climate_status[self._id].temperature[:-1] + @property + def available(self): + """Return True if entity is available.""" + return hub.available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" @@ -95,6 +100,11 @@ class VerisureHygrometer(Entity): # remove % character return hub.climate_status[self._id].humidity[:-1] + @property + def available(self): + """Return True if entity is available.""" + return hub.available + @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" @@ -124,6 +134,11 @@ class VerisureMouseDetection(Entity): """Return the state of the sensor.""" return hub.mouse_status[self._id].count + @property + def available(self): + """Return True if entity is available.""" + return hub.available + @property def unit_of_measurement(self): """Return the unit of measurement of this sensor.""" diff --git a/homeassistant/components/switch/verisure.py b/homeassistant/components/switch/verisure.py index 75833c4016c..f2943058452 100644 --- a/homeassistant/components/switch/verisure.py +++ b/homeassistant/components/switch/verisure.py @@ -42,6 +42,11 @@ class VerisureSmartplug(SwitchDevice): """Return true if on.""" return hub.smartplug_status[self._id].status == 'on' + @property + def available(self): + """Return True if entity is available.""" + return hub.available + def turn_on(self): """Set smartplug status on.""" hub.my_pages.smartplug.set(self._id, 'on') diff --git a/homeassistant/components/verisure.py b/homeassistant/components/verisure.py index 963367eed7a..a445eaaa405 100644 --- a/homeassistant/components/verisure.py +++ b/homeassistant/components/verisure.py @@ -77,7 +77,7 @@ class VerisureHub(object): # "wrong password" message. We will continue to retry after maintenance # regardless of that error. self._disable_wrong_password_error = False - self._wrong_password_given = False + self._password_retries = 1 self._reconnect_timeout = time.time() self.my_pages = verisure.MyPages( @@ -128,11 +128,13 @@ class VerisureHub(object): self.my_pages.smartplug.get, self.smartplug_status) + @property + def available(self): + """Return True if hub is available.""" + return self._password_retries >= 0 + def update_component(self, get_function, status): """Update the status of Verisure components.""" - if self._wrong_password_given: - _LOGGER.error('Wrong password for Verisure, update config') - return try: for overview in get_function(): try: @@ -145,25 +147,26 @@ class VerisureHub(object): def reconnect(self): """Reconnect to Verisure MyPages.""" - if self._reconnect_timeout > time.time(): - return - if not self._lock.acquire(blocking=False): + if (self._reconnect_timeout > time.time() or + not self._lock.acquire(blocking=False) or + self._password_retries < 0): return try: self.my_pages.login() self._disable_wrong_password_error = False + self._password_retries = 1 except self._verisure.LoginError as ex: _LOGGER.error("Wrong user name or password for Verisure MyPages") if self._disable_wrong_password_error: - self._reconnect_timeout = time.time() + 60 + self._reconnect_timeout = time.time() + 60*60 else: - self._wrong_password_given = True + self._password_retries = self._password_retries - 1 except self._verisure.MaintenanceError: self._disable_wrong_password_error = True - self._reconnect_timeout = time.time() + 60 + self._reconnect_timeout = time.time() + 60*60 _LOGGER.error("Verisure MyPages down for maintenance") except self._verisure.Error as ex: _LOGGER.error("Could not login to Verisure MyPages, %s", ex) - self._reconnect_timeout = time.time() + 5 + self._reconnect_timeout = time.time() + 60 finally: self._lock.release() From 8fc07ee6cdbdf9cecc8542bf0656e0bdfcd61913 Mon Sep 17 00:00:00 2001 From: Kyle Hendricks Date: Tue, 3 May 2016 22:34:26 -0400 Subject: [PATCH 074/101] Fix an unresolved merge conflict --- homeassistant/components/zwave.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 134929b76e9..d76956de5bd 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -94,19 +94,16 @@ DISCOVERY_COMPONENTS = [ [COMMAND_CLASS_THERMOSTAT_SETPOINT], TYPE_WHATEVER, GENRE_WHATEVER), -<<<<<<< HEAD ('hvac', DISCOVER_HVAC, [COMMAND_CLASS_THERMOSTAT_FAN_MODE], TYPE_WHATEVER, GENRE_WHATEVER), -======= ('lock', DISCOVER_LOCKS, [COMMAND_CLASS_DOOR_LOCK], TYPE_BOOL, GENRE_USER), ->>>>>>> pr/1969 ] From d5031d90c41aba720e9426ca4af5ae1d2836b364 Mon Sep 17 00:00:00 2001 From: Alex Harvey Date: Wed, 4 May 2016 13:40:31 -0700 Subject: [PATCH 075/101] Update README.rst (#1981) Splitting dev and general chat links in the badges --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 2c370eb2315..5cea28f5dca 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| +Home Assistant |Build Status| |Coverage Status| |Join the chat at https://gitter.im/home-assistant/home-assistant| |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| ================================================================================================================== Home Assistant is a home automation platform running on Python 3. The @@ -92,7 +92,9 @@ section `__ how to reach us. :target: https://travis-ci.org/home-assistant/home-assistant .. |Coverage Status| image:: https://img.shields.io/coveralls/home-assistant/home-assistant.svg :target: https://coveralls.io/r/home-assistant/home-assistant?branch=master -.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://badges.gitter.im/Join%20Chat.svg +.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg :target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg + :target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge .. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png :target: https://home-assistant.io/demo/ From 799582979074db550d0613797361dd78a00edc1c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 5 May 2016 00:28:51 +0200 Subject: [PATCH 076/101] Fix typo --- homeassistant/components/binary_sensor/command_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 1acefe973d2..14eba43d23d 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -1,5 +1,5 @@ """ -Support for custom shell commands to to retrieve values. +Support for custom shell commands to retrieve values. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.command/ From 2274806bee5a6aa4151cb3e796556357a414bd0f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 5 May 2016 00:32:11 +0200 Subject: [PATCH 077/101] UPdate link to docs --- homeassistant/components/binary_sensor/command_line.py | 2 +- homeassistant/components/sensor/command_line.py | 2 +- homeassistant/components/switch/command_line.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 14eba43d23d..e589506eac7 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -2,7 +2,7 @@ Support for custom shell commands to retrieve values. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.command/ +https://home-assistant.io/components/binary_sensor.command_line/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index 696f27d1931..3d73017c2e7 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -2,7 +2,7 @@ Allows to configure custom shell commands to turn a value for a sensor. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.command_sensor/ +https://home-assistant.io/components/sensor.command_line/ """ import logging import subprocess diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index 12b66bb6b7e..40b83371f9a 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -2,7 +2,7 @@ Support for custom shell commands to turn a switch on/off. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/switch.command_switch/ +https://home-assistant.io/components/switch.command_line/ """ import logging import subprocess From ddfda89fc96c978e476ee7aadf54a746cc44fda3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 5 May 2016 09:49:52 +0200 Subject: [PATCH 078/101] Update link to docs --- homeassistant/components/rollershutter/command_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rollershutter/command_line.py b/homeassistant/components/rollershutter/command_line.py index 148c93335ac..c90a8be9410 100644 --- a/homeassistant/components/rollershutter/command_line.py +++ b/homeassistant/components/rollershutter/command_line.py @@ -2,7 +2,7 @@ Support for command roller shutters. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/rollershutter.command_rollershutter/ +https://home-assistant.io/components/rollershutter.command_line/ """ import logging import subprocess From e5d1ed94399f0b6a6506a48b571492ff886e33e8 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 May 2016 02:55:26 +0200 Subject: [PATCH 079/101] Add dweet export component (#1818) --- .coveragerc | 4 +- homeassistant/components/dweet.py | 73 +++++++++++++++++++++++++++++++ requirements_all.txt | 1 + 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/dweet.py diff --git a/.coveragerc b/.coveragerc index 32cbdc38047..67b347c5ee1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -14,6 +14,9 @@ omit = homeassistant/components/bloomsky.py homeassistant/components/*/bloomsky.py + homeassistant/components/dweet.py + homeassistant/components/*/dweet.py + homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py @@ -143,7 +146,6 @@ omit = homeassistant/components/sensor/cpuspeed.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py - homeassistant/components/sensor/dweet.py homeassistant/components/sensor/efergy.py homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/forecast.py diff --git a/homeassistant/components/dweet.py b/homeassistant/components/dweet.py new file mode 100644 index 00000000000..49c1e74f232 --- /dev/null +++ b/homeassistant/components/dweet.py @@ -0,0 +1,73 @@ +""" +A component which allows you to send data to Dweet.io. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/dweet/ +""" +import logging +from datetime import timedelta +import voluptuous as vol + +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNKNOWN +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import state as state_helper +from homeassistant.util import Throttle + + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "dweet" +DEPENDENCIES = [] + +REQUIREMENTS = ['dweepy==0.2.0'] + +CONF_NAME = 'name' +CONF_WHITELIST = 'whitelist' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_WHITELIST): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + + +# pylint: disable=too-many-locals +def setup(hass, config): + """Setup the Dweet.io component.""" + conf = config[DOMAIN] + name = conf[CONF_NAME] + whitelist = conf.get(CONF_WHITELIST, []) + json_body = {} + + def dweet_event_listener(event): + """Listen for new messages on the bus and sends them to Dweet.io.""" + state = event.data.get('new_state') + if state is None or state.state in (STATE_UNKNOWN, '') \ + or state.entity_id not in whitelist: + return + + try: + _state = state_helper.state_as_number(state) + except ValueError: + _state = state.state + + json_body[state.attributes.get('friendly_name')] = _state + + send_data(name, json_body) + + hass.bus.listen(EVENT_STATE_CHANGED, dweet_event_listener) + + return True + + +@Throttle(MIN_TIME_BETWEEN_UPDATES) +def send_data(name, msg): + """Send the collected data to Dweet.io.""" + import dweepy + try: + dweepy.dweet_for(name, msg) + except dweepy.DweepyError: + _LOGGER.error("Error saving data '%s' to Dweet.io", msg) diff --git a/requirements_all.txt b/requirements_all.txt index 082166b5a99..2b876af7448 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -40,6 +40,7 @@ blockchain==1.3.1 # homeassistant.components.notify.xmpp dnspython3==1.12.0 +# homeassistant.components.dweet # homeassistant.components.sensor.dweet dweepy==0.2.0 From 393bd8809122692b9df5d03311ddbc6da25f9c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hern=C3=A1n?= Date: Fri, 6 May 2016 21:57:00 -0300 Subject: [PATCH 080/101] Add Media Player Stop command + Kodi support for it (#1960) * Started adding Stop command to Kodi media player * minor * minor * minor * abstract-method fixed --- .../components/media_player/__init__.py | 19 ++++++++++++++++++- homeassistant/components/media_player/kodi.py | 11 +++++++++-- .../components/media_player/services.yaml | 8 ++++++++ .../components/media_player/universal.py | 7 ++++++- homeassistant/const.py | 1 + 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index c6e4c147c1c..ff8eb8113b9 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( STATE_OFF, STATE_UNKNOWN, STATE_PLAYING, STATE_IDLE, ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET, - SERVICE_VOLUME_MUTE, SERVICE_TOGGLE, + SERVICE_VOLUME_MUTE, SERVICE_TOGGLE, SERVICE_MEDIA_STOP, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK) @@ -82,6 +82,7 @@ SUPPORT_TURN_OFF = 256 SUPPORT_PLAY_MEDIA = 512 SUPPORT_VOLUME_STEP = 1024 SUPPORT_SELECT_SOURCE = 2048 +SUPPORT_STOP = 4096 # simple services that only take entity_id(s) as optional argument SERVICE_TO_METHOD = { @@ -93,6 +94,7 @@ SERVICE_TO_METHOD = { SERVICE_MEDIA_PLAY_PAUSE: 'media_play_pause', SERVICE_MEDIA_PLAY: 'media_play', SERVICE_MEDIA_PAUSE: 'media_pause', + SERVICE_MEDIA_STOP: 'media_stop', SERVICE_MEDIA_NEXT_TRACK: 'media_next_track', SERVICE_MEDIA_PREVIOUS_TRACK: 'media_previous_track', SERVICE_SELECT_SOURCE: 'select_source' @@ -228,6 +230,12 @@ def media_pause(hass, entity_id=None): hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) +def media_stop(hass, entity_id=None): + """Send the media player the stop command.""" + data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} + hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data) + + def media_next_track(hass, entity_id=None): """Send the media player the command for next track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -510,6 +518,10 @@ class MediaPlayerDevice(Entity): """Send pause command.""" raise NotImplementedError() + def media_stop(self): + """Send stop command.""" + raise NotImplementedError() + def media_previous_track(self): """Send previous track command.""" raise NotImplementedError() @@ -536,6 +548,11 @@ class MediaPlayerDevice(Entity): """Boolean if pause is supported.""" return bool(self.supported_media_commands & SUPPORT_PAUSE) + @property + def support_stop(self): + """Boolean if stop is supported.""" + return bool(self.supported_media_commands & SUPPORT_STOP) + @property def support_seek(self): """Boolean if seek is supported.""" diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 1076e1af971..e1c4bd79c8f 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -9,7 +9,7 @@ import urllib from homeassistant.components.media_player import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, MediaPlayerDevice) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) @@ -19,7 +19,7 @@ REQUIREMENTS = ['jsonrpc-requests==0.2'] SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ - SUPPORT_PLAY_MEDIA + SUPPORT_PLAY_MEDIA | SUPPORT_STOP def setup_platform(hass, config, add_devices, discovery_info=None): @@ -229,6 +229,13 @@ class KodiDevice(MediaPlayerDevice): """Pause the media player.""" self._set_play_state(False) + def media_stop(self): + """Stop the media player.""" + players = self._get_players() + + if len(players) != 0: + self._server.Player.Stop(players[0]['playerid']) + def _goto(self, direction): """Helper method used for previous/next track.""" players = self._get_players() diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 59227eda2a5..ebf882825bb 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -86,6 +86,14 @@ media_pause: description: Name(s) of entities to pause on example: 'media_player.living_room_sonos' +media_stop: + description: Send the media player the stop command. + + fields: + entity_id: + description: Name(s) of entities to stop on + example: 'media_player.living_room_sonos' + media_next_track: description: Send the media player the command for next track. diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 7f25553248b..3498a9e5580 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -25,7 +25,8 @@ from homeassistant.const import ( SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON) + SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_IDLE, STATE_OFF, STATE_ON, + SERVICE_MEDIA_STOP) from homeassistant.helpers.event import track_state_change from homeassistant.helpers.service import call_from_config @@ -384,6 +385,10 @@ class UniversalMediaPlayer(MediaPlayerDevice): """Send pause command.""" self._call_service(SERVICE_MEDIA_PAUSE) + def media_stop(self): + """Send stop command.""" + self._call_service(SERVICE_MEDIA_STOP) + def media_previous_track(self): """Send previous track command.""" self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK) diff --git a/homeassistant/const.py b/homeassistant/const.py index 525f0e5b304..2676abbb5bf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -171,6 +171,7 @@ SERVICE_VOLUME_SET = "volume_set" SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" SERVICE_MEDIA_PLAY = "media_play" SERVICE_MEDIA_PAUSE = "media_pause" +SERVICE_MEDIA_STOP = "media_stop" SERVICE_MEDIA_NEXT_TRACK = "media_next_track" SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track" SERVICE_MEDIA_SEEK = "media_seek" From 4a28be9a94984721c2f385d05ec6ec340f86c331 Mon Sep 17 00:00:00 2001 From: Kyle Hendricks Date: Fri, 6 May 2016 20:57:57 -0400 Subject: [PATCH 081/101] [Pioneer AVR] Display the currently select input source as the media title (#1974) --- homeassistant/components/media_player/pioneer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/media_player/pioneer.py b/homeassistant/components/media_player/pioneer.py index 9e987711b76..207e38ecf40 100644 --- a/homeassistant/components/media_player/pioneer.py +++ b/homeassistant/components/media_player/pioneer.py @@ -164,6 +164,11 @@ class PioneerDevice(MediaPlayerDevice): """List of available input sources.""" return list(self._source_name_to_number.keys()) + @property + def media_title(self): + """Title of current playing media.""" + return self._selected_source + def turn_off(self): """Turn off media player.""" self.telnet_command("PF") From 98bedf1bd629da4078832c7fdc59f01b76438ec3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 May 2016 03:03:18 +0200 Subject: [PATCH 082/101] Update links to docs and doc strings (#1994) --- homeassistant/components/binary_sensor/octoprint.py | 12 +++++------- homeassistant/components/octoprint.py | 9 ++++----- homeassistant/components/sensor/octoprint.py | 12 +++++------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/binary_sensor/octoprint.py b/homeassistant/components/binary_sensor/octoprint.py index 803416ce66c..25c922ca20c 100644 --- a/homeassistant/components/binary_sensor/octoprint.py +++ b/homeassistant/components/binary_sensor/octoprint.py @@ -1,11 +1,9 @@ """ Support for monitoring OctoPrint binary sensors. -Uses OctoPrint REST JSON API to query for monitored variables. -For more details about this component, please refer to the documentation at -http://docs.octoprint.org/en/master/api/ +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.octoprint/ """ - import logging import requests @@ -51,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-instance-attributes class OctoPrintBinarySensor(BinarySensorDevice): - """Represents an OctoPrint binary sensor.""" + """Representation an OctoPrint binary sensor.""" # pylint: disable=too-many-arguments def __init__(self, api, condition, sensor_type, sensor_name, @@ -71,7 +69,7 @@ class OctoPrintBinarySensor(BinarySensorDevice): self.api_tool = tool # Set initial state self.update() - _LOGGER.debug("created OctoPrint binary sensor %r", self) + _LOGGER.debug("Created OctoPrint binary sensor %r", self) @property def name(self): @@ -108,4 +106,4 @@ class OctoPrintBinarySensor(BinarySensorDevice): return if self._state is None: - _LOGGER.warning("unable to locate value for %s", self.sensor_type) + _LOGGER.warning("Unable to locate value for %s", self.sensor_type) diff --git a/homeassistant/components/octoprint.py b/homeassistant/components/octoprint.py index 9d7f015a2f4..76db48b5a07 100644 --- a/homeassistant/components/octoprint.py +++ b/homeassistant/components/octoprint.py @@ -1,10 +1,9 @@ """ Support for monitoring OctoPrint 3D printers. -Uses OctoPrint REST JSON API to query for monitored variables. -http://docs.octoprint.org/en/master/api/ +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/octoprint/ """ - import logging import time @@ -52,7 +51,7 @@ def setup(hass, config): class OctoPrintAPI(object): - """Simple json wrapper for OctoPrint's api.""" + """Simple JSON wrapper for OctoPrint's API.""" def __init__(self, api_url, key): """Initialize OctoPrint API and set headers needed later.""" @@ -109,7 +108,7 @@ class OctoPrintAPI(object): # pylint: disable=unused-variable def get_value_from_json(json_dict, sensor_type, group, tool): - """Return the value for sensor_type from the provided json.""" + """Return the value for sensor_type from the JSON.""" if group in json_dict: if sensor_type in json_dict[group]: if sensor_type == "target" and json_dict[sensor_type] is None: diff --git a/homeassistant/components/sensor/octoprint.py b/homeassistant/components/sensor/octoprint.py index 3e47c4d51fe..bb4e6973df8 100644 --- a/homeassistant/components/sensor/octoprint.py +++ b/homeassistant/components/sensor/octoprint.py @@ -1,11 +1,9 @@ """ Support for monitoring OctoPrint sensors. -Uses OctoPrint REST JSON API to query for monitored variables. -For more details about this component, please refer to the documentation at -http://docs.octoprint.org/en/master/api/ +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.octoprint/ """ - import logging import requests @@ -65,7 +63,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # pylint: disable=too-many-instance-attributes class OctoPrintSensor(Entity): - """Represents an OctoPrint sensor.""" + """Representation of an OctoPrint sensor.""" # pylint: disable=too-many-arguments def __init__(self, api, condition, sensor_type, sensor_name, @@ -85,7 +83,7 @@ class OctoPrintSensor(Entity): self.api_tool = tool # Set initial state self.update() - _LOGGER.debug("created OctoPrint sensor %r", self) + _LOGGER.debug("Created OctoPrint sensor %r", self) @property def name(self): @@ -114,5 +112,5 @@ class OctoPrintSensor(Entity): return if self._state is None: - _LOGGER.warning("unable to locate value for %s", self.sensor_type) + _LOGGER.warning("Unable to locate value for %s", self.sensor_type) return From d73f8d5253bad03d871db507e5168a20a99b15b9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 May 2016 03:03:28 +0200 Subject: [PATCH 083/101] Upgrade to PyMata 2.12 (#1993) --- homeassistant/components/arduino.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arduino.py b/homeassistant/components/arduino.py index 6e5e7973fbf..0a981940842 100644 --- a/homeassistant/components/arduino.py +++ b/homeassistant/components/arduino.py @@ -11,7 +11,7 @@ from homeassistant.const import ( from homeassistant.helpers import validate_config DOMAIN = "arduino" -REQUIREMENTS = ['PyMata==2.07a'] +REQUIREMENTS = ['PyMata==2.12'] BOARD = None _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2b876af7448..c316d0c0fb3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ voluptuous==0.8.9 PyISY==1.0.5 # homeassistant.components.arduino -PyMata==2.07a +PyMata==2.12 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From c72ab42c19c409a8f17f27ddc216ab44fbe3c1e1 Mon Sep 17 00:00:00 2001 From: Ellis Percival Date: Sat, 7 May 2016 02:09:18 +0100 Subject: [PATCH 084/101] Enable ZigBee "push" updates for digital/analog sensors. (#1976) --- homeassistant/components/zigbee.py | 210 +++++++++++++++++++++++++---- requirements_all.txt | 2 +- 2 files changed, 185 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zigbee.py b/homeassistant/components/zigbee.py index c413ce55a23..84770390ad9 100644 --- a/homeassistant/components/zigbee.py +++ b/homeassistant/components/zigbee.py @@ -5,14 +5,18 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zigbee/ """ import logging +import pickle from binascii import hexlify, unhexlify +from base64 import b64encode, b64decode from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import JobPriority from homeassistant.helpers.entity import Entity DOMAIN = "zigbee" -REQUIREMENTS = ("xbee-helper==0.0.6",) +REQUIREMENTS = ("xbee-helper==0.0.7",) + +EVENT_ZIGBEE_FRAME_RECEIVED = "zigbee_frame_received" CONF_DEVICE = "device" CONF_BAUD = "baud" @@ -25,9 +29,14 @@ DEFAULT_ADC_MAX_VOLTS = 1.2 GPIO_DIGITAL_OUTPUT_LOW = None GPIO_DIGITAL_OUTPUT_HIGH = None ADC_PERCENTAGE = None +DIGITAL_PINS = None +ANALOG_PINS = None +CONVERT_ADC = None ZIGBEE_EXCEPTION = None ZIGBEE_TX_FAILURE = None +ATTR_FRAME = "frame" + DEVICE = None _LOGGER = logging.getLogger(__name__) @@ -39,17 +48,24 @@ def setup(hass, config): global GPIO_DIGITAL_OUTPUT_LOW global GPIO_DIGITAL_OUTPUT_HIGH global ADC_PERCENTAGE + global DIGITAL_PINS + global ANALOG_PINS + global CONVERT_ADC global ZIGBEE_EXCEPTION global ZIGBEE_TX_FAILURE import xbee_helper.const as xb_const from xbee_helper import ZigBee + from xbee_helper.device import convert_adc from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from serial import Serial, SerialException GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE + DIGITAL_PINS = xb_const.DIGITAL_PINS + ANALOG_PINS = xb_const.ANALOG_PINS + CONVERT_ADC = convert_adc ZIGBEE_EXCEPTION = ZigBeeException ZIGBEE_TX_FAILURE = ZigBeeTxFailure @@ -62,6 +78,19 @@ def setup(hass, config): return False DEVICE = ZigBee(ser) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port) + + def _frame_received(frame): + """Called when a ZigBee frame is received. + + Pickles the frame, then encodes it into base64 since it contains + non JSON serializable binary. + """ + hass.bus.fire( + EVENT_ZIGBEE_FRAME_RECEIVED, + {ATTR_FRAME: b64encode(pickle.dumps(frame)).decode("ascii")}) + + DEVICE.add_frame_rx_handler(_frame_received) + return True @@ -70,6 +99,25 @@ def close_serial_port(*args): DEVICE.zb.serial.close() +def frame_is_relevant(entity, frame): + """Test whether the frame is relevant to the entity.""" + if frame.get("source_addr_long") != entity.config.address: + return False + if "samples" not in frame: + return False + return True + + +def subscribe(hass, callback): + """Subscribe to incoming ZigBee frames.""" + def zigbee_frame_subscriber(event): + """Decode and unpickle the frame from the event bus, and call back.""" + frame = pickle.loads(b64decode(event.data[ATTR_FRAME])) + callback(frame) + + hass.bus.listen(EVENT_ZIGBEE_FRAME_RECEIVED, zigbee_frame_subscriber) + + class ZigBeeConfig(object): """Handle the fetching of configuration from the config file.""" @@ -110,14 +158,65 @@ class ZigBeePinConfig(ZigBeeConfig): return self._config["pin"] -class ZigBeeDigitalPinConfig(ZigBeePinConfig): - """Handle the fetching of configuration from the config file.""" +class ZigBeeDigitalInConfig(ZigBeePinConfig): + """A subclass of ZigBeePinConfig.""" def __init__(self, config): - """Initialize the configuration.""" - super(ZigBeeDigitalPinConfig, self).__init__(config) + """Initialise the ZigBee Digital input config.""" + super(ZigBeeDigitalInConfig, self).__init__(config) self._bool2state, self._state2bool = self.boolean_maps + @property + def boolean_maps(self): + """Create mapping dictionaries for potential inversion of booleans. + + Create dicts to map the pin state (true/false) to potentially inverted + values depending on the on_state config value which should be set to + "low" or "high". + """ + if self._config.get("on_state", "").lower() == "low": + bool2state = { + True: False, + False: True + } + else: + bool2state = { + True: True, + False: False + } + state2bool = {v: k for k, v in bool2state.items()} + return bool2state, state2bool + + @property + def bool2state(self): + """A dictionary mapping the internal value to the ZigBee value. + + For the translation of on/off as being pin high or low. + """ + return self._bool2state + + @property + def state2bool(self): + """A dictionary mapping the ZigBee value to the internal value. + + For the translation of pin high/low as being on or off. + """ + return self._state2bool + + +class ZigBeeDigitalOutConfig(ZigBeePinConfig): + """A subclass of ZigBeePinConfig. + + Set _should_poll to default as False instead of True. The value will + still be overridden by the presence of a 'poll' config entry. + """ + + def __init__(self, config): + """Initialize the ZigBee Digital out.""" + super(ZigBeeDigitalOutConfig, self).__init__(config) + self._bool2state, self._state2bool = self.boolean_maps + self._should_poll = config.get("poll", False) + @property def boolean_maps(self): """Create dicts to map booleans to pin high/low and vice versa. @@ -154,22 +253,6 @@ class ZigBeeDigitalPinConfig(ZigBeePinConfig): """ return self._state2bool -# Create an alias so that ZigBeeDigitalOutConfig has a logical opposite. -ZigBeeDigitalInConfig = ZigBeeDigitalPinConfig - - -class ZigBeeDigitalOutConfig(ZigBeeDigitalPinConfig): - """A subclass of ZigBeeDigitalPinConfig. - - Set _should_poll to default as False instead of True. The value will - still be overridden by the presence of a 'poll' config entry. - """ - - def __init__(self, config): - """Initialize the ZigBee Digital out.""" - super(ZigBeeDigitalOutConfig, self).__init__(config) - self._should_poll = config.get("poll", False) - class ZigBeeAnalogInConfig(ZigBeePinConfig): """Representation of a ZigBee GPIO pin set to analog in.""" @@ -187,6 +270,25 @@ class ZigBeeDigitalIn(Entity): """Initialize the device.""" self._config = config self._state = False + + def handle_frame(frame): + """Handle an incoming frame. + + Handle an incoming frame and update our status if it contains + information relating to this device. + """ + if not frame_is_relevant(self, frame): + return + sample = frame["samples"].pop() + pin_name = DIGITAL_PINS[self._config.pin] + if pin_name not in sample: + # Doesn't contain information about our pin + return + self._state = self._config.state2bool[sample[pin_name]] + self.update_ha_state() + + subscribe(hass, handle_frame) + # Get initial state hass.pool.add_job( JobPriority.EVENT_STATE, (self.update_ha_state, True)) @@ -196,6 +298,11 @@ class ZigBeeDigitalIn(Entity): """Return the name of the input.""" return self._config.name + @property + def config(self): + """The entity's configuration.""" + return self._config + @property def should_poll(self): """Return the state of the polling, if needed.""" @@ -207,11 +314,9 @@ class ZigBeeDigitalIn(Entity): return self._state def update(self): - """Ask the ZigBee device what its output is set to.""" + """Ask the ZigBee device what state its input pin is in.""" try: - pin_state = DEVICE.get_gpio_pin( - self._config.pin, - self._config.address) + sample = DEVICE.get_sample(self._config.address) except ZIGBEE_TX_FAILURE: _LOGGER.warning( "Transmission failure when attempting to get sample from " @@ -221,7 +326,14 @@ class ZigBeeDigitalIn(Entity): _LOGGER.exception( "Unable to get sample from ZigBee device: %s", exc) return - self._state = self._config.state2bool[pin_state] + pin_name = DIGITAL_PINS[self._config.pin] + if pin_name not in sample: + _LOGGER.warning( + "Pin %s (%s) was not in the sample provided by ZigBee device " + "%s.", + self._config.pin, pin_name, hexlify(self._config.address)) + return + self._state = self._config.state2bool[sample[pin_name]] class ZigBeeDigitalOut(ZigBeeDigitalIn): @@ -255,6 +367,24 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): """Set the digital output to its 'off' state.""" self._set_state(False) + def update(self): + """Ask the ZigBee device what its output is set to.""" + try: + pin_state = DEVICE.get_gpio_pin( + self._config.pin, + self._config.address) + except ZIGBEE_TX_FAILURE: + _LOGGER.warning( + "Transmission failure when attempting to get output pin status" + " from ZigBee device at address: %s", + hexlify(self._config.address)) + return + except ZIGBEE_EXCEPTION as exc: + _LOGGER.exception( + "Unable to get output pin status from ZigBee device: %s", exc) + return + self._state = self._config.state2bool[pin_state] + class ZigBeeAnalogIn(Entity): """Representation of a GPIO pin configured as an analog input.""" @@ -263,6 +393,29 @@ class ZigBeeAnalogIn(Entity): """Initialize the ZigBee analog in device.""" self._config = config self._value = None + + def handle_frame(frame): + """Handle an incoming frame. + + Handle an incoming frame and update our status if it contains + information relating to this device. + """ + if not frame_is_relevant(self, frame): + return + sample = frame["samples"].pop() + pin_name = ANALOG_PINS[self._config.pin] + if pin_name not in sample: + # Doesn't contain information about our pin + return + self._value = CONVERT_ADC( + sample[pin_name], + ADC_PERCENTAGE, + self._config.max_voltage + ) + self.update_ha_state() + + subscribe(hass, handle_frame) + # Get initial state hass.pool.add_job( JobPriority.EVENT_STATE, (self.update_ha_state, True)) @@ -272,6 +425,11 @@ class ZigBeeAnalogIn(Entity): """The name of the input.""" return self._config.name + @property + def config(self): + """The entity's configuration.""" + return self._config + @property def should_poll(self): """The state of the polling, if needed.""" diff --git a/requirements_all.txt b/requirements_all.txt index c316d0c0fb3..2c7e6cc48fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ vsure==0.8.1 wakeonlan==0.2.2 # homeassistant.components.zigbee -xbee-helper==0.0.6 +xbee-helper==0.0.7 # homeassistant.components.sensor.yr xmltodict From 72cf7fd9c288c9f885b050a381f666dd5c5ead7b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 May 2016 03:10:13 +0200 Subject: [PATCH 085/101] Add timeout to request for location elevation (#1978) --- homeassistant/util/location.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index d999b824a1d..55f8a834308 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -63,10 +63,11 @@ def distance(lat1, lon1, lat2, lon2): def elevation(latitude, longitude): """Return elevation for given latitude and longitude.""" - req = requests.get(ELEVATION_URL, params={ - 'locations': '{},{}'.format(latitude, longitude), - 'sensor': 'false', - }) + req = requests.get(ELEVATION_URL, + params={'locations': '{},{}'.format(latitude, + longitude), + 'sensor': 'false'}, + timeout=10) if req.status_code != 200: return 0 From 1cd59cf2a9b1cae7275d87cebfbd3d2bcd2db5dd Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 6 May 2016 21:19:37 -0400 Subject: [PATCH 086/101] Added battery level to wink devices (#1979) --- .../components/binary_sensor/wink.py | 18 ++++++++-- homeassistant/components/garage_door/wink.py | 18 ++++++++-- homeassistant/components/light/wink.py | 2 +- homeassistant/components/lock/wink.py | 18 ++++++++-- homeassistant/components/sensor/wink.py | 33 +++++++++++++++++-- homeassistant/components/switch/wink.py | 2 +- homeassistant/components/wink.py | 18 ++++++++-- requirements_all.txt | 2 +- 8 files changed, 98 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index 5521865bae1..c6989cae98c 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -7,10 +7,10 @@ at https://home-assistant.io/components/sensor.wink/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-wink==0.7.5'] +REQUIREMENTS = ['python-wink==0.7.6'] # These are the available sensors mapped to binary_sensor class SENSOR_TYPES = { @@ -48,6 +48,7 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity): """Initialize the Wink binary sensor.""" self.wink = wink self._unit_of_measurement = self.wink.UNIT + self._battery = self.wink.battery_level self.capability = self.wink.capability() @property @@ -85,3 +86,16 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity): def update(self): """Update state of the sensor.""" self.wink.update_state() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._battery: + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } + + @property + def _battery_level(self): + """Return the battery level.""" + return self.wink.battery_level * 100 diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py index 2a041b1100d..ace97b5fba9 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -7,9 +7,9 @@ https://home-assistant.io/components/garage_door.wink/ import logging from homeassistant.components.garage_door import GarageDoorDevice -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL -REQUIREMENTS = ['python-wink==0.7.5'] +REQUIREMENTS = ['python-wink==0.7.6'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -37,6 +37,7 @@ class WinkGarageDoorDevice(GarageDoorDevice): def __init__(self, wink): """Initialize the garage door.""" self.wink = wink + self._battery = self.wink.battery_level @property def unique_id(self): @@ -69,3 +70,16 @@ class WinkGarageDoorDevice(GarageDoorDevice): def open_door(self): """Open the door.""" self.wink.set_state(1) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._battery: + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } + + @property + def _battery_level(self): + """Return the battery level.""" + return self.wink.battery_level * 100 diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index dbaa9e229a1..86d2a29c21f 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -13,7 +13,7 @@ from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin -REQUIREMENTS = ['python-wink==0.7.5'] +REQUIREMENTS = ['python-wink==0.7.6'] def setup_platform(hass, config, add_devices_callback, discovery_info=None): diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index a8710bb352d..f8c0fd479b7 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -7,9 +7,9 @@ https://home-assistant.io/components/lock.wink/ import logging from homeassistant.components.lock import LockDevice -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL -REQUIREMENTS = ['python-wink==0.7.5'] +REQUIREMENTS = ['python-wink==0.7.6'] def setup_platform(hass, config, add_devices, discovery_info=None): @@ -36,6 +36,7 @@ class WinkLockDevice(LockDevice): def __init__(self, wink): """Initialize the lock.""" self.wink = wink + self._battery = self.wink.battery_level @property def unique_id(self): @@ -68,3 +69,16 @@ class WinkLockDevice(LockDevice): def unlock(self, **kwargs): """Unlock the device.""" self.wink.set_state(False) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._battery: + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } + + @property + def _battery_level(self): + """Return the battery level.""" + return self.wink.battery_level * 100 diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index bfb6f2c595b..c175901eaaa 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -7,10 +7,11 @@ at https://home-assistant.io/components/sensor.wink/ import logging from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED, - STATE_OPEN, TEMP_CELSIUS) + STATE_OPEN, TEMP_CELSIUS, + ATTR_BATTERY_LEVEL) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['python-wink==0.7.5'] +REQUIREMENTS = ['python-wink==0.7.6'] SENSOR_TYPES = ['temperature', 'humidity'] @@ -44,6 +45,7 @@ class WinkSensorDevice(Entity): """Initialize the sensor.""" self.wink = wink self.capability = self.wink.capability() + self._battery = self.wink.battery_level if self.wink.UNIT == "°": self._unit_of_measurement = TEMP_CELSIUS else: @@ -88,6 +90,19 @@ class WinkSensorDevice(Entity): """Return true if door is open.""" return self.wink.state() + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._battery: + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } + + @property + def _battery_level(self): + """Return the battery level.""" + return self.wink.battery_level * 100 + class WinkEggMinder(Entity): """Representation of a Wink Egg Minder.""" @@ -95,6 +110,7 @@ class WinkEggMinder(Entity): def __init__(self, wink): """Initialize the sensor.""" self.wink = wink + self._battery = self.wink.battery_level @property def state(self): @@ -114,3 +130,16 @@ class WinkEggMinder(Entity): def update(self): """Update state of the Egg Minder.""" self.wink.update_state() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._battery: + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } + + @property + def _battery_level(self): + """Return the battery level.""" + return self.wink.battery_level * 100 diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index 6047f1386eb..d01c306db1c 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -9,7 +9,7 @@ import logging from homeassistant.components.wink import WinkToggleDevice from homeassistant.const import CONF_ACCESS_TOKEN -REQUIREMENTS = ['python-wink==0.7.5'] +REQUIREMENTS = ['python-wink==0.7.6'] def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 487b48492cb..74939bd47cb 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -9,13 +9,13 @@ import logging from homeassistant import bootstrap from homeassistant.const import ( ATTR_DISCOVERED, ATTR_SERVICE, CONF_ACCESS_TOKEN, - EVENT_PLATFORM_DISCOVERED) + EVENT_PLATFORM_DISCOVERED, ATTR_BATTERY_LEVEL) from homeassistant.helpers import validate_config from homeassistant.helpers.entity import ToggleEntity from homeassistant.loader import get_component DOMAIN = "wink" -REQUIREMENTS = ['python-wink==0.7.5'] +REQUIREMENTS = ['python-wink==0.7.6'] DISCOVER_LIGHTS = "wink.lights" DISCOVER_SWITCHES = "wink.switches" @@ -68,6 +68,7 @@ class WinkToggleDevice(ToggleEntity): def __init__(self, wink): """Initialize the Wink device.""" self.wink = wink + self._battery = self.wink.battery_level @property def unique_id(self): @@ -100,3 +101,16 @@ class WinkToggleDevice(ToggleEntity): def update(self): """Update state of the device.""" self.wink.update_state() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._battery: + return { + ATTR_BATTERY_LEVEL: self._battery_level, + } + + @property + def _battery_level(self): + """Return the battery level.""" + return self.wink.battery_level * 100 diff --git a/requirements_all.txt b/requirements_all.txt index 2c7e6cc48fa..a84934026e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -259,7 +259,7 @@ python-twitch==1.2.0 # homeassistant.components.lock.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.7.5 +python-wink==0.7.6 # homeassistant.components.keyboard pyuserinput==0.1.9 From ca0ea6c2f33885ea348f63ef8da58857c753f5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 7 May 2016 03:24:43 +0200 Subject: [PATCH 087/101] Rfxtrx bug fix (#1992) * Rfxtrx bug fix * Added Sensor Status to data_types Missing dataype for security1 sensors * Misspelling --- homeassistant/components/rfxtrx.py | 4 +++- homeassistant/components/sensor/rfxtrx.py | 17 +++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 30a670a6a27..27d931106c4 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -39,7 +39,9 @@ DATA_TYPES = OrderedDict([ ('Wind direction', ''), ('Rain rate', ''), ('Energy usage', 'W'), - ('Total usage', 'W')]) + ('Total usage', 'W'), + ('Sensor Status', ''), + ('Unknown', '')]) RECEIVED_EVT_SUBSCRIBERS = [] RFX_DEVICES = {} diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 196b0bfbbd1..49912acb621 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -75,10 +75,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): _LOGGER.info("Automatic add rfxtrx.sensor: %s", device_id) - for data_type in DATA_TYPES: - if data_type in event.values: - new_sensor = RfxtrxSensor(event, pkt_id, data_type) + data_type = "Unknown" + for _data_type in DATA_TYPES: + if _data_type in event.values: + data_type = _data_type break + new_sensor = RfxtrxSensor(event, pkt_id, data_type) sub_sensors = {} sub_sensors[new_sensor.data_type] = new_sensor rfxtrx.RFX_DEVICES[device_id] = sub_sensors @@ -94,12 +96,11 @@ class RfxtrxSensor(Entity): def __init__(self, event, name, data_type): """Initialize the sensor.""" self.event = event - self._unit_of_measurement = None - self.data_type = None self._name = name - if data_type: - self.data_type = data_type - self._unit_of_measurement = DATA_TYPES[data_type] + if data_type not in DATA_TYPES: + data_type = "Unknown" + self.data_type = data_type + self._unit_of_measurement = DATA_TYPES[data_type] def __str__(self): """Return the name of the sensor.""" From b86a1ece010d029d3ea7ddebb9cc5d56673ee889 Mon Sep 17 00:00:00 2001 From: Charles Spirakis Date: Fri, 6 May 2016 18:33:46 -0700 Subject: [PATCH 088/101] Allow conversion from date strings to "unix" timestamp. (#1985) "unix" timestamp is number of seconds since Jan 1, 1970 UTC. This allows scripts that use templates to generate time deltas in seconds if desired from state attributes such as last_updated. Some examples: timestamp now is {{ as_timestamp(now) }} timstamp of last change is {{ as_timestamp(states.binary_sensor.garage_door.last_changed) }} seconds since last change is {{ as_timestamp(now) - as_timestamp(states.binary_sensor.garage_door.last_changed) }} --- homeassistant/helpers/template.py | 1 + homeassistant/util/dt.py | 12 ++++++++++++ tests/util/test_dt.py | 13 +++++++++++++ 3 files changed, 26 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b072f1b2df7..8e039432728 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -56,6 +56,7 @@ def render(hass, template, variables=None, **kwargs): 'now': dt_util.as_local(utcnow), 'states': AllStates(hass), 'utcnow': utcnow, + 'as_timestamp': dt_util.as_timestamp, }).render(kwargs).strip() except jinja2.TemplateError as err: raise TemplateError(err) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index fbd962b3e45..bbcefa54fac 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,4 +1,5 @@ """Provides helper methods to handle the time in HA.""" +import calendar import datetime as dt import re @@ -59,6 +60,17 @@ def as_utc(dattim): return dattim.astimezone(UTC) +def as_timestamp(dt_value): + """Convert a date/time into a unix time (seconds since 1970).""" + if hasattr(dt_value, "utctimetuple"): + parsed_dt = dt_value + else: + parsed_dt = parse_datetime(str(dt_value)) + if not parsed_dt: + raise ValueError("not a valid date/time.") + return calendar.timegm(parsed_dt.utctimetuple()) + + def as_local(dattim): """Convert a UTC datetime object to local time zone.""" if dattim.tzinfo == DEFAULT_TIME_ZONE: diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index c98359f3416..da5b56d42a9 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -107,6 +107,19 @@ class TestDateUtil(unittest.TestCase): datetime(1986, 7, 9, tzinfo=dt_util.UTC), dt_util.utc_from_timestamp(521251200)) + def test_as_timestamp(self): + """Test as_timestamp method.""" + ts = 1462401234 + utc_dt = dt_util.utc_from_timestamp(ts) + self.assertEqual(ts, dt_util.as_timestamp(utc_dt)) + utc_iso = utc_dt.isoformat() + self.assertEqual(ts, dt_util.as_timestamp(utc_iso)) + + # confirm the ability to handle a string passed in + delta = dt_util.as_timestamp("2016-01-01 12:12:12") + delta -= dt_util.as_timestamp("2016-01-01 12:12:11") + self.assertEquals(1, delta) + def test_parse_datetime_converts_correctly(self): """Test parse_datetime converts strings.""" assert \ From fb3b3db04e8b256a850ecc7bc35e9dd2057af960 Mon Sep 17 00:00:00 2001 From: Nolan Gilley Date: Fri, 6 May 2016 21:50:32 -0400 Subject: [PATCH 089/101] Check for 'unknown' sensor values in ecobee (#1983) --- homeassistant/components/sensor/ecobee.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index 61b4c448f5b..61ce2b6770c 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -78,18 +78,10 @@ class EcobeeSensor(Entity): data.update() for sensor in data.ecobee.get_remote_sensors(self.index): for item in sensor['capability']: - if ( - item['type'] == self.type and - self.type == 'temperature' and + if (item['type'] == self.type and self.sensor_name == sensor['name']): - self._state = float(item['value']) / 10 - elif ( - item['type'] == self.type and - self.type == 'humidity' and - self.sensor_name == sensor['name']): - self._state = item['value'] - elif ( - item['type'] == self.type and - self.type == 'occupancy' and - self.sensor_name == sensor['name']): - self._state = item['value'] + if (self.type == 'temperature' and + item['value'] != 'unknown'): + self._state = float(item['value']) / 10 + else: + self._state = item['value'] From a1480582d9486d2b18e144b63f5abdc961a2bd37 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Fri, 6 May 2016 22:11:35 -0700 Subject: [PATCH 090/101] Add /api/discovery_info (#1791) * Allow /api/ and /api/config to be accessed without auth. If config is accessed without auth, only show minimal information. Also improves comments * Re-enable auth on /api/ since a lot of tests get broken if it does not require auth * Move the discovery info from /api/config to /api/discovery_info * Flake8 fixes --- homeassistant/components/api.py | 43 +++++++++++++++++++++++---------- homeassistant/const.py | 1 + 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 00426e24894..3b2972a702c 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -16,9 +16,10 @@ from homeassistant.const import ( CONTENT_TYPE_TEXT_PLAIN, EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_HEADER_CONTENT_TYPE, HTTP_NOT_FOUND, HTTP_OK, HTTP_UNPROCESSABLE_ENTITY, MATCH_ALL, URL_API, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_ERROR_LOG, URL_API_EVENT_FORWARD, URL_API_EVENTS, - URL_API_LOG_OUT, URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, - URL_API_STREAM, URL_API_TEMPLATE) + URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, + URL_API_EVENT_FORWARD, URL_API_EVENTS, URL_API_LOG_OUT, URL_API_SERVICES, + URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, + __version__) from homeassistant.exceptions import TemplateError from homeassistant.helpers.state import TrackStates from homeassistant.helpers import template @@ -37,13 +38,18 @@ def setup(hass, config): # /api - for validation purposes hass.http.register_path('GET', URL_API, _handle_get_api) - # /api/stream - hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream) - # /api/config hass.http.register_path('GET', URL_API_CONFIG, _handle_get_api_config) - # /states + # /api/discovery_info + hass.http.register_path('GET', URL_API_DISCOVERY_INFO, + _handle_get_api_discovery_info, + require_auth=False) + + # /api/stream + hass.http.register_path('GET', URL_API_STREAM, _handle_get_api_stream) + + # /api/states hass.http.register_path('GET', URL_API_STATES, _handle_get_api_states) hass.http.register_path( 'GET', re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'), @@ -58,13 +64,13 @@ def setup(hass, config): 'DELETE', re.compile(r'/api/states/(?P[a-zA-Z\._0-9]+)'), _handle_delete_state_entity) - # /events + # /api/events hass.http.register_path('GET', URL_API_EVENTS, _handle_get_api_events) hass.http.register_path( 'POST', re.compile(r'/api/events/(?P[a-zA-Z\._0-9]+)'), _handle_api_post_events_event) - # /services + # /api/services hass.http.register_path('GET', URL_API_SERVICES, _handle_get_api_services) hass.http.register_path( 'POST', @@ -73,23 +79,23 @@ def setup(hass, config): r'(?P[a-zA-Z\._0-9]+)')), _handle_post_api_services_domain_service) - # /event_forwarding + # /api/event_forwarding hass.http.register_path( 'POST', URL_API_EVENT_FORWARD, _handle_post_api_event_forward) hass.http.register_path( 'DELETE', URL_API_EVENT_FORWARD, _handle_delete_api_event_forward) - # /components + # /api/components hass.http.register_path( 'GET', URL_API_COMPONENTS, _handle_get_api_components) - # /error_log + # /api/error_log hass.http.register_path('GET', URL_API_ERROR_LOG, _handle_get_api_error_log) hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out) - # /template + # /api/template hass.http.register_path('POST', URL_API_TEMPLATE, _handle_post_api_template) @@ -176,6 +182,17 @@ def _handle_get_api_config(handler, path_match, data): handler.write_json(handler.server.hass.config.as_dict()) +def _handle_get_api_discovery_info(handler, path_match, data): + needs_auth = (handler.server.hass.config.api.api_password is not None) + params = { + 'base_url': handler.server.hass.config.api.base_url, + 'location_name': handler.server.hass.config.location_name, + 'requires_api_password': needs_auth, + 'version': __version__ + } + handler.write_json(params) + + def _handle_get_api_states(handler, path_match, data): """Return a dict containing all entity ids and their state.""" handler.write_json(handler.server.hass.states.all()) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2676abbb5bf..e0ca1fcfb1c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -198,6 +198,7 @@ URL_ROOT = "/" URL_API = "/api/" URL_API_STREAM = "/api/stream" URL_API_CONFIG = "/api/config" +URL_API_DISCOVERY_INFO = "/api/discovery_info" URL_API_STATES = "/api/states" URL_API_STATES_ENTITY = "/api/states/{}" URL_API_EVENTS = "/api/events" From 67f3fcc5cf75ce21eb04dab1d2a85b64531a8492 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 May 2016 23:35:15 -0700 Subject: [PATCH 091/101] Update frontend --- .../components/frontend/mdi_version.py | 2 +- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 73 ++++++++++--------- .../www_static/home-assistant-polymer | 2 +- .../components/frontend/www_static/mdi.html | 2 +- .../www_static/webcomponents-lite.min.js | 6 +- 6 files changed, 47 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/frontend/mdi_version.py b/homeassistant/components/frontend/mdi_version.py index 2b8df1a660d..7137aafcdbc 100644 --- a/homeassistant/components/frontend/mdi_version.py +++ b/homeassistant/components/frontend/mdi_version.py @@ -1,2 +1,2 @@ """DO NOT MODIFY. Auto-generated by update_mdi script.""" -VERSION = "af8a531f1c2e477c07c4b3394bd1ce13" +VERSION = "1baebe8155deb447230866d7ae854bd9" diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 427c7104c07..1f5b07185dc 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 = "15c711f899d61b09b8eb101a44a48a29" +VERSION = "7d8744fc68d09561ba4dd2408310d4fe" diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index aaf7cc2c93b..a57fcaa57ef 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -1006,7 +1006,7 @@ font-style: italic; --text-primary-color: var(--dark-theme-text-color); --default-primary-color: var(--primary-color); - }