mirror of
https://github.com/home-assistant/core.git
synced 2025-11-06 01:19:29 +00:00
Add integration for Belgian weather provider meteo.be (#144689)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -772,6 +772,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/iqvia/ @bachya
|
||||
/tests/components/iqvia/ @bachya
|
||||
/homeassistant/components/irish_rail_transport/ @ttroy50
|
||||
/homeassistant/components/irm_kmi/ @jdejaegh
|
||||
/tests/components/irm_kmi/ @jdejaegh
|
||||
/homeassistant/components/iron_os/ @tr4nt0r
|
||||
/tests/components/iron_os/ @tr4nt0r
|
||||
/homeassistant/components/isal/ @bdraco
|
||||
|
||||
40
homeassistant/components/irm_kmi/__init__.py
Normal file
40
homeassistant/components/irm_kmi/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Integration for IRM KMI weather."""
|
||||
|
||||
import logging
|
||||
|
||||
from irm_kmi_api import IrmKmiApiClientHa
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import IRM_KMI_TO_HA_CONDITION_MAP, PLATFORMS, USER_AGENT
|
||||
from .coordinator import IrmKmiConfigEntry, IrmKmiCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: IrmKmiConfigEntry) -> bool:
|
||||
"""Set up this integration using UI."""
|
||||
api_client = IrmKmiApiClientHa(
|
||||
session=async_get_clientsession(hass),
|
||||
user_agent=USER_AGENT,
|
||||
cdt_map=IRM_KMI_TO_HA_CONDITION_MAP,
|
||||
)
|
||||
|
||||
entry.runtime_data = IrmKmiCoordinator(hass, entry, api_client)
|
||||
|
||||
await entry.runtime_data.async_config_entry_first_refresh()
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: IrmKmiConfigEntry) -> bool:
|
||||
"""Handle removal of an entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: IrmKmiConfigEntry) -> None:
|
||||
"""Reload config entry."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
132
homeassistant/components/irm_kmi/config_flow.py
Normal file
132
homeassistant/components/irm_kmi/config_flow.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Config flow to set up IRM KMI integration via the UI."""
|
||||
|
||||
import logging
|
||||
|
||||
from irm_kmi_api import IrmKmiApiClient, IrmKmiApiError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_UNIQUE_ID,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
LocationSelector,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_LANGUAGE_OVERRIDE,
|
||||
CONF_LANGUAGE_OVERRIDE_OPTIONS,
|
||||
DOMAIN,
|
||||
OUT_OF_BENELUX,
|
||||
USER_AGENT,
|
||||
)
|
||||
from .coordinator import IrmKmiConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IrmKmiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Configuration flow for the IRM KMI integration."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(_config_entry: IrmKmiConfigEntry) -> OptionsFlow:
|
||||
"""Create the options flow."""
|
||||
return IrmKmiOptionFlow()
|
||||
|
||||
async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult:
|
||||
"""Define the user step of the configuration flow."""
|
||||
errors: dict = {}
|
||||
|
||||
default_location = {
|
||||
ATTR_LATITUDE: self.hass.config.latitude,
|
||||
ATTR_LONGITUDE: self.hass.config.longitude,
|
||||
}
|
||||
|
||||
if user_input:
|
||||
_LOGGER.debug("Provided config user is: %s", user_input)
|
||||
|
||||
lat: float = user_input[CONF_LOCATION][ATTR_LATITUDE]
|
||||
lon: float = user_input[CONF_LOCATION][ATTR_LONGITUDE]
|
||||
|
||||
try:
|
||||
api_data = await IrmKmiApiClient(
|
||||
session=async_get_clientsession(self.hass),
|
||||
user_agent=USER_AGENT,
|
||||
).get_forecasts_coord({"lat": lat, "long": lon})
|
||||
except IrmKmiApiError:
|
||||
_LOGGER.exception(
|
||||
"Encountered an unexpected error while configuring the integration"
|
||||
)
|
||||
return self.async_abort(reason="api_error")
|
||||
|
||||
if api_data["cityName"] in OUT_OF_BENELUX:
|
||||
errors[CONF_LOCATION] = "out_of_benelux"
|
||||
|
||||
if not errors:
|
||||
name: str = api_data["cityName"]
|
||||
country: str = api_data["country"]
|
||||
unique_id: str = f"{name.lower()} {country.lower()}"
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
user_input[CONF_UNIQUE_ID] = unique_id
|
||||
|
||||
return self.async_create_entry(title=name, data=user_input)
|
||||
|
||||
default_location = user_input[CONF_LOCATION]
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_LOCATION, default=default_location
|
||||
): LocationSelector()
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class IrmKmiOptionFlow(OptionsFlowWithReload):
|
||||
"""Option flow for the IRM KMI integration, help change the options once the integration was configured."""
|
||||
|
||||
async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
_LOGGER.debug("Provided config user is: %s", user_input)
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_LANGUAGE_OVERRIDE,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_LANGUAGE_OVERRIDE, "none"
|
||||
),
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=CONF_LANGUAGE_OVERRIDE_OPTIONS,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key=CONF_LANGUAGE_OVERRIDE,
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
102
homeassistant/components/irm_kmi/const.py
Normal file
102
homeassistant/components/irm_kmi/const.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Constants for the IRM KMI integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_CONDITION_FOG,
|
||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_POURING,
|
||||
ATTR_CONDITION_RAINY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SNOWY_RAINY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
)
|
||||
from homeassistant.const import Platform, __version__
|
||||
|
||||
DOMAIN: Final = "irm_kmi"
|
||||
PLATFORMS: Final = [Platform.WEATHER]
|
||||
|
||||
OUT_OF_BENELUX: Final = [
|
||||
"außerhalb der Benelux (Brussels)",
|
||||
"Hors de Belgique (Bxl)",
|
||||
"Outside the Benelux (Brussels)",
|
||||
"Buiten de Benelux (Brussel)",
|
||||
]
|
||||
LANGS: Final = ["en", "fr", "nl", "de"]
|
||||
|
||||
CONF_LANGUAGE_OVERRIDE: Final = "language_override"
|
||||
CONF_LANGUAGE_OVERRIDE_OPTIONS: Final = ["none", "fr", "nl", "de", "en"]
|
||||
|
||||
# Dict to map ('ww', 'dayNight') tuple from IRM KMI to HA conditions.
|
||||
IRM_KMI_TO_HA_CONDITION_MAP: Final = {
|
||||
(0, "d"): ATTR_CONDITION_SUNNY,
|
||||
(0, "n"): ATTR_CONDITION_CLEAR_NIGHT,
|
||||
(1, "d"): ATTR_CONDITION_SUNNY,
|
||||
(1, "n"): ATTR_CONDITION_CLEAR_NIGHT,
|
||||
(2, "d"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(2, "n"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(3, "d"): ATTR_CONDITION_PARTLYCLOUDY,
|
||||
(3, "n"): ATTR_CONDITION_PARTLYCLOUDY,
|
||||
(4, "d"): ATTR_CONDITION_POURING,
|
||||
(4, "n"): ATTR_CONDITION_POURING,
|
||||
(5, "d"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(5, "n"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(6, "d"): ATTR_CONDITION_POURING,
|
||||
(6, "n"): ATTR_CONDITION_POURING,
|
||||
(7, "d"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(7, "n"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(8, "d"): ATTR_CONDITION_SNOWY_RAINY,
|
||||
(8, "n"): ATTR_CONDITION_SNOWY_RAINY,
|
||||
(9, "d"): ATTR_CONDITION_SNOWY_RAINY,
|
||||
(9, "n"): ATTR_CONDITION_SNOWY_RAINY,
|
||||
(10, "d"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(10, "n"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(11, "d"): ATTR_CONDITION_SNOWY,
|
||||
(11, "n"): ATTR_CONDITION_SNOWY,
|
||||
(12, "d"): ATTR_CONDITION_SNOWY,
|
||||
(12, "n"): ATTR_CONDITION_SNOWY,
|
||||
(13, "d"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(13, "n"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(14, "d"): ATTR_CONDITION_CLOUDY,
|
||||
(14, "n"): ATTR_CONDITION_CLOUDY,
|
||||
(15, "d"): ATTR_CONDITION_CLOUDY,
|
||||
(15, "n"): ATTR_CONDITION_CLOUDY,
|
||||
(16, "d"): ATTR_CONDITION_POURING,
|
||||
(16, "n"): ATTR_CONDITION_POURING,
|
||||
(17, "d"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(17, "n"): ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
(18, "d"): ATTR_CONDITION_RAINY,
|
||||
(18, "n"): ATTR_CONDITION_RAINY,
|
||||
(19, "d"): ATTR_CONDITION_POURING,
|
||||
(19, "n"): ATTR_CONDITION_POURING,
|
||||
(20, "d"): ATTR_CONDITION_SNOWY_RAINY,
|
||||
(20, "n"): ATTR_CONDITION_SNOWY_RAINY,
|
||||
(21, "d"): ATTR_CONDITION_RAINY,
|
||||
(21, "n"): ATTR_CONDITION_RAINY,
|
||||
(22, "d"): ATTR_CONDITION_SNOWY,
|
||||
(22, "n"): ATTR_CONDITION_SNOWY,
|
||||
(23, "d"): ATTR_CONDITION_SNOWY,
|
||||
(23, "n"): ATTR_CONDITION_SNOWY,
|
||||
(24, "d"): ATTR_CONDITION_FOG,
|
||||
(24, "n"): ATTR_CONDITION_FOG,
|
||||
(25, "d"): ATTR_CONDITION_FOG,
|
||||
(25, "n"): ATTR_CONDITION_FOG,
|
||||
(26, "d"): ATTR_CONDITION_FOG,
|
||||
(26, "n"): ATTR_CONDITION_FOG,
|
||||
(27, "d"): ATTR_CONDITION_FOG,
|
||||
(27, "n"): ATTR_CONDITION_FOG,
|
||||
}
|
||||
|
||||
IRM_KMI_NAME: Final = {
|
||||
"fr": "Institut Royal Météorologique de Belgique",
|
||||
"nl": "Koninklijk Meteorologisch Instituut van België",
|
||||
"de": "Königliche Meteorologische Institut von Belgien",
|
||||
"en": "Royal Meteorological Institute of Belgium",
|
||||
}
|
||||
|
||||
USER_AGENT: Final = (
|
||||
f"https://www.home-assistant.io/integrations/irm_kmi (version {__version__})"
|
||||
)
|
||||
95
homeassistant/components/irm_kmi/coordinator.py
Normal file
95
homeassistant/components/irm_kmi/coordinator.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""DataUpdateCoordinator for the IRM KMI integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from irm_kmi_api import IrmKmiApiClientHa, IrmKmiApiError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_LOCATION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
TimestampDataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .data import ProcessedCoordinatorData
|
||||
from .utils import preferred_language
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type IrmKmiConfigEntry = ConfigEntry[IrmKmiCoordinator]
|
||||
|
||||
|
||||
class IrmKmiCoordinator(TimestampDataUpdateCoordinator[ProcessedCoordinatorData]):
|
||||
"""Coordinator to update data from IRM KMI."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: IrmKmiConfigEntry,
|
||||
api_client: IrmKmiApiClientHa,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name="IRM KMI weather",
|
||||
update_interval=timedelta(minutes=7),
|
||||
)
|
||||
self._api = api_client
|
||||
self._location = entry.data[CONF_LOCATION]
|
||||
|
||||
async def _async_update_data(self) -> ProcessedCoordinatorData:
|
||||
"""Fetch data from API endpoint.
|
||||
|
||||
This is the place to pre-process the data to lookup tables so entities can quickly look up their data.
|
||||
:return: ProcessedCoordinatorData
|
||||
"""
|
||||
|
||||
self._api.expire_cache()
|
||||
|
||||
try:
|
||||
await self._api.refresh_forecasts_coord(
|
||||
{
|
||||
"lat": self._location[ATTR_LATITUDE],
|
||||
"long": self._location[ATTR_LONGITUDE],
|
||||
}
|
||||
)
|
||||
|
||||
except IrmKmiApiError as err:
|
||||
if (
|
||||
self.last_update_success_time is not None
|
||||
and self.update_interval is not None
|
||||
and self.last_update_success_time - utcnow()
|
||||
< timedelta(seconds=2.5 * self.update_interval.seconds)
|
||||
):
|
||||
return self.data
|
||||
|
||||
_LOGGER.warning(
|
||||
"Could not connect to the API since %s", self.last_update_success_time
|
||||
)
|
||||
raise UpdateFailed(
|
||||
f"Error communicating with API for general forecast: {err}. "
|
||||
f"Last success time is: {self.last_update_success_time}"
|
||||
) from err
|
||||
|
||||
if not self.last_update_success:
|
||||
_LOGGER.warning("Successfully reconnected to the API")
|
||||
|
||||
return await self.process_api_data()
|
||||
|
||||
async def process_api_data(self) -> ProcessedCoordinatorData:
|
||||
"""From the API data, create the object that will be used in the entities."""
|
||||
tz = await dt_util.async_get_time_zone("Europe/Brussels")
|
||||
lang = preferred_language(self.hass, self.config_entry)
|
||||
|
||||
return ProcessedCoordinatorData(
|
||||
current_weather=self._api.get_current_weather(tz),
|
||||
daily_forecast=self._api.get_daily_forecast(tz, lang),
|
||||
hourly_forecast=self._api.get_hourly_forecast(tz),
|
||||
country=self._api.get_country(),
|
||||
)
|
||||
17
homeassistant/components/irm_kmi/data.py
Normal file
17
homeassistant/components/irm_kmi/data.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Define data classes for the IRM KMI integration."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from irm_kmi_api import CurrentWeatherData, ExtendedForecast
|
||||
|
||||
from homeassistant.components.weather import Forecast
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessedCoordinatorData:
|
||||
"""Dataclass that will be exposed to the entities consuming data from an IrmKmiCoordinator."""
|
||||
|
||||
current_weather: CurrentWeatherData
|
||||
country: str
|
||||
hourly_forecast: list[Forecast] = field(default_factory=list)
|
||||
daily_forecast: list[ExtendedForecast] = field(default_factory=list)
|
||||
28
homeassistant/components/irm_kmi/entity.py
Normal file
28
homeassistant/components/irm_kmi/entity.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Base class shared among IRM KMI entities."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, IRM_KMI_NAME
|
||||
from .coordinator import IrmKmiConfigEntry, IrmKmiCoordinator
|
||||
from .utils import preferred_language
|
||||
|
||||
|
||||
class IrmKmiBaseEntity(CoordinatorEntity[IrmKmiCoordinator]):
|
||||
"""Base methods for IRM KMI entities."""
|
||||
|
||||
_attr_attribution = (
|
||||
"Weather data from the Royal Meteorological Institute of Belgium meteo.be"
|
||||
)
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, entry: IrmKmiConfigEntry) -> None:
|
||||
"""Init base properties for IRM KMI entities."""
|
||||
coordinator = entry.runtime_data
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
manufacturer=IRM_KMI_NAME.get(preferred_language(self.hass, entry)),
|
||||
)
|
||||
13
homeassistant/components/irm_kmi/manifest.json
Normal file
13
homeassistant/components/irm_kmi/manifest.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "irm_kmi",
|
||||
"name": "IRM KMI Weather Belgium",
|
||||
"codeowners": ["@jdejaegh"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["zone"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/irm_kmi",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["irm_kmi_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["irm-kmi-api==1.1.0"]
|
||||
}
|
||||
86
homeassistant/components/irm_kmi/quality_scale.yaml
Normal file
86
homeassistant/components/irm_kmi/quality_scale.yaml
Normal file
@@ -0,0 +1,86 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: >
|
||||
No service action implemented in this integration at the moment.
|
||||
appropriate-polling:
|
||||
status: done
|
||||
comment: >
|
||||
Polling interval is set to 7 minutes.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: >
|
||||
No service action implemented in this integration at the moment.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: >
|
||||
No service action implemented in this integration at the moment.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: >
|
||||
There is no authentication for this integration
|
||||
test-coverage: todo
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: >
|
||||
The integration does not look for devices on the network. It uses an online API.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: >
|
||||
The integration does not look for devices on the network. It uses an online API.
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: >
|
||||
This integration does not integrate physical devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: >
|
||||
There is no configuration per se, just a zone to pick.
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
50
homeassistant/components/irm_kmi/strings.json
Normal file
50
homeassistant/components/irm_kmi/strings.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"title": "Royal Meteorological Institute of Belgium",
|
||||
"common": {
|
||||
"language_override_description": "Override the Home Assistant language for the textual weather forecast."
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||
"api_error": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"location": "[%key:common::config_flow::data::location%]"
|
||||
},
|
||||
"data_description": {
|
||||
"location": "[%key:common::config_flow::data::location%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"out_of_benelux": "The location is outside of Benelux. Pick a location in Benelux."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"language_override": {
|
||||
"options": {
|
||||
"none": "Follow Home Assistant server language",
|
||||
"fr": "French",
|
||||
"nl": "Dutch",
|
||||
"de": "German",
|
||||
"en": "English"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Options",
|
||||
"data": {
|
||||
"language_override": "[%key:common::config_flow::data::language%]"
|
||||
},
|
||||
"data_description": {
|
||||
"language_override": "[%key:component::irm_kmi::common::language_override_description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
homeassistant/components/irm_kmi/utils.py
Normal file
18
homeassistant/components/irm_kmi/utils.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Helper functions for use with IRM KMI integration."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_LANGUAGE_OVERRIDE, LANGS
|
||||
|
||||
|
||||
def preferred_language(hass: HomeAssistant, config_entry: ConfigEntry | None) -> str:
|
||||
"""Get the preferred language for the integration if it was overridden by the configuration."""
|
||||
|
||||
if (
|
||||
config_entry is None
|
||||
or config_entry.options.get(CONF_LANGUAGE_OVERRIDE) == "none"
|
||||
):
|
||||
return hass.config.language if hass.config.language in LANGS else "en"
|
||||
|
||||
return config_entry.options.get(CONF_LANGUAGE_OVERRIDE, "en")
|
||||
158
homeassistant/components/irm_kmi/weather.py
Normal file
158
homeassistant/components/irm_kmi/weather.py
Normal file
@@ -0,0 +1,158 @@
|
||||
"""Support for IRM KMI weather."""
|
||||
|
||||
from irm_kmi_api import CurrentWeatherData
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
Forecast,
|
||||
SingleCoordinatorWeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import IrmKmiConfigEntry, IrmKmiCoordinator
|
||||
from .entity import IrmKmiBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
_hass: HomeAssistant,
|
||||
entry: IrmKmiConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the weather entry."""
|
||||
async_add_entities([IrmKmiWeather(entry)])
|
||||
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class IrmKmiWeather(
|
||||
IrmKmiBaseEntity, # WeatherEntity
|
||||
SingleCoordinatorWeatherEntity[IrmKmiCoordinator],
|
||||
):
|
||||
"""Weather entity for IRM KMI weather."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY
|
||||
| WeatherEntityFeature.FORECAST_TWICE_DAILY
|
||||
| WeatherEntityFeature.FORECAST_HOURLY
|
||||
)
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
|
||||
def __init__(self, entry: IrmKmiConfigEntry) -> None:
|
||||
"""Create a new instance of the weather entity from a configuration entry."""
|
||||
IrmKmiBaseEntity.__init__(self, entry)
|
||||
SingleCoordinatorWeatherEntity.__init__(self, entry.runtime_data)
|
||||
self._attr_unique_id = entry.data[CONF_UNIQUE_ID]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available
|
||||
|
||||
@property
|
||||
def current_weather(self) -> CurrentWeatherData:
|
||||
"""Return the current weather."""
|
||||
return self.coordinator.data.current_weather
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
return self.current_weather.get("condition")
|
||||
|
||||
@property
|
||||
def native_temperature(self) -> float | None:
|
||||
"""Return the temperature in native units."""
|
||||
return self.current_weather.get("temperature")
|
||||
|
||||
@property
|
||||
def native_wind_speed(self) -> float | None:
|
||||
"""Return the wind speed in native units."""
|
||||
return self.current_weather.get("wind_speed")
|
||||
|
||||
@property
|
||||
def native_wind_gust_speed(self) -> float | None:
|
||||
"""Return the wind gust speed in native units."""
|
||||
return self.current_weather.get("wind_gust_speed")
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> float | str | None:
|
||||
"""Return the wind bearing."""
|
||||
return self.current_weather.get("wind_bearing")
|
||||
|
||||
@property
|
||||
def native_pressure(self) -> float | None:
|
||||
"""Return the pressure in native units."""
|
||||
return self.current_weather.get("pressure")
|
||||
|
||||
@property
|
||||
def uv_index(self) -> float | None:
|
||||
"""Return the UV index."""
|
||||
return self.current_weather.get("uv_index")
|
||||
|
||||
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
return self.coordinator.data.daily_forecast
|
||||
|
||||
def _async_forecast_daily(self) -> list[Forecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
return self.daily_forecast()
|
||||
|
||||
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||
"""Return the hourly forecast in native units."""
|
||||
return self.coordinator.data.hourly_forecast
|
||||
|
||||
def daily_forecast(self) -> list[Forecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
data: list[Forecast] = self.coordinator.data.daily_forecast
|
||||
|
||||
# The data in daily_forecast might contain nighttime forecast.
|
||||
# The following handle the lowest temperature attribute to be displayed correctly.
|
||||
if (
|
||||
len(data) > 1
|
||||
and not data[0].get("is_daytime")
|
||||
and data[1].get("native_templow") is None
|
||||
):
|
||||
data[1]["native_templow"] = data[0].get("native_templow")
|
||||
if (
|
||||
data[1]["native_templow"] is not None
|
||||
and data[1]["native_temperature"] is not None
|
||||
and data[1]["native_templow"] > data[1]["native_temperature"]
|
||||
):
|
||||
(data[1]["native_templow"], data[1]["native_temperature"]) = (
|
||||
data[1]["native_temperature"],
|
||||
data[1]["native_templow"],
|
||||
)
|
||||
|
||||
if len(data) > 0 and not data[0].get("is_daytime"):
|
||||
return data
|
||||
|
||||
if (
|
||||
len(data) > 1
|
||||
and data[0].get("native_templow") is None
|
||||
and not data[1].get("is_daytime")
|
||||
):
|
||||
data[0]["native_templow"] = data[1].get("native_templow")
|
||||
if (
|
||||
data[0]["native_templow"] is not None
|
||||
and data[0]["native_temperature"] is not None
|
||||
and data[0]["native_templow"] > data[0]["native_temperature"]
|
||||
):
|
||||
(data[0]["native_templow"], data[0]["native_temperature"]) = (
|
||||
data[0]["native_temperature"],
|
||||
data[0]["native_templow"],
|
||||
)
|
||||
|
||||
return [f for f in data if f.get("is_daytime")]
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -310,6 +310,7 @@ FLOWS = {
|
||||
"ipma",
|
||||
"ipp",
|
||||
"iqvia",
|
||||
"irm_kmi",
|
||||
"iron_os",
|
||||
"iskra",
|
||||
"islamic_prayer_times",
|
||||
|
||||
@@ -3118,6 +3118,11 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"irm_kmi": {
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"iron_os": {
|
||||
"name": "IronOS",
|
||||
"integration_type": "hub",
|
||||
@@ -7969,6 +7974,7 @@
|
||||
"input_select",
|
||||
"input_text",
|
||||
"integration",
|
||||
"irm_kmi",
|
||||
"islamic_prayer_times",
|
||||
"local_calendar",
|
||||
"local_ip",
|
||||
|
||||
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -1278,6 +1278,9 @@ iottycloud==0.3.0
|
||||
# homeassistant.components.iperf3
|
||||
iperf3==0.1.11
|
||||
|
||||
# homeassistant.components.irm_kmi
|
||||
irm-kmi-api==1.1.0
|
||||
|
||||
# homeassistant.components.isal
|
||||
isal==1.8.0
|
||||
|
||||
|
||||
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -1109,6 +1109,9 @@ iometer==0.1.0
|
||||
# homeassistant.components.iotty
|
||||
iottycloud==0.3.0
|
||||
|
||||
# homeassistant.components.irm_kmi
|
||||
irm-kmi-api==1.1.0
|
||||
|
||||
# homeassistant.components.isal
|
||||
isal==1.8.0
|
||||
|
||||
|
||||
1
tests/components/irm_kmi/__init__.py
Normal file
1
tests/components/irm_kmi/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests of IRM KMI integration."""
|
||||
123
tests/components/irm_kmi/conftest.py
Normal file
123
tests/components/irm_kmi/conftest.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Fixtures for the IRM KMI integration tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from irm_kmi_api import IrmKmiApiError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.irm_kmi.const import DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_UNIQUE_ID,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
return MockConfigEntry(
|
||||
title="Home",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_LOCATION: {ATTR_LATITUDE: 50.84, ATTR_LONGITUDE: 4.35},
|
||||
CONF_UNIQUE_ID: "city country",
|
||||
},
|
||||
unique_id="50.84-4.35",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[None]:
|
||||
"""Mock setting up a config entry."""
|
||||
with patch("homeassistant.components.irm_kmi.async_setup_entry", return_value=True):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_forecast_in_benelux():
|
||||
"""Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it returns something valid and in the Benelux."""
|
||||
with patch(
|
||||
"homeassistant.components.irm_kmi.config_flow.IrmKmiApiClient.get_forecasts_coord",
|
||||
return_value={"cityName": "Brussels", "country": "BE"},
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_forecast_out_benelux_then_in_belgium():
|
||||
"""Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it returns something outside Benelux."""
|
||||
with patch(
|
||||
"homeassistant.components.irm_kmi.config_flow.IrmKmiApiClient.get_forecasts_coord",
|
||||
side_effect=[
|
||||
{"cityName": "Outside the Benelux (Brussels)", "country": "BE"},
|
||||
{"cityName": "Brussels", "country": "BE"},
|
||||
],
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_get_forecast_api_error():
|
||||
"""Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it raises an error."""
|
||||
with patch(
|
||||
"homeassistant.components.irm_kmi.config_flow.IrmKmiApiClient.get_forecasts_coord",
|
||||
side_effect=IrmKmiApiError,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock]:
|
||||
"""Return a mocked IrmKmi api client."""
|
||||
fixture: str = "forecast.json"
|
||||
|
||||
forecast = json.loads(load_fixture(fixture, "irm_kmi"))
|
||||
with patch(
|
||||
"homeassistant.components.irm_kmi.IrmKmiApiClientHa", autospec=True
|
||||
) as irm_kmi_api_mock:
|
||||
irm_kmi = irm_kmi_api_mock.return_value
|
||||
irm_kmi.get_forecasts_coord.return_value = forecast
|
||||
yield irm_kmi
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_irm_kmi_api_nl():
|
||||
"""Mock a call to IrmKmiApiClientHa.get_forecasts_coord() to return a forecast in The Netherlands."""
|
||||
fixture: str = "forecast_nl.json"
|
||||
forecast = json.loads(load_fixture(fixture, "irm_kmi"))
|
||||
with patch(
|
||||
"homeassistant.components.irm_kmi.coordinator.IrmKmiApiClientHa.get_forecasts_coord",
|
||||
return_value=forecast,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_irm_kmi_api_high_low_temp():
|
||||
"""Mock a call to IrmKmiApiClientHa.get_forecasts_coord() to return high_low_temp.json forecast."""
|
||||
fixture: str = "high_low_temp.json"
|
||||
forecast = json.loads(load_fixture(fixture, "irm_kmi"))
|
||||
with patch(
|
||||
"homeassistant.components.irm_kmi.coordinator.IrmKmiApiClientHa.get_forecasts_coord",
|
||||
return_value=forecast,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_exception_irm_kmi_api(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> Generator[None, MagicMock]:
|
||||
"""Return a mocked IrmKmi api client that will raise an error upon refreshing data."""
|
||||
with patch(
|
||||
"homeassistant.components.irm_kmi.IrmKmiApiClientHa", autospec=True
|
||||
) as irm_kmi_api_mock:
|
||||
irm_kmi = irm_kmi_api_mock.return_value
|
||||
irm_kmi.refresh_forecasts_coord.side_effect = IrmKmiApiError
|
||||
yield irm_kmi
|
||||
1474
tests/components/irm_kmi/fixtures/forecast.json
Normal file
1474
tests/components/irm_kmi/fixtures/forecast.json
Normal file
File diff suppressed because it is too large
Load Diff
1355
tests/components/irm_kmi/fixtures/forecast_nl.json
Normal file
1355
tests/components/irm_kmi/fixtures/forecast_nl.json
Normal file
File diff suppressed because it is too large
Load Diff
1625
tests/components/irm_kmi/fixtures/forecast_out_of_benelux.json
Normal file
1625
tests/components/irm_kmi/fixtures/forecast_out_of_benelux.json
Normal file
File diff suppressed because it is too large
Load Diff
1635
tests/components/irm_kmi/fixtures/high_low_temp.json
Normal file
1635
tests/components/irm_kmi/fixtures/high_low_temp.json
Normal file
File diff suppressed because it is too large
Load Diff
694
tests/components/irm_kmi/snapshots/test_weather.ambr
Normal file
694
tests/components/irm_kmi/snapshots/test_weather.ambr
Normal file
@@ -0,0 +1,694 @@
|
||||
# serializer version: 1
|
||||
# name: test_forecast_service[daily]
|
||||
dict({
|
||||
'weather.home': dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'condition_2': None,
|
||||
'condition_evol': <ConditionEvol.STABLE: 'stable'>,
|
||||
'datetime': '2023-12-28',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.1,
|
||||
'precipitation_probability': None,
|
||||
'sunrise': '2023-12-28T08:47:43+01:00',
|
||||
'sunset': '2023-12-28T16:34:06+01:00',
|
||||
'temperature': 11.0,
|
||||
'templow': 9.0,
|
||||
'text': '''
|
||||
Waarschuwingen
|
||||
Vanavond zijn er in het noordwesten zware windstoten mogelijk van 75-90 km/uur (code geel).
|
||||
|
||||
Vanochtend is het half bewolkt met in het noorden kans op een bui. De wind komt uit het zuidwesten en is matig tot vrij krachtig, aan de kust krachtig tot hard, windkracht 6-7, af en toe even stormachtig, windkracht 8. Aan de kust komen windstoten voor van ongeveer 80 km/uur.
|
||||
Vanmiddag is het half tot zwaar bewolkt met kans op een bui, vooral in het noorden en westen. De middagtemperatuur ligt rond 11°C. De wind komt uit het zuidwesten en is meestal vrij krachtig, aan de kust krachtig tot hard, windkracht 6-7, vooral later ook af en toe stormachtig, windkracht 8. Aan de kust zijn er windstoten tot ongeveer 80 km/uur.
|
||||
Vanavond zijn er buien, alleen in het zuidoosten is het overwegend droog. De wind komt uit het zuidwesten en is meestal vrij krachtig, aan de kust hard tot stormachtig, windkracht 7 tot 8. Vooral in het noordwesten zijn windstoten mogelijk van 75-90 km/uur.
|
||||
|
||||
Komende nacht komen er enkele buien voor. Met een minimumtemperatuur van ongeveer 8°C is het zacht. De wind komt uit het zuidwesten en is matig tot vrij krachtig, aan zee krachtig tot hard, windkracht 6-7, met zware windstoten tot ongeveer 80 km/uur.
|
||||
|
||||
Morgenochtend is het half tot zwaar bewolkt en zijn er enkele buien. De wind komt uit het zuidwesten en is matig tot vrij krachtig, aan zee krachtig hard, windkracht 6-7, met vooral in het noordwesten mogelijk zware windstoten tot ongeveer 80 km/uur.
|
||||
Morgenmiddag is er af en toe ruimte voor de zon en blijft het op de meeste plaatsen droog, alleen in het zuidoosten kan een enkele bui vallen. Met middagtemperaturen van ongeveer 10°C blijft het zacht. De wind uit het zuidwesten is matig tot vrij krachtig, aan zee krachtig tot hard, windkracht 6-7, met in het Waddengebied zware windstoten tot ongeveer 80 km/uur.
|
||||
Morgenavond is het half tot zwaar bewolkt met een enkele bui. De wind komt uit het zuidwesten en is meest matig, aan de kust krachtig tot hard, windkracht 6-7, boven de Wadden eerst stormachtig, windkracht 8.
|
||||
(Bron: KNMI, 2023-12-28T06:56:00+01:00)
|
||||
|
||||
''',
|
||||
'wind_bearing': 225.0,
|
||||
'wind_gust_speed': 33.0,
|
||||
'wind_speed': 32.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'condition_2': None,
|
||||
'condition_evol': <ConditionEvol.STABLE: 'stable'>,
|
||||
'datetime': '2023-12-29',
|
||||
'is_daytime': True,
|
||||
'precipitation': 3.8,
|
||||
'precipitation_probability': None,
|
||||
'sunrise': '2023-12-29T08:47:48+01:00',
|
||||
'sunset': '2023-12-29T16:35:00+01:00',
|
||||
'temperature': 10.0,
|
||||
'text': '',
|
||||
'wind_bearing': 248.0,
|
||||
'wind_gust_speed': 28.0,
|
||||
'wind_speed': 26.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'condition_2': None,
|
||||
'condition_evol': <ConditionEvol.STABLE: 'stable'>,
|
||||
'datetime': '2023-12-30',
|
||||
'is_daytime': True,
|
||||
'precipitation': 1.7,
|
||||
'precipitation_probability': None,
|
||||
'sunrise': '2023-12-30T08:47:49+01:00',
|
||||
'sunset': '2023-12-30T16:35:57+01:00',
|
||||
'temperature': 10.0,
|
||||
'templow': 5.0,
|
||||
'text': '',
|
||||
'wind_bearing': 225.0,
|
||||
'wind_gust_speed': 25.0,
|
||||
'wind_speed': 22.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'condition_2': None,
|
||||
'condition_evol': <ConditionEvol.STABLE: 'stable'>,
|
||||
'datetime': '2023-12-31',
|
||||
'is_daytime': True,
|
||||
'precipitation': 4.2,
|
||||
'precipitation_probability': None,
|
||||
'sunrise': '2023-12-31T08:47:47+01:00',
|
||||
'sunset': '2023-12-31T16:36:56+01:00',
|
||||
'temperature': 9.0,
|
||||
'templow': 7.0,
|
||||
'text': '',
|
||||
'wind_bearing': 203.0,
|
||||
'wind_gust_speed': 31.0,
|
||||
'wind_speed': 30.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'condition_2': None,
|
||||
'condition_evol': <ConditionEvol.STABLE: 'stable'>,
|
||||
'datetime': '2024-01-01',
|
||||
'is_daytime': True,
|
||||
'precipitation': 2.2,
|
||||
'precipitation_probability': None,
|
||||
'sunrise': '2024-01-01T08:47:42+01:00',
|
||||
'sunset': '2024-01-01T16:37:59+01:00',
|
||||
'temperature': 7.0,
|
||||
'templow': 5.0,
|
||||
'text': '',
|
||||
'wind_bearing': 225.0,
|
||||
'wind_gust_speed': 28.0,
|
||||
'wind_speed': 23.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'condition_2': None,
|
||||
'condition_evol': <ConditionEvol.STABLE: 'stable'>,
|
||||
'datetime': '2024-01-02',
|
||||
'is_daytime': True,
|
||||
'precipitation': 1.4,
|
||||
'precipitation_probability': None,
|
||||
'sunrise': '2024-01-02T08:47:32+01:00',
|
||||
'sunset': '2024-01-02T16:39:04+01:00',
|
||||
'temperature': 6.0,
|
||||
'templow': 3.0,
|
||||
'text': '',
|
||||
'wind_bearing': 225.0,
|
||||
'wind_gust_speed': 16.0,
|
||||
'wind_speed': 15.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'condition_2': None,
|
||||
'condition_evol': <ConditionEvol.STABLE: 'stable'>,
|
||||
'datetime': '2024-01-03',
|
||||
'is_daytime': True,
|
||||
'precipitation': 1.0,
|
||||
'precipitation_probability': None,
|
||||
'sunrise': '2024-01-03T08:47:20+01:00',
|
||||
'sunset': '2024-01-03T16:40:12+01:00',
|
||||
'temperature': 6.0,
|
||||
'templow': 3.0,
|
||||
'text': '',
|
||||
'wind_bearing': 203.0,
|
||||
'wind_gust_speed': 14.0,
|
||||
'wind_speed': 13.0,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service[hourly]
|
||||
dict({
|
||||
'weather.home': dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-22T15:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1008.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 33.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-22T16:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1008.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 32.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-22T17:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1007.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 32.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-22T18:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1007.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-22T19:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-22T20:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-22T21:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'datetime': '2025-09-22T22:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.7,
|
||||
'precipitation_probability': 70,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'datetime': '2025-09-22T23:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.1,
|
||||
'precipitation_probability': 10,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 37.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-23T00:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 20,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'datetime': '2025-09-23T01:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 1.9,
|
||||
'precipitation_probability': 80,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 31.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'datetime': '2025-09-23T02:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.6,
|
||||
'precipitation_probability': 70,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 38.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T03:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-23T04:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 34.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T05:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 9.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 35.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-23T06:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 9.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 34.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T07:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 9.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 32.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T08:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 9.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 31.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T09:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 9.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 31.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-23T10:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 9.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 32.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T11:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 32.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T12:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 34.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T13:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 33.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T14:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 31.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'sunny',
|
||||
'datetime': '2025-09-23T15:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 10.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 28.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'sunny',
|
||||
'datetime': '2025-09-23T16:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 9.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 24.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'clear-night',
|
||||
'datetime': '2025-09-23T17:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 8.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 20.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T18:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 8.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 18.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-23T19:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 8.0,
|
||||
'wind_bearing': 203.0,
|
||||
'wind_speed': 15.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'datetime': '2025-09-23T20:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 5.7,
|
||||
'precipitation_probability': 100,
|
||||
'pressure': 1005.0,
|
||||
'temperature': 8.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 22.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'pouring',
|
||||
'datetime': '2025-09-23T21:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 3.8,
|
||||
'precipitation_probability': 100,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 7.0,
|
||||
'wind_bearing': 270.0,
|
||||
'wind_speed': 26.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-23T22:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1006.0,
|
||||
'temperature': 8.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 24.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'cloudy',
|
||||
'datetime': '2025-09-23T23:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1007.0,
|
||||
'temperature': 7.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 22.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-24T00:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1008.0,
|
||||
'temperature': 8.0,
|
||||
'wind_bearing': 270.0,
|
||||
'wind_speed': 26.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'clear-night',
|
||||
'datetime': '2025-09-24T01:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1007.0,
|
||||
'temperature': 7.0,
|
||||
'wind_bearing': 270.0,
|
||||
'wind_speed': 26.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'clear-night',
|
||||
'datetime': '2025-09-24T02:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1008.0,
|
||||
'temperature': 7.0,
|
||||
'wind_bearing': 270.0,
|
||||
'wind_speed': 24.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-24T03:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1008.0,
|
||||
'temperature': 7.0,
|
||||
'wind_bearing': 270.0,
|
||||
'wind_speed': 24.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'clear-night',
|
||||
'datetime': '2025-09-24T04:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1009.0,
|
||||
'temperature': 7.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 23.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'clear-night',
|
||||
'datetime': '2025-09-24T05:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1009.0,
|
||||
'temperature': 6.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 23.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-24T06:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1009.0,
|
||||
'temperature': 6.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 21.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-24T07:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1010.0,
|
||||
'temperature': 6.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 20.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-24T08:00:00+02:00',
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1011.0,
|
||||
'temperature': 6.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 17.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'sunny',
|
||||
'datetime': '2025-09-24T09:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1011.0,
|
||||
'temperature': 6.0,
|
||||
'wind_bearing': 248.0,
|
||||
'wind_speed': 13.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-09-24T10:00:00+02:00',
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1012.0,
|
||||
'temperature': 5.0,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 12.0,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_weather_nl[weather.home-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'weather',
|
||||
'entity_category': None,
|
||||
'entity_id': 'weather.home',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'irm_kmi',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <WeatherEntityFeature: 7>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'city country',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_weather_nl[weather.home-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Weather data from the Royal Meteorological Institute of Belgium meteo.be',
|
||||
'friendly_name': 'Home',
|
||||
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
|
||||
'pressure': 1008.0,
|
||||
'pressure_unit': <UnitOfPressure.HPA: 'hPa'>,
|
||||
'supported_features': <WeatherEntityFeature: 7>,
|
||||
'temperature': 11.0,
|
||||
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
'uv_index': 1,
|
||||
'visibility_unit': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
'wind_bearing': 225.0,
|
||||
'wind_speed': 40.0,
|
||||
'wind_speed_unit': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'weather.home',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'cloudy',
|
||||
})
|
||||
# ---
|
||||
154
tests/components/irm_kmi/test_config_flow.py
Normal file
154
tests/components/irm_kmi/test_config_flow.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Tests for the IRM KMI config flow."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from homeassistant.components.irm_kmi.const import CONF_LANGUAGE_OVERRIDE, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE,
|
||||
ATTR_LONGITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_UNIQUE_ID,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_full_user_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: MagicMock,
|
||||
mock_get_forecast_in_benelux: MagicMock,
|
||||
) -> 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"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION: {ATTR_LATITUDE: 50.123, ATTR_LONGITUDE: 4.456}},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == "Brussels"
|
||||
assert result.get("data") == {
|
||||
CONF_LOCATION: {ATTR_LATITUDE: 50.123, ATTR_LONGITUDE: 4.456},
|
||||
CONF_UNIQUE_ID: "brussels be",
|
||||
}
|
||||
|
||||
|
||||
async def test_user_flow_home(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: MagicMock,
|
||||
mock_get_forecast_in_benelux: MagicMock,
|
||||
) -> None:
|
||||
"""Test the full user configuration flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION: {ATTR_LATITUDE: 50.123, ATTR_LONGITUDE: 4.456}},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert result.get("title") == "Brussels"
|
||||
|
||||
|
||||
async def test_config_flow_location_out_benelux(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: MagicMock,
|
||||
mock_get_forecast_out_benelux_then_in_belgium: MagicMock,
|
||||
) -> None:
|
||||
"""Test configuration flow with a zone outside of Benelux."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION: {ATTR_LATITUDE: 0.123, ATTR_LONGITUDE: 0.456}},
|
||||
)
|
||||
|
||||
assert result.get("type") is FlowResultType.FORM
|
||||
assert result.get("step_id") == "user"
|
||||
assert CONF_LOCATION in result.get("errors")
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION: {ATTR_LATITUDE: 50.123, ATTR_LONGITUDE: 4.456}},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_config_flow_with_api_error(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: MagicMock,
|
||||
mock_get_forecast_api_error: MagicMock,
|
||||
) -> None:
|
||||
"""Test when API returns an error during the configuration flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION: {ATTR_LATITUDE: 50.123, ATTR_LONGITUDE: 4.456}},
|
||||
)
|
||||
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
|
||||
|
||||
async def test_setup_twice_same_location(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: MagicMock,
|
||||
mock_get_forecast_in_benelux: MagicMock,
|
||||
) -> None:
|
||||
"""Test when the user tries to set up the weather twice for the same location."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION: {ATTR_LATITUDE: 50.5, ATTR_LONGITUDE: 4.6}},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
|
||||
# Set up a second time
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_LOCATION: {ATTR_LATITUDE: 50.5, ATTR_LONGITUDE: 4.6}},
|
||||
)
|
||||
assert result.get("type") is FlowResultType.ABORT
|
||||
|
||||
|
||||
async def test_option_flow(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test when the user changes options with the option flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
assert not mock_config_entry.options
|
||||
|
||||
result = await hass.config_entries.options.async_init(
|
||||
mock_config_entry.entry_id, data=None
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_LANGUAGE_OVERRIDE: "none"}
|
||||
43
tests/components/irm_kmi/test_init.py
Normal file
43
tests/components/irm_kmi/test_init.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Tests for the IRM KMI integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.irm_kmi.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_load_unload_config_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_irm_kmi_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the IRM KMI configuration entry loading/unloading."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert not hass.data.get(DOMAIN)
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_exception_irm_kmi_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the IRM KMI configuration entry not ready."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_exception_irm_kmi_api.refresh_forecasts_coord.call_count == 1
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
99
tests/components/irm_kmi/test_weather.py
Normal file
99
tests/components/irm_kmi/test_weather.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Test for the weather entity of the IRM KMI integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECASTS,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-12-28T15:30:00+01:00")
|
||||
async def test_weather_nl(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_irm_kmi_api_nl: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test weather with forecast from the Netherland."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"forecast_type",
|
||||
["daily", "hourly"],
|
||||
)
|
||||
async def test_forecast_service(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_irm_kmi_api_nl: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
forecast_type: str,
|
||||
) -> None:
|
||||
"""Test multiple forecast."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECASTS,
|
||||
{
|
||||
ATTR_ENTITY_ID: "weather.home",
|
||||
"type": forecast_type,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2024-01-21T14:15:00+01:00")
|
||||
@pytest.mark.parametrize(
|
||||
"forecast_type",
|
||||
["daily", "hourly"],
|
||||
)
|
||||
async def test_weather_higher_temp_at_night(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_irm_kmi_api_high_low_temp: AsyncMock,
|
||||
forecast_type: str,
|
||||
) -> None:
|
||||
"""Test that the templow is always lower than temperature, even when API returns the opposite."""
|
||||
# Test case for https://github.com/jdejaegh/irm-kmi-ha/issues/8
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECASTS,
|
||||
{
|
||||
ATTR_ENTITY_ID: "weather.home",
|
||||
"type": forecast_type,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
for forecast in response["weather.home"]["forecast"]:
|
||||
assert (
|
||||
forecast.get("native_temperature") is None
|
||||
or forecast.get("native_templow") is None
|
||||
or forecast["native_temperature"] >= forecast["native_templow"]
|
||||
)
|
||||
Reference in New Issue
Block a user