Skip to content

Commit dba4a27

Browse files
feat: Add remote temperature sensor support (experimental)
Allow the AC to use an external temperature sensor (e.g., room average) instead of its internal sensor for temperature regulation. This feature is gated behind an "Enable experimental features" toggle in the integration options. Users must explicitly opt-in to access the remote temperature functionality. Changes: - Add experimental features toggle in options flow - Add Temperature Source selector entity (Internal/Remote) when enabled - Add external temperature entity configuration when enabled - Send configured sensor temperature to AC on each update cycle - Persist remote/internal mode to survive HA restarts - Restore mode automatically on startup The external temperature entity can be any HA sensor that reports temperature (e.g., a template sensor averaging multiple room sensors).
1 parent 5df0e03 commit dba4a27

6 files changed

Lines changed: 291 additions & 26 deletions

File tree

custom_components/mitsubishi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
6262
raise ConfigEntryNotReady(f"Unable to connect to Mitsubishi AC at {host}") from e
6363

6464
# Create data update coordinator with custom scan interval
65-
coordinator = MitsubishiDataUpdateCoordinator(hass, controller, scan_interval)
65+
coordinator = MitsubishiDataUpdateCoordinator(hass, controller, entry, scan_interval)
6666

6767
# Fetch unit info for device registry enrichment
6868
try:

custom_components/mitsubishi/config_flow.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
from homeassistant.core import HomeAssistant
1212
from homeassistant.data_entry_flow import AbortFlow
1313
from homeassistant.exceptions import HomeAssistantError
14+
from homeassistant.helpers import selector
1415
from pymitsubishi import MitsubishiAPI, MitsubishiController
1516

