Skip to content

Commit 1ded4fe

Browse files
Add two-step options flow for experimental features
When experimental features is enabled, the flow now proceeds to a second step showing the entity selector for external temperature sensor. This provides a better UX than requiring users to save and re-open the config to see the entity selector.
1 parent 0b4d4e2 commit 1ded4fe

3 files changed

Lines changed: 136 additions & 55 deletions

File tree

custom_components/mitsubishi/config_flow.py

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -134,50 +134,38 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
134134
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
135135
"""Initialize options flow."""
136136
self._config_entry = config_entry
137+
self._connection_data: dict[str, Any] = {}
138+
self._experimental_features: bool = False
137139

138140
@property
139141
def config_entry(self) -> config_entries.ConfigEntry:
140142
"""Return the config entry."""
141143
return self._config_entry
142144

143145
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any:
144-
"""Manage the options."""
146+
"""Step 1: Connection settings and experimental features toggle."""
145147
errors: dict[str, str] = {}
146148

147149
if user_input is not None:
148-
_LOGGER.debug("Processing options user input: %s", user_input)
150+
_LOGGER.debug("Processing options step init: %s", user_input)
149151
try:
150-
# Separate options from connection data
151-
experimental_features = user_input.pop(CONF_EXPERIMENTAL_FEATURES, False)
152-
external_temp_entity = user_input.pop(CONF_EXTERNAL_TEMP_ENTITY, None)
152+
# Extract experimental features flag
153+
self._experimental_features = user_input.pop(CONF_EXPERIMENTAL_FEATURES, False)
153154

154155
# Validate the connection configuration
155156
_LOGGER.debug("Validating new configuration")
156157
await validate_input(self.hass, user_input)
157158
_LOGGER.debug("Validation successful for options")
158159

159-
# Update the config entry data (connection settings)
160-
self.hass.config_entries.async_update_entry(
161-
self.config_entry,
162-
data=user_input,
163-
)
164-
165-
# Trigger reload of the integration to apply changes
166-
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
167-
168-
# Build new options, preserving remote_temp_mode if it exists
169-
new_options = {
170-
CONF_EXPERIMENTAL_FEATURES: experimental_features,
171-
}
172-
if experimental_features and external_temp_entity:
173-
new_options[CONF_EXTERNAL_TEMP_ENTITY] = external_temp_entity
174-
# Preserve remote_temp_mode from existing options
175-
if CONF_REMOTE_TEMP_MODE in self.config_entry.options:
176-
new_options[CONF_REMOTE_TEMP_MODE] = self.config_entry.options[
177-
CONF_REMOTE_TEMP_MODE
178-
]
179-
180-
return self.async_create_entry(title="", data=new_options)
160+
# Store connection data for later
161+
self._connection_data = user_input
162+
163+
# If experimental features enabled, go to step 2 for entity selection
164+
if self._experimental_features:
165+
return await self.async_step_experimental()
166+
167+
# Otherwise, save and finish
168+
return await self._async_save_options(external_temp_entity=None)
181169

182170
except CannotConnect:
183171
_LOGGER.error("Cannot connect to device with new settings")
@@ -201,36 +189,67 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
201189
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
202190
)
203191
current_experimental = self.config_entry.options.get(CONF_EXPERIMENTAL_FEATURES, False)
204-
current_external_temp_entity = self.config_entry.options.get(CONF_EXTERNAL_TEMP_ENTITY, "")
205192

206-
# Base schema with connection settings
207-
schema_dict = {
208-
vol.Required(CONF_HOST, default=current_host): str,
209-
vol.Optional(CONF_ENCRYPTION_KEY, default=current_encryption_key): str,
210-
vol.Optional(CONF_ADMIN_USERNAME, default=current_admin_username): str,
211-
vol.Optional(CONF_ADMIN_PASSWORD, default=current_admin_password): str,
212-
vol.Optional(CONF_SCAN_INTERVAL, default=current_scan_interval): vol.All(
213-
vol.Coerce(int), vol.Range(min=10, max=300)
214-
),
215-
vol.Optional(CONF_EXPERIMENTAL_FEATURES, default=current_experimental): bool,
216-
}
193+
options_schema = vol.Schema(
194+
{
195+
vol.Required(CONF_HOST, default=current_host): str,
196+
vol.Optional(CONF_ENCRYPTION_KEY, default=current_encryption_key): str,
197+
vol.Optional(CONF_ADMIN_USERNAME, default=current_admin_username): str,
198+
vol.Optional(CONF_ADMIN_PASSWORD, default=current_admin_password): str,
199+
vol.Optional(CONF_SCAN_INTERVAL, default=current_scan_interval): vol.All(
200+
vol.Coerce(int), vol.Range(min=10, max=300)
201+
),
202+
vol.Optional(CONF_EXPERIMENTAL_FEATURES, default=current_experimental): bool,
203+
}
204+
)
205+
206+
return self.async_show_form(step_id="init", data_schema=options_schema, errors=errors)
207+
208+
async def async_step_experimental(self, user_input: dict[str, Any] | None = None) -> Any:
209+
"""Step 2: Configure experimental features (external temperature sensor)."""
210+
if user_input is not None:
211+
external_temp_entity = user_input.get(CONF_EXTERNAL_TEMP_ENTITY)
212+
return await self._async_save_options(external_temp_entity=external_temp_entity)
217213

