From 4eba1250e9c9b7db3d2e979feb59d3a8d1f84c0a Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sat, 12 Sep 2015 21:42:52 +0200 Subject: [PATCH 1/8] Added a numeric_state automation platform --- .../components/automation/numeric_state.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 homeassistant/components/automation/numeric_state.py diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py new file mode 100644 index 00000000000..96c0dc5a5ce --- /dev/null +++ b/homeassistant/components/automation/numeric_state.py @@ -0,0 +1,61 @@ +""" +homeassistant.components.automation.state +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Offers state listening automation rules. +""" +import logging + +from homeassistant.helpers.event import track_state_change +from homeassistant.const import MATCH_ALL + + +CONF_ENTITY_ID = "state_entity_id" +CONF_BELOW = "state_below" +CONF_ABOVE = "state_above" + + +def register(hass, config, action): + """ Listen for state changes based on `config`. """ + entity_id = config.get(CONF_ENTITY_ID) + + if entity_id is None: + logging.getLogger(__name__).error( + "Missing configuration key %s", CONF_ENTITY_ID) + return False + + below = config.get(CONF_BELOW) + above = config.get(CONF_ABOVE) + + if below is None and above is None: + logging.getLogger(__name__).error( + "Missing configuration key %s or %s", CONF_BELOW, CONF_ABOVE) + + def numeric_in_range(value, range_start, range_end): + """ Checks if value is inside the range + :param value: + :param range_start: + :param range_end: + :return: + """ + value = float(value) + + if range_start is not None and range_end is not None: + return float(range_start) < value < float(range_end) + elif range_end is not None: + return value < float(range_end) + else: + return value > float(range_start) + + def state_automation_listener(entity, from_s, to_s): + """ Listens for state changes and calls action. """ + + # Fire action if we go from outside range into range + if numeric_in_range(to_s.state, above, below) and \ + from_s is None or not numeric_in_range(from_s.state, above, below): + action() + + track_state_change( + hass, entity_id, state_automation_listener) + + return True From 8e89308a1543c1b1aeefd6f7bd33f1260d0660f9 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 13 Sep 2015 12:15:21 +0200 Subject: [PATCH 2/8] Added better handling if we did not get a value for the numeric check --- .../components/automation/numeric_state.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 96c0dc5a5ce..735b6511567 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -7,29 +7,27 @@ Offers state listening automation rules. import logging from homeassistant.helpers.event import track_state_change -from homeassistant.const import MATCH_ALL CONF_ENTITY_ID = "state_entity_id" CONF_BELOW = "state_below" CONF_ABOVE = "state_above" +_LOGGER = logging.getLogger(__name__) def register(hass, config, action): """ Listen for state changes based on `config`. """ entity_id = config.get(CONF_ENTITY_ID) if entity_id is None: - logging.getLogger(__name__).error( - "Missing configuration key %s", CONF_ENTITY_ID) + _LOGGER.error("Missing configuration key %s", CONF_ENTITY_ID) return False below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) if below is None and above is None: - logging.getLogger(__name__).error( - "Missing configuration key %s or %s", CONF_BELOW, CONF_ABOVE) + _LOGGER.error("Missing configuration key %s or %s", CONF_BELOW, CONF_ABOVE) def numeric_in_range(value, range_start, range_end): """ Checks if value is inside the range @@ -38,6 +36,11 @@ def register(hass, config, action): :param range_end: :return: """ + + if value is None: + _LOGGER.warn("Missing value in numeric check") + return False + value = float(value) if range_start is not None and range_end is not None: From 50f5f1860c7a35acaf577d0077ba7659dfa85bef Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 13 Sep 2015 12:53:37 +0200 Subject: [PATCH 3/8] Added a numeric_state automation platform test ( UNTESTED ) --- .../automation/test_numeric_state.py | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 tests/components/automation/test_numeric_state.py diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py new file mode 100644 index 00000000000..bf48d72e3a2 --- /dev/null +++ b/tests/components/automation/test_numeric_state.py @@ -0,0 +1,235 @@ +""" +tests.test_component_demo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tests demo component. +""" +import unittest + +import homeassistant.core as ha +import homeassistant.components.automation as automation +import homeassistant.components.automation.numeric_state as numeric_state +from homeassistant.const import CONF_PLATFORM + + +class TestAutomationState(unittest.TestCase): + """ Test the event automation. """ + + def setUp(self): # pylint: disable=invalid-name + self.hass = ha.HomeAssistant() + self.hass.states.set('test.entity', 'hello') + self.calls = [] + + def record_call(service): + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def tearDown(self): # pylint: disable=invalid-name + """ Stop down stuff we started. """ + self.hass.stop() + + def test_setup_fails_if_no_entity_id(self): + self.assertFalse(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + def test_setup_fails_if_no_condition(self): + self.assertFalse(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + automation.CONF_SERVICE: 'test.automation' + } + })) + + def test_if_fires_on_entity_change_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + # 9 is below 10 + self.hass.states.set('test.entity', 9) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_over_to_below(self): + self.hass.states.set('test.entity', 11) + self.hass.pool.block_till_done() + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + # 9 is below 10 + self.hass.states.set('test.entity', 9) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + + def test_if_not_fires_on_entity_change_below_to_below(self): + self.hass.states.set('test.entity', 9) + self.hass.pool.block_till_done() + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + # 9 is below 10 so this should not fire again + self.hass.states.set('test.entity', 8) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + + def test_if_fires_on_entity_change_above(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_ABOVE: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + # 11 is above 10 + self.hass.states.set('test.entity', 11) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_below_to_above(self): + # set initial state + self.hass.states.set('test.entity', 9) + self.hass.pool.block_till_done() + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_ABOVE: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + # 11 is above 10 and 9 is below + self.hass.states.set('test.entity', 11) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + + def test_if_not_fires_on_entity_change_above_to_above(self): + # set initial state + self.hass.states.set('test.entity', 11) + self.hass.pool.block_till_done() + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_ABOVE: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + # 11 is above 10 so this should fire again + self.hass.states.set('test.entity', 12) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_entity_change_below_range(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_ABOVE: 5, + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + # 9 is below 10 + self.hass.states.set('test.entity', 9) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_below_above_range(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_ABOVE: 5, + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + # 4 is below 5 + self.hass.states.set('test.entity', 4) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_entity_change_over_to_below_range(self): + self.hass.states.set('test.entity', 11) + self.hass.pool.block_till_done() + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_ABOVE: 5, + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + # 9 is below 10 + self.hass.states.set('test.entity', 9) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_fires_on_entity_change_over_to_below_above_range(self): + self.hass.states.set('test.entity', 11) + self.hass.pool.block_till_done() + + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.entity', + numeric_state.CONF_ABOVE: 5, + numeric_state.CONF_BELOW: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + # 9 is below 10 + self.hass.states.set('test.entity', 4) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_if_entity_not_match(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + CONF_PLATFORM: 'numeric_state', + numeric_state.CONF_ENTITY_ID: 'test.another_entity', + numeric_state.CONF_ABOVE: 10, + automation.CONF_SERVICE: 'test.automation' + } + })) + + self.hass.states.set('test.entity', 11) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) From a2ca60159d9b089bc957865bd26d84576d47bd8a Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 13 Sep 2015 13:05:36 +0200 Subject: [PATCH 4/8] Fixed logic --- homeassistant/components/automation/numeric_state.py | 11 +++++++---- tests/components/automation/test_numeric_state.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 735b6511567..b07599390e1 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -15,6 +15,7 @@ CONF_ABOVE = "state_above" _LOGGER = logging.getLogger(__name__) + def register(hass, config, action): """ Listen for state changes based on `config`. """ entity_id = config.get(CONF_ENTITY_ID) @@ -27,7 +28,9 @@ def register(hass, config, action): above = config.get(CONF_ABOVE) if below is None and above is None: - _LOGGER.error("Missing configuration key %s or %s", CONF_BELOW, CONF_ABOVE) + _LOGGER.error("Missing configuration key. One of %s or %s is required", + CONF_BELOW, CONF_ABOVE) + return False def numeric_in_range(value, range_start, range_end): """ Checks if value is inside the range @@ -44,18 +47,18 @@ def register(hass, config, action): value = float(value) if range_start is not None and range_end is not None: - return float(range_start) < value < float(range_end) + return float(range_start) <= value < float(range_end) elif range_end is not None: return value < float(range_end) else: - return value > float(range_start) + return float(range_start) <= value def state_automation_listener(entity, from_s, to_s): """ Listens for state changes and calls action. """ # Fire action if we go from outside range into range if numeric_in_range(to_s.state, above, below) and \ - from_s is None or not numeric_in_range(from_s.state, above, below): + (from_s is None or not numeric_in_range(from_s.state, above, below)): action() track_state_change( diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index bf48d72e3a2..f923c299e51 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -215,7 +215,7 @@ class TestAutomationState(unittest.TestCase): } })) - # 9 is below 10 + # 4 is below 5 so it should not fire self.hass.states.set('test.entity', 4) self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) From e9da02d70cf7c41fcef2b47bf21aaeea74337979 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 13 Sep 2015 19:59:26 +0200 Subject: [PATCH 5/8] Fixed value error exception Fixed unittest --- homeassistant/components/automation/numeric_state.py | 6 +++--- tests/components/automation/test_numeric_state.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b07599390e1..24c9f9db14a 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,12 +40,12 @@ def register(hass, config, action): :return: """ - if value is None: + try: + value = float(value) + except ValueError: _LOGGER.warn("Missing value in numeric check") return False - value = float(value) - if range_start is not None and range_end is not None: return float(range_start) <= value < float(range_end) elif range_end is not None: diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index f923c299e51..0b3a0bfba63 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -12,12 +12,11 @@ import homeassistant.components.automation.numeric_state as numeric_state from homeassistant.const import CONF_PLATFORM -class TestAutomationState(unittest.TestCase): +class TestAutomationNumericState(unittest.TestCase): """ Test the event automation. """ def setUp(self): # pylint: disable=invalid-name self.hass = ha.HomeAssistant() - self.hass.states.set('test.entity', 'hello') self.calls = [] def record_call(service): @@ -96,7 +95,7 @@ class TestAutomationState(unittest.TestCase): # 9 is below 10 so this should not fire again self.hass.states.set('test.entity', 8) self.hass.pool.block_till_done() - self.assertEqual(1, len(self.calls)) + self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_above(self): From 9904727cde33278e8669c391ec189e599c1eb7c9 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 13 Sep 2015 20:16:51 +0200 Subject: [PATCH 6/8] homeassistant/components/automation/numeric_state.py:61:80: E501 line too long (80 > 79 characters) The command "flake8 homeassistant" exited with 1. --- homeassistant/components/automation/numeric_state.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 24c9f9db14a..23c2093637b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -28,7 +28,8 @@ def register(hass, config, action): above = config.get(CONF_ABOVE) if below is None and above is None: - _LOGGER.error("Missing configuration key. One of %s or %s is required", + _LOGGER.error("Missing configuration key." + " One of %s or %s is required", CONF_BELOW, CONF_ABOVE) return False @@ -57,8 +58,8 @@ def register(hass, config, action): """ Listens for state changes and calls action. """ # Fire action if we go from outside range into range - if numeric_in_range(to_s.state, above, below) and \ - (from_s is None or not numeric_in_range(from_s.state, above, below)): + if numeric_in_range(to_s.state, above, below) and (from_s is None + or not numeric_in_range(from_s.state, above, below)): action() track_state_change( From e3dcb458791db32454782acdcecc0e75f316a388 Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 13 Sep 2015 20:27:11 +0200 Subject: [PATCH 7/8] Fixed pylint error --- homeassistant/components/automation/numeric_state.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 23c2093637b..c3f94695216 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -58,8 +58,9 @@ def register(hass, config, action): """ Listens for state changes and calls action. """ # Fire action if we go from outside range into range - if numeric_in_range(to_s.state, above, below) and (from_s is None - or not numeric_in_range(from_s.state, above, below)): + if numeric_in_range(to_s.state, above, below) and \ + (from_s is None or \ + not numeric_in_range(from_s.state, above, below)): action() track_state_change( From 8360ab265cfa34de4513218aa8c7e77da239fe3d Mon Sep 17 00:00:00 2001 From: Stefan Jonasson Date: Sun, 13 Sep 2015 20:34:45 +0200 Subject: [PATCH 8/8] Not used to pylint and flake8 ... --- homeassistant/components/automation/numeric_state.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index c3f94695216..79312bc9e58 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -33,7 +33,7 @@ def register(hass, config, action): CONF_BELOW, CONF_ABOVE) return False - def numeric_in_range(value, range_start, range_end): + def _in_range(value, range_start, range_end): """ Checks if value is inside the range :param value: :param range_start: @@ -58,9 +58,8 @@ def register(hass, config, action): """ Listens for state changes and calls action. """ # Fire action if we go from outside range into range - if numeric_in_range(to_s.state, above, below) and \ - (from_s is None or \ - not numeric_in_range(from_s.state, above, below)): + if _in_range(to_s.state, above, below) and \ + (from_s is None or not _in_range(from_s.state, above, below)): action() track_state_change(