1617
from .const import (
1718
CONF_ADMIN_PASSWORD,
1819
CONF_ADMIN_USERNAME,
1920
CONF_ENCRYPTION_KEY,
21+
CONF_EXPERIMENTAL_FEATURES,
22+
CONF_EXTERNAL_TEMP_ENTITY,
23+
CONF_REMOTE_TEMP_MODE,
2024
CONF_SCAN_INTERVAL,
2125
DEFAULT_ADMIN_PASSWORD,
2226
DEFAULT_ADMIN_USERNAME,
@@ -139,18 +143,37 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
139143
if user_input is not None:
140144
_LOGGER.debug("Processing options user input: %s", user_input)
141145
try:
142-
# Validate the new configuration
146+
# Separate options from connection data
147+
experimental_features = user_input.pop(CONF_EXPERIMENTAL_FEATURES, False)
148+
external_temp_entity = user_input.pop(CONF_EXTERNAL_TEMP_ENTITY, None)
149+
150+
# Validate the connection configuration
143151
_LOGGER.debug("Validating new configuration")
144152
await validate_input(self.hass, user_input)
145153
_LOGGER.debug("Validation successful for options")
146154

147-
# Update the config entry with new data
148-
self.hass.config_entries.async_update_entry(self._config_entry, data=user_input)
155+
# Update the config entry data (connection settings)
156+
self.hass.config_entries.async_update_entry(
157+
self._config_entry,
158+
data=user_input,
159+
)
149160

150161
# Trigger reload of the integration to apply changes
151162
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
152163

153-
return self.async_create_entry(title="", data={})
164+
# Build new options, preserving remote_temp_mode if it exists
165+
new_options = {
166+
CONF_EXPERIMENTAL_FEATURES: experimental_features,
167+
}
168+
if experimental_features and external_temp_entity:
169+
new_options[CONF_EXTERNAL_TEMP_ENTITY] = external_temp_entity
170+
# Preserve remote_temp_mode from existing options
171+
if CONF_REMOTE_TEMP_MODE in self._config_entry.options:
172+
new_options[CONF_REMOTE_TEMP_MODE] = self._config_entry.options[
173+
CONF_REMOTE_TEMP_MODE
174+
]
175+
176+
return self.async_create_entry(title="", data=new_options)
154177

155178
except CannotConnect:
156179
_LOGGER.error("Cannot connect to device with new settings")
@@ -173,18 +196,35 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
173196
current_scan_interval = self._config_entry.data.get(
174197
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
175198
)
199+
current_experimental = self._config_entry.options.get(CONF_EXPERIMENTAL_FEATURES, False)
200+
current_external_temp_entity = self._config_entry.options.get(CONF_EXTERNAL_TEMP_ENTITY, "")
201+
202+
# Base schema with connection settings
203+
schema_dict = {
204+
vol.Required(CONF_HOST, default=current_host): str,
205+
vol.Optional(CONF_ENCRYPTION_KEY, default=current_encryption_key): str,
206+
vol.Optional(CONF_ADMIN_USERNAME, default=current_admin_username): str,
207+
vol.Optional(CONF_ADMIN_PASSWORD, default=current_admin_password): str,
208+
vol.Optional(CONF_SCAN_INTERVAL, default=current_scan_interval): vol.All(
209+
vol.Coerce(int), vol.Range(min=10, max=300)
210+
),
211+
vol.Optional(CONF_EXPERIMENTAL_FEATURES, default=current_experimental): bool,
212+
}
176213

177-
options_schema = vol.Schema(
178-
{
179-
vol.Required(CONF_HOST, default=current_host): str,
180-
vol.Optional(CONF_ENCRYPTION_KEY, default=current_encryption_key): str,
181-
vol.Optional(CONF_ADMIN_USERNAME, default=current_admin_username): str,
182-
vol.Optional(CONF_ADMIN_PASSWORD, default=current_admin_password): str,
183-
vol.Optional(CONF_SCAN_INTERVAL, default=current_scan_interval): vol.All(
184-
vol.Coerce(int), vol.Range(min=10, max=300)
185-
),
186-
}
187-
)
214+
# Only show external temp entity selector if experimental features are enabled
215+
if current_experimental:
216+
schema_dict[
217+
vol.Optional(
218+
CONF_EXTERNAL_TEMP_ENTITY,
219+
description={"suggested_value": current_external_temp_entity},
220+
)
221+
] = selector.EntitySelector(
222+
selector.EntitySelectorConfig(
223+
domain=["sensor", "input_number", "number"],
224+
)
225+
)
226+
227+
options_schema = vol.Schema(schema_dict)
188228

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

custom_components/mitsubishi/const.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,14 @@
1313
CONF_ADMIN_PASSWORD = "admin_password"
1414
DEFAULT_ADMIN_PASSWORD = "me1debug@0567"
1515
CONF_SCAN_INTERVAL = "scan_interval"
16+
17+
# Experimental Features
18+
CONF_EXPERIMENTAL_FEATURES = "experimental_features"
19+
20+
# Remote Temperature Configuration (experimental)
21+
CONF_EXTERNAL_TEMP_ENTITY = "external_temperature_entity"
22+
CONF_REMOTE_TEMP_MODE = "remote_temp_mode"
23+
24+
# Temperature Source Modes
25+
TEMP_SOURCE_INTERNAL = "internal"
26+
TEMP_SOURCE_REMOTE = "remote"

custom_components/mitsubishi/coordinator.py

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@
55
import logging
66
from datetime import timedelta
77

8+
from homeassistant.config_entries import ConfigEntry
9+
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
810
from homeassistant.core import HomeAssistant
911
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1012
from pymitsubishi import MitsubishiController, ParsedDeviceState
1113

12-
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
14+
from .const import (
15+
CONF_EXPERIMENTAL_FEATURES,
16+
CONF_EXTERNAL_TEMP_ENTITY,
17+
CONF_REMOTE_TEMP_MODE,
18+
DEFAULT_SCAN_INTERVAL,
19+
DOMAIN,
20+
)
1321

1422
_LOGGER = logging.getLogger(__name__)
1523

@@ -21,11 +29,16 @@ def __init__(
2129
self,
2230
hass: HomeAssistant,
2331
controller: MitsubishiController,
32+
config_entry: ConfigEntry,
2433
scan_interval: int = DEFAULT_SCAN_INTERVAL,
2534
) -> None:
2635
"""Initialize."""
2736
self.controller = controller
37+
self.config_entry = config_entry
2838
self.unit_info = None # Will be populated on first update or by config flow
39+
# Load persisted remote temperature mode (AC doesn't report it, so we track it)
40+
self._remote_temp_mode = config_entry.options.get(CONF_REMOTE_TEMP_MODE, False)
41+
self._startup_mode_applied = False
2942

3043
super().__init__(
3144
hass,
@@ -34,16 +47,110 @@ def __init__(
3447
update_interval=timedelta(seconds=scan_interval),
3548
)
3649

50+
def set_remote_temp_mode(self, enabled: bool) -> None:
51+
"""Set whether remote temperature mode is enabled and persist to storage."""
52+
self._remote_temp_mode = enabled
53+
_LOGGER.info("Remote temperature mode set to: %s", enabled)
54+
55+
# Persist to config entry options
56+
new_options = dict(self.config_entry.options)
57+
new_options[CONF_REMOTE_TEMP_MODE] = enabled
58+
self.hass.config_entries.async_update_entry(
59+
self.config_entry,
60+
options=new_options,
61+
)
62+
63+
@property
64+
def remote_temp_mode(self) -> bool:
65+
"""Return whether remote temperature mode is enabled."""
66+
return self._remote_temp_mode
67+
68+
@property
69+
def experimental_features_enabled(self) -> bool:
70+
"""Return whether experimental features are enabled."""
71+
return self.config_entry.options.get(CONF_EXPERIMENTAL_FEATURES, False)
72+
3773
async def get_unit_info(self) -> dict:
3874
"""Fetch unit information once for device info."""
3975
unit_info = await self.hass.async_add_executor_job(self.controller.get_unit_info)
4076
return unit_info
4177

4278
async def _async_update_data(self) -> ParsedDeviceState:
4379
"""Update data via library."""
44-
_LOGGER.info("Coordinator fetching device status")
45-
# This invokes a network call, run in executor to avoid blocking
80+
_LOGGER.debug("Coordinator fetching device status")
81+
82+
# Only process remote temperature if experimental features are enabled
83+
if self.experimental_features_enabled:
84+
# On first update after startup, restore the persisted mode to the AC
85+
if not self._startup_mode_applied:
86+
self._startup_mode_applied = True
87+
if self._remote_temp_mode:
88+
_LOGGER.info("Restoring remote temperature mode from persisted state")
89+
await self._send_remote_temperature()
90+
else:
91+
_LOGGER.debug("Starting with internal temperature mode")
92+
elif self._remote_temp_mode:
93+
# Regular update - send remote temperature if enabled
94+
await self._send_remote_temperature()
95+
else:
96+
self._startup_mode_applied = True
97+
98+
# Fetch status from device
4699
state = await self.hass.async_add_executor_job(
47100
self.controller.fetch_status,
48101
)
49102
return state
103+
104+
async def _send_remote_temperature(self) -> None:
105+
"""Send remote temperature to AC if configured and available."""
106+
external_entity_id = self.config_entry.options.get(CONF_EXTERNAL_TEMP_ENTITY)
107+
108+
if not external_entity_id:
109+
_LOGGER.warning(
110+
"Remote temperature mode enabled but no external entity configured, "
111+
"falling back to internal sensor"
112+
)
113+
await self.hass.async_add_executor_job(self.controller.set_current_temperature, None)
114+
self._remote_temp_mode = False
115+
return
116+
117+
state = self.hass.states.get(external_entity_id)
118+
119+
if state is None:
120+
_LOGGER.warning(
121+
"External temperature entity %s not found, falling back to internal sensor",
122+
external_entity_id,
123+
)
124+
await self.hass.async_add_executor_job(self.controller.set_current_temperature, None)
125+
self._remote_temp_mode = False
126+
return
127+
128+
if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
129+
_LOGGER.warning(
130+
"External temperature entity %s is %s, falling back to internal sensor",
131+
external_entity_id,
132+
state.state,
133+
)
134+
await self.hass.async_add_executor_job(self.controller.set_current_temperature, None)
135+
self._remote_temp_mode = False
136+
return
137+
138+
try:
139+
temperature = float(state.state)
140+
_LOGGER.debug(
141+
"Sending remote temperature %.1f from %s to AC",
142+
temperature,
143+
external_entity_id,
144+
)
145+
await self.hass.async_add_executor_job(
146+
self.controller.set_current_temperature, temperature
147+
)
148+
except (ValueError, TypeError) as e:
149+
_LOGGER.error(
150+
"Invalid temperature value '%s' from %s: %s, falling back to internal sensor",
151+
state.state,
152+
external_entity_id,
153+
e,
154+
)
155+
await self.hass.async_add_executor_job(self.controller.set_current_temperature, None)
156+
self._remote_temp_mode = False

0 commit comments

Comments
 (0)