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

Update stookwijzer api to atlas leefomgeving and add extra sensors #104846

Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
b16c468
Add files via upload
fwestenberg Nov 3, 2023
9f4eff8
Update requirements_all.txt
fwestenberg Nov 3, 2023
2ca8e8b
Update requirements_test_all.txt
fwestenberg Nov 3, 2023
2bd8707
Add files via upload
fwestenberg Nov 3, 2023
0cdacf1
Update homeassistant/components/stookwijzer/const.py
fwestenberg Nov 3, 2023
26b8893
Bump stookalert==1.4.2
fwestenberg Nov 3, 2023
89f2614
Bump stookalert==1.4.2
fwestenberg Nov 3, 2023
28993d9
Bump stookalert==1.4.2
fwestenberg Nov 3, 2023
b434d9c
Update homeassistant/components/stookwijzer/strings.json
fwestenberg Nov 3, 2023
b955c59
Update homeassistant/components/stookwijzer/diagnostics.py
fwestenberg Nov 3, 2023
128e08b
remove extra state attributes
fwestenberg Nov 28, 2023
e805815
Merge branch 'dev' into Update-Stookwijzer-api-to-Atlas-Leefomgeving
fwestenberg Nov 28, 2023
d1b7e4b
Add multiple sensors and coordinator
fwestenberg Nov 30, 2023
254cceb
Update diagnostics
fwestenberg Nov 30, 2023
f7eef01
Add coordinator to init
fwestenberg Nov 30, 2023
3aa8c2b
Update Stookwijzer tests
fwestenberg Dec 1, 2023
7c7a743
Merge branch 'dev' into update-stookwijzer-to-atlas-leefomgeving-with…
fwestenberg Jan 31, 2024
25d7acd
Fix mypy inheritance error
fwestenberg Feb 3, 2024
b7208f2
Merge branch 'dev' into update-stookwijzer-to-atlas-leefomgeving-with…
fwestenberg Feb 3, 2024
73cc7a6
Merge branch 'dev' into update-stookwijzer-to-atlas-leefomgeving-with…
fwestenberg Sep 24, 2024
ac9890c
Merge branch 'dev' into update-stookwijzer-to-atlas-leefomgeving-with…
fwestenberg Sep 24, 2024
a566b1d
Use entry.runtime_data, improve coding
fwestenberg Sep 25, 2024
5cb3f14
Improve tests, delete py.typed
fwestenberg Sep 25, 2024
f4992bc
Improve translation
fwestenberg Sep 26, 2024
49fbc2f
Improve description
fwestenberg Sep 26, 2024
c730e67
Improve tests
fwestenberg Sep 26, 2024
c58823c
Bump stookwijzer to v1.5.0
fwestenberg Nov 8, 2024
e496596
Merge branch 'dev' into update-stookwijzer-to-atlas-leefomgeving-with…
fwestenberg Nov 8, 2024
8d328f7
Merge branch 'home-assistant:dev' into update-stookwijzer-to-atlas-le…
fwestenberg Nov 8, 2024
b3c5dd0
Update test URL to match stookwijzer v1.5.0
fwestenberg Nov 8, 2024
22a436d
Remove stookalert binary sensor
fwestenberg Nov 14, 2024
71f76f7
Fix entity name translation
fwestenberg Nov 21, 2024
b3d0691
Rename Stookwijzer sensor to Advice
fwestenberg Nov 21, 2024
a466834
Merge entity.py and sensor.py (obsolete)
fwestenberg Nov 21, 2024
f436c29
Merge branch 'dev' into update-stookwijzer-to-atlas-leefomgeving-with…
frenck Nov 24, 2024
767ff87
Slightly tweak migration + translations
frenck Nov 24, 2024
28f702e
Add missing data description
frenck Nov 24, 2024
b4e1a19
Remove domain from unique ID
frenck Nov 24, 2024
c7ffda3
Remove unneeded variable in config flow
frenck Nov 24, 2024
b803b93
Add translations for failed coordinator update
frenck Nov 24, 2024
2157231
Refactoring...
frenck Nov 24, 2024
dd2ce71
Let HA handles translations with devices classes
frenck Nov 24, 2024
ac2b61a
Clean up commented out stuff
frenck Nov 24, 2024
e59ccfc
Migrate entity entry unique ID
frenck Nov 24, 2024
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
51 changes: 48 additions & 3 deletions homeassistant/components/stookwijzer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,68 @@
"""The Stookwijzer integration."""
from __future__ import annotations

