mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Handle empty or missing state values for MQTT light entities using 'template' schema (#141177)
* check for empty or missing values when processing state messages for MQTT light entities using 'template' schema * normalise warning logs * add tests (one is still failing and I can't work out why) * fix test * improve test coverage after PR review * improve test coverage after PR review
This commit is contained in:
parent
fee152654d
commit
3951c2ea66
@ -62,6 +62,7 @@ from ..entity import MqttEntity
|
||||
from ..models import (
|
||||
MqttCommandTemplate,
|
||||
MqttValueTemplate,
|
||||
PayloadSentinel,
|
||||
PublishPayloadType,
|
||||
ReceiveMessage,
|
||||
)
|
||||
@ -126,7 +127,9 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
||||
_command_templates: dict[
|
||||
str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType]
|
||||
]
|
||||
_value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]]
|
||||
_value_templates: dict[
|
||||
str, Callable[[ReceivePayloadType, ReceivePayloadType], ReceivePayloadType]
|
||||
]
|
||||
_fixed_color_mode: ColorMode | str | None
|
||||
_topics: dict[str, str | None]
|
||||
|
||||
@ -203,73 +206,133 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity):
|
||||
@callback
|
||||
def _state_received(self, msg: ReceiveMessage) -> None:
|
||||
"""Handle new MQTT messages."""
|
||||
state = self._value_templates[CONF_STATE_TEMPLATE](msg.payload)
|
||||
if state == STATE_ON:
|
||||
state_value = self._value_templates[CONF_STATE_TEMPLATE](
|
||||
msg.payload,
|
||||
PayloadSentinel.NONE,
|
||||
)
|
||||
if not state_value:
|
||||
_LOGGER.debug(
|
||||
"Ignoring message from '%s' with empty state value", msg.topic
|
||||
)
|
||||
elif state_value == STATE_ON:
|
||||
self._attr_is_on = True
|
||||
elif state == STATE_OFF:
|
||||
elif state_value == STATE_OFF:
|
||||
self._attr_is_on = False
|
||||
elif state == PAYLOAD_NONE:
|
||||
elif state_value == PAYLOAD_NONE:
|
||||
self._attr_is_on = None
|
||||
else:
|
||||
_LOGGER.warning("Invalid state value received")
|
||||
_LOGGER.warning(
|
||||
"Invalid state value '%s' received from %s",
|
||||
state_value,
|
||||
msg.topic,
|
||||
)
|
||||
|
||||
if CONF_BRIGHTNESS_TEMPLATE in self._config:
|
||||
try:
|
||||
if brightness := int(
|
||||
self._value_templates[CONF_BRIGHTNESS_TEMPLATE](msg.payload)
|
||||
):
|
||||
self._attr_brightness = brightness
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Ignoring zero brightness value for entity %s",
|
||||
self.entity_id,
|
||||
brightness_value = self._value_templates[CONF_BRIGHTNESS_TEMPLATE](
|
||||
msg.payload,
|
||||
PayloadSentinel.NONE,
|
||||
)
|
||||
if not brightness_value:
|
||||
_LOGGER.debug(
|
||||
"Ignoring message from '%s' with empty brightness value",
|
||||
msg.topic,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
if brightness := int(brightness_value):
|
||||
self._attr_brightness = brightness
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Ignoring zero brightness value for entity %s",
|
||||
self.entity_id,
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Invalid brightness value '%s' received from %s",
|
||||
brightness_value,
|
||||
msg.topic,
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid brightness value received from %s", msg.topic)
|
||||
|
||||
if CONF_COLOR_TEMP_TEMPLATE in self._config:
|
||||
try:
|
||||
color_temp = self._value_templates[CONF_COLOR_TEMP_TEMPLATE](
|
||||
msg.payload
|
||||
color_temp_value = self._value_templates[CONF_COLOR_TEMP_TEMPLATE](
|
||||
msg.payload,
|
||||
PayloadSentinel.NONE,
|
||||
)
|
||||
if not color_temp_value:
|
||||
_LOGGER.debug(
|
||||
"Ignoring message from '%s' with empty color temperature value",
|
||||
msg.topic,
|
||||
)
|
||||
self._attr_color_temp_kelvin = (
|
||||
int(color_temp)
|
||||
if self._color_temp_kelvin
|
||||
else color_util.color_temperature_mired_to_kelvin(int(color_temp))
|
||||
if color_temp != "None"
|
||||
else None
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color temperature value received")
|
||||
else:
|
||||
try:
|
||||
self._attr_color_temp_kelvin = (
|
||||
int(color_temp_value)
|
||||
if self._color_temp_kelvin
|
||||
else color_util.color_temperature_mired_to_kelvin(
|
||||
int(color_temp_value)
|
||||
)
|
||||
if color_temp_value != "None"
|
||||
else None
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Invalid color temperature value '%s' received from %s",
|
||||
color_temp_value,
|
||||
msg.topic,
|
||||
)
|
||||
|
||||
if (
|
||||
CONF_RED_TEMPLATE in self._config
|
||||
and CONF_GREEN_TEMPLATE in self._config
|
||||
and CONF_BLUE_TEMPLATE in self._config
|
||||
):
|
||||
try:
|
||||
red = self._value_templates[CONF_RED_TEMPLATE](msg.payload)
|
||||
green = self._value_templates[CONF_GREEN_TEMPLATE](msg.payload)
|
||||
blue = self._value_templates[CONF_BLUE_TEMPLATE](msg.payload)
|
||||
if red == "None" and green == "None" and blue == "None":
|
||||
self._attr_hs_color = None
|
||||
else:
|
||||
self._attr_hs_color = color_util.color_RGB_to_hs(
|
||||
int(red), int(green), int(blue)
|
||||
)
|
||||
red_value = self._value_templates[CONF_RED_TEMPLATE](
|
||||
msg.payload,
|
||||
PayloadSentinel.NONE,
|
||||
)
|
||||
green_value = self._value_templates[CONF_GREEN_TEMPLATE](
|
||||
msg.payload,
|
||||
PayloadSentinel.NONE,
|
||||
)
|
||||
blue_value = self._value_templates[CONF_BLUE_TEMPLATE](
|
||||
msg.payload,
|
||||
PayloadSentinel.NONE,
|
||||
)
|
||||
if not red_value or not green_value or not blue_value:
|
||||
_LOGGER.debug(
|
||||
"Ignoring message from '%s' with empty color value", msg.topic
|
||||
)
|
||||
elif red_value == "None" and green_value == "None" and blue_value == "None":
|
||||
self._attr_hs_color = None
|
||||
self._update_color_mode()
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color value received")
|
||||
else:
|
||||
try:
|
||||
self._attr_hs_color = color_util.color_RGB_to_hs(
|
||||
int(red_value), int(green_value), int(blue_value)
|
||||
)
|
||||
self._update_color_mode()
|
||||
except ValueError:
|
||||
_LOGGER.warning("Invalid color value received from %s", msg.topic)
|
||||
|
||||
if CONF_EFFECT_TEMPLATE in self._config:
|
||||
effect = str(self._value_templates[CONF_EFFECT_TEMPLATE](msg.payload))
|
||||
if (
|
||||
effect_list := self._config[CONF_EFFECT_LIST]
|
||||
) and effect in effect_list:
|
||||
self._attr_effect = effect
|
||||
effect_value = self._value_templates[CONF_EFFECT_TEMPLATE](
|
||||
msg.payload,
|
||||
PayloadSentinel.NONE,
|
||||
)
|
||||
if not effect_value:
|
||||
_LOGGER.debug(
|
||||
"Ignoring message from '%s' with empty effect value", msg.topic
|
||||
)
|
||||
elif (effect_list := self._config[CONF_EFFECT_LIST]) and str(
|
||||
effect_value
|
||||
) in effect_list:
|
||||
self._attr_effect = str(effect_value)
|
||||
else:
|
||||
_LOGGER.warning("Unsupported effect value received")
|
||||
_LOGGER.warning(
|
||||
"Unsupported effect value '%s' received from %s",
|
||||
effect_value,
|
||||
msg.topic,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
|
@ -1545,3 +1545,109 @@ async def test_rgb_value_template_fails(
|
||||
"TypeError: unsupported operand type(s) for *: 'NoneType' and 'int' rendering template"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_config",
|
||||
[
|
||||
help_custom_config(
|
||||
light.DOMAIN,
|
||||
DEFAULT_CONFIG,
|
||||
(
|
||||
{
|
||||
"effect_list": ["rainbow", "colorloop"],
|
||||
"state_topic": "test-topic",
|
||||
"state_template": "{{ value_json.state }}",
|
||||
"brightness_template": "{{ value_json.brightness }}",
|
||||
"color_temp_template": "{{ value_json.color_temp }}",
|
||||
"red_template": "{{ value_json.color.red }}",
|
||||
"green_template": "{{ value_json.color.green }}",
|
||||
"blue_template": "{{ value_json.color.blue }}",
|
||||
"effect_template": "{{ value_json.effect }}",
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
async def test_state_templates_ignore_missing_values(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
) -> None:
|
||||
"""Test that rendering of MQTT value template ignores missing values."""
|
||||
await mqtt_mock_entry()
|
||||
|
||||
# turn on the light
|
||||
async_fire_mqtt_message(hass, "test-topic", '{"state": "on"}')
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("rgb_color") is None
|
||||
assert state.attributes.get("brightness") is None
|
||||
assert state.attributes.get("color_temp_kelvin") is None
|
||||
assert state.attributes.get("effect") is None
|
||||
|
||||
# update brightness and color temperature (with no state)
|
||||
async_fire_mqtt_message(
|
||||
hass, "test-topic", '{"brightness": 255, "color_temp": 145}'
|
||||
)
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("rgb_color") == (
|
||||
246,
|
||||
244,
|
||||
255,
|
||||
) # temp converted to color
|
||||
assert state.attributes.get("brightness") == 255
|
||||
assert state.attributes.get("color_temp_kelvin") == 6896
|
||||
assert state.attributes.get("effect") is None
|
||||
assert state.attributes.get("xy_color") == (0.317, 0.317) # temp converted to color
|
||||
assert state.attributes.get("hs_color") == (
|
||||
251.249,
|
||||
4.253,
|
||||
) # temp converted to color
|
||||
|
||||
# update color
|
||||
async_fire_mqtt_message(
|
||||
hass, "test-topic", '{"color": {"red": 255, "green": 128, "blue": 64}}'
|
||||
)
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||
assert state.attributes.get("brightness") == 255
|
||||
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||
assert state.attributes.get("effect") is None
|
||||
|
||||
# update brightness
|
||||
async_fire_mqtt_message(hass, "test-topic", '{"brightness": 128}')
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||
assert state.attributes.get("brightness") == 128
|
||||
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||
assert state.attributes.get("effect") is None
|
||||
|
||||
# update effect
|
||||
async_fire_mqtt_message(hass, "test-topic", '{"effect": "rainbow"}')
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||
assert state.attributes.get("brightness") == 128
|
||||
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||
assert state.attributes.get("effect") == "rainbow"
|
||||
|
||||
# invalid effect
|
||||
async_fire_mqtt_message(hass, "test-topic", '{"effect": "invalid"}')
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get("rgb_color") == (255, 128, 64)
|
||||
assert state.attributes.get("brightness") == 128
|
||||
assert state.attributes.get("color_temp_kelvin") is None # rgb color has priority
|
||||
assert state.attributes.get("effect") == "rainbow"
|
||||
|
||||
# turn off the light
|
||||
async_fire_mqtt_message(hass, "test-topic", '{"state": "off"}')
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state == STATE_OFF
|
||||
assert state.attributes.get("rgb_color") is None
|
||||
assert state.attributes.get("brightness") is None
|
||||
assert state.attributes.get("color_temp_kelvin") is None
|
||||
assert state.attributes.get("effect") is None
|
||||
|
Loading…
x
Reference in New Issue
Block a user