diff --git a/README.md b/README.md
index 59e81e7..71bd54c 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ This integration provides the following things:
- Weather forecasts (hourly, daily and twice-daily) [using the service `weather.get_forecasts`](https://www.home-assistant.io/integrations/weather/#service-weatherget_forecasts)
- A camera entity for rain radar and short-term rain previsions
- A binary sensor for weather warnings
+- Sensors for active pollens
The following options are available:
@@ -108,6 +109,18 @@ The following table summarizes the different known warning types. Other warning
The sensor has an attribute called `active_warnings_friendly_names`, holding a comma separated list of the friendly names
of the currently active warnings (e.g. `Fog, Ice or snow`). There is no particular order for the list.
+## Pollen details
+
+One sensor per pollen is created and each sensor can have on of the following values: active, green, yellow, orange,
+red, purple or none.
+
+The exact meaning of each color can be found on the IRM KMI webpage: [Pollen allergy and hay fever](https://www.meteo.be/en/weather/forecasts/pollen-allergy-and-hay-fever)
+
+
+
+This data sent to the app would result in oak and ash have the 'active' state, birch would be 'purple' and alder would be 'green'.
+All the other pollens would be 'none'.
+
## Disclaimer
This is a personal project and isn't in any way affiliated with, sponsored or endorsed by [The Royal Meteorological
diff --git a/custom_components/irm_kmi/api.py b/custom_components/irm_kmi/api.py
index 09d7648..2f0a326 100644
--- a/custom_components/irm_kmi/api.py
+++ b/custom_components/irm_kmi/api.py
@@ -54,6 +54,11 @@ class IrmKmiApiClient:
r: ClientResponse = await self._api_wrapper(base_url=url, params={} if params is None else params)
return await r.read()
+ async def get_svg(self, url, params: dict | None = None) -> str:
+ """Get SVG as str at the specified url with the parameters"""
+ r: ClientResponse = await self._api_wrapper(base_url=url, params={} if params is None else params)
+ return await r.text()
+
async def _api_wrapper(
self,
params: dict,
diff --git a/custom_components/irm_kmi/binary_sensor.py b/custom_components/irm_kmi/binary_sensor.py
index 567309d..e911b13 100644
--- a/custom_components/irm_kmi/binary_sensor.py
+++ b/custom_components/irm_kmi/binary_sensor.py
@@ -8,7 +8,6 @@ from homeassistant.components.binary_sensor import (BinarySensorDeviceClass,
BinarySensorEntity)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -36,12 +35,7 @@ class IrmKmiWarning(CoordinatorEntity, BinarySensorEntity):
self._attr_unique_id = entry.entry_id
self.entity_id = binary_sensor.ENTITY_ID_FORMAT.format(f"weather_warning_{str(entry.title).lower()}")
self._attr_name = f"Warning {entry.title}"
- self._attr_device_info = DeviceInfo(
- entry_type=DeviceEntryType.SERVICE,
- identifiers={(DOMAIN, entry.entry_id)},
- manufacturer="IRM KMI",
- name=f"Warning {entry.title}"
- )
+ self._attr_device_info = coordinator.shared_device_info
@property
def is_on(self) -> bool | None:
diff --git a/custom_components/irm_kmi/camera.py b/custom_components/irm_kmi/camera.py
index df5ca56..6f5d2fd 100644
--- a/custom_components/irm_kmi/camera.py
+++ b/custom_components/irm_kmi/camera.py
@@ -6,7 +6,6 @@ from aiohttp import web
from homeassistant.components.camera import Camera, async_get_still_stream
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -36,12 +35,7 @@ class IrmKmiRadar(CoordinatorEntity, Camera):
self.content_type = 'image/svg+xml'
self._name = f"Radar {entry.title}"
self._attr_unique_id = entry.entry_id
- self._attr_device_info = DeviceInfo(
- entry_type=DeviceEntryType.SERVICE,
- identifiers={(DOMAIN, entry.entry_id)},
- manufacturer="IRM KMI",
- name=f"Radar {entry.title}"
- )
+ self._attr_device_info = coordinator.shared_device_info
self._image_index = False
diff --git a/custom_components/irm_kmi/const.py b/custom_components/irm_kmi/const.py
index ead55f2..31c2bdc 100644
--- a/custom_components/irm_kmi/const.py
+++ b/custom_components/irm_kmi/const.py
@@ -14,7 +14,7 @@ from homeassistant.components.weather import (ATTR_CONDITION_CLEAR_NIGHT,
from homeassistant.const import Platform
DOMAIN: Final = 'irm_kmi'
-PLATFORMS: Final = [Platform.WEATHER, Platform.CAMERA, Platform.BINARY_SENSOR]
+PLATFORMS: Final = [Platform.WEATHER, Platform.CAMERA, Platform.BINARY_SENSOR, Platform.SENSOR]
CONFIG_FLOW_VERSION = 3
OUT_OF_BENELUX: Final = ["außerhalb der Benelux (Brussels)",
@@ -133,3 +133,10 @@ MAP_WARNING_ID_TO_SLUG: Final = {
14: 'thunderstorm_large_rainfall',
15: 'storm_surge',
17: 'coldspell'}
+
+POLLEN_NAMES: Final = {'Alder', 'Ash', 'Birch', 'Grasses', 'Hazel', 'Mugwort', 'Oak'}
+
+POLLEN_TO_ICON_MAP: Final = {
+ 'alder': 'mdi:tree', 'ash': 'mdi:tree', 'birch': 'mdi:tree', 'grasses': 'mdi:grass', 'hazel': 'mdi:tree',
+ 'mugwort': 'mdi:sprout', 'oak': 'mdi:tree'
+}
diff --git a/custom_components/irm_kmi/coordinator.py b/custom_components/irm_kmi/coordinator.py
index ca9150a..33a21e0 100644
--- a/custom_components/irm_kmi/coordinator.py
+++ b/custom_components/irm_kmi/coordinator.py
@@ -12,6 +12,7 @@ from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import (DataUpdateCoordinator,
UpdateFailed)
@@ -23,6 +24,7 @@ from .const import MAP_WARNING_ID_TO_SLUG as SLUG_MAP
from .const import OPTION_STYLE_SATELLITE, OUT_OF_BENELUX, STYLE_TO_PARAM_MAP
from .data import (AnimationFrameData, CurrentWeatherData, IrmKmiForecast,
ProcessedCoordinatorData, RadarAnimationData, WarningData)
+from .pollen import PollenParser
from .rain_graph import RainGraph
from .utils import disable_from_config, get_config_value
@@ -47,6 +49,12 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
self._dark_mode = get_config_value(entry, CONF_DARK_MODE)
self._style = get_config_value(entry, CONF_STYLE)
self._config_entry = entry
+ self.shared_device_info = DeviceInfo(
+ entry_type=DeviceEntryType.SERVICE,
+ identifiers={(DOMAIN, entry.entry_id)},
+ manufacturer="IRM KMI",
+ name=f"{entry.title}"
+ )
async def _async_update_data(self) -> ProcessedCoordinatorData:
"""Fetch data from API endpoint.
@@ -124,6 +132,28 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
radar_animation['svg_still'] = rain_graph.get_svg_string(still_image=True)
return radar_animation
+ async def _async_pollen_data(self, api_data: dict) -> dict:
+ _LOGGER.debug("Getting pollen data from API")
+ svg_url = None
+ for module in api_data.get('module', []):
+ _LOGGER.debug(f"module: {module}")
+ if module.get('type', None) == 'svg':
+ url = module.get('data', {}).get('url', {}).get('en', '')
+ if 'pollen' in url:
+ svg_url = url
+ break
+ if svg_url is None:
+ return PollenParser.get_default_data()
+
+ try:
+ _LOGGER.debug(f"Requesting pollen SVG at url {svg_url}")
+ pollen_svg: str = await self._api_client.get_svg(svg_url)
+ except IrmKmiApiError:
+ _LOGGER.warning(f"Could not get pollen data from the API")
+ return PollenParser.get_default_data()
+
+ return PollenParser(pollen_svg).get_pollen_data()
+
async def process_api_data(self, api_data: dict) -> ProcessedCoordinatorData:
"""From the API data, create the object that will be used in the entities"""
return ProcessedCoordinatorData(
@@ -131,7 +161,8 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
daily_forecast=self.daily_list_to_forecast(api_data.get('for', {}).get('daily')),
hourly_forecast=IrmKmiCoordinator.hourly_list_to_forecast(api_data.get('for', {}).get('hourly')),
animation=await self._async_animation_data(api_data=api_data),
- warnings=self.warnings_from_data(api_data.get('for', {}).get('warning'))
+ warnings=self.warnings_from_data(api_data.get('for', {}).get('warning')),
+ pollen=await self._async_pollen_data(api_data=api_data)
)
async def download_images_from_api(self,
diff --git a/custom_components/irm_kmi/data.py b/custom_components/irm_kmi/data.py
index 02b3989..2f12224 100644
--- a/custom_components/irm_kmi/data.py
+++ b/custom_components/irm_kmi/data.py
@@ -62,3 +62,4 @@ class ProcessedCoordinatorData(TypedDict, total=False):
daily_forecast: List[IrmKmiForecast] | None
animation: RadarAnimationData
warnings: List[WarningData]
+ pollen: dict
diff --git a/custom_components/irm_kmi/pollen.py b/custom_components/irm_kmi/pollen.py
new file mode 100644
index 0000000..09b880b
--- /dev/null
+++ b/custom_components/irm_kmi/pollen.py
@@ -0,0 +1,131 @@
+"""Parse pollen info from SVG from IRM KMI api"""
+import logging
+import xml.etree.ElementTree as ET
+from typing import List
+
+from custom_components.irm_kmi.const import POLLEN_NAMES
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class PollenParser:
+ """
+ The SVG looks as follows (see test fixture for a real example)
+
+ Active pollens
+ ---------------------------------
+ Oak active
+ Ash active
+ ---------------------------------
+ Birch ---|---|---|---|-*-
+ Alder -*-|---|---|---|---
+
+ This classe parses the oak and ash as active, birch as purple and alder as green in the example.
+ For active pollen, check if an active text is present on the same line as the pollen name
+ For the color scale, look for a white dot (nearly) at the same level as the pollen name. From the white dot
+ horizontal position, determine the level
+ """
+ def __init__(
+ self,
+ xml_string: str
+ ):
+ self._xml = xml_string
+
+ @staticmethod
+ def _validate_svg(elements: List[ET.Element]) -> bool:
+ """Make sure that the colors of the scale are still where we expect them"""
+ x_values = {"rectgreen": 80,
+ "rectyellow": 95,
+ "rectorange": 110,
+ "rectred": 125,
+ "rectpurple": 140}
+ for e in elements:
+ if e.attrib.get('id', '') in x_values.keys():
+ try:
+ if float(e.attrib.get('x', '0')) != x_values.get(e.attrib.get('id')):
+ return False
+ except ValueError:
+ return False
+ return True
+
+ @staticmethod
+ def get_default_data() -> dict:
+ """Return all the known pollen with 'none' value"""
+ return {k.lower(): 'none' for k in POLLEN_NAMES}
+
+ @staticmethod
+ def get_option_values() -> List[str]:
+ """List all the values that the pollen can have"""
+ return ['active', 'green', 'yellow', 'orange', 'red', 'purple', 'none']
+
+ @staticmethod
+ def _extract_elements(root) -> List[ET.Element]:
+ """Recursively collect all elements of the SVG in a list"""
+ elements = []
+ for child in root:
+ elements.append(child)
+ elements.extend(PollenParser._extract_elements(child))
+ return elements
+
+ @staticmethod
+ def _dot_to_color_value(dot: ET.Element) -> str:
+ """Map the dot horizontal position to a color or 'none'"""
+ try:
+ cx = float(dot.attrib.get('cx'))
+ except ValueError:
+ return 'none'
+
+ if cx > 155:
+ return 'none'
+ elif cx > 140:
+ return 'purple'
+ elif cx > 125:
+ return 'red'
+ elif cx > 110:
+ return 'orange'
+ elif cx > 95:
+ return 'yellow'
+ elif cx > 80:
+ return 'green'
+ else:
+ return 'none'
+
+ def get_pollen_data(self) -> dict:
+ """From the XML string, parse the SVG and extract the pollen data from the image.
+ If an error occurs, return the default value"""
+ pollen_data = self.get_default_data()
+ try:
+ _LOGGER.debug(f"Full SVG: {self._xml}")
+ root = ET.fromstring(self._xml)
+ except ET.ParseError:
+ _LOGGER.warning("Could not parse SVG pollen XML")
+ return pollen_data
+
+ elements: List[ET.Element] = self._extract_elements(root)
+
+ if not self._validate_svg(elements):
+ _LOGGER.warning("Could not validate SVG pollen data")
+ return pollen_data
+
+ pollens = [e for e in elements if 'tspan' in e.tag and e.text in POLLEN_NAMES]
+ active = [e for e in elements if 'tspan' in e.tag and e.text == 'active']
+ dots = [e for e in elements if 'ellipse' in e.tag
+ and 'fill:#ffffff' in e.attrib.get('style', '')
+ and 3 == float(e.attrib.get('rx', '0'))]
+
+ for pollen in pollens:
+ try:
+ y = float(pollen.attrib.get('y'))
+ if y in [float(e.attrib.get('y')) for e in active]:
+ pollen_data[pollen.text.lower()] = 'active'
+ else:
+ dot = [d for d in dots if y - 3 <= float(d.attrib.get('cy', '0')) <= y + 3]
+ if len(dot) == 1:
+ dot = dot[0]
+ pollen_data[pollen.text.lower()] = self._dot_to_color_value(dot)
+ except ValueError | NameError:
+ _LOGGER.warning("Skipped some data in the pollen SVG")
+
+ _LOGGER.debug(f"Pollen data: {pollen_data}")
+ return pollen_data
+
diff --git a/custom_components/irm_kmi/sensor.py b/custom_components/irm_kmi/sensor.py
new file mode 100644
index 0000000..fd1e3fd
--- /dev/null
+++ b/custom_components/irm_kmi/sensor.py
@@ -0,0 +1,47 @@
+"""Sensor for pollen from the IRM KMI"""
+import logging
+
+from homeassistant.components import sensor
+from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
+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 custom_components.irm_kmi import DOMAIN, IrmKmiCoordinator
+from custom_components.irm_kmi.const import POLLEN_NAMES, POLLEN_TO_ICON_MAP
+from custom_components.irm_kmi.pollen import PollenParser
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback):
+ """Set up the sensor platform"""
+ coordinator = hass.data[DOMAIN][entry.entry_id]
+ async_add_entities([IrmKmiPollen(coordinator, entry, pollen.lower()) for pollen in POLLEN_NAMES])
+
+
+class IrmKmiPollen(CoordinatorEntity, SensorEntity):
+ """Representation of a pollen sensor"""
+ _attr_has_entity_name = True
+ _attr_device_class = SensorDeviceClass.ENUM
+
+ def __init__(self,
+ coordinator: IrmKmiCoordinator,
+ entry: ConfigEntry,
+ pollen: str
+ ) -> None:
+ super().__init__(coordinator)
+ SensorEntity.__init__(self)
+ self._attr_unique_id = f"{entry.entry_id}-pollen-{pollen}"
+ self.entity_id = sensor.ENTITY_ID_FORMAT.format(f"{str(entry.title).lower()}_{pollen}_level")
+ self._attr_options = PollenParser.get_option_values()
+ self._attr_device_info = coordinator.shared_device_info
+ self._pollen = pollen
+ self._attr_translation_key = f"pollen_{pollen}"
+ self._attr_icon = POLLEN_TO_ICON_MAP[pollen]
+
+ @property
+ def native_value(self) -> str | None:
+ """Return the state of the sensor."""
+ return self.coordinator.data.get('pollen', {}).get(self._pollen, None)
diff --git a/custom_components/irm_kmi/translations/en.json b/custom_components/irm_kmi/translations/en.json
index 7fd5c43..ebd4add 100644
--- a/custom_components/irm_kmi/translations/en.json
+++ b/custom_components/irm_kmi/translations/en.json
@@ -74,5 +74,93 @@
}
}
}
+ },
+ "entity": {
+ "sensor": {
+ "pollen_alder": {
+ "name": "Alder pollen",
+ "state": {
+ "active": "Active",
+ "green": "Green",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "red": "Red",
+ "purple": "Purple",
+ "none": "None"
+ }
+ },
+ "pollen_ash": {
+ "name": "Ash pollen",
+ "state": {
+ "active": "Active",
+ "green": "Green",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "red": "Red",
+ "purple": "Purple",
+ "none": "None"
+ }
+ },
+ "pollen_birch": {
+ "name": "Birch pollen",
+ "state": {
+ "active": "Active",
+ "green": "Green",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "red": "Red",
+ "purple": "Purple",
+ "none": "None"
+ }
+ },
+ "pollen_grasses": {
+ "name": "Grass pollen",
+ "state": {
+ "active": "Active",
+ "green": "Green",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "red": "Red",
+ "purple": "Purple",
+ "none": "None"
+ }
+ },
+ "pollen_hazel": {
+ "name": "Hazel pollen",
+ "state": {
+ "active": "Active",
+ "green": "Green",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "red": "Red",
+ "purple": "Purple",
+ "none": "None"
+ }
+ },
+ "pollen_mugwort": {
+ "name": "Mugwort pollen",
+ "state": {
+ "active": "Active",
+ "green": "Green",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "red": "Red",
+ "purple": "Purple",
+ "none": "None"
+ }
+ },
+ "pollen_oak": {
+ "name": "Oak pollen",
+ "state": {
+ "active": "Active",
+ "green": "Green",
+ "yellow": "Yellow",
+ "orange": "Orange",
+ "red": "Red",
+ "purple": "Purple",
+ "none": "None"
+ }
+ }
+ }
}
}
diff --git a/custom_components/irm_kmi/translations/fr.json b/custom_components/irm_kmi/translations/fr.json
index 39fd18c..547ddb2 100644
--- a/custom_components/irm_kmi/translations/fr.json
+++ b/custom_components/irm_kmi/translations/fr.json
@@ -74,5 +74,93 @@
}
}
}
+ },
+ "entity": {
+ "sensor": {
+ "pollen_alder": {
+ "name": "Pollen d'aulne",
+ "state": {
+ "active": "Actif",
+ "green": "Vert",
+ "yellow": "Jaune",
+ "orange": "Orange",
+ "red": "Rouge",
+ "purple": "Violet",
+ "none": "Aucun"
+ }
+ },
+ "pollen_ash": {
+ "name": "Pollen de frêne",
+ "state": {
+ "active": "Actif",
+ "green": "Vert",
+ "yellow": "Jaune",
+ "orange": "Orange",
+ "red": "Rouge",
+ "purple": "Violet",
+ "none": "Aucun"
+ }
+ },
+ "pollen_birch": {
+ "name": "Pollen de bouleau",
+ "state": {
+ "active": "Actif",
+ "green": "Vert",
+ "yellow": "Jaune",
+ "orange": "Orange",
+ "red": "Rouge",
+ "purple": "Violet",
+ "none": "Aucun"
+ }
+ },
+ "pollen_grasses": {
+ "name": "Pollen de graminées",
+ "state": {
+ "active": "Actif",
+ "green": "Vert",
+ "yellow": "Jaune",
+ "orange": "Orange",
+ "red": "Rouge",
+ "purple": "Violet",
+ "none": "Aucun"
+ }
+ },
+ "pollen_hazel": {
+ "name": "Pollen de noisetier",
+ "state": {
+ "active": "Actif",
+ "green": "Vert",
+ "yellow": "Jaune",
+ "orange": "Orange",
+ "red": "Rouge",
+ "purple": "Violet",
+ "none": "Aucun"
+ }
+ },
+ "pollen_mugwort": {
+ "name": "Pollen d'armoise",
+ "state": {
+ "active": "Actif",
+ "green": "Vert",
+ "yellow": "Jaune",
+ "orange": "Orange",
+ "red": "Rouge",
+ "purple": "Violet",
+ "none": "Aucun"
+ }
+ },
+ "pollen_oak": {
+ "name": "Pollen de chêne",
+ "state": {
+ "active": "Actif",
+ "green": "Vert",
+ "yellow": "Jaune",
+ "orange": "Orange",
+ "red": "Rouge",
+ "purple": "Violet",
+ "none": "Aucun"
+ }
+ }
+ }
}
}
diff --git a/custom_components/irm_kmi/translations/nl.json b/custom_components/irm_kmi/translations/nl.json
index e520411..620e7a8 100644
--- a/custom_components/irm_kmi/translations/nl.json
+++ b/custom_components/irm_kmi/translations/nl.json
@@ -74,5 +74,93 @@
}
}
}
+ },
+ "entity": {
+ "sensor": {
+ "pollen_alder": {
+ "name": "Elzenpollen",
+ "state": {
+ "active": "Actief",
+ "green": "Groen",
+ "yellow": "Geel",
+ "orange": "Oranje",
+ "red": "Rood",
+ "purple": "Paars",
+ "none": "Geen"
+ }
+ },
+ "pollen_ash": {
+ "name": "Essen pollen",
+ "state": {
+ "active": "Actief",
+ "green": "Groen",
+ "yellow": "Geel",
+ "orange": "Oranje",
+ "red": "Rood",
+ "purple": "Paars",
+ "none": "Geen"
+ }
+ },
+ "pollen_birch": {
+ "name": "Berken pollen",
+ "state": {
+ "active": "Actief",
+ "green": "Groen",
+ "yellow": "Geel",
+ "orange": "Oranje",
+ "red": "Rood",
+ "purple": "Paars",
+ "none": "Geen"
+ }
+ },
+ "pollen_grasses": {
+ "name": "Graspollen",
+ "state": {
+ "active": "Actief",
+ "green": "Groen",
+ "yellow": "Geel",
+ "orange": "Oranje",
+ "red": "Rood",
+ "purple": "Paars",
+ "none": "Geen"
+ }
+ },
+ "pollen_hazel": {
+ "name": "Hazelaar pollen",
+ "state": {
+ "active": "Actief",
+ "green": "Groen",
+ "yellow": "Geel",
+ "orange": "Oranje",
+ "red": "Rood",
+ "purple": "Paars",
+ "none": "Geen"
+ }
+ },
+ "pollen_mugwort": {
+ "name": "Alsem pollen",
+ "state": {
+ "active": "Actief",
+ "green": "Groen",
+ "yellow": "Geel",
+ "orange": "Oranje",
+ "red": "Rood",
+ "purple": "Paars",
+ "none": "Geen"
+ }
+ },
+ "pollen_oak": {
+ "name": "Eiken pollen",
+ "state": {
+ "active": "Actief",
+ "green": "Groen",
+ "yellow": "Geel",
+ "orange": "Oranje",
+ "red": "Rood",
+ "purple": "Paars",
+ "none": "Geen"
+ }
+ }
+ }
}
}
diff --git a/custom_components/irm_kmi/weather.py b/custom_components/irm_kmi/weather.py
index 0adcc46..02172ff 100644
--- a/custom_components/irm_kmi/weather.py
+++ b/custom_components/irm_kmi/weather.py
@@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (UnitOfPrecipitationDepth, UnitOfPressure,
UnitOfSpeed, UnitOfTemperature)
from homeassistant.core import HomeAssistant
-from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -39,12 +38,8 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity):
WeatherEntity.__init__(self)
self._name = entry.title
self._attr_unique_id = entry.entry_id
- self._attr_device_info = DeviceInfo(
- entry_type=DeviceEntryType.SERVICE,
- identifiers={(DOMAIN, entry.entry_id)},
- manufacturer="IRM KMI",
- name=entry.title
- )
+ self._attr_device_info = coordinator.shared_device_info
+
self._deprecated_forecast_as = get_config_value(entry, CONF_USE_DEPRECATED_FORECAST)
@property
diff --git a/img/pollens.png b/img/pollens.png
new file mode 100644
index 0000000..dca48af
Binary files /dev/null and b/img/pollens.png differ
diff --git a/img/sensors.png b/img/sensors.png
index df99686..b26a9ad 100644
Binary files a/img/sensors.png and b/img/sensors.png differ
diff --git a/tests/conftest.py b/tests/conftest.py
index 8230708..98b27a7 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -196,6 +196,32 @@ def mock_image_and_high_temp_irm_kmi_api(request: pytest.FixtureRequest) -> Gene
yield irm_kmi
+@pytest.fixture()
+def mock_svg_pollen(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
+ """Return a mocked IrmKmi api client."""
+ fixture: str = "pollen.svg"
+
+ svg_str = load_fixture(fixture)
+
+ with patch(
+ "custom_components.irm_kmi.coordinator.IrmKmiApiClient", autospec=True
+ ) as irm_kmi_api_mock:
+ irm_kmi = irm_kmi_api_mock.return_value
+ irm_kmi.get_svg.return_value = svg_str
+ yield irm_kmi
+
+
+@pytest.fixture()
+def mock_exception_irm_kmi_api_svg_pollen(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
+ """Return a mocked IrmKmi api client."""
+ with patch(
+ "custom_components.irm_kmi.coordinator.IrmKmiApiClient", autospec=True
+ ) as irm_kmi_api_mock:
+ irm_kmi = irm_kmi_api_mock.return_value
+ irm_kmi.get_svg.side_effect = IrmKmiApiParametersError
+ yield irm_kmi
+
+
@pytest.fixture()
def mock_coordinator(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
"""Return a mocked coordinator."""
diff --git a/tests/fixtures/high_low_temp.json b/tests/fixtures/high_low_temp.json
index 596d675..d865dae 100644
--- a/tests/fixtures/high_low_temp.json
+++ b/tests/fixtures/high_low_temp.json
@@ -1325,18 +1325,6 @@
]
},
"module": [
- {
- "type": "svg",
- "data": {
- "url": {
- "nl": "https:\/\/app.meteo.be\/services\/appv4\/?s=getSvg&ins=92094&e=pollen&l=nl&k=32003c7eac2900f3d73c50f9e27330ab",
- "fr": "https:\/\/app.meteo.be\/services\/appv4\/?s=getSvg&ins=92094&e=pollen&l=fr&k=32003c7eac2900f3d73c50f9e27330ab",
- "en": "https:\/\/app.meteo.be\/services\/appv4\/?s=getSvg&ins=92094&e=pollen&l=en&k=32003c7eac2900f3d73c50f9e27330ab",
- "de": "https:\/\/app.meteo.be\/services\/appv4\/?s=getSvg&ins=92094&e=pollen&l=de&k=32003c7eac2900f3d73c50f9e27330ab"
- },
- "ratio": 3.0458333333333334
- }
- },
{
"type": "uv",
"data": {
diff --git a/tests/fixtures/pollen.svg b/tests/fixtures/pollen.svg
new file mode 100644
index 0000000..0c23b2a
--- /dev/null
+++ b/tests/fixtures/pollen.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/tests/fixtures/pollen_three.svg b/tests/fixtures/pollen_three.svg
new file mode 100644
index 0000000..04db508
--- /dev/null
+++ b/tests/fixtures/pollen_three.svg
@@ -0,0 +1,45 @@
+
+
\ No newline at end of file
diff --git a/tests/fixtures/pollen_two.svg b/tests/fixtures/pollen_two.svg
new file mode 100644
index 0000000..4bfe1b6
--- /dev/null
+++ b/tests/fixtures/pollen_two.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/tests/test_pollen.py b/tests/test_pollen.py
new file mode 100644
index 0000000..667ca1c
--- /dev/null
+++ b/tests/test_pollen.py
@@ -0,0 +1,63 @@
+from unittest.mock import AsyncMock
+
+from homeassistant.core import HomeAssistant
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+
+from custom_components.irm_kmi import IrmKmiCoordinator
+from custom_components.irm_kmi.pollen import PollenParser
+from tests.conftest import get_api_data
+
+
+def test_svg_pollen_parsing():
+ with open("tests/fixtures/pollen.svg", "r") as file:
+ svg_data = file.read()
+ data = PollenParser(svg_data).get_pollen_data()
+ assert data == {'birch': 'purple', 'oak': 'active', 'hazel': 'none', 'mugwort': 'none', 'alder': 'green',
+ 'grasses': 'none', 'ash': 'active'}
+
+ with open("tests/fixtures/pollen_two.svg", "r") as file:
+ svg_data = file.read()
+ data = PollenParser(svg_data).get_pollen_data()
+ assert data == {'birch': 'purple', 'oak': 'active', 'hazel': 'none', 'mugwort': 'none', 'alder': 'active',
+ 'grasses': 'none', 'ash': 'active'}
+
+ with open("tests/fixtures/pollen_three.svg", "r") as file:
+ svg_data = file.read()
+ data = PollenParser(svg_data).get_pollen_data()
+ assert data == {'birch': 'none', 'oak': 'active', 'hazel': 'none', 'mugwort': 'none', 'alder': 'active',
+ 'grasses': 'none', 'ash': 'active'}
+
+def test_pollen_options():
+ assert PollenParser.get_option_values() == ['active', 'green', 'yellow', 'orange', 'red', 'purple', 'none']
+
+
+def test_pollen_default_values():
+ assert PollenParser.get_default_data() == {'birch': 'none', 'oak': 'none', 'hazel': 'none', 'mugwort': 'none',
+ 'alder': 'none', 'grasses': 'none', 'ash': 'none'}
+
+
+async def test_pollen_data_from_api(
+ hass: HomeAssistant,
+ mock_svg_pollen: AsyncMock,
+ mock_config_entry: MockConfigEntry
+) -> None:
+ coordinator = IrmKmiCoordinator(hass, mock_config_entry)
+ api_data = get_api_data("be_forecast_warning.json")
+
+ result = await coordinator._async_pollen_data(api_data)
+ expected = {'mugwort': 'none', 'birch': 'purple', 'alder': 'green', 'ash': 'active', 'oak': 'active',
+ 'grasses': 'none', 'hazel': 'none'}
+ assert result == expected
+
+
+async def test_pollen_error_leads_to_default_values(
+ hass: HomeAssistant,
+ mock_config_entry: MockConfigEntry,
+ mock_exception_irm_kmi_api_svg_pollen: AsyncMock
+) -> None:
+ coordinator = IrmKmiCoordinator(hass, mock_config_entry)
+ api_data = get_api_data("be_forecast_warning.json")
+
+ result = await coordinator._async_pollen_data(api_data)
+ expected = PollenParser.get_default_data()
+ assert result == expected