import logging

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.aiohttp_client import async_get_clientsession

from .const import DOMAIN
from .coordinator import StookwijzerCoordinator, StookwijzerData

PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]

_LOGGER = logging.getLogger(__name__)


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)

if config_entry.version == 1:
session = async_get_clientsession(hass)
x, y = await Stookwijzer.async_transform_coordinates(
session,
config_entry.data[CONF_LOCATION][CONF_LATITUDE],
config_entry.data[CONF_LOCATION][CONF_LONGITUDE],
)

if not x or not y:
_LOGGER.error(
"Migration to version %s not successful", config_entry.version
)
return False

PLATFORMS = [Platform.SENSOR]
config_entry.version = 2
hass.config_entries.async_update_entry(
config_entry, data={CONF_LATITUDE: x, CONF_LONGITUDE: y}
)
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved

_LOGGER.debug("Migration to version %s successful", config_entry.version)

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
frenck marked this conversation as resolved.
Show resolved Hide resolved
"""Set up Stookwijzer from a config entry."""
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = Stookwijzer(
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
entry.data[CONF_LOCATION][CONF_LATITUDE],
entry.data[CONF_LOCATION][CONF_LONGITUDE],
async_get_clientsession(hass),
entry.data[CONF_LATITUDE],
entry.data[CONF_LONGITUDE],
)
client = Stookwijzer(
async_get_clientsession(hass),
entry.data[CONF_LATITUDE],
entry.data[CONF_LONGITUDE],
)
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
coordinator = StookwijzerCoordinator(hass, client)
await coordinator.async_config_entry_first_refresh()

hass.data[DOMAIN][entry.entry_id] = StookwijzerData(coordinator=coordinator)
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

Expand Down
88 changes: 88 additions & 0 deletions homeassistant/components/stookwijzer/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""Support for Stookwijzer Binary Sensors."""
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import cast

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import StookwijzerCoordinator, StookwijzerData
from .entity import StookwijzerEntity


@dataclass
class StookwijzerSensorDescriptionMixin:
"""Required values for Stookwijzer binary sensors."""

value_fn: Callable[[StookwijzerCoordinator], bool | None]
attr_fn: Callable[[StookwijzerCoordinator], list | None]


@dataclass
class StookwijzerBinarySensorDescription(
BinarySensorEntityDescription,
StookwijzerSensorDescriptionMixin,
):
"""Class describing Stookwijzer binary sensor entities."""


STOOKWIJZER_BINARY_SENSORS = (
StookwijzerBinarySensorDescription(
key="stookalert",
device_class=BinarySensorDeviceClass.SAFETY,
value_fn=lambda StookwijzerCoordinator: cast(
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
bool | None, StookwijzerCoordinator.client.alert
),
attr_fn=lambda StookwijzerCoordinator: cast(
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
list | None, StookwijzerCoordinator.client.forecast_alert
),
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Stookwijzer binary sensor from a config entry."""
data: StookwijzerData = hass.data[DOMAIN][entry.entry_id]
coordinator = data.coordinator

assert coordinator is not None
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
async_add_entities(
StookwijzerBinarySensor(description, coordinator, entry)
for description in STOOKWIJZER_BINARY_SENSORS
)


class StookwijzerBinarySensor(
StookwijzerEntity, CoordinatorEntity[StookwijzerCoordinator], BinarySensorEntity
):
"""Defines a Stookwijzer binary sensor."""

entity_description: StookwijzerBinarySensorDescription

def __init__(
self,
description: StookwijzerBinarySensorDescription,
coordinator: StookwijzerCoordinator,
entry: ConfigEntry,
) -> None:
"""Initialize the entity."""
super().__init__(description, coordinator, entry)
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved

@property
def is_on(self) -> bool | None:
"""Return the state of the device."""
return self.entity_description.value_fn(self._coordinator)
18 changes: 14 additions & 4 deletions homeassistant/components/stookwijzer/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

from typing import Any

from stookwijzer import Stookwijzer
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import LocationSelector

from .const import DOMAIN
Expand All @@ -16,19 +18,27 @@
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
) -> FlowResult:
"""Handle a flow initialized by the user."""

