M5: use DB-merged runtime settings for MQTT/discovery; add MQTT test button; move Expose panel into config accordion

This commit is contained in:
2026-06-22 20:11:54 +02:00
parent 75d685f04d
commit 35cdc7f22a
11 changed files with 502 additions and 59 deletions
+61 -17
View File
@@ -340,7 +340,7 @@ def test_publish_discovery_sends_retained_for_enabled_entity(disco_db) -> None:
mock_mgr.publish.side_effect = _capture
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_discovery
@@ -383,7 +383,7 @@ def test_publish_discovery_sends_empty_payload_for_disabled_entity(disco_db) ->
mock_mgr.publish.side_effect = _capture
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_discovery
@@ -425,7 +425,7 @@ def test_publish_discovery_enabled_vs_disabled_payload(disco_db) -> None:
mock_mgr.publish.side_effect = _capture
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_discovery
@@ -501,7 +501,7 @@ def test_publish_states_calls_value_getter_for_enabled_sensor() -> None:
mock_catalog = [CatalogEntry(entity=mock_entity, enabled=True)]
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
patch("app.services.ha_discovery.build_catalog", return_value=mock_catalog),
):
@@ -549,7 +549,7 @@ def test_publish_states_skips_disabled_entities() -> None:
mock_catalog = [CatalogEntry(entity=mock_entity, enabled=False)]
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
patch("app.services.ha_discovery.build_catalog", return_value=mock_catalog),
):
@@ -614,7 +614,7 @@ def test_publish_device_state_pushes_availability_and_state() -> None:
mock_device.last_poll_ok = True
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
patch("app.services.ha_discovery.build_catalog", return_value=mock_catalog),
):
@@ -658,7 +658,7 @@ def test_publish_device_state_publishes_offline_on_failed_poll() -> None:
mock_device.last_poll_ok = False
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
patch("app.services.ha_discovery.build_catalog", return_value=[]),
):
@@ -695,12 +695,13 @@ def test_publish_device_offline_publishes_offline_topic() -> None:
mock_mgr.publish.side_effect = _capture
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_device_offline
from unittest.mock import MagicMock as _MagicMock
publish_device_offline(uuid_val)
publish_device_offline(_MagicMock(), uuid_val)
assert len(published) == 1
topic, payload = published[0]
@@ -713,13 +714,55 @@ def test_publish_device_offline_publishes_offline_topic() -> None:
# ---------------------------------------------------------------------------
def test_should_publish_true_when_db_enabled_bootstrap_disabled(disco_db) -> None:
"""_should_publish must use DB-merged settings: if DB has mqtt_enabled=True and
ha_discovery_enabled=True (bootstrap has both False), publish should proceed.
This is the core regression test for Issue 2: bare get_settings() returns bootstrap
(False); build_runtime_settings returns DB-merged (True).
"""
from app.services.config_page import build_runtime_settings
from app.services.ha_discovery import _should_publish
from app.models.config import AppConfigEntry
from datetime import UTC, datetime
now = datetime.now(UTC)
# Write MQTT + discovery enabled into app_config (DB level)
with Session(disco_db) as session:
session.add(AppConfigEntry(key="MQTT_ENABLED", value="true", updated_at=now))
session.add(AppConfigEntry(key="MQTT_BROKER_HOST", value="broker.test", updated_at=now))
session.add(AppConfigEntry(key="HA_DISCOVERY_ENABLED", value="true", updated_at=now))
session.commit()
# Bootstrap has mqtt_enabled=False and ha_discovery_enabled=False (env defaults)
from app.config import Settings
bootstrap = Settings(_env_file=None, mqtt_enabled=False, ha_discovery_enabled=False,
app_database_url=str(disco_db.url))
with Session(disco_db) as session:
runtime = build_runtime_settings(session, bootstrap)
# Simulate a connected broker for the _should_publish check
mock_mgr_connected = MagicMock()
mock_mgr_connected.is_connected = True
with patch("app.services.ha_discovery.mqtt_manager", mock_mgr_connected):
result = _should_publish(runtime)
assert result is True, (
"_should_publish must return True when DB has mqtt_enabled=True + ha_discovery_enabled=True, "
"even if bootstrap Settings has both False"
)
def test_publish_discovery_noop_when_mqtt_disabled() -> None:
"""publish_discovery must be a no-op when mqtt_enabled=False."""
settings = _make_settings(mqtt_enabled=False, ha_discovery_enabled=True)
mock_mgr = _make_mock_manager(is_connected=False)
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_discovery
@@ -739,7 +782,7 @@ def test_publish_states_noop_when_not_connected() -> None:
mock_mgr = _make_mock_manager(is_connected=False)
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_states
@@ -758,7 +801,7 @@ def test_publish_discovery_noop_when_ha_discovery_disabled() -> None:
mock_mgr = _make_mock_manager(is_connected=True)
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_discovery
@@ -781,7 +824,7 @@ def test_publish_device_state_noop_when_mqtt_disabled() -> None:
mock_device.last_poll_ok = True
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_device_state
@@ -800,12 +843,13 @@ def test_publish_device_offline_noop_when_mqtt_disabled() -> None:
mock_mgr = _make_mock_manager(is_connected=False)
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
):
from app.services.ha_discovery import publish_device_offline
from unittest.mock import MagicMock as _MagicMock
publish_device_offline("some-uuid") # must not raise
publish_device_offline(_MagicMock(), "some-uuid") # must not raise
mock_mgr.publish.assert_not_called()
@@ -984,7 +1028,7 @@ def test_publish_discovery_uses_retain_true_for_enabled_entity() -> None:
catalog = [CatalogEntry(entity=entity, enabled=True)]
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
patch("app.services.ha_discovery.build_catalog", return_value=catalog),
):
@@ -1085,7 +1129,7 @@ def test_real_provider_value_getter_returns_latest_reading_value(disco_db) -> No
mock_device.last_poll_ok = True
with (
patch("app.services.ha_discovery._get_settings", return_value=settings),
patch("app.services.ha_discovery.build_runtime_settings", return_value=settings),
patch("app.services.ha_discovery.mqtt_manager", mock_mgr),
# NOTE: build_catalog is NOT mocked — we use the real modbus provider.
):