Improve lists for MQTT integration (#113184)
* Improve lists for MQTT integration * Extra diagnostics tests * Revert changes where the original version was probably faster * Revert change to gather and await in series
This commit is contained in:
parent
b1346f3ccd
commit
488dae43d4
11 changed files with 144 additions and 116 deletions
|
@ -812,9 +812,11 @@ class MQTT:
|
||||||
subscriptions: list[Subscription] = []
|
subscriptions: list[Subscription] = []
|
||||||
if topic in self._simple_subscriptions:
|
if topic in self._simple_subscriptions:
|
||||||
subscriptions.extend(self._simple_subscriptions[topic])
|
subscriptions.extend(self._simple_subscriptions[topic])
|
||||||
for subscription in self._wildcard_subscriptions:
|
subscriptions.extend(
|
||||||
if subscription.matcher(topic):
|
subscription
|
||||||
subscriptions.append(subscription)
|
for subscription in self._wildcard_subscriptions
|
||||||
|
if subscription.matcher(topic)
|
||||||
|
)
|
||||||
return subscriptions
|
return subscriptions
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -661,15 +661,12 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
self._optimistic or CONF_PRESET_MODE_STATE_TOPIC not in config
|
self._optimistic or CONF_PRESET_MODE_STATE_TOPIC not in config
|
||||||
)
|
)
|
||||||
|
|
||||||
value_templates: dict[str, Template | None] = {}
|
value_templates: dict[str, Template | None] = {
|
||||||
for key in VALUE_TEMPLATE_KEYS:
|
key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS
|
||||||
value_templates[key] = None
|
}
|
||||||
if CONF_VALUE_TEMPLATE in config:
|
value_templates.update(
|
||||||
value_templates = {
|
{key: config[key] for key in VALUE_TEMPLATE_KEYS & config.keys()}
|
||||||
key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS
|
)
|
||||||
}
|
|
||||||
for key in VALUE_TEMPLATE_KEYS & config.keys():
|
|
||||||
value_templates[key] = config[key]
|
|
||||||
self._value_templates = {
|
self._value_templates = {
|
||||||
key: MqttValueTemplate(
|
key: MqttValueTemplate(
|
||||||
template,
|
template,
|
||||||
|
@ -678,11 +675,10 @@ class MqttClimate(MqttTemperatureControlEntity, ClimateEntity):
|
||||||
for key, template in value_templates.items()
|
for key, template in value_templates.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
self._command_templates = {}
|
self._command_templates = {
|
||||||
for key in COMMAND_TEMPLATE_KEYS:
|
key: MqttCommandTemplate(config.get(key), entity=self).async_render
|
||||||
self._command_templates[key] = MqttCommandTemplate(
|
for key in COMMAND_TEMPLATE_KEYS
|
||||||
config.get(key), entity=self
|
}
|
||||||
).async_render
|
|
||||||
|
|
||||||
support = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
support = ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||||
if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or (
|
if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or (
|
||||||
|
|
|
@ -239,11 +239,14 @@ def info_for_config_entry(hass: HomeAssistant) -> dict[str, list[Any]]:
|
||||||
mqtt_data = get_mqtt_data(hass)
|
mqtt_data = get_mqtt_data(hass)
|
||||||
mqtt_info: dict[str, list[Any]] = {"entities": [], "triggers": []}
|
mqtt_info: dict[str, list[Any]] = {"entities": [], "triggers": []}
|
||||||
|
|
||||||
for entity_id in mqtt_data.debug_info_entities:
|
mqtt_info["entities"].extend(
|
||||||
mqtt_info["entities"].append(_info_for_entity(hass, entity_id))
|
_info_for_entity(hass, entity_id) for entity_id in mqtt_data.debug_info_entities
|
||||||
|
)
|
||||||
|
|
||||||
for trigger_key in mqtt_data.debug_info_triggers:
|
mqtt_info["triggers"].extend(
|
||||||
mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key))
|
_info_for_trigger(hass, trigger_key)
|
||||||
|
for trigger_key in mqtt_data.debug_info_triggers
|
||||||
|
)
|
||||||
|
|
||||||
return mqtt_info
|
return mqtt_info
|
||||||
|
|
||||||
|
@ -259,16 +262,16 @@ def info_for_device(hass: HomeAssistant, device_id: str) -> dict[str, list[Any]]
|
||||||
entries = er.async_entries_for_device(
|
entries = er.async_entries_for_device(
|
||||||
entity_registry, device_id, include_disabled_entities=True
|
entity_registry, device_id, include_disabled_entities=True
|
||||||
)
|
)
|
||||||
for entry in entries:
|
mqtt_info["entities"].extend(
|
||||||
if entry.entity_id not in mqtt_data.debug_info_entities:
|
_info_for_entity(hass, entry.entity_id)
|
||||||
continue
|
for entry in entries
|
||||||
|
if entry.entity_id in mqtt_data.debug_info_entities
|
||||||
|
)
|
||||||
|
|
||||||
mqtt_info["entities"].append(_info_for_entity(hass, entry.entity_id))
|
mqtt_info["triggers"].extend(
|
||||||
|
_info_for_trigger(hass, trigger_key)
|
||||||
for trigger_key, trigger in mqtt_data.debug_info_triggers.items():
|
for trigger_key, trigger in mqtt_data.debug_info_triggers.items()
|
||||||
if trigger["device_id"] != device_id:
|
if trigger["device_id"] == device_id
|
||||||
continue
|
)
|
||||||
|
|
||||||
mqtt_info["triggers"].append(_info_for_trigger(hass, trigger_key))
|
|
||||||
|
|
||||||
return mqtt_info
|
return mqtt_info
|
||||||
|
|
|
@ -353,24 +353,20 @@ async def async_get_triggers(
|
||||||
) -> list[dict[str, str]]:
|
) -> list[dict[str, str]]:
|
||||||
"""List device triggers for MQTT devices."""
|
"""List device triggers for MQTT devices."""
|
||||||
mqtt_data = get_mqtt_data(hass)
|
mqtt_data = get_mqtt_data(hass)
|
||||||
triggers: list[dict[str, str]] = []
|
|
||||||
|
|
||||||
if not mqtt_data.device_triggers:
|
if not mqtt_data.device_triggers:
|
||||||
return triggers
|
return []
|
||||||
|
|
||||||
for trig in mqtt_data.device_triggers.values():
|
return [
|
||||||
if trig.device_id != device_id or trig.topic is None:
|
{
|
||||||
continue
|
|
||||||
|
|
||||||
trigger = {
|
|
||||||
**MQTT_TRIGGER_BASE,
|
**MQTT_TRIGGER_BASE,
|
||||||
"device_id": device_id,
|
"device_id": device_id,
|
||||||
"type": trig.type,
|
"type": trig.type,
|
||||||
"subtype": trig.subtype,
|
"subtype": trig.subtype,
|
||||||
}
|
}
|
||||||
triggers.append(trigger)
|
for trig in mqtt_data.device_triggers.values()
|
||||||
|
if trig.device_id == device_id and trig.topic is not None
|
||||||
return triggers
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_attach_trigger(
|
async def async_attach_trigger(
|
||||||
|
|
|
@ -95,36 +95,40 @@ def _async_device_as_dict(hass: HomeAssistant, device: DeviceEntry) -> dict[str,
|
||||||
include_disabled_entities=True,
|
include_disabled_entities=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
for entity_entry in entities:
|
def _state_dict(entity_entry: er.RegistryEntry) -> dict[str, Any] | None:
|
||||||
state = hass.states.get(entity_entry.entity_id)
|
state = hass.states.get(entity_entry.entity_id)
|
||||||
state_dict = None
|
if not state:
|
||||||
if state:
|
return None
|
||||||
state_dict = dict(state.as_dict())
|
|
||||||
|
|
||||||
# The context doesn't provide useful information in this case.
|
state_dict = dict(state.as_dict())
|
||||||
state_dict.pop("context", None)
|
|
||||||
|
|
||||||
entity_domain = split_entity_id(state.entity_id)[0]
|
# The context doesn't provide useful information in this case.
|
||||||
|
state_dict.pop("context", None)
|
||||||
|
|
||||||
# Retract some sensitive state attributes
|
entity_domain = split_entity_id(state.entity_id)[0]
|
||||||
if entity_domain == device_tracker.DOMAIN:
|
|
||||||
state_dict["attributes"] = async_redact_data(
|
|
||||||
state_dict["attributes"], REDACT_STATE_DEVICE_TRACKER
|
|
||||||
)
|
|
||||||
|
|
||||||
data["entities"].append(
|
# Retract some sensitive state attributes
|
||||||
{
|
if entity_domain == device_tracker.DOMAIN:
|
||||||
"device_class": entity_entry.device_class,
|
state_dict["attributes"] = async_redact_data(
|
||||||
"disabled_by": entity_entry.disabled_by,
|
state_dict["attributes"], REDACT_STATE_DEVICE_TRACKER
|
||||||
"disabled": entity_entry.disabled,
|
)
|
||||||
"entity_category": entity_entry.entity_category,
|
return state_dict
|
||||||
"entity_id": entity_entry.entity_id,
|
|
||||||
"icon": entity_entry.icon,
|
data["entities"].extend(
|
||||||
"original_device_class": entity_entry.original_device_class,
|
{
|
||||||
"original_icon": entity_entry.original_icon,
|
"device_class": entity_entry.device_class,
|
||||||
"state": state_dict,
|
"disabled_by": entity_entry.disabled_by,
|
||||||
"unit_of_measurement": entity_entry.unit_of_measurement,
|
"disabled": entity_entry.disabled,
|
||||||
}
|
"entity_category": entity_entry.entity_category,
|
||||||
)
|
"entity_id": entity_entry.entity_id,
|
||||||
|
"icon": entity_entry.icon,
|
||||||
|
"original_device_class": entity_entry.original_device_class,
|
||||||
|
"original_icon": entity_entry.original_icon,
|
||||||
|
"state": state_dict,
|
||||||
|
"unit_of_measurement": entity_entry.unit_of_measurement,
|
||||||
|
}
|
||||||
|
for entity_entry in entities
|
||||||
|
if (state_dict := _state_dict(entity_entry)) is not None
|
||||||
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -403,14 +403,17 @@ async def async_start( # noqa: C901
|
||||||
):
|
):
|
||||||
mqtt_data.integration_unsubscribe.pop(key)()
|
mqtt_data.integration_unsubscribe.pop(key)()
|
||||||
|
|
||||||
for topic in topics:
|
mqtt_data.integration_unsubscribe.update(
|
||||||
key = f"{integration}_{topic}"
|
{
|
||||||
mqtt_data.integration_unsubscribe[key] = await mqtt.async_subscribe(
|
f"{integration}_{topic}": await mqtt.async_subscribe(
|
||||||
hass,
|
hass,
|
||||||
topic,
|
topic,
|
||||||
functools.partial(async_integration_message_received, integration),
|
functools.partial(async_integration_message_received, integration),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
for topic in topics
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_stop(hass: HomeAssistant) -> None:
|
async def async_stop(hass: HomeAssistant) -> None:
|
||||||
|
|
|
@ -319,13 +319,11 @@ class MqttFan(MqttEntity, FanEntity):
|
||||||
ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE),
|
ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE),
|
||||||
ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE),
|
ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE),
|
||||||
}
|
}
|
||||||
self._command_templates = {}
|
self._command_templates = {
|
||||||
for key, tpl in command_templates.items():
|
key: MqttCommandTemplate(tpl, entity=self).async_render
|
||||||
self._command_templates[key] = MqttCommandTemplate(
|
for key, tpl in command_templates.items()
|
||||||
tpl, entity=self
|
}
|
||||||
).async_render
|
|
||||||
|
|
||||||
self._value_templates = {}
|
|
||||||
value_templates: dict[str, Template | None] = {
|
value_templates: dict[str, Template | None] = {
|
||||||
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
|
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
|
||||||
ATTR_DIRECTION: config.get(CONF_DIRECTION_VALUE_TEMPLATE),
|
ATTR_DIRECTION: config.get(CONF_DIRECTION_VALUE_TEMPLATE),
|
||||||
|
@ -333,11 +331,12 @@ class MqttFan(MqttEntity, FanEntity):
|
||||||
ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE),
|
ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE),
|
||||||
ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE),
|
ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE),
|
||||||
}
|
}
|
||||||
for key, tpl in value_templates.items():
|
self._value_templates = {
|
||||||
self._value_templates[key] = MqttValueTemplate(
|
key: MqttValueTemplate(
|
||||||
tpl,
|
tpl, entity=self
|
||||||
entity=self,
|
|
||||||
).async_render_with_possible_json_value
|
).async_render_with_possible_json_value
|
||||||
|
for key, tpl in value_templates.items()
|
||||||
|
}
|
||||||
|
|
||||||
def _prepare_subscribe_topics(self) -> None:
|
def _prepare_subscribe_topics(self) -> None:
|
||||||
"""(Re)Subscribe to topics."""
|
"""(Re)Subscribe to topics."""
|
||||||
|
|
|
@ -254,18 +254,16 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||||
)
|
)
|
||||||
self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None
|
self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None
|
||||||
|
|
||||||
self._command_templates = {}
|
|
||||||
command_templates: dict[str, Template | None] = {
|
command_templates: dict[str, Template | None] = {
|
||||||
CONF_STATE: config.get(CONF_COMMAND_TEMPLATE),
|
CONF_STATE: config.get(CONF_COMMAND_TEMPLATE),
|
||||||
ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE),
|
ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE),
|
||||||
ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE),
|
ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE),
|
||||||
}
|
}
|
||||||
for key, tpl in command_templates.items():
|
self._command_templates = {
|
||||||
self._command_templates[key] = MqttCommandTemplate(
|
key: MqttCommandTemplate(tpl, entity=self).async_render
|
||||||
tpl, entity=self
|
for key, tpl in command_templates.items()
|
||||||
).async_render
|
}
|
||||||
|
|
||||||
self._value_templates = {}
|
|
||||||
value_templates: dict[str, Template | None] = {
|
value_templates: dict[str, Template | None] = {
|
||||||
ATTR_ACTION: config.get(CONF_ACTION_TEMPLATE),
|
ATTR_ACTION: config.get(CONF_ACTION_TEMPLATE),
|
||||||
ATTR_CURRENT_HUMIDITY: config.get(CONF_CURRENT_HUMIDITY_TEMPLATE),
|
ATTR_CURRENT_HUMIDITY: config.get(CONF_CURRENT_HUMIDITY_TEMPLATE),
|
||||||
|
@ -273,11 +271,13 @@ class MqttHumidifier(MqttEntity, HumidifierEntity):
|
||||||
ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE),
|
ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE),
|
||||||
ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE),
|
ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE),
|
||||||
}
|
}
|
||||||
for key, tpl in value_templates.items():
|
self._value_templates = {
|
||||||
self._value_templates[key] = MqttValueTemplate(
|
key: MqttValueTemplate(
|
||||||
tpl,
|
tpl,
|
||||||
entity=self,
|
entity=self,
|
||||||
).async_render_with_possible_json_value
|
).async_render_with_possible_json_value
|
||||||
|
for key, tpl in value_templates.items()
|
||||||
|
}
|
||||||
|
|
||||||
def add_subscription(
|
def add_subscription(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -364,9 +364,13 @@ class MqttSiren(MqttEntity, SirenEntity):
|
||||||
|
|
||||||
def _update(self, data: SirenTurnOnServiceParameters) -> None:
|
def _update(self, data: SirenTurnOnServiceParameters) -> None:
|
||||||
"""Update the extra siren state attributes."""
|
"""Update the extra siren state attributes."""
|
||||||
for attribute, support in SUPPORTED_ATTRIBUTES.items():
|
self._extra_attributes.update(
|
||||||
if self._attr_supported_features & support and attribute in data:
|
{
|
||||||
data_attr = data[attribute] # type: ignore[literal-required]
|
attribute: data_attr
|
||||||
if self._extra_attributes.get(attribute) == data_attr:
|
for attribute, support in SUPPORTED_ATTRIBUTES.items()
|
||||||
continue
|
if self._attr_supported_features & support
|
||||||
self._extra_attributes[attribute] = data_attr
|
and attribute in data
|
||||||
|
and (data_attr := data[attribute]) # type: ignore[literal-required]
|
||||||
|
!= self._extra_attributes.get(attribute)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
|
@ -226,28 +226,23 @@ class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity):
|
||||||
if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic:
|
if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic:
|
||||||
self._attr_current_operation = STATE_OFF
|
self._attr_current_operation = STATE_OFF
|
||||||
|
|
||||||
value_templates: dict[str, Template | None] = {}
|
value_templates: dict[str, Template | None] = {
|
||||||
for key in VALUE_TEMPLATE_KEYS:
|
key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS
|
||||||
value_templates[key] = None
|
}
|
||||||
if CONF_VALUE_TEMPLATE in config:
|
value_templates.update(
|
||||||
value_templates = {
|
{key: config[key] for key in VALUE_TEMPLATE_KEYS & config.keys()}
|
||||||
key: config.get(CONF_VALUE_TEMPLATE) for key in VALUE_TEMPLATE_KEYS
|
)
|
||||||
}
|
|
||||||
for key in VALUE_TEMPLATE_KEYS & config.keys():
|
|
||||||
value_templates[key] = config[key]
|
|
||||||
self._value_templates = {
|
self._value_templates = {
|
||||||
key: MqttValueTemplate(
|
key: MqttValueTemplate(
|
||||||
template,
|
template, entity=self
|
||||||
entity=self,
|
|
||||||
).async_render_with_possible_json_value
|
).async_render_with_possible_json_value
|
||||||
for key, template in value_templates.items()
|
for key, template in value_templates.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
self._command_templates = {}
|
self._command_templates = {
|
||||||
for key in COMMAND_TEMPLATE_KEYS:
|
key: MqttCommandTemplate(config.get(key), entity=self).async_render
|
||||||
self._command_templates[key] = MqttCommandTemplate(
|
for key in COMMAND_TEMPLATE_KEYS
|
||||||
config.get(key), entity=self
|
}
|
||||||
).async_render
|
|
||||||
|
|
||||||
support = WaterHeaterEntityFeature(0)
|
support = WaterHeaterEntityFeature(0)
|
||||||
if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or (
|
if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or (
|
||||||
|
|
|
@ -7,7 +7,7 @@ import pytest
|
||||||
|
|
||||||
from homeassistant.components import mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
|
|
||||||
from tests.common import async_fire_mqtt_message
|
from tests.common import async_fire_mqtt_message
|
||||||
from tests.components.diagnostics import (
|
from tests.components.diagnostics import (
|
||||||
|
@ -25,6 +25,7 @@ default_config = {
|
||||||
async def test_entry_diagnostics(
|
async def test_entry_diagnostics(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
device_registry: dr.DeviceRegistry,
|
device_registry: dr.DeviceRegistry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
hass_client: ClientSessionGenerator,
|
hass_client: ClientSessionGenerator,
|
||||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -260,3 +261,28 @@ async def test_redact_diagnostics(
|
||||||
"mqtt_config": expected_config,
|
"mqtt_config": expected_config,
|
||||||
"mqtt_debug_info": expected_debug_info,
|
"mqtt_debug_info": expected_debug_info,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Disable the entity and remove the state
|
||||||
|
ent_registry = er.async_get(hass)
|
||||||
|
device_tracker_entry = er.async_entries_for_device(ent_registry, device_entry.id)[0]
|
||||||
|
ent_registry.async_update_entity(
|
||||||
|
device_tracker_entry.entity_id, disabled_by=er.RegistryEntryDisabler.USER
|
||||||
|
)
|
||||||
|
hass.states.async_remove(device_tracker_entry.entity_id)
|
||||||
|
|
||||||
|
# Assert disabled entries are filtered
|
||||||
|
assert await get_diagnostics_for_device(
|
||||||
|
hass, hass_client, config_entry, device_entry
|
||||||
|
) == {
|
||||||
|
"connected": True,
|
||||||
|
"device": {
|
||||||
|
"id": device_entry.id,
|
||||||
|
"name": None,
|
||||||
|
"name_by_user": None,
|
||||||
|
"disabled": False,
|
||||||
|
"disabled_by": None,
|
||||||
|
"entities": [],
|
||||||
|
},
|
||||||
|
"mqtt_config": expected_config,
|
||||||
|
"mqtt_debug_info": {"entities": [], "triggers": []},
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue