From da4048a9ec8542c4917fc05d8c4d61b848eae9d6 Mon Sep 17 00:00:00 2001 From: Sam Birch Date: Tue, 3 Oct 2017 04:15:19 +1300 Subject: [PATCH] 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 --- .../components/binary_sensor/threshold.py | 34 +++++++++---- .../binary_sensor/test_threshold.py | 50 +++++++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/binary_sensor/threshold.py b/homeassistant/components/binary_sensor/threshold.py index 866e16ecbe2..5ca037767f2 100644 --- a/homeassistant/components/binary_sensor/threshold.py +++ b/homeassistant/components/binary_sensor/threshold.py @@ -20,15 +20,18 @@ from homeassistant.helpers.event import async_track_state_change _LOGGER = logging.getLogger(__name__) +ATTR_HYSTERESIS = 'hysteresis' ATTR_SENSOR_VALUE = 'sensor_value' ATTR_THRESHOLD = 'threshold' ATTR_TYPE = 'type' +CONF_HYSTERESIS = 'hysteresis' CONF_LOWER = 'lower' CONF_THRESHOLD = 'threshold' CONF_UPPER = 'upper' DEFAULT_NAME = 'Threshold' +DEFAULT_HYSTERESIS = 0.0 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_THRESHOLD): vol.Coerce(float), 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_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) name = config.get(CONF_NAME) threshold = config.get(CONF_THRESHOLD) + hysteresis = config.get(CONF_HYSTERESIS) limit_type = config.get(CONF_TYPE) device_class = config.get(CONF_DEVICE_CLASS) - async_add_devices( - [ThresholdSensor(hass, entity_id, name, threshold, limit_type, - device_class)], True) + async_add_devices([ThresholdSensor( + hass, entity_id, name, threshold, + hysteresis, limit_type, device_class) + ], True) + return True class ThresholdSensor(BinarySensorDevice): """Representation of a Threshold sensor.""" - def __init__(self, hass, entity_id, name, threshold, limit_type, - device_class): + def __init__(self, hass, entity_id, name, threshold, + hysteresis, limit_type, device_class): """Initialize the Threshold sensor.""" self._hass = hass self._entity_id = entity_id self.is_upper = limit_type == 'upper' self._name = name self._threshold = threshold + self._hysteresis = hysteresis self._device_class = device_class - self._deviation = False + self._state = False self.sensor_value = 0 @callback @@ -97,7 +106,7 @@ class ThresholdSensor(BinarySensorDevice): @property def is_on(self): """Return true if sensor is on.""" - return self._deviation + return self._state @property def should_poll(self): @@ -116,13 +125,16 @@ class ThresholdSensor(BinarySensorDevice): ATTR_ENTITY_ID: self._entity_id, ATTR_SENSOR_VALUE: self.sensor_value, ATTR_THRESHOLD: self._threshold, + ATTR_HYSTERESIS: self._hysteresis, ATTR_TYPE: CONF_UPPER if self.is_upper else CONF_LOWER, } @asyncio.coroutine def async_update(self): """Get the latest data and updates the states.""" - if self.is_upper: - self._deviation = bool(self.sensor_value > self._threshold) - else: - self._deviation = bool(self.sensor_value < self._threshold) + if self._hysteresis == 0 and self.sensor_value == self._threshold: + self._state = False + elif self.sensor_value > (self._threshold + self._hysteresis): + self._state = self.is_upper + elif self.sensor_value < (self._threshold - self._hysteresis): + self._state = not self.is_upper diff --git a/tests/components/binary_sensor/test_threshold.py b/tests/components/binary_sensor/test_threshold.py index 5bc62654a1f..d8c49de1cc0 100644 --- a/tests/components/binary_sensor/test_threshold.py +++ b/tests/components/binary_sensor/test_threshold.py @@ -96,3 +96,53 @@ class TestThresholdSensor(unittest.TestCase): state = self.hass.states.get('binary_sensor.test_threshold') 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'