Various Improvements

This commit is contained in:
Diogo Gomes
2018-02-25 21:53:27 +00:00
parent 81e7c3c383
commit 4b56d7e10a
2 changed files with 41 additions and 28 deletions

View File

@@ -7,7 +7,7 @@ https://home-assistant.io/components/sensor.filter/
import asyncio import asyncio
import logging import logging
import statistics import statistics
from collections import deque from collections import deque, Counter
import voluptuous as vol import voluptuous as vol
@@ -15,7 +15,8 @@ from homeassistant.util import slugify
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID) CONF_NAME, CONF_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID,
ATTR_ICON, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
@@ -36,7 +37,7 @@ DEFAULT_FILTER_RADIUS = 2.0
DEFAULT_FILTER_TIME_CONSTANT = 10 DEFAULT_FILTER_TIME_CONSTANT = 10
NAME_TEMPLATE = "{} filter" NAME_TEMPLATE = "{} filter"
ICON = 'mdi: chart-line-variant' ICON = 'mdi:chart-line-variant'
FILTER_SCHEMA = vol.Schema({ FILTER_SCHEMA = vol.Schema({
vol.Optional(CONF_FILTER_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_WINDOW_SIZE): vol.Coerce(int),
@@ -67,8 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the template sensors.""" """Set up the template sensors."""
sensors = []
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
filters = [] filters = []
@@ -81,16 +80,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
radius = _filter.get(CONF_FILTER_RADIUS) radius = _filter.get(CONF_FILTER_RADIUS)
filters.append(OutlierFilter(window_size=window_size, filters.append(OutlierFilter(window_size=window_size,
precision=precision, precision=precision,
entity=entity_id,
radius=radius)) radius=radius))
elif _filter[CONF_FILTER_NAME] == FILTER_NAME_LOWPASS: elif _filter[CONF_FILTER_NAME] == FILTER_NAME_LOWPASS:
time_constant = _filter.get(CONF_FILTER_TIME_CONSTANT) time_constant = _filter.get(CONF_FILTER_TIME_CONSTANT)
filters.append(LowPassFilter(window_size=window_size, filters.append(LowPassFilter(window_size=window_size,
precision=precision, precision=precision,
entity=entity_id,
time_constant=time_constant)) time_constant=time_constant))
sensors.append(SensorFilter(name, entity_id, filters)) async_add_devices([SensorFilter(name, entity_id, filters)])
async_add_devices(sensors)
class SensorFilter(Entity): class SensorFilter(Entity):
@@ -103,6 +102,7 @@ class SensorFilter(Entity):
self._unit_of_measurement = None self._unit_of_measurement = None
self._state = None self._state = None
self._filters = filters self._filters = filters
self._icon = ICON
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
@@ -112,13 +112,18 @@ class SensorFilter(Entity):
"""Handle device state changes.""" """Handle device state changes."""
self._unit_of_measurement = new_state.attributes.get( self._unit_of_measurement = new_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT) ATTR_UNIT_OF_MEASUREMENT)
self._icon = new_state.attributes.get(ATTR_ICON, ICON)
self._state = new_state.state self._state = new_state.state
if self._state == STATE_UNKNOWN:
return
for filt in self._filters: for filt in self._filters:
try: try:
filtered_state = filt.filter_state(self._state) filtered_state = filt.filter_state(self._state)
_LOGGER.debug("%s(%s) -> %s", filt.name, self._state, _LOGGER.debug("%s(%s, %s) -> %s", filt.name, self._entity,
filtered_state) self._state, filtered_state)
self._state = filtered_state self._state = filtered_state
filt.states.append(filtered_state) filt.states.append(filtered_state)
except ValueError: except ValueError:
@@ -143,7 +148,7 @@ class SensorFilter(Entity):
@property @property
def icon(self): def icon(self):
"""Return the icon to use in the frontend, if any.""" """Return the icon to use in the frontend, if any."""
return ICON return self._icon
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@@ -162,9 +167,13 @@ class SensorFilter(Entity):
ATTR_ENTITY_ID: self._entity ATTR_ENTITY_ID: self._entity
} }
for filt in self._filters: for filt in self._filters:
state_attr.update({ for filt_stat_key, filt_stat_value in filt.stats.items():
slugify("{} stats".format(filt.name)): filt.stats filt_stat = slugify("{} {}".format(filt.name, filt_stat_key))
}) _LOGGER.debug("stats(%s): %s: %s", self._entity,
filt_stat, filt_stat_value)
state_attr.update({
filt_stat: filt_stat_value
})
return state_attr return state_attr
@@ -175,14 +184,17 @@ class Filter(object):
Args: Args:
window_size (int): size of the sliding window that holds previous window_size (int): size of the sliding window that holds previous
values values
precision (int): round filtered value to precision value
entity (string): used for debugging only
""" """
def __init__(self, name, window_size=1, precision=None): def __init__(self, name, window_size=1, precision=None, entity=None):
"""Initialize common attributes.""" """Initialize common attributes."""
self.states = deque(maxlen=window_size) self.states = deque(maxlen=window_size)
self.precision = precision self.precision = precision
self._stats = {} self._stats = {}
self._name = name self._name = name
self._entity = entity
@property @property
def name(self): def name(self):
@@ -216,22 +228,29 @@ class OutlierFilter(Filter):
window_size (int): see Filter() window_size (int): see Filter()
""" """
def __init__(self, window_size, precision, radius): def __init__(self, window_size, precision, entity, radius):
"""Initialize Filter.""" """Initialize Filter."""
super().__init__(FILTER_NAME_OUTLIER, window_size, precision) super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity)
self._radius = radius self._radius = radius
self._stats_internal = Counter()
def _filter_state(self, new_state): def _filter_state(self, new_state):
"""Implement the outlier filter.""" """Implement the outlier filter."""
self._stats_internal['total_filtered'] += 1
new_state = float(new_state) new_state = float(new_state)
self._stats['erasures'] = "{0:.2f}%".format(
100 * self._stats_internal['erasures']
/ self._stats_internal['total_filtered']
)
if (len(self.states) > 1 and if (len(self.states) > 1 and
abs(new_state - statistics.median(self.states)) abs(new_state - statistics.median(self.states))
> self._radius): > self._radius):
erasures = self._stats.get('erasures', 0) self._stats_internal['erasures'] += 1
self._stats['erasures'] = erasures+1
_LOGGER.debug("Outlier in %s: %s", self._name, new_state) _LOGGER.debug("Outlier in %s: %s", self._entity, new_state)
return self.states[-1] return self.states[-1]
return new_state return new_state
@@ -244,9 +263,9 @@ class LowPassFilter(Filter):
window_size (int): see Filter() window_size (int): see Filter()
""" """
def __init__(self, window_size, precision, time_constant): def __init__(self, window_size, precision, entity, time_constant):
"""Initialize Filter.""" """Initialize Filter."""
super().__init__(FILTER_NAME_LOWPASS, window_size, precision) super().__init__(FILTER_NAME_LOWPASS, window_size, precision, entity)
self._time_constant = time_constant self._time_constant = time_constant
def _filter_state(self, new_state): def _filter_state(self, new_state):

View File

@@ -45,9 +45,6 @@ class TestFilterSensor(unittest.TestCase):
with assert_setup_component(1): with assert_setup_component(1):
assert setup_component(self.hass, 'sensor', config) assert setup_component(self.hass, 'sensor', config)
self.hass.start()
self.hass.block_till_done()
for value in self.values: for value in self.values:
self.hass.states.set(config['sensor']['entity_id'], value) self.hass.states.set(config['sensor']['entity_id'], value)
self.hass.block_till_done() self.hass.block_till_done()
@@ -72,9 +69,6 @@ class TestFilterSensor(unittest.TestCase):
with assert_setup_component(1): with assert_setup_component(1):
assert setup_component(self.hass, 'sensor', config) assert setup_component(self.hass, 'sensor', config)
self.hass.start()
self.hass.block_till_done()
for value in self.values: for value in self.values:
self.hass.states.set(config['sensor']['entity_id'], value) self.hass.states.set(config['sensor']['entity_id'], value)
self.hass.block_till_done() self.hass.block_till_done()