mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add Fuelprices.dk (#163932)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
co-authored by
Joost Lekkerkerker
parent
684b3e56ed
commit
fab4809201
Generated
+2
@@ -607,6 +607,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/frontend/ @home-assistant/frontend
|
||||
/homeassistant/components/frontier_silicon/ @wlcrs
|
||||
/tests/components/frontier_silicon/ @wlcrs
|
||||
/homeassistant/components/fuelprices_dk/ @MTrab
|
||||
/tests/components/fuelprices_dk/ @MTrab
|
||||
/homeassistant/components/fujitsu_fglair/ @crevetor
|
||||
/tests/components/fujitsu_fglair/ @crevetor
|
||||
/homeassistant/components/fully_kiosk/ @cgarwood
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Initialize the Fuelprices.dk component."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_COMPANY, CONF_STATION, SUBENTRY_TYPE_STATION
|
||||
from .coordinator import FuelPricesDKCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
type FuelpricesDkConfigEntry = ConfigEntry[dict[str, FuelPricesDKCoordinator]]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: FuelpricesDkConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Fuelprices.dk from a config entry."""
|
||||
config_entry.async_on_unload(config_entry.add_update_listener(_update_listener))
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
runtime_data: dict[str, FuelPricesDKCoordinator] = {}
|
||||
|
||||
for subentry in config_entry.get_subentries_of_type(SUBENTRY_TYPE_STATION):
|
||||
subentry_id = subentry.subentry_id
|
||||
company = subentry.data[CONF_COMPANY]
|
||||
station = subentry.data[CONF_STATION]
|
||||
|
||||
coordinator = FuelPricesDKCoordinator(
|
||||
hass,
|
||||
api_key,
|
||||
company,
|
||||
station,
|
||||
subentry_id,
|
||||
config_entry,
|
||||
)
|
||||
runtime_data[subentry_id] = coordinator
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
config_entry.runtime_data = runtime_data
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def _update_listener(hass: HomeAssistant, entry: FuelpricesDkConfigEntry) -> None:
|
||||
"""Handle options or subentry updates by reloading the entry."""
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: FuelpricesDkConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
@@ -0,0 +1,369 @@
|
||||
"""Config flow for the Fuelprices.dk integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, override
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pybraendstofpriser import Braendstofpriser
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import CONF_COMPANY, CONF_STATION, DOMAIN, WEBSITE_URL
|
||||
|
||||
|
||||
def _get_api_error_key(exc: ClientResponseError) -> str:
|
||||
"""Map API errors to config flow errors."""
|
||||
if exc.status == 401:
|
||||
return "invalid_api_key"
|
||||
if exc.status == 429:
|
||||
return "rate_limit_exceeded"
|
||||
return "cannot_connect"
|
||||
|
||||
|
||||
class FuelpricesDkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Fuelprices.dk."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
@override
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this handler."""
|
||||
return {"station": FuelpricesDkStationSubentryFlow}
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.api: Braendstofpriser
|
||||
self.companies: list[dict[str, Any]] = []
|
||||
self.stations: Any = {}
|
||||
self.company_name = ""
|
||||
self.user_input: dict[str, Any] = {}
|
||||
|
||||
async def _async_validate_api_key(
|
||||
self, api_key: str
|
||||
) -> tuple[Braendstofpriser | None, list[dict[str, Any]], str | None]:
|
||||
"""Validate the API key and fetch available companies."""
|
||||
api = Braendstofpriser(api_key)
|
||||
try:
|
||||
companies = await api.list_companies()
|
||||
except ClientResponseError as exc:
|
||||
return None, [], _get_api_error_key(exc)
|
||||
|
||||
if not companies:
|
||||
return None, [], "cannot_connect"
|
||||
|
||||
return api, companies, None
|
||||
|
||||
async def _async_fetch_stations(self, company_name: str) -> tuple[Any, str | None]:
|
||||
"""Fetch stations for a company."""
|
||||
try:
|
||||
stations = await self.api.list_stations(company_name=company_name)
|
||||
except ClientResponseError as exc:
|
||||
return None, _get_api_error_key(exc)
|
||||
|
||||
if not stations:
|
||||
return None, "cannot_connect"
|
||||
|
||||
return stations, None
|
||||
|
||||
def _show_company_selection_form(self, errors: dict[str, str]) -> ConfigFlowResult:
|
||||
"""Show the company selection form."""
|
||||
return self.async_show_form(
|
||||
step_id="company_selection",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_COMPANY, default=self.company_name): vol.In(
|
||||
[c["company"] for c in self.companies]
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step - Enter API key."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
api, companies, error = await self._async_validate_api_key(
|
||||
user_input[CONF_API_KEY]
|
||||
)
|
||||
if error is None:
|
||||
assert api is not None
|
||||
self.api = api
|
||||
self.companies = companies
|
||||
self.user_input = dict(user_input)
|
||||
return await self.async_step_company_selection()
|
||||
|
||||
errors["base"] = error
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={"website_url": WEBSITE_URL},
|
||||
)
|
||||
|
||||
async def async_step_company_selection(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the company selection step."""
|
||||
if user_input is not None:
|
||||
self.company_name = user_input[CONF_COMPANY]
|
||||
self.user_input.update(user_input)
|
||||
self.stations = {}
|
||||
return await self.async_step_station_selection()
|
||||
|
||||
return self._show_company_selection_form({})
|
||||
|
||||
async def async_step_station_selection(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the station selection step."""
|
||||
if not self.stations:
|
||||
stations, error = await self._async_fetch_stations(self.company_name)
|
||||
if error is not None:
|
||||
return self._show_company_selection_form({"base": error})
|
||||
self.stations = stations
|
||||
|
||||
if user_input is not None:
|
||||
user_input[CONF_STATION] = self.stations.find(
|
||||
"name", user_input[CONF_STATION]
|
||||
)
|
||||
|
||||
# Create the main config entry with the first station subentry
|
||||
self.user_input.update(user_input)
|
||||
unique_id = (
|
||||
f"{self.user_input[CONF_COMPANY]}_{self.user_input[CONF_STATION]['id']}"
|
||||
)
|
||||
title = (
|
||||
f"{self.user_input[CONF_COMPANY]} - "
|
||||
f"{self.user_input[CONF_STATION]['name']}"
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title="Fuelprices.dk",
|
||||
data={CONF_API_KEY: self.user_input[CONF_API_KEY]},
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "station",
|
||||
"data": {
|
||||
CONF_COMPANY: self.user_input[CONF_COMPANY],
|
||||
CONF_STATION: self.user_input[CONF_STATION],
|
||||
},
|
||||
"title": title,
|
||||
"unique_id": unique_id,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
stations = [s["name"] for s in self.stations]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="station_selection",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATION): vol.In(stations),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a reauth flow when API key is invalid/expired."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm a new API key."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
api = Braendstofpriser(user_input[CONF_API_KEY])
|
||||
try:
|
||||
await api.list_companies()
|
||||
except ClientResponseError as exc:
|
||||
errors["base"] = _get_api_error_key(exc)
|
||||
|
||||
if not errors:
|
||||
entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
if entry is not None:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={CONF_API_KEY: user_input[CONF_API_KEY]},
|
||||
)
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class FuelpricesDkStationSubentryFlow(ConfigSubentryFlow):
|
||||
"""Handle station subentries for Fuelprices.dk."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the subentry flow."""
|
||||
self.api: Braendstofpriser
|
||||
self.companies: list[dict[str, Any]] = []
|
||||
self.stations: Any = {}
|
||||
self.company_name = ""
|
||||
self._errors: dict[str, str] = {}
|
||||
self.user_input: dict[str, Any] = {}
|
||||
|
||||
async def _async_fetch_stations(self, company_name: str) -> tuple[Any, str | None]:
|
||||
"""Fetch stations for a company."""
|
||||
try:
|
||||
stations = await self.api.list_stations(company_name=company_name)
|
||||
except ClientResponseError as exc:
|
||||
return None, _get_api_error_key(exc)
|
||||
|
||||
if not stations:
|
||||
return None, "cannot_connect"
|
||||
|
||||
return stations, None
|
||||
|
||||
def _show_company_selection_form(
|
||||
self, errors: dict[str, str] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Show the company selection form."""
|
||||
default_company = self.user_input.get(CONF_COMPANY)
|
||||
company_field = (
|
||||
vol.Required(CONF_COMPANY, default=default_company)
|
||||
if default_company
|
||||
else vol.Required(CONF_COMPANY)
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="company_selection",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
company_field: vol.In([c["company"] for c in self.companies]),
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the initial step for adding a station subentry."""
|
||||
await self._async_init_api()
|
||||
return await self.async_step_company_selection(user_input)
|
||||
|
||||
async def _async_init_api(self) -> None:
|
||||
"""Initialize API client and fetch companies."""
|
||||
entry = self._get_entry()
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
self.api = Braendstofpriser(api_key)
|
||||
try:
|
||||
self.companies = await self.api.list_companies()
|
||||
except ClientResponseError as exc:
|
||||
self._errors["base"] = _get_api_error_key(exc)
|
||||
return
|
||||
|
||||
if not self.companies:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
|
||||
async def async_step_company_selection(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the company selection step."""
|
||||
if self._errors:
|
||||
return self.async_abort(reason=self._errors["base"])
|
||||
|
||||
if user_input is not None:
|
||||
self.company_name = user_input[CONF_COMPANY]
|
||||
self.user_input.update(user_input)
|
||||
self.stations = {}
|
||||
return await self.async_step_station_selection()
|
||||
|
||||
return self._show_company_selection_form()
|
||||
|
||||
async def async_step_station_selection(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the station selection step."""
|
||||
if not self.stations:
|
||||
stations, error = await self._async_fetch_stations(self.company_name)
|
||||
if error is not None:
|
||||
self.user_input[CONF_COMPANY] = self.company_name
|
||||
return self._show_company_selection_form({"base": error})
|
||||
self.stations = stations
|
||||
|
||||
if user_input is not None:
|
||||
user_input[CONF_STATION] = self.stations.find(
|
||||
"name", user_input[CONF_STATION]
|
||||
)
|
||||
|
||||
# Set UniqueID and abort if already existing
|
||||
unique_id = (
|
||||
f"{self.user_input[CONF_COMPANY]}_{user_input[CONF_STATION]['id']}"
|
||||
)
|
||||
entry = self._get_entry()
|
||||
for subentry in entry.subentries.values():
|
||||
if subentry.unique_id == unique_id:
|
||||
return self.async_abort(reason="station_already_configured")
|
||||
|
||||
# Process the user input and show next selection form
|
||||
self.user_input.update(user_input)
|
||||
return await self._async_create_or_update_subentry()
|
||||
|
||||
stations = [s["name"] for s in self.stations]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="station_selection",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_STATION): vol.In(stations),
|
||||
}
|
||||
),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def _async_create_or_update_subentry(self) -> SubentryFlowResult:
|
||||
"""Create the station subentry."""
|
||||
subentry_data = {
|
||||
CONF_COMPANY: self.user_input[CONF_COMPANY],
|
||||
CONF_STATION: self.user_input[CONF_STATION],
|
||||
}
|
||||
unique_id = (
|
||||
f"{self.user_input[CONF_COMPANY]}_{self.user_input[CONF_STATION]['id']}"
|
||||
)
|
||||
title = (
|
||||
f"{self.user_input[CONF_COMPANY]} - {self.user_input[CONF_STATION]['name']}"
|
||||
)
|
||||
|
||||
entry = self._get_entry()
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data=subentry_data,
|
||||
unique_id=unique_id,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Constants for the Fuelprices.dk integration."""
|
||||
|
||||
DOMAIN = "fuelprices_dk"
|
||||
|
||||
CONF_COMPANY = "company"
|
||||
CONF_STATION = "station"
|
||||
|
||||
SUBENTRY_TYPE_STATION = "station"
|
||||
|
||||
WEBSITE_URL = "https://fuelprices.dk"
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Coordinator for the Fuelprices.dk integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pybraendstofpriser import Braendstofpriser
|
||||
from pybraendstofpriser.exceptions import ProductNotFoundError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import FuelpricesDkConfigEntry
|
||||
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FuelPricesDKCoordinator(DataUpdateCoordinator[dict[str, float | None]]):
|
||||
"""Data update coordinator for the Fuelprices.dk integration."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api_key: str,
|
||||
company: str,
|
||||
station: dict[str, Any],
|
||||
subentry_id: str,
|
||||
config_entry: FuelpricesDkConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
name=company,
|
||||
logger=_LOGGER,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
|
||||
self._api = Braendstofpriser(api_key)
|
||||
self.company = company
|
||||
self.station_id: int = station["id"]
|
||||
self.station_name: str = station["name"]
|
||||
self.subentry_id = subentry_id
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> dict[str, float | None]:
|
||||
"""Handle data update request from the coordinator."""
|
||||
try:
|
||||
data = await self._api.get_prices(self.station_id)
|
||||
except ProductNotFoundError as exc:
|
||||
raise ConfigEntryError(exc) from exc
|
||||
except ClientResponseError as exc:
|
||||
if exc.status == 401:
|
||||
raise ConfigEntryAuthFailed(exc) from exc
|
||||
raise ConfigEntryError(exc) from exc
|
||||
|
||||
self.station_name = data["station"]["name"]
|
||||
|
||||
return dict(data["prices"])
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "fuelprices_dk",
|
||||
"name": "Fuelprices.dk",
|
||||
"codeowners": ["@MTrab"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/fuelprices_dk",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pybraendstofpriser==2.2.0"]
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional actions.
|
||||
docs-conditions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional conditions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
docs-triggers:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional triggers.
|
||||
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: |
|
||||
The integration does not provide any additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration cannot be discovered, it connects to a cloud service.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration cannot be discovered, it connects to a cloud service.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional icons.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Sensor platform for the Fuelprices.dk integration."""
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
RestoreSensor,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify as util_slugify
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import FuelPricesDKCoordinator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import FuelpricesDkConfigEntry
|
||||
|
||||
SENSORS = [
|
||||
SensorEntityDescription(
|
||||
key="price",
|
||||
name="Fuel Price",
|
||||
native_unit_of_measurement="DKK/L",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:gas-station",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: FuelpricesDkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform for Fuelprices.dk."""
|
||||
|
||||
for coordinator in entry.runtime_data.values():
|
||||
async_add_entities(
|
||||
(
|
||||
FuelpricesDkSensor(
|
||||
coordinator,
|
||||
coordinator.station_name,
|
||||
product_key,
|
||||
sensor,
|
||||
)
|
||||
for sensor in SENSORS
|
||||
for product_key in coordinator.data
|
||||
),
|
||||
config_subentry_id=coordinator.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class FuelpricesDkSensor(CoordinatorEntity[FuelPricesDKCoordinator], RestoreSensor):
|
||||
"""Sensor for Fuelprices.dk."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FuelPricesDKCoordinator,
|
||||
station_name: str,
|
||||
product_key: str,
|
||||
description: SensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
|
||||
self._product_key = product_key
|
||||
self._station_name = station_name
|
||||
|
||||
self._attr_name = product_key
|
||||
|
||||
self._attr_unique_id = util_slugify(
|
||||
f"{self.coordinator.station_id}_{self.entity_description.key}_{product_key}"
|
||||
)
|
||||
self._attr_config_subentry_id = self.coordinator.subentry_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(self.coordinator.station_id))},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
name=self._station_name,
|
||||
manufacturer=self.coordinator.company,
|
||||
model=self.coordinator.station_name,
|
||||
)
|
||||
|
||||
@property
|
||||
@override
|
||||
def available(self) -> bool:
|
||||
"""Return whether the entity is available."""
|
||||
return super().available and self._product_key in self.coordinator.data
|
||||
|
||||
@property
|
||||
@override
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value of the sensor."""
|
||||
price = self.coordinator.data[self._product_key]
|
||||
if isinstance(price, int | float):
|
||||
return float(price)
|
||||
return None
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This API key is already configured.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "Invalid API key provided",
|
||||
"rate_limit_exceeded": "Too many requests to the API. Please try again later.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"station_already_configured": "The selected station for this company is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:component::fuelprices_dk::config::abort::invalid_api_key%]",
|
||||
"rate_limit_exceeded": "[%key:component::fuelprices_dk::config::abort::rate_limit_exceeded%]"
|
||||
},
|
||||
"step": {
|
||||
"company_selection": {
|
||||
"data": {
|
||||
"company": "Select company"
|
||||
},
|
||||
"data_description": {
|
||||
"company": "The company you want to fetch prices from"
|
||||
},
|
||||
"description": "Select company"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "Your personal API key"
|
||||
},
|
||||
"description": "Your API key needs to be updated."
|
||||
},
|
||||
"station_selection": {
|
||||
"data": {
|
||||
"station": "Select station"
|
||||
},
|
||||
"data_description": {
|
||||
"station": "The station you want to create sensors for"
|
||||
},
|
||||
"description": "Select station to show prices for"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "Your personal API key"
|
||||
},
|
||||
"description": "Enter your Fuelprices.dk API key\nIf you do not have an API key, you can get one for free at {website_url}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_subentries": {
|
||||
"station": {
|
||||
"abort": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:component::fuelprices_dk::config::abort::invalid_api_key%]",
|
||||
"rate_limit_exceeded": "[%key:component::fuelprices_dk::config::abort::rate_limit_exceeded%]",
|
||||
"station_already_configured": "[%key:component::fuelprices_dk::config::abort::station_already_configured%]"
|
||||
},
|
||||
"entry_type": "Station",
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:component::fuelprices_dk::config::abort::invalid_api_key%]",
|
||||
"rate_limit_exceeded": "[%key:component::fuelprices_dk::config::abort::rate_limit_exceeded%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "Add station"
|
||||
},
|
||||
"step": {
|
||||
"company_selection": {
|
||||
"data": {
|
||||
"company": "[%key:component::fuelprices_dk::config::step::company_selection::data::company%]"
|
||||
},
|
||||
"data_description": {
|
||||
"company": "[%key:component::fuelprices_dk::config::step::company_selection::data_description::company%]"
|
||||
},
|
||||
"description": "[%key:component::fuelprices_dk::config::step::company_selection::description%]"
|
||||
},
|
||||
"station_selection": {
|
||||
"data": {
|
||||
"station": "[%key:component::fuelprices_dk::config::step::station_selection::data::station%]"
|
||||
},
|
||||
"data_description": {
|
||||
"station": "[%key:component::fuelprices_dk::config::step::station_selection::data_description::station%]"
|
||||
},
|
||||
"description": "[%key:component::fuelprices_dk::config::step::station_selection::description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -257,6 +257,7 @@ FLOWS = {
|
||||
"fritzbox_callmonitor",
|
||||
"fronius",
|
||||
"frontier_silicon",
|
||||
"fuelprices_dk",
|
||||
"fujitsu_fglair",
|
||||
"fully_kiosk",
|
||||
"fumis",
|
||||
|
||||
@@ -2319,6 +2319,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"fuelprices_dk": {
|
||||
"name": "Fuelprices.dk",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"fujitsu": {
|
||||
"name": "Fujitsu",
|
||||
"integrations": {
|
||||
|
||||
Generated
+3
@@ -2063,6 +2063,9 @@ pyblu==2.0.8
|
||||
# homeassistant.components.neato
|
||||
pybotvac==0.0.29
|
||||
|
||||
# homeassistant.components.fuelprices_dk
|
||||
pybraendstofpriser==2.2.0
|
||||
|
||||
# homeassistant.components.braviatv
|
||||
pybravia==0.4.1
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""Tests for the Danish Fuelprices integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Set up the integration from a mock config entry."""
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Common fixtures for Fuelprices.dk tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pybraendstofpriser import Flist
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.fuelprices_dk.const import (
|
||||
CONF_COMPANY,
|
||||
CONF_STATION,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigSubentryData
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_API_KEY = "test-api-key"
|
||||
TEST_COMPANY = "Circle K"
|
||||
TEST_STATION = {"id": 1234, "name": "Aarhus C"}
|
||||
TEST_PRICES = {"Blyfri95": 14.29, "Diesel": 12.99, "Blyfri98": 14.99}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry for config flow tests."""
|
||||
with patch(
|
||||
"homeassistant.components.fuelprices_dk.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Create a standard mock config entry with one station subentry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Fuelprices.dk",
|
||||
version=1,
|
||||
data={CONF_API_KEY: TEST_API_KEY},
|
||||
subentries_data=[
|
||||
ConfigSubentryData(
|
||||
subentry_type="station",
|
||||
title=f"{TEST_COMPANY} - {TEST_STATION['name']}",
|
||||
unique_id=f"{TEST_COMPANY}_{TEST_STATION['id']}",
|
||||
data={
|
||||
CONF_COMPANY: TEST_COMPANY,
|
||||
CONF_STATION: TEST_STATION,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_braendstofpriser() -> Generator[AsyncMock]:
|
||||
"""Mock the pybraendstofpriser client used by the integration."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.fuelprices_dk.config_flow.Braendstofpriser",
|
||||
autospec=True,
|
||||
) as mock_config_flow_client,
|
||||
patch(
|
||||
"homeassistant.components.fuelprices_dk.coordinator.Braendstofpriser",
|
||||
new=mock_config_flow_client,
|
||||
),
|
||||
):
|
||||
client = mock_config_flow_client.return_value
|
||||
client.list_companies.return_value = [{"company": TEST_COMPANY}]
|
||||
client.list_stations.return_value = Flist([TEST_STATION])
|
||||
client.get_prices.return_value = {
|
||||
"station": {
|
||||
"id": TEST_STATION["id"],
|
||||
"name": TEST_STATION["name"],
|
||||
"last_update": "2024-01-01T12:00:00",
|
||||
},
|
||||
"prices": TEST_PRICES,
|
||||
}
|
||||
yield client
|
||||
@@ -0,0 +1,166 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensors[sensor.aarhus_c_blyfri95-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.aarhus_c_blyfri95',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Blyfri95',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:gas-station',
|
||||
'original_name': 'Blyfri95',
|
||||
'platform': 'fuelprices_dk',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '1234_price_blyfri95',
|
||||
'unit_of_measurement': 'DKK/L',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.aarhus_c_blyfri95-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Aarhus C Blyfri95',
|
||||
<EntityStateAttribute.ICON: 'icon'>: 'mdi:gas-station',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'DKK/L',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.aarhus_c_blyfri95',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '14.29',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.aarhus_c_blyfri98-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.aarhus_c_blyfri98',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Blyfri98',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:gas-station',
|
||||
'original_name': 'Blyfri98',
|
||||
'platform': 'fuelprices_dk',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '1234_price_blyfri98',
|
||||
'unit_of_measurement': 'DKK/L',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.aarhus_c_blyfri98-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Aarhus C Blyfri98',
|
||||
<EntityStateAttribute.ICON: 'icon'>: 'mdi:gas-station',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'DKK/L',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.aarhus_c_blyfri98',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '14.99',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.aarhus_c_diesel-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.aarhus_c_diesel',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Diesel',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': 'mdi:gas-station',
|
||||
'original_name': 'Diesel',
|
||||
'platform': 'fuelprices_dk',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '1234_price_diesel',
|
||||
'unit_of_measurement': 'DKK/L',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.aarhus_c_diesel-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Aarhus C Diesel',
|
||||
<EntityStateAttribute.ICON: 'icon'>: 'mdi:gas-station',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'DKK/L',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.aarhus_c_diesel',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '12.99',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,447 @@
|
||||
"""Test the Fuelprices.dk config flow."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pybraendstofpriser import Flist
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.fuelprices_dk.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import TEST_API_KEY, TEST_COMPANY, TEST_STATION
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
def _client_error(status: int) -> ClientResponseError:
|
||||
"""Create an aiohttp client response error with a specific status code."""
|
||||
return ClientResponseError(
|
||||
request_info=Mock(),
|
||||
history=(),
|
||||
status=status,
|
||||
message="error",
|
||||
headers=None,
|
||||
)
|
||||
|
||||
|
||||
async def test_full_user_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a full successful config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: TEST_API_KEY}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "company_selection"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "station_selection"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"station": TEST_STATION["name"]}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Fuelprices.dk"
|
||||
assert result["data"] == {CONF_API_KEY: TEST_API_KEY}
|
||||
assert len(result["subentries"]) == 1
|
||||
subentry = result["subentries"][0]
|
||||
assert subentry["subentry_type"] == "station"
|
||||
assert subentry["title"] == f"{TEST_COMPANY} - {TEST_STATION['name']}"
|
||||
assert subentry["unique_id"] == f"{TEST_COMPANY}_{TEST_STATION['id']}"
|
||||
assert subentry["data"] == {"company": TEST_COMPANY, "station": TEST_STATION}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "error"),
|
||||
[
|
||||
(401, "invalid_api_key"),
|
||||
(429, "rate_limit_exceeded"),
|
||||
(500, "cannot_connect"),
|
||||
],
|
||||
)
|
||||
async def test_user_flow_recovers_from_api_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
status: int,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test the user flow shows an error and then recovers."""
|
||||
mock_braendstofpriser.list_companies.side_effect = _client_error(status)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: TEST_API_KEY}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_braendstofpriser.list_companies.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: TEST_API_KEY}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "company_selection"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"station": TEST_STATION["name"]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_user_flow_recovers_without_companies(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
) -> None:
|
||||
"""Test the user flow recovers when the API returns no companies."""
|
||||
mock_braendstofpriser.list_companies.return_value = []
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: TEST_API_KEY}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_braendstofpriser.list_companies.return_value = [{"company": TEST_COMPANY}]
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: TEST_API_KEY}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "company_selection"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"station": TEST_STATION["name"]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_user_flow_duplicate_api_key_aborts(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test flow aborts when the same API key is already configured."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: TEST_API_KEY}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"configure_stations",
|
||||
[
|
||||
lambda mock: setattr(mock.list_stations, "side_effect", _client_error(500)),
|
||||
lambda mock: setattr(mock.list_stations, "return_value", Flist([])),
|
||||
],
|
||||
ids=["error", "empty"],
|
||||
)
|
||||
async def test_user_flow_station_error_returns_to_company_selection(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
configure_stations: Callable[[AsyncMock], None],
|
||||
) -> None:
|
||||
"""Test station loading errors return the user to company selection."""
|
||||
configure_stations(mock_braendstofpriser)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: TEST_API_KEY}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "company_selection"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_braendstofpriser.list_stations.side_effect = None
|
||||
mock_braendstofpriser.list_stations.return_value = Flist([TEST_STATION])
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"station": TEST_STATION["name"]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_user_flow_allows_different_api_key(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
) -> None:
|
||||
"""Test flow allows a different API key."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "other-api-key"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "company_selection"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"station": TEST_STATION["name"]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_API_KEY: "other-api-key"}
|
||||
|
||||
|
||||
async def test_reauth_success(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
) -> None:
|
||||
"""Test reauthentication updates the API key."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "new-api-key"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "error"),
|
||||
[
|
||||
(401, "invalid_api_key"),
|
||||
(429, "rate_limit_exceeded"),
|
||||
(500, "cannot_connect"),
|
||||
],
|
||||
)
|
||||
async def test_reauth_recovers_from_api_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
status: int,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test reauth shows an error and then recovers."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_braendstofpriser.list_companies.side_effect = _client_error(status)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "bad-key"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_braendstofpriser.list_companies.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_API_KEY: "new-api-key"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
|
||||
async def test_subentry_flow_create(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test creating a station subentry."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
new_station = {"id": 4321, "name": "Aarhus N"}
|
||||
mock_braendstofpriser.list_stations.return_value = Flist(
|
||||
[TEST_STATION, new_station]
|
||||
)
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "station"),
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "company_selection"
|
||||
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "station_selection"
|
||||
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"], {"station": new_station["name"]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == f"{TEST_COMPANY} - {new_station['name']}"
|
||||
assert result["unique_id"] == f"{TEST_COMPANY}_{new_station['id']}"
|
||||
|
||||
|
||||
async def test_subentry_flow_duplicate_station(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test subentry flow aborts for an already configured station."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "station"),
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"], {"station": TEST_STATION["name"]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "station_already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status", "reason"),
|
||||
[
|
||||
(401, "invalid_api_key"),
|
||||
(429, "rate_limit_exceeded"),
|
||||
(500, "cannot_connect"),
|
||||
],
|
||||
)
|
||||
async def test_subentry_flow_api_init_error(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
status: int,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Test subentry flow aborts for API init errors."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
mock_braendstofpriser.list_companies.side_effect = _client_error(status)
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "station"),
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == reason
|
||||
|
||||
|
||||
async def test_subentry_flow_no_companies_aborts(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test subentry flow aborts when no companies are returned."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
mock_braendstofpriser.list_companies.return_value = []
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "station"),
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"configure_stations",
|
||||
[
|
||||
lambda mock: setattr(mock.list_stations, "side_effect", _client_error(500)),
|
||||
lambda mock: setattr(mock.list_stations, "return_value", Flist([])),
|
||||
],
|
||||
ids=["error", "empty"],
|
||||
)
|
||||
async def test_subentry_flow_station_error_returns_to_company_selection(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
configure_stations: Callable[[AsyncMock], None],
|
||||
) -> None:
|
||||
"""Test station loading errors return the user to company selection."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
configure_stations(mock_braendstofpriser)
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "station"),
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "company_selection"
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
new_station = {"id": 4321, "name": "Aarhus N"}
|
||||
mock_braendstofpriser.list_stations.side_effect = None
|
||||
mock_braendstofpriser.list_stations.return_value = Flist(
|
||||
[TEST_STATION, new_station]
|
||||
)
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"], {"company": TEST_COMPANY}
|
||||
)
|
||||
result = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"], {"station": new_station["name"]}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Test initialization for Fuelprices.dk."""
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from pybraendstofpriser import Flist
|
||||
from pybraendstofpriser.exceptions import ProductNotFoundError
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntryState,
|
||||
ConfigSubentry,
|
||||
ConfigSubentryData,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import TEST_API_KEY, TEST_COMPANY, TEST_PRICES, TEST_STATION
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def _client_error(status: int) -> ClientResponseError:
|
||||
"""Create an aiohttp client response error with a specific status code."""
|
||||
return ClientResponseError(
|
||||
request_info=Mock(),
|
||||
history=(),
|
||||
status=status,
|
||||
message="error",
|
||||
headers=None,
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_and_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the config entry is set up and unloaded correctly."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_reload_on_subentry_added(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the entry reloads and adds entities when a subentry is added."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(entities) == len(TEST_PRICES)
|
||||
|
||||
mock_braendstofpriser.get_prices.reset_mock()
|
||||
|
||||
new_station = {"id": 4321, "name": "Aarhus N"}
|
||||
hass.config_entries.async_add_subentry(
|
||||
mock_config_entry,
|
||||
ConfigSubentry(
|
||||
subentry_type="station",
|
||||
title=f"{TEST_COMPANY} - {new_station['name']}",
|
||||
unique_id=f"{TEST_COMPANY}_{new_station['id']}",
|
||||
data={"company": TEST_COMPANY, "station": new_station},
|
||||
),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entities = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(entities) == len(TEST_PRICES) * 2
|
||||
assert mock_braendstofpriser.get_prices.await_count == 2
|
||||
|
||||
|
||||
async def test_skips_non_station_subentries(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test setup skips unsupported subentry types."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain="fuelprices_dk",
|
||||
version=1,
|
||||
data={CONF_API_KEY: TEST_API_KEY},
|
||||
subentries_data=[
|
||||
ConfigSubentryData(
|
||||
subentry_type="other",
|
||||
title="Other",
|
||||
unique_id="other_1",
|
||||
data={"company": TEST_COMPANY, "station": TEST_STATION},
|
||||
)
|
||||
],
|
||||
)
|
||||
await setup_integration(hass, config_entry)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert not er.async_entries_for_config_entry(entity_registry, config_entry.entry_id)
|
||||
mock_braendstofpriser.get_prices.assert_not_called()
|
||||
|
||||
|
||||
async def test_stations_use_flist(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test setup completes when the API returns an Flist of stations."""
|
||||
mock_braendstofpriser.list_stations.return_value = Flist([TEST_STATION])
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "expected_state"),
|
||||
[
|
||||
(_client_error(401), ConfigEntryState.SETUP_ERROR),
|
||||
(_client_error(500), ConfigEntryState.SETUP_ERROR),
|
||||
(ProductNotFoundError("missing"), ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
ids=["auth_failed", "cannot_connect", "product_not_found"],
|
||||
)
|
||||
async def test_setup_error_handling(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
side_effect: Exception,
|
||||
expected_state: ConfigEntryState,
|
||||
) -> None:
|
||||
"""Test setup handles API errors during the first refresh."""
|
||||
mock_braendstofpriser.get_prices.side_effect = side_effect
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is expected_state
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Test sensor platform for Fuelprices.dk."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import TEST_PRICES
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the sensor entities."""
|
||||
with patch("homeassistant.components.fuelprices_dk.PLATFORMS", ["sensor"]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_sensor_updates(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the sensor updates when the coordinator refreshes."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.get("sensor.aarhus_c_blyfri95").state == "14.29"
|
||||
|
||||
mock_braendstofpriser.get_prices.return_value = {
|
||||
"station": {"id": 1234, "name": "Aarhus C", "last_update": None},
|
||||
"prices": {**TEST_PRICES, "Blyfri95": 15.99},
|
||||
}
|
||||
|
||||
freezer.tick(3600)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.aarhus_c_blyfri95").state == "15.99"
|
||||
|
||||
|
||||
async def test_sensor_becomes_unavailable_when_product_missing(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test a sensor becomes unavailable when its product is not returned."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.get("sensor.aarhus_c_blyfri95").state == "14.29"
|
||||
|
||||
remaining = {k: v for k, v in TEST_PRICES.items() if k != "Blyfri95"}
|
||||
mock_braendstofpriser.get_prices.return_value = {
|
||||
"station": {"id": 1234, "name": "Aarhus C", "last_update": None},
|
||||
"prices": remaining,
|
||||
}
|
||||
|
||||
freezer.tick(3600)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.aarhus_c_blyfri95").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_sensor_ignores_non_numeric_price(
|
||||
hass: HomeAssistant,
|
||||
mock_braendstofpriser: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test a sensor reports unknown when the API returns a non-numeric price."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
mock_braendstofpriser.get_prices.return_value = {
|
||||
"station": {"id": 1234, "name": "Aarhus C", "last_update": None},
|
||||
"prices": {**TEST_PRICES, "Blyfri95": "n/a"},
|
||||
}
|
||||
|
||||
freezer.tick(3600)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.aarhus_c_blyfri95").state == STATE_UNKNOWN
|
||||
Reference in New Issue
Block a user