218-
# Only show external temp entity selector if experimental features are enabled
219-
if current_experimental:
220-
schema_dict[
214+
current_external_temp_entity = self.config_entry.options.get(CONF_EXTERNAL_TEMP_ENTITY, "")
215+
216+
experimental_schema = vol.Schema(
217+
{
221218
vol.Optional(
222219
CONF_EXTERNAL_TEMP_ENTITY,
223220
description={"suggested_value": current_external_temp_entity},
224-
)
225-
] = selector.EntitySelector(
226-
selector.EntitySelectorConfig(
227-
domain=["sensor", "input_number", "number"],
228-
)
229-
)
221+
): selector.EntitySelector(
222+
selector.EntitySelectorConfig(
223+
domain=["sensor", "input_number", "number"],
224+
)
225+
),
226+
}
227+
)
230228

231-
options_schema = vol.Schema(schema_dict)
229+
return self.async_show_form(step_id="experimental", data_schema=experimental_schema)
232230

233-
return self.async_show_form(step_id="init", data_schema=options_schema, errors=errors)
231+
async def _async_save_options(self, external_temp_entity: str | None) -> Any:
232+
"""Save connection data and options."""
233+
# Update the config entry data (connection settings)
234+
self.hass.config_entries.async_update_entry(
235+
self.config_entry,
236+
data=self._connection_data,
237+
)
238+
239+
# Trigger reload of the integration to apply changes
240+
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
241+
242+
# Build new options
243+
new_options: dict[str, Any] = {
244+
CONF_EXPERIMENTAL_FEATURES: self._experimental_features,
245+
}
246+
if self._experimental_features and external_temp_entity:
247+
new_options[CONF_EXTERNAL_TEMP_ENTITY] = external_temp_entity
248+
# Preserve remote_temp_mode from existing options
249+
if CONF_REMOTE_TEMP_MODE in self.config_entry.options:
250+
new_options[CONF_REMOTE_TEMP_MODE] = self.config_entry.options[CONF_REMOTE_TEMP_MODE]
251+
252+
return self.async_create_entry(title="", data=new_options)
234253

235254

236255
class CannotConnect(HomeAssistantError):

custom_components/mitsubishi/strings.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,19 @@
3434
"admin_password": "Admin Password (default: me1debug@0567)",
3535
"scan_interval": "Update Interval (seconds, 10-300)",
3636
"enable_capability_detection": "Enable capability detection",
37-
"experimental_features": "Enable experimental features",
37+
"experimental_features": "Enable experimental features"
38+
},
39+
"data_description": {
40+
"experimental_features": "Enable experimental features like remote temperature sensor support. These features may change or be removed in future versions."
41+
}
42+
},
43+
"experimental": {
44+
"title": "Experimental Features Configuration",
45+
"description": "⚠️ WARNING: The remote temperature feature should NOT be used for critical heating/cooling needs. If your Home Assistant server goes offline, your external sensor becomes unavailable, or provides invalid readings, the AC will NOT automatically revert to its internal sensor. You must manually switch back to Internal mode.",
46+
"data": {
3847
"external_temperature_entity": "External Temperature Sensor"
3948
},
4049
"data_description": {
41-
"experimental_features": "Enable experimental features like remote temperature sensor support. These features may change or be removed in future versions. WARNING: The remote temperature feature should NOT be used for critical heating/cooling needs. If your Home Assistant server goes offline, your external sensor becomes unavailable, or provides invalid readings, the AC will NOT automatically revert to its internal sensor. You must manually switch back to Internal mode.",
4250
"external_temperature_entity": "Select a temperature sensor to use as the room temperature when Remote mode is selected. Leave empty to only use the internal sensor. The AC must receive regular temperature updates - if updates stop, the AC continues using the last received value indefinitely."
4351
}
4452
}

tests/test_config_flow.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
CONF_ADMIN_PASSWORD,
1919
CONF_ADMIN_USERNAME,
2020
CONF_ENCRYPTION_KEY,
21+
CONF_EXPERIMENTAL_FEATURES,
22+
CONF_EXTERNAL_TEMP_ENTITY,
2123
CONF_SCAN_INTERVAL,
2224
DOMAIN,
2325
)
@@ -346,7 +348,7 @@ async def test_options_flow_init(self, hass: HomeAssistant, mock_config_entry):
346348
assert CONF_SCAN_INTERVAL in schema_keys
347349

