Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Nord Pool empty response #134033

Merged
merged 3 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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
}
6 changes: 5 additions & 1 deletion homeassistant/components/nordpool/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
index: int,
) -> float | None:
"""Validate and return."""
if result := func(entity)[area][index]:
try:
result = func(entity)[area][index]
except (KeyError, IndexError):
gjohansson-ST marked this conversation as resolved.
Show resolved Hide resolved
return None

Check warning on line 40 in homeassistant/components/nordpool/sensor.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/nordpool/sensor.py#L39-L40

Added lines #L39 - L40 were not covered by tests
if result:
return result / 1000
return None

Expand Down
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