Add LastAlertDetection module for smartcam devices (#1762)
Some checks are pending
CI / Perform Lint Checks (3.14) (push) Waiting to run
CI / Python 3.11 on macos-latest (push) Blocked by required conditions
CI / Python 3.12 on macos-latest (push) Blocked by required conditions
CI / Python 3.13 on macos-latest (push) Blocked by required conditions
CI / Python 3.14 on macos-latest (push) Blocked by required conditions
CI / Python 3.11 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.12 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.13 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.14 on ubuntu-latest (push) Blocked by required conditions
CI / Python 3.11 on windows-latest (push) Blocked by required conditions
CI / Python 3.12 on windows-latest (push) Blocked by required conditions
CI / Python 3.13 on windows-latest (push) Blocked by required conditions
CI / Python 3.14 on windows-latest (push) Blocked by required conditions
CodeQL Checks / Analyze (python) (push) Waiting to run

Add a `LastAlertDetection` module for SMARTCAM devices exposing `getLastAlarmInfo` as two `Info` sensor features:

- `last_alert_timestamp` — tz-aware `datetime` of the last alert, `None` when the device has never detected anything (`last_alarm_time` is `""` or `"0"` on such devices, and an unexpected value is also mapped to `None`
- `last_alert_type` — `LastAlertType` enum (`Motion`; `Unknown` fallback with a one-time warning per unknown raw value), `None` when unset.
This commit is contained in:
Julien Herr
2026-09-18 18:05:48 +02:00
committed by GitHub
parent 454e2697dc
commit b7b41f2c81
4 changed files with 231 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ from .childsetup import ChildSetup
from .device import DeviceModule
from .glassdetection import GlassDetection
from .homekit import HomeKit
from .lastalertdetection import LastAlertDetection
from .led import Led
from .lensmask import LensMask
from .linecrossingdetection import LineCrossingDetection
@@ -33,6 +34,7 @@ __all__ = [
"ChildSetup",
"DeviceModule",
"GlassDetection",
"LastAlertDetection",
"Led",
"LineCrossingDetection",
"MeowDetection",

View File

@@ -0,0 +1,102 @@
"""Module for the last alert reported by the camera."""
from __future__ import annotations
import logging
from datetime import datetime
from enum import StrEnum
from ...feature import Feature
from ..smartcammodule import SmartCamModule
_LOGGER = logging.getLogger(__name__)
class LastAlertType(StrEnum):
"""Type of the alert reported by the camera."""
Motion = "motion"
Unknown = "unknown"
class LastAlertDetection(SmartCamModule):
"""Implementation of the last alert reported by the camera.
Backed by ``getLastAlarmInfo`` (``last_alarm_time``/``last_alarm_type``),
which is refreshed within a second of a detection and needs no SD card.
The timestamp keeps advancing while the motion continues.
Hub children are excluded as their modules are only refreshed once a day,
which defeats the purpose of polling the last alert.
"""
REQUIRED_COMPONENT = "detection"
QUERY_GETTER_NAME = "getLastAlarmInfo"
QUERY_MODULE_NAME = "system"
QUERY_SECTION_NAMES = "last_alarm_info"
_logged_unknown_types: set[str] = set()
async def _check_supported(self) -> bool:
"""Additional check to see if the module is supported by the device."""
return not self._device._is_hub_child
def _initialize_features(self) -> None:
"""Initialize features after the initial update."""
self._add_feature(
Feature(
device=self._device,
id="last_alert_timestamp",
name="Last alert time",
attribute_getter="last_alert_timestamp",
container=self,
category=Feature.Category.Info,
type=Feature.Type.Sensor,
)
)
self._add_feature(
Feature(
device=self._device,
id="last_alert_type",
name="Last alert type",
attribute_getter="last_alert_type",
container=self,
category=Feature.Category.Info,
type=Feature.Type.Sensor,
)
)
@property
def last_alert_timestamp(self) -> datetime | None:
"""Return timestamp of the last alert, None if nothing was reported yet.
Devices report an empty string or 0 when nothing has been detected yet.
Unparseable values are reported as None as well.
"""
try:
timestamp = int(self.data["last_alarm_info"].get("last_alarm_time"))
if not timestamp:
return None
return datetime.fromtimestamp(timestamp, tz=self._device.timezone)
except (TypeError, ValueError, OverflowError, OSError):
return None
@property
def last_alert_type(self) -> LastAlertType | None:
"""Return the type of the last alert, None if nothing was reported yet.
Unknown types are reported as :attr:`LastAlertType.Unknown`.
"""
alert_type = self.data["last_alarm_info"].get("last_alarm_type")
if not alert_type:
return None
try:
return LastAlertType(alert_type)
except ValueError:
if alert_type not in self._logged_unknown_types:
self._logged_unknown_types.add(alert_type)
_LOGGER.warning(
"Unknown alert type, please create an issue describing it: %s",
alert_type,
)
return LastAlertType.Unknown

View File

@@ -54,6 +54,10 @@ class SmartCamModule(SmartModule):
SmartCamBattery: Final[ModuleName[modules.Battery]] = ModuleName("Battery")
SmartCamLastAlertDetection: Final[ModuleName[modules.LastAlertDetection]] = (
ModuleName("LastAlertDetection")
)
SmartCamDeviceModule: Final[ModuleName[modules.DeviceModule]] = ModuleName(
"devicemodule"
)