mirror of
https://github.com/esphome/esphome.git
synced 2026-09-27 15:00:24 +00:00
[mdns] Advertise ota_signed when signed OTA verification is compiled in (#19407)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
aba49c3d4f
commit
623aec2984
@@ -118,6 +118,9 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_OTA_SIGNED_VERIFICATION
|
||||
txt_count++; // ota_signed
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
txt_count += 2; // project_name and project_version
|
||||
#endif
|
||||
@@ -186,6 +189,13 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_OTA_SIGNED_VERIFICATION
|
||||
// Signals that an unsigned OTA image is rejected; serial flash is unaffected.
|
||||
MDNS_STATIC_CONST_CHAR(TXT_OTA_SIGNED, "ota_signed");
|
||||
MDNS_STATIC_CONST_CHAR(VALUE_TRUE, "1");
|
||||
txt_records.push_back({MDNS_STR(TXT_OTA_SIGNED), MDNS_STR(VALUE_TRUE)});
|
||||
#endif
|
||||
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
|
||||
MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
|
||||
|
||||
@@ -56,6 +56,7 @@ TXT_RECORD_PROJECT_VERSION = b"project_version"
|
||||
TXT_RECORD_NETWORK = b"network"
|
||||
TXT_RECORD_FRIENDLY_NAME = b"friendly_name"
|
||||
TXT_RECORD_VERSION = b"version"
|
||||
TXT_RECORD_OTA_SIGNED = b"ota_signed"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -76,6 +77,8 @@ class DiscoveredImport:
|
||||
project_name: str
|
||||
project_version: str
|
||||
network: str
|
||||
# Defaults False so entries persisted before this field still load.
|
||||
ota_signed: bool = False
|
||||
|
||||
|
||||
class DashboardBrowser(AsyncServiceBrowser):
|
||||
@@ -169,6 +172,7 @@ class DashboardImportDiscovery:
|
||||
project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode()
|
||||
project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode()
|
||||
network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode()
|
||||
ota_signed = info.properties.get(TXT_RECORD_OTA_SIGNED) == b"1"
|
||||
friendly_name = info.properties.get(TXT_RECORD_FRIENDLY_NAME)
|
||||
if friendly_name is not None:
|
||||
friendly_name = friendly_name.decode()
|
||||
@@ -180,6 +184,7 @@ class DashboardImportDiscovery:
|
||||
project_name=project_name,
|
||||
project_version=project_version,
|
||||
network=network,
|
||||
ota_signed=ota_signed,
|
||||
)
|
||||
is_new = name not in self.import_state
|
||||
self.import_state[name] = discovered
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Signed OTA plus the native api: the only combination that emits ota_signed.
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
signed_ota_verification:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
|
||||
mdns:
|
||||
@@ -28,6 +28,7 @@ def _make_service_info(
|
||||
network: str | None = "wifi",
|
||||
friendly_name: str | None = "Living Room",
|
||||
version: str | None = "2025.1.0",
|
||||
ota_signed: str | None = None,
|
||||
) -> MagicMock:
|
||||
"""Build a fake ``AsyncServiceInfo`` with the TXT records we care about.
|
||||
|
||||
@@ -48,6 +49,8 @@ def _make_service_info(
|
||||
properties[b"friendly_name"] = friendly_name.encode()
|
||||
if version is not None:
|
||||
properties[b"version"] = version.encode()
|
||||
if ota_signed is not None:
|
||||
properties[b"ota_signed"] = ota_signed.encode()
|
||||
info.properties = properties
|
||||
info.load_from_cache.return_value = True
|
||||
return info
|
||||
@@ -77,6 +80,7 @@ def test_added_service_populates_import_state_and_fires_callback() -> None:
|
||||
assert entry.project_version == "1.0.0"
|
||||
assert entry.network == "wifi"
|
||||
assert entry.friendly_name == "Living Room"
|
||||
assert entry.ota_signed is False
|
||||
on_update.assert_called_once_with(name, entry)
|
||||
|
||||
|
||||
@@ -206,6 +210,20 @@ def test_network_defaults_to_wifi_when_txt_absent() -> None:
|
||||
assert discovery.import_state[name].network == "wifi"
|
||||
|
||||
|
||||
def test_ota_signed_txt_is_parsed() -> None:
|
||||
"""``ota_signed=1`` marks a device that only accepts signed OTA images.
|
||||
|
||||
Lets a consumer check whether it holds a key the device trusts before
|
||||
an update is attempted, instead of failing at install time.
|
||||
"""
|
||||
discovery = DashboardImportDiscovery()
|
||||
info = _make_service_info(ota_signed="1")
|
||||
name = f"signed.{ESPHOME_SERVICE_TYPE}"
|
||||
discovery._process_service_info(name, info)
|
||||
|
||||
assert discovery.import_state[name].ota_signed is True
|
||||
|
||||
|
||||
def test_friendly_name_optional() -> None:
|
||||
"""``friendly_name`` may be ``None`` if the device doesn't broadcast it.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user