From 41a0f2c198722c683045bc26d9436b254ce7f8a6 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 29 Nov 2015 10:47:20 +0000 Subject: [PATCH 1/5] Add elevation attribute --- homeassistant/components/sun.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index efe0a7dec2b..687c0856e5f 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -12,12 +12,14 @@ import urllib import homeassistant.util as util import homeassistant.util.dt as dt_util -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import ( + track_point_in_utc_time, track_utc_time_change) from homeassistant.helpers.entity import Entity REQUIREMENTS = ['astral==0.8.1'] DOMAIN = "sun" ENTITY_ID = "sun.sun" +ENTITY_ID_ELEVATION = "sun.elevation" CONF_ELEVATION = 'elevation' @@ -26,6 +28,7 @@ STATE_BELOW_HORIZON = "below_horizon" STATE_ATTR_NEXT_RISING = "next_rising" STATE_ATTR_NEXT_SETTING = "next_setting" +STATE_ATTR_ELEVATION = "elevation" _LOGGER = logging.getLogger(__name__) @@ -139,11 +142,13 @@ class Sun(Entity): self.hass = hass self.location = location self._state = self.next_rising = self.next_setting = None + track_utc_time_change(hass, self.timer_update, second=30) @property def should_poll(self): - """ We trigger updates ourselves after sunset/sunrise """ - return False + """ We trigger updates ourselves after sunset/sunrise, + but sun angle requires poll """ + return True @property def name(self): @@ -159,8 +164,11 @@ class Sun(Entity): @property def state_attributes(self): return { - STATE_ATTR_NEXT_RISING: dt_util.datetime_to_str(self.next_rising), - STATE_ATTR_NEXT_SETTING: dt_util.datetime_to_str(self.next_setting) + STATE_ATTR_NEXT_RISING: + dt_util.datetime_to_str(self.next_rising), + STATE_ATTR_NEXT_SETTING: + dt_util.datetime_to_str(self.next_setting), + STATE_ATTR_ELEVATION: round(self.solar_elevation, 2) } @property @@ -168,6 +176,15 @@ class Sun(Entity): """ Returns the datetime when the next change to the state is. """ return min(self.next_rising, self.next_setting) + @property + def solar_elevation(self): + """ Returns the angle the sun is above the horizon""" + from astral import Astral + return Astral().solar_elevation( + dt_util.utcnow(), + self.location.latitude, + self.location.longitude) + def update_as_of(self, utc_point_in_time): """ Calculate sun state at a point in UTC time. """ mod = -1 @@ -198,3 +215,7 @@ class Sun(Entity): track_point_in_utc_time( self.hass, self.point_in_time_listener, self.next_change + timedelta(seconds=1)) + + def timer_update(self, time): + """ Needed to update solar elevation. """ + self.update_ha_state() From f4c3fbe8fde415c23bedbb714402f694b1bfbcfa Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 29 Nov 2015 11:56:47 +0000 Subject: [PATCH 2/5] Add attribuet config to numeric state platform to allow trigger based in attributes rather than states. --- .../components/automation/numeric_state.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index ab3529235d6..fe0a4f98bd9 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -14,6 +14,7 @@ from homeassistant.helpers.event import track_state_change CONF_ENTITY_ID = "entity_id" CONF_BELOW = "below" CONF_ABOVE = "above" +CONF_ATTRIBUTE = "attribute" _LOGGER = logging.getLogger(__name__) @@ -28,6 +29,7 @@ def trigger(hass, config, action): below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) + attribute = config.get(CONF_ATTRIBUTE) if below is None and above is None: _LOGGER.error("Missing configuration key." @@ -40,8 +42,8 @@ def trigger(hass, config, action): """ Listens for state changes and calls action. """ # Fire action if we go from outside range into range - if _in_range(to_s.state, above, below) and \ - (from_s is None or not _in_range(from_s.state, above, below)): + if _in_range(to_s, above, below, attribute) and \ + (from_s is None or not _in_range(from_s, above, below, attribute)): action() track_state_change( @@ -61,6 +63,7 @@ def if_action(hass, config): below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) + attribute = config.get(CONF_ABOVE) if below is None and above is None: _LOGGER.error("Missing configuration key." @@ -71,14 +74,15 @@ def if_action(hass, config): def if_numeric_state(): """ Test numeric state condition. """ state = hass.states.get(entity_id) - return state is not None and _in_range(state.state, above, below) + return state is not None and _in_range(state, above, below, attribute) return if_numeric_state -def _in_range(value, range_start, range_end): +def _in_range(state, range_start, range_end, attribute): """ Checks if value is inside the range """ - + value = (state.state if attribute is None + else state.attributes.get(attribute)) try: value = float(value) except ValueError: From 73e3ce504479172eaee245793a9ca0a7c878e6b1 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 29 Nov 2015 12:18:54 +0000 Subject: [PATCH 3/5] Fix bug --- homeassistant/components/automation/numeric_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index fe0a4f98bd9..9c9215cacb5 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -63,7 +63,7 @@ def if_action(hass, config): below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) - attribute = config.get(CONF_ABOVE) + attribute = config.get(CONF_ATTRIBUTE) if below is None and above is None: _LOGGER.error("Missing configuration key." From aff1c273725be75426bfa790a3b33f4198ed8773 Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 29 Nov 2015 20:45:03 +0000 Subject: [PATCH 4/5] Remove unused and potentially confusing property --- homeassistant/components/sun.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 687c0856e5f..2e1c0c9b377 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -144,12 +144,6 @@ class Sun(Entity): self._state = self.next_rising = self.next_setting = None track_utc_time_change(hass, self.timer_update, second=30) - @property - def should_poll(self): - """ We trigger updates ourselves after sunset/sunrise, - but sun angle requires poll """ - return True - @property def name(self): return "Sun" From cb0eb2df7dc788ee7a9cf37b9e2ec080f936186b Mon Sep 17 00:00:00 2001 From: pavoni Date: Sun, 29 Nov 2015 21:37:08 +0000 Subject: [PATCH 5/5] Add tests --- .../automation/test_numeric_state.py | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index a04b8d01f4e..8280f396f93 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -253,6 +253,156 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.pool.block_till_done() self.assertEqual(0, len(self.calls)) + def test_if_fires_on_entity_change_below_with_attribute(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 9 is below 10 + self.hass.states.set('test.entity', 9, { 'test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_entity_change_not_below_with_attribute(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10 + self.hass.states.set('test.entity', 11, { 'test_attribute': 9 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_attribute_change_with_attribute_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 9 is below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 9 }) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_attribute_change_with_attribute_not_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_on_entity_change_with_attribute_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10, entity state value should not be tested + self.hass.states.set('test.entity', '9', { 'test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_not_fires_on_entity_change_with_not_attribute_below(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10, entity state value should not be tested + self.hass.states.set('test.entity', 'entity') + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + + def test_if_fires_on_attribute_change_with_attribute_below_multiple_attributes(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 9 is not below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 9, 'not_test_attribute': 11 }) + self.hass.pool.block_till_done() + self.assertEqual(1, len(self.calls)) + + def test_if_not_fires_on_attribute_change_with_attribute_not_below_multiple_attributes(self): + self.assertTrue(automation.setup(self.hass, { + automation.DOMAIN: { + 'trigger': { + 'platform': 'numeric_state', + 'entity_id': 'test.entity', + 'attribute': 'test_attribute', + 'below': 10, + }, + 'action': { + 'service': 'test.automation' + } + } + })) + # 11 is not below 10 + self.hass.states.set('test.entity', 'entity', { 'test_attribute': 11, 'not_test_attribute': 9 }) + self.hass.pool.block_till_done() + self.assertEqual(0, len(self.calls)) + def test_if_action(self): entity_id = 'domain.test_entity' test_state = 10