Skip to content

Commit 71fe23e

Browse files
KanaduchiKanaduchi
authored andcommitted
#13 Refactor usage of roles to add support for delegated devices
1 parent 8584582 commit 71fe23e

5 files changed

Lines changed: 125 additions & 38 deletions

File tree

src/pymelcloud/client.py

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""MEL API access."""
22
from datetime import datetime, timedelta
33
from typing import Any, Dict, List, Optional
4+
from aiohttp import ClientResponseError
45

56
from aiohttp import ClientSession
67

@@ -172,14 +173,23 @@ async def fetch_device_units(self, device) -> Optional[Dict[Any, Any]]:
172173
173174
User provided info such as indoor/outdoor unit model names and
174175
serial numbers.
176+
If the request returns 403, then ignore it
175177
"""
176-
async with self._session.post(
177-
f"{BASE_URL}/Device/ListDeviceUnits",
178-
headers=_headers(self._token),
179-
json={"deviceId": device.device_id},
180-
raise_for_status=True,
181-
) as resp:
182-
return await resp.json()
178+
try:
179+
async with self._session.post(
180+
f"{BASE_URL}/Device/ListDeviceUnits",
181+
headers=_headers(self._token),
182+
json={"deviceId": device.device_id}
183+
) as resp:
184+
resp.raise_for_status()
185+
data = await resp.json()
186+
if isinstance(data, dict):
187+
return data
188+
return None
189+
except ClientResponseError as e:
190+
if e.status == 403:
191+
return None
192+
raise
183193

184194
async def fetch_device_state(self, device) -> Optional[Dict[Any, Any]]:
185195
"""Fetch state information of a device.
@@ -197,23 +207,32 @@ async def fetch_device_state(self, device) -> Optional[Dict[Any, Any]]:
197207
return await resp.json()
198208

199209
async def fetch_energy_report(self, device) -> Optional[Dict[Any, Any]]:
200-
"""Fetch energy report containing today and 1-2 days from the past."""
210+
"""Fetch energy report containing today and 1-2 days from the past.
211+
If the request returns 403, then ignore it"""
201212
device_id = device.device_id
202213
from_str = (datetime.today() - timedelta(days=2)).strftime("%Y-%m-%d")
203214
to_str = (datetime.today() + timedelta(days=2)).strftime("%Y-%m-%d")
204215

205-
async with self._session.post(
206-
f"{BASE_URL}/EnergyCost/Report",
207-
headers=_headers(self._token),
208-
json={
209-
"DeviceId": device_id,
210-
"UseCurrency": False,
211-
"FromDate": f"{from_str}T00:00:00",
212-
"ToDate": f"{to_str}T00:00:00"
213-
},
214-
raise_for_status=True,
215-
) as resp:
216-
return await resp.json()
216+
try:
217+
async with self._session.post(
218+
f"{BASE_URL}/EnergyCost/Report",
219+
headers=_headers(self._token),
220+
json={
221+
"DeviceId": device_id,
222+
"UseCurrency": False,
223+
"FromDate": f"{from_str}T00:00:00",
224+
"ToDate": f"{to_str}T00:00:00"
225+
}
226+
) as resp:
227+
resp.raise_for_status()
228+
data = await resp.json()
229+
if isinstance(data, dict):
230+
return data
231+
return None
232+
except ClientResponseError as e:
233+
if e.status == 403:
234+
return None
235+
raise
217236

218237
async def set_device_state(self, device):
219238
"""Update device state.

src/pymelcloud/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
DEVICE_TYPE_UNKNOWN = "unknown"
77

88
ACCESS_LEVEL = {
9+
"USER": 2,
910
"GUEST": 3,
1011
"OWNER": 4,
1112
}

src/pymelcloud/device.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ class Device(ABC):
2424
"""MELCloud base device representation."""
2525

2626
def __init__(
27-
self,
28-
device_conf: Dict[str, Any],
29-
client: Client,
30-
set_debounce=timedelta(seconds=1),
27+
self,
28+
device_conf: Dict[str, Any],
29+
client: Client,
30+
set_debounce=timedelta(seconds=1),
3131
):
3232
"""Initialize a device."""
3333
self.device_id = device_conf.get("DeviceID")
@@ -96,9 +96,7 @@ async def update(self):
9696
self._state = await self._client.fetch_device_state(self)
9797
self._energy_report = await self._client.fetch_energy_report(self)
9898

99-
if self._device_units is None and self.access_level != ACCESS_LEVEL.get(
100-
"GUEST"
101-
):
99+
if self._device_units is None:
102100
self._device_units = await self._client.fetch_device_units(self)
103101

