Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ repos:
- custom_components/
additional_dependencies:
- types-requests
- homeassistant-stubs
- homeassistant-stubs>=2024.8.0 # Minimum version with ClimateEntityFeature.TURN_ON/OFF
pass_filenames: false

# YAML formatting
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mitsubishi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady(f"Unable to connect to Mitsubishi AC at {host}") from e

# Create data update coordinator with custom scan interval
coordinator = MitsubishiDataUpdateCoordinator(hass, controller, scan_interval)
coordinator = MitsubishiDataUpdateCoordinator(hass, controller, entry, scan_interval)

# Fetch unit info for device registry enrichment
try:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mitsubishi/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class MitsubishiClimate(MitsubishiEntity, ClimateEntity):
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.SWING_HORIZONTAL_MODE # type: ignore[attr-defined]
| ClimateEntityFeature.SWING_HORIZONTAL_MODE
)

def __init__(
Expand Down
158 changes: 143 additions & 15 deletions custom_components/mitsubishi/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from pymitsubishi import MitsubishiAPI, MitsubishiController

from .const import (
CONF_ADMIN_PASSWORD,
CONF_ADMIN_USERNAME,
CONF_ENCRYPTION_KEY,
CONF_EXPERIMENTAL_FEATURES,
CONF_EXTERNAL_TEMP_ENTITY,
CONF_REMOTE_TEMP_MODE,
CONF_SCAN_INTERVAL,
DEFAULT_ADMIN_PASSWORD,
DEFAULT_ADMIN_USERNAME,
Expand All @@ -25,6 +29,30 @@
DOMAIN,
)


def _get_experimental_schema(suggested_value: str | None = None) -> vol.Schema:
"""Get the schema for experimental features configuration."""
entity_selector = selector.EntitySelector(
selector.EntitySelectorConfig(
domain=["sensor", "input_number", "number"],
)
)
if suggested_value:
return vol.Schema(
{
vol.Optional(
CONF_EXTERNAL_TEMP_ENTITY,
description={"suggested_value": suggested_value},
): entity_selector,
}
)
return vol.Schema(
{
vol.Optional(CONF_EXTERNAL_TEMP_ENTITY): entity_selector,
}
)


_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
Expand All @@ -36,6 +64,7 @@
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): vol.All(
vol.Coerce(int), vol.Range(min=10, max=300)
),
vol.Optional(CONF_EXPERIMENTAL_FEATURES, default=False): bool,
}
)

Expand Down Expand Up @@ -80,6 +109,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
MINOR_VERSION = 1

def __init__(self) -> None:
Comment thread
niobos marked this conversation as resolved.
"""Initialize the config flow."""
super().__init__()
self._connection_data: dict[str, Any] = {}
self._experimental_features: bool = False
self._device_info: dict[str, Any] = {}

@staticmethod
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
Expand All @@ -95,6 +131,9 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Any
if user_input is not None:
_LOGGER.debug("Processing user input for config flow")
try:
# Extract experimental features flag
self._experimental_features = user_input.pop(CONF_EXPERIMENTAL_FEATURES, False)

_LOGGER.debug("Calling validate_input")
info = await validate_input(self.hass, user_input)
_LOGGER.debug("Validation successful, info: %s", info)
Expand All @@ -103,8 +142,16 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Any
await self.async_set_unique_id(info["unique_id"])
self._abort_if_unique_id_configured()

_LOGGER.debug("Creating config entry")
return self.async_create_entry(title=info["title"], data=user_input)
# Store for later
self._connection_data = user_input
self._device_info = info

# If experimental features enabled, go to step 2
if self._experimental_features:
return await self.async_step_experimental()

# Otherwise, create entry directly
return self._create_entry(external_temp_entity=None)
Comment on lines +149 to +154

Copilot AI Jan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no test coverage for the initial config flow with experimental features enabled. The config flow at lines 124-129 in config_flow.py shows that when experimental_features is True, the flow proceeds to async_step_experimental, but there are no tests that verify this multi-step configuration works correctly during initial setup. Consider adding a test similar to test_options_flow_with_experimental_features but for the initial config flow.

Copilot uses AI. Check for mistakes.

except CannotConnect:
_LOGGER.error("Cannot connect to device")
Expand All @@ -123,6 +170,31 @@ async def async_step_user(self, user_input: dict[str, Any] | None = None) -> Any
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

async def async_step_experimental(self, user_input: dict[str, Any] | None = None) -> Any:
"""Step 2: Configure experimental features (external temperature sensor)."""
if user_input is not None:
external_temp_entity = user_input.get(CONF_EXTERNAL_TEMP_ENTITY)
return self._create_entry(external_temp_entity=external_temp_entity)

return self.async_show_form(step_id="experimental", data_schema=_get_experimental_schema())

def _create_entry(self, external_temp_entity: str | None) -> Any:
"""Create the config entry with options."""
_LOGGER.debug("Creating config entry")

# Build options
options: dict[str, Any] = {
CONF_EXPERIMENTAL_FEATURES: self._experimental_features,
}
if self._experimental_features and external_temp_entity:
options[CONF_EXTERNAL_TEMP_ENTITY] = external_temp_entity

