mirror of
https://github.com/jdejaegh/irm-kmi-ha.git
synced 2025-06-27 03:35:56 +02:00
Merge pull request #18 from jdejaegh/12-pollen-and-uv-index-data-as-attributes
Fetch pollen level data from API
This commit is contained in:
commit
28cebe63c3
21 changed files with 643 additions and 35 deletions
13
README.md
13
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)
|
- 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 camera entity for rain radar and short-term rain previsions
|
||||||
- A binary sensor for weather warnings
|
- A binary sensor for weather warnings
|
||||||
|
- Sensors for active pollens
|
||||||
|
|
||||||
The following options are available:
|
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
|
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.
|
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)
|
||||||
|
|
||||||
|
<img height="200" src="https://github.com/jdejaegh/irm-kmi-ha/raw/main/img/pollens.png" alt="Pollen data"/>
|
||||||
|
|
||||||
|
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
|
## Disclaimer
|
||||||
|
|
||||||
This is a personal project and isn't in any way affiliated with, sponsored or endorsed by [The Royal Meteorological
|
This is a personal project and isn't in any way affiliated with, sponsored or endorsed by [The Royal Meteorological
|
||||||
|
|
|
@ -54,6 +54,11 @@ class IrmKmiApiClient:
|
||||||
r: ClientResponse = await self._api_wrapper(base_url=url, params={} if params is None else params)
|
r: ClientResponse = await self._api_wrapper(base_url=url, params={} if params is None else params)
|
||||||
return await r.read()
|
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(
|
async def _api_wrapper(
|
||||||
self,
|
self,
|
||||||
params: dict,
|
params: dict,
|
||||||
|
|
|
@ -8,7 +8,6 @@ from homeassistant.components.binary_sensor import (BinarySensorDeviceClass,
|
||||||
BinarySensorEntity)
|
BinarySensorEntity)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
@ -36,12 +35,7 @@ class IrmKmiWarning(CoordinatorEntity, BinarySensorEntity):
|
||||||
self._attr_unique_id = entry.entry_id
|
self._attr_unique_id = entry.entry_id
|
||||||
self.entity_id = binary_sensor.ENTITY_ID_FORMAT.format(f"weather_warning_{str(entry.title).lower()}")
|
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_name = f"Warning {entry.title}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = coordinator.shared_device_info
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
|
||||||
identifiers={(DOMAIN, entry.entry_id)},
|
|
||||||
manufacturer="IRM KMI",
|
|
||||||
name=f"Warning {entry.title}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool | None:
|
def is_on(self) -> bool | None:
|
||||||
|
|
|
@ -6,7 +6,6 @@ from aiohttp import web
|
||||||
from homeassistant.components.camera import Camera, async_get_still_stream
|
from homeassistant.components.camera import Camera, async_get_still_stream
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
@ -36,12 +35,7 @@ class IrmKmiRadar(CoordinatorEntity, Camera):
|
||||||
self.content_type = 'image/svg+xml'
|
self.content_type = 'image/svg+xml'
|
||||||
self._name = f"Radar {entry.title}"
|
self._name = f"Radar {entry.title}"
|
||||||
self._attr_unique_id = entry.entry_id
|
self._attr_unique_id = entry.entry_id
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = coordinator.shared_device_info
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
|
||||||
identifiers={(DOMAIN, entry.entry_id)},
|
|
||||||
manufacturer="IRM KMI",
|
|
||||||
name=f"Radar {entry.title}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._image_index = False
|
self._image_index = False
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ from homeassistant.components.weather import (ATTR_CONDITION_CLEAR_NIGHT,
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
DOMAIN: Final = 'irm_kmi'
|
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
|
CONFIG_FLOW_VERSION = 3
|
||||||
|
|
||||||
OUT_OF_BENELUX: Final = ["außerhalb der Benelux (Brussels)",
|
OUT_OF_BENELUX: Final = ["außerhalb der Benelux (Brussels)",
|
||||||
|
@ -133,3 +133,10 @@ MAP_WARNING_ID_TO_SLUG: Final = {
|
||||||
14: 'thunderstorm_large_rainfall',
|
14: 'thunderstorm_large_rainfall',
|
||||||
15: 'storm_surge',
|
15: 'storm_surge',
|
||||||
17: 'coldspell'}
|
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'
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_ZONE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import issue_registry
|
from homeassistant.helpers import issue_registry
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (DataUpdateCoordinator,
|
from homeassistant.helpers.update_coordinator import (DataUpdateCoordinator,
|
||||||
UpdateFailed)
|
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 .const import OPTION_STYLE_SATELLITE, OUT_OF_BENELUX, STYLE_TO_PARAM_MAP
|
||||||
from .data import (AnimationFrameData, CurrentWeatherData, IrmKmiForecast,
|
from .data import (AnimationFrameData, CurrentWeatherData, IrmKmiForecast,
|
||||||
ProcessedCoordinatorData, RadarAnimationData, WarningData)
|
ProcessedCoordinatorData, RadarAnimationData, WarningData)
|
||||||
|
from .pollen import PollenParser
|
||||||
from .rain_graph import RainGraph
|
from .rain_graph import RainGraph
|
||||||
from .utils import disable_from_config, get_config_value
|
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._dark_mode = get_config_value(entry, CONF_DARK_MODE)
|
||||||
self._style = get_config_value(entry, CONF_STYLE)
|
self._style = get_config_value(entry, CONF_STYLE)
|
||||||
self._config_entry = entry
|
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:
|
async def _async_update_data(self) -> ProcessedCoordinatorData:
|
||||||
"""Fetch data from API endpoint.
|
"""Fetch data from API endpoint.
|
||||||
|
@ -124,6 +132,28 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
|
||||||
radar_animation['svg_still'] = rain_graph.get_svg_string(still_image=True)
|
radar_animation['svg_still'] = rain_graph.get_svg_string(still_image=True)
|
||||||
return radar_animation
|
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:
|
async def process_api_data(self, api_data: dict) -> ProcessedCoordinatorData:
|
||||||
"""From the API data, create the object that will be used in the entities"""
|
"""From the API data, create the object that will be used in the entities"""
|
||||||
return ProcessedCoordinatorData(
|
return ProcessedCoordinatorData(
|
||||||
|
@ -131,7 +161,8 @@ class IrmKmiCoordinator(DataUpdateCoordinator):
|
||||||
daily_forecast=self.daily_list_to_forecast(api_data.get('for', {}).get('daily')),
|
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')),
|
hourly_forecast=IrmKmiCoordinator.hourly_list_to_forecast(api_data.get('for', {}).get('hourly')),
|
||||||
animation=await self._async_animation_data(api_data=api_data),
|
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,
|
async def download_images_from_api(self,
|
||||||
|
|
|
@ -62,3 +62,4 @@ class ProcessedCoordinatorData(TypedDict, total=False):
|
||||||
daily_forecast: List[IrmKmiForecast] | None
|
daily_forecast: List[IrmKmiForecast] | None
|
||||||
animation: RadarAnimationData
|
animation: RadarAnimationData
|
||||||
warnings: List[WarningData]
|
warnings: List[WarningData]
|
||||||
|
pollen: dict
|
||||||
|
|
131
custom_components/irm_kmi/pollen.py
Normal file
131
custom_components/irm_kmi/pollen.py
Normal file
|
@ -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
|
||||||
|
|
47
custom_components/irm_kmi/sensor.py
Normal file
47
custom_components/irm_kmi/sensor.py
Normal file
|
@ -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)
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (UnitOfPrecipitationDepth, UnitOfPressure,
|
from homeassistant.const import (UnitOfPrecipitationDepth, UnitOfPressure,
|
||||||
UnitOfSpeed, UnitOfTemperature)
|
UnitOfSpeed, UnitOfTemperature)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
@ -39,12 +38,8 @@ class IrmKmiWeather(CoordinatorEntity, WeatherEntity):
|
||||||
WeatherEntity.__init__(self)
|
WeatherEntity.__init__(self)
|
||||||
self._name = entry.title
|
self._name = entry.title
|
||||||
self._attr_unique_id = entry.entry_id
|
self._attr_unique_id = entry.entry_id
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = coordinator.shared_device_info
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
|
||||||
identifiers={(DOMAIN, entry.entry_id)},
|
|
||||||
manufacturer="IRM KMI",
|
|
||||||
name=entry.title
|
|
||||||
)
|
|
||||||
self._deprecated_forecast_as = get_config_value(entry, CONF_USE_DEPRECATED_FORECAST)
|
self._deprecated_forecast_as = get_config_value(entry, CONF_USE_DEPRECATED_FORECAST)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
BIN
img/pollens.png
Normal file
BIN
img/pollens.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
img/sensors.png
BIN
img/sensors.png
Binary file not shown.
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 32 KiB |
|
@ -196,6 +196,32 @@ def mock_image_and_high_temp_irm_kmi_api(request: pytest.FixtureRequest) -> Gene
|
||||||
yield irm_kmi
|
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()
|
@pytest.fixture()
|
||||||
def mock_coordinator(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
def mock_coordinator(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||||
"""Return a mocked coordinator."""
|
"""Return a mocked coordinator."""
|
||||||
|
|
12
tests/fixtures/high_low_temp.json
vendored
12
tests/fixtures/high_low_temp.json
vendored
|
@ -1325,18 +1325,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"module": [
|
"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",
|
"type": "uv",
|
||||||
"data": {
|
"data": {
|
||||||
|
|
2
tests/fixtures/pollen.svg
vendored
Normal file
2
tests/fixtures/pollen.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.2 KiB |
45
tests/fixtures/pollen_three.svg
vendored
Normal file
45
tests/fixtures/pollen_three.svg
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg viewBox="35 88 129 63.0" version="1.1" id="svg740" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs id="defs737"/>
|
||||||
|
<g id="layer1">
|
||||||
|
<rect id="rectangle-white" x="35" y="88" width="129" height="63.0" rx="5" fill="white" fill-opacity="0.15"/>
|
||||||
|
<g id="g1495" transform="translate(30.342966,94.25)">
|
||||||
|
<g id="layer1-2">
|
||||||
|
<ellipse style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="path268" cx="13.312511" cy="0.0"
|
||||||
|
rx="1.6348698" ry="1.5258785"/>
|
||||||
|
<ellipse style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="path272" cx="16.208567"
|
||||||
|
cy="1.463598" rx="1.1366237" ry="1.1366239"/>
|
||||||
|
<rect style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="rect326" width="0.79407966"
|
||||||
|
height="3.5655735" x="12.923257" y="1.401318"/>
|
||||||
|
<rect style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="rect328" width="0.68508834"
|
||||||
|
height="2.5535111" x="15.866023" y="2.36669"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="54.476421" y="98.75" id="text228"><tspan id="tspan226" style="fill:#ffffff;stroke-width:0.264583" x="54.476421" y="98.75">Active pollen</tspan></text>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="139.37601" y="98.75" id="text334"><tspan id="tspan332" style="stroke-width:0.264583" x="139.37601" y="98.75"></tspan></text>
|
||||||
|
<rect style="fill:#607eaa;stroke-width:0.264583" id="rect392" width="127.80161" height="0.44039145"
|
||||||
|
x="35.451504" y="105.5"/>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="42.973724" y="119.0" id="text993"><tspan id="tspan991" style="stroke-width:0.264583" x="42.973724" y="119.0">Alder</tspan></text>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="142.0" y="119.0" id="text1001"><tspan id="tspan999" style="stroke-width:0.264583" x="142.0" y="119.0">active</tspan></text>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="42.973724" y="128.0" id="text993"><tspan id="tspan991" style="stroke-width:0.264583" x="42.973724" y="128.0">Ash</tspan></text>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="142.0" y="128.0" id="text1001"><tspan id="tspan999" style="stroke-width:0.264583" x="142.0" y="128.0">active</tspan></text>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="42.973724" y="137.0" id="text993"><tspan id="tspan991" style="stroke-width:0.264583" x="42.973724" y="137.0">Oak</tspan></text>
|
||||||
|
<text xml:space="preserve"
|
||||||
|
style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583"
|
||||||
|
x="142.0" y="137.0" id="text1001"><tspan id="tspan999" style="stroke-width:0.264583" x="142.0" y="137.0">active</tspan></text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.6 KiB |
2
tests/fixtures/pollen_two.svg
vendored
Normal file
2
tests/fixtures/pollen_two.svg
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg viewBox="35 88 129 63.0" version="1.1" id="svg740" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <defs id="defs737" /> <g id="layer1"> <rect id="rectangle-white" x="35" y="88" width="129" height="63.0" rx="5" fill="white" fill-opacity="0.15"/> <g id="g1495" transform="translate(30.342966,94.25)"> <g id="layer1-2"> <ellipse style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="path268" cx="13.312511" cy="0.0" rx="1.6348698" ry="1.5258785" /> <ellipse style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="path272" cx="16.208567" cy="1.463598" rx="1.1366237" ry="1.1366239" /> <rect style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="rect326" width="0.79407966" height="3.5655735" x="12.923257" y="1.401318" /> <rect style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="rect328" width="0.68508834" height="2.5535111" x="15.866023" y="2.36669" /> </g> </g> <text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="54.476421" y="98.75" id="text228"><tspan id="tspan226" style="fill:#ffffff;stroke-width:0.264583" x="54.476421" y="98.75">Active pollen</tspan></text> <text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="139.37601" y="98.75" id="text334"><tspan id="tspan332" style="stroke-width:0.264583" x="139.37601" y="98.75"></tspan></text> <rect style="fill:#607eaa;stroke-width:0.264583" id="rect392" width="127.80161" height="0.44039145" x="35.451504" y="105.5" /><text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="42.973724" y="119.0" id="text993"><tspan id="tspan991" style="stroke-width:0.264583" x="42.973724" y="119.0">Alder</tspan></text> <text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="142.0" y="119.0" id="text1001"><tspan id="tspan999" style="stroke-width:0.264583" x="142.0" y="119.0">active</tspan></text><text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="42.973724" y="128.0" id="text993"><tspan id="tspan991" style="stroke-width:0.264583" x="42.973724" y="128.0">Ash</tspan></text> <text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="142.0" y="128.0" id="text1001"><tspan id="tspan999" style="stroke-width:0.264583" x="142.0" y="128.0">active</tspan></text><text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="42.973724" y="137.0" id="text993"><tspan id="tspan991" style="stroke-width:0.264583" x="42.973724" y="137.0">Oak</tspan></text> <text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="142.0" y="137.0" id="text1001"><tspan id="tspan999" style="stroke-width:0.264583" x="142.0" y="137.0">active</tspan></text><rect style="fill:#607eaa;stroke-width:0.264583" id="rect392" width="127.80161" height="0.44039145" x="35.451504" y="141.0" /> <rect style="fill:#70ad47;fill-opacity:1;stroke-width:0.2187" id="rectgreen" width="15.0" height="4.0" x="80.0" y="144.0" /> <rect style="fill:#ffd966;fill-opacity:1;stroke-width:0.2187" id="rectyellow" width="15.0" height="4.0" x="95.0" y="144.0" /> <rect style="fill:#ed7d31;fill-opacity:1;stroke-width:0.2187" id="rectorange" width="15.0" height="4.0" x="110.0" y="144.0" /> <rect style="fill:#c00000;fill-opacity:1;stroke-width:0.2187" id="rectred" width="15.0" height="4.0" x="125.0" y="144.0" /> <rect style="fill:#7030a0;fill-opacity:1;stroke-width:0.2187" id="rectpurple" width="15.0" height="4.0" x="140.0" y="144.0" /> <ellipse style="fill:#70ad47;fill-opacity:1;stroke-width:0.264583" id="path1639" cx="80.0" cy="146.0" rx="2.0" ry="2.0" /> <ellipse style="fill:#7030a0;fill-opacity:1;stroke-width:0.264583" id="path1641" cx="155.0" cy="146.0" rx="2.0" ry="2.0" /> <text xml:space="preserve" style="font-family:Arial,Helvetica,sans-serif;font-size:6.0px;fill:#ffffff;stroke-width:0.264583" x="42.351849" y="148.0" id="text396"><tspan id="tspan394" style="stroke-width:0.264583" x="42.351849" y="148.0">Birch</tspan></text> <ellipse style="fill:#ffffff;fill-opacity:1;stroke-width:0.264583" id="path1644" cx="147.5" cy="146.0" rx="3.0" ry="3.0" /></g> </svg>
|
After Width: | Height: | Size: 4.4 KiB |
63
tests/test_pollen.py
Normal file
63
tests/test_pollen.py
Normal file
|
@ -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
|
Loading…
Add table
Reference in a new issue