Skip to content

Commit

Permalink
Add ESPhome discovery via MQTT (#116499)
Browse files Browse the repository at this point in the history
Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
Links2004 and bdraco committed May 10, 2024
1 parent 62d70b1 commit ed4c319
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 2 deletions.
40 changes: 39 additions & 1 deletion homeassistant/components/esphome/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections.abc import Mapping
import json
import logging
from typing import Any
from typing import Any, cast

from aioesphomeapi import (
APIClient,
Expand All @@ -31,6 +31,8 @@
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import callback
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 (
CONF_ALLOW_SERVICE_CALLS,
Expand Down Expand Up @@ -250,6 +252,42 @@ async def async_step_zeroconf(

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(
self, discovery_info: dhcp.DhcpServiceInfo
) -> ConfigFlowResult:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/esphome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"requirements": [
"aioesphomeapi==24.3.0",
"esphome-dashboard-api==1.2.3",
Expand Down
5 changes: 4 additions & 1 deletion homeassistant/components/esphome/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"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": {
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/generated/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"dsmr_reader": [
"dsmr/#",
],
"esphome": [
"esphome/discover/#",
],
"fully_kiosk": [
"fully/deviceInfo/+",
],
Expand Down
70 changes: 70 additions & 0 deletions tests/components/esphome/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo

from . import VALID_NOISE_PSK

Expand Down Expand Up @@ -1414,3 +1415,72 @@ async def test_user_discovers_name_no_dashboard(
CONF_DEVICE_NAME: "test",
}
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"

0 comments on commit ed4c319

Please sign in to comment.