Skip to content

Commit

Permalink
Fix Nord Pool empty response (#134033)
Browse files Browse the repository at this point in the history
* Fix Nord Pool empty response

* Mods

* reset validate prices
  • Loading branch information
gjohansson-ST authored Dec 28, 2024
1 parent b3aede6 commit 645f2e4
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 24 deletions.
17 changes: 9 additions & 8 deletions homeassistant/components/nordpool/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import asyncio
from collections.abc import Callable
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -73,7 +72,7 @@ async def fetch_data(self, now: datetime) -> None:
self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow())
)
data = await self.api_call()
if data:
if data and data.entries:
self.async_set_updated_data(data)

async def api_call(self, retry: int = 3) -> DeliveryPeriodsData | None:
Expand All @@ -90,18 +89,20 @@ async def api_call(self, retry: int = 3) -> DeliveryPeriodsData | None:
self.config_entry.data[CONF_AREAS],
)
except (
NordPoolEmptyResponseError,
NordPoolResponseError,
NordPoolError,
) as error:
LOGGER.debug("Connection error: %s", error)
if retry > 0:
next_run = (4 - retry) * 15
LOGGER.debug("Wait %d seconds for next try", next_run)
await asyncio.sleep(next_run)
return await self.api_call(retry - 1)
self.async_set_update_error(error)

if data:
current_day = dt_util.utcnow().strftime("%Y-%m-%d")
for entry in data.entries:
if entry.requested_date == current_day:
LOGGER.debug("Data for current day found")
return data

self.async_set_update_error(NordPoolEmptyResponseError("No current day data"))
return data

def merge_price_entries(self) -> list[DeliveryPeriodEntry]:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/nordpool/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"iot_class": "cloud_polling",
"loggers": ["pynordpool"],
"quality_scale": "platinum",
"requirements": ["pynordpool==0.2.3"],
"requirements": ["pynordpool==0.2.4"],
"single_config_entry": true
}
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2118,7 +2118,7 @@ pynetio==0.1.9.1
pynobo==1.8.1

# homeassistant.components.nordpool
pynordpool==0.2.3
pynordpool==0.2.4

# homeassistant.components.nuki
pynuki==1.6.3
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1720,7 +1720,7 @@ pynetgear==0.10.10
pynobo==1.8.1

# homeassistant.components.nordpool
pynordpool==0.2.3
pynordpool==0.2.4

# homeassistant.components.nuki
pynuki==1.6.3
Expand Down
8 changes: 0 additions & 8 deletions tests/components/nordpool/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from collections.abc import AsyncGenerator
import json
from typing import Any
from unittest.mock import patch

from pynordpool import API, NordPoolClient
import pytest
Expand All @@ -20,13 +19,6 @@
from tests.test_util.aiohttp import AiohttpClientMocker


@pytest.fixture(autouse=True)
async def no_sleep() -> AsyncGenerator[None]:
"""No sleeping."""
with patch("homeassistant.components.nordpool.coordinator.asyncio.sleep"):
yield


@pytest.fixture
async def load_int(hass: HomeAssistant, get_client: NordPoolClient) -> MockConfigEntry:
"""Set up the Nord Pool integration in Home Assistant."""
Expand Down
9 changes: 5 additions & 4 deletions tests/components/nordpool/test_coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4
assert mock_data.call_count == 1
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE

Expand All @@ -69,7 +69,7 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4
assert mock_data.call_count == 1
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE
assert "Authentication error" in caplog.text
Expand All @@ -84,7 +84,8 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4
# Empty responses does not raise
assert mock_data.call_count == 3
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE
assert "Empty response" in caplog.text
Expand All @@ -99,7 +100,7 @@ async def test_coordinator(
freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
assert mock_data.call_count == 4
assert mock_data.call_count == 1
state = hass.states.get("sensor.nord_pool_se3_current_price")
assert state.state == STATE_UNAVAILABLE
assert "Response error" in caplog.text
Expand Down
139 changes: 138 additions & 1 deletion tests/components/nordpool/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@

from __future__ import annotations

from datetime import timedelta
from http import HTTPStatus
from typing import Any

from freezegun.api import FrozenDateTimeFactory
from pynordpool import API
import pytest
from syrupy.assertion import SnapshotAssertion

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er

from tests.common import snapshot_platform
from tests.common import async_fire_time_changed, snapshot_platform
from tests.test_util.aiohttp import AiohttpClientMocker


@pytest.mark.freeze_time("2024-11-05T18:00:00+00:00")
Expand Down Expand Up @@ -59,3 +67,132 @@ async def test_sensor_no_previous_price(
assert current_price.state == "0.12666" # SE3 2024-11-05T23:00:00Z
assert last_price.state == "0.28914" # SE3 2024-11-05T22:00:00Z
assert next_price.state == "0.07406" # SE3 2024-11-06T00:00:00Z


@pytest.mark.freeze_time("2024-11-05T11:00:01+01:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_sensor_empty_response(
hass: HomeAssistant,
load_int: ConfigEntry,
load_json: list[dict[str, Any]],
aioclient_mock: AiohttpClientMocker,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test the Nord Pool sensor with empty response."""

responses = list(load_json)

current_price = hass.states.get("sensor.nord_pool_se3_current_price")
last_price = hass.states.get("sensor.nord_pool_se3_previous_price")
next_price = hass.states.get("sensor.nord_pool_se3_next_price")
assert current_price is not None
assert last_price is not None
assert next_price is not None
assert current_price.state == "0.92737"
assert last_price.state == "1.03132"
assert next_price.state == "0.92505"

aioclient_mock.clear_requests()
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-04",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[1],
)
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-05",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[0],
)
# Future date without data should return 204
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-06",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
status=HTTPStatus.NO_CONTENT,
)

freezer.tick(timedelta(hours=1))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)

# All prices should be known as tomorrow is not loaded by sensors

current_price = hass.states.get("sensor.nord_pool_se3_current_price")
last_price = hass.states.get("sensor.nord_pool_se3_previous_price")
next_price = hass.states.get("sensor.nord_pool_se3_next_price")
assert current_price is not None
assert last_price is not None
assert next_price is not None
assert current_price.state == "0.92505"
assert last_price.state == "0.92737"
assert next_price.state == "0.94949"

aioclient_mock.clear_requests()
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-04",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[1],
)
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-05",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
json=responses[0],
)
# Future date without data should return 204
aioclient_mock.request(
"GET",
url=API + "/DayAheadPrices",
params={
"date": "2024-11-06",
"market": "DayAhead",
"deliveryArea": "SE3,SE4",
"currency": "SEK",
},
status=HTTPStatus.NO_CONTENT,
)

freezer.move_to("2024-11-05T22:00:01+00:00")
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)

# Current and last price should be known, next price should be unknown
# as api responds with empty data (204)

current_price = hass.states.get("sensor.nord_pool_se3_current_price")
last_price = hass.states.get("sensor.nord_pool_se3_previous_price")
next_price = hass.states.get("sensor.nord_pool_se3_next_price")
assert current_price is not None
assert last_price is not None
assert next_price is not None
assert current_price.state == "0.28914"
assert last_price.state == "0.5223"
assert next_price.state == STATE_UNKNOWN

0 comments on commit 645f2e4

Please sign in to comment.