mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Add YAML and discovery info export feature for MQTT device subentries
This commit is contained in:
parent
03366038ce
commit
fb7cd59703
@ -8,6 +8,7 @@ from collections.abc import Callable, Mapping
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
import json
|
||||
import logging
|
||||
import queue
|
||||
from ssl import PROTOCOL_TLS_CLIENT, SSLContext, SSLError
|
||||
@ -24,6 +25,7 @@ from cryptography.hazmat.primitives.serialization import (
|
||||
)
|
||||
from cryptography.x509 import load_der_x509_certificate, load_pem_x509_certificate
|
||||
import voluptuous as vol
|
||||
import yaml
|
||||
|
||||
from homeassistant.components.file_upload import process_uploaded_file
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
@ -62,6 +64,7 @@ from homeassistant.const import (
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
CONF_PROTOCOL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USERNAME,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@ -172,6 +175,7 @@ SET_CLIENT_CERT = "set_client_cert"
|
||||
|
||||
BOOLEAN_SELECTOR = BooleanSelector()
|
||||
TEXT_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
|
||||
URL_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.URL))
|
||||
PUBLISH_TOPIC_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.TEXT))
|
||||
PORT_SELECTOR = vol.All(
|
||||
NumberSelector(NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=1, max=65535)),
|
||||
@ -1623,8 +1627,11 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
if len(self._subentry_data["components"]) > 1:
|
||||
menu_options.append("delete_entity")
|
||||
menu_options.extend(["device", "availability"])
|
||||
if self._subentry_data != self._get_reconfigure_subentry().data:
|
||||
menu_options.append("save_changes")
|
||||
menu_options.append(
|
||||
"save_changes"
|
||||
if self._subentry_data != self._get_reconfigure_subentry().data
|
||||
else "export"
|
||||
)
|
||||
return self.async_show_menu(
|
||||
step_id="summary_menu",
|
||||
menu_options=menu_options,
|
||||
@ -1666,6 +1673,111 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
title=self._subentry_data[CONF_DEVICE][CONF_NAME],
|
||||
)
|
||||
|
||||
async def async_step_export(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Export the MQTT device config as YAML or discovery payload."""
|
||||
return self.async_show_menu(
|
||||
step_id="export",
|
||||
menu_options=["export_yaml", "export_discovery"],
|
||||
)
|
||||
|
||||
async def async_step_export_yaml(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Export the MQTT device config as YAML."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_summary_menu()
|
||||
|
||||
subentry = self._get_reconfigure_subentry()
|
||||
mqtt_yaml_config_base: dict[str, list[dict[str, dict[str, Any]]]] = {DOMAIN: []}
|
||||
mqtt_yaml_config = mqtt_yaml_config_base[DOMAIN]
|
||||
|
||||
for component_id, component_data in self._subentry_data["components"].items():
|
||||
component_config: dict[str, Any] = component_data.copy()
|
||||
component_config[CONF_UNIQUE_ID] = f"{subentry.subentry_id}_{component_id}"
|
||||
component_config[CONF_DEVICE] = {
|
||||
key: value
|
||||
for key, value in self._subentry_data["device"].items()
|
||||
if key != "mqtt_settings"
|
||||
} | {"identifiers": [subentry.subentry_id]}
|
||||
platform = component_config.pop(CONF_PLATFORM)
|
||||
component_config.update(self._subentry_data.get("availability", {}))
|
||||
component_config.update(
|
||||
self._subentry_data["device"].get("mqtt_settings", {}).copy()
|
||||
)
|
||||
mqtt_yaml_config.append({platform: component_config})
|
||||
|
||||
yaml_config = yaml.dump(mqtt_yaml_config_base)
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional("yaml"): TEMPLATE_SELECTOR,
|
||||
}
|
||||
)
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema=data_schema,
|
||||
suggested_values={"yaml": yaml_config},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="export_yaml",
|
||||
last_step=False,
|
||||
data_schema=data_schema,
|
||||
description_placeholders={
|
||||
"url": "https://www.home-assistant.io/integrations/mqtt/"
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_export_discovery(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Export the MQTT device config dor MQTT discovery."""
|
||||
|
||||
if user_input is not None:
|
||||
return await self.async_step_summary_menu()
|
||||
|
||||
subentry = self._get_reconfigure_subentry()
|
||||
discovery_topic = f"homeassistant/device/{subentry.subentry_id}/config"
|
||||
discovery_payload: dict[str, Any] = {}
|
||||
discovery_payload.update(self._subentry_data.get("availability", {}))
|
||||
discovery_payload["dev"] = {
|
||||
key: value
|
||||
for key, value in self._subentry_data["device"].items()
|
||||
if key != "mqtt_settings"
|
||||
} | {"identifiers": [subentry.subentry_id]}
|
||||
discovery_payload["o"] = {"name": "MQTT subentry export"}
|
||||
discovery_payload["cmps"] = {}
|
||||
|
||||
for component_id, component_data in self._subentry_data["components"].items():
|
||||
component_config: dict[str, Any] = component_data.copy()
|
||||
component_config[CONF_UNIQUE_ID] = f"{subentry.subentry_id}_{component_id}"
|
||||
component_config.update(self._subentry_data.get("availability", {}))
|
||||
component_config.update(
|
||||
self._subentry_data["device"].get("mqtt_settings", {}).copy()
|
||||
)
|
||||
discovery_payload["cmps"][component_id] = component_config
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional("discovery_topic"): TEXT_SELECTOR,
|
||||
vol.Optional("discovery_payload"): TEMPLATE_SELECTOR,
|
||||
}
|
||||
)
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
data_schema=data_schema,
|
||||
suggested_values={
|
||||
"discovery_topic": discovery_topic,
|
||||
"discovery_payload": json.dumps(discovery_payload, indent=2),
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="export_discovery",
|
||||
last_step=False,
|
||||
data_schema=data_schema,
|
||||
description_placeholders={
|
||||
"url": "https://www.home-assistant.io/integrations/mqtt/"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_is_pem_data(data: bytes) -> bool:
|
||||
|
@ -172,6 +172,7 @@
|
||||
"delete_entity": "Delete an entity",
|
||||
"availability": "Configure availability",
|
||||
"device": "Update device properties",
|
||||
"export": "Export MQTT device configuration",
|
||||
"save_changes": "Save changes"
|
||||
}
|
||||
},
|
||||
@ -270,6 +271,36 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"export": {
|
||||
"title": "Export MQTT device config",
|
||||
"description": "An export allows you set up the MQTT Device configuration via YAMl or MQTT discovery. The configuration can also be helpfull for troubleshooting. The export includes the unique ID's used in the configuration. You cannot activate this configuration unless you remove the MQTT-device or change the unique id's. Removing the MQTT device subentry will also remove the device from the device registry. After removing the MQTT device subentry, it can be set up via YAML or discovery with the same unique ID's.",
|
||||
"menu_options": {
|
||||
"export_discovery": "Export MQTT discovery information",
|
||||
"export_yaml": "Export to YAML configuration"
|
||||
}
|
||||
},
|
||||
"export_yaml": {
|
||||
"title": "[%key:component::mqtt::config_subentries::device::step::export::title%]",
|
||||
"description": "Copy the YAML configuration below:",
|
||||
"data": {
|
||||
"yaml": "YAML"
|
||||
},
|
||||
"data_description": {
|
||||
"yaml": "Place your YAML configuration [configuration.yaml]({url}#yaml-configuration-listed-per-item)."
|
||||
}
|
||||
},
|
||||
"export_discovery": {
|
||||
"title": "[%key:component::mqtt::config_subentries::device::step::export::title%]",
|
||||
"description": "To allow [discovery]({url}#device-discovery-payload), the discovery payload needs to be published to the discovery topic",
|
||||
"data": {
|
||||
"discovery_topic": "Discovery topic",
|
||||
"discovery_payload": "Discovery payload"
|
||||
},
|
||||
"data_description": {
|
||||
"discovery_topic": "The [discovery topic]({url}#discovery-topic) to publish the discovery payload to, to trigger MQTT discovery. An empty payload published to this topic, will remove the device and discovered entities.",
|
||||
"discovery_payload": "The JSON [discovery payload]({url}#discovery-discovery-payload) that contains information about the MQTT device."
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
@ -2982,6 +2982,7 @@ async def test_subentry_reconfigure_remove_entity(
|
||||
"delete_entity",
|
||||
"device",
|
||||
"availability",
|
||||
"export",
|
||||
]
|
||||
|
||||
# assert we can delete an entity
|
||||
@ -3105,6 +3106,7 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
|
||||
"delete_entity",
|
||||
"device",
|
||||
"availability",
|
||||
"export",
|
||||
]
|
||||
|
||||
# assert we can update an entity
|
||||
@ -3280,6 +3282,7 @@ async def test_subentry_reconfigure_edit_entity_single_entity(
|
||||
"update_entity",
|
||||
"device",
|
||||
"availability",
|
||||
"export",
|
||||
]
|
||||
|
||||
# assert we can update the entity, there is no select step
|
||||
@ -3425,6 +3428,7 @@ async def test_subentry_reconfigure_edit_entity_reset_fields(
|
||||
"update_entity",
|
||||
"device",
|
||||
"availability",
|
||||
"export",
|
||||
]
|
||||
|
||||
# assert we can update the entity, there is no select step
|
||||
@ -3550,6 +3554,7 @@ async def test_subentry_reconfigure_add_entity(
|
||||
"update_entity",
|
||||
"device",
|
||||
"availability",
|
||||
"export",
|
||||
]
|
||||
|
||||
# assert we can update the entity, there is no select step
|
||||
@ -3649,6 +3654,7 @@ async def test_subentry_reconfigure_update_device_properties(
|
||||
"delete_entity",
|
||||
"device",
|
||||
"availability",
|
||||
"export",
|
||||
]
|
||||
|
||||
# assert we can update the device properties
|
||||
@ -3803,3 +3809,79 @@ async def test_subentry_reconfigure_availablity(
|
||||
"payload_available": "1",
|
||||
"payload_not_available": "0",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mqtt_config_subentries_data",
|
||||
[
|
||||
(
|
||||
ConfigSubentryData(
|
||||
data=MOCK_NOTIFY_SUBENTRY_DATA_MULTI,
|
||||
subentry_type="device",
|
||||
title="Mock subentry",
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(("flow_step"), ["export_yaml", "export_discovery"])
|
||||
async def test_subentry_reconfigure_export_settings(
|
||||
hass: HomeAssistant,
|
||||
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
flow_step: str,
|
||||
) -> None:
|
||||
"""Test the subentry ConfigFlow reconfigure and update device properties."""
|
||||
await mqtt_mock_entry()
|
||||
config_entry: MockConfigEntry = hass.config_entries.async_entries(mqtt.DOMAIN)[0]
|
||||
subentry_id: str
|
||||
subentry: ConfigSubentry
|
||||
subentry_id, subentry = next(iter(config_entry.subentries.items()))
|
||||
result = await config_entry.start_subentry_reconfigure_flow(
|
||||
hass, "device", subentry_id
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "summary_menu"
|
||||
|
||||
# assert we have a device for the subentry
|
||||
device = device_registry.async_get_device(identifiers={(mqtt.DOMAIN, subentry_id)})
|
||||
assert device is not None
|
||||
|
||||
# assert we entity for all subentry components
|
||||
components = deepcopy(dict(subentry.data))["components"]
|
||||
assert len(components) == 2
|
||||
|
||||
# assert menu options, we have the option to export
|
||||
assert result["menu_options"] == [
|
||||
"entity",
|
||||
"update_entity",
|
||||
"delete_entity",
|
||||
"device",
|
||||
"availability",
|
||||
"export",
|
||||
]
|
||||
|
||||
# Open export menu
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": "export"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "export"
|
||||
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
{"next_step_id": flow_step},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == flow_step
|
||||
assert result["description_placeholders"] == {
|
||||
"url": "https://www.home-assistant.io/integrations/mqtt/"
|
||||
}
|
||||
|
||||
# Back to summary menu
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "summary_menu"
|
||||
|
Loading…
x
Reference in New Issue
Block a user