diff --git a/src/amcrest2mqtt/interface.py b/src/amcrest2mqtt/interface.py index 46f1305..553f8e4 100644 --- a/src/amcrest2mqtt/interface.py +++ b/src/amcrest2mqtt/interface.py @@ -79,7 +79,7 @@ class AmcrestServiceProtocol(Protocol): async def process_device_event(self, device_id: str, code: str, payload: Any) -> None: ... async def publish_device_availability(self, device_id: str, online: bool = True) -> None: ... async def publish_device_discovery(self, device_id: str) -> None: ... - async def publish_device_state(self, device_id: str) -> None: ... + async def publish_device_state(self, device_id: str, subject: str = "", sub: str = "") -> None: ... async def publish_service_availability(self, status: str = "online") -> None: ... async def publish_service_discovery(self) -> None: ... async def publish_service_state(self) -> None: ... @@ -100,7 +100,7 @@ class AmcrestServiceProtocol(Protocol): def b_to_gb(self, total: int) -> float: ... def b_to_mb(self, total: int) -> float: ... def classify_device(self, device: dict) -> str: ... - def get_component_type(self, device_id: str) -> str: ... + def get_platform(self, device_id: str) -> str: ... def get_component(self, device_id: str) -> dict[str, Any]: ... def get_device_availability_topic(self, device_id: str) -> str: ... def get_device_image_topic(self, device_id: str) -> str: ... diff --git a/src/amcrest2mqtt/mixins/amcrest.py b/src/amcrest2mqtt/mixins/amcrest.py index 776bc38..c279148 100644 --- a/src/amcrest2mqtt/mixins/amcrest.py +++ b/src/amcrest2mqtt/mixins/amcrest.py @@ -71,34 +71,11 @@ class AmcrestMixin: self.logger.error(f"device you specified is not a supported model: {device["device_type"]}") return "" - async def build_camera(self: Amcrest2Mqtt, device: dict) -> str: - raw_id = cast(str, device["serial_number"]) + async def build_camera(self: Amcrest2Mqtt, camera: dict) -> str: + raw_id = cast(str, camera["serial_number"]) device_id = raw_id - component = { - "component_type": "camera", - "name": device["device_name"], - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "video"), - "topic": self.mqtt_helper.stat_t(device_id, "video"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "icon": "mdi:video", - "via_device": self.mqtt_helper.service_slug, - "device": { - "name": device["device_name"], - "identifiers": [self.mqtt_helper.device_slug(device_id)], - "manufacturer": device["vendor"], - "model": device["device_type"], - "sw_version": device["software_version"], - "hw_version": device["hardware_version"], - "connections": [ - ["host", device["host"]], - ["mac", device["network"]["mac"]], - ["ip address", device["network"]["ip_address"]], - ], - "configuration_url": f"http://{device['host']}/", - "serial_number": device["serial_number"], - }, - } + rtc_url = "" if "webrtc" in self.amcrest_config: webrtc_config = self.amcrest_config["webrtc"] @@ -109,247 +86,209 @@ class AmcrestMixin: if rtc_source: rtc_url = f"http://{rtc_host}:{rtc_port}/{rtc_link}?src={rtc_source}" - component["url_topic"] = rtc_url - - modes = {} - - device_block = self.mqtt_helper.device_block( - device["device_name"], - self.mqtt_helper.device_slug(device_id), - device["vendor"], - device["software_version"], - ) - - modes["reboot"] = { - "component_type": "button", - "name": "Reboot", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "reboot"), - "cmd_t": self.mqtt_helper.cmd_t(device_id, "button", "reboot"), - "payload_press": "PRESS", - "icon": "mdi:restart", - "entity_category": "diagnostic", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["snapshot"] = { - "component_type": "image", - "name": "Timed snapshot", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "snapshot"), - "image_topic": self.mqtt_helper.stat_t(device_id, "snapshot"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "image_encoding": "b64", - "content_type": "image/jpeg", - "icon": "mdi:camera", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["recording_time"] = { - "component_type": "sensor", - "name": "Recording time", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "recording_time"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "recording_time"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "device_class": "timestamp", - "icon": "mdi:clock", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["recording_url"] = { - "component_type": "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"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "clip_url": f"media-source://media_source/local/Videos/amcrest/{device["device_name"]}-latest.mp4", - "icon": "mdi:web", - "enabled_by_default": False, - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["privacy"] = { - "component_type": "switch", - "name": "Privacy mode", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "privacy"), - "stat_t": self.mqtt_helper.stat_t(device_id, "switch", "privacy"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "cmd_t": self.mqtt_helper.cmd_t(device_id, "switch", "privacy"), - "payload_on": "ON", - "payload_off": "OFF", - "device_class": "switch", - "icon": "mdi:camera-outline", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["motion_detection"] = { - "component_type": "switch", - "name": "Motion detection", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion_detection"), - "stat_t": self.mqtt_helper.stat_t(device_id, "switch", "motion_detection"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "cmd_t": self.mqtt_helper.cmd_t(device_id, "switch", "motion_detection"), - "payload_on": "ON", - "payload_off": "OFF", - "device_class": "switch", - "icon": "mdi:motion-sensor", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["save_recordings"] = { - "component_type": "switch", - "name": "Save recordings", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "save_recordings"), - "stat_t": self.mqtt_helper.stat_t(device_id, "switch", "save_recordings"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "cmd_t": self.mqtt_helper.cmd_t(device_id, "switch", "save_recordings"), - "payload_on": "ON", - "payload_off": "OFF", - "device_class": "switch", - "icon": "mdi:content-save-outline", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["motion"] = { - "component_type": "binary_sensor", - "name": "Motion sensor", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion"), - "stat_t": self.mqtt_helper.stat_t(device_id, "binary_sensor", "motion"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "payload_on": True, - "payload_off": False, - "device_class": "motion", - "icon": "mdi:eye-outline", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["motion_region"] = { - "component_type": "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"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "icon": "mdi:map-marker", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["motion_snapshot"] = { - "component_type": "image", - "name": "Motion snapshot", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion_snapshot"), - "image_topic": self.mqtt_helper.stat_t(device_id, "motion_snapshot"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "image_encoding": "b64", - "content_type": "image/jpeg", - "icon": "mdi:camera", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["storage_used"] = { - "component_type": "sensor", - "name": "Storage used", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "storage_used"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "storage_used"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "device_class": "data_size", - "state_class": "measurement", - "unit_of_measurement": "GB", - "entity_category": "diagnostic", - "icon": "mdi:micro-sd", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - - modes["storage_used_pct"] = { - "component_type": "sensor", - "name": "Storage used %", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "storage_used_pct"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "storage_used_pct"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "state_class": "measurement", - "unit_of_measurement": "%", - "entity_category": "diagnostic", - "icon": "mdi:micro-sd", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - modes["storage_total"] = { - "component_type": "sensor", - "name": "Storage total", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "storage_total"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "storage_total"), + device = { + "platform": "mqtt", + "stat_t": self.mqtt_helper.stat_t(device_id, "state"), "avty_t": self.mqtt_helper.avty_t(device_id), - "device_class": "data_size", - "state_class": "measurement", - "unit_of_measurement": "GB", - "entity_category": "diagnostic", - "icon": "mdi:micro-sd", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, + "device": { + "name": camera["device_name"], + "identifiers": [ + self.mqtt_helper.device_slug(device_id), + camera["serial_number"], + ], + "manufacturer": camera["vendor"], + "model": camera["device_type"], + "sw_version": camera["software_version"], + "hw_version": camera["hardware_version"], + "connections": [ + ["host", camera["host"]], + ["mac", camera["network"]["mac"]], + ["ip address", camera["network"]["ip_address"]], + ], + "configuration_url": f"http://{camera['host']}/", + "serial_number": camera["serial_number"], + "via_device": self.service, + }, + "origin": {"name": self.service_name, "sw": self.config["version"], "support_url": "https://github.com/weirdTangent/amcrest2mqtt"}, + "qos": self.qos, + "cmps": { + "video": { + "platform": "camera", + "name": "Video", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "video"), + "topic": self.mqtt_helper.stat_t(device_id, "camera", "video"), + "icon": "mdi:video", + "web_url": rtc_url, + }, + "reboot": { + "platform": "button", + "name": "Reboot", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "reboot"), + "cmd_t": self.mqtt_helper.cmd_t(device_id, "button", "reboot"), + "payload_press": "PRESS", + "icon": "mdi:restart", + "entity_category": "diagnostic", + }, + "snapshot": { + "platform": "image", + "name": "Timed snapshot", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "snapshot"), + "image_topic": self.mqtt_helper.stat_t(device_id, "image", "snapshot"), + "image_encoding": "b64", + "content_type": "image/jpeg", + "icon": "mdi:camera", + }, + "recording": { + "platform": "sensor", + "name": "Last recording", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "recording"), + "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "recording"), + "device_class": "timestamp", + "icon": "mdi:clock", + }, + "privacy": { + "platform": "switch", + "name": "Privacy mode", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "privacy"), + "stat_t": self.mqtt_helper.stat_t(device_id, "switch", "privacy"), + "cmd_t": self.mqtt_helper.cmd_t(device_id, "switch", "privacy"), + "payload_on": "ON", + "payload_off": "OFF", + "device_class": "switch", + "icon": "mdi:camera-outline", + }, + "motion_detection": { + "platform": "switch", + "name": "Motion detection", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion_detection"), + "stat_t": self.mqtt_helper.stat_t(device_id, "switch", "motion_detection"), + "cmd_t": self.mqtt_helper.cmd_t(device_id, "switch", "motion_detection"), + "payload_on": "ON", + "payload_off": "OFF", + "device_class": "switch", + "icon": "mdi:motion-sensor", + }, + "save_recordings": { + "platform": "switch", + "name": "Save recordings", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "save_recordings"), + "stat_t": self.mqtt_helper.stat_t(device_id, "switch", "save_recordings"), + "cmd_t": self.mqtt_helper.cmd_t(device_id, "switch", "save_recordings"), + "payload_on": "ON", + "payload_off": "OFF", + "device_class": "switch", + "icon": "mdi:content-save-outline", + }, + "motion": { + "platform": "binary_sensor", + "name": "Motion sensor", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion"), + "stat_t": self.mqtt_helper.stat_t(device_id, "binary_sensor", "motion"), + "payload_on": True, + "payload_off": False, + "device_class": "motion", + "icon": "mdi:eye-outline", + }, + "motion_region": { + "platform": "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": { + "platform": "image", + "name": "Motion snapshot", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "motion_snapshot"), + "image_topic": self.mqtt_helper.stat_t(device_id, "image", "motion_snapshot"), + "image_encoding": "b64", + "content_type": "image/jpeg", + "icon": "mdi:camera", + }, + "storage_used": { + "platform": "sensor", + "name": "Storage used", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "storage_used"), + "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "storage_used"), + "device_class": "data_size", + "state_class": "measurement", + "unit_of_measurement": "GB", + "entity_category": "diagnostic", + "icon": "mdi:micro-sd", + }, + "storage_used_pct": { + "platform": "sensor", + "name": "Storage used %", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "storage_used_pct"), + "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "storage_used_pct"), + "state_class": "measurement", + "unit_of_measurement": "%", + "entity_category": "diagnostic", + "icon": "mdi:micro-sd", + }, + "storage_total": { + "platform": "sensor", + "name": "Storage total", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "storage_total"), + "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "storage_total"), + "device_class": "data_size", + "state_class": "measurement", + "unit_of_measurement": "GB", + "entity_category": "diagnostic", + "icon": "mdi:micro-sd", + }, + "event_text": { + "platform": "sensor", + "name": "Last event", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "event_text"), + "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "event_text"), + "icon": "mdi:note", + }, + "event": { + "platform": "sensor", + "name": "Last event time", + "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "event"), + "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "event"), + "device_class": "timestamp", + "icon": "mdi:clock", + }, + }, } - modes["event_text"] = { - "component_type": "sensor", - "name": "Last event", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "event_text"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "event_text"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "icon": "mdi:note", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } - modes["event_time"] = { - "component_type": "sensor", - "name": "Last event time", - "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "event_time"), - "stat_t": self.mqtt_helper.stat_t(device_id, "sensor", "event_time"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "device_class": "timestamp", - "icon": "mdi:clock", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, - } + if "media" in self.config and "media_source" in self.config["media"]: + device["cmps"]["recording_url"] = { + "platform": "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"), + "clip_url": f"{self.config["media"]["media_source"]}/{camera["device_name"]}-latest.mp4", + "icon": "mdi:web", + "enabled_by_default": False, + } - if device.get("is_doorbell", None): - modes["doorbell"] = { - "component_type": "binary_sensor", - "name": "Doorbell" if device["device_name"] == "Doorbell" else f"{device["device_name"]} Doorbell", + if camera.get("is_doorbell", None): + device["cmps"]["doorbell"] = { + "platform": "binary_sensor", + "name": "Doorbell" if camera["device_name"] == "Doorbell" else f"{camera["device_name"]} Doorbell", "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "doorbell"), "stat_t": self.mqtt_helper.stat_t(device_id, "binary_sensor", "doorbell"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "payload_on": "on", - "payload_off": "off", + "payload_on": "ON", + "payload_off": "OFF", "icon": "mdi:doorbell", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, } - if device.get("is_ad410", None): - modes["human"] = { - "component_type": "binary_sensor", + if camera.get("is_ad410", None): + device["cmps"]["human"] = { + "platform": "binary_sensor", "name": "Human Sensor", "uniq_id": self.mqtt_helper.dev_unique_id(device_id, "human"), "stat_t": self.mqtt_helper.stat_t(device_id, "binary_sensor", "human"), - "avty_t": self.mqtt_helper.avty_t(device_id), - "payload_on": "on", - "payload_off": "off", + "payload_on": "ON", + "payload_off": "OFF", "icon": "mdi:person", - "via_device": self.mqtt_helper.service_slug, - "device": device_block, } - # store device and any "modes" - self.upsert_device(device_id, component=component, modes=modes) - # defaults - which build_device_states doesn't update (events do) self.upsert_state( device_id, @@ -369,16 +308,13 @@ class AmcrestMixin: "storage_total": 0, "storage_used_pct": 0, "motion_region": "n/a", - "event_text": "", - "event_time": None, - "recording_time": None, - "recording_url": "", }, ) + self.upsert_device(device_id, component=device, modes={k: v for k, v in device["cmps"].items()}) await self.build_device_states(device_id) - if not self.states[device_id]["internal"].get("discovered", None): - self.logger.info(f'added new camera: "{device["device_name"]}" {device["vendor"]} {device["device_type"]}] ({device_id})') + if not self.is_discovered(device_id): + self.logger.info(f'added new camera: "{camera["device_name"]}" {camera["vendor"]} {camera["device_type"]}] ({device_id})') await self.publish_device_discovery(device_id) await self.publish_device_availability(device_id, online=True) diff --git a/src/amcrest2mqtt/mixins/amcrest_api.py b/src/amcrest2mqtt/mixins/amcrest_api.py index 822bb6c..9041f83 100644 --- a/src/amcrest2mqtt/mixins/amcrest_api.py +++ b/src/amcrest2mqtt/mixins/amcrest_api.py @@ -12,9 +12,9 @@ from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: from amcrest2mqtt.interface import AmcrestServiceProtocol as Amcrest2Mqtt -SNAPSHOT_TIMEOUT_S = 10 +SNAPSHOT_TIMEOUT_S = 20 SNAPSHOT_MAX_TRIES = 3 -SNAPSHOT_BASE_BACKOFF_S = 5 +SNAPSHOT_BASE_BACKOFF_S = 8 class AmcrestAPIMixin: @@ -117,7 +117,11 @@ class AmcrestAPIMixin: build = sw_info[1].strip() sw_version = f"{version} ({build})" - network_config = dict(item.split("=", 1) for item in net_config[0].splitlines() if "=" in item) + if isinstance(net_config, tuple): + net_config_str = cast(str, net_config[0]) + else: + net_config_str = cast(str, net_config) + network_config = dict(item.split("=", 1) for item in net_config_str.splitlines() if "=" in item) interface = network_config.get("table.Network.DefaultInterface") if not interface: diff --git a/src/amcrest2mqtt/mixins/publish.py b/src/amcrest2mqtt/mixins/publish.py index 3cbd718..c3301ed 100644 --- a/src/amcrest2mqtt/mixins/publish.py +++ b/src/amcrest2mqtt/mixins/publish.py @@ -1,7 +1,7 @@ import asyncio from datetime import timezone import json -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING if TYPE_CHECKING: from amcrest2mqtt.interface import AmcrestServiceProtocol as Amcrest2Mqtt @@ -12,172 +12,103 @@ class PublishMixin: # Service ------------------------------------------------------------------------------------- async def publish_service_discovery(self: Amcrest2Mqtt) -> None: - device_block = self.mqtt_helper.device_block( - self.service_name, - self.mqtt_helper.service_slug, - "weirdTangent", - self.config["version"], - ) - - self.logger.debug("publishing service entity") - self.mqtt_helper.safe_publish( - topic=self.mqtt_helper.disc_t("binary_sensor", "service"), - payload=json.dumps( - { + device_id = "service" + + device = { + "platform": "mqtt", + "stat_t": self.mqtt_helper.stat_t(device_id, "service"), + "cmd_t": self.mqtt_helper.cmd_t(device_id), + "avty_t": self.mqtt_helper.avty_t(device_id), + "device": { + "name": self.service_name, + "identifiers": [ + self.mqtt_helper.service_slug, + ], + "manufacturer": "weirdTangent", + "sw_version": self.config["version"], + }, + "origin": { + "name": self.service_name, + "sw_version": self.config["version"], + "support_url": "https://github.com/weirdtangent/amcrest2mqtt", + }, + "qos": self.qos, + "cmps": { + "server": { + "platform": "binary_sensor", "name": self.service_name, - "uniq_id": self.mqtt_helper.svc_unique_id("service"), - "stat_t": self.mqtt_helper.avty_t("service"), - "avty_t": self.mqtt_helper.avty_t("service"), + "uniq_id": self.mqtt_helper.svc_unique_id("server"), + "stat_t": self.mqtt_helper.stat_t(device_id, "service", "server"), "payload_on": "online", "payload_off": "offline", "device_class": "connectivity", "entity_category": "diagnostic", "icon": "mdi:server", - "device": device_block, - "origin": { - "name": self.service_name, - "sw_version": self.config["version"], - "support_url": "https://github.com/weirdtangent/amcrest2mqtt", - }, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) - - self.mqtt_helper.safe_publish( - topic=self.mqtt_helper.disc_t("sensor", "api_calls"), - payload=json.dumps( - { - "name": f"{self.service_name} API Calls Today", + }, + "api_calls": { + "platform": "sensor", + "name": "API calls today", "uniq_id": self.mqtt_helper.svc_unique_id("api_calls"), - "stat_t": self.mqtt_helper.stat_t("service", "service", "api_calls"), - "avty_t": self.mqtt_helper.avty_t("service"), + "stat_t": self.mqtt_helper.stat_t(device_id, "service", "api_calls"), "unit_of_measurement": "calls", "entity_category": "diagnostic", - "icon": "mdi:api", "state_class": "total_increasing", - "device": device_block, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) - self.mqtt_helper.safe_publish( - topic=self.mqtt_helper.disc_t("binary_sensor", "rate_limited"), - payload=json.dumps( - { - "name": f"{self.service_name} Rate Limited by Amcrest", + "icon": "mdi:api", + }, + "rate_limited": { + "platform": "binary_sensor", + "name": "Rate limited", "uniq_id": self.mqtt_helper.svc_unique_id("rate_limited"), - "stat_t": self.mqtt_helper.stat_t("service", "service", "rate_limited"), - "avty_t": self.mqtt_helper.avty_t("service"), + "stat_t": self.mqtt_helper.stat_t(device_id, "service", "rate_limited"), "payload_on": "YES", "payload_off": "NO", "device_class": "problem", "entity_category": "diagnostic", "icon": "mdi:speedometer-slow", - "device": device_block, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) - self.mqtt_helper.safe_publish( - topic=self.mqtt_helper.disc_t("sensor", "last_call_date"), - payload=json.dumps( - { - "name": f"{self.service_name} Last Device Check", - "uniq_id": self.mqtt_helper.svc_unique_id("last_call_date"), - "stat_t": self.mqtt_helper.stat_t("service", "service", "last_call_date"), - "avty_t": self.mqtt_helper.avty_t("service"), + }, + "last_call": { + "platform": "sensor", + "name": "Last device check", + "uniq_id": self.mqtt_helper.svc_unique_id("last_call"), + "stat_t": self.mqtt_helper.stat_t(device_id, "service", "last_call"), "device_class": "timestamp", "entity_category": "diagnostic", "icon": "mdi:clock-outline", - "device": device_block, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) - self.mqtt_helper.safe_publish( - topic=self.mqtt_helper.disc_t("number", "storage_refresh"), - payload=json.dumps( - { - "name": f"{self.service_name} Device Refresh Interval", - "uniq_id": self.mqtt_helper.svc_unique_id("storage_refresh"), - "stat_t": self.mqtt_helper.stat_t("service", "service", "storage_refresh"), - "avty_t": self.mqtt_helper.avty_t("service"), - "cmd_t": self.mqtt_helper.cmd_t("service", "storage_refresh"), + }, + "storage_interval": { + "platform": "number", + "name": "Refresh interval", + "uniq_id": self.mqtt_helper.svc_unique_id("storage_interval"), + "stat_t": self.mqtt_helper.stat_t(device_id, "service", "storage_interval"), + "cmd_t": self.mqtt_helper.cmd_t(device_id, "storage_interval"), "unit_of_measurement": "s", "min": 1, "max": 3600, "step": 1, "icon": "mdi:timer-refresh", "mode": "box", - "device": device_block, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) - self.mqtt_helper.safe_publish( - topic=self.mqtt_helper.disc_t("number", "device_list_refresh"), - payload=json.dumps( - { - "name": f"{self.service_name} Device List Refresh Interval", - "uniq_id": self.mqtt_helper.svc_unique_id("device_list_refresh"), - "stat_t": self.mqtt_helper.stat_t("service", "service", "device_list_refresh"), - "avty_t": self.mqtt_helper.avty_t("service"), - "cmd_t": self.mqtt_helper.cmd_t("service", "device_list_refresh"), - "unit_of_measurement": "s", - "min": 1, - "max": 3600, - "step": 1, - "icon": "mdi:format-list-bulleted", - "mode": "box", - "device": device_block, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) - self.mqtt_helper.safe_publish( - topic=self.mqtt_helper.disc_t("number", "snapshot_refresh"), - payload=json.dumps( - { - "name": f"{self.service_name} Snapshot Refresh Interval", - "uniq_id": self.mqtt_helper.svc_unique_id("snapshot_refresh"), - "stat_t": self.mqtt_helper.stat_t("service", "service", "snapshot_refresh"), - "avty_t": self.mqtt_helper.avty_t("service"), - "cmd_t": self.mqtt_helper.cmd_t("service", "snapshot_refresh"), + }, + "snapshot_interval": { + "platform": "number", + "name": "Snapshot interval", + "uniq_id": self.mqtt_helper.svc_unique_id("snapshot_interval"), + "stat_t": self.mqtt_helper.stat_t(device_id, "service", "snapshot_interval"), + "cmd_t": self.mqtt_helper.cmd_t("service", "snapshot_interval"), "unit_of_measurement": "m", "min": 1, "max": 60, "step": 1, "icon": "mdi:lightning-bolt", "mode": "box", - "device": device_block, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) - await asyncio.to_thread( - self.mqtt_helper.safe_publish, - topic=self.mqtt_helper.disc_t("button", "refresh_device_list"), - payload=json.dumps( - { - "name": f"{self.service_name} Refresh Device List", - "uniq_id": self.mqtt_helper.svc_unique_id("refresh_device_list"), - "cmd_t": self.mqtt_helper.cmd_t("service", "refresh_device_list", "command"), - "payload_press": "refresh", - "icon": "mdi:refresh", - "device": device_block, - } - ), - qos=self.mqtt_config["qos"], - retain=True, - ) + }, + }, + } + + topic = self.mqtt_helper.disc_t("device", device_id) + payload = {k: v for k, v in device.items() if k != "platform"} + await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, json.dumps(payload), retain=True) + self.upsert_state(device_id, internal={"discovered": True}) + self.logger.debug(f"discovery published for {self.service} ({self.mqtt_helper.service_slug})") async def publish_service_availability(self: Amcrest2Mqtt, status: str = "online") -> None: @@ -189,15 +120,14 @@ class PublishMixin: # to HomeAssistant as UTC last_call_date = self.last_call_date local_tz = last_call_date.astimezone().tzinfo - utc_dt = last_call_date.replace(tzinfo=local_tz).astimezone(timezone.utc) service = { + "server": "online", "api_calls": self.api_calls, - "last_call_date": utc_dt.isoformat(), + "last_call": last_call_date.replace(tzinfo=local_tz).astimezone(timezone.utc).isoformat(), + "storage_interval": self.device_interval, + "snapshot_interval": self.snapshot_update_interval, "rate_limited": "YES" if self.rate_limited else "NO", - "storage_refresh": self.device_interval, - "device_list_refresh": self.device_list_interval, - "snapshot_refresh": self.snapshot_update_interval, } for key, value in service.items(): @@ -212,19 +142,14 @@ class PublishMixin: # Devices ------------------------------------------------------------------------------------- async def publish_device_discovery(self: Amcrest2Mqtt, device_id: str) -> None: - async def _publish_one(dev_id: str, defn: dict, suffix: str = "") -> None: - eff_device_id = dev_id if not suffix else f"{dev_id}_{suffix}" - topic = self.mqtt_helper.disc_t(defn["component_type"], f"{dev_id}_{suffix}" if suffix else dev_id) - payload = {k: v for k, v in defn.items() if k != "component_type"} - await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, json.dumps(payload), retain=True) - self.upsert_state(eff_device_id, internal={"discovered": True}) - - await _publish_one(device_id, self.get_component(device_id)) + component = self.get_component(device_id) + for slug, mode in self.get_modes(device_id).items(): + component["cmps"][f"{device_id}_{slug}"] = mode - # Publish any modes (0..n) - modes = self.get_modes(device_id) - for slug, mode in modes.items(): - await _publish_one(device_id, mode, suffix=slug) + topic = self.mqtt_helper.disc_t("device", device_id) + payload = {k: v for k, v in component.items() if k != "platform"} + await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, json.dumps(payload), retain=True) + self.upsert_state(device_id, internal={"discovered": True}) async def publish_device_availability(self: Amcrest2Mqtt, device_id: str, online: bool = True) -> None: payload = "online" if online else "offline" @@ -232,34 +157,20 @@ class PublishMixin: avty_t = self.get_device_availability_topic(device_id) await asyncio.to_thread(self.mqtt_helper.safe_publish, avty_t, payload, retain=True) - async def publish_device_state(self: Amcrest2Mqtt, device_id: str) -> None: - async def _publish_one(dev_id: str, defn: str | dict[str, Any], suffix: str = "") -> None: - topic = self.get_device_state_topic(dev_id, suffix) - if isinstance(defn, dict): - flat: dict[str, Any] = {k: v for k, v in defn.items() if k != "component_type"} - meta = self.states[dev_id].get("meta") - if isinstance(meta, dict) and "last_update" in meta: - flat["last_update"] = meta["last_update"] - await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, json.dumps(flat), retain=True) - else: - await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, defn, retain=True) - + async def publish_device_state(self: Amcrest2Mqtt, device_id: str, subject: str = "", sub: str = "") -> None: if not self.is_discovered(device_id): self.logger.debug(f"discovery not complete for {device_id} yet, holding off on sending state") return - states = self.states[device_id] - await _publish_one(device_id, states[self.get_component_type(device_id)]) - - modes = self.get_modes(device_id) - for name, mode in modes.items(): - component_type = mode["component_type"] - - # if no state yet, skip it - if component_type not in states or (isinstance(states[component_type], dict) and name not in states[component_type]): + for state, value in self.states[device_id].items(): + if subject and state != subject: continue - - type_states = states[component_type][name] if isinstance(states[component_type], dict) else states[component_type] - await _publish_one(device_id, type_states, name) - - await self.publish_service_state() + if isinstance(value, dict): + for k, v in value.items(): + if sub and k != sub: + continue + topic = self.mqtt_helper.stat_t(device_id, state, k) + await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, v, retain=True) + else: + topic = self.mqtt_helper.stat_t(device_id, state) + await asyncio.to_thread(self.mqtt_helper.safe_publish, topic, value, retain=True) diff --git a/src/amcrest2mqtt/mixins/topics.py b/src/amcrest2mqtt/mixins/topics.py index f2c9b4a..ca2689c 100644 --- a/src/amcrest2mqtt/mixins/topics.py +++ b/src/amcrest2mqtt/mixins/topics.py @@ -20,8 +20,8 @@ class TopicsMixin: def get_component(self: Amcrest2Mqtt, device_id: str) -> dict[str, Any]: return cast(dict[str, Any], self.devices[device_id]["component"]) - def get_component_type(self: Amcrest2Mqtt, device_id: str) -> str: - return cast(str, self.devices[device_id]["component"].get("component_type", "unknown")) + def get_platform(self: Amcrest2Mqtt, device_id: str) -> str: + return cast(str, self.devices[device_id]["component"].get("platform", "unknown")) def get_modes(self: Amcrest2Mqtt, device_id: str) -> dict[str, Any]: return cast(dict[str, Any], self.devices[device_id]["modes"]) @@ -35,7 +35,7 @@ class TopicsMixin: def get_device_state_topic(self: Amcrest2Mqtt, device_id: str, mode_name: str = "") -> str: component = self.get_mode(device_id, mode_name) if mode_name else self.get_component(device_id) - match component["component_type"]: + match component["platform"]: case "camera": return cast(str, component["topic"]) case "image": diff --git a/uv.lock b/uv.lock index 0e58eb9..7139967 100644 --- a/uv.lock +++ b/uv.lock @@ -188,37 +188,37 @@ wheels = [ [[package]] name = "coverage" -version = "7.11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/12/3e2d2ec71796e0913178478e693a06af6a3bc9f7f9cb899bf85a426d8370/coverage-7.11.1.tar.gz", hash = "sha256:b4b3a072559578129a9e863082a2972a2abd8975bc0e2ec57da96afcd6580a8a", size = 814037, upload-time = "2025-11-07T10:52:41.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/c2/ac2c3417eaa4de1361036ebbc7da664242b274b2e00c4b4a1cfc7b29920b/coverage-7.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0305463c45c5f21f0396cd5028de92b1f1387e2e0756a85dd3147daa49f7a674", size = 216967, upload-time = "2025-11-07T10:51:45.55Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a3/afef455d03c468ee303f9df9a6f407e8bea64cd576fca914ff888faf52ca/coverage-7.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fa4d468d5efa1eb6e3062be8bd5f45cbf28257a37b71b969a8c1da2652dfec77", size = 217298, upload-time = "2025-11-07T10:51:47.31Z" }, - { url = "https://files.pythonhosted.org/packages/9d/59/6e2fb3fb58637001132dc32228b4fb5b332d75d12f1353cb00fe084ee0ba/coverage-7.11.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d2b2f5fc8fe383cbf2d5c77d6c4b2632ede553bc0afd0cdc910fa5390046c290", size = 248337, upload-time = "2025-11-07T10:51:49.48Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5e/ce442bab963e3388658da8bde6ddbd0a15beda230afafaa25e3c487dc391/coverage-7.11.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bde6488c1ad509f4fb1a4f9960fd003d5a94adef61e226246f9699befbab3276", size = 250853, upload-time = "2025-11-07T10:51:51.215Z" }, - { url = "https://files.pythonhosted.org/packages/d1/2f/43f94557924ca9b64e09f1c3876da4eec44a05a41e27b8a639d899716c0e/coverage-7.11.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a69e0d6fa0b920fe6706a898c52955ec5bcfa7e45868215159f45fd87ea6da7c", size = 252190, upload-time = "2025-11-07T10:51:53.262Z" }, - { url = "https://files.pythonhosted.org/packages/8c/fa/a04e769b92bc5628d4bd909dcc3c8219efe5e49f462e29adc43e198ecfde/coverage-7.11.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:976e51e4a549b80e4639eda3a53e95013a14ff6ad69bb58ed604d34deb0e774c", size = 248335, upload-time = "2025-11-07T10:51:55.388Z" }, - { url = "https://files.pythonhosted.org/packages/99/d0/b98ab5d2abe425c71117a7c690ead697a0b32b83256bf0f566c726b7f77b/coverage-7.11.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d61fcc4d384c82971a3d9cf00d0872881f9ded19404c714d6079b7a4547e2955", size = 250209, upload-time = "2025-11-07T10:51:57.263Z" }, - { url = "https://files.pythonhosted.org/packages/9c/3f/b9c4fbd2e6d1b64098f99fb68df7f7c1b3e0a0968d24025adb24f359cdec/coverage-7.11.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:284c5df762b533fae3ebd764e3b81c20c1c9648d93ef34469759cb4e3dfe13d0", size = 248163, upload-time = "2025-11-07T10:51:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/08/fc/3e4d54fb6368b0628019eefd897fc271badbd025410fd5421a65fb58758f/coverage-7.11.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:bab32cb1d4ad2ac6dcc4e17eee5fa136c2a1d14ae914e4bce6c8b78273aece3c", size = 247983, upload-time = "2025-11-07T10:52:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/b9/4a/a5700764a12e932b35afdddb2f59adbca289c1689455d06437f609f3ef35/coverage-7.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36f2fed9ce392ca450fb4e283900d0b41f05c8c5db674d200f471498be3ce747", size = 249646, upload-time = "2025-11-07T10:52:02.856Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2c/45ed33d9e80a1cc9b44b4bd535d44c154d3204671c65abd90ec1e99522a2/coverage-7.11.1-cp314-cp314-win32.whl", hash = "sha256:853136cecb92a5ba1cc8f61ec6ffa62ca3c88b4b386a6c835f8b833924f9a8c5", size = 219700, upload-time = "2025-11-07T10:52:05.05Z" }, - { url = "https://files.pythonhosted.org/packages/90/d7/5845597360f6434af1290118ebe114642865f45ce47e7e822d9c07b371be/coverage-7.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:77443d39143e20927259a61da0c95d55ffc31cf43086b8f0f11a92da5260d592", size = 220516, upload-time = "2025-11-07T10:52:07.259Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d0/d311a06f9cf7a48a98ffcfd0c57db0dcab6da46e75c439286a50dc648161/coverage-7.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:829acb88fa47591a64bf5197e96a931ce9d4b3634c7f81a224ba3319623cdf6c", size = 219091, upload-time = "2025-11-07T10:52:09.216Z" }, - { url = "https://files.pythonhosted.org/packages/a7/3d/c6a84da4fa9b840933045b19dd19d17b892f3f2dd1612903260291416dba/coverage-7.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2ad1fe321d9522ea14399de83e75a11fb6a8887930c3679feb383301c28070d9", size = 217700, upload-time = "2025-11-07T10:52:11.348Z" }, - { url = "https://files.pythonhosted.org/packages/94/10/a4fc5022017dd7ac682dc423849c241dfbdad31734b8f96060d84e70b587/coverage-7.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f69c332f0c3d1357c74decc9b1843fcd428cf9221bf196a20ad22aa1db3e1b6c", size = 217968, upload-time = "2025-11-07T10:52:13.203Z" }, - { url = "https://files.pythonhosted.org/packages/59/2d/a554cd98924d296de5816413280ac3b09e42a05fb248d66f8d474d321938/coverage-7.11.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:576baeea4eebde684bf6c91c01e97171c8015765c8b2cfd4022a42b899897811", size = 259334, upload-time = "2025-11-07T10:52:15.079Z" }, - { url = "https://files.pythonhosted.org/packages/05/98/d484cb659ec33958ca96b6f03438f56edc23b239d1ad0417b7a97fc1848a/coverage-7.11.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:28ad84c694fa86084cfd3c1eab4149844b8cb95bd8e5cbfc4a647f3ee2cce2b3", size = 261445, upload-time = "2025-11-07T10:52:17.134Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/920cba122cc28f4557c0507f8bd7c6e527ebcc537d0309186f66464a8fd9/coverage-7.11.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1043ff958f09fc3f552c014d599f3c6b7088ba97d7bc1bd1cce8603cd75b520", size = 263858, upload-time = "2025-11-07T10:52:19.836Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a0/036397bdbee0f3bd46c2e26fdfbb1a61b2140bf9059240c37b61149047fa/coverage-7.11.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c6681add5060c2742dafcf29826dff1ff8eef889a3b03390daeed84361c428bd", size = 258381, upload-time = "2025-11-07T10:52:21.687Z" }, - { url = "https://files.pythonhosted.org/packages/b6/61/2533926eb8990f182eb287f4873216c8ca530cc47241144aabf46fe80abe/coverage-7.11.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:773419b225ec9a75caa1e941dd0c83a91b92c2b525269e44e6ee3e4c630607db", size = 261321, upload-time = "2025-11-07T10:52:23.612Z" }, - { url = "https://files.pythonhosted.org/packages/32/6e/618f7e203a998e4f6b8a0fa395744a416ad2adbcdc3735bc19466456718a/coverage-7.11.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a9cb272a0e0157dbb9b2fd0b201b759bd378a1a6138a16536c025c2ce4f7643b", size = 258933, upload-time = "2025-11-07T10:52:25.514Z" }, - { url = "https://files.pythonhosted.org/packages/22/40/6b1c27f772cb08a14a338647ead1254a57ee9dabbb4cacbc15df7f278741/coverage-7.11.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e09adb2a7811dc75998eef68f47599cf699e2b62eed09c9fefaeb290b3920f34", size = 257756, upload-time = "2025-11-07T10:52:27.845Z" }, - { url = "https://files.pythonhosted.org/packages/73/07/f9cd12f71307a785ea15b009c8d8cc2543e4a867bd04b8673843970b6b43/coverage-7.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1335fa8c2a2fea49924d97e1e3500cfe8d7c849f5369f26bb7559ad4259ccfab", size = 260086, upload-time = "2025-11-07T10:52:29.776Z" }, - { url = "https://files.pythonhosted.org/packages/34/02/31c5394f6f5d72a466966bcfdb61ce5a19862d452816d6ffcbb44add16ee/coverage-7.11.1-cp314-cp314t-win32.whl", hash = "sha256:4782d71d2a4fa7cef95e853b7097c8bbead4dbd0e6f9c7152a6b11a194b794db", size = 220483, upload-time = "2025-11-07T10:52:31.752Z" }, - { url = "https://files.pythonhosted.org/packages/7f/96/81e1ef5fbfd5090113a96e823dbe055e4c58d96ca73b1fb0ad9d26f9ec36/coverage-7.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:939f45e66eceb63c75e8eb8fc58bb7077c00f1a41b0e15c6ef02334a933cfe93", size = 221592, upload-time = "2025-11-07T10:52:33.724Z" }, - { url = "https://files.pythonhosted.org/packages/38/7a/a5d050de44951ac453a2046a0f3fb5471a4a557f0c914d00db27d543d94c/coverage-7.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:01c575bdbef35e3f023b50a146e9a75c53816e4f2569109458155cd2315f87d9", size = 219627, upload-time = "2025-11-07T10:52:36.285Z" }, - { url = "https://files.pythonhosted.org/packages/76/32/bd9f48c28e23b2f08946f8e83983617b00619f5538dbd7e1045fa7e88c00/coverage-7.11.1-py3-none-any.whl", hash = "sha256:0fa848acb5f1da24765cee840e1afe9232ac98a8f9431c6112c15b34e880b9e8", size = 208689, upload-time = "2025-11-07T10:52:38.646Z" }, +version = "7.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/32/e6/7c4006cf689ed7a4aa75dcf1f14acbc04e585714c220b5cc6d231096685a/coverage-7.11.2.tar.gz", hash = "sha256:ae43149b7732df15c3ca9879b310c48b71d08cd8a7ba77fda7f9108f78499e93", size = 814849, upload-time = "2025-11-08T20:26:33.011Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/d1/43d17c299249085d6e0df36db272899e92aa09e68e27d3e92a4cf8d9523e/coverage-7.11.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7f933bc1fead57373922e383d803e1dd5ec7b5a786c220161152ebee1aa3f006", size = 217170, upload-time = "2025-11-08T20:25:39.254Z" }, + { url = "https://files.pythonhosted.org/packages/78/66/f21c03307079a0b7867b364af057430018a3d4a18ed1b99e1adaf5a0f305/coverage-7.11.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f80cb5b328e870bf3df0568b41643a85ee4b8ccd219a096812389e39aa310ea4", size = 217497, upload-time = "2025-11-08T20:25:41.277Z" }, + { url = "https://files.pythonhosted.org/packages/f0/dd/0a2257154c32f442fe3b4622501ab818ae4bd7cde33bd7a740630f6bd24c/coverage-7.11.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6b2498f86f2554ed6cb8df64201ee95b8c70fb77064a8b2ae8a7185e7a4a5f0", size = 248539, upload-time = "2025-11-08T20:25:43.349Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ca/c55ab0ee5ebfc4ab56cfc1b3585cba707342dc3f891fe19f02e07bc0c25f/coverage-7.11.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a913b21f716aa05b149a8656e9e234d9da04bc1f9842136ad25a53172fecc20e", size = 251057, upload-time = "2025-11-08T20:25:45.083Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/a149b88ebe714b76d95427d609e629446d1df5d232f4bdaec34e471da124/coverage-7.11.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5769159986eb174f0f66d049a52da03f2d976ac1355679371f1269e83528599", size = 252393, upload-time = "2025-11-08T20:25:47.272Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a4/a992c805e95c46f0ac1b83782aa847030cb52bbfd8fc9015cff30f50fb9e/coverage-7.11.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89565d7c9340858424a5ca3223bfefe449aeb116942cdc98cd76c07ca50e9db8", size = 248534, upload-time = "2025-11-08T20:25:49.034Z" }, + { url = "https://files.pythonhosted.org/packages/78/01/318ed024ae245dbc76152bc016919aef69c508a5aac0e2da5de9b1efea61/coverage-7.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7fc943097fa48de00d14d2a2f3bcebfede024e031d7cd96063fe135f8cbe96e", size = 250412, upload-time = "2025-11-08T20:25:51.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f9/f05c7984ef48c8d1c6c1ddb243223b344dcd8c6c0d54d359e4e325e2fa7e/coverage-7.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:72a3d109ac233666064d60b29ae5801dd28bc51d1990e69f183a2b91b92d4baf", size = 248367, upload-time = "2025-11-08T20:25:53.399Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ac/461ed0dcaba0c727b760057ffa9837920d808a35274e179ff4a94f6f755a/coverage-7.11.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4648c90cf741fb61e142826db1557a44079de0ca868c5c5a363c53d852897e84", size = 248187, upload-time = "2025-11-08T20:25:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bf/8510ce8c7b1a8d682726df969e7523ee8aac23964b2c8301b8ce2400c1b4/coverage-7.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f1aa017b47e1879d7bac50161b00d2b886f2ff3882fa09427119e1b3572ede1", size = 249849, upload-time = "2025-11-08T20:25:57.186Z" }, + { url = "https://files.pythonhosted.org/packages/75/6f/ea1c8990ca35d607502c9e531f164573ea59bb6cd5cd4dc56d7cc3d1fcb5/coverage-7.11.2-cp314-cp314-win32.whl", hash = "sha256:44b6e04bb94e59927a2807cd4de86386ce34248eaea95d9f1049a72f81828c38", size = 219908, upload-time = "2025-11-08T20:25:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/1e/04/a64e2a8b9b65ae84670207dc6073e3d48ee9192646440b469e9b8c335d1f/coverage-7.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7ea36e981a8a591acdaa920704f8dc798f9fff356c97dbd5d5702046ae967ce1", size = 220724, upload-time = "2025-11-08T20:26:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/73/df/eb4e9f9d0d55f7ec2b55298c30931a665c2249c06e3d1d14c5a6df638c77/coverage-7.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:4aaf2212302b6f748dde596424b0f08bc3e1285192104e2480f43d56b6824f35", size = 219296, upload-time = "2025-11-08T20:26:02.918Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b5/e9bb3b17a65fe92d1c7a2363eb5ae9893fafa578f012752ed40eee6aa3c8/coverage-7.11.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84e8e0f5ab5134a2d32d4ebadc18b433dbbeddd0b73481f816333b1edd3ff1c8", size = 217905, upload-time = "2025-11-08T20:26:04.633Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/1f38dd0b63a9d82fb3c9d7fbe1c9dab26ae77e5b45e801d129664e039034/coverage-7.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5db683000ff6217273071c752bd6a1d341b6dc5d6aaa56678c53577a4e70e78a", size = 218172, upload-time = "2025-11-08T20:26:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/fd/5d/2aeb513c6841270783b216478c6edc65b128c6889850c5f77568aa3a3098/coverage-7.11.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2970c03fefee2a5f1aebc91201a0706a7d0061cc71ab452bb5c5345b7174a349", size = 259537, upload-time = "2025-11-08T20:26:08.481Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/ddd9b22ec1b5c69cc579b149619c354f981aaaafc072b92574f2d3d6c267/coverage-7.11.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9f28b900d96d83e2ae855b68d5cf5a704fa0b5e618999133fd2fb3bbe35ecb1", size = 261648, upload-time = "2025-11-08T20:26:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/29/e2/8743b7281decd3f73b964389fea18305584dd6ba96f0aff91b4880b50310/coverage-7.11.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8b9a7ebc6a29202fb095877fd8362aab09882894d1c950060c76d61fb116114", size = 264061, upload-time = "2025-11-08T20:26:12.306Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/46daea7c4349c4530c62383f45148cc878845374b7a632e3ac2769b2f26a/coverage-7.11.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f8f6bcaa7fe162460abb38f7a5dbfd7f47cfc51e2a0bf0d3ef9e51427298391", size = 258580, upload-time = "2025-11-08T20:26:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/f9b1c2d921d585dd6499e05bd71420950cac4e800f71525eb3d2690944fe/coverage-7.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:461577af3f8ad4da244a55af66c0731b68540ce571dbdc02598b5ec9e7a09e73", size = 261526, upload-time = "2025-11-08T20:26:16.353Z" }, + { url = "https://files.pythonhosted.org/packages/86/7d/55acee453a71a71b08b05848d718ce6ac4559d051b4a2c407b0940aa72be/coverage-7.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5b284931d57389ec97a63fb1edf91c68ec369cee44bc40b37b5c3985ba0a2914", size = 259135, upload-time = "2025-11-08T20:26:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3f/cf1e0217efdebab257eb0f487215fe02ff2b6f914cea641b2016c33358e1/coverage-7.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2ca963994d28e44285dc104cf94b25d8a7fd0c6f87cf944f46a23f473910703f", size = 257959, upload-time = "2025-11-08T20:26:19.894Z" }, + { url = "https://files.pythonhosted.org/packages/68/0e/e9be33e55346e650c3218a313e888df80418415462c63bceaf4b31e36ab5/coverage-7.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7d3fccd5781c5d29ca0bd1ea272630f05cd40a71d419e7e6105c0991400eb14", size = 260290, upload-time = "2025-11-08T20:26:22.05Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1d/9e93937c2a9bd255bb5efeff8c5df1c8322e508371f76f21a58af0e36a31/coverage-7.11.2-cp314-cp314t-win32.whl", hash = "sha256:f633da28958f57b846e955d28661b2b323d8ae84668756e1eea64045414dbe34", size = 220691, upload-time = "2025-11-08T20:26:24.043Z" }, + { url = "https://files.pythonhosted.org/packages/bf/30/893b5a67e2914cf2be8e99c511b8084eaa8c0585e42d8b3cd78208f5f126/coverage-7.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:410cafc1aba1f7eb8c09823d5da381be30a2c9b3595758a4c176fcfc04732731", size = 221800, upload-time = "2025-11-08T20:26:26.24Z" }, + { url = "https://files.pythonhosted.org/packages/2b/8b/6d93448c494a35000cc97d8d5d9c9b3774fa2b0c0d5be55f16877f962d71/coverage-7.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:595c6bb2b565cc2d930ee634cae47fa959dfd24cc0e8ae4cf2b6e7e131e0d1f7", size = 219838, upload-time = "2025-11-08T20:26:28.479Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/99766a75c88e576f47c2d9a06416ff5d95be9b42faca5c37e1ab77c4cd1a/coverage-7.11.2-py3-none-any.whl", hash = "sha256:2442afabe9e83b881be083238bb7cf5afd4a10e47f29b6094470338d2336b33c", size = 208891, upload-time = "2025-11-08T20:26:30.739Z" }, ] [[package]] @@ -288,7 +288,7 @@ wheels = [ [[package]] name = "json-logging-graystorm" version = "0.1.3" -source = { git = "https://github.com/weirdtangent/json_logging.git?branch=main#82662d518f271eed752ba34067db286b3723249c" } +source = { git = "https://github.com/weirdtangent/json_logging.git?branch=main#27a2f812f9e0cb329b7cb35a17b3ce038ba93632" } [[package]] name = "jsonschema"