Add options flow to the airq integration (#109337)
* Add support for options to airq integration Expose to the user the following configuration: 1. A choice between fetching from the device either: a. the averaged (previous and the new default behaviour) or b. noisy momentary sensor reading 2. A toggle to clip (spuriously) negative sensor values (default functionality, previously unexposed) To those ends: - Introduce an `OptionsFlowHandler` alongside with a listener `AirQCoordinator.async_set_options` - Introduce constants to handle represent options - Add tests and strings * Drop OptionsFlowHandler in favour of SchemaOptionsFlowHandler Modify `AirQCoordinator.__init__` to accommodate the change in option handling, and drop `async_set_options` which slipped through the previous commit. * Ruff formatting
This commit is contained in:
parent
f188668d8a
commit
be5d6425dc
6 changed files with 101 additions and 5 deletions
|
@ -6,6 +6,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE
|
||||
from .coordinator import AirQCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
@ -16,7 +17,12 @@ AirQConfigEntry = ConfigEntry[AirQCoordinator]
|
|||
async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool:
|
||||
"""Set up air-Q from a config entry."""
|
||||
|
||||
coordinator = AirQCoordinator(hass, entry)
|
||||
coordinator = AirQCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
clip_negative=entry.options.get(CONF_CLIP_NEGATIVE, True),
|
||||
return_average=entry.options.get(CONF_RETURN_AVERAGE, True),
|
||||
)
|
||||
|
||||
# Query the device for the first time and initialise coordinator.data
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
@ -24,6 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool
|
|||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
@ -31,3 +38,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool
|
|||
async def async_unload_entry(hass: HomeAssistant, entry: AirQConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
|
|
@ -9,11 +9,17 @@ from aioairq import AirQ, InvalidAuth
|
|||
from aiohttp.client_exceptions import ClientConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.selector import BooleanSelector
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -23,6 +29,16 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
OPTIONS_FLOW = {
|
||||
"init": SchemaFlowFormStep(
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_RETURN_AVERAGE, default=True): BooleanSelector(),
|
||||
vol.Optional(CONF_CLIP_NEGATIVE, default=True): BooleanSelector(),
|
||||
}
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
@ -72,3 +88,11 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> SchemaOptionsFlowHandler:
|
||||
"""Return the options flow."""
|
||||
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
from typing import Final
|
||||
|
||||
CONF_RETURN_AVERAGE: Final = "return_average"
|
||||
CONF_CLIP_NEGATIVE: Final = "clip_negatives"
|
||||
DOMAIN: Final = "airq"
|
||||
MANUFACTURER: Final = "CorantGmbH"
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
|
||||
|
|
|
@ -26,6 +26,8 @@ class AirQCoordinator(DataUpdateCoordinator):
|
|||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
clip_negative: bool = True,
|
||||
return_average: bool = True,
|
||||
) -> None:
|
||||
"""Initialise a custom coordinator."""
|
||||
super().__init__(
|
||||
|
@ -44,6 +46,8 @@ class AirQCoordinator(DataUpdateCoordinator):
|
|||
manufacturer=MANUFACTURER,
|
||||
identifiers={(DOMAIN, self.device_id)},
|
||||
)
|
||||
self.clip_negative = clip_negative
|
||||
self.return_average = return_average
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch the data from the device."""
|
||||
|
@ -57,4 +61,7 @@ class AirQCoordinator(DataUpdateCoordinator):
|
|||
hw_version=info["hw_version"],
|
||||
)
|
||||
)
|
||||
return await self.airq.get_latest_data() # type: ignore[no-any-return]
|
||||
return await self.airq.get_latest_data( # type: ignore[no-any-return]
|
||||
return_average=self.return_average,
|
||||
clip_negative_values=self.clip_negative,
|
||||
)
|
||||
|
|
|
@ -19,6 +19,21 @@
|
|||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure air-Q integration",
|
||||
"data": {
|
||||
"return_average": "Show values averaged by the device",
|
||||
"clip_negatives": "Clip negative values"
|
||||
},
|
||||
"data_description": {
|
||||
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)",
|
||||
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"acetaldehyde": {
|
||||
|
|
|
@ -7,7 +7,11 @@ from aiohttp.client_exceptions import ClientConnectionError
|
|||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.airq.const import DOMAIN
|
||||
from homeassistant.components.airq.const import (
|
||||
CONF_CLIP_NEGATIVE,
|
||||
CONF_RETURN_AVERAGE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
@ -27,6 +31,10 @@ TEST_DEVICE_INFO = DeviceInfo(
|
|||
sw_version="sw",
|
||||
hw_version="hw",
|
||||
)
|
||||
DEFAULT_OPTIONS = {
|
||||
CONF_CLIP_NEGATIVE: True,
|
||||
CONF_RETURN_AVERAGE: True,
|
||||
}
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
|
@ -103,3 +111,31 @@ async def test_duplicate_error(hass: HomeAssistant) -> None:
|
|||
)
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_input", [{}, {CONF_RETURN_AVERAGE: False}, {CONF_CLIP_NEGATIVE: False}]
|
||||
)
|
||||
async def test_options_flow(hass: HomeAssistant, user_input) -> None:
|
||||
"""Test that the options flow works."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=TEST_USER_DATA, unique_id=TEST_DEVICE_INFO["id"]
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
assert entry.options == {}
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input=user_input
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == entry.options == DEFAULT_OPTIONS | user_input
|
||||
|
|
Loading…
Add table
Reference in a new issue