Environment Canada integration: add get_alerts action (#172393)

This commit is contained in:
Glenn Waters
2026-06-08 10:16:42 -04:00
committed by GitHub
parent d4accebb3b
commit 676a8c39eb
8 changed files with 216 additions and 17 deletions
@@ -8,9 +8,12 @@ from env_canada import ECAirQuality, ECMap, ECWeather
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import CONF_STATION from .const import CONF_STATION, DOMAIN
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator, ECRuntimeData from .coordinator import ECConfigEntry, ECDataUpdateCoordinator, ECRuntimeData
from .services import async_setup_services
DEFAULT_RADAR_UPDATE_INTERVAL = timedelta(minutes=5) DEFAULT_RADAR_UPDATE_INTERVAL = timedelta(minutes=5)
DEFAULT_WEATHER_UPDATE_INTERVAL = timedelta(minutes=5) DEFAULT_WEATHER_UPDATE_INTERVAL = timedelta(minutes=5)
@@ -19,6 +22,14 @@ PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Environment Canada services."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) -> bool:
"""Set up EC as config entry.""" """Set up EC as config entry."""
@@ -19,6 +19,9 @@
} }
}, },
"services": { "services": {
"get_alerts": {
"service": "mdi:bell-alert"
},
"get_forecasts": { "get_forecasts": {
"service": "mdi:weather-cloudy-clock" "service": "mdi:weather-cloudy-clock"
}, },
@@ -0,0 +1,56 @@
"""Define services for the Environment Canada integration."""
from typing import Any
from env_canada import ECWeather
import voluptuous as vol
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, service
from .const import DOMAIN
SERVICE_GET_ALERTS = "get_alerts"
SERVICE_GET_ALERTS_SCHEMA = vol.Schema({vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string})
SNAKE_MAPPING = {
"alertColourLevel": "alert_colour_level",
"expiryTime": "expiry_time",
}
async def _async_get_alerts(call: ServiceCall) -> dict[str, Any]:
"""Return the active alerts."""
entry = service.async_get_config_entry(
call.hass, DOMAIN, call.data[ATTR_CONFIG_ENTRY_ID]
)
ec: ECWeather | None = entry.runtime_data.weather_coordinator.ec_data
if ec is None:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="not_connected",
)
data: dict[str, Any] = ec.alerts
return {
k: [
{SNAKE_MAPPING.get(ik, ik): iv for ik, iv in item.items()}
for item in v["value"]
]
for k, v in data.items()
}
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Environment Canada integration."""
hass.services.async_register(
DOMAIN,
SERVICE_GET_ALERTS,
_async_get_alerts,
schema=SERVICE_GET_ALERTS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
@@ -1,3 +1,11 @@
get_alerts:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: environment_canada
get_forecasts: get_forecasts:
target: target:
entity: entity:
@@ -112,7 +112,22 @@
} }
} }
}, },
"exceptions": {
"not_connected": {
"message": "Environment Canada is not connected"
}
},
"services": { "services": {
"get_alerts": {
"description": "Retrieves the alerts from the selected weather service.",
"fields": {
"config_entry_id": {
"description": "The Environment Canada service to retrieve alerts from.",
"name": "Environment Canada service"
}
},
"name": "Get alerts"
},
"get_forecasts": { "get_forecasts": {
"description": "Retrieves the forecast from selected weather services.", "description": "Retrieves the forecast from selected weather services.",
"name": "Get forecasts" "name": "Get forecasts"
@@ -1,29 +1,49 @@
{ {
"alerts": { "alerts": {
"warnings": { "warnings": {
"value": [], "label": "Warnings",
"label": "Warnings"
},
"watches": {
"value": [],
"label": "Watches"
},
"advisories": {
"value": [ "value": [
{ {
"title": "Frost Advisory", "title": "Air Quality Warning",
"date": "Monday October 03, 2022 at 15:05 EDT" "date": "2026-06-04T10:35:16.386Z",
"alertColourLevel": "yellow",
"expiryTime": "2026-06-04T19:00:16.386Z",
"text": "Wildfire smoke is causing poor air quality. Conditions are expected to improve later this morning. Air quality and visibility due to wildfire smoke can fluctuate over short distances and can vary considerably from hour to hour. As smoke levels increase, health risks increase. Limit time outdoors. Consider reducing or rescheduling outdoor sports, activities and events.",
"area": "Yellowknife Region",
"status": "continued",
"confidence": "High",
"impact": "Moderate",
"alert_code": "AQW"
},
{
"title": "Wow, it is hot out there!",
"date": "2026-06-04T10:35:16.386Z",
"alertColourLevel": "red",
"expiryTime": "2026-06-04T19:00:16.386Z",
"text": "It is so hot you can fry an egg on the pavement!",
"area": "Yellowknife Region",
"status": "continued",
"confidence": "High",
"impact": "Moderate",
"alert_code": "HOT"
} }
], ]
"label": "Advisories" },
"watches": {
"label": "Watches",
"value": []
},
"advisories": {
"label": "Advisories",
"value": []
}, },
"statements": { "statements": {
"value": [], "label": "Statements",
"label": "Statements" "value": []
}, },
"endings": { "endings": {
"value": [], "label": "Endings",
"label": "Endings" "value": []
} }
}, },
"conditions": { "conditions": {
@@ -0,0 +1,39 @@
# serializer version: 1
# name: test_get_alerts
dict({
'advisories': list([
]),
'endings': list([
]),
'statements': list([
]),
'warnings': list([
dict({
'alert_code': 'AQW',
'alert_colour_level': 'yellow',
'area': 'Yellowknife Region',
'confidence': 'High',
'date': '2026-06-04T10:35:16.386Z',
'expiry_time': '2026-06-04T19:00:16.386Z',
'impact': 'Moderate',
'status': 'continued',
'text': 'Wildfire smoke is causing poor air quality. Conditions are expected to improve later this morning. Air quality and visibility due to wildfire smoke can fluctuate over short distances and can vary considerably from hour to hour. As smoke levels increase, health risks increase. Limit time outdoors. Consider reducing or rescheduling outdoor sports, activities and events.',
'title': 'Air Quality Warning',
}),
dict({
'alert_code': 'HOT',
'alert_colour_level': 'red',
'area': 'Yellowknife Region',
'confidence': 'High',
'date': '2026-06-04T10:35:16.386Z',
'expiry_time': '2026-06-04T19:00:16.386Z',
'impact': 'Moderate',
'status': 'continued',
'text': 'It is so hot you can fry an egg on the pavement!',
'title': 'Wow, it is hot out there!',
}),
]),
'watches': list([
]),
})
# ---
@@ -0,0 +1,47 @@
"""Tests for the Environment Canada services."""
from typing import Any
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.environment_canada.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from . import init_integration
SERVICE_GET_ALERTS = "get_alerts"
async def test_get_alerts(
hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
) -> None:
"""Test the get_alerts service returns active alerts."""
config_entry = await init_integration(hass, ec_data)
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_ALERTS,
{"config_entry_id": config_entry.entry_id},
blocking=True,
return_response=True,
)
assert response == snapshot
async def test_get_alerts_not_connected(
hass: HomeAssistant, ec_data: dict[str, Any]
) -> None:
"""Test get_alerts raises when weather data is not connected."""
config_entry = await init_integration(hass, ec_data)
config_entry.runtime_data.weather_coordinator.ec_data = None
with pytest.raises(HomeAssistantError, match="not connected"):
await hass.services.async_call(
DOMAIN,
SERVICE_GET_ALERTS,
{"config_entry_id": config_entry.entry_id},
blocking=True,
return_response=True,
)