Add ESPhome discovery via MQTT (#116499)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Markus 2024-05-10 13:32:42 +02:00 committed by GitHub
parent 62d70b1b10
commit ed4c3196ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 117 additions and 2 deletions

View File

@ -6,7 +6,7 @@ from collections import OrderedDict
from collections.abc import Mapping from collections.abc import Mapping
import json import json
import logging import logging
from typing import Any from typing import Any, cast
from aioesphomeapi import ( from aioesphomeapi import (
APIClient, APIClient,
@ -31,6 +31,8 @@ from homeassistant.config_entries import (
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from homeassistant.util.json import json_loads_object
from .const import ( from .const import (
CONF_ALLOW_SERVICE_CALLS, CONF_ALLOW_SERVICE_CALLS,
@ -250,6 +252,42 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_discovery_confirm() return await self.async_step_discovery_confirm()
async def async_step_mqtt(
self, discovery_info: MqttServiceInfo
) -> ConfigFlowResult:
"""Handle MQTT discovery."""
device_info = json_loads_object(discovery_info.payload)
if "mac" not in device_info:
return self.async_abort(reason="mqtt_missing_mac")
# there will be no port if the API is not enabled
if "port" not in device_info:
return self.async_abort(reason="mqtt_missing_api")
if "ip" not in device_info:
return self.async_abort(reason="mqtt_missing_ip")
# mac address is lowercase and without :, normalize it
unformatted_mac = cast(str, device_info["mac"])
mac_address = format_mac(unformatted_mac)
device_name = cast(str, device_info["name"])
self._device_name = device_name
self._name = cast(str, device_info.get("friendly_name", device_name))
self._host = cast(str, device_info["ip"])
self._port = cast(int, device_info["port"])
self._noise_required = "api_encryption" in device_info
# Check if already configured
await self.async_set_unique_id(mac_address)
self._abort_if_unique_id_configured(
updates={CONF_HOST: self._host, CONF_PORT: self._port}
)
return await self.async_step_discovery_confirm()
async def async_step_dhcp( async def async_step_dhcp(
self, discovery_info: dhcp.DhcpServiceInfo self, discovery_info: dhcp.DhcpServiceInfo
) -> ConfigFlowResult: ) -> ConfigFlowResult:

View File

@ -14,6 +14,7 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"requirements": [ "requirements": [
"aioesphomeapi==24.3.0", "aioesphomeapi==24.3.0",
"esphome-dashboard-api==1.2.3", "esphome-dashboard-api==1.2.3",

View File

@ -5,7 +5,10 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"mdns_missing_mac": "Missing MAC address in MDNS properties.", "mdns_missing_mac": "Missing MAC address in MDNS properties.",
"service_received": "Service received" "service_received": "Service received",
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
"mqtt_missing_api": "Missing API port in MQTT properties.",
"mqtt_missing_ip": "Missing IP address in MQTT properties."
}, },
"error": { "error": {
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",

View File

@ -10,6 +10,9 @@ MQTT = {
"dsmr_reader": [ "dsmr_reader": [
"dsmr/#", "dsmr/#",
], ],
"esphome": [
"esphome/discover/#",
],
"fully_kiosk": [ "fully_kiosk": [
"fully/deviceInfo/+", "fully/deviceInfo/+",
], ],

View File

@ -30,6 +30,7 @@ from homeassistant.components.hassio import HassioServiceInfo
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from . import VALID_NOISE_PSK from . import VALID_NOISE_PSK
@ -1414,3 +1415,72 @@ async def test_user_discovers_name_no_dashboard(
CONF_DEVICE_NAME: "test", CONF_DEVICE_NAME: "test",
} }
assert mock_client.noise_psk == VALID_NOISE_PSK assert mock_client.noise_psk == VALID_NOISE_PSK
async def mqtt_discovery_test_abort(hass: HomeAssistant, payload: str, reason: str):
"""Test discovery aborted."""
service_info = MqttServiceInfo(
topic="esphome/discover/test",
payload=payload,
qos=0,
retain=False,
subscribed_topic="esphome/discover/#",
timestamp=None,
)
flow = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_MQTT}, data=service_info
)
assert flow["type"] is FlowResultType.ABORT
assert flow["reason"] == reason
async def test_discovery_mqtt_no_mac(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test discovery aborted if mac is missing in MQTT payload."""
await mqtt_discovery_test_abort(hass, "{}", "mqtt_missing_mac")
async def test_discovery_mqtt_no_api(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test discovery aborted if api/port is missing in MQTT payload."""
await mqtt_discovery_test_abort(hass, '{"mac":"abcdef123456"}', "mqtt_missing_api")
async def test_discovery_mqtt_no_ip(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test discovery aborted if ip is missing in MQTT payload."""
await mqtt_discovery_test_abort(
hass, '{"mac":"abcdef123456","port":6053}', "mqtt_missing_ip"
)
async def test_discovery_mqtt_initiation(
hass: HomeAssistant, mock_client, mock_zeroconf: None, mock_setup_entry: None
) -> None:
"""Test discovery importing works."""
service_info = MqttServiceInfo(
topic="esphome/discover/test",
payload='{"name":"mock_name","mac":"1122334455aa","port":6053,"ip":"192.168.43.183"}',
qos=0,
retain=False,
subscribed_topic="esphome/discover/#",
timestamp=None,
)
flow = await hass.config_entries.flow.async_init(
"esphome", context={"source": config_entries.SOURCE_MQTT}, data=service_info
)
result = await hass.config_entries.flow.async_configure(
flow["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "test"
assert result["data"][CONF_HOST] == "192.168.43.183"
assert result["data"][CONF_PORT] == 6053
assert result["result"]
assert result["result"].unique_id == "11:22:33:44:55:aa"