From da503197c9994d1b7bda4ac5fa3b3a6001fad641 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 12 Mar 2018 01:44:05 +0000 Subject: [PATCH 1/7] Added Time based SMA --- homeassistant/components/sensor/filter.py | 56 ++++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index cde50699b29c82..39976a0e51d31b 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -8,6 +8,7 @@ import statistics from collections import deque, Counter from numbers import Number +from datetime import datetime import voluptuous as vol @@ -26,6 +27,7 @@ FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' +FILTER_NAME_TIME_SMA = 'time_sma' FILTERS = Registry() CONF_FILTERS = 'filters' @@ -44,24 +46,32 @@ ICON = 'mdi:chart-line-variant' FILTER_SCHEMA = vol.Schema({ - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), }) FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_RADIUS, default=DEFAULT_FILTER_RADIUS): vol.Coerce(float), }) FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_LOWPASS, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_TIME_CONSTANT, default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), }) +FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, + cv.positive_timedelta) +}) + FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, }) @@ -72,6 +82,7 @@ vol.Required(CONF_FILTERS): vol.All(cv.ensure_list, [vol.Any(FILTER_OUTLIER_SCHEMA, FILTER_LOWPASS_SCHEMA, + FILTER_TIME_SMA_SCHEMA, FILTER_THROTTLE_SCHEMA)]) }) @@ -277,6 +288,47 @@ def _filter_state(self, new_state): return filtered +@FILTERS.register(FILTER_NAME_TIME_SMA) +class TimeSMAFilter(Filter): + """Simple Moving Average (SMA) Filter. + + The window_size is determined by time, and SMA is time weighted. + """ + + def __init__(self, window_size, precision, entity): + """Initialize Filter.""" + super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) + self._time_window = int(window_size.total_seconds()) + self.last_leak = None + self.queue = deque() + + def _leak(self): + """Remove timeouted elements.""" + now = int(datetime.now().timestamp()) + + while self.queue: + timestamp, _ = self.queue[0] + if timestamp + self._time_window <= now: + self.last_leak = self.queue.popleft() + else: + break + return now + + def _filter_state(self, new_state): + now = self._leak() + self.queue.append((now, float(new_state))) + moving_sum = 0 + start = now - self._time_window + _, prev_val = self.last_leak or (0, float(new_state)) + + for timestamp, val in self.queue: + moving_sum += (timestamp-start)*prev_val + start, prev_val = timestamp, val + moving_sum += (now-start)*prev_val + + return moving_sum/self._time_window + + @FILTERS.register(FILTER_NAME_THROTTLE) class ThrottleFilter(Filter): """Throttle Filter. From 90e1dcba7c7ed45f91ab97feacf8b780d3e1f35f Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 12 Mar 2018 21:51:57 +0000 Subject: [PATCH 2/7] move "now" to _filter_state() --- homeassistant/components/sensor/filter.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 39976a0e51d31b..bab25b3cccf4a2 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -302,20 +302,19 @@ def __init__(self, window_size, precision, entity): self.last_leak = None self.queue = deque() - def _leak(self): + def _leak(self, now): """Remove timeouted elements.""" - now = int(datetime.now().timestamp()) - while self.queue: timestamp, _ = self.queue[0] if timestamp + self._time_window <= now: self.last_leak = self.queue.popleft() else: - break - return now + return def _filter_state(self, new_state): - now = self._leak() + now = int(datetime.now().timestamp()) + + self._leak(now) self.queue.append((now, float(new_state))) moving_sum = 0 start = now - self._time_window From 790d67adcd761df0f9898d64d353fb29e0f33b11 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 15 Mar 2018 18:38:47 +0000 Subject: [PATCH 3/7] Addressed comments --- homeassistant/components/sensor/filter.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index bab25b3cccf4a2..a5dd5a10b6aad2 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -8,7 +8,6 @@ import statistics from collections import deque, Counter from numbers import Number -from datetime import datetime import voluptuous as vol @@ -21,6 +20,7 @@ from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -36,6 +36,9 @@ CONF_FILTER_PRECISION = 'precision' CONF_FILTER_RADIUS = 'radius' CONF_FILTER_TIME_CONSTANT = 'time_constant' +CONF_TIME_SMA_TYPE = 'variant' + +TIME_SMA_LAST = 'last' DEFAULT_WINDOW_SIZE = 1 DEFAULT_PRECISION = 2 @@ -68,6 +71,9 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, + vol.Optional(CONF_TIME_SMA_TYPE, + default=TIME_SMA_LAST): vol.All(cv.ensure_list, [vol.Any(TIME_SMA_LAST)]), + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, cv.positive_timedelta) }) @@ -293,9 +299,12 @@ class TimeSMAFilter(Filter): """Simple Moving Average (SMA) Filter. The window_size is determined by time, and SMA is time weighted. + + Args: + variant (enum): type of argorithm used to connect discrete values """ - def __init__(self, window_size, precision, entity): + def __init__(self, window_size, precision, entity, variant): """Initialize Filter.""" super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) self._time_window = int(window_size.total_seconds()) @@ -312,7 +321,7 @@ def _leak(self, now): return def _filter_state(self, new_state): - now = int(datetime.now().timestamp()) + now = int(dt_util.utcnow().timestamp()) self._leak(now) self.queue.append((now, float(new_state))) From 212e341e333f69c22b275eb09ca7a29a29b73245 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 15 Mar 2018 22:27:46 +0000 Subject: [PATCH 4/7] fix long line --- homeassistant/components/sensor/filter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index a5dd5a10b6aad2..9779e399b15888 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -72,7 +72,8 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, vol.Optional(CONF_TIME_SMA_TYPE, - default=TIME_SMA_LAST): vol.All(cv.ensure_list, [vol.Any(TIME_SMA_LAST)]), + default=TIME_SMA_LAST): vol.All( + cv.ensure_list, [vol.Any(TIME_SMA_LAST)]), vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, cv.positive_timedelta) From 3467fdddc17a53de15880c3aa800f2995f9cbef0 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 16 Mar 2018 02:24:09 +0000 Subject: [PATCH 5/7] type and name --- homeassistant/components/sensor/filter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 9779e399b15888..7e74b82d4216cc 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -27,7 +27,7 @@ FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' -FILTER_NAME_TIME_SMA = 'time_sma' +FILTER_NAME_TIME_SMA = 'time_simple_moving_average' FILTERS = Registry() CONF_FILTERS = 'filters' @@ -36,7 +36,7 @@ CONF_FILTER_PRECISION = 'precision' CONF_FILTER_RADIUS = 'radius' CONF_FILTER_TIME_CONSTANT = 'time_constant' -CONF_TIME_SMA_TYPE = 'variant' +CONF_TIME_SMA_TYPE = 'type' TIME_SMA_LAST = 'last' @@ -72,8 +72,8 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, vol.Optional(CONF_TIME_SMA_TYPE, - default=TIME_SMA_LAST): vol.All( - cv.ensure_list, [vol.Any(TIME_SMA_LAST)]), + default=TIME_SMA_LAST): vol.In( + [None, TIME_SMA_LAST]), vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, cv.positive_timedelta) @@ -305,7 +305,7 @@ class TimeSMAFilter(Filter): variant (enum): type of argorithm used to connect discrete values """ - def __init__(self, window_size, precision, entity, variant): + def __init__(self, window_size, precision, entity, type): """Initialize Filter.""" super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) self._time_window = int(window_size.total_seconds()) From 2f767f98fe13939fa3b7e61b8f93f4e47904c306 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 16 Mar 2018 02:29:19 +0000 Subject: [PATCH 6/7] # pylint: disable=redefined-builtin --- homeassistant/components/sensor/filter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 7e74b82d4216cc..aad7fec26a0039 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -53,6 +53,7 @@ default=DEFAULT_PRECISION): vol.Coerce(int), }) +# pylint: disable=redefined-builtin FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, vol.Optional(CONF_FILTER_WINDOW_SIZE, From 8480db5944a3121f0e484ea8be0ce50c0c7dea33 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 16 Mar 2018 19:38:28 +0000 Subject: [PATCH 7/7] added test --- tests/components/sensor/test_filter.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index dd1112d65f8832..0d4082731ab38f 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -1,8 +1,11 @@ """The test for the data filter sensor platform.""" +from datetime import timedelta import unittest +from unittest.mock import patch from homeassistant.components.sensor.filter import ( - LowPassFilter, OutlierFilter, ThrottleFilter) + LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) +import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, assert_setup_component @@ -90,3 +93,16 @@ def test_throttle(self): if not filt.skip_processing: filtered.append(new_state) self.assertEqual([20, 21], filtered) + + def test_time_sma(self): + """Test if time_sma filter works.""" + filt = TimeSMAFilter(window_size=timedelta(minutes=2), + precision=2, + entity=None, + type='last') + past = dt_util.utcnow() - timedelta(minutes=5) + for state in self.values: + with patch('homeassistant.util.dt.utcnow', return_value=past): + filtered = filt.filter_state(state) + past += timedelta(minutes=1) + self.assertEqual(21.5, filtered)