1 """Support for Tasmota device discovery."""
3 from __future__
import annotations
5 from collections.abc
import Awaitable, Callable
7 from typing
import TypedDict, cast
9 from hatasmota
import const
as tasmota_const
10 from hatasmota.discovery
import (
12 get_device_config
as tasmota_get_device_config,
13 get_entities_for_platform
as tasmota_get_entities_for_platform,
14 get_entity
as tasmota_get_entity,
15 get_trigger
as tasmota_get_trigger,
16 get_triggers
as tasmota_get_triggers,
19 from hatasmota.entity
import TasmotaEntityConfig
20 from hatasmota.models
import DiscoveryHashType, TasmotaDeviceConfig
21 from hatasmota.mqtt
import TasmotaMQTTClient
22 from hatasmota.sensor
import TasmotaBaseSensorConfig
23 from hatasmota.utils
import get_topic_command, get_topic_stat
29 device_registry
as dr,
30 entity_registry
as er,
36 from .const
import DOMAIN, PLATFORMS
38 _LOGGER = logging.getLogger(__name__)
40 ALREADY_DISCOVERED =
"tasmota_discovered_components"
41 DISCOVERY_DATA =
"tasmota_discovery_data"
42 TASMOTA_DISCOVERY_ENTITY_NEW =
"tasmota_discovery_entity_new_{}"
43 TASMOTA_DISCOVERY_ENTITY_UPDATED =
"tasmota_discovery_entity_updated_{}_{}_{}_{}"
44 TASMOTA_DISCOVERY_INSTANCE =
"tasmota_discovery_instance"
46 MQTT_TOPIC_URL =
"https://tasmota.github.io/docs/Home-Assistant/#tasmota-integration"
48 type SetupDeviceCallback = Callable[[TasmotaDeviceConfig, str], Awaitable[
None]]
52 hass: HomeAssistant, discovery_hash: DiscoveryHashType
54 """Clear entry in ALREADY_DISCOVERED list."""
55 if ALREADY_DISCOVERED
not in hass.data:
58 del hass.data[ALREADY_DISCOVERED][discovery_hash]
62 """Set entry in ALREADY_DISCOVERED list."""
63 hass.data[ALREADY_DISCOVERED][discovery_hash] = {}
70 own_device_config: TasmotaDeviceConfig,
72 """Log and create repairs issue if several devices share the same topic."""
75 for other_mac, other_config
in hass.data[DISCOVERY_DATA].items():
76 if own_mac
and other_mac == own_mac:
78 if command_topic == get_topic_command(other_config):
79 offenders.append((other_mac, tasmota_get_device_config(other_config)))
80 issue_id = f
"topic_duplicated_{command_topic}"
83 offenders.append((own_mac, own_device_config))
85 f
"'{cfg[tasmota_const.CONF_NAME]}' ({cfg[tasmota_const.CONF_IP]})"
86 for _, cfg
in offenders
90 "Multiple Tasmota devices are sharing the same topic '%s'. Offending"
94 ", ".join(offender_strings),
96 ir.async_create_issue(
101 "key":
"topic_duplicated",
102 "mac":
" ".join([mac
for mac, _
in offenders]),
103 "topic": command_topic,
106 learn_more_url=MQTT_TOPIC_URL,
107 severity=ir.IssueSeverity.ERROR,
108 translation_key=
"topic_duplicated",
109 translation_placeholders={
110 "topic": command_topic,
111 "offenders":
"\n\n* " +
"\n\n* ".join(offender_strings),
119 """Typed result dict."""
128 discovery_topic: str,
129 config_entry: ConfigEntry,
130 tasmota_mqtt: TasmotaMQTTClient,
131 setup_device: SetupDeviceCallback,
133 """Start Tasmota device discovery."""
135 def _discover_entity(
136 tasmota_entity_config: TasmotaEntityConfig |
None,
137 discovery_hash: DiscoveryHashType,
140 """Handle adding or updating a discovered entity."""
141 if not tasmota_entity_config:
143 entity_registry = er.async_get(hass)
144 unique_id = unique_id_from_hash(discovery_hash)
145 entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
147 _LOGGER.debug(
"Removing entity: %s %s", platform, discovery_hash)
148 entity_registry.async_remove(entity_id)
151 if discovery_hash
in hass.data[ALREADY_DISCOVERED]:
153 "Entity already added, sending update: %s %s",
159 TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
160 tasmota_entity_config,
163 tasmota_entity = tasmota_get_entity(tasmota_entity_config, tasmota_mqtt)
164 if not tasmota_entity:
165 _LOGGER.error(
"Failed to create entity %s %s", platform, discovery_hash)
169 "Adding new entity: %s %s %s",
172 tasmota_entity.unique_id,
175 hass.data[ALREADY_DISCOVERED][discovery_hash] =
None
179 TASMOTA_DISCOVERY_ENTITY_NEW.format(platform),
184 async
def async_device_discovered(payload: dict, mac: str) ->
None:
185 """Process the received message."""
187 if ALREADY_DISCOVERED
not in hass.data:
191 _LOGGER.debug(
"Received discovery data for tasmota device: %s", mac)
192 tasmota_device_config = tasmota_get_device_config(payload)
195 hass.data[DISCOVERY_DATA][mac] = payload
199 command_topic = get_topic_command(payload)
if payload
else None
200 state_topic = get_topic_stat(payload)
if payload
else None
203 issue_id = f
"topic_no_prefix_{mac}"
204 if payload
and command_topic == state_topic:
206 "Tasmota device '%s' with IP %s doesn't set %%prefix%% in its topic",
207 tasmota_device_config[tasmota_const.CONF_NAME],
208 tasmota_device_config[tasmota_const.CONF_IP],
210 ir.async_create_issue(
214 data={
"key":
"topic_no_prefix"},
216 learn_more_url=MQTT_TOPIC_URL,
217 severity=ir.IssueSeverity.ERROR,
218 translation_key=
"topic_no_prefix",
219 translation_placeholders={
220 "name": tasmota_device_config[tasmota_const.CONF_NAME],
221 "ip": tasmota_device_config[tasmota_const.CONF_IP],
226 ir.async_delete_issue(hass, DOMAIN, issue_id)
229 issue_reg = ir.async_get(hass)
231 issue
for key, issue
in issue_reg.issues.items()
if key[0] == DOMAIN
233 for issue
in tasmota_issues:
234 if issue.data
and issue.data[
"key"] ==
"topic_duplicated":
235 issue_data: DuplicatedTopicIssueData = cast(
236 DuplicatedTopicIssueData, issue.data
238 macs = issue_data[
"mac"].split()
241 if payload
and command_topic == issue_data[
"topic"]:
247 ir.async_delete_issue(hass, DOMAIN, issue.issue_id)
251 assert isinstance(command_topic, str)
262 tasmota_triggers = tasmota_get_triggers(payload)
263 for trigger_config
in tasmota_triggers:
264 discovery_hash: DiscoveryHashType = (
268 trigger_config.trigger_id,
270 if discovery_hash
in hass.data[ALREADY_DISCOVERED]:
272 "Trigger already added, sending update: %s",
277 TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash),
280 elif trigger_config.is_active:
281 _LOGGER.debug(
"Adding new trigger: %s", discovery_hash)
282 hass.data[ALREADY_DISCOVERED][discovery_hash] =
None
284 tasmota_trigger = tasmota_get_trigger(trigger_config, tasmota_mqtt)
288 TASMOTA_DISCOVERY_ENTITY_NEW.format(
"device_automation"),
293 for platform
in PLATFORMS:
294 tasmota_entities = tasmota_get_entities_for_platform(payload, platform)
295 for tasmota_entity_config, discovery_hash
in tasmota_entities:
296 _discover_entity(tasmota_entity_config, discovery_hash, platform)
298 async
def async_sensors_discovered(
299 sensors: list[tuple[TasmotaBaseSensorConfig, DiscoveryHashType]], mac: str
301 """Handle discovery of (additional) sensors."""
302 platform = sensor.DOMAIN
304 device_registry = dr.async_get(hass)
305 entity_registry = er.async_get(hass)
306 device = device_registry.async_get_device(
307 connections={(dr.CONNECTION_NETWORK_MAC, mac)}
311 _LOGGER.warning(
"Got sensors for unknown device mac: %s", mac)
314 orphaned_entities = {
317 entity_registry, device.id, include_disabled_entities=
True
319 if entry.domain == sensor.DOMAIN
and entry.platform == DOMAIN
321 for tasmota_sensor_config, discovery_hash
in sensors:
322 if tasmota_sensor_config:
323 orphaned_entities.discard(tasmota_sensor_config.unique_id)
324 _discover_entity(tasmota_sensor_config, discovery_hash, platform)
325 for unique_id
in orphaned_entities:
326 entity_id = entity_registry.async_get_entity_id(platform, DOMAIN, unique_id)
328 _LOGGER.debug(
"Removing entity: %s %s", platform, entity_id)
329 entity_registry.async_remove(entity_id)
331 hass.data[ALREADY_DISCOVERED] = {}
332 hass.data[DISCOVERY_DATA] = {}
334 tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt)
335 await tasmota_discovery.start_discovery(
336 async_device_discovered, async_sensors_discovered
338 hass.data[TASMOTA_DISCOVERY_INSTANCE] = tasmota_discovery
342 """Stop Tasmota device discovery."""
343 hass.data.pop(ALREADY_DISCOVERED)
344 tasmota_discovery = hass.data.pop(TASMOTA_DISCOVERY_INSTANCE)
345 await tasmota_discovery.stop_discovery()
RoborockDataUpdateCoordinator|RoborockDataUpdateCoordinatorA01|None setup_device(HomeAssistant hass, UserData user_data, HomeDataDevice device, HomeDataProduct product_info, list[HomeDataRoom] home_data_rooms)
None async_start(HomeAssistant hass, str discovery_topic, ConfigEntry config_entry, TasmotaMQTTClient tasmota_mqtt, SetupDeviceCallback setup_device)
bool warn_if_topic_duplicated(HomeAssistant hass, str command_topic, str|None own_mac, TasmotaDeviceConfig own_device_config)
None clear_discovery_hash(HomeAssistant hass, DiscoveryHashType discovery_hash)
None async_stop(HomeAssistant hass)
None set_discovery_hash(HomeAssistant hass, DiscoveryHashType discovery_hash)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
list[RegistryEntry] async_entries_for_device(EntityRegistry registry, str device_id, bool include_disabled_entities=False)