From cfed1ff7994295aa30dfc3b0e4da3377e48dbbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20=C3=96berg?= <62830707+droberg@users.noreply.github.com> Date: Thu, 3 Mar 2022 20:41:59 +0100 Subject: [PATCH] Add config flow for selecting precision of DS18B20 devices (#64315) Co-authored-by: epenet --- homeassistant/components/onewire/__init__.py | 8 + .../components/onewire/config_flow.py | 177 ++++++++++++- homeassistant/components/onewire/const.py | 14 ++ homeassistant/components/onewire/sensor.py | 48 +++- homeassistant/components/onewire/strings.json | 27 ++ .../components/onewire/translations/en.json | 27 ++ tests/components/onewire/conftest.py | 7 +- tests/components/onewire/const.py | 48 ++++ tests/components/onewire/test_diagnostics.py | 7 +- tests/components/onewire/test_options_flow.py | 237 ++++++++++++++++++ 10 files changed, 590 insertions(+), 10 deletions(-) create mode 100644 tests/components/onewire/test_options_flow.py diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index 70a0a5fc856..ceef037bfcc 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -30,6 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(options_update_listener)) + return True @@ -41,3 +43,9 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok + + +async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + _LOGGER.info("Configuration options updated, reloading OneWire integration") + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 20b76ff236b..a7373206666 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -1,14 +1,17 @@ """Config flow for 1-Wire component.""" from __future__ import annotations +import logging from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.device_registry import DeviceRegistry from .const import ( CONF_MOUNT_DIR, @@ -17,8 +20,15 @@ from .const import ( DEFAULT_OWSERVER_HOST, DEFAULT_OWSERVER_PORT, DEFAULT_SYSBUS_MOUNT_DIR, + DEVICE_SUPPORT_OPTIONS, DOMAIN, + INPUT_ENTRY_CLEAR_OPTIONS, + INPUT_ENTRY_DEVICE_SELECTION, + OPTION_ENTRY_DEVICE_OPTIONS, + OPTION_ENTRY_SENSOR_PRECISION, + PRECISION_MAPPING_FAMILY_28, ) +from .model import OWServerDeviceDescription from .onewirehub import CannotConnect, InvalidPath, OneWireHub DATA_SCHEMA_USER = vol.Schema( @@ -37,6 +47,9 @@ DATA_SCHEMA_MOUNTDIR = vol.Schema( ) +_LOGGER = logging.getLogger(__name__) + + async def validate_input_owserver( hass: HomeAssistant, data: dict[str, Any] ) -> dict[str, str]: @@ -164,3 +177,163 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): data_schema=DATA_SCHEMA_MOUNTDIR, errors=errors, ) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: + """Get the options flow for this handler.""" + return OnewireOptionsFlowHandler(config_entry) + + +class OnewireOptionsFlowHandler(OptionsFlow): + """Handle OneWire Config options.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize OneWire Network options flow.""" + self.entry_id = config_entry.entry_id + self.options = dict(config_entry.options) + self.configurable_devices: dict[str, OWServerDeviceDescription] = {} + self.devices_to_configure: dict[str, OWServerDeviceDescription] = {} + self.current_device: str = "" + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + controller: OneWireHub = self.hass.data[DOMAIN][self.entry_id] + if controller.type == CONF_TYPE_SYSBUS: + return self.async_abort( + reason="SysBus setup does not have any config options." + ) + + all_devices: list[OWServerDeviceDescription] = controller.devices # type: ignore[assignment] + if not all_devices: + return self.async_abort(reason="No configurable devices found.") + + device_registry = dr.async_get(self.hass) + self.configurable_devices = { + self._get_device_long_name(device_registry, device.id): device + for device in all_devices + if device.family in DEVICE_SUPPORT_OPTIONS + } + + return await self.async_step_device_selection(user_input=None) + + async def async_step_device_selection( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select what devices to configure.""" + errors = {} + if user_input is not None: + if user_input.get(INPUT_ENTRY_CLEAR_OPTIONS): + # Reset all options + self.options = {} + return await self._update_options() + + selected_devices: list[str] = ( + user_input.get(INPUT_ENTRY_DEVICE_SELECTION) or [] + ) + if selected_devices: + self.devices_to_configure = { + device_name: self.configurable_devices[device_name] + for device_name in selected_devices + } + + return await self.async_step_configure_device(user_input=None) + errors["base"] = "device_not_selected" + + return self.async_show_form( + step_id="device_selection", + data_schema=vol.Schema( + { + vol.Optional( + INPUT_ENTRY_CLEAR_OPTIONS, + default=False, + ): bool, + vol.Optional( + INPUT_ENTRY_DEVICE_SELECTION, + default=self._get_current_configured_sensors(), + description="Multiselect with list of devices to choose from", + ): cv.multi_select( + {device: False for device in self.configurable_devices.keys()} + ), + } + ), + errors=errors, + ) + + async def async_step_configure_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Config precision option for device.""" + if user_input is not None: + self._update_device_options(user_input) + if self.devices_to_configure: + return await self.async_step_configure_device(user_input=None) + return await self._update_options() + + self.current_device, description = self.devices_to_configure.popitem() + data_schema: vol.Schema + if description.family == "28": + data_schema = vol.Schema( + { + vol.Required( + OPTION_ENTRY_SENSOR_PRECISION, + default=self._get_current_setting( + description.id, OPTION_ENTRY_SENSOR_PRECISION, "temperature" + ), + ): vol.In(PRECISION_MAPPING_FAMILY_28), + } + ) + + return self.async_show_form( + step_id="configure_device", + data_schema=data_schema, + description_placeholders={"sensor_id": self.current_device}, + ) + + async def _update_options(self) -> FlowResult: + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) + + @staticmethod + def _get_device_long_name( + device_registry: DeviceRegistry, current_device: str + ) -> str: + device = device_registry.async_get_device({(DOMAIN, current_device)}) + if device and device.name_by_user: + return f"{device.name_by_user} ({current_device})" + return current_device + + def _get_current_configured_sensors(self) -> list[str]: + """Get current list of sensors that are configured.""" + configured_sensors = self.options.get(OPTION_ENTRY_DEVICE_OPTIONS) + if not configured_sensors: + return [] + return [ + device_name + for device_name, description in self.configurable_devices.items() + if description.id in configured_sensors + ] + + def _get_current_setting(self, device_id: str, setting: str, default: Any) -> Any: + """Get current value for setting.""" + if entry_device_options := self.options.get(OPTION_ENTRY_DEVICE_OPTIONS): + if device_options := entry_device_options.get(device_id): + return device_options.get(setting) + return default + + def _update_device_options(self, user_input: dict[str, Any]) -> None: + """Update the global config with the new options for the current device.""" + options: dict[str, dict[str, Any]] = self.options.setdefault( + OPTION_ENTRY_DEVICE_OPTIONS, {} + ) + + description = self.configurable_devices[self.current_device] + device_options: dict[str, Any] = options.setdefault(description.id, {}) + if description.family == "28": + device_options[OPTION_ENTRY_SENSOR_PRECISION] = user_input[ + OPTION_ENTRY_SENSOR_PRECISION + ] + + self.options.update({OPTION_ENTRY_DEVICE_OPTIONS: options}) diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 285b2c51be5..7fce90cc012 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -37,6 +37,20 @@ DEVICE_SUPPORT_OWSERVER = { } DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] +DEVICE_SUPPORT_OPTIONS = ["28"] + +PRECISION_MAPPING_FAMILY_28 = { + "temperature": "Default", + "temperature9": "9 Bits", + "temperature10": "10 Bits", + "temperature11": "11 Bits", + "temperature12": "12 Bits", +} + +OPTION_ENTRY_DEVICE_OPTIONS = "device_options" +OPTION_ENTRY_SENSOR_PRECISION = "precision" +INPUT_ENTRY_CLEAR_OPTIONS = "clear_device_options" +INPUT_ENTRY_DEVICE_SELECTION = "device_selection" MANUFACTURER_MAXIM = "Maxim Integrated" MANUFACTURER_HOBBYBOARDS = "Hobby Boards" diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index cac3630d473..759b1f9eccf 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable, Mapping import copy from dataclasses import dataclass import logging @@ -38,6 +39,9 @@ from .const import ( DEVICE_KEYS_0_3, DEVICE_KEYS_A_B, DOMAIN, + OPTION_ENTRY_DEVICE_OPTIONS, + OPTION_ENTRY_SENSOR_PRECISION, + PRECISION_MAPPING_FAMILY_28, READ_MODE_FLOAT, READ_MODE_INT, ) @@ -54,7 +58,24 @@ from .onewirehub import OneWireHub class OneWireSensorEntityDescription(OneWireEntityDescription, SensorEntityDescription): """Class describing OneWire sensor entities.""" - override_key: str | None = None + override_key: Callable[[str, Mapping[str, Any]], str] | None = None + + +def _get_sensor_precision_family_28(device_id: str, options: Mapping[str, Any]) -> str: + """Get precision form config flow options.""" + precision: str = ( + options.get(OPTION_ENTRY_DEVICE_OPTIONS, {}) + .get(device_id, {}) + .get(OPTION_ENTRY_SENSOR_PRECISION, "temperature") + ) + if precision in PRECISION_MAPPING_FAMILY_28: + return precision + _LOGGER.warning( + "Invalid sensor precision `%s` for device `%s`: reverting to default", + precision, + device_id, + ) + return "temperature" SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION = OneWireSensorEntityDescription( @@ -185,7 +206,17 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), - "28": (SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION,), + "28": ( + OneWireSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + override_key=_get_sensor_precision_family_28, + read_mode=READ_MODE_FLOAT, + state_class=SensorStateClass.MEASUREMENT, + ), + ), "30": ( SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION, OneWireSensorEntityDescription( @@ -195,7 +226,7 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { name="Thermocouple temperature", native_unit_of_measurement=TEMP_CELSIUS, read_mode=READ_MODE_FLOAT, - override_key="typeK/temperature", + override_key=lambda d, o: "typeK/temperature", state_class=SensorStateClass.MEASUREMENT, ), OneWireSensorEntityDescription( @@ -352,13 +383,15 @@ async def async_setup_entry( """Set up 1-Wire platform.""" onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( - get_entities, onewirehub, config_entry.data + get_entities, onewirehub, config_entry.data, config_entry.options ) async_add_entities(entities, True) def get_entities( - onewirehub: OneWireHub, config: MappingProxyType[str, Any] + onewirehub: OneWireHub, + config: MappingProxyType[str, Any], + options: MappingProxyType[str, Any], ) -> list[SensorEntity]: """Get a list of entities.""" if not onewirehub.devices: @@ -400,9 +433,12 @@ def get_entities( description.device_class = SensorDeviceClass.HUMIDITY description.native_unit_of_measurement = PERCENTAGE description.name = f"Wetness {s_id}" + override_key = None + if description.override_key: + override_key = description.override_key(device_id, options) device_file = os.path.join( os.path.split(device.path)[0], - description.override_key or description.key, + override_key or description.key, ) name = f"{device_id} {description.name}" entities.append( diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index 928907b319a..2a6ee1eed6f 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -22,5 +22,32 @@ "title": "Set up 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Select devices to configure" + }, + "step": { + "ack_no_options": { + "data": { }, + "description": "There are no options for the SysBus implementation", + "title": "OneWire SysBus Options" + }, + "device_selection": { + "data": { + "clear_device_options": "Clear all device configurations", + "device_selection": "Select devices to configure" + }, + "description": "Select what configuration steps to process", + "title": "OneWire Device Options" + }, + "configure_device": { + "data": { + "precision": "Sensor Precision" + }, + "description": "Select sensor precision for {sensor_id}", + "title": "OneWire Sensor Precision" + } + } } } diff --git a/homeassistant/components/onewire/translations/en.json b/homeassistant/components/onewire/translations/en.json index ff4d1cb53b9..4b48e216b63 100644 --- a/homeassistant/components/onewire/translations/en.json +++ b/homeassistant/components/onewire/translations/en.json @@ -22,5 +22,32 @@ "title": "Set up 1-Wire" } } + }, + "options": { + "error": { + "device_not_selected": "Select devices to configure" + }, + "step": { + "ack_no_options": { + "data": {}, + "description": "There are no options for the SysBus implementation", + "title": "OneWire SysBus Options" + }, + "configure_device": { + "data": { + "precision": "Sensor Precision" + }, + "description": "Select sensor precision for {sensor_id}", + "title": "OneWire Sensor Precision" + }, + "device_selection": { + "data": { + "clear_device_options": "Clear all device configurations", + "device_selection": "Select devices to configure" + }, + "description": "Select what configuration steps to process", + "title": "OneWire Device Options" + } + } } } \ No newline at end of file diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index 189baa3e7da..ab456c7d7df 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -37,7 +37,12 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: CONF_HOST: "1.2.3.4", CONF_PORT: 1234, }, - options={}, + options={ + "device_options": { + "28.222222222222": {"precision": "temperature9"}, + "28.222222222223": {"precision": "temperature5"}, + } + }, entry_id="2", ) config_entry.add_to_hass(hass) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 8d3a8270752..d77b374c7c4 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -435,6 +435,54 @@ MOCK_OWPROXY_DEVICES = { }, ], }, + "28.222222222222": { + # This device has precision options in the config entry + ATTR_INJECT_READS: [ + b"DS18B20", # read device type + ], + ATTR_DEVICE_INFO: { + ATTR_IDENTIFIERS: {(DOMAIN, "28.222222222222")}, + ATTR_MANUFACTURER: MANUFACTURER_MAXIM, + ATTR_MODEL: "DS18B20", + ATTR_NAME: "28.222222222222", + }, + Platform.SENSOR: [ + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_DEVICE_FILE: "/28.222222222222/temperature9", + ATTR_ENTITY_ID: "sensor.28_222222222222_temperature", + ATTR_INJECT_READS: b" 26.984", + ATTR_STATE: "27.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "/28.222222222222/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ], + }, + "28.222222222223": { + # This device has an illegal precision option in the config entry + ATTR_INJECT_READS: [ + b"DS18B20", # read device type + ], + ATTR_DEVICE_INFO: { + ATTR_IDENTIFIERS: {(DOMAIN, "28.222222222223")}, + ATTR_MANUFACTURER: MANUFACTURER_MAXIM, + ATTR_MODEL: "DS18B20", + ATTR_NAME: "28.222222222223", + }, + Platform.SENSOR: [ + { + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_DEVICE_FILE: "/28.222222222223/temperature", + ATTR_ENTITY_ID: "sensor.28_222222222223_temperature", + ATTR_INJECT_READS: b" 26.984", + ATTR_STATE: "27.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "/28.222222222223/temperature", + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ], + }, "29.111111111111": { ATTR_INJECT_READS: [ b"DS2408", # read device type diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index bc164a9b138..ded4811a586 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -54,7 +54,12 @@ async def test_entry_diagnostics( "port": 1234, "type": "OWServer", }, - "options": {}, + "options": { + "device_options": { + "28.222222222222": {"precision": "temperature9"}, + "28.222222222223": {"precision": "temperature5"}, + } + }, "title": "Mock Title", }, "devices": [DEVICE_DETAILS], diff --git a/tests/components/onewire/test_options_flow.py b/tests/components/onewire/test_options_flow.py new file mode 100644 index 00000000000..2edb9da7ffc --- /dev/null +++ b/tests/components/onewire/test_options_flow.py @@ -0,0 +1,237 @@ +"""Tests for 1-Wire config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant.components.onewire.const import ( + CONF_TYPE_SYSBUS, + DOMAIN, + INPUT_ENTRY_CLEAR_OPTIONS, + INPUT_ENTRY_DEVICE_SELECTION, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import setup_owproxy_mock_devices +from .const import MOCK_OWPROXY_DEVICES + + +class FakeDevice: + """Mock Class for mocking DeviceEntry.""" + + name_by_user = "Given Name" + + +class FakeOWHubSysBus: + """Mock Class for mocking onewire hub.""" + + type = CONF_TYPE_SYSBUS + + +async def test_user_owserver_options_clear( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test clearing the options.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that the clear-input action clears the options dict + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == {} + + +async def test_user_owserver_options_empty_selection( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test leaving the selection of devices empty.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that an empty selection does not modify the options + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "device_selection" + assert result["errors"] == {"base": "device_not_selected"} + + +async def test_user_owserver_options_set_single( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test configuring a single device.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Clear config options to certify functionality when starting from scratch + config_entry.options = {} + + # Verify that first config step comes back with a selection list of all the 28-family devices + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "28.111111111111": False, + "28.222222222222": False, + "28.222222222223": False, + } + + # Verify that a single selected device to configure comes back as a form with the device to configure + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["description_placeholders"]["sensor_id"] == "28.111111111111" + + # Verify that the setting for the device comes back as default when no input is given + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["data"]["device_options"]["28.111111111111"]["precision"] + == "temperature" + ) + + +async def test_user_owserver_options_set_multiple( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test configuring multiple consecutive devices in a row.""" + setup_owproxy_mock_devices( + owproxy, Platform.SENSOR, [x for x in MOCK_OWPROXY_DEVICES if "28." in x] + ) + + # Initialize onewire hub + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that first config step comes back with a selection list of all the 28-family devices + with patch( + "homeassistant.helpers.device_registry.DeviceRegistry.async_get_device", + return_value=FakeDevice(), + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["data_schema"].schema["device_selection"].options == { + "Given Name (28.111111111111)": False, + "Given Name (28.222222222222)": False, + "Given Name (28.222222222223)": False, + } + + # Verify that selecting two devices to configure comes back as a + # form with the first device to configure using it's long name as entry + with patch( + "homeassistant.helpers.device_registry.DeviceRegistry.async_get_device", + return_value=FakeDevice(), + ): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + INPUT_ENTRY_DEVICE_SELECTION: [ + "Given Name (28.111111111111)", + "Given Name (28.222222222222)", + ] + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert ( + result["description_placeholders"]["sensor_id"] + == "Given Name (28.222222222222)" + ) + + # Verify that next sensor is coming up for configuration after the first + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"precision": "temperature"}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert ( + result["description_placeholders"]["sensor_id"] + == "Given Name (28.111111111111)" + ) + + # Verify that the setting for the device comes back as default when no input is given + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"precision": "temperature9"}, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + result["data"]["device_options"]["28.222222222222"]["precision"] + == "temperature" + ) + assert ( + result["data"]["device_options"]["28.111111111111"]["precision"] + == "temperature9" + ) + + +async def test_user_owserver_options_no_devices( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, +): + """Test that options does not change when no devices are available.""" + # Initialize onewire hub + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Verify that first config step comes back with an empty list of possible devices to choose from + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "No configurable devices found." + + +async def test_user_sysbus_options( + hass: HomeAssistant, + config_entry: ConfigEntry, +): + """Test that SysBus options flow aborts on init.""" + hass.data[DOMAIN] = {config_entry.entry_id: FakeOWHubSysBus()} + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "SysBus setup does not have any config options."