Initial commit: Shelly Manager with Textual CLI, Streamlit UI, and comprehensive .gitignore
Shelly device management app with mDNS/subnet discovery, inventory, configuration, and mass operations for Gen1/Gen2+ devices. Includes .gitignore excluding runtime data (device DB, user config), AI conversation history, build artifacts, and common Python/OS patterns.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
"""Unit tests for models and helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from shelly_manager.core.discovery import classify_generation, normalize_mac
|
||||
from shelly_manager.core.models import (
|
||||
DeviceFilter,
|
||||
ShellyDevice,
|
||||
infer_capabilities,
|
||||
preserve_firmware_check_metadata,
|
||||
)
|
||||
|
||||
|
||||
def test_classify_generation_gen1() -> None:
|
||||
assert classify_generation({"type": "SHSW-21", "mac": "A"}) == 1
|
||||
|
||||
|
||||
def test_classify_generation_gen2() -> None:
|
||||
assert classify_generation({"gen": 2, "model": "x"}) == 2
|
||||
assert classify_generation({"gen": 3, "model": "x"}) == 3
|
||||
|
||||
|
||||
def test_normalize_mac() -> None:
|
||||
assert normalize_mac("aa:bb:cc:dd:ee:ff") == "AABBCCDDEEFF"
|
||||
|
||||
|
||||
def test_device_filter_matches() -> None:
|
||||
d = ShellyDevice(
|
||||
id="AABB",
|
||||
name="Kitchen",
|
||||
mac="AA:BB",
|
||||
ip="192.168.1.10",
|
||||
generation=2,
|
||||
model="Plus1",
|
||||
firmware="1.0",
|
||||
online=True,
|
||||
last_seen=datetime.now(UTC),
|
||||
capabilities=["switch"],
|
||||
status={},
|
||||
settings={},
|
||||
tags=["t1"],
|
||||
)
|
||||
assert DeviceFilter(generations=[2]).matches(d)
|
||||
assert not DeviceFilter(generations=[1]).matches(d)
|
||||
assert DeviceFilter(tags=["t1"]).matches(d)
|
||||
assert not DeviceFilter(tags=["missing"]).matches(d)
|
||||
assert DeviceFilter(name_contains="kit").matches(d)
|
||||
assert DeviceFilter(ip_prefix="192.168.1").matches(d)
|
||||
assert DeviceFilter(only_device_ids=["AABB"]).matches(d)
|
||||
assert not DeviceFilter(only_device_ids=["OTHER"]).matches(d)
|
||||
|
||||
|
||||
def test_device_filter_settings_truthy() -> None:
|
||||
d = ShellyDevice(
|
||||
id="AABB",
|
||||
name="Kitchen",
|
||||
mac="AA:BB",
|
||||
ip="192.168.1.10",
|
||||
generation=2,
|
||||
model="Plus1",
|
||||
firmware="1.0",
|
||||
online=True,
|
||||
last_seen=datetime.now(UTC),
|
||||
capabilities=[],
|
||||
status={},
|
||||
settings={"ble": {"enable": True}},
|
||||
tags=[],
|
||||
)
|
||||
f = DeviceFilter(settings_path="ble.enable", settings_match="truthy")
|
||||
assert f.matches(d)
|
||||
d2 = d.model_copy(update={"settings": {"ble": {"enable": False}}})
|
||||
assert not f.matches(d2)
|
||||
|
||||
|
||||
def test_shelly_device_http_url() -> None:
|
||||
d = ShellyDevice(
|
||||
id="AABB",
|
||||
name="Kitchen",
|
||||
mac="AA:BB",
|
||||
ip="192.168.1.10",
|
||||
generation=2,
|
||||
model="Plus1",
|
||||
firmware="1.0",
|
||||
online=True,
|
||||
last_seen=datetime.now(UTC),
|
||||
capabilities=[],
|
||||
status={},
|
||||
settings={},
|
||||
tags=[],
|
||||
http_port=8080,
|
||||
)
|
||||
assert d.http_url == "http://192.168.1.10:8080/"
|
||||
|
||||
|
||||
def test_device_filter_capabilities_and_status() -> None:
|
||||
d = ShellyDevice(
|
||||
id="AABB",
|
||||
name="Kitchen",
|
||||
mac="AA:BB",
|
||||
ip="192.168.1.10",
|
||||
generation=2,
|
||||
model="Plus1",
|
||||
firmware="1.0",
|
||||
online=True,
|
||||
last_seen=datetime.now(UTC),
|
||||
capabilities=["switch"],
|
||||
status={"switch:0": {"output": True}},
|
||||
settings={},
|
||||
tags=[],
|
||||
)
|
||||
assert DeviceFilter(capabilities_any=["switch"]).matches(d)
|
||||
assert not DeviceFilter(capabilities_any=["relay"]).matches(d)
|
||||
assert DeviceFilter(status_path="switch:0|output", status_match="truthy").matches(d)
|
||||
|
||||
|
||||
def test_device_filter_settings_falsy() -> None:
|
||||
d = ShellyDevice(
|
||||
id="AABB",
|
||||
name="Kitchen",
|
||||
mac="AA:BB",
|
||||
ip="192.168.1.10",
|
||||
generation=2,
|
||||
model="Plus1",
|
||||
firmware="1.0",
|
||||
online=True,
|
||||
last_seen=datetime.now(UTC),
|
||||
capabilities=[],
|
||||
status={},
|
||||
settings={},
|
||||
tags=[],
|
||||
)
|
||||
f = DeviceFilter(settings_path="ble.enable", settings_match="falsy")
|
||||
assert f.matches(d)
|
||||
|
||||
|
||||
def test_infer_capabilities_gen1_relay() -> None:
|
||||
caps = infer_capabilities(1, {"relays": [{"ison": True}]}, {})
|
||||
assert "relay" in caps
|
||||
|
||||
|
||||
def test_infer_capabilities_gen2_switch() -> None:
|
||||
caps = infer_capabilities(2, {"switch:0": {"output": True}}, {})
|
||||
assert "switch" in caps
|
||||
|
||||
|
||||
def test_preserve_firmware_check_metadata() -> None:
|
||||
ts = datetime.now(UTC)
|
||||
prev = ShellyDevice(
|
||||
id="AABBCCDDEEFF",
|
||||
name="A",
|
||||
mac="AA:BB:CC:DD:EE:FF",
|
||||
ip="192.168.1.1",
|
||||
generation=2, # type: ignore[arg-type]
|
||||
model="Plus1",
|
||||
firmware="1",
|
||||
online=True,
|
||||
last_seen=ts,
|
||||
capabilities=[],
|
||||
status={},
|
||||
settings={},
|
||||
firmware_check_result={"stable": {"version": "1.7.4"}},
|
||||
firmware_check_at=ts,
|
||||
)
|
||||
fresh = ShellyDevice(
|
||||
id="AABBCCDDEEFF",
|
||||
name="A",
|
||||
mac="AA:BB:CC:DD:EE:FF",
|
||||
ip="192.168.1.1",
|
||||
generation=2, # type: ignore[arg-type]
|
||||
model="Plus1",
|
||||
firmware="1",
|
||||
online=True,
|
||||
last_seen=ts,
|
||||
capabilities=[],
|
||||
status={"sys": {}},
|
||||
settings={},
|
||||
firmware_check_result=None,
|
||||
firmware_check_at=None,
|
||||
)
|
||||
preserve_firmware_check_metadata(fresh, prev)
|
||||
assert fresh.firmware_check_result == prev.firmware_check_result
|
||||
assert fresh.firmware_check_at == prev.firmware_check_at
|
||||
Reference in New Issue
Block a user