Add hysteresis attribute to threshold binary sensor (#9596)

* Added hysteresis attribute to threshold binary sensor

* Added threshold binary sensor hysteresis test case

* Changed threshold binary sensor property name to be more self explanatory

* Pulled default hysteresis value into top level declaration

* Fixed linter errors

* Fixed additional linter errors

* Move comment to docs
This commit is contained in:
Sam Birch 2017-10-03 04:15:19 +13:00 committed by Fabian Affolter
parent b4551cc127
commit da4048a9ec
2 changed files with 73 additions and 11 deletions

View File

@ -20,15 +20,18 @@ from homeassistant.helpers.event import async_track_state_change
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
ATTR_HYSTERESIS = 'hysteresis'
ATTR_SENSOR_VALUE = 'sensor_value' ATTR_SENSOR_VALUE = 'sensor_value'
ATTR_THRESHOLD = 'threshold' ATTR_THRESHOLD = 'threshold'
ATTR_TYPE = 'type' ATTR_TYPE = 'type'
CONF_HYSTERESIS = 'hysteresis'
CONF_LOWER = 'lower' CONF_LOWER = 'lower'
CONF_THRESHOLD = 'threshold' CONF_THRESHOLD = 'threshold'
CONF_UPPER = 'upper' CONF_UPPER = 'upper'
DEFAULT_NAME = 'Threshold' DEFAULT_NAME = 'Threshold'
DEFAULT_HYSTERESIS = 0.0
SENSOR_TYPES = [CONF_LOWER, CONF_UPPER] SENSOR_TYPES = [CONF_LOWER, CONF_UPPER]
@ -36,6 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_THRESHOLD): vol.Coerce(float), vol.Required(CONF_THRESHOLD): vol.Coerce(float),
vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES), vol.Required(CONF_TYPE): vol.In(SENSOR_TYPES),
vol.Optional(
CONF_HYSTERESIS, default=DEFAULT_HYSTERESIS): vol.Coerce(float),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}) })
@ -47,28 +52,32 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
threshold = config.get(CONF_THRESHOLD) threshold = config.get(CONF_THRESHOLD)
hysteresis = config.get(CONF_HYSTERESIS)
limit_type = config.get(CONF_TYPE) limit_type = config.get(CONF_TYPE)
device_class = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
async_add_devices( async_add_devices([ThresholdSensor(
[ThresholdSensor(hass, entity_id, name, threshold, limit_type, hass, entity_id, name, threshold,
device_class)], True) hysteresis, limit_type, device_class)
], True)
return True return True
class ThresholdSensor(BinarySensorDevice): class ThresholdSensor(BinarySensorDevice):
"""Representation of a Threshold sensor.""" """Representation of a Threshold sensor."""
def __init__(self, hass, entity_id, name, threshold, limit_type, def __init__(self, hass, entity_id, name, threshold,
device_class): hysteresis, limit_type, device_class):
"""Initialize the Threshold sensor.""" """Initialize the Threshold sensor."""
self._hass = hass self._hass = hass
self._entity_id = entity_id self._entity_id = entity_id
self.is_upper = limit_type == 'upper' self.is_upper = limit_type == 'upper'
self._name = name self._name = name
self._threshold = threshold self._threshold = threshold
self._hysteresis = hysteresis
self._device_class = device_class self._device_class = device_class
self._deviation = False self._state = False
self.sensor_value = 0 self.sensor_value = 0
@callback @callback
@ -97,7 +106,7 @@ class ThresholdSensor(BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return true if sensor is on.""" """Return true if sensor is on."""
return self._deviation return self._state
@property @property
def should_poll(self): def should_poll(self):
@ -116,13 +125,16 @@ class ThresholdSensor(BinarySensorDevice):
ATTR_ENTITY_ID: self._entity_id, ATTR_ENTITY_ID: self._entity_id,
ATTR_SENSOR_VALUE: self.sensor_value, ATTR_SENSOR_VALUE: self.sensor_value,
ATTR_THRESHOLD: self._threshold, ATTR_THRESHOLD: self._threshold,
ATTR_HYSTERESIS: self._hysteresis,
ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER, ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER,
} }
@asyncio.coroutine @asyncio.coroutine
def async_update(self): def async_update(self):
"""Get the latest data and updates the states.""" """Get the latest data and updates the states."""
if self.is_upper: if self._hysteresis == 0 and self.sensor_value == self._threshold:
self._deviation = bool(self.sensor_value > self._threshold) self._state = False
else: elif self.sensor_value > (self._threshold + self._hysteresis):
self._deviation = bool(self.sensor_value < self._threshold) self._state = self.is_upper
elif self.sensor_value < (self._threshold - self._hysteresis):
self._state = not self.is_upper

View File

@ -96,3 +96,53 @@ class TestThresholdSensor(unittest.TestCase):
state = self.hass.states.get('binary_sensor.test_threshold') state = self.hass.states.get('binary_sensor.test_threshold')
assert state.state == 'off' assert state.state == 'off'
def test_sensor_hysteresis(self):
"""Test if source is above threshold using hysteresis."""
config = {
'binary_sensor': {
'platform': 'threshold',
'threshold': '15',
'hysteresis': '2.5',
'name': 'Test_threshold',
'type': 'upper',
'entity_id': 'sensor.test_monitored',
}
}
assert setup_component(self.hass, 'binary_sensor', config)
self.hass.states.set('sensor.test_monitored', 20)
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test_threshold')
assert state.state == 'on'
self.hass.states.set('sensor.test_monitored', 13)
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test_threshold')
assert state.state == 'on'
self.hass.states.set('sensor.test_monitored', 12)
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test_threshold')
assert state.state == 'off'
self.hass.states.set('sensor.test_monitored', 17)
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test_threshold')
assert state.state == 'off'
self.hass.states.set('sensor.test_monitored', 18)
self.hass.block_till_done()
state = self.hass.states.get('binary_sensor.test_threshold')
assert state.state == 'on'