This is a Home Assistant integration for Mitsubishi Heavy Industries air conditioners that use the WF-RAC WiFi module and the "Smart M-Air" app.
Not compatible with Mitsubishi Electric systems (e.g. those using a MAC-577IF2-E interface) or the MELCloud platform — those are a different manufacturer with a different app and protocol. If your unit uses MELCloud, see Home Assistant's built-in MELCloud integration instead.
Any Mitsubishi Heavy Industries air conditioner that ships with the WF-RAC WiFi module — the
network interface controlled through the Smart M-Air app — should work. The integration talks
to the module's local HTTP API rather than to a specific indoor/outdoor unit model, and it probes
both plain HTTP and HTTPS on setup, so it doesn't matter which of the module's firmware branches
(WF-RAC, WF-RAC-HTTPS, WCBN4612L) yours happens to run.
Confirmed working on a SRK20ZS-WF + SRK35ZS-WF multi-split on an SCM45ZS-W outdoor unit. Some
entities are conditional on what the unit itself reports supporting — Occupancy and the Home Leave
Mode entities, for example, only appear on units that report the corresponding capability, and a
few diagnostic sensors depend on the model-identifier byte the unit sends back. An unsupported
feature simply doesn't create its entity, rather than failing.
Created by @jeatheak. In July 2026, jeatheak transferred ownership of this repository to @blues-sechseck, who continues to maintain it. Thanks, jeatheak, for building this in the first place!
This integration is developed with AI assistance. Every change is reviewed by me and tested on real hardware before it ships; bugs are mine, not the tool's.
Since 2026.8, the fan_mode/swing_mode/swing_horizontal_mode state values were renamed to snake_case (e.g. "Up/Down Auto" → "up_down_auto", "Quiet" → "quiet", "3D Auto" → "3d_auto") to satisfy Home Assistant's own validation rules — the old capitalized values were never actually valid. If you have automations, scripts, or dashboards that call climate.set_fan_mode, select.select_option, or the set_horizontal_swing_mode/set_vertical_swing_mode services with the old capitalized strings, update them to the new lowercase values. See the 2026.8 release notes for the full list.
Install using HACS
This integration is part of the HACS default list — no custom repository needed. In HACS, go to Integrations, search for "Mitsubishi WF-RAC", and install it from there.
Needs Home Assistant 2026.4 or newer. That is the oldest release the test suite runs against, so it is a version this has actually been shown to work on rather than one it is merely hoped to.
Already installed? Jump straight to setup:
Clone or copy this repository and copy the folder custom_components/mitsubishi_wf_rac into
/custom_components/mitsubishi_wf_rac.
Units on the same network are found by themselves and show up as discovered devices — confirm one and you are done. Adding one by hand asks for the same details:
| Field | Description |
|---|---|
| Host (IP) address | The local IP address of the airco's wireless module. A changed address is picked up when the module announces itself again; give the module a fixed address in your router, or correct it here via Reconfigure. |
| Port | The port the module's local API listens on, 51443 on every firmware branch seen so far. Discovery fills this in; correct it only if your module announces something else. |
| Ignore duplicate IP address | Off by default. Adds the airco even though another entry already uses that IP address — meant for re-adding a unit whose old entry went missing. The module accepts one connection at a time, so two entries polling it produce errors in the log. |
Discovery asks only for the port; the address is the one the module announced. The setup connects to the airco right away and registers Home Assistant as an operator on it, so the unit has to be reachable at that moment.
The entry is named after the unit — WF-RAC plus the last four characters of the airco id, which
match the label on the module. Rename it whenever you like: use Home Assistant's own rename on the
device or the entry, and the device name follows. Everything else is configured afterwards under
Configure (see Options).
This integration follows standard integration removal — no extra steps (like disabling a cloud account) are required.
- Go to Settings > Devices & Services.
- Find the Mitsubishi WF-RAC integration and select it.
- Select the three-dot menu next to the entry, then Delete.
If you installed manually rather than through HACS, also delete the
custom_components/mitsubishi_wf_rac folder and restart Home Assistant.
This integration creates one device per airco with the following entities.
| Entity | Attribute | Available values | Description |
|---|---|---|---|
climate.<name> |
hvac_mode |
off, auto, cool, heat, dry, fan_only |
Operating mode of the unit. |
fan_mode |
auto, quiet, low, medium, high |
Fan speed. quiet is the lowest of the four steps, not the remote's ECO setting - ECO can't be set or read through the WLAN module, and while the remote has it running the unit reports the lowest step. |
|
swing_mode |
up_down_auto, highest, middle, normal, lowest, 3d_auto |
Vertical louver position. 3d_auto hands vertical and horizontal swing over to the unit's own automatic mode. |
|
swing_horizontal_mode |
left_right_auto, left_left, left_center, center_center, center_right, right_right, left_right, right_left, 3d_auto |
Horizontal louver position. 3d_auto behaves as above. |
|
target_temperature |
16–30 °C (cool), 18–30 °C (other modes) | Setpoint. Cooling accepts a lower minimum than heating/auto/dry in practice; heating below 18 °C isn't a reliable plain setpoint (see Home Leave Mode for that instead). With a Target Temp. Offset set, the offered range moves with it - see the sign convention below. | |
current_temperature |
°C | Indoor temperature as measured by the unit, corrected by the "Indoor Temp. Sensor Offset" option if set. | |
preset_mode |
none, away |
Only on units that report the Vacant bit. away is the unit's own Home Leave Mode, entered with the away-target of the direction the unit is currently running in - so it needs cool or heat, and refuses in auto/dry/fan_only. The Home Leave Mode select names the direction explicitly and works from any mode. |
|
target_temperature step |
0.5 °C | The setpoint travels the wire in 0.5 K steps; anything finer is truncated by the unit. | |
hvac_action |
off, idle, cooling, heating, drying, fan |
What the unit is actually doing right now. idle means the unit is on but the compressor is stopped (e.g. setpoint satisfied) - same signal as the Compressor Demand binary sensor below. In auto mode, cooling/heating reflects the unit's own cool/heat decision, not just the configured mode. |
The diagnostic operation-data sensors below are disabled by default. Enabling a sensor makes the integration request its value; leaving all of them disabled makes no extra request at all - unless an external temperature override is armed, which asks for one segment on its own behalf (see External temperature override).
Any one of them switches the request on, so pick one that always has something to show. Indoor Coil Temperature is the safest choice: it is per indoor unit and reads a temperature whatever the system is doing. Compressor Frequency is the more telling value where it works, but reads a constant 0 on older firmware (#207). Hot Gas Temperature is a poor first choice: it reads unknown whenever the outdoor unit is idle, which looks like a broken sensor rather than a resting one.
| Entity | Values | Description |
|---|---|---|
| Indoor Temperature | °C | What the unit reports as the room temperature, exposed as its own sensor. Normally the same value as the climate entity's current_temperature; while an override is in effect the two part company, because this one keeps following the unit while the climate entity shows the temperature the unit was handed (see Indoor temperature override). |
| Outdoor Temperature (shared on multi-split) | °C | Outdoor unit temperature, corrected by the "Outdoor Temp. Sensor Offset" option if set. On multi-split systems this is an outdoor-unit-level value - reads identically on every indoor unit sharing one outdoor unit, since there's only one outdoor sensor. |
| Target Temperature (disabled by default) | °C | Current setpoint, exposed as its own sensor. Off by default because the climate entity already carries the same value as its target_temperature attribute. |
| Energy Usage (current run) | kWh, increasing | Energy consumption of the current run, as reported by the unit in 0.25 kWh steps. The unit clears this counter to 0 every time it is switched on, and holds the last value while it is off — so a low or zero reading is normal, not a fault, and a run consuming less than 0.25 kWh reads 0 throughout. For a lifetime figure use Energy Usage Total below. Only created if the unit actually reports this value — not all models do. |
| Energy Usage Total | kWh, increasing | Lifetime total, accumulated by the integration from the counter above and kept across restarts. This is the one to put on the Energy dashboard. Reset it with the "Reset Energy Usage Total" button on the device page, or set it to a specific value with the mitsubishi_wf_rac.set_energy_total action (useful when carrying a figure over from an existing meter). Resetting it does not erase the history already recorded in Home Assistant's long-term statistics. Accuracy depends on run length: whatever a run consumes above its last completed 0.25 kWh step is never reported by the unit and cannot be accumulated, which costs about 0.125 kWh per run on average regardless of how long the run is. A unit cycling every half hour therefore totals noticeably low; one running for hours at a time is off by little. Use an external energy meter if you need exact figures on a frequently cycling unit. |
| Compressor Frequency (diagnostic, disabled by default, shared on multi-split) | Hz | Actual compressor speed, not just on/off. Outdoor-unit-level - identical on every indoor unit sharing one outdoor unit. On older firmware (mcu131/wireless010) this reads a constant 0 even with the compressor confirmed running (#207). |
| Compressor Frequency (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data value behind Compressor Frequency, useful for protocol work. Both bytes in this segment carry data. |
| Operating Current (diagnostic, disabled by default, shared on multi-split) | A | Compressor operating current. It has the same outdoor-unit-level sharing as Compressor Frequency and reads a constant 0 on older firmware (#207). |
| Operating Current (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data byte behind Operating Current, useful for protocol work. |
| Hot Gas Temperature (diagnostic, disabled by default, shared on multi-split) | °C | Compressor discharge (hot gas) temperature. It has the same outdoor-unit-level sharing as Compressor Frequency. Below about 41 °C the sensor carries no resolution — it only means "30 °C or colder" — so the value reads unknown there rather than a number, which is where an idle outdoor unit sits. The raw byte keeps the distinction. |
| Hot Gas Temperature (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data byte behind Hot Gas Temperature, useful for protocol work. |
| EEV Pulses (diagnostic, disabled by default) | pulses | Electronic expansion valve position, raw pulse count (0-255). |
| EEV Position (diagnostic, disabled by default) | % | Same value as EEV Pulses, linearly mapped to 0-255=0-100%. The real full-open pulse count is unknown, so treat this as relative, not calibrated - useful for comparing indoor units on the same system. |
| Indoor Coil Temperature (diagnostic, disabled by default) | °C | Indoor heat-exchanger temperature (MHI's THI-R1). Per indoor unit, not shared. In cooling it drops as the coil gets cold and rises back to room temperature once the compressor stops - the clearest signal there is for what the unit is actually doing. Works in heating too, where the coil is the condenser and runs to 45 °C and beyond. |
| Indoor Coil Temperature (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data byte behind Indoor Coil Temperature, useful for protocol work. |
| Indoor Coil Outlet Temperature (diagnostic, disabled by default) | °C | Indoor heat-exchanger sensor on the gas line (MHI's THI-R3). The name describes cooling, where this point is the evaporator's outlet; in heating the flow reverses and the same point sits closer to the coil's inlet. Also per indoor unit. Equal to Indoor Coil Temperature while the compressor is off; while it runs the difference between the two is the evaporator superheat. |
| Indoor Coil Outlet Temperature (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data byte behind Indoor Coil Outlet Temperature, useful for protocol work. |
| Outdoor Coil Temperature (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data byte behind the outdoor coil temperature, useful for protocol work. |
| Discharge Superheat (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data byte behind discharge superheat, useful for protocol work. |
| Protection Number (raw) (diagnostic, disabled by default) | unitless | Undecoded operation-data byte behind the protection number, useful for protocol work. No module tested here has answered this code, so it may remain unknown. |
| Humidity (disabled by default) | % | Relative humidity at the indoor unit's return-air sensor. Whether a unit has this sensor depends on the series: in the ZS-WF series only the SRK35 and SRK50, in the ZSX and ZT-WF series every size. Units without it answer a constant 30, which shows as unknown until the unit has reported any other value since Home Assistant started, as does 0 (a broken sensor wire). The reading comes from the intake, so while heating it is lower than a sensor in the room. On a multi-split this also applies to a unit that is switched off while another indoor unit heats: the idle unit's coil warms up and its reading drops. |
| Research data (beta releases only, diagnostic, disabled by default) | count | Operation-data codes whose meaning is not known yet, as raw bytes in the attributes (0xAB: "10 1E FF", with the time each was last seen). Lets testers with other models see what their unit answers. Asked for in the slots the regular operation-data codes leave free, never in a request of its own; codes a unit refuses are dropped for that unit. Final releases remove this sensor again, and the next beta brings it back. The diagnostics download carries the same data. |
| Airco ID (diagnostic, disabled by default) | text | Internal ID of the airco. |
| Operator ID (diagnostic, disabled by default) | text | Internal operator/account ID. |
| Device ID (diagnostic, disabled by default) | text | Internal device ID. |
| IP (diagnostic, disabled by default) | text | Local IP address of the WF-RAC module. |
| Accounts (diagnostic, disabled by default) | number | Number of app accounts currently connected to the unit. |
| Error (diagnostic) | error code | Raw error code reported by the unit; 00 means no error. |
| Updated By (diagnostic) | text | Which account last changed the unit's settings (this integration or the Smart M-Air app). |
| Account Expires (diagnostic, disabled by default) | text | Expiry of the current operator session. |
| LED Status (diagnostic, disabled by default) | text | State of the unit's status LED. |
| Auto Heating (diagnostic) | text | State of the unit's automatic heating assist. |
| Model Nr (diagnostic, disabled by default) | number | Raw model-identifier byte reported by the unit. Used to gate which optional features (occupancy, Home Leave) are exposed; mostly useful for diagnosing unsupported models. |
| Cool Hot Judge (diagnostic, disabled by default) | cooling, heating |
Raw cool/heat state reported by the unit's compressor, independent of the configured mode. unknown while off or in fan_only. Useful for detecting the "wait/hold" state on multi-split systems where one indoor unit is blocked because the outdoor unit is already committed to the opposite mode for a sibling unit. |
| Entity | Values | Description |
|---|---|---|
| Problem | on/off | On whenever the unit reports an error code (error_code attribute holds the raw code; error_description is added when the code is documented in the MHI service/user manuals). |
| Occupancy | on/off | Only created on units that report the "Vacant"/Home Leave bit (see Home Leave Mode below). This is not a physical presence/motion sensor - it just mirrors that bit, which is off unless Home Leave mode was actually entered. It will read "occupied" even in an empty room if Home Leave was never triggered. |
| Compressor Demand | on/off | Whether this indoor unit is currently calling for the compressor, as opposed to just being powered on (e.g. off while a setpoint is already satisfied). Comes from the same status poll as every other sensor - no extra request needed. On a single-split system that is the same thing as the compressor running. On a multi-split it is not: each indoor unit reports its own demand, so one can read "on" while a sibling on the same outdoor unit reads "off" at the same moment, and this sensor can go "off" while the shared compressor keeps running for the sibling. To tell whether the outdoor unit is running at all, either check this sensor across every indoor unit, or use Compressor Frequency, which is identical across all indoor units sharing one outdoor unit. |
| Defrosting (diagnostic, disabled by default, shared on multi-split) | on/off | Whether the outdoor unit is defrosting its coil. It is the outdoor unit's state, so an idle indoor unit on the same multi-split shows it as well. The unit is only asked while a defrost is possible, i.e. the compressor is running (Compressor Frequency above 0) and the outdoor air is below 8 °C; outside that the sensor reads off without asking. While it is possible, the reading comes with the other operation data about every two minutes and turns unknown if it stops arriving. Not confirmed on a real defrost yet; please report what you see. |
| Entity | Values | Description |
|---|---|---|
| Firmware Update (opt-in) | on/off | Reports whether newer WF-RAC module firmware is available, by comparing the version reported locally against the manufacturer's getFirmware endpoint. Only created if "Check for firmware updates" is enabled in the integration's options - off by default, since it's the only call this integration makes outside the local network. Read-only; installing an update isn't offered here. |
| Entity | Values | Description |
|---|---|---|
| Silent operation (disabled by default) | on/off | The unit's own silent operation: it limits the outdoor unit's compressor and fan to make it quieter, at the cost of maximum capacity. The state is what the unit reports, read on every operation-data request, so the switch stays unavailable on a unit that never answers. A change is only accepted once the unit confirms it. The IR remote can switch it as well, and a state set there may only clear with the remote. Mitsubishi's documentation is not consistent on multi-split systems, and on mine I could not measure a difference while heating; please report what you see on yours. |
The unit's own frost-protection/low-power standby mode for when nobody's home, with independent cooling and heating away-targets. Only created on units confirmed to support it.
| Entity | Values | Description |
|---|---|---|
| Home Leave Mode (select) | off, away_cool, away_heat |
Enters/leaves Home Leave mode in either direction. |
climate.<name> preset_mode |
none, away |
The same mode as a thermostat preset, for cards and voice assistants. Entering it uses the direction the unit is running in; the select above is the way to name it. |
| Home Leave Cooling/Heating Temp Rule (number, disabled by default) | 10–50 °C | Outdoor/room temperature threshold at which Home Leave engages for that mode. |
| Home Leave Cooling/Heating Temp Setting (number, disabled by default) | 10–50 °C | Target temperature while Home Leave is active for that mode. |
| Home Leave Cooling/Heating Airflow (select, disabled by default) | auto, 1–4 |
Fan speed while Home Leave is active for that mode. |
The number/select entities above stay unknown until the climate entity's "Request Home Leave Mode status" action has been called once - the unit omits these values from a plain poll otherwise. Writing to them before that is refused rather than guessed at. See the request_home_leave_mode_status/set_home_leave_mode climate actions.
These duplicate the climate entity's swing/fan attributes as standalone entities, useful for dashboards or automations that prefer a plain select over a climate attribute. Installations that declined them during setup get them as disabled entities, to be enabled from the entity list when wanted.
| Entity | Values | Description |
|---|---|---|
| Horizontal Swing Direction | same as swing_horizontal_mode above |
|
| Vertical Swing Direction | same as swing_mode above |
|
| Fan Speed | same as fan_mode above |
The integration polls the WF-RAC module directly over the local network every 60 seconds — there is no cloud, push, or webhook involved. A single poll reads the unit's full state in one request (mode, setpoint, temperatures, energy counter, and so on). The diagnostic operation-data sensors described above (compressor frequency, coil temperatures, EEV position, etc.) cost one additional request every second poll, made only for the segments an enabled sensor actually needs, and not at all if none of them are enabled.
That second request is why those sensors update every two minutes rather than every minute: the module grants whoever last sent it a command 60 seconds of exclusive control, and asking for operation data counts as one. Requesting it every minute would hold that grip permanently and leave the Smart M-Air app unable to control the unit at all. On the same principle the integration stops asking for a few minutes when it notices another client controlling the unit — the External Control sensor shows while that is happening, and the operation-data sensors hold their last values meanwhile.
The only outbound internet request this integration ever makes is the optional firmware-version check ("Check for firmware updates" under Options, off by default) — everything else, including every poll and every command, stays on the local network.
- Whole-home climate scheduling and automations — set mode, fan speed, swing and setpoint like
any other
climateentity, from automations, scripts, or dashboards. - Energy dashboard tracking — feed Energy Usage Total into Home Assistant's built-in Energy dashboard for a lifetime consumption figure per unit.
- Presence-based energy saving — drive Home Leave Mode from a
person/zone trigger instead of a plain schedule, so the unit throttles back to a frost-protection setpoint while everyone's away and returns to normal the moment someone gets home. - Reacting to what the compressor is actually doing — Compressor Demand and
hvac_actiondistinguish "on but idle, setpoint satisfied" from "actively heating/cooling", useful for automations that should only fire while the unit is genuinely running. - Diagnosing multi-split behaviour — the diagnostic operation-data sensors (compressor frequency, indoor coil temperature, EEV position) make short cycling, an oversubscribed outdoor unit, or a struggling indoor unit visible in history graphs, without a service call or the app.
Set Home Leave Mode automatically when the last person leaves, back to normal when someone returns:
automation:
- alias: "AC: enable Home Leave Mode when everyone's away"
trigger:
- trigger: state
entity_id: zone.home
to: "0"
action:
- action: select.select_option
target:
entity_id: select.<name>_home_leave_mode
data:
option: away_cool
- alias: "AC: back to normal when someone gets home"
trigger:
- trigger: state
entity_id: zone.home
from: "0"
action:
- action: select.select_option
target:
entity_id: select.<name>_home_leave_mode
data:
option: "off"Notify when the unit reports a fault:
automation:
- alias: "AC: notify on error"
trigger:
- trigger: state
entity_id: binary_sensor.<name>_problem
to: "on"
action:
- action: notify.notify
data:
message: >-
{{ state_attr('binary_sensor.<name>_problem', 'error_description')
or state_attr('binary_sensor.<name>_problem', 'error_code') }}Community automation blueprints live in blueprints/. They are not part of
the download - HACS installs the integration folder only - so you import one yourself under
Settings -> Automations & scenes -> Blueprints -> Import blueprint, pasting the URL of the
.yaml file. See the directory README for what is in there and how
to contribute one.
Configurable via the integration's "Configure" (options) flow. The host/IP address itself isn't here - it's connection-critical, so changing it goes through "Reconfigure" instead, which re-validates the new address against the device before saving it.
The dialog groups these into three sections: Indoor temperature source (the source sensor and its three overshoot corrections), Setpoint offsets (what gets sent to the unit) and Sensor offsets (what Home Assistant shows). The overshoot fields only appear once a source is picked and saved - without one there is no room temperature for them to correct.
| Option | Range | Description |
|---|---|---|
| Retry limit | 3 or higher, default 3 | Consecutive failed polls before the device is marked unavailable. At the 60 s poll interval, 3 is about 3 minutes - enough to ride through the module's hourly WiFi reassociation. Raise it on a weak link; it cannot be set lower. |
| Indoor Temp. Sensor Offset | -15..15 °C | Added to the unit's own indoor-sensor reading before it's shown as current_temperature / the Indoor Temperature sensor - display-only, doesn't change what the unit does. Suspended while an external temperature override is in effect: the unit is reporting the value it was given rather than measuring, so there is nothing to calibrate. |
| Outdoor Temp. Sensor Offset | -15..15 °C | Same, for the Outdoor Temperature sensor. |
| Target Temp. Offset | -5..5 °C | Calibrates the setpoint sent to the unit - see "Target Temp. Offset sign convention" below. Applies to every hvac_mode unless overridden by the two options below. |
| Target Temp. Offset (Cooling) | -5..5 °C, unset by default | Overrides Target Temp. Offset for cool and dry mode. Leave unset to keep using Target Temp. Offset for those modes too. |
| Target Temp. Offset (Heating) | -5..5 °C, unset by default | Overrides Target Temp. Offset for heat mode. Leave unset to keep using Target Temp. Offset for heat too. |
| Cooling overshoot | -5..5 °C, in steps of 0.25 | How far past your setting the room actually goes before the unit stops. Applies in cool mode only. Set 22 °C, room settles at 21.5 °C: enter 0.5. Quarter degrees are the finest step that reaches the unit - the room temperature it is fed is carried in 0.25 °C steps. Only shown, and only has an effect, while an Indoor temperature source is configured. |
| Dry overshoot | -5..5 °C, in steps of 0.25 | The same for dry, which cools as well and takes the same sign. Opens on 0: the one unit measured in this mode landed on its setting. |
| Heating overshoot | -5..5 °C, in steps of 0.25 | The same for heat, and in that mode only: how far above your setting the room ends up. Both fields take a positive number for a room that goes past the setting, but the direction of the correction follows the mode: cooling hands the unit the room temperature minus the figure, heating hands it the room temperature plus the figure. Opens on 0, although the one unit measured needs 2.5 to 3 - see "Heating needs a much larger figure" below. |
| Check for firmware updates | on/off, off by default | Creates the Firmware Update entity (see Update above) and periodically checks the manufacturer's getFirmware endpoint. The only outbound internet call this integration makes - leave off to stay fully local. |
The unit's internal temperature sensor is a return-air sensor built into the indoor unit, not a sensor sitting where you actually care about the temperature. It reads a biased version of the room - but which direction, and by how much, depends on your installation, not on cooling vs. heating alone:
- Short-circuited airflow: the unit's own outflow gets pulled straight back into the return before it mixes into the room. In cooling this reads below the true room temperature.
- Stratification: a high wall mount and a low fan speed let conditioned air pool near the ceiling instead of mixing down to where you live. In cooling this reads above the true room temperature - the opposite of the case above, and just as real. Fan speed matters here: on one multi-split installation, the unit capped at a low night-time fan speed showed a markedly larger bias than a sibling unit running medium/high in the same house.
There's no way to predict which case applies to your unit from its mode alone - you have to measure.
Target Temp. Offset corrects for this bias: true_room ≈ PresetTemp + offset. To land the room on the temperature you actually requested, the setpoint sent to the unit is commanded PresetTemp = requested − offset. Concretely: a negative offset raises the setpoint actually sent to the unit (a positive offset lowers it).
The offered setpoint range moves with the offset, so the bounds shown are the values you can actually ask for. With a +1 °C offset in cooling the card offers 17–31 °C: 17 goes out as 16, the lowest the unit takes, and reads back as 17. In off and fan_only the card spans every regulating mode, each shifted by its own offset - the setpoint there applies to whichever mode is turned on next.
Measuring it: place a reference sensor away from the unit's own airflow, then average current_temperature (the Indoor Temperature sensor) minus that reference, split by the climate entity's hvac_action. Use the average while cooling (or heating) as your starting point for Target Temp. Offset (Cooling) / (Heating) - that's the state the thermostat loop actually regulates in, and it's not interchangeable with idle or off: on the installation above, the same unit's average bias moved by more than a kelvin between cooling and off. This is also why no single value is correct for both cool and heat at once, and why the offset isn't a fixed mounting/calibration error you can look up - it calibrates your installation's operating regime, and only a measurement of your unit, in the state it's actually controlling in, gets it right.
These are entity services (use as mitsubishi_wf_rac.<service>), so each one is called with a
target. All but the last target the climate entity.
| Service | Fields | Description |
|---|---|---|
set_horizontal_swing_mode |
swing_mode |
Set the horizontal (left/right) louver position. |
set_vertical_swing_mode |
swing_mode |
Set the vertical (up/down) louver position. |
request_home_leave_mode_status |
— | Ask the unit to report its Home Leave Mode thresholds/airflow (only on supporting models). |
set_home_leave_mode |
temp_rule_cooling, temp_setting_cooling, air_flow_cooling, temp_rule_heating, temp_setting_heating, air_flow_heating |
Write new Home Leave Mode thresholds/airflow (only on supporting models). |
set_external_temperature |
temperature (optional) |
Provide a room temperature to the AC, replacing the one its own indoor sensor reads. Omit temperature or pass null to revert to the internal sensor. See below. |
set_energy_total |
value |
Targets the Energy Usage Total sensor, not the climate entity. Sets that total to a given figure in kWh — 0 to reset it, or a reading carried over from a meter you kept previously. Only the integration's own total; the unit's own counter cannot be written. |
set_external_temperature hands the unit a room temperature measured out in the room, and it regulates on that instead of the one its own return-air sensor reads.
The value is not a setting the unit stores under a flag of its own. It has no set-bit, which means it can only travel inside a frame sent for some other reason - and any frame that leaves it out sends the unit back to its internal sensor. Three things follow from that.
- The action arms the value, it does not send it. What carries it is the operation-data request, which the integration starts making for as long as an override is armed - the same request an enabled diagnostic sensor triggers, asked for on the override's own behalf, so nothing has to be switched on by hand. The value reaches the unit within a minute, or sooner if a command goes out for another reason in the meantime. Clearing takes effect the same way: the next frame carries "internal sensor" again.
- Nothing is written just for the override. A frame carrying it also re-asserts power, mode, fan speed, setpoint and both louver axes, and it takes the unit's 60-second write lock. Sending one per sensor reading would end any running self-clean cycle and keep the official app locked out for as long as the override is in use.
- While the unit is off or in
fan_only, the override is not written. Neither mode regulates on a room temperature, and a request carrying the value ends a self-clean cycle started from the remote - self-clean runs from the off state. The unit falls back to its own sensor there, and the first frame after it returns to a regulating mode writes the value again.
The Indoor temperature override sensor (diagnostic) says whether the unit is actually regulating on your value right now, so an override that is armed but not yet in effect - after a restart, or while the unit is off - is visible rather than something to deduce.
You can select a sensor under Indoor temperature source in the integration options. The integration reads that temperature sensor immediately after setup and follows its state changes itself, converting its unit to °C and rounding to the protocol's 0.25 °C steps. If the source becomes unavailable, unknown, or disappears, it clears the override on the next frame so the unit returns to its internal sensor. When a source is configured, use of set_external_temperature with a value is refused to avoid two competing writers; calling it without temperature still clears the override, though only until the source reports again. The source cannot be one of this integration's own temperature sensors: while an override is armed those report the injected value back, so feeding one in would walk the override away from the room. Without a configured source, automations using the action remain supported as before. Clearing that option hands control back: the value it had armed is dropped rather than restored, and the unit is back on its own sensor.
The unit has no timeout of its own. Whatever room temperature it last received is what it regulates on, until another frame replaces it or it loses power. While Home Assistant is running that is covered - the value is refreshed every poll cycle, and a source that goes unavailable or unknown clears the override within a cycle. It does mean the arrangement rests on your source sensor saying when it stops measuring. A battery sensor that simply goes quiet, or a REST or template sensor that keeps serving its last reading, leaves the unit regulating on a number that has stopped being true - and if the room then cools, it heats against a reading that never moves.
Most integrations already handle this: ESPHome, Z-Wave JS and ZHA mark a device unavailable once it stops answering. Where yours does not, give it an availability rule of its own - expire_after on an MQTT sensor, or a template sensor wrapping the original and going unavailable when last_updated falls behind. Anything that produces unavailable hands the unit back to its internal sensor.
Which sensor. What matters is how often it reports, not which radio it uses. The unit acts on a reading that moves within minutes, so a sensor that reports every 15 minutes lets the room run past the setting before the unit hears of it - one reader measured about a kelvin either way on a slow sensor, and the same unit held the room within half a kelvin on a fast one. Anything reporting every minute or two is fine; Bluetooth thermometers and WiFi sensors usually do, Zigbee and Thread sensors are usually held back by their own reporting interval rather than by the radio. A Shelly H&T Gen3 on USB power wakes only every five minutes, on battery it reports within a minute of a change. The value reaches the unit with the next frame, within the poll interval, and the unit's own control re-evaluates the room every few minutes anyway, so there is nothing to gain below that. The sensor has to carry device_class: temperature to appear in the picker - MQTT and template sensors sometimes lack it, and one line fixes that.
Shutting Home Assistant down, disabling the integration or removing it hands the unit back; losing Home Assistant does not. An orderly stop spends one last frame clearing the override, so the unit measures for itself while Home Assistant is away and takes the override up again when it returns. A reload does not do this: it re-arms the override seconds later. An abrupt end cannot do that: a host that loses power or a network that disappears sends nothing, and the unit keeps the last value it was given until something else writes or it is switched off at the wall. Worth weighing before arming an override for a room that is heated while nobody is home.
What an armed override costs is one request per poll cycle, which holds the unit's write lock for part of the cycle exactly as an enabled operation-data sensor does.
Units that act on the operation-data request. On a unit that switches off or changes its settings when it is asked for operation data (observed on two modules), the integration stops sending that request and raises a repair. The override cannot ride on that request either, so the unit is handed back to its own sensor, an Indoor temperature source is no longer passed to it, and the climate entity shows the temperature the unit measures. The detection can be triggered by someone using the remote at the same moment; confirming the repair makes the integration ask again.
While an override is in effect, the unit stops reporting its own sensor. Measured on hardware: the value you inject comes back on the next cycle as the unit's reported room temperature, overshoot correction included - the two protocol fields it travels in carry the same byte. So the Indoor Temperature sensor follows your override as well and is no longer a measurement of the room; the climate entity shows the value you supplied instead, and your own sensor remains the measurement. Clearing the override brings the real reading back within a cycle.
An action-driven override survives a restart and a reload. It is re-armed, not re-sent: the unit stays on whatever it last received until the next frame goes out. With a configured source, its current state is used instead of a restored value. The Indoor Temperature sensor shows what the unit reports throughout, so it keeps agreeing with the official app. current_temperature does not: with an Indoor temperature source configured, the climate entity shows that sensor's reading - including while the unit is off or in fan_only, where nothing is written and the unit's own reading means least. An override armed from an automation instead of a source is only shown once the unit is actually using it: there is no sensor behind it, so before then it is an intention rather than a measurement.
With a room temperature supplied, most units cool the room a little further than asked before stopping - about half a kelvin, measured across four different models, and the same figure repeats every cycle. That is the unit's own thermostat band, not a sensor error: its return-air sensor is out of the loop while it regulates on your value. Heating goes considerably further, see below.
Until 2026.9.10 the figure looked like a full kelvin. The room temperature reached the unit half a kelvin warm: the library encoded it with a constant taken from the SPI-bus projects, while the unit and the official app read the byte through the manufacturer's own table, which sits half a kelvin higher across the whole living range. That is also why the app used to show 26.5 for a sent 26.0. The encoding now follows the table, and a stored overshoot figure is shifted by that half kelvin on upgrade, so the unit receives exactly what it received before; only the number in the field changes, and a field at 0 stays at 0. Someone feeding the value through the action without a source gets the corrected reading and nothing to shift.
Set Cooling overshoot to how far it goes: aim for 22 °C, watch where the room settles, and enter the difference as a positive number. The field opens on 0.5 because that is what the units measured so far need; it is a starting point, not a measurement of yours, and it only takes effect once you save the options. If your unit stops short instead and never quite gets there, a negative number corrects that the other way. The integration then hands the unit a room temperature that much lower, so the unit reaches its own stopping point exactly when your room is on target. Your setpoint and everything shown in Home Assistant stay the number you asked for.
Which modes it corrects: cool, dry and heat, each with its own figure. They are separate
fields rather than one shared between the modes that cool, because the band being corrected is a property
of how the unit runs, and dry runs a different airflow. Note that this is not the same grouping as the
setpoint offsets above, where the cooling override covers dry as well - worth knowing before reading a
difference between the two as a fault.
Dry opens on 0 where cooling opens on 0.5, and that is a measured figure rather than a placeholder: the one unit measured in dry landed on its setting, with a band about three times as wide as in cooling - the compressor stops 1.5 K below the setting and ramps 1.5 K above it. That is how the mode works, not something a correction should narrow. If yours does settle past the setting, finding the figure takes one evening and nothing extra: set a target it can reach, leave the room alone until the compressor is cycling rather than ramping, and read your source sensor. The gap between that reading and your target is the number for this field.
Heating needs a much larger figure. Measured on an SRK20ZS-WF with a room sensor as source: over five cycles the unit stopped heating once the room temperature it was fed stood 2.5 to 3 K above the setting. Most of that is the 2 °C the unit adds to every heating setpoint (see "Heating: the unit adds 2 °C of its own" below), the rest is the same band as in cooling. On that unit the stopping points put the Heating overshoot at 2.75, with none of the five more than a quarter degree from it. The field still opens on 0: one unit is not a default, and a unit with the compensation switched off should need about 1 instead - not measured yet. Find your figure the same way as for dry, with a target the unit can reach and a few full cycles.
If a figure is already set, correct from there - there is no need to go back to 0 and measure again. If the room still goes past your setting, raise the figure by the remaining gap; if it now stops short, lower it by that much. "Past" means below the setting in cooling and dry, above it in heating. For example, heating with 3 set and the room settling 0.5 K below your setting: it stops short, so enter 2.5.
auto is not corrected at all. Which direction it is running in is the unit's own cool/heat decision,
and some units never report it - so there is nothing to hang the sign of a correction on. A correction
would not help there anyway, which is worth knowing before you go looking for one.
In heat the unit does not regulate to the number you set but to that number plus 2 °C - the setting-temperature correction in MHI's service documentation, and the factory state on the units checked. With the return-air sensor near the ceiling it roughly cancels that sensor's warm reading. With a sensor placed where you sit it does not: the room ends about 2 K above your setting with Compressor Demand still on, as reported on an SRK-ZS-WF set to 22 °C with the room at 24.8 °C.
Three ways out. With an Indoor temperature source at seating height, switching the compensation off is the recommended one: the sensor already measures where you sit, so there is nothing left for the 2 °C to correct. The service manuals for the ZS-WF, ZSX-WF and ZT-WF series document it at the indoor unit, under "Countermeasure for excessive temperature rise": with the unit powered and having run at least once since, hold the indoor unit's ON/OFF button for 30 seconds or more until it beeps twice. The same hold, answered by three beeps, switches it back on. The ZTL and ZR-WF manuals do not list the procedure. Or leave the unit as it is and set Target Temp. Offset (Heating) to 2, which lowers the setting sent to the unit by that much. With an Indoor temperature source configured, Heating overshoot covers the same 2 K in quarter degrees and leaves the setting as the official app shows it - use one of the two for it, not both. Both are the way to go where the switch is not documented, or where you would rather keep the unit as it left the factory: if the source drops out, the unit falls back to its own return-air sensor, and that is the case the compensation was made for.
Nothing on the wire says which state a unit is in, so the integration cannot allow for it - and two units of the same model can disagree by 2 K on what a heating setpoint means. Worth knowing before comparing heating figures. Whether the switch also removes the 2 °C from auto below has not been checked.
auto is not a thermostat that holds your number. The service documentation for these units spells out
two things that together explain a room which never settles:
- Cooling is controlled to the temperature you set; heating is controlled to that temperature plus
2 °C (dry, plus 1). So
autoat 23 heats to 25 and cools to 23. - The direction changes only after the thermostat has been off for 20 minutes, and only once the room is more than 1 K from your setting: below it by more than 1 K it heats, above by more than 1 K it cools, and in between it keeps doing whatever it was doing. It will also not switch to heating at all while the outdoor temperature is 28 °C or above.
Those two combine into a loop. Heating ends 2 K above your setting, which is past the 1 K threshold, so the unit is guaranteed to change over to cooling; cooling ends below your setting by its own band, drifts further through the 20-minute wait, and changes back. Measured on a multi-split with 23 set: turning points at 24.8 and 20.8, about two hours a lap. This is the unit working as designed, not a fault, and no overshoot figure can narrow it - the correction shifts both turning points the same way, so it moves the whole swing without making it smaller.
If you want a room held closely, cool or heat does it: there the setting means what it says, the
overshoot correction applies, and the band is a fraction as wide. Choosing the direction is then an
automation's job.
Why here and not in Target Temp. Offset: on the units measured so far, a half-degree setpoint is rounded up to the next whole one, so that field is too coarse for a correction of less than a degree (owners of ZT and ZTL units report their models do take half degrees). The room temperature the unit is fed has 0.25 °C steps on every model, so correcting there is the finer of the two - and it leaves the setpoint matching what the official app shows.
While an override is in effect, the climate entity's current_temperature shows the room temperature you supplied and the Indoor Temperature sensor keeps showing what the unit reports - which is the value it was handed, minus any correction. The two therefore differ by exactly the correction, and how far apart they sit is not a measurement of anything. Without a correction they agree, and so does the official app.
- Self-clean cannot be started or monitored from Home Assistant. The official app has no way to trigger it either - the unit's self-clean cycle can only be started from its own IR remote, and nothing on the wire distinguishes "self-clean running" from "off" (see §6.4 of the protocol reference).
- The Firmware Update entity is read-only. It reports whether newer WF-RAC module firmware is available; installing it isn't offered through this integration - the module updates itself via the official app.
- Not every entity appears on every unit. Occupancy, Home Leave Mode, and a few diagnostic
sensors only get created if the unit itself reports support for the underlying feature (see the
notes under Entities above) - missing rather than
unavailableis expected there, not a bug. automode swings by several degrees, by design. The unit heats to 2 °C above your setting and cools to it, and only changes direction after 20 minutes off and more than 1 K of deviation - so a room inautocycles rather than settles (measured: 4 K peak to peak). Nothing in this integration can narrow that;coolorheatholds a room far more closely (see Whatautodoes with your setting above).- One device, one connection. The WF-RAC module handles requests one at a time. This integration already serializes its own requests to respect that, but running a second tool (a custom script, another integration instance, the Smart M-Air app at the same moment) against the same unit can still cause slow or occasionally failed responses on either side.
The WF-RAC module drops and re-establishes its WiFi association roughly once an hour. This is designed behaviour of the module, confirmed by MHI support under ticket reference 813958: the interface does "connect / disconnect every one hour … to avoid too much cache by the communication" (source). The reassociation takes seconds to about a minute and cannot be turned off.
This integration polls every 60 seconds, so a reassociation can cost a poll. That does not become a visible outage: the device is only reported unavailable after three consecutive failed polls, about three minutes, which rides through the reassociation. If your link is weak enough that this still shows up, raise Retry limit in the options; three is the minimum, not a target.
A missed poll is not logged as a warning either — it is a debug line, and the log only speaks up
when the device actually crosses the threshold and again when it comes back. Turn on debug logging
for custom_components.mitsubishi_wf_rac if you want to watch the individual polls.
This is the same hourly reassociation as above, but the module fails to re-bind port 51443
afterwards instead of coming back within a minute. The outage starts right on the hourly tick, not
at a random point in between, which is what distinguishes it from the WiFi roaming problem below.
There's no router setting that fixes this - it clears itself on the next hourly reassociation, so
it's a matter of waiting it out.
Outages in this range and starting at a random point (not on the hourly tick) are a network-side problem: the module mishandles WiFi roaming and steering management frames. Recommended setup:
- A dedicated 2.4 GHz-only SSID. The module is 2.4 GHz only, and on a shared SSID band steering tries to push it onto a band it cannot join.
- 802.11r (Fast Roaming) and 802.11v (BSS Transition / Handoff Suggestions) off on that SSID. Band steering is itself implemented via the same 802.11v frames.
- Plain WPA2, not WPA2/WPA3 mixed mode. This also matters during initial pairing.
- On UniFi, "Force WiFi 4 Mode" and "DTIM Interval Lock" under IoT Optimization are safe to enable.
- Blocking the module's outbound internet access at the router removes these outages in some setups; this integration only needs LAN access. The hourly reassociation continues either way.
If you are writing your own client against the WF-RAC module — or building a CNS/SPI replacement
such as an ESP32 running MHI-AC-Ctrl — docs/wf-rac-module-reference.md
documents the interface end to end: mDNS discovery, the HTTPS API and its envelope rules, the
airconStat blob, the 18-byte state block and how it maps onto the CNS/SPI frame, the operation-data
channel, and what the module deliberately does not forward. Every non-obvious claim carries a
confidence tag saying whether it was observed on hardware, read out of a firmware image, or inferred.
It is written for people outside this project, so nothing in it assumes Home Assistant.