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,147 @@
|
||||
"""DeviceManager with discovery mocked."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from shelly_manager.core.device_manager import DeviceManager, shelly_from_probe
|
||||
from shelly_manager.core.models import DiscoveredEndpoint, ShellyDevice
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shelly_from_probe_gen2() -> None:
|
||||
d = shelly_from_probe(
|
||||
"10.0.0.2",
|
||||
2,
|
||||
{"gen": 2, "mac": "AABBCCDDEEFF", "model": "Plus1", "fw_id": "x", "id": "shellyplus1-abc"},
|
||||
)
|
||||
assert d.generation == 2
|
||||
assert d.ip == "10.0.0.2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discover_all_persists_after_mdns_probe_and_enrich(
|
||||
app_config_sqlite,
|
||||
) -> None:
|
||||
dm = DeviceManager(app_config_sqlite)
|
||||
|
||||
enriched = ShellyDevice(
|
||||
id="AABBCCDDEEFF",
|
||||
name="enriched",
|
||||
mac="AA:BB:CC:DD:EE:FF",
|
||||
ip="10.0.0.1",
|
||||
generation=2,
|
||||
model="Plus1",
|
||||
firmware="1",
|
||||
online=True,
|
||||
last_seen=datetime.now(UTC),
|
||||
capabilities=["switch"],
|
||||
status={"ok": True},
|
||||
settings={"sys": {}},
|
||||
)
|
||||
|
||||
ep = DiscoveredEndpoint(ip="10.0.0.1", port=80, source="mdns_http", txt={})
|
||||
probe_payload = {
|
||||
"gen": 2,
|
||||
"mac": "AA:BB:CC:DD:EE:FF",
|
||||
"model": "Plus1",
|
||||
"fw_id": "1",
|
||||
"id": "shellyplus1-abc",
|
||||
}
|
||||
|
||||
fake_rt = MagicMock()
|
||||
fake_rt.__aenter__ = AsyncMock(return_value=fake_rt)
|
||||
fake_rt.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with (
|
||||
patch("shelly_manager.core.device_manager.browse_mdns", new_callable=AsyncMock) as mdns,
|
||||
patch("shelly_manager.core.device_manager.probe_ip", new_callable=AsyncMock) as probe,
|
||||
patch(
|
||||
"shelly_manager.core.device_manager.fetch_device_snapshot",
|
||||
new_callable=AsyncMock,
|
||||
) as enrich,
|
||||
patch("shelly_manager.core.device_manager.ShellyRuntime", return_value=fake_rt),
|
||||
):
|
||||
mdns.return_value = [ep]
|
||||
probe.return_value = (2, probe_payload)
|
||||
enrich.return_value = enriched
|
||||
|
||||
sess = MagicMock()
|
||||
await dm.discover_all(session=sess)
|
||||
|
||||
stored = await dm.storage.get_device("AABBCCDDEEFF")
|
||||
assert stored is not None
|
||||
assert stored.name == "enriched"
|
||||
assert stored.status.get("ok") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_device_by_ip_invalid(app_config_sqlite) -> None:
|
||||
dm = DeviceManager(app_config_sqlite)
|
||||
dev, err = await dm.add_device_by_ip("not-an-ip")
|
||||
assert dev is None
|
||||
assert err is not None
|
||||
assert "Invalid" in err
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_device_by_ip_no_shelly_json(app_config_sqlite) -> None:
|
||||
dm = DeviceManager(app_config_sqlite)
|
||||
with patch("shelly_manager.core.device_manager.fetch_shelly_json", new_callable=AsyncMock) as fj:
|
||||
fj.return_value = None
|
||||
sess = MagicMock()
|
||||
dev, err = await dm.add_device_by_ip("192.168.1.1", session=sess)
|
||||
assert dev is None
|
||||
assert err is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_device_by_ip_persists_after_enrich(app_config_sqlite) -> None:
|
||||
dm = DeviceManager(app_config_sqlite)
|
||||
enriched = ShellyDevice(
|
||||
id="AABBCCDDEEFF",
|
||||
name="enriched",
|
||||
mac="AA:BB:CC:DD:EE:FF",
|
||||
ip="10.0.0.5",
|
||||
generation=2,
|
||||
model="Plus1",
|
||||
firmware="1",
|
||||
online=True,
|
||||
last_seen=datetime.now(UTC),
|
||||
capabilities=["switch"],
|
||||
status={"ok": True},
|
||||
settings={"sys": {}},
|
||||
)
|
||||
probe_payload = {
|
||||
"gen": 2,
|
||||
"mac": "AA:BB:CC:DD:EE:FF",
|
||||
"model": "Plus1",
|
||||
"fw_id": "1",
|
||||
"id": "shellyplus1-abc",
|
||||
}
|
||||
fake_rt = MagicMock()
|
||||
fake_rt.__aenter__ = AsyncMock(return_value=fake_rt)
|
||||
fake_rt.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
with (
|
||||
patch("shelly_manager.core.device_manager.fetch_shelly_json", new_callable=AsyncMock) as fj,
|
||||
patch(
|
||||
"shelly_manager.core.device_manager.fetch_device_snapshot",
|
||||
new_callable=AsyncMock,
|
||||
) as enrich,
|
||||
patch("shelly_manager.core.device_manager.ShellyRuntime", return_value=fake_rt),
|
||||
):
|
||||
fj.return_value = probe_payload
|
||||
enrich.return_value = enriched
|
||||
sess = MagicMock()
|
||||
dev, err = await dm.add_device_by_ip("10.0.0.5", session=sess)
|
||||
|
||||
assert err is None
|
||||
assert dev is not None
|
||||
assert dev.name == "enriched"
|
||||
stored = await dm.storage.get_device("AABBCCDDEEFF")
|
||||
assert stored is not None
|
||||
assert stored.status.get("ok") is True
|
||||
Reference in New Issue
Block a user