-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add Tasmota integration #39624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Tasmota integration #39624
Changes from all commits
bdd6ff3
77b97b3
5ecd586
59095a2
0e4d0ac
e838d02
baebc0c
2096fba
f23fbc4
648d112
18f3084
529e439
0e4a09b
0cbc847
b7c27c3
8c3e21c
4771a6f
39a0101
41cdb5c
35e6e21
b2a7677
e135f04
8f5d2a8
1b3b73c
84760d4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| """The Tasmota integration.""" | ||
| import logging | ||
|
|
||
| from hatasmota.const import ( | ||
| CONF_MAC, | ||
| CONF_MANUFACTURER, | ||
| CONF_MODEL, | ||
| CONF_NAME, | ||
| CONF_SW_VERSION, | ||
| ) | ||
| from hatasmota.discovery import clear_discovery_topic | ||
| from hatasmota.mqtt import TasmotaMQTTClient | ||
|
|
||
| from homeassistant.components import mqtt | ||
| from homeassistant.components.mqtt.subscription import ( | ||
| async_subscribe_topics, | ||
| async_unsubscribe_topics, | ||
| ) | ||
| from homeassistant.core import callback | ||
| from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC | ||
| from homeassistant.helpers.dispatcher import async_dispatcher_connect | ||
| from homeassistant.helpers.typing import HomeAssistantType | ||
|
|
||
| from . import discovery | ||
| from .const import CONF_DISCOVERY_PREFIX | ||
| from .discovery import TASMOTA_DISCOVERY_DEVICE | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| DEVICE_MACS = "tasmota_devices" | ||
|
|
||
|
|
||
| async def async_setup(hass: HomeAssistantType, config: dict): | ||
| """Set up the Tasmota component.""" | ||
| return True | ||
|
|
||
|
|
||
| async def async_setup_entry(hass, entry): | ||
| """Set up Tasmota from a config entry.""" | ||
| hass.data[DEVICE_MACS] = {} | ||
|
|
||
| def _publish(*args, **kwds): | ||
| mqtt.async_publish(hass, *args, **kwds) | ||
|
|
||
| async def _subscribe_topics(sub_state, topics): | ||
| # Optionally mark message handlers as callback | ||
| for topic in topics.values(): | ||
| if "msg_callback" in topic and "event_loop_safe" in topic: | ||
| topic["msg_callback"] = callback(topic["msg_callback"]) | ||
| return await async_subscribe_topics(hass, sub_state, topics) | ||
|
|
||
| async def _unsubscribe_topics(sub_state): | ||
| return await async_unsubscribe_topics(hass, sub_state) | ||
|
|
||
| tasmota_mqtt = TasmotaMQTTClient(_publish, _subscribe_topics, _unsubscribe_topics) | ||
|
|
||
| discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] | ||
| await discovery.async_start(hass, discovery_prefix, entry, tasmota_mqtt) | ||
|
|
||
| async def async_discover_device(config, mac): | ||
| """Discover and add a Tasmota device.""" | ||
| await async_setup_device(hass, mac, config, entry, tasmota_mqtt) | ||
|
|
||
| async_dispatcher_connect(hass, TASMOTA_DISCOVERY_DEVICE, async_discover_device) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| async def _remove_device(hass, config_entry, mac, tasmota_mqtt): | ||
| """Remove device from device registry.""" | ||
| device_registry = await hass.helpers.device_registry.async_get_registry() | ||
| device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) | ||
|
|
||
| if device is None: | ||
| return | ||
|
|
||
| _LOGGER.debug("Removing tasmota device %s", mac) | ||
| device_registry.async_remove_device(device.id) | ||
| clear_discovery_topic(mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt) | ||
|
|
||
|
|
||
| async def _update_device(hass, config_entry, config): | ||
| """Add or update device registry.""" | ||
| device_registry = await hass.helpers.device_registry.async_get_registry() | ||
| config_entry_id = config_entry.entry_id | ||
| device_info = { | ||
| "connections": {(CONNECTION_NETWORK_MAC, config[CONF_MAC])}, | ||
| "manufacturer": config[CONF_MANUFACTURER], | ||
| "model": config[CONF_MODEL], | ||
| "name": config[CONF_NAME], | ||
| "sw_version": config[CONF_SW_VERSION], | ||
| "config_entry_id": config_entry_id, | ||
| } | ||
| _LOGGER.debug("Adding or updating tasmota device %s", config[CONF_MAC]) | ||
| device = device_registry.async_get_or_create(**device_info) | ||
| hass.data[DEVICE_MACS][device.id] = config[CONF_MAC] | ||
|
|
||
|
|
||
| async def async_setup_device(hass, mac, config, config_entry, tasmota_mqtt): | ||
| """Set up the Tasmota device.""" | ||
| if not config: | ||
| await _remove_device(hass, config_entry, mac, tasmota_mqtt) | ||
| else: | ||
| await _update_device(hass, config_entry, config) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Config flow for Tasmota.""" | ||
| import logging | ||
|
|
||
| import voluptuous as vol | ||
|
|
||
| from homeassistant import config_entries | ||
| from homeassistant.components.mqtt import valid_subscribe_topic | ||
|
|
||
| from .const import ( # pylint:disable=unused-import | ||
| CONF_DISCOVERY_PREFIX, | ||
| DEFAULT_PREFIX, | ||
| DOMAIN, | ||
| ) | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): | ||
| """Handle a config flow.""" | ||
|
|
||
| VERSION = 1 | ||
| CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH | ||
|
|
||
| async def async_step_user(self, user_input=None): | ||
| """Handle a flow initialized by the user.""" | ||
| if self._async_current_entries(): | ||
| return self.async_abort(reason="single_instance_allowed") | ||
|
|
||
| return await self.async_step_config() | ||
|
|
||
| async def async_step_config(self, user_input=None): | ||
| """Confirm the setup.""" | ||
| errors = {} | ||
| data = {CONF_DISCOVERY_PREFIX: DEFAULT_PREFIX} | ||
|
|
||
| if user_input is not None: | ||
| bad_prefix = False | ||
| if self.show_advanced_options: | ||
| prefix = user_input[CONF_DISCOVERY_PREFIX] | ||
| try: | ||
| valid_subscribe_topic(f"{prefix}/#") | ||
| except vol.Invalid: | ||
| errors["base"] = "invalid_discovery_topic" | ||
| bad_prefix = True | ||
| else: | ||
| data = user_input | ||
| if not bad_prefix: | ||
| return self.async_create_entry(title="Tasmota", data=data) | ||
|
|
||
| fields = {} | ||
| if self.show_advanced_options: | ||
| fields[vol.Optional(CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX)] = str | ||
|
|
||
| return self.async_show_form( | ||
| step_id="config", data_schema=vol.Schema(fields), errors=errors | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """Constants used by multiple Tasmota modules.""" | ||
| CONF_DISCOVERY_PREFIX = "discovery_prefix" | ||
|
|
||
| DEFAULT_PREFIX = "tasmota/discovery" | ||
|
|
||
| DOMAIN = "tasmota" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| """Support for MQTT discovery.""" | ||
| import asyncio | ||
| import logging | ||
|
|
||
| from hatasmota.const import CONF_RELAY | ||
| from hatasmota.discovery import ( | ||
| TasmotaDiscovery, | ||
| get_device_config as tasmota_get_device_config, | ||
| get_entities_for_platform as tasmota_get_entities_for_platform, | ||
| get_entity as tasmota_get_entity, | ||
| has_entities_with_platform as tasmota_has_entities_with_platform, | ||
| ) | ||
|
|
||
| from homeassistant.helpers.dispatcher import async_dispatcher_send | ||
| from homeassistant.helpers.typing import HomeAssistantType | ||
|
|
||
| from .const import DOMAIN | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| SUPPORTED_COMPONENTS = { | ||
| "switch": CONF_RELAY, | ||
| } | ||
|
|
||
| ALREADY_DISCOVERED = "tasmota_discovered_components" | ||
| CONFIG_ENTRY_IS_SETUP = "tasmota_config_entry_is_setup" | ||
| DATA_CONFIG_ENTRY_LOCK = "tasmota_config_entry_lock" | ||
| TASMOTA_DISCOVERY_DEVICE = "tasmota_discovery_device" | ||
| TASMOTA_DISCOVERY_ENTITY_NEW = "tasmota_discovery_entity_new_{}" | ||
| TASMOTA_DISCOVERY_ENTITY_UPDATED = "tasmota_discovery_entity_updated_{}_{}_{}" | ||
|
|
||
|
|
||
| def clear_discovery_hash(hass, discovery_hash): | ||
| """Clear entry in ALREADY_DISCOVERED list.""" | ||
| del hass.data[ALREADY_DISCOVERED][discovery_hash] | ||
|
|
||
|
|
||
| def set_discovery_hash(hass, discovery_hash): | ||
| """Set entry in ALREADY_DISCOVERED list.""" | ||
| hass.data[ALREADY_DISCOVERED][discovery_hash] = {} | ||
|
|
||
|
|
||
| async def async_start( | ||
| hass: HomeAssistantType, discovery_topic, config_entry, tasmota_mqtt | ||
| ) -> bool: | ||
| """Start MQTT Discovery.""" | ||
|
|
||
| async def async_device_discovered(payload, mac): | ||
| """Process the received message.""" | ||
|
|
||
| if ALREADY_DISCOVERED not in hass.data: | ||
| hass.data[ALREADY_DISCOVERED] = {} | ||
|
|
||
| _LOGGER.debug("Received discovery data for tasmota device: %s", mac) | ||
| tasmota_device_config = tasmota_get_device_config(payload) | ||
| async_dispatcher_send( | ||
| hass, TASMOTA_DISCOVERY_DEVICE, tasmota_device_config, mac | ||
| ) | ||
|
|
||
| async with hass.data[DATA_CONFIG_ENTRY_LOCK]: | ||
| for component, component_key in SUPPORTED_COMPONENTS.items(): | ||
| if not tasmota_has_entities_with_platform(payload, component_key): | ||
| continue | ||
| config_entries_key = f"{component}.tasmota" | ||
| if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: | ||
| await hass.config_entries.async_forward_entry_setup( | ||
| config_entry, component | ||
| ) | ||
| hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) | ||
|
|
||
| for component, component_key in SUPPORTED_COMPONENTS.items(): | ||
| tasmota_entities = tasmota_get_entities_for_platform(payload, component_key) | ||
| for (idx, tasmota_entity_config) in enumerate(tasmota_entities): | ||
| discovery_hash = (mac, component, idx) | ||
| if not tasmota_entity_config: | ||
| # Entity disabled, clean up entity registry | ||
| entity_registry = ( | ||
| await hass.helpers.entity_registry.async_get_registry() | ||
| ) | ||
| unique_id = "{}_{}_{}".format(*discovery_hash) | ||
| entity_id = entity_registry.async_get_entity_id( | ||
| component, DOMAIN, unique_id | ||
| ) | ||
| if entity_id: | ||
| _LOGGER.debug( | ||
| "Removing entity: %s %s", component, discovery_hash | ||
| ) | ||
| entity_registry.async_remove(entity_id) | ||
| continue | ||
|
|
||
| if discovery_hash in hass.data[ALREADY_DISCOVERED]: | ||
| _LOGGER.debug( | ||
| "Entity already added, sending update: %s %s", | ||
| component, | ||
| discovery_hash, | ||
| ) | ||
| async_dispatcher_send( | ||
| hass, | ||
| TASMOTA_DISCOVERY_ENTITY_UPDATED.format(*discovery_hash), | ||
| tasmota_entity_config, | ||
| ) | ||
| else: | ||
| _LOGGER.debug("Adding new entity: %s %s", component, discovery_hash) | ||
| hass.data[ALREADY_DISCOVERED][discovery_hash] = None | ||
|
|
||
| tasmota_entity = tasmota_get_entity( | ||
| tasmota_entity_config, component_key, tasmota_mqtt | ||
| ) | ||
|
|
||
| async_dispatcher_send( | ||
| hass, | ||
| TASMOTA_DISCOVERY_ENTITY_NEW.format(component), | ||
| tasmota_entity, | ||
| discovery_hash, | ||
| ) | ||
|
|
||
| hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() | ||
| hass.data[CONFIG_ENTRY_IS_SETUP] = set() | ||
|
|
||
| tasmota_discovery = TasmotaDiscovery(discovery_topic, tasmota_mqtt) | ||
| await tasmota_discovery.start_discovery(async_device_discovered) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be time to introduce a new Integration MQTT discovery type so that the MQTT integration can notify other integrations if it finds data at topics that those integrations can manage. So in here we would do
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a great idea and I didn't forget about it!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yesss |
||
| "domain": "tasmota", | ||
| "name": "Tasmota (beta)", | ||
| "config_flow": true, | ||
| "documentation": "https://www.home-assistant.io/integrations/tasmota", | ||
| "requirements": ["hatasmota==0.0.9"], | ||
| "dependencies": ["mqtt"], | ||
| "codeowners": ["@emontnemery"] | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.