diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 0ec196270a6..cd69967e6a7 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -181,7 +181,7 @@ class MqttBinarySensor( expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) self._expiration_trigger = async_track_point_in_utc_time( - self.hass, self.value_is_expired, expiration_at + self.hass, self._value_is_expired, expiration_at ) value_template = self._config.get(CONF_VALUE_TEMPLATE) @@ -250,7 +250,7 @@ class MqttBinarySensor( await MqttDiscoveryUpdate.async_will_remove_from_hass(self) @callback - def value_is_expired(self, *_): + def _value_is_expired(self, *_): """Triggered when value is expired.""" self._expiration_trigger = None diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 2704c5ae3a1..3ad58468cab 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -109,6 +109,11 @@ class MqttSensor( self._sub_state = None self._expiration_trigger = None + expire_after = config.get(CONF_EXPIRE_AFTER) + if expire_after is not None and expire_after > 0: + self._expired = True + else: + self._expired = None device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -145,6 +150,9 @@ class MqttSensor( # auto-expire enabled? expire_after = self._config.get(CONF_EXPIRE_AFTER) if expire_after is not None and expire_after > 0: + # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + self._expired = False + # Reset old trigger if self._expiration_trigger: self._expiration_trigger() @@ -154,7 +162,7 @@ class MqttSensor( expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) self._expiration_trigger = async_track_point_in_utc_time( - self.hass, self.value_is_expired, expiration_at + self.hass, self._value_is_expired, expiration_at ) if template is not None: @@ -186,10 +194,10 @@ class MqttSensor( await MqttDiscoveryUpdate.async_will_remove_from_hass(self) @callback - def value_is_expired(self, *_): + def _value_is_expired(self, *_): """Triggered when value is expired.""" self._expiration_trigger = None - self._state = None + self._expired = True self.async_write_ha_state() @property @@ -231,3 +239,12 @@ class MqttSensor( def device_class(self) -> Optional[str]: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) + + @property + def available(self) -> bool: + """Return true if the device is available and value has not expired.""" + expire_after = self._config.get(CONF_EXPIRE_AFTER) + # pylint: disable=no-member + return MqttAvailability.available.fget(self) and ( + expire_after is None or not self._expired + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 19cac2e53c8..5ec5fccbe28 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start import homeassistant.components.sensor as sensor -from homeassistant.const import EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -73,6 +73,38 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): assert state.attributes.get("unit_of_measurement") == "fav unit" +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock, legacy_patchable_time, caplog +): + """Test the expiration of the value.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + "availability_topic": "availability-topic", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic", "online") + + # State should be unavailable since expire_after is defined and > 0 + state = hass.states.get("sensor.test") + assert state.state == STATE_UNAVAILABLE + + await expires_helper(hass, mqtt_mock, caplog) + + async def test_setting_sensor_value_expires( hass, mqtt_mock, legacy_patchable_time, caplog ): @@ -93,9 +125,15 @@ async def test_setting_sensor_value_expires( ) await hass.async_block_till_done() + # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("sensor.test") - assert state.state == "unknown" + assert state.state == STATE_UNAVAILABLE + await expires_helper(hass, mqtt_mock, caplog) + + +async def expires_helper(hass, mqtt_mock, caplog): + """Run the basic expiry code.""" realnow = dt_util.utcnow() now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): @@ -142,7 +180,7 @@ async def test_setting_sensor_value_expires( # Value is expired now state = hass.states.get("sensor.test") - assert state.state == "unknown" + assert state.state == STATE_UNAVAILABLE async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock):