mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +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.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, Platform
|
||||||
from homeassistant.core import HomeAssistant, callback
|
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 .const import DOMAIN, LOGGER
|
||||||
from .coordinator import StookwijzerConfigEntry, StookwijzerCoordinator
|
from .coordinator import StookwijzerConfigEntry, StookwijzerCoordinator
|
||||||
|
from .services import setup_services
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
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:
|
async def async_setup_entry(hass: HomeAssistant, entry: StookwijzerConfigEntry) -> bool:
|
||||||
"""Set up Stookwijzer from a config entry."""
|
"""Set up Stookwijzer from a config entry."""
|
||||||
|
@ -5,3 +5,6 @@ from typing import Final
|
|||||||
|
|
||||||
DOMAIN: Final = "stookwijzer"
|
DOMAIN: Final = "stookwijzer"
|
||||||
LOGGER = logging.getLogger(__package__)
|
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": {
|
"issues": {
|
||||||
"location_migration_failed": {
|
"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.",
|
"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": {
|
"exceptions": {
|
||||||
"no_data_received": {
|
"no_data_received": {
|
||||||
"message": "No data received from Stookwijzer."
|
"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."""
|
"""Fixtures for Stookwijzer integration tests."""
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from typing import Required, TypedDict
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.stookwijzer.const import DOMAIN
|
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.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
class Forecast(TypedDict):
|
|
||||||
"""Typed Stookwijzer forecast dict."""
|
|
||||||
|
|
||||||
datetime: Required[str]
|
|
||||||
advice: str | None
|
|
||||||
final: bool | None
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_config_entry() -> MockConfigEntry:
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
"""Return the default mocked config entry."""
|
"""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