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:
maziggy
2026-01-30 14:17:26 +01:00
parent 00e3478a3e
commit 04f4dccd41
15 changed files with 836 additions and 306 deletions
+48 -15
View File
@@ -360,12 +360,21 @@ async def export_backup(
"ha_power_entity": plug.ha_power_entity,
"ha_energy_today_entity": plug.ha_energy_today_entity,
"ha_energy_total_entity": plug.ha_energy_total_entity,
# MQTT plug fields
# MQTT plug fields (legacy)
"mqtt_topic": plug.mqtt_topic,
"mqtt_power_path": plug.mqtt_power_path,
"mqtt_energy_path": plug.mqtt_energy_path,
"mqtt_state_path": plug.mqtt_state_path,
"mqtt_multiplier": plug.mqtt_multiplier,
# MQTT power fields
"mqtt_power_topic": plug.mqtt_power_topic,
"mqtt_power_path": plug.mqtt_power_path,
"mqtt_power_multiplier": plug.mqtt_power_multiplier,
# MQTT energy fields
"mqtt_energy_topic": plug.mqtt_energy_topic,
"mqtt_energy_path": plug.mqtt_energy_path,
"mqtt_energy_multiplier": plug.mqtt_energy_multiplier,
# MQTT state fields
"mqtt_state_topic": plug.mqtt_state_topic,
"mqtt_state_path": plug.mqtt_state_path,
"mqtt_state_on_value": plug.mqtt_state_on_value,
"printer_serial": printer_id_to_serial.get(plug.printer_id) if plug.printer_id else None,
"enabled": plug.enabled,
"auto_on": plug.auto_on,
@@ -1270,10 +1279,16 @@ async def import_backup(
result = await db.execute(select(SmartPlug).where(SmartPlug.ha_entity_id == plug_data["ha_entity_id"]))
existing = result.scalar_one_or_none()
plug_identifier = plug_data["ha_entity_id"]
elif plug_type == "mqtt" and plug_data.get("mqtt_topic"):
result = await db.execute(select(SmartPlug).where(SmartPlug.mqtt_topic == plug_data["mqtt_topic"]))
elif plug_type == "mqtt" and (plug_data.get("mqtt_power_topic") or plug_data.get("mqtt_topic")):
# Check by mqtt_power_topic first (new format), fall back to mqtt_topic (legacy)
power_topic = plug_data.get("mqtt_power_topic") or plug_data.get("mqtt_topic")
result = await db.execute(
select(SmartPlug).where(
(SmartPlug.mqtt_power_topic == power_topic) | (SmartPlug.mqtt_topic == power_topic)
)
)
existing = result.scalar_one_or_none()
plug_identifier = plug_data["mqtt_topic"]
plug_identifier = power_topic
elif plug_data.get("ip_address"):
result = await db.execute(select(SmartPlug).where(SmartPlug.ip_address == plug_data["ip_address"]))
existing = result.scalar_one_or_none()
@@ -1290,12 +1305,21 @@ async def import_backup(
existing.ha_power_entity = plug_data.get("ha_power_entity")
existing.ha_energy_today_entity = plug_data.get("ha_energy_today_entity")
existing.ha_energy_total_entity = plug_data.get("ha_energy_total_entity")
# MQTT fields
# MQTT fields (legacy)
existing.mqtt_topic = plug_data.get("mqtt_topic")
existing.mqtt_power_path = plug_data.get("mqtt_power_path")
existing.mqtt_energy_path = plug_data.get("mqtt_energy_path")
existing.mqtt_state_path = plug_data.get("mqtt_state_path")
existing.mqtt_multiplier = plug_data.get("mqtt_multiplier", 1.0)
# MQTT power fields
existing.mqtt_power_topic = plug_data.get("mqtt_power_topic")
existing.mqtt_power_path = plug_data.get("mqtt_power_path")
existing.mqtt_power_multiplier = plug_data.get("mqtt_power_multiplier", 1.0)
# MQTT energy fields
existing.mqtt_energy_topic = plug_data.get("mqtt_energy_topic")
existing.mqtt_energy_path = plug_data.get("mqtt_energy_path")
existing.mqtt_energy_multiplier = plug_data.get("mqtt_energy_multiplier", 1.0)
# MQTT state fields
existing.mqtt_state_topic = plug_data.get("mqtt_state_topic")
existing.mqtt_state_path = plug_data.get("mqtt_state_path")
existing.mqtt_state_on_value = plug_data.get("mqtt_state_on_value")
existing.printer_id = printer_id
existing.enabled = plug_data.get("enabled", True)
existing.auto_on = plug_data.get("auto_on", True)
@@ -1325,12 +1349,21 @@ async def import_backup(
ha_power_entity=plug_data.get("ha_power_entity"),
ha_energy_today_entity=plug_data.get("ha_energy_today_entity"),
ha_energy_total_entity=plug_data.get("ha_energy_total_entity"),
# MQTT fields
# MQTT fields (legacy)
mqtt_topic=plug_data.get("mqtt_topic"),
mqtt_power_path=plug_data.get("mqtt_power_path"),
mqtt_energy_path=plug_data.get("mqtt_energy_path"),
mqtt_state_path=plug_data.get("mqtt_state_path"),
mqtt_multiplier=plug_data.get("mqtt_multiplier", 1.0),
# MQTT power fields
mqtt_power_topic=plug_data.get("mqtt_power_topic"),
mqtt_power_path=plug_data.get("mqtt_power_path"),
mqtt_power_multiplier=plug_data.get("mqtt_power_multiplier", 1.0),
# MQTT energy fields
mqtt_energy_topic=plug_data.get("mqtt_energy_topic"),
mqtt_energy_path=plug_data.get("mqtt_energy_path"),
mqtt_energy_multiplier=plug_data.get("mqtt_energy_multiplier", 1.0),
# MQTT state fields
mqtt_state_topic=plug_data.get("mqtt_state_topic"),
mqtt_state_path=plug_data.get("mqtt_state_path"),
mqtt_state_on_value=plug_data.get("mqtt_state_on_value"),
printer_id=printer_id,
enabled=plug_data.get("enabled", True),
auto_on=plug_data.get("auto_on", True),
+87 -20
View File
@@ -96,17 +96,44 @@ async def create_smart_plug(
await db.commit()
await db.refresh(plug)
# Subscribe MQTT plugs to their topic
if plug.plug_type == "mqtt" and plug.mqtt_topic:
mqtt_relay.smart_plug_service.subscribe(
plug_id=plug.id,
topic=plug.mqtt_topic,
power_path=plug.mqtt_power_path,
energy_path=plug.mqtt_energy_path,
state_path=plug.mqtt_state_path,
multiplier=plug.mqtt_multiplier or 1.0,
)
logger.info(f"Created MQTT plug '{plug.name}' subscribed to {plug.mqtt_topic}")
# Subscribe MQTT plugs to their topics
if plug.plug_type == "mqtt":
# Determine effective topics (new fields take priority, fall back to legacy)
power_topic = plug.mqtt_power_topic or plug.mqtt_topic
energy_topic = plug.mqtt_energy_topic or plug.mqtt_topic
state_topic = plug.mqtt_state_topic or plug.mqtt_topic
# Only subscribe if at least one data source is configured
if (
(power_topic and plug.mqtt_power_path)
or (energy_topic and plug.mqtt_energy_path)
or (state_topic and plug.mqtt_state_path)
):
mqtt_relay.smart_plug_service.subscribe(
plug_id=plug.id,
# Power source
power_topic=power_topic if plug.mqtt_power_path else None,
power_path=plug.mqtt_power_path,
power_multiplier=plug.mqtt_power_multiplier or plug.mqtt_multiplier or 1.0,
# Energy source
energy_topic=energy_topic if plug.mqtt_energy_path else None,
energy_path=plug.mqtt_energy_path,
energy_multiplier=plug.mqtt_energy_multiplier or plug.mqtt_multiplier or 1.0,
# State source
state_topic=state_topic if plug.mqtt_state_path else None,
state_path=plug.mqtt_state_path,
state_on_value=plug.mqtt_state_on_value,
)
topics = [
t
for t in [
power_topic if plug.mqtt_power_path else None,
energy_topic if plug.mqtt_energy_path else None,
state_topic if plug.mqtt_state_path else None,
]
if t
]
logger.info(f"Created MQTT plug '{plug.name}' subscribed to {', '.join(set(topics))}")
elif plug.plug_type == "homeassistant":
logger.info(f"Created Home Assistant plug '{plug.name}' ({plug.ha_entity_id})")
else:
@@ -338,9 +365,19 @@ async def update_smart_plug(
if result.scalar_one_or_none():
raise HTTPException(400, "This printer already has a smart plug assigned")
# Check if MQTT topic is changing - need to resubscribe
old_topic = plug.mqtt_topic
# Track old MQTT settings for comparison
old_plug_type = plug.plug_type
old_mqtt_config = {
"power_topic": plug.mqtt_power_topic or plug.mqtt_topic,
"power_path": plug.mqtt_power_path,
"power_multiplier": plug.mqtt_power_multiplier,
"energy_topic": plug.mqtt_energy_topic or plug.mqtt_topic,
"energy_path": plug.mqtt_energy_path,
"energy_multiplier": plug.mqtt_energy_multiplier,
"state_topic": plug.mqtt_state_topic or plug.mqtt_topic,
"state_path": plug.mqtt_state_path,
"state_on_value": plug.mqtt_state_on_value,
}
for field, value in update_data.items():
setattr(plug, field, value)
@@ -353,20 +390,50 @@ async def update_smart_plug(
# Changed away from MQTT - unsubscribe
mqtt_relay.smart_plug_service.unsubscribe(plug.id)
elif plug.plug_type == "mqtt":
# Is now MQTT - check if topic changed or newly MQTT
if old_plug_type != "mqtt" or old_topic != plug.mqtt_topic:
# Unsubscribe from old topic first
# Check if any MQTT config changed
new_mqtt_config = {
"power_topic": plug.mqtt_power_topic or plug.mqtt_topic,
"power_path": plug.mqtt_power_path,
"power_multiplier": plug.mqtt_power_multiplier,
"energy_topic": plug.mqtt_energy_topic or plug.mqtt_topic,
"energy_path": plug.mqtt_energy_path,
"energy_multiplier": plug.mqtt_energy_multiplier,
"state_topic": plug.mqtt_state_topic or plug.mqtt_topic,
"state_path": plug.mqtt_state_path,
"state_on_value": plug.mqtt_state_on_value,
}
mqtt_changed = old_plug_type != "mqtt" or old_mqtt_config != new_mqtt_config
if mqtt_changed:
# Unsubscribe from old topics first
if old_plug_type == "mqtt":
mqtt_relay.smart_plug_service.unsubscribe(plug.id)
# Subscribe to new topic
if plug.mqtt_topic:
# Subscribe to new topics
power_topic = plug.mqtt_power_topic or plug.mqtt_topic
energy_topic = plug.mqtt_energy_topic or plug.mqtt_topic
state_topic = plug.mqtt_state_topic or plug.mqtt_topic
if (
(power_topic and plug.mqtt_power_path)
or (energy_topic and plug.mqtt_energy_path)
or (state_topic and plug.mqtt_state_path)
):
mqtt_relay.smart_plug_service.subscribe(
plug_id=plug.id,
topic=plug.mqtt_topic,
# Power source
power_topic=power_topic if plug.mqtt_power_path else None,
power_path=plug.mqtt_power_path,
power_multiplier=plug.mqtt_power_multiplier or plug.mqtt_multiplier or 1.0,
# Energy source
energy_topic=energy_topic if plug.mqtt_energy_path else None,
energy_path=plug.mqtt_energy_path,
energy_multiplier=plug.mqtt_energy_multiplier or plug.mqtt_multiplier or 1.0,
# State source
state_topic=state_topic if plug.mqtt_state_path else None,
state_path=plug.mqtt_state_path,
multiplier=plug.mqtt_multiplier or 1.0,
state_on_value=plug.mqtt_state_on_value,
)
logger.info(f"Updated smart plug '{plug.name}'")