From 616c7628d7ce23e96ae22bdad31e0356b234cbab Mon Sep 17 00:00:00 2001 From: siberx Date: Sat, 23 Feb 2019 05:51:34 -0800 Subject: [PATCH] Fixes the sensor.filter outlier filter (handle step-changes correctly) (#21332) * Fix outlier filter median return, Add/update filter outlier tests * Switch outlier filter to store raw vals (handles step-changes correctly) * Filter store_raw as attribute instead of filter_state parameter * Fix linting issues --- homeassistant/components/sensor/filter.py | 14 ++++++++++---- tests/components/sensor/test_filter.py | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 3d05dd28e79..92e2cc751ac 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -313,6 +313,7 @@ class Filter: self._entity = entity self._skip_processing = False self._window_size = window_size + self._store_raw = False @property def window_size(self): @@ -337,7 +338,10 @@ class Filter: """Implement a common interface for filters.""" filtered = self._filter_state(FilterState(new_state)) filtered.set_precision(self.precision) - self.states.append(copy(filtered)) + if self._store_raw: + self.states.append(copy(FilterState(new_state))) + else: + self.states.append(copy(filtered)) new_state.state = filtered.state return new_state @@ -402,12 +406,14 @@ class OutlierFilter(Filter): super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._radius = radius self._stats_internal = Counter() + self._store_raw = True def _filter_state(self, new_state): """Implement the outlier filter.""" + median = statistics.median([s.state for s in self.states]) \ + if self.states else 0 if (len(self.states) == self.states.maxlen and - abs(new_state.state - - statistics.median([s.state for s in self.states])) > + abs(new_state.state - median) > self._radius): self._stats_internal['erasures'] += 1 @@ -415,7 +421,7 @@ class OutlierFilter(Filter): _LOGGER.debug("Outlier nr. %s in %s: %s", self._stats_internal['erasures'], self._entity, new_state) - return self.states[-1] + new_state.state = median return new_state diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index b43d38da5e8..29308f2a83d 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -106,6 +106,23 @@ class TestFilterSensor(unittest.TestCase): precision=2, entity=None, radius=4.0) + for state in self.values: + filtered = filt.filter_state(state) + assert 21 == filtered.state + + def test_outlier_step(self): + """ + Test step-change handling in outlier. + + Test if outlier filter handles long-running step-changes correctly. + It should converge to no longer filter once just over half the + window_size is occupied by the new post step-change values. + """ + filt = OutlierFilter(window_size=3, + precision=2, + entity=None, + radius=1.1) + self.values[-1].state = 22 for state in self.values: filtered = filt.filter_state(state) assert 22 == filtered.state @@ -119,7 +136,7 @@ class TestFilterSensor(unittest.TestCase): out = ha.State('sensor.test_monitored', 4000) for state in [out]+self.values: filtered = filt.filter_state(state) - assert 22 == filtered.state + assert 21 == filtered.state def test_lowpass(self): """Test if lowpass filter works."""