Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions pycheckwatt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,21 +755,38 @@ async def get_ems_settings(self, rpi_serial=None):
async def get_price_zone(self):
"""Fetch Price Zone from CheckWatt."""

serial = self.rpi_serial
if not serial:
_LOGGER.error("Cannot fetch price zone without an RPi serial")
return False

try:
endpoint = "/ems/pricezone"
endpoint = f"/site/Statuses?serial={serial}"
# Define headers with the JwtToken
headers = {
**self._get_headers(),
"authorization": f"Bearer {self.jwt_token}",
}

# First fetch the revenue
# The site status contains the market bidding area (price zone).
async with self.session.get(
self.base_url + endpoint, headers=headers
) as response:
response.raise_for_status()
if response.status == 200:
self.price_zone = await response.text()
statuses = await response.json()
if (
not isinstance(statuses, list)
or not statuses
or not isinstance(statuses[0], dict)
):
_LOGGER.error("Invalid site status response for price zone")
return False
price_zone = statuses[0].get("Mba")
if not isinstance(price_zone, str) or not price_zone.strip():
_LOGGER.error("Site status contains no valid price zone")
return False
self.price_zone = price_zone.strip()
return True

_LOGGER.error(
Expand All @@ -790,7 +807,8 @@ async def get_spot_price(self):
end_date = datetime.now() + timedelta(days=1)
to_date = end_date.strftime("%Y-%m-%d")
if self.price_zone is None:
await self.get_price_zone()
if not await self.get_price_zone():
return False
endpoint = f"/ems/spotprice?zone={self.price_zone}&fromDate={from_date}&toDate={to_date}" # noqa: E501
# Define headers with the JwtToken
headers = {
Expand Down
94 changes: 94 additions & 0 deletions tests/unit/test_price_zone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Regression coverage for price zones obtained from site statuses."""

from unittest.mock import AsyncMock, Mock

import pytest
from aiohttp import ClientResponseError

from pycheckwatt import CheckwattManager


@pytest.fixture
def manager():
manager = CheckwattManager("test_user", "test_pass")
manager.customer_details = {"Meter": [{"RpiSerial": "abc123"}]}
manager.jwt_token = "test-token"
manager.session = Mock()
return manager


def mock_response(manager, payload):
response = Mock(status=200)
response.json = AsyncMock(return_value=payload)
context = manager.session.get.return_value
context.__aenter__ = AsyncMock(return_value=response)
context.__aexit__ = AsyncMock(return_value=False)
return response


@pytest.mark.asyncio
async def test_site_status_zone_is_used_for_spot_prices(manager):
response = mock_response(manager, None)
prices = {"Prices": [{"Price": 0.42}]}
response.json.side_effect = [[{"Mba": "SE3"}], prices]

assert await manager.get_spot_price() is True
assert manager.price_zone == "SE3"
assert manager.spot_prices == prices
requests = manager.session.get.call_args_list
assert len(requests) == 2
assert requests[0].args[0] == (
"https://api.checkwatt.se/site/Statuses?serial=ABC123"
)
assert requests[0].kwargs["headers"]["authorization"] == "Bearer test-token"
assert (
requests[1]
.args[0]
.startswith("https://api.checkwatt.se/ems/spotprice?zone=SE3&")
)


@pytest.mark.asyncio
@pytest.mark.parametrize(
"payload",
[
None,
{},
[],
[None],
[{}],
[{"Mba": None}],
[{"Mba": ""}],
[{"Mba": " "}],
[{"Mba": 3}],
],
)
async def test_invalid_zone_stops_spot_price_request(manager, payload):
mock_response(manager, payload)

assert await manager.get_spot_price() is False
assert manager.price_zone is None
manager.session.get.assert_called_once()


@pytest.mark.asyncio
async def test_missing_serial_does_not_send_request(manager):
manager.customer_details = {"Meter": []}

assert await manager.get_price_zone() is False
manager.session.get.assert_not_called()


@pytest.mark.asyncio
async def test_http_failure_stops_spot_price_request(manager):
response = mock_response(manager, None)
response.raise_for_status.side_effect = ClientResponseError(
request_info=Mock(real_url="https://api.checkwatt.se/site/Statuses"),
history=(),
status=404,
)

assert await manager.get_spot_price() is False
assert manager.price_zone is None
manager.session.get.assert_called_once()
response.json.assert_not_awaited()
Loading