test_mqtt_smart_plug_subscribe.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """
  2. Tests for subscribe_plug_to_mqtt — the shared helper that resolves a
  3. SmartPlug row's per-type topic fields (with legacy fallback) and calls
  4. MQTTSmartPlugService.subscribe().
  5. Regression guard for #1010, where the startup-restore code path had
  6. drifted from the create/update routes: it only looked at the legacy
  7. `mqtt_topic` field and silently skipped plugs whose topics were set
  8. only in the newer per-type fields, so the MQTT smart-plug subscription
  9. was lost on every Bambuddy restart until the user re-saved the plug.
  10. """
  11. from types import SimpleNamespace
  12. from unittest.mock import MagicMock
  13. from backend.app.services.mqtt_smart_plug import subscribe_plug_to_mqtt
  14. def _plug(**overrides):
  15. """Build a SmartPlug-shaped record. All fields default to None/defaults."""
  16. defaults = {
  17. "id": 1,
  18. "mqtt_topic": None,
  19. "mqtt_power_topic": None,
  20. "mqtt_power_path": None,
  21. "mqtt_power_multiplier": None,
  22. "mqtt_energy_topic": None,
  23. "mqtt_energy_path": None,
  24. "mqtt_energy_multiplier": None,
  25. "mqtt_state_topic": None,
  26. "mqtt_state_path": None,
  27. "mqtt_state_on_value": None,
  28. "mqtt_multiplier": None,
  29. }
  30. defaults.update(overrides)
  31. return SimpleNamespace(**defaults)
  32. def test_per_type_topics_restored_without_legacy_mqtt_topic():
  33. """#1010: plug configured only with per-type topics must still subscribe."""
  34. service = MagicMock()
  35. plug = _plug(
  36. id=42,
  37. mqtt_power_topic="shellies/plug-living/power",
  38. mqtt_power_path="value",
  39. mqtt_state_topic="shellies/plug-living/relay/0",
  40. mqtt_state_on_value="on",
  41. )
  42. topics = subscribe_plug_to_mqtt(service, plug)
  43. service.subscribe.assert_called_once()
  44. kwargs = service.subscribe.call_args.kwargs
  45. assert kwargs["plug_id"] == 42
  46. assert kwargs["power_topic"] == "shellies/plug-living/power"
  47. assert kwargs["power_path"] == "value"
  48. assert kwargs["state_topic"] == "shellies/plug-living/relay/0"
  49. assert kwargs["state_on_value"] == "on"
  50. # energy wasn't configured, so no per-type topic
  51. assert kwargs["energy_topic"] is None
  52. assert set(topics) == {"shellies/plug-living/power", "shellies/plug-living/relay/0"}
  53. def test_legacy_single_topic_falls_back_for_all_data_types():
  54. """Backward-compat: a plug with only the legacy mqtt_topic must still work."""
  55. service = MagicMock()
  56. plug = _plug(
  57. id=7,
  58. mqtt_topic="zigbee2mqtt/shelly-office",
  59. mqtt_power_path="power",
  60. mqtt_energy_path="energy",
  61. mqtt_state_path="state",
  62. mqtt_state_on_value="ON",
  63. mqtt_multiplier=0.001, # legacy
  64. )
  65. topics = subscribe_plug_to_mqtt(service, plug)
  66. kwargs = service.subscribe.call_args.kwargs
  67. assert kwargs["power_topic"] == "zigbee2mqtt/shelly-office"
  68. assert kwargs["energy_topic"] == "zigbee2mqtt/shelly-office"
  69. assert kwargs["state_topic"] == "zigbee2mqtt/shelly-office"
  70. # Legacy multiplier flows through for both power and energy.
  71. assert kwargs["power_multiplier"] == 0.001
  72. assert kwargs["energy_multiplier"] == 0.001
  73. assert topics == ["zigbee2mqtt/shelly-office"]
  74. def test_per_type_multipliers_override_legacy():
  75. service = MagicMock()
  76. plug = _plug(
  77. mqtt_power_topic="t/power",
  78. mqtt_power_multiplier=0.5,
  79. mqtt_energy_topic="t/energy",
  80. mqtt_energy_multiplier=0.25,
  81. mqtt_multiplier=9.0, # should be overridden by per-type values
  82. )
  83. subscribe_plug_to_mqtt(service, plug)
  84. kwargs = service.subscribe.call_args.kwargs
  85. assert kwargs["power_multiplier"] == 0.5
  86. assert kwargs["energy_multiplier"] == 0.25
  87. def test_per_type_topics_beat_legacy_topic_when_both_set():
  88. """If both legacy and per-type topic are set, per-type wins."""
  89. service = MagicMock()
  90. plug = _plug(
  91. mqtt_topic="old/topic",
  92. mqtt_power_topic="new/power",
  93. mqtt_energy_topic="new/energy",
  94. )
  95. subscribe_plug_to_mqtt(service, plug)
  96. kwargs = service.subscribe.call_args.kwargs
  97. assert kwargs["power_topic"] == "new/power"
  98. assert kwargs["energy_topic"] == "new/energy"
  99. # state has no per-type topic set, so it falls back to legacy
  100. assert kwargs["state_topic"] == "old/topic"
  101. def test_no_topics_configured_skips_subscribe():
  102. """Nothing to subscribe to means the service is not touched."""
  103. service = MagicMock()
  104. plug = _plug(id=99) # all fields None
  105. topics = subscribe_plug_to_mqtt(service, plug)
  106. service.subscribe.assert_not_called()
  107. assert topics == []
  108. def test_returns_unique_topic_list_when_same_topic_used_for_multiple_types():
  109. service = MagicMock()
  110. plug = _plug(
  111. mqtt_power_topic="shared/topic",
  112. mqtt_energy_topic="shared/topic",
  113. mqtt_state_topic="shared/topic",
  114. )
  115. topics = subscribe_plug_to_mqtt(service, plug)
  116. assert topics == ["shared/topic"]