From 459a7f023a8962dedae7ff63cd6ab7cc5beda6ee Mon Sep 17 00:00:00 2001 From: Jeff Culverhouse Date: Tue, 18 Nov 2025 11:04:06 -0500 Subject: [PATCH] feat!: move recording_url and motion_region to attributes these are no longer sensors themselves but attributes of the motion sensor and they update when a change to motion happens also, store_recording_in_media now returns the file name instead of the full path --- src/amcrest2mqtt/mixins/amcrest.py | 24 +++++----------------- src/amcrest2mqtt/mixins/events.py | 7 +++++-- src/amcrest2mqtt/mixins/helpers.py | 32 +++--------------------------- src/amcrest2mqtt/mixins/publish.py | 6 +++++- 4 files changed, 18 insertions(+), 51 deletions(-) diff --git a/src/amcrest2mqtt/mixins/amcrest.py b/src/amcrest2mqtt/mixins/amcrest.py index 0463a1f..521894f 100644 --- a/src/amcrest2mqtt/mixins/amcrest.py +++ b/src/amcrest2mqtt/mixins/amcrest.py @@ -138,19 +138,12 @@ class AmcrestMixin: "name": "Motion", "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion"), "stat_t": self.mqtt_helper.stat_t(device_id, "binary_sensor", "motion"), - "jsn_atr_t": self.mqtt_helper.stat_t(device_id, "attributes"), + "json_attributes_topic": self.mqtt_helper.stat_t(device_id, "attributes"), "payload_on": True, "payload_off": False, "device_class": "motion", "icon": "mdi:eye-outline", }, - "motion_region": { - "p": "sensor", - "name": "Motion region", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion_region"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "motion_region"), - "icon": "mdi:map-marker", - }, "motion_snapshot": { "p": "image", "name": "Motion snapshot", @@ -243,15 +236,6 @@ class AmcrestMixin: }, } - if "media" in self.config and "media_source" in self.config["media"]: - device["cmps"]["recording_url"] = { - "p": "sensor", - "name": "Recording url", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "recording_url"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "recording_url"), - "icon": "mdi:web", - } - if camera.get("is_doorbell", None): device["cmps"]["doorbell"] = { "p": "binary_sensor", @@ -283,8 +267,10 @@ class AmcrestMixin: webrtc=rtc_url, switch={"save_recordings": "ON" if "path" in self.config["media"] else "OFF"}, binary_sensor={"motion": False}, - sensor={"motion_region": ""}, - attributes={"recording_url": f"{self.config["media"]["media_source"]}/{camera["device_name"]}-latest.mp4"}, + attributes={ + "recording_url": f"{self.config["media"]["media_source"]}/{camera["device_name"]}-latest.mp4", + "region": "", + }, image={"motion_snapshot": ""}, ) diff --git a/src/amcrest2mqtt/mixins/events.py b/src/amcrest2mqtt/mixins/events.py index c2b551e..4483702 100644 --- a/src/amcrest2mqtt/mixins/events.py +++ b/src/amcrest2mqtt/mixins/events.py @@ -32,7 +32,10 @@ class EventsMixin: event += ": snapshot" elif payload["file"].endswith(".mp4"): if "path" in self.config["media"] and self.states[device_id]["switch"].get("save_recordings", "OFF") == "ON": - await self.store_recording_in_media(device_id, payload["file"]) + file_name = await self.store_recording_in_media(device_id, payload["file"]) + if file_name: + self.upsert_state(device_id, attributes={"recording_url": f"{self.config["media"]["media_source"]}/{file_name}"}) + needs_publish.add(device_id) event += ": video" elif event == "motion": region = payload["region"] if payload["state"] != "off" else "" @@ -41,7 +44,7 @@ class EventsMixin: self.upsert_state( device_id, binary_sensor={"motion": payload["state"]}, - sensor={"motion_region": region}, + attributes={"region": region}, ) needs_publish.add(device_id) event += motion diff --git a/src/amcrest2mqtt/mixins/helpers.py b/src/amcrest2mqtt/mixins/helpers.py index 41e89e4..d9c47e2 100644 --- a/src/amcrest2mqtt/mixins/helpers.py +++ b/src/amcrest2mqtt/mixins/helpers.py @@ -12,7 +12,7 @@ import threading from types import FrameType import yaml from pathlib import Path -from datetime import datetime, timezone +from datetime import datetime from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: @@ -272,11 +272,7 @@ class HelpersMixin: self.logger.error(f"failed to save recording to {file_path}: {err!r}") return None - self.upsert_state( - device_id, - media={"recording": str(file_path)}, - sensor={"recording_time": datetime.now(timezone.utc).isoformat()}, - ) + # update the latest symlink local_file = Path(f"./{file_name}") latest_link = Path(f"{path}/{name}-latest.mp4") @@ -288,29 +284,7 @@ class HelpersMixin: self.logger.error(f"failed to save symlink {latest_link} -> {local_file}: {err!r}") pass - if "media_source" in self.config["media"]: - url = f"{self.config["media"]["media_source"]}/{file_name}" - self.upsert_state(device_id, sensor={"recording_url": url}) - return url - - self.upsert_state( - device_id, - media={"recording": file_path}, - sensor={"recording_time": datetime.now(timezone.utc).isoformat()}, - ) - - # update symlink to "lastest" recording - local_file = Path(f"./{file_name}") - latest_link = Path(f"{path}/{name}-latest.mp4") - if latest_link.is_symlink(): - latest_link.unlink() - latest_link.symlink_to(local_file) - - if "media_source" in self.config["media"]: - url = f"{self.config["media"]["media_source"]}/{file_name}" - self.upsert_state(device_id, sensor={"recording_url": url}) - return url - return None + return file_name def handle_signal(self: Amcrest2Mqtt, signum: int, _: FrameType | None) -> Any: sig_name = signal.Signals(signum).name diff --git a/src/amcrest2mqtt/mixins/publish.py b/src/amcrest2mqtt/mixins/publish.py index 46ef4ac..e5494bf 100644 --- a/src/amcrest2mqtt/mixins/publish.py +++ b/src/amcrest2mqtt/mixins/publish.py @@ -169,7 +169,11 @@ class PublishMixin: for state, value in self.states[device_id].items(): if subject and state != subject: continue - if isinstance(value, dict): + # Attributes need to be published as a single JSON object to the attributes topic + if state == "attributes" and isinstance(value, dict): + topic = self.mqtt_helper.stat_t(device_id, "attributes") + await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, json.dumps(value)) + elif isinstance(value, dict): for k, v in value.items(): if sub and k != sub: continue