diff --git a/PyMyGekko/__init__.py b/PyMyGekko/__init__.py index ea21957..dcfae23 100644 --- a/PyMyGekko/__init__.py +++ b/PyMyGekko/__init__.py @@ -13,6 +13,8 @@ from PyMyGekko.resources.AlarmsLogics import AlarmsLogicValueAccessor from PyMyGekko.resources.Blinds import Blind from PyMyGekko.resources.Blinds import BlindValueAccessor +from PyMyGekko.resources.Cams import Cam +from PyMyGekko.resources.Cams import CamValueAccessor from PyMyGekko.resources.EnergyCosts import EnergyCost from PyMyGekko.resources.EnergyCosts import EnergyCostValueAccessor from PyMyGekko.resources.HotWaterSystems import HotWaterSystem @@ -70,6 +72,7 @@ def __init__( self._data_provider ) self._blind_value_accessor = BlindValueAccessor(self._data_provider) + self._cam_value_accessor = CamValueAccessor(self._data_provider) self._energy_costs_value_accessor = EnergyCostValueAccessor(self._data_provider) self._hot_water_systems_value_accessor = HotWaterSystemValueAccessor( self._data_provider @@ -120,6 +123,10 @@ def get_blinds(self) -> list[Blind]: """Returns the MyGekko blinds""" return self._blind_value_accessor.blinds + def get_cams(self) -> list[Cam]: + """Returns the MyGekko cams""" + return self._cam_value_accessor.cams + def get_energy_costs(self) -> list[EnergyCost]: """Returns the MyGekko energy_costs""" return self._energy_costs_value_accessor.energy_costs diff --git a/PyMyGekko/resources/Cams.py b/PyMyGekko/resources/Cams.py new file mode 100644 index 0000000..f0f8359 --- /dev/null +++ b/PyMyGekko/resources/Cams.py @@ -0,0 +1,107 @@ +"""MyGekko Cams implementation""" +from __future__ import annotations + +from enum import IntEnum + +from PyMyGekko.data_provider import DataProviderBase +from PyMyGekko.data_provider import EntityValueAccessor +from PyMyGekko.resources import Entity + + +class Cam(Entity): + """Class for MyGekko Cam""" + + def __init__( + self, entity_id: str, name: str, value_accessor: CamValueAccessor + ) -> None: + super().__init__(entity_id, name, "/cams/") + self._value_accessor = value_accessor + self._supported_features = self._value_accessor.get_features(self) + + @property + def supported_features(self) -> list[CamFeature]: + """Returns the supported features""" + return self._supported_features + + @property + def image_url(self) -> str | None: + """Returns the image url""" + return self._value_accessor.get_value(self, "imagepath") + + @property + def stream_url(self) -> str | None: + """Returns the stream url""" + return self._value_accessor.get_value(self, "streampath") + + +class CamNewRecordAvailableState(IntEnum): + """MyGekko Cams Record Available State""" + + NO = 0 + YES = 1 + + +class CamFeature(IntEnum): + """MyGekko Cams Feature""" + + ON_OFF = 0 + STREAM = 1 + + +class CamValueAccessor(EntityValueAccessor): + """Cam value accessor""" + + def __init__(self, data_provider: DataProviderBase): + self._data = {} + self._data_provider = data_provider + self._data_provider.subscribe(self) + + def update_status(self, status, hardware): + if status is not None and "cams" in status: + cams = status["cams"] + for key in cams: + if key.startswith("item"): + if key not in self._data: + self._data[key] = {} + + if "sumstate" in cams[key] and "value" in cams[key]["sumstate"]: + ( + self._data[key]["newRecordsAvailableState"], + *_other, + ) = cams[ + key + ]["sumstate"]["value"].split( + ";", + ) + + def update_resources(self, resources): + if resources is not None and "cams" in resources: + cams = resources["cams"] + for key in cams: + if key.startswith("item"): + if key not in self._data: + self._data[key] = {} + self._data[key]["name"] = cams[key]["name"] + self._data[key]["imagepath"] = cams[key]["imagepath"] + self._data[key]["streampath"] = cams[key]["streampath"] + + @property + def cams(self): + """Returns the cams read from MyGekko""" + result: list[Cam] = [] + for key, data in self._data.items(): + result.append(Cam(key, data["name"], self)) + + return result + + def get_features(self, door: Cam) -> list[CamFeature]: + """Returns the supported features""" + result = list() + + if door and door.entity_id: + if door.entity_id in self._data: + data = self._data[door.entity_id] + if "streampath" in data and data["streampath"]: + result.append(CamFeature.STREAM) + + return result diff --git a/README.md b/README.md index fdd9b67..6962103 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,10 @@ hatch build ### Release - Increase version, then + ``` - hatch build - hatch publish -u __token__ -a +hatch build +hatch publish -u __token__ -a ``` --- diff --git a/tests/cams/data/api_var_response_879015.json b/tests/cams/data/api_var_response_879015.json new file mode 100644 index 0000000..0a1177d --- /dev/null +++ b/tests/cams/data/api_var_response_879015.json @@ -0,0 +1,26 @@ +{ + "cams": { + "item2": { + "name": "DoorBird", + "page": "Eingang", + "imagepath": "http://XXX@192.168.1.50/bha-api/image.cgi", + "streampath": "http://XXX@192.168.1.50/bha-api/video.cgi", + "sumstate": { + "description": "Summary of the states and their formats", + "format": "newRecordsAvailableState enum[0=no,1=yes](); ", + "type": "STRING", + "permission": "READ" + } + }, + "group0": { + "name": "Grp 1", + "sumstate": { + "description": "Summary of the group states and their formats", + "format": "State[0=Off|1=On]", + "type": "AO", + "permission": "READ", + "index": 7500000 + } + } + } +} diff --git a/tests/cams/data/api_var_status_response_879015.json b/tests/cams/data/api_var_status_response_879015.json new file mode 100644 index 0000000..afe5c3a --- /dev/null +++ b/tests/cams/data/api_var_status_response_879015.json @@ -0,0 +1,30 @@ +{ + "globals": { + "network": { + "gekkoname": { + "value": "myGEKKO" + }, + "language": { + "value": "0" + }, + "version": { + "value": "879015" + }, + "hardware": { + "value": "Slide 2 (AC0DFE3012E6)" + } + } + }, + "cams": { + "item2": { + "sumstate": { + "value": "0;" + } + }, + "group0": { + "sumstate": { + "value": "1" + } + } + } +} diff --git a/tests/cams/test_pymygekko_cams_879015.py b/tests/cams/test_pymygekko_cams_879015.py new file mode 100644 index 0000000..51a5651 --- /dev/null +++ b/tests/cams/test_pymygekko_cams_879015.py @@ -0,0 +1,55 @@ +import logging + +import pytest +from aiohttp import ClientSession +from aiohttp import web +from PyMyGekko import MyGekkoApiClientBase +from PyMyGekko.resources.Cams import CamFeature + +_LOGGER: logging.Logger = logging.getLogger(__name__) + + +async def var_response(request): + varResponseFile = open("tests/cams/data/api_var_response_879015.json") + return web.Response(status=200, body=varResponseFile.read()) + + +async def var_status_response(request): + statusResponseFile = open("tests/cams/data/api_var_status_response_879015.json") + return web.Response(status=200, body=statusResponseFile.read()) + + +@pytest.fixture +def mock_server(aiohttp_server): + app = web.Application() + app.router.add_get("/api/v1/var", var_response) + app.router.add_get("/api/v1/var/status", var_status_response) + return aiohttp_server(app) + + +@pytest.mark.asyncio +async def test_get_cams(mock_server): + _LOGGER.setLevel(logging.DEBUG) + + server = await mock_server + async with ClientSession() as session: + api = MyGekkoApiClientBase( + {}, + session, + scheme=server.scheme, + host=server.host, + port=server.port, + ) + + await api.read_data() + cams = api.get_cams() + + assert cams is not None + assert len(cams) == 1 + + assert cams[0].entity_id == "item2" + assert cams[0].name == "DoorBird" + assert cams[0].image_url == "http://XXX@192.168.1.50/bha-api/image.cgi" + assert cams[0].stream_url == "http://XXX@192.168.1.50/bha-api/video.cgi" + assert len(cams[0].supported_features) == 1 + assert CamFeature.STREAM in cams[0].supported_features