UltraWideLock v0.4.0

Approach Direction#

How the Apple Home app's "Approach Direction" control is exposed by an Aliro lock, and every trap hit making it appear on the ESP32-S3 port. Validated on silicon 2026-07-21: the control renders and its Left/Front/Right selection round-trips to the device.

note

Convention: VERIFIED = confirmed on silicon or against SDK source; INFERRED = consistent with observation but not directly proven.

---

1. It is not a standard Matter attribute#

VERIFIED. No attribute of the standard Door Lock cluster (0x0101) carries approach direction, through spec revision 1.6 — not in the Aliro attribute block (0x00800x0088), not anywhere else. Searching the standard cluster for it will always come up empty, and that emptiness is not evidence the feature does not exist.

The control is driven by a manufacturer-specific cluster using a Matter MEI (Manufacturer Extensible Identifier): vendor ID in the upper 16 bits, cluster ID in the lower 16. Manufacturer clusters live in a separate ID space that a search of the standard data model cannot reach.

Vendor 0x1349 is Apple (CHIPVendorIdentifiers.hpp:46), so the cluster is 0x1349FC03.

2. The descriptor#

VERIFIED on silicon — the control renders and its selection round-trips with exactly these values — and against the type tables in chip-types.xml and attribute-metadata.h.

Cluster 0x1349FC03, mask 0x40 (server), on the door lock endpoint — the same endpoint as the Door Lock cluster, not endpoint 0. Three attributes, 7 bytes total:

AttributeSizeTypeMaskDefault
0x0000 direction10x18 bitmap80x03 writable\nonvolatile7
0xFFFC FeatureMap40x1B bitmap3200
0xFFFD ClusterRevision20x21 int16u01

Type codes per chip-types.xml lines 28/30/33; mask bits per attribute-metadata.h:110,112 (MATTER_ATTRIBUTE_FLAG_WRITABLE = 0x01, ..._NONVOLATILE = 0x02).

Two things are easy to get wrong here:

  • The direction attribute is a bitmap8, not int8u. Declaring it as an unsigned integer is the wrong type code (0x20 instead of 0x18).
  • The default of 7 is 0b111, all three directions permitted, matching Home's "unlock when you approach from any direction". Which single bit means Left versus Right is still unknown and nothing in the port depends on it.

3. The expensive one: missing global attributes fail silently, then loudly#

VERIFIED, cost three pairing cycles.

FeatureMap and ClusterRevision are mandatory on every Matter cluster. esp_matter's cluster::create() emits neither — every generated cluster in the SDK adds them explicitly (see any file under data_model/generated/clusters/).

Declaring the cluster with only the direction attribute produces a cluster that cannot answer ClusterRevision. The failure does not look like a failure:

  1. Home runs a complete commissioning — PASE, attestation, CSR request, AddNOC, CommissioningComplete — on two fabrics
  2. Roughly 0.5 s later it sends RemoveFabric for its own fabric
  3. The UI says "Unable to Add Accessory"

Nothing in the device log errors. The read Home makes immediately before bailing succeeds, so the rejection is based on a value it received, not on an error status.

How to recognise it: search the log for OpCreds: Received a RemoveFabric Command appearing after a successful Commissioning completed successfully. That sequence means the accessory was accepted and then rejected, which is a data-model problem, not a connectivity or crypto problem.

Fix:

c
cluster_t *c = cluster::create(endpoint, kApproachDirectionClusterId, CLUSTER_FLAG_SERVER);
attribute::create(c, 0x0000, ATTRIBUTE_FLAG_WRITABLE | ATTRIBUTE_FLAG_NONVOLATILE,
                  esp_matter_bitmap8(7));
cluster::global::attribute::create_feature_map(c, 0);
cluster::global::attribute::create_cluster_revision(c, 1);

create_feature_map / create_cluster_revision are declared in generated/esp_matter_data_model_utils.h:62, already pulled in by esp_matter.h:25.

4. The endpoint descriptor is cached at commissioning#

VERIFIED. A controller reads the endpoint's cluster list once, when the accessory is commissioned, and caches it. Adding a cluster to a device that is already paired changes nothing visible — the control will not appear no matter how many times the app is restarted.

Any newly declared cluster requires removing the accessory and adding it again.

5. Removal and re-pairing traps#

Three separate ways to end up unable to re-add a device, all seen on the bench:

5.1 Removing while the device is unreachable. Removing an accessory sends RemoveFabric to the device. If the device is offline at that moment, the controller removes it locally and the device never hears about it — it still boots with Fabric already commissioned. Disabling BLE advertisement and cannot be discovered.

5.2 Multiple fabrics. A device commissioned into Apple Home typically holds two fabrics. Removing the accessory in the app clears one; the device stays commissioned on the other, so it still will not advertise.

For this ESP32-specific path, the fallback is a device-side factory reset, which clears every fabric at once: matter> factoryreset. If the console is unresponsive, erase only the nvs partition rather than the whole flash, so esp_secure_cert and fctry survive. The portable DWM3001CDK stack now supports authenticated, durable, per-fabric removal; use a surviving controller's Manage fabrics action there and reserve SW2 factory reset for a node no administrator can reach.

5.3 Trust-store exhaustion. Tangential but it will bite during repeated pairing cycles. A Matter factory reset does not touch the reader's own provisioning namespace, and nothing evicts superseded credentials, so each re-pair burns a slot. Once the store is at ULTRAWIDELOCK_TRUST_MAX, the reader verifies the phone's signature and then rejects the credential:

device signature OK
credential key NOT trusted (not in trust store); rejecting

ultrawidelock trust reports FAILED (trust store full or NVS error), which covers three distinct causes and does not say which. Run ultrawidelock prov to see the true count, and ultrawidelock clear to empty the store.

6. Red herring: cluster 0x1349FC00#

VERIFIED as a dead end. During commissioning, Home reads clusterId 0x1349FC00, attributeId 0x0001 and the device answers err = 5c3.

This looks like a promising lead and is not one. This port renders the Approach Direction control while still answering UNSUPPORTED_CLUSTER for 0x1349FC00, so that cluster cannot be what gates the control. Do not spend build cycles guessing at its datatype.

7. Decoding the error codes in these logs#

Interaction Model status codes appear in device logs as err = 0x500 + status:

Log valueStatusMeaning
5c30xc3UNSUPPORTED_CLUSTER
5860x86UNSUPPORTED_ATTRIBUTE

Per src/protocols/interaction_model/StatusCodeList.h. UNSUPPORTED_CLUSTER on a manufacturer cluster during commissioning is normal probing and is not by itself a problem — the device answers the same way for optional standard clusters it does not implement, such as ICD Management (0x0046).

8. The control is cosmetic on single-antenna hardware#

VERIFIED. Nothing gates an unlock on the stored value. Measuring which direction a phone approaches from requires angle of arrival, which needs a dual-antenna UWB part. The DW3110 used on the DWM3000EVB has a single antenna and cannot produce it.

The attribute is stored, reported, and round-trips correctly to the app. The behaviour behind it is not implemented, and on this hardware cannot be.

9. Port coverage#

PortStatus
apps/esp32-matter-lockimplemented: runtime cluster::create() in app_main.cpp
nRF5340 door lock appimplemented: static tables in integrations/nrfconnect-door-lock/matter-aliro-door-lock-app/src/matter/zap_uwb/

The two ports build their data models differently. The ESP32 port constructs endpoints at runtime, so the cluster is a few lines of C++. The nRF app's is a set of static tables compiled into the image.

9.1 The nRF data model is not regenerated at build time#

VERIFIED. It is tempting to assume the .zap file is the source of truth and edit it. It is not, on this build. ncs_configure_data_model passes BYPASS_IDL to chip_configure_data_model, which takes the branch that skips the chip_zapgen target entirely and instead adds the checked-in zap-generated/ directory as an include path. endpoint_config.h appears nowhere in build.ninja as a build rule. Editing lock.zap alone changes nothing in the image.

The variant in use is selected by src/matter/CMakeLists.txt: with CONFIG_NCS_SAMPLE_MATTER_ZAP_FILE_PATH unset and CONFIG_DOOR_LOCK_BLE_UWB=y, that is zap_uwb/. Read it from a configured build's .config, not from Kconfig defaults.

So the cluster is added by patching the generated endpoint_config.h: three attribute records, one cluster record, and the counters that describe them.

9.2 The counters must stay consistent with the tables#

The static tables are index-addressed, and four #defines must agree with them: GENERATED_ATTRIBUTE_COUNT, GENERATED_CLUSTER_COUNT, ZAP_FIXED_ENDPOINT_DATA_VERSION_COUNT and ATTRIBUTE_MAX_SIZE, plus the per-endpoint GENERATED_ENDPOINT_TYPES row { cluster index, cluster count, endpoint size }.

Two structural rules make the edit an append rather than a renumbering:

  • A cluster's attributes must be contiguous in the attribute table, and an endpoint's clusters contiguous in the cluster table. The door lock endpoint's clusters are already last, so a new cluster appends at the end of both tables without moving anything.
  • clusterSize is the sum of the sizes of that cluster's attributes that are not EXTERNAL_STORAGE, and an endpoint's size is the sum of its clusters' sizes. All three new attributes are RAM-backed, so the cluster adds 1 + 4 + 2 = 7 bytes, and both the endpoint row and ATTRIBUTE_MAX_SIZE grow by 7.

These are checkable rather than guessable: every counter can be re-derived from the tables in the same file, which is the cheapest way to catch an arithmetic slip before a build.

One thing this path does get for free: FeatureMap and ClusterRevision are written out per cluster like any other attribute, so §3 — the trap that cost the most time on the other port — cannot occur here as an omission by a helper.

9.3 It used to collide with the opt-in Home Assistant data model#

The Home Assistant variant adds a third endpoint and so bumps the same counter lines. While both were patches into the fetched application, two of them could not both change GENERATED_ATTRIBUTE_COUNT 205 in either order, and the stack had to be cumulative: Approach Direction applied first, the HA patches cut against a tree that already had it, and tests/tooling/patch_drift_check.sh held the ordering.

Neither is a patch now. Both data models are complete files in integrations/nrfconnect-door-lock/matter-aliro-door-lock-app/src/matter/ -- zap_uwb and zap_uwb_ha -- and CONFIG_ULTRAWIDELOCK_HA picks one. Each carries its own counters, so there is no composition left to break; what remains is the ordinary duty to regenerate both when the data model changes.

That HA=1 patch stack is unrelated to Matter multi-admin sharing on the DWM3001CDK. The DWM image needs no Home Assistant build variant.