Bump stookwijzer to v1.5.1 (#131567)

This commit is contained in:
Franck Nijhof 2024-11-25 21:33:47 +01:00 committed by GitHub
parent 1b62e12261
commit 4a8f3eea69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 399 additions and 73 deletions

View File

@ -7,8 +7,10 @@ from stookwijzer import Stookwijzer
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .const import DOMAIN, LOGGER
PLATFORMS = [Platform.SENSOR]
@ -16,8 +18,9 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Stookwijzer from a config entry."""
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = Stookwijzer(
entry.data[CONF_LOCATION][CONF_LATITUDE],
entry.data[CONF_LOCATION][CONF_LONGITUDE],
async_get_clientsession(hass),
entry.data[CONF_LATITUDE],
entry.data[CONF_LONGITUDE],
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@ -28,3 +31,42 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
del hass.data[DOMAIN][entry.entry_id]
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
LOGGER.debug("Migrating from version %s", entry.version)
if entry.version == 1:
latitude, longitude = await Stookwijzer.async_transform_coordinates(
async_get_clientsession(hass),
entry.data[CONF_LOCATION][CONF_LATITUDE],
entry.data[CONF_LOCATION][CONF_LONGITUDE],
)
if not latitude or not longitude:
ir.async_create_issue(
hass,
DOMAIN,
"location_migration_failed",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="location_migration_failed",
translation_placeholders={
"entry_title": entry.title,
},
)
return False
hass.config_entries.async_update_entry(
entry,
version=2,
data={
CONF_LATITUDE: latitude,
CONF_LONGITUDE: longitude,
},
)
LOGGER.debug("Migration to version %s successful", entry.version)
return True

View File

@ -4,10 +4,12 @@ from __future__ import annotations
from typing import Any
from stookwijzer import Stookwijzer
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import LocationSelector
from .const import DOMAIN
@ -16,21 +18,29 @@ from .const import DOMAIN
class StookwijzerFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for Stookwijzer."""
VERSION = 1
VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
latitude, longitude = await Stookwijzer.async_transform_coordinates(
async_get_clientsession(self.hass),
user_input[CONF_LOCATION][CONF_LATITUDE],
user_input[CONF_LOCATION][CONF_LONGITUDE],
)
if latitude and longitude:
return self.async_create_entry(
title="Stookwijzer",
data=user_input,
data={CONF_LATITUDE: latitude, CONF_LONGITUDE: longitude},
)
errors["base"] = "unknown"
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(

View File

@ -1,16 +1,7 @@
"""Constants for the Stookwijzer integration."""
from enum import StrEnum
import logging
from typing import Final
DOMAIN: Final = "stookwijzer"
LOGGER = logging.getLogger(__package__)
class StookwijzerState(StrEnum):
"""Stookwijzer states for sensor entity."""
BLUE = "blauw"
ORANGE = "oranje"
RED = "rood"

View File

@ -17,16 +17,6 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
client: Stookwijzer = hass.data[DOMAIN][entry.entry_id]
last_updated = None
if client.last_updated:
last_updated = client.last_updated.isoformat()
return {
"state": client.state,
"last_updated": last_updated,
"lqi": client.lqi,
"windspeed": client.windspeed,
"weather": client.weather,
"concentrations": client.concentrations,
"advice": client.advice,
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/stookwijzer",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["stookwijzer==1.3.0"]
"requirements": ["stookwijzer==1.5.1"]
}

View File

@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, StookwijzerState
from .const import DOMAIN
SCAN_INTERVAL = timedelta(minutes=60)
@ -30,37 +30,33 @@ async def async_setup_entry(
class StookwijzerSensor(SensorEntity):
"""Defines a Stookwijzer binary sensor."""
_attr_attribution = "Data provided by stookwijzer.nu"
_attr_attribution = "Data provided by atlasleefomgeving.nl"
_attr_device_class = SensorDeviceClass.ENUM
_attr_has_entity_name = True
_attr_name = None
_attr_translation_key = "stookwijzer"
_attr_translation_key = "advice"
def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None:
"""Initialize a Stookwijzer device."""
self._client = client
self._attr_options = [cls.value for cls in StookwijzerState]
self._attr_options = ["code_yellow", "code_orange", "code_red"]
self._attr_unique_id = entry.entry_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.entry_id}")},
name="Stookwijzer",
manufacturer="stookwijzer.nu",
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Atlas Leefomgeving",
entry_type=DeviceEntryType.SERVICE,
configuration_url="https://www.stookwijzer.nu",
configuration_url="https://www.atlasleefomgeving.nl/stookwijzer",
)
def update(self) -> None:
async def async_update(self) -> None:
"""Update the data from the Stookwijzer handler."""
self._client.update()
await self._client.async_update()
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._client.state is not None
return self._client.advice is not None
@property
def native_value(self) -> str | None:
"""Return the state of the device."""
if self._client.state is None:
return None
return StookwijzerState(self._client.state).value
return self._client.advice

View File

@ -7,17 +7,27 @@
"location": "[%key:common::config_flow::data::location%]"
}
}
},
"error": {
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
"sensor": {
"stookwijzer": {
"advice": {
"name": "Advice code",
"state": {
"blauw": "Blue",
"oranje": "Orange",
"rood": "Red"
"code_yellow": "Yellow",
"code_orange": "Orange",
"code_red": "Red"
}
}
}
},
"issues": {
"location_migration_failed": {
"description": "The Stookwijzer integration was unable to automatically migrate your location to a new format the updated integrations uses.\n\nMake sure you are connected to the internet and restart Home Assistant to try again.\n\nIf this doesn't resolve the error, remove and re-add the integration.",
"title": "Migration of your location failed"
}
}
}

