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!
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.
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.
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. | |
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). | |
current_temperature |
°C | Indoor temperature as measured by the unit, corrected by the "Indoor Temp. Sensor Offset" option if set. | |
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.
| Entity | Values | Description |
|---|---|---|
| Indoor Temperature | °C | Same value as the climate entity's current_temperature, exposed as its own sensor. |
| 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. |
| 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 outlet, on the suction side (MHI's THI-R3). Also per indoor unit. Equal to Indoor Coil Temperature while the compressor is off; the difference between the two while it runs 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. |
| 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. |
| 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. |
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. |
| 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 per poll, made only for the segments an enabled sensor actually needs, and not at all if none of them are enabled.
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') }}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.
| 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. |
| 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. |
| 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).
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.
- 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).
- No external temperature sensor input. The unit always controls off its own built-in return-air sensor; there is currently no way to feed it a reading from an external Home Assistant sensor instead. The Indoor/Outdoor Temp. Sensor Offset options only correct what's displayed, not what the unit's own control loop uses.
- 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. - 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.