if user_input is not None:
return self.async_create_entry(
title="Stookwijzer",
data=user_input,
session = async_get_clientsession(self.hass)
x, y = await Stookwijzer.async_transform_coordinates(
session,
user_input[CONF_LOCATION][CONF_LATITUDE],
user_input[CONF_LOCATION][CONF_LONGITUDE],
)

if x and y:
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
return self.async_create_entry(
title="Stookwijzer",
data={CONF_LATITUDE: x, CONF_LONGITUDE: y},
)

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/stookwijzer/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
class StookwijzerState(StrEnum):
"""Stookwijzer states for sensor entity."""

BLUE = "blauw"
ORANGE = "oranje"
RED = "rood"
CODE_YELLOW = "code_yellow"
CODE_ORANGE = "code_orange"
CODE_RED = "code_red"
40 changes: 40 additions & 0 deletions homeassistant/components/stookwijzer/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Class representing a Stookwijzer update coordinator."""
from dataclasses import dataclass
from datetime import timedelta
import logging

from stookwijzer import Stookwijzer

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

SCAN_INTERVAL = timedelta(minutes=60)


class StookwijzerCoordinator(DataUpdateCoordinator[None]):
"""Devialet update coordinator."""

def __init__(self, hass: HomeAssistant, client: Stookwijzer) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.client = client

async def _async_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.client.async_update()
frenck marked this conversation as resolved.
Show resolved Hide resolved


@dataclass
class StookwijzerData:
"""Config Entry global data."""

coordinator: StookwijzerCoordinator | None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the data only contains a coordinator, we can just as well just store the coordinator directly and remove this extra class / layer

10 changes: 6 additions & 4 deletions homeassistant/components/stookwijzer/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ async def async_get_config_entry_diagnostics(
return {
"state": client.state,
"last_updated": last_updated,
"lqi": client.lqi,
"windspeed": client.windspeed,
"weather": client.weather,
"concentrations": client.concentrations,
"alert": client.alert,
"air_quality_index": client.lki,
"windspeed_bft": client.windspeed_bft,
"windspeed_ms": client.windspeed_ms,
"forecast_advice": client.forecast_advice,
"forecast_alert": client.forecast_alert,
}
59 changes: 59 additions & 0 deletions homeassistant/components/stookwijzer/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""The Stookwijzer integration entities."""
from __future__ import annotations

from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import StookwijzerCoordinator


class StookwijzerEntity(CoordinatorEntity, Entity):
"""Base class for Stookwijzer entities."""

_attr_attribution = "Data provided by atlasleefomgeving.nl"
_attr_should_poll = False
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved

def __init__(
self,
description: EntityDescription,
coordinator: StookwijzerCoordinator,
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
entry: ConfigEntry,
) -> None:
"""Initialize a Stookwijzer device."""

self.entity_description = description
super().__init__(coordinator)

self._coordinator = coordinator
self._attr_unique_id = DOMAIN + description.key
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems incorrect.

  • We shouldn't be prefixing the domain.
  • This integration can have multiple instances, meaning this ID isn't unique.
  • We shouldn't be using string concatenation using +; use f-strings instead.

Suggestion in this case, is to use the last resort and prefix using the config entry ID instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hehe what about it?

Let's discuss on Discord :)

self._attr_name = description.key.title()
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=description.key,
manufacturer="Atlas Leefomgeving",
entry_type=DeviceEntryType.SERVICE,
configuration_url="https://www.atlasleefomgeving.nl/stookwijzer",
)

@callback
def _handle_coordinator_update(self) -> None:
self.async_write_ha_state()

Check warning on line 46 in homeassistant/components/stookwijzer/entity.py

View check run for this annotation

Codecov / codecov/patch

homeassistant/components/stookwijzer/entity.py#L46

Added line #L46 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the default already? I think this method can be removed.


@property
def available(self) -> bool:
"""Return if entity is available."""
return self._coordinator.client.advice is not None
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved

@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
if not self.entity_description.attr_fn: # type: ignore[attr-defined]
return None
frenck marked this conversation as resolved.
Show resolved Hide resolved

return {"forecast": self.entity_description.attr_fn(self._coordinator)} # type: ignore[attr-defined]
fwestenberg marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion homeassistant/components/stookwijzer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.4.9"]
}
Loading