mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Allow to migrate and MQTT subentry to YAML or discovery using a repair flow
This commit is contained in:
parent
11b0b2bd3d
commit
02d911b7c7
@ -247,6 +247,59 @@ def async_setup_entity_entry_helper(
|
||||
"""Set up entity creation dynamically through MQTT discovery."""
|
||||
mqtt_data = hass.data[DATA_MQTT]
|
||||
|
||||
@callback
|
||||
def _async_migrate_subentry(
|
||||
config: dict[str, Any], raw_config: dict[str, Any], migration_type: str
|
||||
) -> bool:
|
||||
"""Start a repair flow to allow migration of MQTT device subentries.
|
||||
|
||||
If a YAML config or discovery is detected using the ID
|
||||
of an existing mqtt subentry, and exported configuration is detected,
|
||||
and a repair flow is offered to migrate the subentry.
|
||||
"""
|
||||
if (
|
||||
CONF_DEVICE in config
|
||||
and CONF_IDENTIFIERS in config[CONF_DEVICE]
|
||||
and config[CONF_DEVICE][CONF_IDENTIFIERS]
|
||||
and (subentry_id := config[CONF_DEVICE][CONF_IDENTIFIERS][0])
|
||||
in entry.subentries
|
||||
):
|
||||
name: str = config[CONF_DEVICE].get(CONF_NAME, "-")
|
||||
if migration_type == "subentry_migration_yaml":
|
||||
_LOGGER.info(
|
||||
"Starting migration repair flow for MQTT subentry %s "
|
||||
"for migration to YAML config: %s",
|
||||
subentry_id,
|
||||
raw_config,
|
||||
)
|
||||
elif migration_type == "subentry_migration_discovery":
|
||||
_LOGGER.info(
|
||||
"Starting migration repair flow for MQTT subentry %s "
|
||||
"for migration to configuration via MQTT discovery: %s",
|
||||
subentry_id,
|
||||
raw_config,
|
||||
)
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
subentry_id,
|
||||
issue_domain=DOMAIN,
|
||||
is_fixable=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
learn_more_url=learn_more_url(domain),
|
||||
data={
|
||||
"entry_id": entry.entry_id,
|
||||
"subentry_id": subentry_id,
|
||||
"name": name,
|
||||
"migration_type": migration_type,
|
||||
},
|
||||
translation_placeholders={"name": name},
|
||||
translation_key=migration_type,
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _async_setup_entity_entry_from_discovery(
|
||||
discovery_payload: MQTTDiscoveryPayload,
|
||||
@ -263,9 +316,22 @@ def async_setup_entity_entry_helper(
|
||||
entity_class = schema_class_mapping[config[CONF_SCHEMA]]
|
||||
if TYPE_CHECKING:
|
||||
assert entity_class is not None
|
||||
async_add_entities(
|
||||
[entity_class(hass, config, entry, discovery_payload.discovery_data)]
|
||||
)
|
||||
if _async_migrate_subentry(
|
||||
config, discovery_payload, "subentry_migration_discovery"
|
||||
):
|
||||
_handle_discovery_failure(hass, discovery_payload)
|
||||
_LOGGER.debug(
|
||||
"MQTT discovery skipped, as device exists in subentry, "
|
||||
"and repair flow must be completed first"
|
||||
)
|
||||
else:
|
||||
async_add_entities(
|
||||
[
|
||||
entity_class(
|
||||
hass, config, entry, discovery_payload.discovery_data
|
||||
)
|
||||
]
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
_handle_discovery_failure(hass, discovery_payload)
|
||||
async_handle_schema_error(discovery_payload, err)
|
||||
@ -346,6 +412,11 @@ def async_setup_entity_entry_helper(
|
||||
entity_class = schema_class_mapping[config[CONF_SCHEMA]]
|
||||
if TYPE_CHECKING:
|
||||
assert entity_class is not None
|
||||
if _async_migrate_subentry(
|
||||
config, yaml_config, "subentry_migration_yaml"
|
||||
):
|
||||
continue
|
||||
|
||||
entities.append(entity_class(hass, config, entry, None))
|
||||
except vol.Invalid as exc:
|
||||
error = str(exc)
|
||||
|
105
homeassistant/components/mqtt/repairs.py
Normal file
105
homeassistant/components/mqtt/repairs.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""Repairs for MQTT."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class MQTTDeviceEntryMigration(RepairsFlow):
|
||||
"""Handler to migrate device from subentry to main entry and reload."""
|
||||
|
||||
def __init__(
|
||||
self, entry_id: str, subentry_id: str, name: str, migration_type: str
|
||||
) -> None:
|
||||
"""Initialize the flow."""
|
||||
self.entry_id = entry_id
|
||||
self.subentry_id = subentry_id
|
||||
self.name = name
|
||||
self.migration_type = migration_type
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the first step of a fix flow."""
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> data_entry_flow.FlowResult:
|
||||
"""Handle the confirm step of a fix flow."""
|
||||
if user_input is not None and self.migration_type == "subentry_migration_yaml":
|
||||
# Via YAML the device was already registered and bound to the entry,
|
||||
# so it is safe to remove the subentry from here.
|
||||
device_registry = dr.async_get(self.hass)
|
||||
subentry_device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, self.subentry_id)}
|
||||
)
|
||||
entry = self.hass.config_entries.async_get_entry(self.entry_id)
|
||||
if TYPE_CHECKING:
|
||||
assert entry is not None
|
||||
assert subentry_device is not None
|
||||
self.hass.config_entries.async_remove_subentry(entry, self.subentry_id)
|
||||
return self.async_create_entry(data={})
|
||||
if (
|
||||
user_input is not None
|
||||
and self.migration_type == "subentry_migration_discovery"
|
||||
):
|
||||
# The device offered via discovery was already set up through the subentry,
|
||||
# so we need to update the device before removing the subentry and reload.
|
||||
device_registry = dr.async_get(self.hass)
|
||||
subentry_device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, self.subentry_id)}
|
||||
)
|
||||
entry = self.hass.config_entries.async_get_entry(self.entry_id)
|
||||
if TYPE_CHECKING:
|
||||
assert entry is not None
|
||||
assert subentry_device is not None
|
||||
device_registry.async_update_device(
|
||||
subentry_device.id,
|
||||
remove_config_entry_id=self.entry_id,
|
||||
remove_config_subentry_id=self.subentry_id,
|
||||
add_config_entry_id=self.entry_id,
|
||||
)
|
||||
self.hass.config_entries.async_remove_subentry(entry, self.subentry_id)
|
||||
self.hass.config_entries.async_schedule_reload(self.entry_id)
|
||||
return self.async_create_entry(data={})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
description_placeholders={"name": self.name},
|
||||
)
|
||||
|
||||
|
||||
async def async_create_fix_flow(
|
||||
hass: HomeAssistant,
|
||||
issue_id: str,
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create flow."""
|
||||
if TYPE_CHECKING:
|
||||
assert data is not None
|
||||
entry_id = data["entry_id"]
|
||||
subentry_id = data["subentry_id"]
|
||||
name = data["name"]
|
||||
migration_type = data["migration_type"]
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(entry_id, str)
|
||||
assert isinstance(subentry_id, str)
|
||||
assert isinstance(name, str)
|
||||
assert isinstance(migration_type, str)
|
||||
return MQTTDeviceEntryMigration(
|
||||
entry_id=entry_id,
|
||||
subentry_id=subentry_id,
|
||||
name=name,
|
||||
migration_type=migration_type,
|
||||
)
|
@ -3,6 +3,28 @@
|
||||
"invalid_platform_config": {
|
||||
"title": "Invalid config found for MQTT {domain} item",
|
||||
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
|
||||
},
|
||||
"subentry_migration_discovery": {
|
||||
"title": "MQTT Device \"{name}\" subentry migration to MQTT discovery",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::mqtt::issues::subentry_migration_discovery::title%]",
|
||||
"description": "Exported MQTT Device \"{name}\" identified via MQTT discovery. Confirm that the MQTT device is migrated to the main MQTT configuration, the replaced subentry of the MQTT device is to be removed, and MQTT is to be reloaded. Make sure that the discovery is retained at the MQTT broker, or is resent after the reload is completed, so that the MQTT will be set up correctly."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"subentry_migration_yaml": {
|
||||
"title": "MQTT Device \"{name}\" subentry migration to YAML",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::mqtt::issues::subentry_migration_yaml::title%]",
|
||||
"description": "Exported MQTT Device \"{name}\" identified in YAML configuration. Confirm to migrate the MQTT device to main MQTT config entry, and to remove the replaced MQTT device subentry."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
@ -631,7 +653,7 @@
|
||||
},
|
||||
"export": {
|
||||
"title": "Export MQTT device config",
|
||||
"description": "An export allows you set up the MQTT device configuration via YAML or MQTT discovery. The configuration export can also be helpful for troubleshooting.",
|
||||
"description": "An export allows you to migrate the MQTT device configuration to YAML based configuration or MQTT discovery. The configuration export can also be helpful for troubleshooting.",
|
||||
"menu_options": {
|
||||
"export_discovery": "Export MQTT discovery information",
|
||||
"export_yaml": "Export to YAML configuration"
|
||||
@ -639,7 +661,7 @@
|
||||
},
|
||||
"export_yaml": {
|
||||
"title": "[%key:component::mqtt::config_subentries::device::step::export::title%]",
|
||||
"description": "The export includes the unique ID's used in the subentry configuration. You must change these ID's or remove the MQTT device subentry to avoid conflicts. Note that removing the MQTT device subentry will also remove the device from the device registry.",
|
||||
"description": "You can copy the configuration below and place it your configuration.yaml file. Home Assistant will detect if the MQTT device was tried to set up via YAML instead, and will offer a repair flow to clean up the redundant subentry.",
|
||||
"data": {
|
||||
"yaml": "Copy the YAML configuration below:"
|
||||
},
|
||||
@ -649,7 +671,7 @@
|
||||
},
|
||||
"export_discovery": {
|
||||
"title": "[%key:component::mqtt::config_subentries::device::step::export::title%]",
|
||||
"description": "The export includes the unique ID's used in the subentry configuration. You must change these ID's or remove the MQTT device subentry to avoid conflicts. Note that removing the MQTT device subentry will also remove the device from the device registry.\n\nTo allow [discovery]({url}#device-discovery-payload), the discovery payload needs to be published to the discovery topic. Copy the needed information from the fields below.",
|
||||
"description": "To allow setup via MQTT [discovery]({url}#device-discovery-payload), the discovery payload needs to be published to the discovery topic. Copy the information from the fields below. Home Assistant will detect if the MQTT device was tried set up via MQTT discovery instead, and will offer a repair flow to clean up the redundant subentry.",
|
||||
"data": {
|
||||
"discovery_topic": "Discovery topic",
|
||||
"discovery_payload": "Discovery payload:"
|
||||
|
Loading…
x
Reference in New Issue
Block a user