"""Shelly Gen2 RPC klient — parser Switch.GetStatus a stavba RPC volání (mock httpx).""" from __future__ import annotations import asyncio import json import unittest import httpx from services.shelly_client import ( ShellySwitchStatus, get_switch_status, parse_switch_status, set_switch, shelly_base_url, ) class ParseSwitchStatusTests(unittest.TestCase): def test_full_gen2_payload(self) -> None: st = parse_switch_status( { "id": 0, "source": "HTTP_in", "output": True, "apower": 745.3, "voltage": 231.2, "current": 3.25, "aenergy": {"total": 12345.678, "by_minute": [123, 120, 118]}, "temperature": {"tC": 41.2}, } ) self.assertEqual( st, ShellySwitchStatus(output=True, apower_w=745.3, aenergy_total_wh=12345.678), ) def test_minimal_payload_without_metering(self) -> None: # Levnější relé bez měření: jen output. st = parse_switch_status({"id": 0, "output": False}) self.assertFalse(st.output) self.assertIsNone(st.apower_w) self.assertIsNone(st.aenergy_total_wh) def test_missing_output_raises(self) -> None: # Gen1 /relay/0 odpověď ('ison') nesmí tiše projít — podporujeme jen Gen2. with self.assertRaises(ValueError): parse_switch_status({"ison": True, "has_timer": False}) def test_zero_values_kept(self) -> None: st = parse_switch_status( {"id": 0, "output": False, "apower": 0.0, "aenergy": {"total": 0.0}} ) self.assertEqual(st.apower_w, 0.0) self.assertEqual(st.aenergy_total_wh, 0.0) class ShellyBaseUrlTests(unittest.TestCase): def test_defaults(self) -> None: self.assertEqual(shelly_base_url(None, "192.168.1.50", None), "http://192.168.1.50:80") def test_https_default_port(self) -> None: self.assertEqual(shelly_base_url("https", "shelly.local", None), "https://shelly.local:443") def test_unknown_protocol_falls_back_to_http(self) -> None: self.assertEqual(shelly_base_url("modbus_tcp", "1.2.3.4", 8080), "http://1.2.3.4:8080") class ShellyRpcTests(unittest.TestCase): """RPC přes httpx.MockTransport — bez sítě.""" def _client(self, handler) -> httpx.AsyncClient: return httpx.AsyncClient(transport=httpx.MockTransport(handler)) def test_get_switch_status(self) -> None: seen: dict[str, str] = {} def handler(request: httpx.Request) -> httpx.Response: seen["path"] = request.url.path seen["id"] = request.url.params.get("id") return httpx.Response( 200, json={"id": 0, "output": True, "apower": 740.0, "aenergy": {"total": 999.5}}, ) async def run() -> ShellySwitchStatus: async with self._client(handler) as client: return await get_switch_status("http://192.168.1.50:80", 0, client=client) st = asyncio.run(run()) self.assertEqual(seen["path"], "/rpc/Switch.GetStatus") self.assertEqual(seen["id"], "0") self.assertTrue(st.output) self.assertEqual(st.apower_w, 740.0) self.assertEqual(st.aenergy_total_wh, 999.5) def test_set_switch_sends_json_bool_and_returns_was_on(self) -> None: seen: dict[str, str] = {} def handler(request: httpx.Request) -> httpx.Response: seen["path"] = request.url.path seen["id"] = request.url.params.get("id") seen["on"] = request.url.params.get("on") return httpx.Response(200, json={"was_on": False}) async def run() -> bool | None: async with self._client(handler) as client: return await set_switch("http://192.168.1.50:80/", True, 0, client=client) was_on = asyncio.run(run()) self.assertEqual(seen["path"], "/rpc/Switch.Set") self.assertEqual(seen["id"], "0") # Gen2 RPC parsuje query jako JSON — bool musí být doslova 'true'/'false'. self.assertEqual(seen["on"], "true") self.assertIs(was_on, False) def test_set_switch_off(self) -> None: seen: dict[str, str] = {} def handler(request: httpx.Request) -> httpx.Response: seen["on"] = request.url.params.get("on") return httpx.Response(200, json={"was_on": True}) async def run() -> bool | None: async with self._client(handler) as client: return await set_switch("http://10.0.0.7", False, client=client) self.assertIs(asyncio.run(run()), True) self.assertEqual(seen["on"], "false") def test_http_error_raises(self) -> None: def handler(request: httpx.Request) -> httpx.Response: return httpx.Response(500, text="boom") async def run() -> None: async with self._client(handler) as client: await get_switch_status("http://10.0.0.7", client=client) with self.assertRaises(httpx.HTTPStatusError): asyncio.run(run()) def test_non_gen2_body_raises_value_error(self) -> None: def handler(request: httpx.Request) -> httpx.Response: # Gen1 odpověď — klient ji odmítne (žádný Gen1 fallback). return httpx.Response(200, content=json.dumps({"ison": True})) async def run() -> None: async with self._client(handler) as client: await get_switch_status("http://10.0.0.7", client=client) with self.assertRaises(ValueError): asyncio.run(run()) if __name__ == "__main__": unittest.main()