Environment Canada: Add a detailed forecast action (#138806)

* Add forecast service.

* Add detailed Environment Canada forecast data.

* Add icon and translations.

* Fix missing commas

* Add const.

* Add test.
pull/138235/head
Glenn Waters 2025-02-19 16:07:53 -05:00 committed by GitHub
parent 0a0a96fb3b
commit 406f894dc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 626 additions and 3 deletions

View File

@ -5,3 +5,4 @@ ATTR_STATION = "station"
CONF_STATION = "station"
CONF_TITLE = "title"
DOMAIN = "environment_canada"
SERVICE_ENVIRONMENT_CANADA_FORECASTS = "get_forecasts"

View File

@ -21,6 +21,9 @@
"services": {
"set_radar_type": {
"service": "mdi:radar"
},
"get_forecasts": {
"service": "mdi:weather-cloudy-clock"
}
}
}

View File

@ -1,3 +1,9 @@
get_forecasts:
target:
entity:
integration: environment_canada
domain: weather
set_radar_type:
target:
entity:

View File

@ -113,6 +113,10 @@
}
},
"services": {
"get_forecasts": {
"name": "Get forecasts",
"description": "Retrieves the forecast from selected weather services."
},
"set_radar_type": {
"name": "Set radar type",
"description": "Sets the type of radar image to retrieve.",

View File

@ -35,11 +35,16 @@ from homeassistant.const import (
UnitOfSpeed,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.core import (
HomeAssistant,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.helpers import entity_platform, entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .const import DOMAIN, SERVICE_ENVIRONMENT_CANADA_FORECASTS
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator
# Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/
@ -78,6 +83,14 @@ async def async_setup_entry(
async_add_entities([ECWeatherEntity(config_entry.runtime_data.weather_coordinator)])
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_ENVIRONMENT_CANADA_FORECASTS,
None,
"_async_environment_canada_forecasts",
supports_response=SupportsResponse.ONLY,
)
def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> str:
"""Calculate unique ID."""
@ -185,6 +198,23 @@ class ECWeatherEntity(
"""Return the hourly forecast in native units."""
return get_forecast(self.ec_data, True)
def _async_environment_canada_forecasts(self) -> ServiceResponse:
"""Return the native Environment Canada forecast."""
daily = []
for f in self.ec_data.daily_forecasts:
day = f.copy()
day["timestamp"] = day["timestamp"].isoformat()
daily.append(day)
hourly = []
for f in self.ec_data.hourly_forecasts:
hour = f.copy()
hour["timestamp"] = hour["period"].isoformat()
del hour["period"]
hourly.append(hour)
return {"daily_forecast": daily, "hourly_forecast": hourly}
def get_forecast(ec_data, hourly) -> list[Forecast] | None:
"""Build the forecast array."""

View File

@ -37,6 +37,7 @@ async def init_integration(hass: HomeAssistant, ec_data) -> MockConfigEntry:
weather_mock.conditions = ec_data["conditions"]
weather_mock.alerts = ec_data["alerts"]
weather_mock.daily_forecasts = ec_data["daily_forecasts"]
weather_mock.hourly_forecasts = ec_data["hourly_forecasts"]
weather_mock.metadata = ec_data["metadata"]
radar_mock = mock_ec()

View File

@ -19,6 +19,9 @@ def ec_data():
if t := weather.get("timestamp"):
with contextlib.suppress(ValueError):
weather["timestamp"] = datetime.fromisoformat(t)
elif t := weather.get("period"):
with contextlib.suppress(ValueError):
weather["period"] = datetime.fromisoformat(t)
return weather
return json.loads(

View File

@ -238,6 +238,224 @@
"timestamp": "2022-10-09 15:00:00+00:00"
}
],
"hourly_forecasts": [
{
"period": "2025-02-19T19:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -11,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-19T20:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -10,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-19T21:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -10,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-19T22:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -11,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-19T23:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -11,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T00:00:00+00:00",
"condition": "Cloudy",
"temperature": -12,
"icon_code": "10",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T01:00:00+00:00",
"condition": "Cloudy",
"temperature": -13,
"icon_code": "10",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T02:00:00+00:00",
"condition": "Cloudy",
"temperature": -13,
"icon_code": "10",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T03:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -14,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T04:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -14,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T05:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -15,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T06:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -15,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T07:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -15,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T08:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -16,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T09:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -16,
"icon_code": "33",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T10:00:00+00:00",
"condition": "Partly cloudy",
"temperature": -16,
"icon_code": "32",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T11:00:00+00:00",
"condition": "Partly cloudy",
"temperature": -16,
"icon_code": "32",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "W"
},
{
"period": "2025-02-20T12:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -16,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 5,
"wind_direction": "VR"
},
{
"period": "2025-02-20T13:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -15,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 5,
"wind_direction": "VR"
},
{
"period": "2025-02-20T14:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -14,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 5,
"wind_direction": "VR"
},
{
"period": "2025-02-20T15:00:00+00:00",
"condition": "A mix of sun and cloud",
"temperature": -13,
"icon_code": "02",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "NW"
},
{
"period": "2025-02-20T16:00:00+00:00",
"condition": "Mainly cloudy",
"temperature": -11,
"icon_code": "03",
"precip_probability": 20,
"wind_speed": 10,
"wind_direction": "NW"
},
{
"period": "2025-02-20T17:00:00+00:00",
"condition": "Periods of light snow",
"temperature": -10,
"icon_code": "16",
"precip_probability": 70,
"wind_speed": 10,
"wind_direction": "NW"
},
{
"period": "2025-02-20T18:00:00+00:00",
"condition": "Periods of light snow",
"temperature": -8,
"icon_code": "16",
"precip_probability": 70,
"wind_speed": 20,
"wind_direction": "NW"
}
],
"metadata": {
"attribution": "Data provided by Environment Canada",
"timestamp": "2022/10/3",

View File

@ -92,3 +92,337 @@
}),
})
# ---
# name: test_get_environment_canada_raw_forecast_data
dict({
'weather.home_forecast': dict({
'daily_forecast': list([
dict({
'icon_code': '30',
'period': 'Monday night',
'precip_probability': 0,
'temperature': -1,
'temperature_class': 'low',
'text_summary': 'Clear. Fog patches developing after midnight. Low minus 1 with frost.',
'timestamp': '2022-10-03T15:00:00+00:00',
}),
dict({
'icon_code': '00',
'period': 'Tuesday',
'precip_probability': 0,
'temperature': 18,
'temperature_class': 'high',
'text_summary': 'Sunny. Fog patches dissipating in the morning. High 18. UV index 5 or moderate.',
'timestamp': '2022-10-04T15:00:00+00:00',
}),
dict({
'icon_code': '30',
'period': 'Tuesday night',
'precip_probability': 0,
'temperature': 3,
'temperature_class': 'low',
'text_summary': 'Clear. Fog patches developing overnight. Low plus 3.',
'timestamp': '2022-10-04T15:00:00+00:00',
}),
dict({
'icon_code': '00',
'period': 'Wednesday',
'precip_probability': 0,
'temperature': 20,
'temperature_class': 'high',
'text_summary': 'Sunny. High 20.',
'timestamp': '2022-10-05T15:00:00+00:00',
}),
dict({
'icon_code': '30',
'period': 'Wednesday night',
'precip_probability': 0,
'temperature': 9,
'temperature_class': 'low',
'text_summary': 'Clear. Low 9.',
'timestamp': '2022-10-05T15:00:00+00:00',
}),
dict({
'icon_code': '02',
'period': 'Thursday',
'precip_probability': 0,
'temperature': 20,
'temperature_class': 'high',
'text_summary': 'A mix of sun and cloud. High 20.',
'timestamp': '2022-10-06T15:00:00+00:00',
}),
dict({
'icon_code': '12',
'period': 'Thursday night',
'precip_probability': 0,
'temperature': 7,
'temperature_class': 'low',
'text_summary': 'Showers. Low 7.',
'timestamp': '2022-10-06T15:00:00+00:00',
}),
dict({
'icon_code': '12',
'period': 'Friday',
'precip_probability': 40,
'temperature': 13,
'temperature_class': 'high',
'text_summary': 'Cloudy with 40 percent chance of showers. High 13.',
'timestamp': '2022-10-07T15:00:00+00:00',
}),
dict({
'icon_code': '32',
'period': 'Friday night',
'precip_probability': 0,
'temperature': 1,
'temperature_class': 'low',
'text_summary': 'Cloudy periods. Low plus 1.',
'timestamp': '2022-10-07T15:00:00+00:00',
}),
dict({
'icon_code': '02',
'period': 'Saturday',
'precip_probability': 0,
'temperature': 10,
'temperature_class': 'high',
'text_summary': 'A mix of sun and cloud. High 10.',
'timestamp': '2022-10-08T15:00:00+00:00',
}),
dict({
'icon_code': '32',
'period': 'Saturday night',
'precip_probability': 0,
'temperature': 3,
'temperature_class': 'low',
'text_summary': 'Cloudy periods. Low plus 3.',
'timestamp': '2022-10-08T15:00:00+00:00',
}),
dict({
'icon_code': '02',
'period': 'Sunday',
'precip_probability': 0,
'temperature': 12,
'temperature_class': 'high',
'text_summary': 'A mix of sun and cloud. High 12.',
'timestamp': '2022-10-09T15:00:00+00:00',
}),
]),
'hourly_forecast': list([
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -11,
'timestamp': '2025-02-19T19:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -10,
'timestamp': '2025-02-19T20:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -10,
'timestamp': '2025-02-19T21:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -11,
'timestamp': '2025-02-19T22:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -11,
'timestamp': '2025-02-19T23:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Cloudy',
'icon_code': '10',
'precip_probability': 20,
'temperature': -12,
'timestamp': '2025-02-20T00:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Cloudy',
'icon_code': '10',
'precip_probability': 20,
'temperature': -13,
'timestamp': '2025-02-20T01:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Cloudy',
'icon_code': '10',
'precip_probability': 20,
'temperature': -13,
'timestamp': '2025-02-20T02:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -14,
'timestamp': '2025-02-20T03:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -14,
'timestamp': '2025-02-20T04:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -15,
'timestamp': '2025-02-20T05:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -15,
'timestamp': '2025-02-20T06:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -15,
'timestamp': '2025-02-20T07:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -16,
'timestamp': '2025-02-20T08:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '33',
'precip_probability': 20,
'temperature': -16,
'timestamp': '2025-02-20T09:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Partly cloudy',
'icon_code': '32',
'precip_probability': 20,
'temperature': -16,
'timestamp': '2025-02-20T10:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'Partly cloudy',
'icon_code': '32',
'precip_probability': 20,
'temperature': -16,
'timestamp': '2025-02-20T11:00:00+00:00',
'wind_direction': 'W',
'wind_speed': 10,
}),
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -16,
'timestamp': '2025-02-20T12:00:00+00:00',
'wind_direction': 'VR',
'wind_speed': 5,
}),
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -15,
'timestamp': '2025-02-20T13:00:00+00:00',
'wind_direction': 'VR',
'wind_speed': 5,
}),
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -14,
'timestamp': '2025-02-20T14:00:00+00:00',
'wind_direction': 'VR',
'wind_speed': 5,
}),
dict({
'condition': 'A mix of sun and cloud',
'icon_code': '02',
'precip_probability': 20,
'temperature': -13,
'timestamp': '2025-02-20T15:00:00+00:00',
'wind_direction': 'NW',
'wind_speed': 10,
}),
dict({
'condition': 'Mainly cloudy',
'icon_code': '03',
'precip_probability': 20,
'temperature': -11,
'timestamp': '2025-02-20T16:00:00+00:00',
'wind_direction': 'NW',
'wind_speed': 10,
}),
dict({
'condition': 'Periods of light snow',
'icon_code': '16',
'precip_probability': 70,
'temperature': -10,
'timestamp': '2025-02-20T17:00:00+00:00',
'wind_direction': 'NW',
'wind_speed': 10,
}),
dict({
'condition': 'Periods of light snow',
'icon_code': '16',
'precip_probability': 70,
'temperature': -8,
'timestamp': '2025-02-20T18:00:00+00:00',
'wind_direction': 'NW',
'wind_speed': 20,
}),
]),
}),
})
# ---

View File

@ -5,6 +5,10 @@ from typing import Any
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.environment_canada.const import (
DOMAIN,
SERVICE_ENVIRONMENT_CANADA_FORECASTS,
)
from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN,
SERVICE_GET_FORECASTS,
@ -56,3 +60,22 @@ async def test_forecast_daily_with_some_previous_days_data(
return_response=True,
)
assert response == snapshot
async def test_get_environment_canada_raw_forecast_data(
hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
) -> None:
"""Test forecast with half day at start."""
await init_integration(hass, ec_data)
response = await hass.services.async_call(
DOMAIN,
SERVICE_ENVIRONMENT_CANADA_FORECASTS,
{
"entity_id": "weather.home_forecast",
},
blocking=True,
return_response=True,
)
assert response == snapshot