mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
Enhanced MQTT support with separate topics and multipliers (#173)
- Add separate MQTT topics for power, energy, and state monitoring - mqtt_power_topic, mqtt_power_path, mqtt_power_multiplier - mqtt_energy_topic, mqtt_energy_path, mqtt_energy_multiplier - mqtt_state_topic, mqtt_state_path, mqtt_state_on_value - Support different MQTT topics per data type (e.g., Zigbee2MQTT with separate power/energy/state topics) - Individual multipliers for power and energy (e.g., mW→W, Wh→kWh) - Configurable ON value for state monitoring (e.g., "ON", "true", "1") - Maintain backward compatibility with legacy mqtt_topic/mqtt_multiplier - Database migration auto-copies legacy fields to new fields - Update backup/restore to handle new MQTT fields - Add backend tests for new MQTT configurations - Update frontend form with organized Power/Energy/State sections Closes #173
This commit is contained in:
@@ -315,9 +315,19 @@ def smart_plug_factory(db_session):
|
||||
defaults["ha_entity_id"] = "switch.test"
|
||||
defaults["ip_address"] = None
|
||||
elif plug_type == "mqtt":
|
||||
# Legacy fields (for backward compatibility tests)
|
||||
defaults["mqtt_topic"] = kwargs.get("mqtt_topic", "test/topic")
|
||||
defaults["mqtt_power_path"] = kwargs.get("mqtt_power_path", "power")
|
||||
defaults["mqtt_multiplier"] = kwargs.get("mqtt_multiplier", 1.0)
|
||||
# New separate topic/path/multiplier fields
|
||||
defaults["mqtt_power_topic"] = kwargs.get("mqtt_power_topic")
|
||||
defaults["mqtt_power_path"] = kwargs.get("mqtt_power_path", "power")
|
||||
defaults["mqtt_power_multiplier"] = kwargs.get("mqtt_power_multiplier", 1.0)
|
||||
defaults["mqtt_energy_topic"] = kwargs.get("mqtt_energy_topic")
|
||||
defaults["mqtt_energy_path"] = kwargs.get("mqtt_energy_path")
|
||||
defaults["mqtt_energy_multiplier"] = kwargs.get("mqtt_energy_multiplier", 1.0)
|
||||
defaults["mqtt_state_topic"] = kwargs.get("mqtt_state_topic")
|
||||
defaults["mqtt_state_path"] = kwargs.get("mqtt_state_path")
|
||||
defaults["mqtt_state_on_value"] = kwargs.get("mqtt_state_on_value")
|
||||
defaults["ip_address"] = None
|
||||
defaults["ha_entity_id"] = None
|
||||
else:
|
||||
|
||||
@@ -694,3 +694,132 @@ class TestSmartPlugsAPI:
|
||||
result = response.json()
|
||||
assert result["mqtt_topic"] == "new/topic"
|
||||
assert result["mqtt_power_path"] == "new_power"
|
||||
|
||||
# ========================================================================
|
||||
# Enhanced MQTT Integration tests (separate topics per data type)
|
||||
# ========================================================================
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_mqtt_plug_with_separate_topics(self, async_client: AsyncClient, mock_mqtt_smart_plug_service):
|
||||
"""Verify MQTT plug can be created with separate topics for power, energy, and state."""
|
||||
data = {
|
||||
"name": "MQTT Separate Topics",
|
||||
"plug_type": "mqtt",
|
||||
"mqtt_power_topic": "zigbee/power",
|
||||
"mqtt_power_path": "power_l1",
|
||||
"mqtt_power_multiplier": 0.001,
|
||||
"mqtt_energy_topic": "zigbee/energy",
|
||||
"mqtt_energy_path": "energy_total",
|
||||
"mqtt_energy_multiplier": 1.0,
|
||||
"mqtt_state_topic": "zigbee/state",
|
||||
"mqtt_state_path": "state",
|
||||
"mqtt_state_on_value": "ON",
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/smart-plugs/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["name"] == "MQTT Separate Topics"
|
||||
assert result["plug_type"] == "mqtt"
|
||||
# Power fields
|
||||
assert result["mqtt_power_topic"] == "zigbee/power"
|
||||
assert result["mqtt_power_path"] == "power_l1"
|
||||
assert result["mqtt_power_multiplier"] == 0.001
|
||||
# Energy fields
|
||||
assert result["mqtt_energy_topic"] == "zigbee/energy"
|
||||
assert result["mqtt_energy_path"] == "energy_total"
|
||||
assert result["mqtt_energy_multiplier"] == 1.0
|
||||
# State fields
|
||||
assert result["mqtt_state_topic"] == "zigbee/state"
|
||||
assert result["mqtt_state_path"] == "state"
|
||||
assert result["mqtt_state_on_value"] == "ON"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_mqtt_plug_energy_only(self, async_client: AsyncClient, mock_mqtt_smart_plug_service):
|
||||
"""Verify MQTT plug can be created with only energy monitoring."""
|
||||
data = {
|
||||
"name": "Energy Only Monitor",
|
||||
"plug_type": "mqtt",
|
||||
"mqtt_energy_topic": "sensors/energy",
|
||||
"mqtt_energy_path": "kwh",
|
||||
"mqtt_energy_multiplier": 0.001, # Wh to kWh
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/smart-plugs/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mqtt_energy_topic"] == "sensors/energy"
|
||||
assert result["mqtt_energy_path"] == "kwh"
|
||||
assert result["mqtt_energy_multiplier"] == 0.001
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_mqtt_plug_state_only(self, async_client: AsyncClient, mock_mqtt_smart_plug_service):
|
||||
"""Verify MQTT plug can be created with only state monitoring."""
|
||||
data = {
|
||||
"name": "State Only Monitor",
|
||||
"plug_type": "mqtt",
|
||||
"mqtt_state_topic": "switches/outlet",
|
||||
"mqtt_state_path": "state",
|
||||
"mqtt_state_on_value": "true",
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/smart-plugs/", json=data)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mqtt_state_topic"] == "switches/outlet"
|
||||
assert result["mqtt_state_path"] == "state"
|
||||
assert result["mqtt_state_on_value"] == "true"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_mqtt_plug_no_data_source_fails(self, async_client: AsyncClient):
|
||||
"""Verify creating MQTT plug without any complete data source fails."""
|
||||
data = {
|
||||
"name": "Invalid MQTT Plug",
|
||||
"plug_type": "mqtt",
|
||||
# Has topic but no path - not a complete data source
|
||||
"mqtt_power_topic": "zigbee/power",
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
response = await async_client.post("/api/v1/smart-plugs/", json=data)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_update_mqtt_plug_separate_multipliers(
|
||||
self, async_client: AsyncClient, smart_plug_factory, db_session, mock_mqtt_smart_plug_service
|
||||
):
|
||||
"""Verify MQTT plug multipliers can be updated separately."""
|
||||
plug = await smart_plug_factory(
|
||||
plug_type="mqtt",
|
||||
mqtt_power_topic="test/power",
|
||||
mqtt_power_path="power",
|
||||
mqtt_power_multiplier=1.0,
|
||||
mqtt_energy_topic="test/energy",
|
||||
mqtt_energy_path="energy",
|
||||
mqtt_energy_multiplier=1.0,
|
||||
)
|
||||
|
||||
response = await async_client.patch(
|
||||
f"/api/v1/smart-plugs/{plug.id}",
|
||||
json={
|
||||
"mqtt_power_multiplier": 0.001, # Change power multiplier only
|
||||
"mqtt_energy_multiplier": 0.001, # Change energy multiplier only
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["mqtt_power_multiplier"] == 0.001
|
||||
assert result["mqtt_energy_multiplier"] == 0.001
|
||||
|
||||
Reference in New Issue
Block a user