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:
Renat Sibgatulin 2024-05-15 09:13:26 +02:00 committed by GitHub
parent f188668d8a
commit be5d6425dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 101 additions and 5 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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³"

View file

@ -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,
)

View file

@ -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": {

View file

@ -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