homebridge-xiaomi-air-purifier-modern
v1.0.7
Published
Modern Homebridge plugin for Xiaomi Mi Air Purifier (2H/3/3H/4/Pro)
Downloads
304
Maintainers
Readme
homebridge-xiaomi-air-purifier-modern
Modern, production-quality Homebridge plugin for Xiaomi Mi Air Purifier (2H / 3 / 3H / 4 / Pro).
Built from scratch in TypeScript on top of Node.js built-ins (no native dependencies, no external HTTP traffic), targeting Homebridge 2.x and current LTS Node.js (22.x / 24.x).
Features
| HomeKit Service | Description |
|-----------------|-------------|
| AirPurifier | Main purifier power ON/OFF. Uses the native AirPurifier service with Active, CurrentAirPurifierState, TargetAirPurifierState, and RotationSpeed characteristics. |
| Air Quality Sensor | AQI mapped to Excellent/Good/Fair/Poor + PM2.5 Density |
| Temperature Sensor | Current temperature |
| Humidity Sensor | Current relative humidity |
| Switch: Child Lock | Optional control (enableChildLockControl) |
| Switch: LED Night Mode | LED indicator on/off |
| Switch: Mode AUTO ON/OFF | Dedicated switch: ON=auto, OFF=sleep; unavailable while Power OFF |
| Switch: Mode NIGHT ON/OFF | Dedicated switch: ON=sleep, OFF=auto; unavailable while Power OFF |
| Filter Maintenance | filter1_life as filter life level + change indication |
| Contact Sensor: Filter Replace Alert | Optional extra sensor for filter replacement warning (exposeFilterReplaceAlertSensor, default false) |
How this plugin compares to other Xiaomi air-purifier plugins
| Capability | homebridge-xiaomi-mi-air-purifier (verified, last release 2020) | homebridge-xiaomi-air-purifier-modern (this plugin) |
|------------|-------------------------------------------------------------------|--------------------------------------------------------|
| Homebridge target | Homebridge 1.x | Homebridge 2.x (native AirPurifier service with Active / CurrentAirPurifierState / TargetAirPurifierState / RotationSpeed) |
| Node.js | Legacy (Node 10/12) | Node 22 LTS and Node 24 LTS |
| Implementation | JavaScript + abandoned miio dependency | TypeScript, Node dgram only, zero runtime deps |
| Supported models | 2 / 2S (zhimi.airpurifier.m1, m2) | 2H, 3, 3H, 4, Pro (zhimi.airpurifier.2h/3/3h/4/pro) — MIOT + legacy MIIO transports |
| Multi-device | Single accessory | Dynamic platform with multiple devices in one instance |
| Reconnect / resilience | Manual restart on failure | Exponential backoff with jitter + automatic UDP transport reset to recover from MIIO error -5001 stuck state |
| Mode handling | One switch, auto only | Separate AUTO / NIGHT switches with state sync after writes |
| Filter status | None | FilterMaintenance (life + change indication) + optional Filter Replace Alert contact sensor |
| Settings GUI | Minimal | Full config.schema.json with grouped fieldsets, validation, password-masked token field |
| Tests | None | 100 % coverage (statements / branches / functions / lines) on Vitest v4 + v8 |
| Maintenance | Last release 2020 | Active — semantic-release pipeline with GitHub releases per version |
This plugin is intended for users on Homebridge 2.x with the newer hardware (model 3 / 3H / 4 / Pro), where the legacy verified plugin does not provide working transport.
Requirements
- Homebridge 2.0.2 or newer (Homebridge 1.x is no longer supported)
- Node.js 22.x or 24.x (Node 20 reached end-of-life and is no longer supported)
- npm >=10.0.0
- Xiaomi Mi Air Purifier on the same LAN (UDP 54321)
- Device token (32-char hex)
Installation
Homebridge UI
Search for homebridge-xiaomi-air-purifier-modern in the Homebridge plugin store.
CLI
npm install -g homebridge-xiaomi-air-purifier-modernConfiguration
This is a Dynamic Platform Plugin. Add it under the platforms array with a devices list:
"platforms": [
{
"platform": "XiaomiMiAirPurifier",
"devices": [
{
"name": "Office Purifier",
"address": "10.10.1.17",
"token": "00112233445566778899aabbccddeeff",
"model": "zhimi.airpurifier.3h",
"connectTimeoutMs": 15000,
"operationTimeoutMs": 15000,
"reconnectDelayMs": 15000,
"keepAliveIntervalMs": 60000
}
]
}
]Configuration fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| platform | string | Yes | Must be "XiaomiMiAirPurifier" |
| name | string | Yes | HomeKit display name (per device) |
| address | string | Yes | LAN IP address |
| token | string | Yes | 32-character hex token |
| model | string | Yes | Xiaomi model identifier |
| enableAirQuality | boolean | No | Expose Air Quality Sensor service (default true) |
| enableTemperature | boolean | No | Expose Temperature Sensor service (default true) |
| enableHumidity | boolean | No | Expose Humidity Sensor service (default true) |
| enableChildLockControl | boolean | No | Expose Child Lock switch service (default false) |
| filterChangeThreshold | integer | No | Filter warning threshold in percent, warning is raised when filter1_life is at or below threshold (default 10) |
| exposeFilterReplaceAlertSensor | boolean | No | Adds optional HomeKit Filter Replace Alert contact sensor workaround for Home app visibility (default false) |
| connectTimeoutMs | integer | No | MIIO handshake timeout in milliseconds (default 15000) |
| operationTimeoutMs | integer | No | MIIO operation timeout in milliseconds (default 15000) |
| reconnectDelayMs | integer | No | Maximum reconnect backoff delay cap in milliseconds — first retry starts fast (~400 ms base), subsequent retries grow exponentially up to this ceiling (default 15000) |
| keepAliveIntervalMs | integer | No | Keep-alive poll interval in milliseconds (default 60000, min 1000) |
| operationPollIntervalMs | integer | No | Polling interval for control-related state refresh in milliseconds (default 10000, min 1000) |
| sensorPollIntervalMs | integer | No | Polling interval for slower sensor updates (temperature, humidity, AQI) in milliseconds (default 30000, min 1000) |
| transportResetThreshold | integer | No | After this many consecutive failed polls the plugin recreates the underlying UDP socket and MIIO session — equivalent to restarting Homebridge, but automatic. Set to 0 to disable. Default 12 (≈2 min at the default 10 s operation cadence). |
| transportResetCooldownMs | integer | No | Minimum delay between two consecutive automatic transport resets, in milliseconds. Prevents reset thrashing when the device is genuinely unreachable. Default 300000 (5 min). |
| maskDeviceAddressInLogs | boolean | No | Masks the device IP address in plugin logs (10.10.*.*). Enabled by default; set to false only if you need the full IP in logs |
| _bridge | object | No | Optional child bridge configuration |
Known model strings
| Model | String |
|-------|--------|
| Mi Air Purifier 2H | zhimi.airpurifier.2h |
| Mi Air Purifier 3 | zhimi.airpurifier.3 |
| Mi Air Purifier 3H | zhimi.airpurifier.3h |
| Mi Air Purifier 4 | zhimi.airpurifier.4 |
| Mi Air Purifier Pro | zhimi.airpurifier.pro |
Model / firmware support status
| Model | Firmware support level | Notes |
|-------|-------------------------|-------|
| Mi Air Purifier 2H (zhimi.airpurifier.2h) | Validated | Covered by integration-style read/write tests. |
| Mi Air Purifier 3 / 3H (zhimi.airpurifier.3, zhimi.airpurifier.3h) | Validated | Legacy + MIOT fallback paths validated. |
| Mi Air Purifier 4 / Pro (zhimi.airpurifier.4, zhimi.airpurifier.pro) | Validated | Primary MIOT mode and fallback paths validated. |
| Other zhimi.airpurifier.* variants | Best effort | Transport supports MIOT + legacy probing, but behavior may differ by firmware branch. |
Token extraction
The token is required for local LAN control and is different from your Xiaomi account password.
Never share your token publicly — it grants full LAN control of the device.
Recommended tools (community-maintained, well-known):
- Xiaomi Cloud Tokens Extractor — Python script that signs into your Mi Home account and prints tokens for all paired devices. The easiest method when you still have access to the Mi Home account that paired the device.
- python-miio —
miiocliCLI can read tokens stored locally and discover devices on the LAN (miiocli discover --handshake true). - Android — Mi Home backup parsing: install Mi Home 5.4.54 (older version that keeps tokens unencrypted), pair the device, then extract the SQLite database from
/data/data/com.xiaomi.smarthome/databases/miio2.db. - iOS — iMazing backup parsing: read tokens from the encrypted Mi Home backup using this guide.
- Packet capture during pairing (advanced) — Wireshark on the IoT VLAN during the initial Mi Home pairing flow.
The token must be exactly 32 hexadecimal characters (0-9, a-f). The plugin will reject any other format with a clear error in the Homebridge log.
HomeKit mapping details
AQI mapping
| AQI range | HomeKit AirQuality | |-----------|--------------------| | < 0 / NaN | UNKNOWN (0) | | 0–35 | Excellent (1) | | 36–75 | Good (2) | | 76–115 | Fair (3) | | 116–150 | Poor (4) | | > 150 | Inferior (5) |
PM2.5 Density
The Air Quality Sensor service exposes PM2_5Density set to the raw AQI value reported by the device's PM2.5 sensor (clamped to [0, 1000]). Note: the device reports an AQI index derived from its PM2.5 sensor, not a direct µg/m³ measurement. In practice the values are close (especially in the 0–500 range), and this mapping is the standard approach used across the Homebridge ecosystem for Xiaomi purifiers.
Rotation Speed and fan mode
Setting Rotation Speed in HomeKit automatically switches the device to favorite mode. This is required by the MIOT protocol for manual fan speed control. The mode change is reflected in the HomeKit Mode switches after the next state poll.
Mode switch (AUTO/NIGHT)
- Separate switches are exposed:
Mode AUTO ON/OFFandMode NIGHT ON/OFF. - When
Poweris OFF, mode writes are intentionally rejected by plugin logic (onSetignored and switch state refreshed from device); in HomeKit this behaves as non-accepting/unavailable control. - Polling and write-after-read sync keep HomeKit, plugin state, and device state consistent.
Filter life mapping
FilterLifeLevel=filter1_lifeFilterChangeIndication=CHANGE_FILTERwhen<= filterChangeThreshold(default10), otherwiseFILTER_OK- Optional:
ContactSensorStateonFilter Replace Alert=CONTACT_NOT_DETECTED(open = alert) when replacement is needed, otherwiseCONTACT_DETECTED(closed = normal) (only whenexposeFilterReplaceAlertSensor: true)
Default behavior keeps only FilterMaintenance to avoid duplicate warning presentation in Homebridge. Enable exposeFilterReplaceAlertSensor only if your Home app does not surface filter maintenance status.
Detailed resiliency and status scenarios (restart/reconnect, Wi-Fi outage behavior, and filter replacement signaling) are documented in docs/reliability-test-plan.md and automated in test/network-scenarios.test.ts.
Polling and reconnect
- Operational polling: every 10s
- Sensor polling: every 30s
- Exponential backoff with jitter on reconnect attempts
- Auto transport recovery: after
transportResetThresholdconsecutive failed polls (default12, ≈2 min), the plugin tears down the UDP socket and MIIO session and rebuilds them from scratch — automatically performing what would otherwise require a Homebridge restart. This recovers from device-side stuck states (e.g. firmware that keeps replyingMIIO error -5001until the source UDP port rotates), most common on olderzhimi.airpurifier.prounits. AtransportResetCooldownMs(default 5 min) prevents thrashing when the device is genuinely offline. MIIO error -5001and-10000are now retried with a tight cap of 2 attempts per call before propagating, instead of failing immediately on the first attempt.- Timers are cleaned on Homebridge shutdown
Every device-specific log line is prefixed with the configured device name
([<name>]), so with multiple purifiers you can always tell which one a message
refers to — this covers low-level transport and poll-failure diagnostics, not
just the connection lifecycle events:
[Office Purifier] Connected to "Office Purifier" @ 10.10.1.17!
[Office Purifier] Disconnected from "Office Purifier" @ 10.10.1.17 (code ETIMEDOUT): MIIO timeout after 15000ms
[Office Purifier] Reconnected to "Office Purifier" @ 10.10.1.17.
[Office Purifier] Device read failed (attempt 1, code ETIMEDOUT): MIIO timeout after 15000ms
[Office Purifier] Persistent device errors (12 consecutive failures, last code -5001) — recreating MIIO transport (new UDP socket, fresh handshake) to break stuck device-side state.Network hardening (recommended)
Because MIIO uses local UDP (54321) without TLS, treat purifier traffic as trusted-LAN only:
- Put IoT devices in a dedicated VLAN / SSID.
- Allow only Homebridge host ↔ purifier UDP 54321 in ACL/firewall rules.
- Block WAN egress from IoT VLAN when possible.
- Keep
maskDeviceAddressInLogsenabled (the default) when log forwarding goes to a shared SIEM or external support channels.
Device response integrity
Responses from the device are authenticated defensively before they are trusted:
- The token-keyed MD5 checksum on every reply is enforced fail-closed — a datagram whose checksum does not match is rejected (treated as a transient transport error and retried), never decrypted or acted upon. Only a party holding the device token can produce a valid checksum.
- The decrypted response id is correlated with the request id, so a captured/replayed reply belonging to a different request is not accepted.
- Only datagrams originating from the configured device address and MIIO port are accepted; packets from any other source on the LAN are ignored.
These checks reduce the risk of response spoofing/replay on an untrusted LAN, but they are defence-in-depth — network segmentation (above) is still the primary control.
Privacy and data flow
- The plugin communicates only with the configured purifier over LAN UDP 54321 (MIIO/MIOT). No data is sent to the Xiaomi cloud, no telemetry, no analytics, no error reporting to any external service.
- Runtime dependencies: zero — the published package depends only on Node.js built-ins (
node:dgram,node:crypto,node:net, etc.). - The plugin does not write any files to disk. All Homebridge accessory state is managed by Homebridge itself in the user's storage directory (
api.user.storagePath()); the plugin only reads it back via the standardconfigureAccessorycallback. - Tokens are read from
config.jsononly and are never logged, never forwarded, and never persisted anywhere else.
Support & deprecation policy
- Supported runtime: active LTS Node versions listed in
package.jsonengines (currently Node 22.x and Node 24.x). - Homebridge support target: stable 2.0.2+ validated in CI on Node 22 and Node 24.
- Homebridge 1.x support has been dropped — users on Homebridge 1.x must stay on plugin v1.0.2 or upgrade Homebridge to 2.x.
- Node.js 20.x support has been dropped (end-of-life) — users on Node 20 must stay on plugin v1.0.2 or upgrade Node.js to 22 / 24.
- Deprecations are announced in
CHANGELOG.mdbefore removal in the next major version. - Dynamic Platform Plugin — supports multiple devices in a single plugin instance with automatic cached accessory management. See
config.schema.jsonfor the full schema.
Troubleshooting
Device not responding / timeout
- Verify IP and token
- Ensure UDP 54321 is allowed on LAN
- Confirm Homebridge host and purifier are on the same subnet
- Power cycle purifier and retry
Wrong token
- Token must be exactly 32 hex characters
- Regenerate token after device reset if needed
Failed to configure device: Invalid or missing config field: address
This error means at least one entry in the devices array is empty or
incomplete — typically a leftover entry created by clicking Add Device in
Homebridge UI without filling in address, token, or model. Three real
purifiers may still appear correctly in HomeKit while a fourth ghost entry
keeps producing this error on every restart.
Starting with this release the message names the offending entry, e.g.
Failed to configure device #4 ("Air Purifier"): missing required config fields: address, token, modelTo inspect your saved configuration safely (the jq filter strips token
values so you can paste output without leaking secrets):
jq '.platforms[]
| select(.platform == "XiaomiMiAirPurifier")
| .devices
| map(del(.token))' \
~/.homebridge/config.jsonRemove the empty entry (or fill in the missing fields), save the file, then
restart the child bridge for XiaomiMiAirPurifier from Homebridge UI — or
restart Homebridge entirely if you do not run this plugin in a child bridge.
Cached HomeKit accessories for the three working purifiers are unaffected by
this fix; their UUIDs are derived from the device IP and remain stable.
Model differences
Some properties are model/firmware-specific. The transport supports both legacy MIIO and MIOT property APIs and falls back where possible.
Development
export NODE_OPTIONS="--unhandled-rejections=strict --trace-warnings --trace-uncaught --throw-deprecation --pending-deprecation --trace-deprecation"
npm ci
npm run lint
npm run typecheck
npm test
npm run buildRunning tests
npm test # run all tests
npx vitest --coverage # run tests with coverage reportTests enforce 100% coverage (statements, branches, functions, lines) via vitest v4 + v8 provider. The test suite covers 9 automated scenarios for network resilience, device status synchronization, and filter lifecycle — see docs/reliability-test-plan.md for details.
AI Notice
This codebase was created entirely with the help of AI.
License
MIT
