feat(discovery): unify service and camera discovery to new HA device schema

* Reworked publish.py and amcrest.py to emit single-payload MQTT device discovery blocks using "platform": "mqtt" and nested "cmps" for entities

* Simplified service entity publication (publish_service_discovery) into unified "device" payload

* Replaced component_type with platform throughout topics and interface definitions

* Refactored build_camera() to generate full per-camera device definition including all sensors, switches, and media links

* Updated publish_device_state() to support targeted subject/sub publishing

* Increased snapshot timeout and backoff (20s, 8s) for better camera stability

* Updated type handling in amcrest_api for network config parsing (cast from tuple or str)

* Bumped coverage dependency → 7.11.2 and refreshed json-logging-graystorm SHA
pull/106/head
Jeff Culverhouse 3 months ago
parent 94a4297665
commit 8c7bfd2b9e

@ -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: ...

@ -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)

@ -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:

@ -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)

@ -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":

@ -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"

Loading…
Cancel
Save