Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions esphome_device_builder/controllers/_device_mqtt_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from typing import Any

import yaml
from esphome import yaml_util
from esphome.core import EsphomeError

from ..helpers.device_yaml import load_device_yaml
from ..helpers.yaml import FastestSafeLoader
Expand Down Expand Up @@ -299,12 +301,29 @@ def _load_secrets(config_dir: Path) -> dict[str, Any]:
# equivalent of SafeLoader. Same noqa rationale as the
# ``_TolerantYamlLoader`` call above.
data = yaml.load(f, Loader=FastestSafeLoader) # noqa: S506
except yaml.YAMLError:
_LOGGER.warning("Could not parse secrets.yaml — MQTT broker secrets unavailable")
return {}
except (yaml.YAMLError, OSError):
# A plain secrets.yaml is key/value scalars the fast loader
# handles. ESPHome tags (``!include`` / ``!secret``) and merge
# keys are not — the documented HA-shared
# ``<<: !include ../secrets.yaml`` pattern lands here. Retry with
# ESPHome's loader, which resolves them the way a compile does.
data = _load_secrets_via_esphome(secrets_path)
return data if isinstance(data, dict) else {}


def _load_secrets_via_esphome(secrets_path: Path) -> dict[str, Any] | None:
"""Load *secrets_path* with ESPHome's loader (``!include`` / merge keys)."""
try:
data = yaml_util.load_yaml(secrets_path)
except (EsphomeError, yaml.YAMLError, OSError):
_LOGGER.warning("Could not parse secrets.yaml — MQTT broker secrets unavailable")
return None
if not isinstance(data, dict):
_LOGGER.warning("Could not parse secrets.yaml — MQTT broker secrets unavailable")
return None
return data


def _safe_mtime(path: Path) -> float:
"""Return *path*'s mtime, or ``0.0`` when the file is missing."""
try:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_mqtt_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,52 @@ async def test_coordinator_resolves_secrets_from_secrets_yaml(
assert stub_monitor.instances[0].broker.password == "shh"


async def test_coordinator_resolves_secrets_via_included_secrets_file(
tmp_path: Path,
stub_monitor: type[_RecordingMonitor],
caplog: pytest.LogCaptureFixture,
) -> None:
# secrets.yaml pulls in a shared file via the merge-key +
# ``!include`` pattern (HA-shared secrets); the plain SafeLoader
# rejects it, so the ESPHome-loader fallback must resolve it.
(tmp_path / "shared.yaml").write_text("mqtt_broker: 10.0.0.9\nmqtt_pw: shh\n")
(tmp_path / "secrets.yaml").write_text("<<: !include shared.yaml\n")
devices = [
_write_device(
tmp_path,
"alpha",
"mqtt:\n broker: !secret mqtt_broker\n password: !secret mqtt_pw\n",
)
]
coord = _make_coordinator(tmp_path, devices)
with caplog.at_level("WARNING"):
await coord.reconcile()
assert coord.active_brokers == 1
assert stub_monitor.instances[0].broker.host == "10.0.0.9"
assert stub_monitor.instances[0].broker.password == "shh"
assert "Could not parse secrets.yaml" not in caplog.text


async def test_coordinator_warns_when_secrets_unparseable_by_both_loaders(
tmp_path: Path,
stub_monitor: type[_RecordingMonitor],
caplog: pytest.LogCaptureFixture,
) -> None:
(tmp_path / "secrets.yaml").write_text("<<: !include does_not_exist.yaml\n")
devices = [
_write_device(
tmp_path,
"alpha",
"mqtt:\n broker: !secret mqtt_broker\n",
)
]
coord = _make_coordinator(tmp_path, devices)
with caplog.at_level("WARNING"):
await coord.reconcile()
assert coord.active_brokers == 0
assert "Could not parse secrets.yaml" in caplog.text


async def test_coordinator_resolves_broker_pulled_in_via_packages(
tmp_path: Path,
stub_monitor: type[_RecordingMonitor],
Expand Down
Loading