View File

@ -2746,7 +2746,7 @@ steamodd==4.21
stookalert==0.1.4
# homeassistant.components.stookwijzer
stookwijzer==1.3.0
stookwijzer==1.5.1
# homeassistant.components.streamlabswater
streamlabswater==1.0.1

View File

@ -2195,7 +2195,7 @@ steamodd==4.21
stookalert==0.1.4
# homeassistant.components.stookwijzer
stookwijzer==1.3.0
stookwijzer==1.5.1
# homeassistant.components.streamlabswater
streamlabswater==1.0.1

View File

@ -0,0 +1,92 @@
"""Fixtures for Stookwijzer integration tests."""
from collections.abc import Generator
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.stookwijzer.const import DOMAIN
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
title="Stookwijzer",
domain=DOMAIN,
data={
CONF_LATITUDE: 200000.1234567890,
CONF_LONGITUDE: 450000.1234567890,
},
version=2,
entry_id="12345",
)
@pytest.fixture
def mock_v1_config_entry() -> MockConfigEntry:
"""Return the default mocked version 1 config entry."""
return MockConfigEntry(
title="Stookwijzer",
domain=DOMAIN,
data={
CONF_LOCATION: {
CONF_LATITUDE: 1.0,
CONF_LONGITUDE: 1.1,
},
},
version=1,
entry_id="12345",
)
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Mock setting up a config entry."""
with patch(
"homeassistant.components.stookwijzer.async_setup_entry", return_value=True
) as mock_setup:
yield mock_setup
@pytest.fixture
def mock_stookwijzer() -> Generator[MagicMock]:
"""Return a mocked Stookwijzer client."""
with (
patch(
"homeassistant.components.stookwijzer.Stookwijzer",
autospec=True,
) as stookwijzer_mock,
patch(
"homeassistant.components.stookwijzer.config_flow.Stookwijzer",
new=stookwijzer_mock,
),
):
stookwijzer_mock.async_transform_coordinates.return_value = (
200000.123456789,
450000.123456789,
)
client = stookwijzer_mock.return_value
client.advice = "code_yellow"
yield stookwijzer_mock
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_stookwijzer: MagicMock,
) -> MockConfigEntry:
"""Set up the Stookwijzer integration for testing."""
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@ -0,0 +1,6 @@
# serializer version: 1
# name: test_get_diagnostics
dict({
'advice': 'code_yellow',
})
# ---

View File

@ -0,0 +1,60 @@
# serializer version: 1
# name: test_entities[sensor.stookwijzer_advice_code-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'code_yellow',
'code_orange',
'code_red',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.stookwijzer_advice_code',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Advice code',
'platform': 'stookwijzer',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'advice',
'unique_id': '12345',
'unit_of_measurement': None,
})
# ---
# name: test_entities[sensor.stookwijzer_advice_code-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by atlasleefomgeving.nl',
'device_class': 'enum',
'friendly_name': 'Stookwijzer Advice code',
'options': list([
'code_yellow',
'code_orange',
'code_red',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.stookwijzer_advice_code',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'code_yellow',
})
# ---

View File

@ -1,6 +1,8 @@
"""Tests for the Stookwijzer config flow."""
from unittest.mock import patch
from unittest.mock import AsyncMock, MagicMock
import pytest
from homeassistant.components.stookwijzer.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
@ -9,35 +11,65 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
async def test_full_user_flow(hass: HomeAssistant) -> None:
async def test_full_user_flow(
hass: HomeAssistant,
mock_stookwijzer: MagicMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test the full user configuration flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result.get("type") is FlowResultType.FORM
assert result.get("step_id") == "user"
assert "flow_id" in result
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
with patch(
"homeassistant.components.stookwijzer.async_setup_entry", return_value=True
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_LOCATION: {
CONF_LATITUDE: 1.0,
CONF_LONGITUDE: 1.1,
}
},
user_input={CONF_LOCATION: {CONF_LATITUDE: 1.0, CONF_LONGITUDE: 1.1}},
)
assert result2.get("type") is FlowResultType.CREATE_ENTRY
assert result2.get("data") == {
"location": {
"latitude": 1.0,
"longitude": 1.1,
},
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Stookwijzer"
assert result["data"] == {
CONF_LATITUDE: 200000.123456789,
CONF_LONGITUDE: 450000.123456789,
}
assert len(mock_setup_entry.mock_calls) == 1
assert len(mock_stookwijzer.async_transform_coordinates.mock_calls) == 1
@pytest.mark.usefixtures("mock_setup_entry")
async def test_connection_error(
hass: HomeAssistant,
mock_stookwijzer: MagicMock,
) -> None:
"""Test user configuration flow while connection fails."""
original_return_value = mock_stookwijzer.async_transform_coordinates.return_value
mock_stookwijzer.async_transform_coordinates.return_value = (None, None)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_LOCATION: {CONF_LATITUDE: 1.0, CONF_LONGITUDE: 1.1}},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "unknown"}
# Ensure we can continue the flow, when it now works
mock_stookwijzer.async_transform_coordinates.return_value = original_return_value
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_LOCATION: {CONF_LATITUDE: 1.0, CONF_LONGITUDE: 1.1}},
)
assert result["type"] is FlowResultType.CREATE_ENTRY

View File

@ -0,0 +1,22 @@
"""Test the Stookwijzer diagnostics."""
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_get_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Stookwijzer diagnostics."""
assert (
await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
== snapshot
)