return self.async_create_entry(
title=self._device_info["title"],
data=self._connection_data,
options=options,
)


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for the Mitsubishi integration."""
Expand All @@ -131,26 +203,38 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
super().__init__()
Comment thread
niobos marked this conversation as resolved.
self._config_entry = config_entry
self._connection_data: dict[str, Any] = {}
self._experimental_features: bool = False

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

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

if user_input is not None:
_LOGGER.debug("Processing options user input: %s", user_input)
_LOGGER.debug("Processing options step init: %s", user_input)
try:
# Validate the new configuration
# Extract experimental features flag
self._experimental_features = user_input.pop(CONF_EXPERIMENTAL_FEATURES, False)

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

# Update the config entry with new data
self.hass.config_entries.async_update_entry(self._config_entry, data=user_input)
# Store connection data for later
self._connection_data = user_input

# Trigger reload of the integration to apply changes
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
# If experimental features enabled, go to step 2 for entity selection
if self._experimental_features:
return await self.async_step_experimental()

return self.async_create_entry(title="", data={})
# Otherwise, save and finish
return await self._async_save_options(external_temp_entity=None)

except CannotConnect:
_LOGGER.error("Cannot connect to device with new settings")
Expand All @@ -160,19 +244,20 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
errors["base"] = "unknown"

# Create options schema with current values as defaults
current_host = self._config_entry.data.get(CONF_HOST, "")
current_encryption_key = self._config_entry.data.get(
current_host = self.config_entry.data.get(CONF_HOST, "")
current_encryption_key = self.config_entry.data.get(
CONF_ENCRYPTION_KEY, DEFAULT_ENCRYPTION_KEY
)
current_admin_username = self._config_entry.data.get(
current_admin_username = self.config_entry.data.get(
CONF_ADMIN_USERNAME, DEFAULT_ADMIN_USERNAME
)
current_admin_password = self._config_entry.data.get(
current_admin_password = self.config_entry.data.get(
CONF_ADMIN_PASSWORD, DEFAULT_ADMIN_PASSWORD
)
current_scan_interval = self._config_entry.data.get(
current_scan_interval = self.config_entry.data.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
)
current_experimental = self.config_entry.options.get(CONF_EXPERIMENTAL_FEATURES, False)

options_schema = vol.Schema(
{
Expand All @@ -183,11 +268,54 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
vol.Optional(CONF_SCAN_INTERVAL, default=current_scan_interval): vol.All(
vol.Coerce(int), vol.Range(min=10, max=300)
),
vol.Optional(CONF_EXPERIMENTAL_FEATURES, default=current_experimental): bool,
}
)

return self.async_show_form(step_id="init", data_schema=options_schema, errors=errors)

async def async_step_experimental(self, user_input: dict[str, Any] | None = None) -> Any:
"""Step 2: Configure experimental features (external temperature sensor)."""
if user_input is not None:
external_temp_entity = user_input.get(CONF_EXTERNAL_TEMP_ENTITY)
return await self._async_save_options(external_temp_entity=external_temp_entity)

current_external_temp_entity = self.config_entry.options.get(CONF_EXTERNAL_TEMP_ENTITY, "")

return self.async_show_form(
step_id="experimental",
data_schema=_get_experimental_schema(current_external_temp_entity or None),
)

async def _async_save_options(self, external_temp_entity: str | None) -> Any:
"""Save connection data and options."""
# Update the config entry data (connection settings)
self.hass.config_entries.async_update_entry(
self.config_entry,
data=self._connection_data,
)

# Trigger reload of the integration to apply changes
await self.hass.config_entries.async_reload(self.config_entry.entry_id)

# Build new options
new_options: dict[str, Any] = {
CONF_EXPERIMENTAL_FEATURES: self._experimental_features,
}
if self._experimental_features:
# Only preserve experimental settings when experimental features are enabled
if external_temp_entity:
new_options[CONF_EXTERNAL_TEMP_ENTITY] = external_temp_entity
# Preserve remote_temp_mode from existing options
if CONF_REMOTE_TEMP_MODE in self.config_entry.options:
new_options[CONF_REMOTE_TEMP_MODE] = self.config_entry.options[
CONF_REMOTE_TEMP_MODE
]
# When experimental features are disabled, don't preserve remote_temp_mode
# This ensures a clean state when the feature is turned off
Comment on lines +314 to +315

Copilot AI Jan 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When remote temperature mode is disabled in the options flow (by unchecking experimental features), the remote_temp_mode persisted state is not explicitly cleared. This could cause the coordinator to attempt restoring remote mode on the next restart even though experimental features are disabled, leading to unexpected behavior. Consider explicitly setting CONF_REMOTE_TEMP_MODE to False when experimental features are disabled.

Suggested change
# When experimental features are disabled, don't preserve remote_temp_mode
# This ensures a clean state when the feature is turned off
else:
# When experimental features are disabled, explicitly disable remote_temp_mode
# This ensures a clean state when the feature is turned off
new_options[CONF_REMOTE_TEMP_MODE] = False

Copilot uses AI. Check for mistakes.

return self.async_create_entry(title="", data=new_options)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
11 changes: 11 additions & 0 deletions custom_components/mitsubishi/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@
CONF_ADMIN_PASSWORD = "admin_password"
DEFAULT_ADMIN_PASSWORD = "me1debug@0567"
CONF_SCAN_INTERVAL = "scan_interval"

# Experimental Features
CONF_EXPERIMENTAL_FEATURES = "experimental_features"

# Remote Temperature Configuration (experimental)
CONF_EXTERNAL_TEMP_ENTITY = "external_temperature_entity"
CONF_REMOTE_TEMP_MODE = "remote_temp_mode"

# Temperature Source Modes
TEMP_SOURCE_INTERNAL = "Internal"
TEMP_SOURCE_REMOTE = "Remote"
Loading
Loading