HomeKit integration - New features

Hello everyone,

After completing the external integration for my Neomitis heatings, I continued using my pro account Claude :slight_smile:

I tackled porting everything possible from Homekit to Gladys.

There are X PRs made. I haven’t been able to test them yet, so testers are welcome :wink:

Everything has been entirely done by Claude. If I made some mistakes, I’m open to your criticism and help to correct them (programming is not my job).

Below you will find a status of what has been done and what remains to be done (by Claude):

## Where We Are

Gladys’s HomeKit bridge exposed 12 types of devices. The rest — smoke detectors, locks, thermostats, batteries, buttons — remained invisible on the iPhone, **without error message** : the devices simply did not appear.

**Merged:** [#2781](feat(homekit): expose light, CO, CO2 and air quality sensors by Dreamthy · Pull Request #2781 · GladysAssistant/Gladys · GitHub) — light, CO, CO2, and air quality sensors.

**Pending review:**

- [#2792](feat(homekit): expose sirens as a Switch by Dreamthy · Pull Request #2792 · GladysAssistant/Gladys · GitHub) — **Siren** (`Switch`)

- [#2793](feat(homekit): expose smoke sensors by Dreamthy · Pull Request #2793 · GladysAssistant/Gladys · GitHub) — **Smoke detector** (`SmokeSensor`)

- [#2794](Feat/homekit lock by Dreamthy · Pull Request #2794 · GladysAssistant/Gladys · GitHub) — **Lock** (`LockMechanism`)

- [#2795](feat(homekit): expose buttons as stateless programmable switches by Dreamthy · Pull Request #2795 · GladysAssistant/Gladys · GitHub) — **Button, remote** (`StatelessProgrammableSwitch`)

- [#2796](Feat/homekit fan by Dreamthy · Pull Request #2796 · GladysAssistant/Gladys · GitHub) — **Fan** (`Fanv2`)

- [#2797](feat(homekit): expose device batteries by Dreamthy · Pull Request #2797 · GladysAssistant/Gladys · GitHub) — **Battery level** (`Battery`)

- [#2798](feat(homekit): expose PM2.5 and PM10 densities on the air quality sensor by Dreamthy · Pull Request #2798 · GladysAssistant/Gladys · GitHub) — **PM2.5 and PM10 particles** (`AirQualitySensor`)

- [#2799](Feat/homekit thermostat by Dreamthy · Pull Request #2799 · GladysAssistant/Gladys · GitHub) — **Thermostat and air conditioning** (`Thermostat`)

On the integration side, it mainly affects **Zigbee2mqtt**, **Z-Wave**, **Matter**, and **Xiaomi**, plus **Nuki** for locks, **Netatmo** and **MELCloud** for the thermostat, and **Bluetooth** for batteries.

A ninth PR is coming: the **choice of exposed devices**. Today, the bridge exposes everything it knows how to expose, which floods the Home app on a large installation. It adds an option to expose only a selection.

Each PR covers a category, is readable alone, and has 100% test coverage on the added lines.

## Actual Coverage

`hap-nodejs`, the library used by Gladys as well as Homebridge, exposes 73 services. But not all of them are device types: 38 are plumbing of the protocol (pairing, transport, firmware), pieces that only exist inside other services, or obsolete duplicates.

So there are **35 services actually exposable**. The bridge covered **12** before this project, it will cover **15** with the ongoing PRs — that is **43 %**, plus features added to existing services.

### Where does the number 73 come from — and why Apple’s documentation is not the right reference

Classic pitfall at the start: the HomeKit documentation on Apple’s developer site describes the **HomeKit framework**, which is used to write an iOS app that *controls* HomeKit accessories. A bridge, on the other hand, falls under **HAP**, the HomeKit Accessory Protocol.

Apple publishes a *HomeKit Accessory Protocol Specification (Non-Commercial Version)*, whose Release R2 is the latest public version. The commercial version is under NDA, reserved for the MFi program.

Interesting detail found in the `hap-nodejs` code: it does not transcribe the PDF of the spec, it **generates** its definitions from Apple resources present on macOS — `HomeKitDaemon.framework` and the metadata of the HomeKit Accessory Simulator.

Practical consequence: it follows what iOS actually implements, not what the R2 of 2019 described. You can find services annotated « since iOS 15 », absent from the public spec. And it gives a verifiable local reference, with the characteristics, their limits, and their units.

## Five Pitfalls for Anyone Who Wants to Contribute

### 1. A category is not enough: it’s the category + type pair

The trickiest of the five. A Gladys feature has a **category** and a **type**. The bridge checks that the pair exists in its mapping table. If the type is not there, the feature is ignored — **without error or log**.

Yet an integration can reassign the category of a feature *without touching its type*. Two real cases, both found along the way:

- **Z-Wave** reclassifies binary sensors as `co2-sensor`, leaving them `type: binary`. I had mapped `co2-sensor` for decimal and integer types, not binary → all Z-Wave CO2 detectors remained invisible. Found during review of the first PR.

- **Nuki** reports its battery level as `lock:integer`, where others use `sensor:integer`. Mapping the expected types alone would have silently excluded all Nuki locks.

The method to avoid this: list the **actually produced** category + type pairs by the integrations, and never assume the « obvious » types of a category.

### 2. Units are not guessable

HomeKit expects particle densities in **µg/m³**. Gladys lets an integration declare them in milligrams, micrograms **or** nanograms per cubic meter. Without conversion, a sensor in mg/m³ displays a thousand times too low, and nothing signals it.

Same issue for VOCs, but with a different outcome: Gladys stores them in **ppb**, HomeKit wants µg/m³. The conversion requires the molar mass of the compound, which a generic « VOC » feature does not carry. No factor can be honestly chosen, so VOCs remain out of the bridge, deliberately.

### 3. The bridge times out after 5 seconds by default

Useful to not flood the iPhone with state changes. Fatal for a **button** : a press is an event, not a state. With a 5-second delay, HomeKit reacts too late, or swallows the press if a second follows. This delay must be explicitly set to zero.

### 4. Gladys is sometimes richer than HomeKit

`BUTTON_STATUS` counts **over a hundred values** : click, double-click, long press, but also rotation, shake, directional arrows, brightness gestures. HomeKit only knows **three**.

The choice made: only report the three that have an exact equivalent, and **ignore the others** rather than mapping them to one of the three. Triggering the wrong automation for someone is worse than triggering none.

### 5. One device, one service, not three

HomeKit models in a single service what Gladys splits into several categories:

- The service `Thermostat` ← hot setpoint, cold setpoint, mode, temperature sensor

- The service `AirQualitySensor` ← air quality index, PM2.5, PM10

- The service `Battery` ← level, low battery alert

Without grouping, the Home app displays three tiles for a single device. This happened with a Matter fan: it appeared as **three distinct fans**.

## What is Blocked, and Why

These are limitations of **Gladys’s core**, not of the bridge.

**`GarageDoorOpener`, `Outlet`, `Door`, `Window`** — there is no `device_class` in the Gladys model to say « this shutter is a garage door » or « this outlet is an outlet, not a switch ». This is the most structural blockage: it closes four services at once. The lift goes far beyond the HomeKit bridge and deserves its own discussion.

**`SecuritySystem`** — the home alarm is not a device: it lives in `t_house.alarm_mode`, but the bridge iterates over the devices.

**`Valve`** — the seven `water-valve` types are *all read-only* : flow rate, watering volume, operating status. No opening command. An unactionable valve tile would be worse than nothing — and these valves are already controllable, their command passing through a `switch:binary` feature that the bridge already exposes.

**`OccupancySensor`** — HomeKit expects a stable boolean, while Gladys reports presence in `sensor:push`, an event without persistent state. A state would need to be synthesized with an arbitrarily chosen timeout: this is business logic, not mapping.

**`HeaterCooler`** — the `heater` category has only one type, `pilot-wire-mode`. The pilot wire is a French specificity without HomeKit equivalent.

**`Television` and `CameraRTPStreamManagement`** — intentionally out of scope: these are separate projects, not mapping additions.

## Why not an external integration?

This is **not possible today**, for three structural reasons:

  1. The bridge must announce itself in **mDNS on the local network** for the iPhone to discover it. An external integration runs in an isolated network where multicast does not pass.

  2. It must **listen on a fixed port**, reachable from the LAN.

  3. It must see **all devices** from all integrations, while the external model isolates each integration in its own namespace.

Beware of a common confusion: there are discussions about a **HomeKit Controller**** integration, which goes in the *other* direction — controlling HomeKit accessories from Gladys. That one could be portable externally. The bridge, no.

## Where to help

**Test on real hardware.** This is the number one need. Everything is covered by automated tests, but real pairing has not been validated on all types of devices. If you have a lock, a thermostat, a fan, or a smoke detector and an iPhone, your feedback is worth more than any unit test.

**Review the PRs**, especially the mapping decisions: gas detection thresholds, air quality bands, lock states. They are all documented and justified in the descriptions.

**Remaining feasible, not done: ** `FilterMaintenance`, for HEPA filter tracking. Only one producer today, so low value — but it’s a simple contribution for anyone who wants to get started.

Thanks for your PRs @jeromeme !! :smiley:

I’ve started automatic Cursor reviews on your PRs!

Thanks @pierre-gilles for the reviews, it helped to fix some issues.

Updated status:

Fixed — 25 defects

First review pass (15)

  1. A 0s delay changed to 5s with a || — the smoke detector fix was dead code
  2. Two consecutive single clicks: the second was swallowed
  3. A fan was writing to a read-only feature
  4. Turned off without prior reading → turned back on at full speed
  5. The lock forgot the command as soon as the first poll
  6. A read-only lock still accepted commands
  7. Silent battery announced as low (null <= 20 is true in JS)
  8. Non-contiguous air conditioning modes: « hot » offered on a cold-only air conditioner
  9. Temperature setpoint linked on a device that doesn’t have one → crash on first poll
  10. Empty mode list, rejected by HAP
  11. No bounding on the notification path: a sensor that went off the rails brought down the bridge
  12. 5-second delay on a smoke detector (reported by Pierre-Gilles)
  13. /device route not tested
  14. Write order: a partial failure left all devices exposed
  15. /device failure took down the pairing QR code

Second pass (8)

  1. Two alarms in the same instant: the first one was lost
  2. A lock command overwrote the real state — Nuki announced as locked while it was turning
  3. Multi-button remote: all presses on button 1
  4. Matter buttons completely silent
  5. The raw fan speed overwrote the displayed percentage
  6. Going below 20% battery never notified, only on the next poll
  7. Thermostat never « at rest » between its two setpoints
  8. Auto mode written on an air conditioner that doesn’t declare it

This morning (2)

  1. Xiaomi long presses never transmitted
  2. Four tests already merged deleted by a rebase — the CI remained green

Status

  • 3 PRs merged: #2781, #2792, #2798
  • 7 open PRs, all green, no conflicts
  • 5 approved by Cursor: #2793, #2796, #2797, #2799, #2800
  • 2 pending his new pass: #2794, #2795
  • 46 review threads answered
  • 2 follow-up issues opened: #2806, #2812

What’s left to do on the HomeKit bridge

Test

  • Pair a real iPhone. Nothing has ever been tested on real hardware:
    everything is validated by automated tests. A native Gladys installation is
    needed — not Docker, multicast mDNS does not go through the VM.
    Priority to locks, thermostats, and remotes.
  • Check accessory names: HAP rejects emoji and punctuation, and
    only issues a warning. The accessory then never appears, without error.

Review

Seven open PRs, all green, all reviewable separately: #2793 smoke,
#2794 lock, #2795 button, #2796 fan, #2797 battery, #2799 thermostat,
#2800 device selection.

Two open projects for anyone

  • #2806 — map thermostat:mode and thermostat:operating-state. The types
    exist in the core since #2752, but no integration produces
    them yet. To be done at the same time as the first one that will emit them (#2730 on the
    Z-Wave side).
  • #2812 — index HomeKit services by feature rather than by type. A
    single fix would unlock multi-button remotes, multiple shutters, and
    thermostats with multiple probes.

Blocked by Gladys core

These HomeKit services are not reachable without an evolution of the
device model. The first one is the most structuring: it would unlock four at once.

  • GarageDoorOpener, Outlet, Door, Window — a notion of
    device_class is missing to distinguish a plug from a switch, or a garage door from a shutter.
  • SecuritySystem — the alarm lives in t_house.alarm_mode, not in a
    device.
  • Valve — the seven water-valve types are all read-only. These valves
    remain controllable via switch:binary.
  • OccupancySensor — Gladys reports presence without persistent state.
  • HeaterCooler — the heater category only has the French pilot wire, with no HomeKit equivalent.

Doable, not done

  • FilterMaintenance (HEPA filter) — a single producer, low value.

Thanks a lot for all these PRs :slight_smile: It’s really cool!!

Everything is good for me, it’s merged on master!

Maybe our features are just not precise enough :slight_smile:

Don’t hesitate to create a feature request for each type of feature you would like to have in Gladys in Demande de fonctionnalités, it can totally be added!

In itself, I don’t see the issue, the integration could map the alarm mode in Homekit?

What do you mean?

No problem, hoping everything works well :slight_smile:

For the alarm, I will make sure it creates a device in Homekit with the following mapping:

  • Gladys / disarmed = Homekit / DISARMED
  • Gladys / armed = Homekit / AWAY_ARM
  • Gladys / partially-armed = Homekit / STAY_ARM
  • Gladys / panic = Homekit / ALARM_TRIGGERED
    Only the Homekit / NIGHT_ARM state would not be integrated.

For the device_class, I will study this with Claude and, if necessary, make a request :wink:

For the OccupancySensor, this is an error reported by Claude:

Quote
I had dismissed `presence-sensor` thinking there was no state to read, because its type is `SENSOR.PUSH`.
By reading `lan-manager.scanPresence.js`, this is false: it does send 1 when the device is detected and 0 when it disappears. It is a real boolean, and it maps directly to `OccupancySensor`. Same for Bluetooth. I will make the PR.