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

@@ -0,0 +1,123 @@
"""Tests for smartcam last alert detection module."""
from __future__ import annotations
import logging
from datetime import datetime
import pytest
from kasa import Device
from kasa.smart import SmartDevice
from kasa.smartcam.modules.lastalertdetection import (
LastAlertDetection,
LastAlertType,
)
from kasa.smartcam.smartcammodule import SmartCamModule
from ...device_fixtures import parametrize
lastalertdetection_smartcam = parametrize(
"has last alert detection",
component_filter="detection",
protocol_filter={"SMARTCAM"},
)
lastalertdetection_hub_child = parametrize(
"hub child with detection",
component_filter="detection",
protocol_filter={"SMARTCAM.CHILD"},
)
def _set_last_alarm_info(dev: Device, time: str, type_: str = "") -> None:
dev._last_update["getLastAlarmInfo"]["system"]["last_alarm_info"] = {
"last_alarm_time": time,
"last_alarm_type": type_,
}
@lastalertdetection_smartcam
async def test_last_alert_features(dev: Device) -> None:
"""Test that the module and its features are available."""
last_alert = dev.modules.get(SmartCamModule.SmartCamLastAlertDetection)
assert last_alert
for feat_id in ("last_alert_timestamp", "last_alert_type"):
feat = dev.features.get(feat_id)
assert feat
assert feat.value == getattr(last_alert, feat_id)
@lastalertdetection_smartcam
async def test_last_alert_values(dev: Device) -> None:
"""Test that a reported alert is exposed as a tz-aware datetime and an enum."""
last_alert = dev.modules.get(SmartCamModule.SmartCamLastAlertDetection)
assert last_alert
_set_last_alarm_info(dev, "1734967724", "motion")
alert_time = last_alert.last_alert_timestamp
assert isinstance(alert_time, datetime)
assert alert_time.tzinfo == dev.timezone
assert alert_time.timestamp() == 1734967724
assert last_alert.last_alert_type is LastAlertType.Motion
assert dev.features["last_alert_type"].value is LastAlertType.Motion
@lastalertdetection_smartcam
async def test_last_alert_type_unknown_logs_once(
dev: Device, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that an unknown alert type falls back to Unknown with a single warning."""
last_alert = dev.modules.get(SmartCamModule.SmartCamLastAlertDetection)
assert last_alert
caplog.set_level(logging.WARNING)
# The warned-once set is process-wide, reset it so each fixture starts clean.
LastAlertDetection._logged_unknown_types.clear()
_set_last_alarm_info(dev, "1734967724", "vehicle")
assert last_alert.last_alert_type is LastAlertType.Unknown
assert "Unknown alert type" in caplog.text
assert "vehicle" in caplog.text
caplog.clear()
assert last_alert.last_alert_type is LastAlertType.Unknown
assert "Unknown alert type" not in caplog.text
_set_last_alarm_info(dev, "1734967724", "person")
assert last_alert.last_alert_type is LastAlertType.Unknown
assert "person" in caplog.text
@lastalertdetection_smartcam
@pytest.mark.parametrize(
"time_raw",
[
"", # never triggered (C100)
"0", # never triggered (C110, C220, ...)
"nonsense", # unexpected value must not break feature access
"99999999999999999", # out of range for datetime.fromtimestamp
],
)
async def test_last_alert_never_triggered(dev: Device, time_raw: str) -> None:
"""Test that devices that never reported an alert return None."""
last_alert = dev.modules.get(SmartCamModule.SmartCamLastAlertDetection)
assert last_alert
_set_last_alarm_info(dev, time_raw)
assert last_alert.last_alert_timestamp is None
assert last_alert.last_alert_type is None
@lastalertdetection_hub_child
async def test_last_alert_not_exposed_on_hub_children(dev: Device) -> None:
"""Test that hub children do not expose the module.
Hub child modules are only refreshed once a day, which defeats the purpose
of polling the last alert.
"""
assert isinstance(dev, SmartDevice)
assert dev._is_hub_child
assert SmartCamModule.SmartCamLastAlertDetection not in dev.modules
assert "last_alert_timestamp" not in dev.features
assert "last_alert_type" not in dev.features