Skip to content

Commit 856dba0

Browse files
committed
test: improve coverage
1 parent f8b5709 commit 856dba0

File tree

7 files changed

+232
-47
lines changed

7 files changed

+232
-47
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ dev = [
4141
"mkdocs == 1.6.1",
4242
"mkdocs-material == 9.5.50",
4343
"mkdocstrings == 0.28.1",
44-
"mkdocstrings-python == 1.16.1"
44+
"mkdocstrings-python == 1.16.1",
45+
"python-dateutil == 2.9.0.post0"
4546
]
4647

4748
[tool.pytest.ini_options]

src/ecactus/async_client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ async def login(
6666
except ApiResponseError as err:
6767
if err.code == 20414:
6868
raise AuthenticationError from err
69+
if err.code == 20000:
70+
raise AuthenticationError("Missing Account or Password") from err
71+
raise
6972
self.access_token = data["accessToken"]
7073
self.refresh_token = data["refreshToken"]
7174

src/ecactus/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ def login(
6666
except ApiResponseError as err:
6767
if err.code == 20414:
6868
raise AuthenticationError from err
69+
if err.code == 20000:
70+
raise AuthenticationError("Missing Account or Password") from err
71+
raise
6972
self.access_token = data["accessToken"]
7073
self.refresh_token = data["refreshToken"]
7174

src/ecactus/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ class ParameterVerificationFailedError(EcosApiError):
9292
def __init__(self, message: str | None = None) -> None:
9393
"""Initialize the exception with a default error message."""
9494
if message is None:
95-
super().__init__("Parameter Verification Failed")
95+
super().__init__("Parameter verification failed")
9696
else:
97-
super().__init__(f"Parameter Verification Failed: {message}")
97+
super().__init__(f"Parameter verification failed: {message}")
9898

9999

100100
class InvalidJsonError(EcosApiError):

tests/mock_server.py

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Any
99

1010
from aiohttp import web
11+
from dateutil.relativedelta import relativedelta
1112
from multidict import CIMultiDictProxy
1213

1314
# Configure logging
@@ -108,7 +109,7 @@ def _not_implemented_response() -> web.Response:
108109
async def handle_login(self, request: web.Request) -> web.Response:
109110
"""Mock login endpoint."""
110111
output: JSON = {}
111-
data: dict = await request.json() # Parse the JSON payload
112+
data = await request.json() # Parse the JSON payload
112113
if (
113114
not data.get("clientVersion")
114115
or data.get("clientType") != "BROWSER"
@@ -423,45 +424,84 @@ async def handle_get_insight(self, request: web.Request) -> web.Response:
423424
return EcosMockServer._ok_response(
424425
code=20404, message="Parameter verification failed", success=False
425426
)
426-
if request_payload.get("periodType") != 0:
427-
return EcosMockServer._not_implemented_response()
428-
start_date = datetime.fromtimestamp(
429-
int(request_payload.get("timestamp") / 1000)
430-
).replace(
431-
hour=0, minute=0, second=0, microsecond=0
432-
) # convert timestamp (in milliseconds) to datetime
433-
end_date = start_date + timedelta(days=1)
434-
fake_data: dict[str, float] = await self._generate_metrics_data(
435-
start_date, end_date
436-
)
437-
latest_timestamp = max(fake_data.keys())
438-
fake_data[str(int(latest_timestamp) - 1)] = fake_data.pop(
439-
latest_timestamp
440-
) # rename the latest timestamp by (timestamp - 1 sec)
441-
442-
return EcosMockServer._success_response(
443-
data={
444-
"selfPowered": 31.0,
445-
"deviceRealtimeDto": {
446-
"solarPowerDps": fake_data,
447-
"batteryPowerDps": fake_data,
448-
"gridPowerDps": fake_data,
449-
"meterPowerDps": fake_data,
450-
"homePowerDps": fake_data,
451-
"epsPowerDps": fake_data,
452-
},
453-
"deviceStatisticsDto": {
454-
"consumptionEnergy": 42.5,
455-
"fromBattery": 0.0,
456-
"toBattery": 0.0,
457-
"fromGrid": 29.2,
458-
"toGrid": 6.3,
459-
"fromSolar": 19.6,
460-
"eps": 0.0,
461-
},
462-
"insightConsumptionDataDto": None,
463-
}
464-
)
427+
if request_payload.get("periodType") == 0: # 5-minute power measurement for the provided day
428+
start_date = datetime.fromtimestamp(
429+
int(request_payload.get("timestamp", int(datetime.now().timestamp()*1000)) / 1000)
430+
).replace(
431+
hour=0, minute=0, second=0, microsecond=0
432+
) # convert timestamp (in milliseconds) to datetime
433+
end_date = start_date + timedelta(days=1)
434+
fake_data: dict[str, float] = await self._generate_metrics_data(
435+
start_date, end_date
436+
)
437+
latest_timestamp = max(fake_data.keys())
438+
fake_data[str(int(latest_timestamp) - 1)] = fake_data.pop(
439+
latest_timestamp
440+
) # rename the latest timestamp by (timestamp - 1 sec)
441+
return EcosMockServer._success_response(
442+
data={
443+
"selfPowered": 31.0,
444+
"deviceRealtimeDto": {
445+
"solarPowerDps": fake_data,
446+
"batteryPowerDps": fake_data,
447+
"gridPowerDps": fake_data,
448+
"meterPowerDps": fake_data,
449+
"homePowerDps": fake_data,
450+
"epsPowerDps": fake_data,
451+
},
452+
"deviceStatisticsDto": {
453+
"consumptionEnergy": 42.5,
454+
"fromBattery": 0.0,
455+
"toBattery": 0.0,
456+
"fromGrid": 29.2,
457+
"toGrid": 6.3,
458+
"fromSolar": 19.6,
459+
"eps": 0.0,
460+
},
461+
"insightConsumptionDataDto": None,
462+
}
463+
)
464+
if request_payload.get("periodType") == 2: # daily energy for the provided month
465+
start_date = datetime.fromtimestamp(
466+
int(request_payload.get("timestamp", int(datetime.now().timestamp()*1000)) / 1000)
467+
).replace(
468+
day=1, hour=0, minute=0, second=0, microsecond=0
469+
) # convert timestamp (in milliseconds) to datetime
470+
end_date = start_date + relativedelta(months=1)
471+
fake_data = await self._generate_metrics_data(
472+
start_date, end_date, interval=timedelta(days=1)
473+
)
474+
latest_timestamp = max(fake_data.keys())
475+
fake_data[str(int(latest_timestamp) - 1)] = fake_data.pop(
476+
latest_timestamp
477+
) # rename the latest timestamp by (timestamp - 1 sec)
478+
return EcosMockServer._success_response(
479+
data={
480+
"selfPowered": 31.0,
481+
"deviceRealtimeDto": None,
482+
"deviceStatisticsDto": {
483+
"consumptionEnergy": 42.5,
484+
"fromBattery": 0.0,
485+
"toBattery": 0.0,
486+
"fromGrid": 29.2,
487+
"toGrid": 6.3,
488+
"fromSolar": 19.6,
489+
"eps": 0.0,
490+
},
491+
"insightConsumptionDataDto": {
492+
"fromBatteryDps": fake_data,
493+
"toBatteryDps": fake_data,
494+
"fromGridDps": fake_data,
495+
"toGridDps": fake_data,
496+
"fromSolarDps": fake_data,
497+
"homeEnergyDps": fake_data,
498+
"epsDps": fake_data,
499+
"selfPoweredDps": fake_data,
500+
},
501+
}
502+
)
503+
# if request_payload.get("periodType") not in (0, 2):
504+
return EcosMockServer._not_implemented_response()
465505

466506
async def catch_all(self, request: web.Request) -> web.Response:
467507
"""Catch all endpoint."""

tests/test_async_client.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Unit tests for asynchronous Ecos class."""
22

3-
from datetime import datetime
3+
from datetime import datetime, timedelta
44
import logging
55

66
import pytest
@@ -9,7 +9,9 @@
99
from ecactus.exceptions import (
1010
AuthenticationError,
1111
HomeDoesNotExistError,
12+
HttpError,
1213
InitializationError,
14+
InvalidJsonError,
1315
ParameterVerificationFailedError,
1416
UnauthorizedDeviceError,
1517
UnauthorizedError,
@@ -34,6 +36,28 @@ def bad_client(mock_server):
3436
return ecactus.AsyncEcos(url=mock_server.url, access_token="wrong_token")
3537

3638

39+
def test_exceptions():
40+
"""Test exceptions."""
41+
exc = InitializationError()
42+
assert str(exc) == "Initialization error"
43+
exc = AuthenticationError()
44+
assert str(exc) == "Account or password or country error"
45+
exc = UnauthorizedError()
46+
assert str(exc) == "Unauthorized"
47+
exc = HomeDoesNotExistError()
48+
assert str(exc) == "Home does not exist"
49+
exc = HomeDoesNotExistError("home_id")
50+
assert str(exc) == "Home does not exist: home_id"
51+
exc = UnauthorizedDeviceError()
52+
assert str(exc) == "Device is not authorized"
53+
exc = ParameterVerificationFailedError()
54+
assert str(exc) == "Parameter verification failed"
55+
exc = InvalidJsonError()
56+
assert str(exc) == "Invalid JSON"
57+
exc = HttpError(404, "Not Found")
58+
assert str(exc) == "HTTP error: 404 Not Found"
59+
60+
3761
def test_client():
3862
"""Test ECOS client."""
3963
with pytest.raises(InitializationError):
@@ -44,6 +68,17 @@ def test_client():
4468
assert "weiheng-tech.com" in client.url
4569

4670

71+
async def test_ensure_login(mock_server):
72+
"""Test autologin."""
73+
temp_client = ecactus.AsyncEcos(url=mock_server.url)
74+
with pytest.raises(AuthenticationError) as excinfo:
75+
user = await temp_client.get_user()
76+
assert str(excinfo.value) == "Missing Account or Password"
77+
temp_client = ecactus.AsyncEcos(email=LOGIN, password=PASSWORD, url=mock_server.url)
78+
user = await temp_client.get_user()
79+
assert user.username == LOGIN
80+
81+
4782
async def test_login(mock_server, client):
4883
"""Test login."""
4984
with pytest.raises(AuthenticationError):
@@ -95,7 +130,32 @@ async def test_get_today_device_data(client, bad_client):
95130
await client.get_today_device_data(device_id=0)
96131
power_ts = await client.get_today_device_data(device_id=1234567890123456789)
97132
assert len(power_ts.metrics) > 0
98-
133+
# get the first timestamp
134+
first_timestamp = power_ts.metrics[0].timestamp
135+
assert power_ts.find_by_timestamp(first_timestamp, exact=False).solar is not None
136+
assert power_ts.find_by_timestamp(first_timestamp, exact=True).solar is not None
137+
# get a timestamp that does not exist
138+
before_timestamp = first_timestamp - timedelta(seconds=1)
139+
assert power_ts.find_by_timestamp(before_timestamp, exact=False).solar == power_ts.metrics[0].solar # the nearst metric is returned
140+
assert power_ts.find_by_timestamp(before_timestamp, exact=True) is None # exact lookup returns None
141+
# get a timestamp after the last one
142+
last_timestamp = power_ts.metrics[-1].timestamp
143+
after_timestamp = last_timestamp + timedelta(seconds=1)
144+
assert power_ts.find_by_timestamp(after_timestamp, exact=False).solar == power_ts.metrics[-1].solar # value is 1/10th of the position
145+
assert power_ts.find_by_timestamp(after_timestamp, exact=True) is None
146+
# get a timestamp within 2 existings
147+
if len(power_ts.metrics) > 2:
148+
timestamp1 = power_ts.metrics[1].timestamp
149+
timestamp2 = power_ts.metrics[2].timestamp
150+
delta = timestamp2 - timestamp1
151+
if delta >= timedelta(seconds=2):
152+
timestamp = timestamp1 + delta/2
153+
assert power_ts.find_by_timestamp(timestamp, exact=False).solar == power_ts.metrics[1].solar # returns the value in 2nd position
154+
timestamp = timestamp1 + delta/2 + timedelta(seconds=1)
155+
assert power_ts.find_by_timestamp(timestamp, exact=False).solar == power_ts.metrics[2].solar # returns the value in 3rd position
156+
# return series between 2 dates
157+
between_ts = power_ts.find_between(first_timestamp, last_timestamp)
158+
assert len(between_ts.metrics) == len(power_ts.metrics)
99159

100160
async def test_get_realtime_device_data(client, bad_client):
101161
"""Test get realtime device data."""
@@ -147,10 +207,19 @@ async def test_get_insight(client, bad_client):
147207
await client.get_insight(
148208
device_id=1234567890123456789, start_date=now, period_type=1
149209
)
210+
with pytest.raises(ParameterVerificationFailedError):
211+
await client.get_insight(
212+
device_id=1234567890123456789, period_type=1
213+
)
150214
insight = await client.get_insight(
151215
device_id=1234567890123456789, start_date=now, period_type=0
152216
)
153217
assert len(insight.power_timeseries.metrics) > 1
218+
insight = await client.get_insight(
219+
device_id=1234567890123456789, start_date=now, period_type=2
220+
)
221+
assert len(insight.energy_timeseries.metrics) > 1
222+
154223

155224

156225
# TODO test 404

0 commit comments

Comments
 (0)