348350
async def test_options_flow_successful_update(self, hass: HomeAssistant, mock_config_entry):
349-
"""Test successful options flow update."""
351+
"""Test successful options flow update without experimental features."""
350352
# Mock the config entries registry methods
351353
hass.config_entries.async_update_entry = MagicMock()
352354
hass.config_entries.async_reload = AsyncMock()
@@ -361,6 +363,7 @@ async def test_options_flow_successful_update(self, hass: HomeAssistant, mock_co
361363
CONF_SCAN_INTERVAL: 60,
362364
CONF_ADMIN_USERNAME: "admin",
363365
CONF_ADMIN_PASSWORD: "password",
366+
CONF_EXPERIMENTAL_FEATURES: False,
364367
}
365368

366369
with patch(
@@ -370,13 +373,62 @@ async def test_options_flow_successful_update(self, hass: HomeAssistant, mock_co
370373
result = await options_flow.async_step_init(new_data)
371374

372375
assert result["type"] == FlowResultType.CREATE_ENTRY
373-
# Options flow now returns experimental_features setting
376+
# Options flow returns experimental_features setting
374377
assert result["data"] == {"experimental_features": False}
378+
# Connection data should not include experimental_features
379+
expected_connection_data = {
380+
CONF_HOST: "192.168.1.101",
381+
CONF_ENCRYPTION_KEY: "new_key",
382+
CONF_SCAN_INTERVAL: 60,
383+
CONF_ADMIN_USERNAME: "admin",
384+
CONF_ADMIN_PASSWORD: "password",
385+
}
375386
hass.config_entries.async_update_entry.assert_called_once_with(
376-
mock_config_entry, data=new_data
387+
mock_config_entry, data=expected_connection_data
377388
)
378389
hass.config_entries.async_reload.assert_called_once_with("test_entry_id")
379390

391+
async def test_options_flow_with_experimental_features(self, hass: HomeAssistant, mock_config_entry):
392+
"""Test options flow with experimental features enabled goes to step 2."""
393+
# Mock the config entries registry methods
394+
hass.config_entries.async_update_entry = MagicMock()
395+
hass.config_entries.async_reload = AsyncMock()
396+
397+
# Create options flow
398+
options_flow = OptionsFlowHandler(mock_config_entry)
399+
options_flow.hass = hass
400+
401+
new_data = {
402+
CONF_HOST: "192.168.1.101",
403+
CONF_ENCRYPTION_KEY: "new_key",
404+
CONF_SCAN_INTERVAL: 60,
405+
CONF_ADMIN_USERNAME: "admin",
406+
CONF_ADMIN_PASSWORD: "password",
407+
CONF_EXPERIMENTAL_FEATURES: True,
408+
}
409+
410+
with patch(
411+
"custom_components.mitsubishi.config_flow.validate_input",
412+
return_value={"title": "Test Device", "unique_id": "test_mac"},
413+
):
414+
result = await options_flow.async_step_init(new_data)
415+
416+
# Should go to experimental step
417+
assert result["type"] == FlowResultType.FORM
418+
assert result["step_id"] == "experimental"
419+
420+
# Now complete the experimental step with an entity selection
421+
result2 = await options_flow.async_step_experimental({
422+
CONF_EXTERNAL_TEMP_ENTITY: "sensor.room_temp"
423+
})
424+
425+
assert result2["type"] == FlowResultType.CREATE_ENTRY
426+
assert result2["data"] == {
427+
"experimental_features": True,
428+
"external_temperature_entity": "sensor.room_temp",
429+
}
430+
hass.config_entries.async_reload.assert_called_with("test_entry_id")
431+
380432
async def test_options_flow_cannot_connect(self, hass: HomeAssistant, mock_config_entry):
381433
"""Test options flow with connection error."""
382434
# Create options flow
@@ -389,6 +441,7 @@ async def test_options_flow_cannot_connect(self, hass: HomeAssistant, mock_confi
389441
CONF_SCAN_INTERVAL: 30,
390442
CONF_ADMIN_USERNAME: "admin",
391443
CONF_ADMIN_PASSWORD: "password",
444+
CONF_EXPERIMENTAL_FEATURES: False,
392445
}
393446

394447
with patch(
@@ -413,6 +466,7 @@ async def test_options_flow_unknown_error(self, hass: HomeAssistant, mock_config
413466
CONF_SCAN_INTERVAL: 30,
414467
CONF_ADMIN_USERNAME: "admin",
415468
CONF_ADMIN_PASSWORD: "password",
469+
CONF_EXPERIMENTAL_FEATURES: False,
416470
}
417471

418472
with patch(

0 commit comments

Comments
 (0)