mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Add Stookwijzer forecast service (#138392)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
1ef07544d5
commit
9068a09620
@ -8,13 +8,27 @@ from stookwijzer import Stookwijzer
|
||||
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import StookwijzerConfigEntry, StookwijzerCoordinator
|
||||
from .services import setup_services
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Stookwijzer component."""
|
||||
setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: StookwijzerConfigEntry) -> bool:
|
||||
"""Set up Stookwijzer from a config entry."""
|
||||
|
@ -5,3 +5,6 @@ from typing import Final
|
||||
|
||||
DOMAIN: Final = "stookwijzer"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
SERVICE_GET_FORECAST = "get_forecast"
|
||||
|
7
homeassistant/components/stookwijzer/icons.json
Normal file
7
homeassistant/components/stookwijzer/icons.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"services": {
|
||||
"get_forecast": {
|
||||
"service": "mdi:clock-plus-outline"
|
||||
}
|
||||
}
|
||||
}
|
76
homeassistant/components/stookwijzer/services.py
Normal file
76
homeassistant/components/stookwijzer/services.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Define services for the Stookwijzer integration."""
|
||||
|
||||
from typing import Required, TypedDict, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_GET_FORECAST
|
||||
from .coordinator import StookwijzerConfigEntry
|
||||
|
||||
SERVICE_GET_FORECAST_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Forecast(TypedDict):
|
||||
"""Typed Stookwijzer forecast dict."""
|
||||
|
||||
datetime: Required[str]
|
||||
advice: str | None
|
||||
final: bool | None
|
||||
|
||||
|
||||
def async_get_entry(
|
||||
hass: HomeAssistant, config_entry_id: str
|
||||
) -> StookwijzerConfigEntry:
|
||||
"""Get the Overseerr config entry."""
|
||||
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="integration_not_found",
|
||||
translation_placeholders={"target": DOMAIN},
|
||||
)
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": entry.title},
|
||||
)
|
||||
return cast(StookwijzerConfigEntry, entry)
|
||||
|
||||
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Stookwijzer integration."""
|
||||
|
||||
async def async_get_forecast(call: ServiceCall) -> ServiceResponse | None:
|
||||
"""Get the forecast from API endpoint."""
|
||||
entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
client = entry.runtime_data.client
|
||||
|
||||
return cast(
|
||||
ServiceResponse,
|
||||
{
|
||||
"forecast": cast(
|
||||
list[Forecast], await client.async_get_forecast() or []
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
async_get_forecast,
|
||||
schema=SERVICE_GET_FORECAST_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
7
homeassistant/components/stookwijzer/services.yaml
Normal file
7
homeassistant/components/stookwijzer/services.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
get_forecast:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: stookwijzer
|
@ -27,6 +27,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"get_forecast": {
|
||||
"name": "Get forecast",
|
||||
"description": "Retrieves the advice forecast from Stookwijzer.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "Stookwijzer instance",
|
||||
"description": "The Stookwijzer instance to get the forecast from."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"location_migration_failed": {
|
||||
"description": "The Stookwijzer integration was unable to automatically migrate your location to a new format the updated integration uses.\n\nMake sure you are connected to the Internet and restart Home Assistant to try again.\n\nIf this doesn't resolve the error, remove and re-add the integration.",
|
||||
@ -36,6 +48,12 @@
|
||||
"exceptions": {
|
||||
"no_data_received": {
|
||||
"message": "No data received from Stookwijzer."
|
||||
},
|
||||
"not_loaded": {
|
||||
"message": "{target} is not loaded."
|
||||
},
|
||||
"integration_not_found": {
|
||||
"message": "Integration \"{target}\" not found in registry."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,18 @@
|
||||
"""Fixtures for Stookwijzer integration tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import Required, TypedDict
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.stookwijzer.const import DOMAIN
|
||||
from homeassistant.components.stookwijzer.services import Forecast
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
class Forecast(TypedDict):
|
||||
"""Typed Stookwijzer forecast dict."""
|
||||
|
||||
datetime: Required[str]
|
||||
advice: str | None
|
||||
final: bool | None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
|
27
tests/components/stookwijzer/snapshots/test_services.ambr
Normal file
27
tests/components/stookwijzer/snapshots/test_services.ambr
Normal file
@ -0,0 +1,27 @@
|
||||
# serializer version: 1
|
||||
# name: test_service_get_forecast
|
||||
dict({
|
||||
'forecast': tuple(
|
||||
dict({
|
||||
'advice': 'code_yellow',
|
||||
'datetime': '2025-02-12T17:00:00+01:00',
|
||||
'final': True,
|
||||
}),
|
||||
dict({
|
||||
'advice': 'code_yellow',
|
||||
'datetime': '2025-02-12T23:00:00+01:00',
|
||||
'final': True,
|
||||
}),
|
||||
dict({
|
||||
'advice': 'code_orange',
|
||||
'datetime': '2025-02-13T05:00:00+01:00',
|
||||
'final': False,
|
||||
}),
|
||||
dict({
|
||||
'advice': 'code_orange',
|
||||
'datetime': '2025-02-13T11:00:00+01:00',
|
||||
'final': False,
|
||||
}),
|
||||
),
|
||||
})
|
||||
# ---
|
72
tests/components/stookwijzer/test_services.py
Normal file
72
tests/components/stookwijzer/test_services.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Tests for the Stookwijzer services."""
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.stookwijzer.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_service_get_forecast(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the Stookwijzer forecast service."""
|
||||
|
||||
assert snapshot == await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_service_entry_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test error handling when entry is not loaded."""
|
||||
mock_config_entry2 = MockConfigEntry(domain=DOMAIN)
|
||||
mock_config_entry2.add_to_hass(hass)
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="Mock Title is not loaded"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_service_integration_not_found(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test error handling when integration not in registry."""
|
||||
with pytest.raises(
|
||||
ServiceValidationError, match='Integration "stookwijzer" not found in registry'
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_FORECAST,
|
||||
{ATTR_CONFIG_ENTRY_ID: "bad-config_id"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user