npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

homebridge-xiaomi-air-purifier-modern

v1.0.7

Published

Modern Homebridge plugin for Xiaomi Mi Air Purifier (2H/3/3H/4/Pro)

Downloads

304

Readme

homebridge-xiaomi-air-purifier-modern

CI npm Homebridge Node

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-modern

Configuration

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):

  1. 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.
  2. python-miiomiiocli CLI can read tokens stored locally and discover devices on the LAN (miiocli discover --handshake true).
  3. 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.
  4. iOS — iMazing backup parsing: read tokens from the encrypted Mi Home backup using this guide.
  5. 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/OFF and Mode NIGHT ON/OFF.
  • When Power is OFF, mode writes are intentionally rejected by plugin logic (onSet ignored 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_life
  • FilterChangeIndication = CHANGE_FILTER when <= filterChangeThreshold (default 10), otherwise FILTER_OK
  • Optional: ContactSensorState on Filter Replace Alert = CONTACT_NOT_DETECTED (open = alert) when replacement is needed, otherwise CONTACT_DETECTED (closed = normal) (only when exposeFilterReplaceAlertSensor: 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 transportResetThreshold consecutive failed polls (default 12, ≈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 replying MIIO error -5001 until the source UDP port rotates), most common on older zhimi.airpurifier.pro units. A transportResetCooldownMs (default 5 min) prevents thrashing when the device is genuinely offline.
  • MIIO error -5001 and -10000 are 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:

  1. Put IoT devices in a dedicated VLAN / SSID.
  2. Allow only Homebridge host ↔ purifier UDP 54321 in ACL/firewall rules.
  3. Block WAN egress from IoT VLAN when possible.
  4. Keep maskDeviceAddressInLogs enabled (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 standard configureAccessory callback.
  • Tokens are read from config.json only and are never logged, never forwarded, and never persisted anywhere else.

Support & deprecation policy

  • Supported runtime: active LTS Node versions listed in package.json engines (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.md before 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.json for the full schema.

Troubleshooting

Device not responding / timeout

  1. Verify IP and token
  2. Ensure UDP 54321 is allowed on LAN
  3. Confirm Homebridge host and purifier are on the same subnet
  4. 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, model

To 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.json

Remove 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 build

Running tests

npm test                  # run all tests
npx vitest --coverage     # run tests with coverage report

Tests 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