diff --git a/src/pymelcloud/client.py b/src/pymelcloud/client.py index b182362..52e6eff 100644 --- a/src/pymelcloud/client.py +++ b/src/pymelcloud/client.py @@ -1,6 +1,7 @@ """MEL API access.""" from datetime import datetime, timedelta from typing import Any, Dict, List, Optional +from aiohttp import ClientResponseError, ContentTypeError from aiohttp import ClientSession @@ -172,14 +173,26 @@ async def fetch_device_units(self, device) -> Optional[Dict[Any, Any]]: User provided info such as indoor/outdoor unit model names and serial numbers. + If the request returns 403, then ignore it """ - async with self._session.post( - f"{BASE_URL}/Device/ListDeviceUnits", - headers=_headers(self._token), - json={"deviceId": device.device_id}, - raise_for_status=True, - ) as resp: - return await resp.json() + try: + async with self._session.post( + f"{BASE_URL}/Device/ListDeviceUnits", + headers=_headers(self._token), + json={"deviceId": device.device_id} + ) as resp: + resp.raise_for_status() + try: + data = await resp.json() + except ContentTypeError: + return None + if isinstance(data, dict): + return data + return None + except ClientResponseError as e: + if e.status == 403: + return None + raise async def fetch_device_state(self, device) -> Optional[Dict[Any, Any]]: """Fetch state information of a device. @@ -197,23 +210,35 @@ async def fetch_device_state(self, device) -> Optional[Dict[Any, Any]]: return await resp.json() async def fetch_energy_report(self, device) -> Optional[Dict[Any, Any]]: - """Fetch energy report containing today and 1-2 days from the past.""" + """Fetch energy report containing today and 1-2 days from the past. + If the request returns 403, then ignore it""" device_id = device.device_id from_str = (datetime.today() - timedelta(days=2)).strftime("%Y-%m-%d") to_str = (datetime.today() + timedelta(days=2)).strftime("%Y-%m-%d") - async with self._session.post( - f"{BASE_URL}/EnergyCost/Report", - headers=_headers(self._token), - json={ - "DeviceId": device_id, - "UseCurrency": False, - "FromDate": f"{from_str}T00:00:00", - "ToDate": f"{to_str}T00:00:00" - }, - raise_for_status=True, - ) as resp: - return await resp.json() + try: + async with self._session.post( + f"{BASE_URL}/EnergyCost/Report", + headers=_headers(self._token), + json={ + "DeviceId": device_id, + "UseCurrency": False, + "FromDate": f"{from_str}T00:00:00", + "ToDate": f"{to_str}T00:00:00" + } + ) as resp: + resp.raise_for_status() + try: + data = await resp.json() + except ContentTypeError: + return None + if isinstance(data, dict): + return data + return None + except ClientResponseError as e: + if e.status == 403: + return None + raise async def set_device_state(self, device): """Update device state. diff --git a/src/pymelcloud/const.py b/src/pymelcloud/const.py index eb12ee0..0bf3a5a 100644 --- a/src/pymelcloud/const.py +++ b/src/pymelcloud/const.py @@ -6,6 +6,7 @@ DEVICE_TYPE_UNKNOWN = "unknown" ACCESS_LEVEL = { + "USER": 2, "GUEST": 3, "OWNER": 4, } diff --git a/src/pymelcloud/device.py b/src/pymelcloud/device.py index c98eb18..336fd6d 100644 --- a/src/pymelcloud/device.py +++ b/src/pymelcloud/device.py @@ -24,10 +24,10 @@ class Device(ABC): """MELCloud base device representation.""" def __init__( - self, - device_conf: Dict[str, Any], - client: Client, - set_debounce=timedelta(seconds=1), + self, + device_conf: Dict[str, Any], + client: Client, + set_debounce=timedelta(seconds=1), ): """Initialize a device.""" self.device_id = device_conf.get("DeviceID") @@ -96,9 +96,7 @@ async def update(self): self._state = await self._client.fetch_device_state(self) self._energy_report = await self._client.fetch_energy_report(self) - if self._device_units is None and self.access_level != ACCESS_LEVEL.get( - "GUEST" - ): + if self._device_units is None: self._device_units = await self._client.fetch_device_units(self) async def set(self, properties: Dict[str, Any]): diff --git a/tests/test_ata_properties.py b/tests/test_ata_properties.py index 0801001..1072473 100644 --- a/tests/test_ata_properties.py +++ b/tests/test_ata_properties.py @@ -4,10 +4,9 @@ import pytest from unittest.mock import AsyncMock, Mock, patch -from aiohttp.web import HTTPForbidden + from src.pymelcloud import DEVICE_TYPE_ATA -import src.pymelcloud from src.pymelcloud.const import ACCESS_LEVEL from src.pymelcloud.ata_device import ( OPERATION_MODE_HEAT, @@ -92,12 +91,3 @@ async def test_ata(): assert device.wifi_signal == -51 assert device.has_error is False assert device.error_code == 8000 - - -@pytest.mark.asyncio -async def test_ata_guest(): - device = _build_device("ata_guest_listdevices.json", "ata_guest_get.json") - device._client.fetch_device_units = AsyncMock(side_effect=HTTPForbidden) - assert device.device_type == DEVICE_TYPE_ATA - assert device.access_level == ACCESS_LEVEL["GUEST"] - await device.update() diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..3db3d56 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,63 @@ +"""Client tests.""" +import pytest +from unittest.mock import AsyncMock, Mock, patch +from aiohttp import ClientResponseError, ClientSession +from pymelcloud.client import Client + + +@pytest.mark.asyncio +async def test_fetch_energy_report_ignores_403(): + session = Mock(spec=ClientSession) + cm = AsyncMock() + session.post.return_value = cm + + resp = Mock() + cm.__aenter__.return_value = resp + + request_info = Mock() + request_info.real_url = "https://example.test/EnergyCost/Report" + + resp.raise_for_status.side_effect = ClientResponseError( + request_info=request_info, + history=(), + status=403, + message="Forbidden", + ) + + client = Client(token="dummy", session=session) + + class DummyDevice: + device_id = 123 + + device = DummyDevice() + result = await client.fetch_energy_report(device) + assert result is None + + +@pytest.mark.asyncio +async def test_fetch_device_units_ignores_403(): + session = Mock(spec=ClientSession) + cm = AsyncMock() + session.post.return_value = cm + + resp = Mock() + cm.__aenter__.return_value = resp + + request_info = Mock() + request_info.real_url = "https://example.test/Device/ListDeviceUnits" + + resp.raise_for_status.side_effect = ClientResponseError( + request_info=request_info, + history=(), + status=403, + message="Forbidden", + ) + + client = Client(token="dummy", session=session) + + class DummyDevice: + device_id = 123 + + device = DummyDevice() + result = await client.fetch_device_units(device) + assert result is None