104102
async def set(self, properties: Dict[str, Any]):

tests/test_ata_properties.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44

55
import pytest
66
from unittest.mock import AsyncMock, Mock, patch
7-
from aiohttp.web import HTTPForbidden
7+
88
from src.pymelcloud import DEVICE_TYPE_ATA
99

10-
import src.pymelcloud
1110
from src.pymelcloud.const import ACCESS_LEVEL
1211
from src.pymelcloud.ata_device import (
1312
OPERATION_MODE_HEAT,
@@ -92,12 +91,3 @@ async def test_ata():
9291
assert device.wifi_signal == -51
9392
assert device.has_error is False
9493
assert device.error_code == 8000
95-
96-
97-
@pytest.mark.asyncio
98-
async def test_ata_guest():
99-
device = _build_device("ata_guest_listdevices.json", "ata_guest_get.json")
100-
device._client.fetch_device_units = AsyncMock(side_effect=HTTPForbidden)
101-
assert device.device_type == DEVICE_TYPE_ATA
102-
assert device.access_level == ACCESS_LEVEL["GUEST"]
103-
await device.update()

tests/test_client.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Client tests."""
2+
import json
3+
import os
4+
5+
import pytest
6+
from unittest.mock import AsyncMock, Mock, patch
7+
8+
from aiohttp.web import HTTPForbidden
9+
from src.pymelcloud import DEVICE_TYPE_ATA
10+
11+
from src.pymelcloud.const import ACCESS_LEVEL
12+
from src.pymelcloud.ata_device import (AtaDevice)
13+
14+
15+
def _build_device(device_conf_name: str, device_state_name: str) -> AtaDevice:
16+
test_dir = os.path.join(os.path.dirname(__file__), "samples")
17+
with open(os.path.join(test_dir, device_conf_name), "r") as json_file:
18+
device_conf = json.load(json_file)
19+
20+
with open(os.path.join(test_dir, device_state_name), "r") as json_file:
21+
device_state = json.load(json_file)
22+
23+
with patch("src.pymelcloud.client.Client") as _client:
24+
_client.update_confs = AsyncMock()
25+
_client.device_confs.__iter__ = Mock(return_value=[device_conf].__iter__())
26+
_client.fetch_device_units = AsyncMock(return_value=[])
27+
_client.fetch_device_state = AsyncMock(return_value=device_state)
28+
_client.fetch_energy_report = AsyncMock(return_value=None)
29+
client = _client
30+
31+
return AtaDevice(device_conf, client)
32+
33+
34+
@pytest.mark.asyncio
35+
async def test_ata_guest():
36+
device = _build_device("ata_guest_listdevices.json", "ata_guest_get.json")
37+
assert device.device_type == DEVICE_TYPE_ATA
38+
assert device.access_level == ACCESS_LEVEL["GUEST"]
39+
40+
request_info = Mock()
41+
request_info.real_url = "https://example.test/Device/ListDeviceUnits"
42+
43+
device._client.fetch_device_units = AsyncMock(side_effect=HTTPForbidden)
44+
45+
with pytest.raises(HTTPForbidden) as exc:
46+
await device.update()
47+
assert exc.value.status == 403
48+
49+
50+
@pytest.mark.asyncio
51+
async def test_ata_energy_report_403():
52+
device = _build_device("ata_listdevice.json", "ata_get.json")
53+
device._client.fetch_device_state = AsyncMock(return_value={})
54+
device._client.fetch_device_units = AsyncMock(return_value=None)
55+
56+
request_info = Mock()
57+
request_info.real_url = "https://example.test/EnergyCost/Report"
58+
59+
device._client.fetch_energy_report = AsyncMock(side_effect=HTTPForbidden)
60+
61+
with pytest.raises(HTTPForbidden) as exc:
62+
await device.update()
63+
assert exc.value.status == 403
64+
65+
66+
@pytest.mark.asyncio
67+
async def test_ata_device_units_403():
68+
device = _build_device("ata_listdevice.json", "ata_get.json")
69+
assert device.access_level == ACCESS_LEVEL["OWNER"]
70+
device._client.fetch_device_state = AsyncMock(return_value={})
71+
72+
request_info = Mock()
73+
request_info.real_url = "https://example.test/Device/ListDeviceUnits"
74+
75+
device._client.fetch_device_units = AsyncMock(side_effect=HTTPForbidden)
76+
77+
with pytest.raises(HTTPForbidden) as exc:
78+
await device.update()
79+
assert exc.value.status == 403

0 commit comments

Comments
 (0)