modbus: add explicit cascade delete (device + readings + toggles + HA cleanup)
A disabled device with historical readings still cannot be deleted by default
(409 guard stays, to prevent accidental data loss). A new explicit opt-in lets
the user remove it for real:
- DELETE /api/modbus/devices/{uuid}?cascade=true removes the device's readings
and exposed_entity_toggle rows, then the device, in one transaction (FK order:
readings before device). Best-effort clears the HA discovery configs first
(MQTT not connected -> no-op, never blocks the delete). Default (no cascade)
unchanged. Returns 200 with deletion counts.
- Frontend: a 'Force Delete (all data)' button appears only after the 409, with
a red irreversible warning; cascade is sent only on that explicit action.
- Also corrected a stale comment: FK RESTRICT IS enforced at runtime now.
This commit is contained in:
@@ -407,6 +407,74 @@ def publish_device_state(session: Session, device: Any) -> None:
|
||||
)
|
||||
|
||||
|
||||
def clear_device_discovery(session: Session, device_uuid: str) -> None:
|
||||
"""Best-effort: send empty retained payloads to all HA Discovery config topics
|
||||
for the given device, effectively removing the device's entities from HA.
|
||||
|
||||
This must be called **before** the device rows are deleted from the DB, so
|
||||
that ``build_catalog`` can still enumerate the device's entities.
|
||||
|
||||
Behaviour
|
||||
---------
|
||||
- No-op if MQTT / HA Discovery is not enabled or the MQTT client is not
|
||||
connected.
|
||||
- All exceptions are caught internally; this function never raises.
|
||||
The caller proceeds with the DB deletion regardless of MQTT outcome.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
session:
|
||||
Active SQLAlchemy session (device must still exist in DB at call time).
|
||||
device_uuid:
|
||||
UUID string of the device being deleted.
|
||||
"""
|
||||
from app.config import get_settings
|
||||
settings = build_runtime_settings(session, get_settings())
|
||||
if not _should_publish(settings):
|
||||
logger.debug(
|
||||
"clear_device_discovery: skipped (MQTT/Discovery not enabled or not connected)"
|
||||
)
|
||||
return
|
||||
|
||||
discovery_prefix = settings.ha_discovery_prefix
|
||||
state_prefix = settings.ha_state_topic_prefix
|
||||
|
||||
try:
|
||||
catalog = build_catalog(session)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"clear_device_discovery: failed to build catalog for device uuid=%s; skipping HA cleanup",
|
||||
device_uuid,
|
||||
)
|
||||
return
|
||||
|
||||
cleared = 0
|
||||
for entry in catalog:
|
||||
entity = entry.entity
|
||||
# Only clear entities belonging to this device.
|
||||
if entity.device.identifiers[1] != device_uuid:
|
||||
continue
|
||||
try:
|
||||
topic, _config = build_discovery_payload(entity, discovery_prefix, state_prefix)
|
||||
mqtt_manager.publish(topic, b"", retain=True)
|
||||
cleared += 1
|
||||
logger.debug(
|
||||
"clear_device_discovery: cleared config topic for entity %r → %s",
|
||||
entity.key,
|
||||
topic,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"clear_device_discovery: error clearing entity %r; continuing", entity.key
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"clear_device_discovery: cleared %d HA discovery topic(s) for device uuid=%s",
|
||||
cleared,
|
||||
device_uuid,
|
||||
)
|
||||
|
||||
|
||||
def publish_device_offline(session: Session, device_uuid: str) -> None:
|
||||
"""Publish an "offline" availability payload for *device_uuid*.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user