View File

@ -0,0 +1,55 @@
"""Test the Stookwijzer init."""
from unittest.mock import MagicMock
from homeassistant.components.stookwijzer.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from tests.common import MockConfigEntry
async def test_migrate_entry(
hass: HomeAssistant,
mock_v1_config_entry: MockConfigEntry,
mock_stookwijzer: MagicMock,
) -> None:
"""Test successful migration of entry data."""
assert mock_v1_config_entry.version == 1
mock_v1_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_v1_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_v1_config_entry.state is ConfigEntryState.LOADED
assert len(mock_stookwijzer.async_transform_coordinates.mock_calls) == 1
assert mock_v1_config_entry.version == 2
assert mock_v1_config_entry.data == {
CONF_LATITUDE: 200000.123456789,
CONF_LONGITUDE: 450000.123456789,
}
async def test_entry_migration_failure(
hass: HomeAssistant,
mock_v1_config_entry: MockConfigEntry,
mock_stookwijzer: MagicMock,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test successful migration of entry data."""
assert mock_v1_config_entry.version == 1
# Failed getting the transformed coordinates
mock_stookwijzer.async_transform_coordinates.return_value = (None, None)
mock_v1_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_v1_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_v1_config_entry.state is ConfigEntryState.MIGRATION_ERROR
assert issue_registry.async_get_issue(DOMAIN, "location_migration_failed")
assert len(mock_stookwijzer.async_transform_coordinates.mock_calls) == 1

View File

@ -0,0 +1,20 @@
"""Tests for the Stookwijzer sensor platform."""
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("init_integration")
async def test_entities(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Stookwijzer entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)