Add support for last_reset to MQTT sensor (#51036)
* Add support for last_reset to MQTT sensor * Update abbreviations * Improve test coverage
This commit is contained in:
parent
4f9b7254d2
commit
789a14fc44
3 changed files with 155 additions and 9 deletions
|
@ -77,6 +77,8 @@ ABBREVIATIONS = {
|
||||||
"json_attr": "json_attributes",
|
"json_attr": "json_attributes",
|
||||||
"json_attr_t": "json_attributes_topic",
|
"json_attr_t": "json_attributes_topic",
|
||||||
"json_attr_tpl": "json_attributes_template",
|
"json_attr_tpl": "json_attributes_template",
|
||||||
|
"lrst_t": "last_reset_topic",
|
||||||
|
"lrst_val_tpl": "last_reset_value_template",
|
||||||
"max": "max",
|
"max": "max",
|
||||||
"min": "min",
|
"min": "min",
|
||||||
"max_mirs": "max_mireds",
|
"max_mirs": "max_mireds",
|
||||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import functools
|
import functools
|
||||||
|
import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -36,7 +37,11 @@ from .mixins import (
|
||||||
async_setup_entry_helper,
|
async_setup_entry_helper,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_EXPIRE_AFTER = "expire_after"
|
CONF_EXPIRE_AFTER = "expire_after"
|
||||||
|
CONF_LAST_RESET_TOPIC = "last_reset_topic"
|
||||||
|
CONF_LAST_RESET_VALUE_TEMPLATE = "last_reset_value_template"
|
||||||
CONF_STATE_CLASS = "state_class"
|
CONF_STATE_CLASS = "state_class"
|
||||||
|
|
||||||
DEFAULT_NAME = "MQTT Sensor"
|
DEFAULT_NAME = "MQTT Sensor"
|
||||||
|
@ -46,6 +51,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||||
|
vol.Optional(CONF_LAST_RESET_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
|
vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
||||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||||
|
@ -79,6 +86,8 @@ async def _async_setup_entity(
|
||||||
class MqttSensor(MqttEntity, SensorEntity):
|
class MqttSensor(MqttEntity, SensorEntity):
|
||||||
"""Representation of a sensor that can be updated using MQTT."""
|
"""Representation of a sensor that can be updated using MQTT."""
|
||||||
|
|
||||||
|
_attr_last_reset = None
|
||||||
|
|
||||||
def __init__(self, hass, config, config_entry, discovery_data):
|
def __init__(self, hass, config, config_entry, discovery_data):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._state = None
|
self._state = None
|
||||||
|
@ -102,9 +111,13 @@ class MqttSensor(MqttEntity, SensorEntity):
|
||||||
template = self._config.get(CONF_VALUE_TEMPLATE)
|
template = self._config.get(CONF_VALUE_TEMPLATE)
|
||||||
if template is not None:
|
if template is not None:
|
||||||
template.hass = self.hass
|
template.hass = self.hass
|
||||||
|
last_reset_template = self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE)
|
||||||
|
if last_reset_template is not None:
|
||||||
|
last_reset_template.hass = self.hass
|
||||||
|
|
||||||
async def _subscribe_topics(self):
|
async def _subscribe_topics(self):
|
||||||
"""(Re)Subscribe to topics."""
|
"""(Re)Subscribe to topics."""
|
||||||
|
topics = {}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@log_messages(self.hass, self.entity_id)
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
@ -140,16 +153,49 @@ class MqttSensor(MqttEntity, SensorEntity):
|
||||||
self._state = payload
|
self._state = payload
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
topics["state_topic"] = {
|
||||||
|
"topic": self._config[CONF_STATE_TOPIC],
|
||||||
|
"msg_callback": message_received,
|
||||||
|
"qos": self._config[CONF_QOS],
|
||||||
|
}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@log_messages(self.hass, self.entity_id)
|
||||||
|
def last_reset_message_received(msg):
|
||||||
|
"""Handle new last_reset messages."""
|
||||||
|
payload = msg.payload
|
||||||
|
|
||||||
|
template = self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE)
|
||||||
|
if template is not None:
|
||||||
|
variables = {"entity_id": self.entity_id}
|
||||||
|
payload = template.async_render_with_possible_json_value(
|
||||||
|
payload,
|
||||||
|
self._state,
|
||||||
|
variables=variables,
|
||||||
|
)
|
||||||
|
if not payload:
|
||||||
|
_LOGGER.debug("Ignoring empty last_reset message from '%s'", msg.topic)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
last_reset = dt_util.parse_datetime(payload)
|
||||||
|
if last_reset is None:
|
||||||
|
raise ValueError
|
||||||
|
self._attr_last_reset = last_reset
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Invalid last_reset message '%s' from '%s'", msg.payload, msg.topic
|
||||||
|
)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
if CONF_LAST_RESET_TOPIC in self._config:
|
||||||
|
topics["state_topic"] = {
|
||||||
|
"topic": self._config[CONF_LAST_RESET_TOPIC],
|
||||||
|
"msg_callback": last_reset_message_received,
|
||||||
|
"qos": self._config[CONF_QOS],
|
||||||
|
}
|
||||||
|
|
||||||
self._sub_state = await subscription.async_subscribe_topics(
|
self._sub_state = await subscription.async_subscribe_topics(
|
||||||
self.hass,
|
self.hass, self._sub_state, topics
|
||||||
self._sub_state,
|
|
||||||
{
|
|
||||||
"state_topic": {
|
|
||||||
"topic": self._config[CONF_STATE_TOPIC],
|
|
||||||
"msg_callback": message_received,
|
|
||||||
"qos": self._config[CONF_QOS],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -206,6 +206,104 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock):
|
||||||
assert state.state == "100"
|
assert state.state == "100"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock):
|
||||||
|
"""Test the setting of the last_reset property via MQTT."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
sensor.DOMAIN,
|
||||||
|
{
|
||||||
|
sensor.DOMAIN: {
|
||||||
|
"platform": "mqtt",
|
||||||
|
"name": "test",
|
||||||
|
"state_topic": "test-topic",
|
||||||
|
"unit_of_measurement": "fav unit",
|
||||||
|
"last_reset_topic": "last-reset-topic",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "last-reset-topic", "2020-01-02 08:11:00")
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert state.attributes.get("last_reset") == "2020-01-02T08:11:00"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("datestring", ["2020-21-02 08:11:00", "Hello there!"])
|
||||||
|
async def test_setting_sensor_bad_last_reset_via_mqtt_message(
|
||||||
|
hass, caplog, datestring, mqtt_mock
|
||||||
|
):
|
||||||
|
"""Test the setting of the last_reset property via MQTT."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
sensor.DOMAIN,
|
||||||
|
{
|
||||||
|
sensor.DOMAIN: {
|
||||||
|
"platform": "mqtt",
|
||||||
|
"name": "test",
|
||||||
|
"state_topic": "test-topic",
|
||||||
|
"unit_of_measurement": "fav unit",
|
||||||
|
"last_reset_topic": "last-reset-topic",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "last-reset-topic", datestring)
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert state.attributes.get("last_reset") is None
|
||||||
|
assert "Invalid last_reset message" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setting_sensor_empty_last_reset_via_mqtt_message(
|
||||||
|
hass, caplog, mqtt_mock
|
||||||
|
):
|
||||||
|
"""Test the setting of the last_reset property via MQTT."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
sensor.DOMAIN,
|
||||||
|
{
|
||||||
|
sensor.DOMAIN: {
|
||||||
|
"platform": "mqtt",
|
||||||
|
"name": "test",
|
||||||
|
"state_topic": "test-topic",
|
||||||
|
"unit_of_measurement": "fav unit",
|
||||||
|
"last_reset_topic": "last-reset-topic",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
async_fire_mqtt_message(hass, "last-reset-topic", "")
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert state.attributes.get("last_reset") is None
|
||||||
|
assert "Ignoring empty last_reset message" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock):
|
||||||
|
"""Test the setting of the value via MQTT with JSON payload."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
sensor.DOMAIN,
|
||||||
|
{
|
||||||
|
sensor.DOMAIN: {
|
||||||
|
"platform": "mqtt",
|
||||||
|
"name": "test",
|
||||||
|
"state_topic": "test-topic",
|
||||||
|
"unit_of_measurement": "fav unit",
|
||||||
|
"last_reset_topic": "last-reset-topic",
|
||||||
|
"last_reset_value_template": "{{ value_json.last_reset }}",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
async_fire_mqtt_message(
|
||||||
|
hass, "last-reset-topic", '{ "last_reset": "2020-01-02 08:11:00" }'
|
||||||
|
)
|
||||||
|
state = hass.states.get("sensor.test")
|
||||||
|
assert state.attributes.get("last_reset") == "2020-01-02T08:11:00"
|
||||||
|
|
||||||
|
|
||||||
async def test_force_update_disabled(hass, mqtt_mock):
|
async def test_force_update_disabled(hass, mqtt_mock):
|
||||||
"""Test force update option."""
|
"""Test force update option."""
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue