Skip to content

Commit b7eeaa7

Browse files
feat: Add remote temperature sensor support
Allow the AC to use an external temperature sensor (e.g., room average) instead of its internal sensor for temperature regulation. Changes: - Add Temperature Source selector entity (Internal/Remote) - Add external temperature entity configuration in options flow - 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 b7eeaa7

6 files changed

Lines changed: 238 additions & 10 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: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import voluptuous as vol
99
from homeassistant import config_entries
1010
from homeassistant.const import CONF_HOST
11+
from homeassistant.helpers import selector
1112
from homeassistant.core import HomeAssistant
1213
from homeassistant.data_entry_flow import AbortFlow
1314
from homeassistant.exceptions import HomeAssistantError
@@ -17,6 +18,7 @@
1718
CONF_ADMIN_PASSWORD,
1819
CONF_ADMIN_USERNAME,
1920
CONF_ENCRYPTION_KEY,
21+
CONF_EXTERNAL_TEMP_ENTITY,
2022
CONF_SCAN_INTERVAL,
2123
DEFAULT_ADMIN_PASSWORD,
2224
DEFAULT_ADMIN_USERNAME,
@@ -139,18 +141,26 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
139141
if user_input is not None:
140142
_LOGGER.debug("Processing options user input: %s", user_input)
141143
try:
142-
# Validate the new configuration
144+
# Separate connection data from options
145+
external_temp_entity = user_input.pop(CONF_EXTERNAL_TEMP_ENTITY, None)
146+
147+
# Validate the connection configuration
143148
_LOGGER.debug("Validating new configuration")
144149
await validate_input(self.hass, user_input)
145150
_LOGGER.debug("Validation successful for options")
146151

147-
# Update the config entry with new data
148-
self.hass.config_entries.async_update_entry(self._config_entry, data=user_input)
152+
# Update the config entry data (connection settings)
153+
self.hass.config_entries.async_update_entry(
154+
self._config_entry,
155+
data=user_input,
156+
)
149157

150158
# Trigger reload of the integration to apply changes
151159
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
152160

153-
return self.async_create_entry(title="", data={})
161+
# Return options via async_create_entry (this sets config_entry.options)
162+
new_options = {CONF_EXTERNAL_TEMP_ENTITY: external_temp_entity} if external_temp_entity else {}
163+
return self.async_create_entry(title="", data=new_options)
154164

155165
except CannotConnect:
156166
_LOGGER.error("Cannot connect to device with new settings")
@@ -173,6 +183,9 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
173183
current_scan_interval = self._config_entry.data.get(
174184
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
175185
)
186+
current_external_temp_entity = self._config_entry.options.get(
187+
CONF_EXTERNAL_TEMP_ENTITY, ""
188+
)
176189

177190
options_schema = vol.Schema(
178191
{
@@ -183,6 +196,14 @@ async def async_step_init(self, user_input: dict[str, Any] | None = None) -> Any
183196
vol.Optional(CONF_SCAN_INTERVAL, default=current_scan_interval): vol.All(
184197
vol.Coerce(int), vol.Range(min=10, max=300)
185198
),
199+
vol.Optional(
200+
CONF_EXTERNAL_TEMP_ENTITY,
201+
description={"suggested_value": current_external_temp_entity},
202+
): selector.EntitySelector(
203+
selector.EntitySelectorConfig(
204+
domain=["sensor", "input_number", "number"],
205+
)
206+
),
186207
}
187208
)
188209

custom_components/mitsubishi/const.py

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

custom_components/mitsubishi/coordinator.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
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 CONF_EXTERNAL_TEMP_ENTITY, CONF_REMOTE_TEMP_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN
1315

1416
_LOGGER = logging.getLogger(__name__)
1517

@@ -21,11 +23,16 @@ def __init__(
2123
self,
2224
hass: HomeAssistant,
2325
controller: MitsubishiController,
26+
config_entry: ConfigEntry,
2427
scan_interval: int = DEFAULT_SCAN_INTERVAL,
2528
) -> None:
2629
"""Initialize."""
2730
self.controller = controller
31+
self.config_entry = config_entry
2832
self.unit_info = None # Will be populated on first update or by config flow
33+
# Load persisted remote temperature mode (AC doesn't report it, so we track it)
34+
self._remote_temp_mode = config_entry.options.get(CONF_REMOTE_TEMP_MODE, False)
35+
self._startup_mode_applied = False
2936

3037
super().__init__(
3138
hass,
@@ -34,16 +41,109 @@ def __init__(
3441
update_interval=timedelta(seconds=scan_interval),
3542
)
3643

44+
def set_remote_temp_mode(self, enabled: bool) -> None:
45+
"""Set whether remote temperature mode is enabled and persist to storage."""
46+
self._remote_temp_mode = enabled
47+
_LOGGER.info("Remote temperature mode set to: %s", enabled)
48+
49+
# Persist to config entry options
50+
new_options = dict(self.config_entry.options)
51+
new_options[CONF_REMOTE_TEMP_MODE] = enabled
52+
self.hass.config_entries.async_update_entry(
53+
self.config_entry,
54+
options=new_options,
55+
)
56+
57+
@property
58+
def remote_temp_mode(self) -> bool:
59+
"""Return whether remote temperature mode is enabled."""
60+
return self._remote_temp_mode
61+
3762
async def get_unit_info(self) -> dict:
3863
"""Fetch unit information once for device info."""
3964
unit_info = await self.hass.async_add_executor_job(self.controller.get_unit_info)
4065
return unit_info
4166

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

custom_components/mitsubishi/select.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
from __future__ import annotations
44

5+
import logging
56
from typing import Any
67

78
from homeassistant.components.select import SelectEntity
89
from homeassistant.config_entries import ConfigEntry
10+
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
911
from homeassistant.core import HomeAssistant
1012
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1113

12-
from .const import DOMAIN
14+
from .const import CONF_EXTERNAL_TEMP_ENTITY, DOMAIN, TEMP_SOURCE_INTERNAL, TEMP_SOURCE_REMOTE
1315
from .coordinator import MitsubishiDataUpdateCoordinator
1416
from .entity import MitsubishiEntity
1517

18+
_LOGGER = logging.getLogger(__name__)
19+
1620

1721
async def async_setup_entry(
1822
hass: HomeAssistant,
@@ -24,6 +28,7 @@ async def async_setup_entry(
2428
async_add_entities(
2529
[
2630
MitsubishiPowerSavingSelect(coordinator, config_entry),
31+
MitsubishiTemperatureSourceSelect(coordinator, config_entry),
2732
]
2833
)
2934

@@ -64,3 +69,93 @@ async def async_select_option(self, option: str) -> None:
6469
def extra_state_attributes(self) -> dict[str, Any]:
6570
"""Return entity specific state attributes."""
6671
return {"source": "Mitsubishi AC"}
72+
73+
74+
class MitsubishiTemperatureSourceSelect(MitsubishiEntity, SelectEntity):
75+
"""Temperature source mode select for Mitsubishi AC."""
76+
77+
_attr_name = "Temperature Source"
78+
_attr_icon = "mdi:thermometer"
79+
_attr_options = ["Internal", "Remote"]
80+
81+
def __init__(
82+
self,
83+
coordinator: MitsubishiDataUpdateCoordinator,
84+
config_entry: ConfigEntry,
85+
) -> None:
86+
"""Initialize the temperature source select."""
87+
super().__init__(coordinator, config_entry, "temperature_source_select")
88+
self._config_entry = config_entry
89+
90+
@property
91+
def current_option(self) -> str | None:
92+
"""Return the current temperature source mode."""
93+
return "Remote" if self.coordinator.remote_temp_mode else "Internal"
94+
95+
async def async_select_option(self, option: str) -> None:
96+
"""Set the temperature source mode."""
97+
if option == "Internal":
98+
# Switch to internal sensor
99+
self.coordinator.set_remote_temp_mode(False)
100+
await self.hass.async_add_executor_job(
101+
self.coordinator.controller.set_current_temperature,
102+
None,
103+
)
104+
_LOGGER.info("Switched to internal temperature sensor")
105+
else:
106+
# Switch to remote sensor - check if external entity is configured
107+
external_entity_id = self._config_entry.options.get(CONF_EXTERNAL_TEMP_ENTITY)
108+
if not external_entity_id:
109+
_LOGGER.warning(
110+
"Cannot switch to remote mode: No external temperature entity configured. "
111+
"Configure one in the integration options first."
112+
)
113+
return
114+
115+
# Check if the entity is available
116+
state = self.hass.states.get(external_entity_id)
117+
if state is None or state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
118+
_LOGGER.warning(
119+
"Cannot switch to remote mode: External entity %s is not available",
120+
external_entity_id,
121+
)
122+
return
123+
124+
try:
125+
temp = float(state.state)
126+
except (ValueError, TypeError):
127+
_LOGGER.warning(
128+
"Cannot switch to remote mode: Invalid temperature value from %s",
129+
external_entity_id,
130+
)
131+
return
132+
133+
# Enable remote mode and send the temperature
134+
self.coordinator.set_remote_temp_mode(True)
135+
await self.hass.async_add_executor_job(
136+
self.coordinator.controller.set_current_temperature,
137+
temp,
138+
)
139+
_LOGGER.info(
140+
"Switched to remote temperature sensor (%s: %.1f)",
141+
external_entity_id,
142+
temp,
143+
)
144+
145+
self.async_write_ha_state()
146+
147+
@property
148+
def extra_state_attributes(self) -> dict[str, Any]:
149+
"""Return entity specific state attributes."""
150+
attrs = {"source": "Mitsubishi AC"}
151+
external_entity_id = self._config_entry.options.get(CONF_EXTERNAL_TEMP_ENTITY)
152+
if external_entity_id:
153+
attrs["external_temperature_entity"] = external_entity_id
154+
# Get current value from external entity
155+
state = self.hass.states.get(external_entity_id)
156+
if state and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN):
157+
try:
158+
attrs["external_temperature"] = float(state.state)
159+
except (ValueError, TypeError):
160+
attrs["external_temperature"] = None
161+
return attrs

custom_components/mitsubishi/strings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@
3333
"admin_username": "Admin Username (default: admin)",
3434
"admin_password": "Admin Password (default: me1debug@0567)",
3535
"scan_interval": "Update Interval (seconds, 10-300)",
36-
"enable_capability_detection": "Enable capability detection"
36+
"enable_capability_detection": "Enable capability detection",
37+
"external_temperature_entity": "External Temperature Sensor"
38+
},
39+
"data_description": {
40+
"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."
3741
}
3842
}
3943
},

0 commit comments

Comments
 (0)