@homebridge-plugins/homebridge-matter 0.2.0 → 0.2.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MATTER_API.md DELETED
@@ -1,3869 +0,0 @@
1
- # Homebridge Matter Integration API
2
-
3
- This document serves as a comprehensive guide for integrating Matter devices into Homebridge plugins. It covers the core concepts, patterns, and device-specific implementations.
4
-
5
- ---
6
-
7
- ## Table of Contents
8
-
9
- 1. [Core Concepts](#core-concepts)
10
- 2. [The Two-Way Flow Architecture](#the-two-way-flow-architecture)
11
- 3. [Implementing Matter Devices](#implementing-matter-devices)
12
- 4. [Reading and Updating State](#reading-and-updating-state)
13
- 5. [Monitoring External Changes](#monitoring-external-changes)
14
- 6. [Using Matter Types](#using-matter-types)
15
- 7. [Best Practices](#best-practices)
16
- 8. [API Reference](#api-reference)
17
- 9. [Device Reference](#device-reference)
18
-
19
- ---
20
-
21
- ## Core Concepts
22
-
23
- ### Matter Device Architecture
24
-
25
- Matter devices in Homebridge are **virtual devices** that bridge your physical IoT devices (cloud API, HTTP, MQTT, etc.) to the Matter protocol. Understanding this separation is crucial:
26
-
27
- - **Physical Device**: Your actual IoT device (controlled via API, MQTT, HTTP, etc.)
28
- - **Virtual Matter Device**: A Matter representation in Homebridge that Home app communicates with
29
- - **Your Plugin**: Acts as the bridge between the two, translating commands and synchronizing state
30
-
31
- ### Coming from HAP? Quick Reference
32
-
33
- **If you're familiar with HAP (HomeKit Accessory Protocol), here's the essential translation:**
34
-
35
- | HAP Concept | Matter Equivalent | Quick Description |
36
- |--------------------|-------------------|------------------------------------------------|
37
- | **Accessory** | **Endpoint** | The device (one tile in Home app) |
38
- | **Service** | **Cluster** | A capability (Lightbulb, Switch, etc.) |
39
- | **Characteristic** | **Attribute** | A property (On, Brightness, Temperature, etc.) |
40
-
41
- **Key difference**: Matter uses a **declarative configuration** approach (define everything upfront) instead of HAP's object-oriented approach (add services, set characteristics). Both protocols automatically update state after handlers complete successfully.
42
-
43
- ### Clusters (HAP: Services)
44
-
45
- **Clusters** are the building blocks of Matter devices, equivalent to **Services** in HAP. They define functionality and state:
46
-
47
- - **OnOff Cluster** *(HAP: Switch/Lightbulb Service)*: Controls power state (on/off)
48
- - **LevelControl Cluster** *(HAP: Brightness Characteristic)*: Controls brightness/level (1-254)
49
- - **ColorControl Cluster** *(HAP: Hue/Saturation Characteristics)*: Controls color (hue/saturation/temperature)
50
- - And many more...
51
-
52
- Each cluster has:
53
- - **Attributes** *(HAP: Characteristics)*: State values (e.g., `onOff: true`, `currentLevel: 127`)
54
- - **Commands** *(HAP: Characteristic setters)*: Actions that can be invoked (e.g., `on()`, `off()`, `moveToLevel()`)
55
-
56
- #### Understanding Commands (vs HAP Characteristic Setters)
57
-
58
- **Commands** are a new concept if you're coming from HAP. Here's how they compare:
59
-
60
- **In HAP:**
61
- ```typescript
62
- // You set a characteristic value, which triggers onSet
63
- service.getCharacteristic(Characteristic.On)
64
- .onSet(async (value: boolean) => {
65
- // value is true or false
66
- await device.setPower(value)
67
- })
68
- ```
69
-
70
- **In Matter:**
71
- ```typescript
72
- // Separate commands are invoked for different actions
73
- handlers: {
74
- onOff: {
75
- on: async () => {
76
- // Explicit "on" command was invoked
77
- await device.turnOn()
78
- },
79
- off: async () => {
80
- // Explicit "off" command was invoked
81
- await device.turnOff()
82
- },
83
- },
84
- }
85
- ```
86
-
87
- **Key differences:**
88
-
89
- 1. **Action-oriented vs Value-oriented**
90
- - HAP: You set a value (`true`/`false`) and interpret what to do
91
- - Matter: Specific commands (`on()`, `off()`) with clear intent
92
-
93
- 2. **Command Parameters**
94
- - Simple commands like `on()` and `off()` take no parameters
95
- - Complex commands like `moveToLevelWithOnOff()` receive parameters:
96
- ```typescript
97
- handlers: {
98
- levelControl: {
99
- moveToLevelWithOnOff: async (request) => {
100
- const level = request.level // 1-254
101
- const transitionTime = request.transitionTime // Optional
102
- await device.setBrightness(level)
103
- },
104
- },
105
- }
106
- ```
107
-
108
- 3. **Handler Mapping**
109
- - Each command maps to a handler function in your `handlers` object
110
- - Command names are predefined by the Matter spec (not arbitrary)
111
- - Common commands:
112
- - **OnOff**: `on()`, `off()`, `toggle()`
113
- - **LevelControl**: `moveToLevel()`, `moveToLevelWithOnOff()`, `step()`, `stop()`
114
- - **ColorControl**: `moveToHue()`, `moveToSaturation()`, `moveToHueAndSaturation()`
115
- - **DoorLock**: `lockDoor()`, `unlockDoor()`
116
-
117
- **Practical example - Brightness:**
118
-
119
- ```typescript
120
- // HAP approach
121
- service.getCharacteristic(Characteristic.Brightness)
122
- .onSet(async (value: number) => {
123
- // You receive the target brightness value
124
- await device.setBrightness(value)
125
- })
126
-
127
- // Matter approach
128
- handlers: {
129
- levelControl: {
130
- moveToLevelWithOnOff: async (request) => {
131
- // Command with parameters
132
- const targetLevel = request.level // 1-254
133
- await device.setBrightness(targetLevel)
134
- },
135
- },
136
- }
137
- ```
138
-
139
- The command-based approach is more explicit about the user's intent and can provide additional context (like transition time for smooth dimming).
140
-
141
- #### Type-Safe Handler Arguments with MatterRequests
142
-
143
- For commands with parameters, import `MatterRequests` for TypeScript autocomplete and type checking:
144
-
145
- ```typescript
146
- import type { MatterRequests } from 'homebridge'
147
-
148
- handlers: {
149
- levelControl: {
150
- moveToLevelWithOnOff: async (request: MatterRequests.MoveToLevel) => {
151
- const { level, transitionTime } = request // Fully typed!
152
- await device.setBrightness(level)
153
- },
154
- },
155
- }
156
- ```
157
-
158
- **Quick Examples:**
159
- - `MatterRequests.MoveToLevel` - Brightness control
160
- - `MatterRequests.MoveToHueAndSaturation` - Color control
161
- - `MatterRequests.LockDoor` - Door lock with optional PIN
162
- - `MatterRequests.SetpointRaiseLower` - Thermostat temperature
163
-
164
- **Why use this?**
165
- ✅ Autocomplete shows available properties
166
- ✅ Compile-time errors for typos
167
- ✅ Hover to see parameter types
168
-
169
- **Note:** TypeScript types only - not available on `api.matter` at runtime.
170
-
171
- <details>
172
- <summary><strong>Click to see all MatterRequests types</strong></summary>
173
-
174
- #### Level Control
175
- ```typescript
176
- MatterRequests.MoveToLevel // { level, transitionTime?, optionsMask?, optionsOverride? }
177
- MatterRequests.Move // { moveMode, rate?, optionsMask?, optionsOverride? }
178
- MatterRequests.Step // { stepMode, stepSize, transitionTime?, ... }
179
- MatterRequests.Stop // { optionsMask?, optionsOverride? }
180
- ```
181
-
182
- #### Color Control
183
- ```typescript
184
- MatterRequests.MoveToHue // { hue, direction, transitionTime?, ... }
185
- MatterRequests.MoveToSaturation // { saturation, transitionTime?, ... }
186
- MatterRequests.MoveToHueAndSaturation // { hue, saturation, transitionTime?, ... }
187
- MatterRequests.MoveToColorTemperature // { colorTemperatureMireds, transitionTime?, ... }
188
- MatterRequests.MoveHue // { moveMode, rate?, ... }
189
- MatterRequests.MoveSaturation // { moveMode, rate?, ... }
190
- MatterRequests.MoveColorTemperature // { moveMode, rate?, ... }
191
- MatterRequests.StepHue // { stepMode, stepSize, transitionTime?, ... }
192
- MatterRequests.StepSaturation // { stepMode, stepSize, transitionTime?, ... }
193
- MatterRequests.StepColorTemperature // { stepMode, stepSize, transitionTime?, ... }
194
- ```
195
-
196
- #### Door Lock
197
- ```typescript
198
- MatterRequests.LockDoor // { pinCode? }
199
- MatterRequests.UnlockDoor // { pinCode? }
200
- ```
201
-
202
- #### Window Covering
203
- ```typescript
204
- MatterRequests.GoToLiftPercentage // { liftPercent100thsValue }
205
- MatterRequests.GoToTiltPercentage // { tiltPercent100thsValue }
206
- ```
207
-
208
- #### Thermostat
209
- ```typescript
210
- MatterRequests.SetpointRaiseLower // { mode, amount }
211
- ```
212
-
213
- #### Fan Control
214
- ```typescript
215
- MatterRequests.FanStep // { direction, wrap?, lowestOff? }
216
- ```
217
-
218
- </details>
219
-
220
- ### Endpoints vs Clusters (HAP: Accessories vs Services)
221
-
222
- Understanding the relationship between endpoints and clusters is fundamental to Matter architecture.
223
-
224
- #### The Hierarchy
225
-
226
- ```
227
- Matter Node (Homebridge instance)
228
- └── Endpoint 1 (e.g., "Living Room Light")
229
- ├── Cluster: OnOff
230
- ├── Cluster: LevelControl
231
- └── Cluster: ColorControl
232
- └── Endpoint 2 (e.g., "Bedroom Light")
233
- ├── Cluster: OnOff
234
- └── Cluster: LevelControl
235
- └── Endpoint 3 (e.g., "Temperature Sensor")
236
- └── Cluster: TemperatureMeasurement
237
- ```
238
-
239
- #### Endpoints
240
-
241
- **Endpoints** are individual addressable "devices" or "sub-devices" within a Matter node.
242
-
243
- - Each endpoint represents **one accessory** that appears in Home app
244
- - Each endpoint has a **device type** (OnOffLight, DimmableLight, TemperatureSensor, etc.)
245
- - Endpoints are numbered (Endpoint 0 is reserved for root/node-level, Endpoint 1+ are your devices)
246
- - Each endpoint appears as a **separate tile** in Home app
247
-
248
- **Examples**:
249
- - A single smart bulb = 1 endpoint
250
- - A 3-gang light switch = 3 endpoints (one per switch)
251
- - A combo device (light + motion sensor) = 2 endpoints
252
-
253
- #### Clusters
254
-
255
- **Clusters** are the functional building blocks **within** an endpoint.
256
-
257
- - Each cluster provides a **specific capability** (power control, brightness, temperature sensing, etc.)
258
- - Clusters contain **attributes** (state values) and **commands** (actions)
259
- - Multiple clusters combine to create the full functionality of an endpoint
260
- - Clusters are reusable across different device types
261
-
262
- **Examples**:
263
- - **OnOff cluster**: Provides on/off capability
264
- - **LevelControl cluster**: Provides brightness/level control
265
- - **TemperatureMeasurement cluster**: Provides temperature reading
266
-
267
- #### In Practice
268
-
269
- When you create a Matter accessory in Homebridge:
270
-
271
- ```typescript
272
- const accessory = {
273
- // This creates an ENDPOINT
274
- uuid: api.matter.uuid.generate('my-light'),
275
- displayName: 'Living Room Light',
276
- deviceType: api.matter.deviceTypes.DimmableLight,
277
-
278
- // These are CLUSTERS within the endpoint
279
- clusters: {
280
- onOff: { onOff: false }, // OnOff cluster
281
- levelControl: { currentLevel: 127 }, // LevelControl cluster
282
- },
283
- }
284
- ```
285
-
286
- **Key Points**:
287
- - **Each accessory you create = 1 endpoint**
288
- - The `clusters` object defines which clusters (capabilities) that endpoint has
289
- - The device type determines which clusters are required/optional for that endpoint
290
-
291
- #### HAP vs Matter: Detailed Comparison
292
-
293
- For HAP developers, here's how common patterns translate:
294
-
295
- | Aspect | HAP | Matter |
296
- |---------------------------|-------------------------------------------------------|----------------------------------------------------------|
297
- | **Structure** | Object-oriented (classes, methods) | Configuration-based (objects) |
298
- | **Services/Clusters** | Added dynamically with `addService()` | Defined upfront in `clusters` object |
299
- | **Handlers** | Registered per-characteristic (`onSet`, `onGet`) | Grouped by cluster in `handlers` object |
300
- | **State Updates** | `updateCharacteristic()` per characteristic | `updateAccessoryState()` per cluster |
301
- | **Reading State** | Via getter methods or cached values | Direct property access: `accessory.clusters.onOff.onOff` |
302
- | **Multiple Capabilities** | Multiple services on one accessory | Multiple clusters in one endpoint |
303
- | **Automatic Updates** | Automatic after handlers, manual for external changes | Automatic after handlers, manual for external changes |
304
-
305
- ---
306
-
307
- ## The Two-Way Flow Architecture
308
-
309
- There are **TWO separate flows** for every Matter device, which is similar to HAP.
310
-
311
- ### Flow A: Home App → Physical Device (AUTOMATIC)
312
-
313
- When a user controls the device via Home app:
314
-
315
- ```
316
- 1. User taps in Home App
317
- 2. Matter command received by Homebridge
318
- 3. Your handler runs (e.g., on(), off(), moveToLevel())
319
- 4. You control your physical device (API call, MQTT, etc.)
320
- 5. Homebridge AUTOMATICALLY updates Matter state
321
- 6. All controllers (iPhone, iPad, etc.) are notified
322
- ```
323
-
324
- **✅ Key Point**: After your handler completes, Homebridge **automatically** updates the Matter state. **DO NOT** call `api.matter.updateAccessoryState()` in handlers!
325
-
326
- ```typescript
327
- handlers: {
328
- onOff: {
329
- on: async () => {
330
- // Control physical device
331
- await myDeviceAPI.turnOn()
332
-
333
- // ❌ WRONG: Do NOT manually update state here!
334
- // api.matter.updateAccessoryState(...)
335
-
336
- // ✅ Homebridge automatically updates state after this handler
337
- },
338
- },
339
- }
340
- ```
341
-
342
- ### Flow B: Physical Device → Home App (MANUAL)
343
-
344
- When your physical device changes externally (button press, cloud app, automation):
345
-
346
- ```
347
- 1. Physical device changes state
348
- 2. ❌ Homebridge has NO IDEA this happened!
349
- 3. You MUST detect the change (events/polling)
350
- 4. You MUST call api.matter.updateAccessoryState()
351
- 5. Then all controllers are notified
352
- ```
353
-
354
- **⚠️ Key Point**: You **must** monitor your device and explicitly update Matter state when the physical device changes.
355
-
356
- ```typescript
357
- // Example: MQTT listener detecting external changes
358
- mqttClient.on('message', (topic, message) => {
359
- const deviceState = JSON.parse(message.toString())
360
- const deviceIsOn = deviceState.state === 'ON'
361
-
362
- // Check if state changed
363
- const currentMatterState = accessory.clusters.onOff.onOff
364
- if (deviceIsOn !== currentMatterState) {
365
- // ✅ Update Matter state - this is required!
366
- api.matter.updateAccessoryState(
367
- accessory.uuid,
368
- api.matter.clusterNames.OnOff,
369
- { onOff: deviceIsOn }
370
- )
371
- }
372
- })
373
- ```
374
-
375
- ### Why No Automatic Detection?
376
-
377
- Your physical device is **not** a Matter device—it's a regular IoT device (HTTP, MQTT, cloud API, etc.). The virtual Matter device in Homebridge cannot magically detect when your physical device changes. You must explicitly tell Homebridge when changes occur.
378
-
379
- ---
380
-
381
- ## Implementing Matter Devices
382
-
383
- Now that you understand the core concepts and two-way flow architecture, let's implement a Matter device.
384
-
385
- ### Basic Structure
386
-
387
- Every Matter device registration follows this pattern:
388
-
389
- ```typescript
390
- import type { API } from 'homebridge'
391
-
392
- export function registerMyDevice(api: API) {
393
- const accessory = {
394
- // Identity
395
- uuid: api.matter.uuid.generate('unique-device-id'),
396
- displayName: 'My Device',
397
- deviceType: api.matter.deviceTypes.OnOffLight,
398
- serialNumber: 'DEVICE-001',
399
- manufacturer: 'My Company',
400
- model: 'Model v1',
401
-
402
- // Optional: Persistent context storage
403
- context: {
404
- deviceId: 'my-device-123',
405
- },
406
-
407
- // State: Initial values for all cluster attributes
408
- // These values are only used when the accessory is first created
409
- // After that, Homebridge automatically persists and restores state across restarts
410
- clusters: {
411
- onOff: {
412
- onOff: false, // Initial state (only used on first creation)
413
- },
414
- },
415
-
416
- // Handlers: Respond to commands from Home app
417
- handlers: {
418
- onOff: {
419
- on: async () => {
420
- // Control your physical device
421
- },
422
- off: async () => {
423
- // Control your physical device
424
- },
425
- },
426
- },
427
- }
428
-
429
- return [accessory]
430
- }
431
- ```
432
-
433
- ### Device Identity
434
-
435
- Every accessory needs unique identification:
436
-
437
- ```typescript
438
- {
439
- uuid: api.matter.uuid.generate('unique-id'), // Must be unique per device
440
- displayName: 'Living Room Light', // Name shown in Home app
441
- deviceType: api.matter.deviceTypes.OnOffLight, // Matter device type
442
- serialNumber: 'LIGHT-001', // Unique serial number
443
- manufacturer: 'My Company', // Manufacturer name
444
- model: 'Smart Light v1', // Model identifier
445
- }
446
- ```
447
-
448
- ### Available Device Types
449
-
450
- Access all Matter device types via `api.matter.deviceTypes`:
451
-
452
- ```typescript
453
- // Common examples
454
- api.matter.deviceTypes.OnOffLight
455
- api.matter.deviceTypes.DimmableLight
456
- api.matter.deviceTypes.TemperatureSensor
457
- api.matter.deviceTypes.Thermostat
458
- api.matter.deviceTypes.DoorLock
459
- // ... see full list below
460
- ```
461
-
462
- <details>
463
- <summary><strong>Click to see all available device types</strong></summary>
464
-
465
- #### Lighting
466
- ```typescript
467
- api.matter.deviceTypes.OnOffLight
468
- api.matter.deviceTypes.DimmableLight
469
- api.matter.deviceTypes.ColorTemperatureLight
470
- api.matter.deviceTypes.ExtendedColorLight
471
- ```
472
-
473
- #### Switches & Outlets
474
- ```typescript
475
- api.matter.deviceTypes.OnOffSwitch
476
- api.matter.deviceTypes.OnOffOutlet
477
- ```
478
-
479
- #### Sensors
480
- ```typescript
481
- api.matter.deviceTypes.TemperatureSensor
482
- api.matter.deviceTypes.HumiditySensor
483
- api.matter.deviceTypes.LightSensor
484
- api.matter.deviceTypes.MotionSensor
485
- api.matter.deviceTypes.ContactSensor
486
- api.matter.deviceTypes.LeakSensor
487
- api.matter.deviceTypes.SmokeSensor
488
- ```
489
-
490
- #### HVAC
491
- ```typescript
492
- api.matter.deviceTypes.Thermostat
493
- api.matter.deviceTypes.Fan
494
- api.matter.deviceTypes.RoomAirConditioner
495
- ```
496
-
497
- #### Security
498
- ```typescript
499
- api.matter.deviceTypes.DoorLock
500
- ```
501
-
502
- #### Window Coverings
503
- ```typescript
504
- api.matter.deviceTypes.WindowCovering
505
- ```
506
-
507
- #### Appliances
508
- ```typescript
509
- api.matter.deviceTypes.RoboticVacuumCleaner
510
- ```
511
-
512
- #### Other
513
- ```typescript
514
- api.matter.deviceTypes.GenericSwitch
515
- api.matter.deviceTypes.Pump
516
- ```
517
-
518
- </details>
519
-
520
- ### Persistent Context Storage
521
-
522
- Store custom data that persists across Homebridge restarts:
523
-
524
- ```typescript
525
- {
526
- context: {
527
- deviceId: 'my-light-123',
528
- lastKnownState: true,
529
- customData: { /* anything */ }
530
- },
531
- }
532
-
533
- // Access later:
534
- const deviceId = accessory.context.deviceId
535
- ```
536
-
537
- ### Cluster Configuration
538
-
539
- Define initial state for all clusters your device supports:
540
-
541
- ```typescript
542
- clusters: {
543
- onOff: {
544
- onOff: false, // Boolean: true = on, false = off
545
- },
546
- levelControl: {
547
- currentLevel: 127, // Number: 1-254 (127 = 50%)
548
- minLevel: 1, // Minimum brightness
549
- maxLevel: 254, // Maximum brightness
550
- },
551
- }
552
- ```
553
-
554
- **Important notes about state persistence:**
555
-
556
- - These values are **initial/default values only** - used when the accessory is first created
557
- - Once created, Homebridge **automatically persists** all state changes
558
- - On restart, Homebridge **restores the last known state**, not these initial values
559
- - State persists across Homebridge restarts, updates, and system reboots
560
- - This works the same way as HAP - you don't need to manually save/restore state
561
-
562
- **Example lifecycle:**
563
- 1. First run: Accessory created with `onOff: false` (your initial value)
564
- 2. User turns light on in Home app → state becomes `onOff: true`
565
- 3. Homebridge restarts → state is still `onOff: true` (persisted)
566
- 4. Not reset to `onOff: false` (initial values are ignored after first creation)
567
-
568
- ---
569
-
570
- ## Reading and Updating State
571
-
572
- After implementing your device, you'll need to read and update its state for both Flow A (handlers) and Flow B (external changes).
573
-
574
- ### Reading Current State
575
-
576
- There are two ways to read cluster state:
577
-
578
- #### Method 1: Direct Property Access (Recommended)
579
-
580
- Access cluster attributes directly from the accessory object:
581
-
582
- ```typescript
583
- // Read power state
584
- const isOn = accessory.clusters.onOff.onOff // boolean
585
-
586
- // Read brightness
587
- const level = accessory.clusters.levelControl.currentLevel // 1-254
588
- const percent = Math.round((level / 254) * 100) // Convert to percentage
589
-
590
- // Read color
591
- const hue = accessory.clusters.colorControl.currentHue // 0-254
592
- const saturation = accessory.clusters.colorControl.currentSaturation // 0-254
593
- ```
594
-
595
- **When to use**: This is the recommended approach in most cases when you have a reference to the accessory object.
596
-
597
- **Benefits**:
598
- - ✅ Simple and direct
599
- - ✅ TypeScript autocomplete works well
600
- - ✅ No additional function call overhead
601
- - ✅ Used in all official examples
602
-
603
- #### Method 2: API Method (Special Cases)
604
-
605
- Use the API method when you don't have a reference to the accessory object:
606
-
607
- ```typescript
608
- // Read state by UUID
609
- const state = api.matter.getAccessoryState(uuid, api.matter.clusterNames.OnOff)
610
- if (state) {
611
- const isOn = state.onOff // boolean
612
- }
613
-
614
- // Read brightness
615
- const levelState = api.matter.getAccessoryState(uuid, api.matter.clusterNames.LevelControl)
616
- if (levelState) {
617
- const level = levelState.currentLevel // 1-254
618
- }
619
- ```
620
-
621
- **When to use**:
622
- - When you only have the UUID (not the accessory object)
623
- - After plugin restart when you need to read state but lost local variables
624
- - When multiple parts of code need to access the same accessory state
625
- - For debugging and logging utilities
626
-
627
- **Note**: Returns `undefined` if the accessory or cluster is not found.
628
-
629
- ### Updating State
630
-
631
- Use `updateAccessoryState()` to manually update cluster attributes:
632
-
633
- ```typescript
634
- api.matter.updateAccessoryState(
635
- accessory.uuid, // UUID of the accessory
636
- api.matter.clusterNames.OnOff, // Cluster name (use constants!)
637
- { onOff: true } // New attribute values
638
- )
639
- ```
640
-
641
- **When to use**:
642
-
643
- 1. **External changes (Flow B)** - When your physical device changes state externally (most common use case)
644
- 2. **Side effects in handlers** - When a handler needs to update OTHER attributes as a side effect
645
-
646
- **IMPORTANT - What NOT to do**:
647
- - ❌ **Never update the same attribute that the handler is already updating**
648
-
649
- For example, in an `on()` handler, don't manually update `onOff` - it's automatically updated by Homebridge.
650
-
651
- **Valid example - Side effect updates**:
652
-
653
- If your physical light always resets to 100% brightness when turned on:
654
-
655
- ```typescript
656
- handlers: {
657
- onOff: {
658
- on: async () => {
659
- await myLightAPI.turnOn()
660
-
661
- // ✅ VALID: Update brightness as a side effect
662
- // The light physically resets to 100%, so update Matter to match
663
- api.matter.updateAccessoryState(
664
- accessory.uuid,
665
- api.matter.clusterNames.LevelControl,
666
- { currentLevel: 254 } // 100% brightness
667
- )
668
-
669
- // ❌ WRONG: Don't update onOff - it's automatically updated
670
- // api.matter.updateAccessoryState(
671
- // accessory.uuid,
672
- // api.matter.clusterNames.OnOff,
673
- // { onOff: true } // Redundant and unnecessary
674
- // )
675
- },
676
- },
677
- }
678
- ```
679
-
680
- ### Available Cluster Names
681
-
682
- Use these constants with `updateAccessoryState()` and `getAccessoryState()` for type safety:
683
-
684
- ```typescript
685
- // Common examples
686
- api.matter.clusterNames.OnOff
687
- api.matter.clusterNames.LevelControl
688
- api.matter.clusterNames.ColorControl
689
- api.matter.clusterNames.Thermostat
690
- // ... see full list below
691
- ```
692
-
693
- <details>
694
- <summary><strong>Click to see all available cluster names</strong></summary>
695
-
696
- #### Control Clusters
697
- ```typescript
698
- api.matter.clusterNames.OnOff
699
- api.matter.clusterNames.LevelControl
700
- api.matter.clusterNames.ColorControl
701
- api.matter.clusterNames.DoorLock
702
- api.matter.clusterNames.WindowCovering
703
- api.matter.clusterNames.Thermostat
704
- api.matter.clusterNames.FanControl
705
- ```
706
-
707
- #### Sensor Clusters
708
- ```typescript
709
- api.matter.clusterNames.TemperatureMeasurement
710
- api.matter.clusterNames.RelativeHumidityMeasurement
711
- api.matter.clusterNames.IlluminanceMeasurement
712
- api.matter.clusterNames.OccupancySensing
713
- api.matter.clusterNames.BooleanState
714
- api.matter.clusterNames.SmokeCoAlarm
715
- ```
716
-
717
- #### Robotic Vacuum Cleaner Clusters
718
- ```typescript
719
- api.matter.clusterNames.RvcRunMode
720
- api.matter.clusterNames.RvcOperationalState
721
- api.matter.clusterNames.RvcCleanMode
722
- ```
723
-
724
- #### Pump & Other
725
- ```typescript
726
- api.matter.clusterNames.PumpConfigurationAndControl
727
- ```
728
-
729
- #### Identification
730
- ```typescript
731
- api.matter.clusterNames.Identify
732
- ```
733
-
734
- #### Device Information (Read-Only)
735
- These are set during registration and cannot be updated:
736
- ```typescript
737
- api.matter.clusterNames.BasicInformation
738
- api.matter.clusterNames.BridgedDeviceBasicInformation
739
- ```
740
-
741
- </details>
742
-
743
- ### Updating Multiple Properties
744
-
745
- Update each cluster separately:
746
-
747
- ```typescript
748
- // Update power state
749
- api.matter.updateAccessoryState(
750
- accessory.uuid,
751
- api.matter.clusterNames.OnOff,
752
- { onOff: true }
753
- )
754
-
755
- // Update brightness
756
- api.matter.updateAccessoryState(
757
- accessory.uuid,
758
- api.matter.clusterNames.LevelControl,
759
- { currentLevel: 200 }
760
- )
761
-
762
- // Update multiple attributes in same cluster
763
- api.matter.updateAccessoryState(
764
- accessory.uuid,
765
- api.matter.clusterNames.ColorControl,
766
- {
767
- currentHue: 180,
768
- currentSaturation: 254,
769
- colorMode: api.matter.types.ColorControl.ColorMode.CurrentHueAndCurrentSaturation
770
- }
771
- )
772
- ```
773
-
774
- ---
775
-
776
- ## Monitoring External Changes
777
-
778
- To implement Flow B (physical device → Home app), you must monitor your physical device and call `updateAccessoryState()` when it changes externally.
779
-
780
- There are two approaches:
781
-
782
- ### Recommended: Event-Based Updates
783
-
784
- Use this when your device supports push notifications (MQTT, WebSocket, webhooks, SSE).
785
-
786
- **Advantages**:
787
- - ✅ Instant updates
788
- - ✅ More efficient
789
- - ✅ Better user experience
790
- - ✅ Lower overhead
791
-
792
- #### MQTT Example
793
-
794
- ```typescript
795
- import mqtt from 'mqtt'
796
-
797
- const mqttClient = mqtt.connect('mqtt://broker-url')
798
-
799
- mqttClient.subscribe('home/light-001/status')
800
- mqttClient.on('message', (topic, message) => {
801
- if (topic === 'home/light-001/status') {
802
- const deviceState = JSON.parse(message.toString())
803
- const deviceIsOn = deviceState.state === 'ON'
804
-
805
- // Compare with current Matter state
806
- const currentMatterState = accessory.clusters.onOff.onOff
807
-
808
- if (deviceIsOn !== currentMatterState) {
809
- log.info(`Device changed: ${deviceIsOn ? 'ON' : 'OFF'}`)
810
-
811
- // Update Matter state
812
- api.matter.updateAccessoryState(
813
- accessory.uuid,
814
- api.matter.clusterNames.OnOff,
815
- { onOff: deviceIsOn }
816
- )
817
- }
818
- }
819
- })
820
- ```
821
-
822
- #### WebSocket Example
823
-
824
- ```typescript
825
- import WebSocket from 'ws'
826
-
827
- const ws = new WebSocket('wss://api.example.com/devices/light-001/events')
828
-
829
- ws.on('message', (data) => {
830
- const event = JSON.parse(data.toString())
831
-
832
- if (event.type === 'state_changed') {
833
- const deviceIsOn = event.state === 'ON'
834
- const currentMatterState = accessory.clusters.onOff.onOff
835
-
836
- if (deviceIsOn !== currentMatterState) {
837
- api.matter.updateAccessoryState(
838
- accessory.uuid,
839
- api.matter.clusterNames.OnOff,
840
- { onOff: deviceIsOn }
841
- )
842
- }
843
- }
844
- })
845
-
846
- // Handle reconnection
847
- ws.on('close', () => {
848
- log.warn('WebSocket disconnected, reconnecting in 5s...')
849
- setTimeout(() => startWebSocket(), 5000)
850
- })
851
- ```
852
-
853
- #### Webhook Example
854
-
855
- ```typescript
856
- import express from 'express'
857
-
858
- const app = express()
859
- app.use(express.json())
860
-
861
- app.post('/webhook/light-001/state', (req, res) => {
862
- const deviceIsOn = req.body.state === 'ON'
863
- const currentMatterState = accessory.clusters.onOff.onOff
864
-
865
- if (deviceIsOn !== currentMatterState) {
866
- api.matter.updateAccessoryState(
867
- accessory.uuid,
868
- api.matter.clusterNames.OnOff,
869
- { onOff: deviceIsOn }
870
- )
871
- }
872
-
873
- res.sendStatus(200)
874
- })
875
-
876
- app.listen(3000)
877
- ```
878
-
879
- ### Fallback: Polling-Based Updates
880
-
881
- Use this **only** if your device doesn't support events.
882
-
883
- **Disadvantages**:
884
- - ⚠️ Delayed updates (depends on interval)
885
- - ⚠️ Higher network overhead
886
- - ⚠️ Can strain device APIs
887
- - ⚠️ Use 5-10 second intervals minimum
888
-
889
- ```typescript
890
- setInterval(async () => {
891
- try {
892
- // Fetch state from physical device
893
- const response = await fetch('https://api.example.com/devices/light-001/state')
894
- const data = await response.json()
895
- const deviceIsOn = data.state === 'ON'
896
-
897
- // Compare with current Matter state
898
- const currentMatterState = accessory.clusters.onOff.onOff
899
-
900
- // Only update if changed (avoid unnecessary updates)
901
- if (deviceIsOn !== currentMatterState) {
902
- log.info(`Device changed (polling): ${deviceIsOn ? 'ON' : 'OFF'}`)
903
-
904
- api.matter.updateAccessoryState(
905
- accessory.uuid,
906
- api.matter.clusterNames.OnOff,
907
- { onOff: deviceIsOn }
908
- )
909
- }
910
- } catch (error) {
911
- log.error(`Error polling device: ${error}`)
912
- }
913
- }, 5000) // Poll every 5 seconds
914
- ```
915
-
916
- ---
917
-
918
- ## Using Matter Types
919
-
920
- Homebridge provides access to all Matter.js cluster types via `api.matter.types`. This gives you type-safe access to enums, types, and constants.
921
-
922
- ### Accessing Types
923
-
924
- Matter types are available directly on the API - no imports needed:
925
-
926
- ```typescript
927
- // Use types with cluster names for type-safe updates
928
- api.matter.updateAccessoryState(
929
- uuid,
930
- api.matter.clusterNames.FanControl,
931
- { fanMode: api.matter.types.FanControl.FanMode.High }
932
- )
933
-
934
- // Everything is under api.matter
935
- api.matter.updateAccessoryState(
936
- uuid,
937
- api.matter.clusterNames.Thermostat,
938
- { systemMode: api.matter.types.Thermostat.SystemMode.Heat }
939
- )
940
- ```
941
-
942
- **Consistent API Pattern**:
943
- - `api.matter.deviceTypes.*` - Device type definitions
944
- - `api.matter.clusterNames.*` - Cluster name constants (strings)
945
- - `api.matter.types.*` - Matter.js type definitions and enums
946
- - All accessible from `api.matter` - no separate imports required
947
-
948
- ### Discovering Available Values
949
-
950
- Three ways to discover enum values:
951
-
952
- 1. **TypeScript Autocomplete**: Type `api.matter.types.FanControl.` and see suggestions
953
- 2. **Matter.js Reference**: https://github.com/project-chip/matter.js
954
- 3. **Matter Specification**: https://csa-iot.org/developer-resource/specifications-download-request/
955
-
956
- ### All Available Clusters
957
-
958
- Matter types provides access to ALL 130+ Matter clusters:
959
-
960
- ```typescript
961
- api.matter.types.FanControl
962
- api.matter.types.Thermostat
963
- api.matter.types.DoorLock
964
- api.matter.types.ColorControl
965
- api.matter.types.WindowCovering
966
- api.matter.types.SmokeCoAlarm
967
- api.matter.types.OccupancySensing
968
- api.matter.types.TemperatureMeasurement
969
- // ... and 120+ more!
970
- ```
971
-
972
- ---
973
-
974
- ## Best Practices
975
-
976
- ### 1. Always Compare Before Updating
977
-
978
- Avoid unnecessary updates by checking if state actually changed:
979
-
980
- ```typescript
981
- const currentState = accessory.clusters.onOff.onOff
982
- if (newState !== currentState) {
983
- api.matter.updateAccessoryState(...)
984
- }
985
- ```
986
-
987
- ### 2. Use Events Over Polling
988
-
989
- Whenever possible, use event-based updates (MQTT, WebSocket, webhooks) instead of polling for better performance and user experience.
990
-
991
- ### 3. Don't Update the Same Attribute in Handlers
992
-
993
- Handlers automatically update their own attribute. Only manually update OTHER attributes (side effects) or for external changes (Flow B).
994
-
995
- ```typescript
996
- // ❌ WRONG - Redundant update
997
- handlers: {
998
- onOff: {
999
- on: async () => {
1000
- await myDevice.turnOn()
1001
- api.matter.updateAccessoryState(uuid, api.matter.clusterNames.OnOff, { onOff: true })
1002
- // Redundant! onOff is already updated automatically
1003
- }
1004
- }
1005
- }
1006
-
1007
- // ✅ CORRECT - No manual update needed
1008
- handlers: {
1009
- onOff: {
1010
- on: async () => {
1011
- await myDevice.turnOn()
1012
- // State automatically updated
1013
- }
1014
- }
1015
- }
1016
-
1017
- // ✅ ALSO CORRECT - Update different attribute as side effect
1018
- handlers: {
1019
- onOff: {
1020
- on: async () => {
1021
- await myDevice.turnOn()
1022
- // Light resets to 100% when turned on, so update brightness too
1023
- api.matter.updateAccessoryState(uuid, api.matter.clusterNames.LevelControl, { currentLevel: 254 })
1024
- }
1025
- }
1026
- }
1027
- ```
1028
-
1029
- ### 4. Handle Errors Gracefully
1030
-
1031
- Always throw errors from handlers so the Home app can be notified of failures:
1032
-
1033
- ```typescript
1034
- handlers: {
1035
- onOff: {
1036
- on: async () => {
1037
- try {
1038
- // Control your physical device
1039
- await myDevice.turnOn()
1040
- log.info('Successfully turned on device')
1041
- } catch (error) {
1042
- // Log the error for debugging
1043
- log.error(`Failed to turn on device: ${error}`)
1044
-
1045
- // ✅ IMPORTANT: Re-throw the error
1046
- // This propagates the error to the Matter protocol, which:
1047
- // 1. Notifies the Home app that the command failed
1048
- // 2. Prevents state from updating incorrectly
1049
- // 3. Shows an error message to the user
1050
- throw error
1051
- }
1052
- },
1053
-
1054
- off: async () => {
1055
- try {
1056
- await myDevice.turnOff()
1057
- log.info('Successfully turned off device')
1058
- } catch (error) {
1059
- log.error(`Failed to turn off device: ${error}`)
1060
- throw error // Always re-throw!
1061
- }
1062
- },
1063
- },
1064
- }
1065
- ```
1066
-
1067
- **Why throw errors?**
1068
-
1069
- Without throwing:
1070
- - ❌ Home app thinks command succeeded
1071
- - ❌ State updates incorrectly (shows "on" when device is actually off)
1072
- - ❌ User has no feedback that something went wrong
1073
-
1074
- With throwing:
1075
- - ✅ Home app displays error to user
1076
- - ✅ State remains unchanged (accurate)
1077
- - ✅ User knows to try again or investigate the issue
1078
-
1079
- ### 5. Log Clearly
1080
-
1081
- Distinguish between the two flows in your logs:
1082
-
1083
- ```typescript
1084
- log.info('[Device] Home app → Physical device: Turning ON')
1085
- log.info('[Device] Physical device → Home app: State changed to ON')
1086
- ```
1087
-
1088
- ### 6. Use Cluster Name Constants
1089
-
1090
- Always use `api.matter.clusterNames.*` constants instead of strings:
1091
-
1092
- ```typescript
1093
- // ✅ CORRECT
1094
- api.matter.updateAccessoryState(uuid, api.matter.clusterNames.OnOff, {...})
1095
-
1096
- // ❌ WRONG
1097
- api.matter.updateAccessoryState(uuid, 'onOff', {...})
1098
- ```
1099
-
1100
- ---
1101
-
1102
- ## API Reference
1103
-
1104
- Complete reference for all Matter API methods and properties available in Homebridge.
1105
-
1106
- ### Platform API Methods
1107
-
1108
- #### `api.isMatterAvailable(): boolean`
1109
-
1110
- Check if Matter is available in the current version of Homebridge.
1111
-
1112
- **Returns:** `true` if Homebridge version is >= 2.0.0-alpha.0
1113
-
1114
- **Usage:**
1115
- ```typescript
1116
- if (api.isMatterAvailable()) {
1117
- log.info('Matter is available in this Homebridge version')
1118
- } else {
1119
- log.warn('Matter requires Homebridge >= 2.0.0-alpha.0')
1120
- }
1121
- ```
1122
-
1123
- **When to use:**
1124
- - Plugin compatibility checks
1125
- - Conditional feature loading
1126
- - Version-specific functionality
1127
-
1128
- ---
1129
-
1130
- #### `api.isMatterEnabled(): boolean`
1131
-
1132
- Check if Matter is enabled for this bridge instance.
1133
-
1134
- **Returns:** `true` if Matter is enabled in the bridge configuration
1135
-
1136
- **Configuration:**
1137
- - For main bridge: Set `bridge.matter = true` in config.json
1138
- - For child bridge: Set `_bridge.matter = true` in platform config
1139
-
1140
- **Usage:**
1141
- ```typescript
1142
- if (api.isMatterEnabled()) {
1143
- // Register Matter accessories
1144
- api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories)
1145
- } else {
1146
- log.info('Matter is not enabled for this bridge')
1147
- }
1148
- ```
1149
-
1150
- **When to use:**
1151
- - Runtime checks before registering Matter accessories
1152
- - Conditional accessory registration
1153
- - User feedback about Matter status
1154
-
1155
- ---
1156
-
1157
- ### Matter API Properties
1158
-
1159
- All properties are accessed via `api.matter.*`
1160
-
1161
- #### `api.matter.uuid`
1162
-
1163
- UUID generator for creating unique accessory identifiers (alias of `api.hap.uuid`).
1164
-
1165
- **Type:** `HAP['uuid']`
1166
-
1167
- **Methods:**
1168
- - `generate(data: string): string` - Generate deterministic UUID from string
1169
- - `isValid(uuid: string): boolean` - Validate UUID format
1170
-
1171
- **Usage:**
1172
- ```typescript
1173
- const uuid = api.matter.uuid.generate('my-light-123')
1174
- // Output: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
1175
-
1176
- if (api.matter.uuid.isValid(uuid)) {
1177
- // UUID is valid
1178
- }
1179
- ```
1180
-
1181
- **Important:**
1182
- - UUIDs must be deterministic (same input = same output)
1183
- - Use unique identifiers (device ID, MAC address, etc.)
1184
- - UUIDs persist across restarts for state restoration
1185
-
1186
- ---
1187
-
1188
- #### `api.matter.deviceTypes`
1189
-
1190
- Available Matter device types for creating accessories.
1191
-
1192
- **Type:** `typeof deviceTypes` (from Matter.js)
1193
-
1194
- **Common Device Types:**
1195
- - Lighting: `OnOffLight`, `DimmableLight`, `ColorTemperatureLight`, `ExtendedColorLight`
1196
- - Switches & Outlets: `OnOffSwitch`, `OnOffOutlet`
1197
- - Sensors: `ContactSensor`, `TemperatureSensor`, `HumiditySensor`, `OccupancySensor`, etc.
1198
- - HVAC: `Thermostat`, `Fan`
1199
- - Closure: `DoorLock`, `WindowCovering`
1200
- - Robotic: `RoboticVacuumCleaner`
1201
-
1202
- **Usage:**
1203
- ```typescript
1204
- const accessory = {
1205
- uuid: api.matter.uuid.generate('my-light'),
1206
- displayName: 'Living Room Light',
1207
- deviceType: api.matter.deviceTypes.DimmableLight,
1208
- // ...
1209
- }
1210
- ```
1211
-
1212
- **See:** [Available Device Types](#available-device-types) for complete list
1213
-
1214
- ---
1215
-
1216
- #### `api.matter.clusters`
1217
-
1218
- Direct access to Matter.js cluster definitions for advanced use cases.
1219
-
1220
- **Type:** `typeof clusters` (from Matter.js)
1221
-
1222
- **Usage:**
1223
- ```typescript
1224
- // Access cluster attributes programmatically
1225
- const onOffAttrs = api.matter.clusters.OnOffCluster.attributes
1226
- console.log(Object.keys(onOffAttrs))
1227
- // Output: ['onOff', 'clusterRevision', 'featureMap', ...]
1228
-
1229
- // Check if cluster supports specific features
1230
- const levelControlFeatures = api.matter.clusters.LevelControlCluster.features
1231
- ```
1232
-
1233
- **When to use:**
1234
- - Advanced cluster introspection
1235
- - Dynamic attribute discovery
1236
- - Custom cluster implementations
1237
-
1238
- **Note:** Most plugins should use the higher-level APIs instead.
1239
-
1240
- ---
1241
-
1242
- #### `api.matter.clusterNames`
1243
-
1244
- Cluster name constants for type safety and autocomplete with state methods.
1245
-
1246
- **Type:** `typeof clusterNames`
1247
-
1248
- **Available Names:**
1249
- - `OnOff`, `LevelControl`, `ColorControl`
1250
- - `DoorLock`, `WindowCovering`
1251
- - `Thermostat`, `FanControl`
1252
- - `TemperatureMeasurement`, `RelativeHumidityMeasurement`
1253
- - And many more...
1254
-
1255
- **Usage:**
1256
- ```typescript
1257
- // Type-safe cluster references
1258
- api.matter.updateAccessoryState(
1259
- uuid,
1260
- api.matter.clusterNames.OnOff, // Autocomplete available!
1261
- { onOff: true }
1262
- )
1263
-
1264
- const state = api.matter.getAccessoryState(
1265
- uuid,
1266
- api.matter.clusterNames.LevelControl
1267
- )
1268
- ```
1269
-
1270
- **Benefits:**
1271
- - Autocomplete in IDEs
1272
- - Compile-time error checking
1273
- - Prevents typos in cluster names
1274
-
1275
- ---
1276
-
1277
- #### `api.matter.types`
1278
-
1279
- Type-safe enum values for cluster attributes (modes, states, etc.).
1280
-
1281
- **Type:** `typeof MatterTypes` (from Homebridge)
1282
-
1283
- **Common Types:**
1284
- - `DoorLock.LockState` - Lock states (Locked, Unlocked, etc.)
1285
- - `DoorLock.LockType` - Lock types (DeadBolt, Magnetic, etc.)
1286
- - `FanControl.FanMode` - Fan modes (Off, Low, Medium, High, Auto, etc.)
1287
- - `FanControl.FanModeSequence` - Supported mode sequences
1288
- - `Thermostat.SystemMode` - HVAC modes (Off, Heat, Cool, Auto, etc.)
1289
- - `ColorControl.ColorMode` - Color modes (HS, XY, ColorTemperature)
1290
- - `RvcRunMode.ModeTag` - Vacuum run mode tags (Idle, Cleaning, Mapping)
1291
- - `RvcCleanMode.ModeTag` - Vacuum clean mode tags (Vacuum, Mop)
1292
- - `RvcOperationalState.OperationalState` - Vacuum states (Stopped, Running, Docked, etc.)
1293
-
1294
- **Usage:**
1295
- ```typescript
1296
- // Door lock states
1297
- clusters: {
1298
- doorLock: {
1299
- lockState: api.matter.types.DoorLock.LockState.Unlocked,
1300
- lockType: api.matter.types.DoorLock.LockType.DeadBolt
1301
- }
1302
- }
1303
-
1304
- // Fan modes
1305
- clusters: {
1306
- fanControl: {
1307
- fanMode: api.matter.types.FanControl.FanMode.Auto,
1308
- fanModeSequence: api.matter.types.FanControl.FanModeSequence.OffLowMedHigh
1309
- }
1310
- }
1311
-
1312
- // Color modes
1313
- clusters: {
1314
- colorControl: {
1315
- colorMode: api.matter.types.ColorControl.ColorMode.ColorTemperatureMireds
1316
- }
1317
- }
1318
- ```
1319
-
1320
- **Benefits:**
1321
- - Type safety prevents invalid values
1322
- - IDE autocomplete shows available options
1323
- - Self-documenting code
1324
- - Compile-time validation
1325
-
1326
- **See:** [Using Matter Types](#using-matter-types) for detailed examples
1327
-
1328
- ---
1329
-
1330
- ### Matter API Methods
1331
-
1332
- #### `api.matter.registerPlatformAccessories()`
1333
-
1334
- Register Matter accessories with the platform (standard registration method).
1335
-
1336
- **Signature:**
1337
- ```typescript
1338
- registerPlatformAccessories(
1339
- pluginIdentifier: string,
1340
- platformName: string,
1341
- accessories: MatterAccessory[]
1342
- ): void
1343
- ```
1344
-
1345
- **Parameters:**
1346
- - `pluginIdentifier` - Plugin identifier (e.g., `'homebridge-example'`)
1347
- - `platformName` - Platform name (e.g., `'ExamplePlatform'`)
1348
- - `accessories` - Array of Matter accessories to register
1349
-
1350
- **Usage:**
1351
- ```typescript
1352
- const PLUGIN_NAME = 'homebridge-example'
1353
- const PLATFORM_NAME = 'ExamplePlatform'
1354
-
1355
- const accessories = [
1356
- {
1357
- uuid: api.matter.uuid.generate('my-light'),
1358
- displayName: 'Living Room Light',
1359
- deviceType: api.matter.deviceTypes.OnOffLight,
1360
- // ...
1361
- }
1362
- ]
1363
-
1364
- api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories)
1365
- ```
1366
-
1367
- **When to use:**
1368
- - Standard accessory registration
1369
- - Multiple accessories on shared bridge
1370
- - Most common use case
1371
-
1372
- **See also:** `publishExternalAccessories()` for isolated accessories
1373
-
1374
- ---
1375
-
1376
- #### `api.matter.unregisterPlatformAccessories()`
1377
-
1378
- Unregister Matter accessories by UUID.
1379
-
1380
- **Signature:**
1381
- ```typescript
1382
- unregisterPlatformAccessories(
1383
- pluginIdentifier: string,
1384
- platformName: string,
1385
- accessories: MatterAccessory[]
1386
- ): void
1387
- ```
1388
-
1389
- **Parameters:**
1390
- - `pluginIdentifier` - Plugin identifier
1391
- - `platformName` - Platform name
1392
- - `accessories` - Array of accessories to unregister (only `uuid` is required)
1393
-
1394
- **Usage:**
1395
- ```typescript
1396
- // Unregister accessories
1397
- const accessoriesToRemove = [
1398
- { uuid: 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' }
1399
- ]
1400
-
1401
- api.matter.unregisterPlatformAccessories(
1402
- PLUGIN_NAME,
1403
- PLATFORM_NAME,
1404
- accessoriesToRemove
1405
- )
1406
- ```
1407
-
1408
- **When to use:**
1409
- - Removing accessories from Homebridge
1410
- - Cleanup during plugin shutdown
1411
- - User-initiated accessory removal
1412
-
1413
- ---
1414
-
1415
- #### `api.matter.publishExternalAccessories()`
1416
-
1417
- Publish accessories on dedicated Matter bridges (isolated from other accessories).
1418
-
1419
- **Signature:**
1420
- ```typescript
1421
- publishExternalAccessories(
1422
- pluginIdentifier: string,
1423
- accessories: MatterAccessory[]
1424
- ): void
1425
- ```
1426
-
1427
- **Parameters:**
1428
- - `pluginIdentifier` - Plugin identifier
1429
- - `accessories` - Array of accessories to publish externally
1430
-
1431
- **Usage:**
1432
- ```typescript
1433
- const accessories = [
1434
- {
1435
- uuid: api.matter.uuid.generate('robot-vacuum'),
1436
- displayName: 'Robot Vacuum',
1437
- deviceType: api.matter.deviceTypes.RoboticVacuumCleaner,
1438
- // ...
1439
- }
1440
- ]
1441
-
1442
- // Publish on dedicated bridge
1443
- api.matter.publishExternalAccessories(PLUGIN_NAME, accessories)
1444
- ```
1445
-
1446
- **When to use:**
1447
- - Robotic Vacuum Cleaners (required by Apple Home)
1448
- - Cameras and video doorbells
1449
- - Devices requiring isolation
1450
- - Testing single accessories
1451
-
1452
- **Behavior:**
1453
- - Each accessory gets its own Matter server instance
1454
- - Separate port allocation (e.g., 5541, 5542, etc.)
1455
- - Independent QR codes for commissioning
1456
- - Complete isolation from other accessories
1457
-
1458
- **Similar to:** HAP's `api.publishExternalAccessories()`
1459
-
1460
- ---
1461
-
1462
- #### `api.matter.updateAccessoryState()`
1463
-
1464
- Update accessory cluster state when device changes externally (Flow B).
1465
-
1466
- **Signature:**
1467
- ```typescript
1468
- updateAccessoryState(
1469
- uuid: string,
1470
- cluster: string,
1471
- attributes: Record<string, any>
1472
- ): void
1473
- ```
1474
-
1475
- **Parameters:**
1476
- - `uuid` - Accessory UUID
1477
- - `cluster` - Cluster name (use `api.matter.clusterNames.*`)
1478
- - `attributes` - Attributes to update (key-value pairs)
1479
-
1480
- **Usage:**
1481
- ```typescript
1482
- // Device turned on via native app
1483
- api.matter.updateAccessoryState(
1484
- uuid,
1485
- api.matter.clusterNames.OnOff,
1486
- { onOff: true }
1487
- )
1488
-
1489
- // Brightness changed via physical button
1490
- api.matter.updateAccessoryState(
1491
- uuid,
1492
- api.matter.clusterNames.LevelControl,
1493
- { currentLevel: 200 }
1494
- )
1495
-
1496
- // Update multiple attributes at once
1497
- api.matter.updateAccessoryState(
1498
- uuid,
1499
- api.matter.clusterNames.ColorControl,
1500
- {
1501
- colorMode: api.matter.types.ColorControl.ColorMode.ColorTemperatureMireds,
1502
- colorTemperatureMireds: 250
1503
- }
1504
- )
1505
- ```
1506
-
1507
- **IMPORTANT:**
1508
- - ❌ **DO NOT** use inside handlers (state updates automatically)
1509
- - ✅ **DO** use for external changes (webhooks, polling, events)
1510
-
1511
- **When to use:**
1512
- - Native app controls
1513
- - Physical button presses
1514
- - Webhook notifications
1515
- - Polling results
1516
- - MQTT/WebSocket messages
1517
-
1518
- **See:** [Flow B: Physical Device → Home App](#flow-b-physical-device--home-app-manual)
1519
-
1520
- ---
1521
-
1522
- #### `api.matter.getAccessoryState()`
1523
-
1524
- Get current cluster state from a Matter accessory.
1525
-
1526
- **Signature:**
1527
- ```typescript
1528
- getAccessoryState(
1529
- uuid: string,
1530
- cluster: string
1531
- ): Record<string, any> | undefined
1532
- ```
1533
-
1534
- **Parameters:**
1535
- - `uuid` - Accessory UUID
1536
- - `cluster` - Cluster name (use `api.matter.clusterNames.*`)
1537
-
1538
- **Returns:**
1539
- - Object with current attribute values, or `undefined` if not found
1540
-
1541
- **Usage:**
1542
- ```typescript
1543
- // Read OnOff state
1544
- const state = api.matter.getAccessoryState(uuid, api.matter.clusterNames.OnOff)
1545
- if (state?.onOff) {
1546
- log.info('Light is currently on')
1547
- }
1548
-
1549
- // Read level control state
1550
- const levelState = api.matter.getAccessoryState(
1551
- uuid,
1552
- api.matter.clusterNames.LevelControl
1553
- )
1554
- log.info(`Current brightness: ${levelState?.currentLevel}`)
1555
-
1556
- // Check color mode
1557
- const colorState = api.matter.getAccessoryState(
1558
- uuid,
1559
- api.matter.clusterNames.ColorControl
1560
- )
1561
- if (colorState?.colorMode === api.matter.types.ColorControl.ColorMode.ColorTemperatureMireds) {
1562
- log.info(`Color temp: ${colorState.colorTemperatureMireds} mireds`)
1563
- }
1564
- ```
1565
-
1566
- **When to use:**
1567
- - Reading state after plugin restart
1568
- - Verifying current state before changes
1569
- - Debugging and logging
1570
- - Conditional logic based on state
1571
-
1572
- **Note:** State is persisted across restarts automatically.
1573
-
1574
- ---
1575
-
1576
- ## Additional Resources
1577
-
1578
- - **Matter.js Documentation**: https://github.com/project-chip/matter.js
1579
- - **Matter Specification**: https://csa-iot.org/developer-resource/specifications-download-request/
1580
- - **Homebridge Documentation**: https://developers.homebridge.io
1581
- - **Example Devices**: See the `src/devices/` directory for complete working examples
1582
-
1583
- ---
1584
-
1585
- ## Appendix: Common Attribute Value Types
1586
-
1587
- | Type | Range/Format | Common Uses | Notes |
1588
- |-------------|---------------------------|--------------------------------------|------------------------------------------|
1589
- | Boolean | `true`, `false` | Power states, binary sensors | Simple on/off values |
1590
- | Uint8 | 0-254 | Brightness, hue, saturation | 0 often reserved, 254 = 100% |
1591
- | Uint16 | 0-65535 | Color XY values, extended ranges | Full 16-bit range |
1592
- | Enum | Varies | Modes, states | Use `api.matter.types` for type safety |
1593
- | Temperature | Hundredths of degrees C | Thermostat, temperature sensors | 2500 = 25.00°C |
1594
- | Percentage | 0-100 or 0-254 | Sensors (0-100), Controls (0-254) | Check device spec for range |
1595
- | Mireds | 147-454 | Color temperature | Reciprocal megakelvin: 1000000 / kelvin |
1596
-
1597
- ---
1598
-
1599
- ## Appendix: Value Conversion Formulas
1600
-
1601
- ### Brightness (Matter ↔ Percentage)
1602
-
1603
- ```typescript
1604
- // To Matter (1-254)
1605
- const matterLevel = Math.max(1, Math.round((percent / 100) * 254))
1606
-
1607
- // From Matter (to 0-100%)
1608
- const percent = Math.round((matterLevel / 254) * 100)
1609
- ```
1610
-
1611
- ### Hue (Matter ↔ Degrees)
1612
-
1613
- ```typescript
1614
- // To Matter (0-254)
1615
- const matterHue = Math.round((degrees / 360) * 254)
1616
-
1617
- // From Matter (to 0-360°)
1618
- const degrees = Math.round((matterHue / 254) * 360)
1619
- ```
1620
-
1621
- ### Saturation (Matter ↔ Percentage)
1622
-
1623
- ```typescript
1624
- // To Matter (0-254)
1625
- const matterSat = Math.round((percent / 100) * 254)
1626
-
1627
- // From Matter (to 0-100%)
1628
- const percent = Math.round((matterSat / 254) * 100)
1629
- ```
1630
-
1631
- ### Color Temperature (Mireds ↔ Kelvin)
1632
-
1633
- ```typescript
1634
- // To Mireds
1635
- const mireds = Math.round(1000000 / kelvin)
1636
-
1637
- // From Mireds
1638
- const kelvin = Math.round(1000000 / mireds)
1639
- ```
1640
-
1641
- ### XY Color (Matter ↔ Float)
1642
-
1643
- ```typescript
1644
- // To Matter (0-65535)
1645
- const matterX = Math.round(floatX * 65535)
1646
- const matterY = Math.round(floatY * 65535)
1647
-
1648
- // From Matter (to 0.0-1.0)
1649
- const floatX = matterX / 65535
1650
- const floatY = matterY / 65535
1651
- ```
1652
-
1653
- ### Temperature (Matter ↔ Celsius)
1654
-
1655
- ```typescript
1656
- // To Matter (hundredths)
1657
- const matterTemp = Math.round(celsius * 100)
1658
-
1659
- // From Matter
1660
- const celsius = matterTemp / 100
1661
- ```
1662
-
1663
- ---
1664
-
1665
- ## Device Reference
1666
-
1667
- This section documents all available Matter device types with their clusters, attributes, handlers, and usage examples.
1668
-
1669
- ### On/Off Light
1670
-
1671
- | Property | Value |
1672
- |--------------------------|--------------------------------------------------------|
1673
- | **Device Type** | `api.matter.deviceTypes.OnOffLight` |
1674
- | **Description** | A lighting device capable of being switched on or off. |
1675
- | **Matter Specification** | § 4.1 |
1676
-
1677
- #### Required Clusters
1678
-
1679
- ###### `OnOff` Cluster
1680
-
1681
- Controls the power state of the light.
1682
-
1683
- **Attributes**:
1684
-
1685
- ```typescript
1686
- // All OnOff cluster attributes via api.matter
1687
- const onOffAttrs = api.matter.clusters.OnOffCluster.attributes
1688
- console.log(Object.keys(onOffAttrs))
1689
- // Output: ['onOff', 'clusterRevision', 'featureMap', ...]
1690
- ```
1691
-
1692
- | Attribute | Type | Range/Values | Description |
1693
- |-----------|---------|-----------------|----------------------------------|
1694
- | `onOff` | boolean | `true`, `false` | Power state (true=on, false=off) |
1695
-
1696
- **Reading State**:
1697
-
1698
- ```typescript
1699
- const isOn = accessory.clusters.onOff.onOff
1700
- ```
1701
-
1702
- <details>
1703
- <summary><strong>Handlers</strong></summary>
1704
-
1705
- ```typescript
1706
- handlers: {
1707
- onOff: {
1708
- /**
1709
- * Called when user turns light ON via Home app
1710
- */
1711
- on: async () => {
1712
- // Control your physical device
1713
- await myLightAPI.turnOn()
1714
- // State automatically updated by Homebridge
1715
- },
1716
-
1717
- /**
1718
- * Called when user turns light OFF via Home app
1719
- */
1720
- off: async () => {
1721
- // Control your physical device
1722
- await myLightAPI.turnOff()
1723
- // State automatically updated by Homebridge
1724
- },
1725
- },
1726
- }
1727
- ```
1728
-
1729
- </details>
1730
-
1731
- ### Dimmable Light
1732
-
1733
- | Property | Value |
1734
- |--------------------------|--------------------------------------------------------|
1735
- | **Device Type** | `api.matter.deviceTypes.DimmableLight` |
1736
- | **Description** | A lighting device with on/off and brightness control. |
1737
- | **Matter Specification** | § 4.2 |
1738
-
1739
- #### Required Clusters
1740
-
1741
- ###### `OnOff` Cluster
1742
-
1743
- Controls the power state of the light.
1744
-
1745
- **Attributes**:
1746
-
1747
- | Attribute | Type | Range/Values | Description |
1748
- |-----------|---------|-----------------|----------------------------------|
1749
- | `onOff` | boolean | `true`, `false` | Power state (true=on, false=off) |
1750
-
1751
- **Reading State**:
1752
-
1753
- ```typescript
1754
- const isOn = accessory.clusters.onOff.onOff
1755
- ```
1756
-
1757
- ###### `LevelControl` Cluster
1758
-
1759
- Controls the brightness level of the light.
1760
-
1761
- **Attributes**:
1762
-
1763
- ```typescript
1764
- // All LevelControl cluster attributes via api.matter
1765
- const levelAttrs = api.matter.clusters.LevelControlCluster.attributes
1766
- console.log(Object.keys(levelAttrs))
1767
- // Output: ['currentLevel', 'minLevel', 'maxLevel', 'onLevel', 'options', ...]
1768
- ```
1769
-
1770
- | Attribute | Type | Range/Values | Description |
1771
- |----------------|--------|--------------|--------------------------------------------------|
1772
- | `currentLevel` | number | 1-254 | Current brightness (1 = 0.4%, 254 = 100%) |
1773
- | `minLevel` | number | 1-254 | Minimum brightness level |
1774
- | `maxLevel` | number | 1-254 | Maximum brightness level |
1775
- | `onLevel` | number | 0-254 | Brightness when turned on (0 = restore previous) |
1776
-
1777
- **Reading State**:
1778
-
1779
- ```typescript
1780
- const level = accessory.clusters.levelControl.currentLevel
1781
- const brightnessPercent = Math.round((level / 254) * 100)
1782
- ```
1783
-
1784
- <details>
1785
- <summary><strong>Handlers</strong></summary>
1786
-
1787
- ```typescript
1788
- handlers: {
1789
- onOff: {
1790
- on: async () => {
1791
- log.info('[Dimmable Light] Turning ON')
1792
- await myLightAPI.turnOn()
1793
- },
1794
-
1795
- off: async () => {
1796
- log.info('[Dimmable Light] Turning OFF')
1797
- await myLightAPI.turnOff()
1798
- },
1799
- },
1800
-
1801
- levelControl: {
1802
- /**
1803
- * Called when user adjusts brightness via Home app
1804
- * Also called when turning on with specific brightness
1805
- */
1806
- moveToLevelWithOnOff: async (request: MatterRequests.MoveToLevel) => {
1807
- const { level, transitionTime } = request
1808
- const brightnessPercent = Math.round((level / 254) * 100)
1809
-
1810
- log.info(`[Dimmable Light] Setting brightness to ${brightnessPercent}%`)
1811
- await myLightAPI.setBrightness(brightnessPercent, transitionTime)
1812
- },
1813
- },
1814
- }
1815
- ```
1816
-
1817
- </details>
1818
-
1819
- ---
1820
-
1821
- ### Color Temperature Light
1822
-
1823
- | Property | Value |
1824
- |--------------------------|---------------------------------------------------------------------------|
1825
- | **Device Type** | `api.matter.deviceTypes.ColorTemperatureLight` |
1826
- | **Description** | A lighting device with on/off, brightness, and color temperature control. |
1827
- | **Matter Specification** | § 4.3 |
1828
-
1829
- #### Required Clusters
1830
-
1831
- ###### `OnOff` Cluster
1832
-
1833
- Controls the power state of the light.
1834
-
1835
- **Attributes**:
1836
-
1837
- | Attribute | Type | Range/Values | Description |
1838
- |-----------|---------|-----------------|----------------------------------|
1839
- | `onOff` | boolean | `true`, `false` | Power state (true=on, false=off) |
1840
-
1841
- ###### `LevelControl` Cluster
1842
-
1843
- Controls the brightness level of the light.
1844
-
1845
- **Attributes**:
1846
-
1847
- | Attribute | Type | Range/Values | Description |
1848
- |----------------|--------|--------------|--------------------------------------------------|
1849
- | `currentLevel` | number | 1-254 | Current brightness (1 = 0.4%, 254 = 100%) |
1850
- | `minLevel` | number | 1-254 | Minimum brightness level |
1851
- | `maxLevel` | number | 1-254 | Maximum brightness level |
1852
-
1853
- ###### `ColorControl` Cluster
1854
-
1855
- Controls the color temperature of the light.
1856
-
1857
- **Attributes**:
1858
-
1859
- ```typescript
1860
- // All ColorControl cluster attributes via api.matter
1861
- const colorAttrs = api.matter.clusters.ColorControlCluster.attributes
1862
- console.log(Object.keys(colorAttrs))
1863
- ```
1864
-
1865
- | Attribute | Type | Range/Values | Description |
1866
- |---------------------------------|--------|--------------|-------------------------------------------------------|
1867
- | `colorMode` | number | 0-2 | Current color mode (2 = Color Temperature) |
1868
- | `colorTemperatureMireds` | number | 147-454 | Color temp in mireds (reciprocal megakelvin) |
1869
- | `colorTempPhysicalMinMireds` | number | 147-500 | Coolest temperature supported (e.g., 147 = ~6800K) |
1870
- | `colorTempPhysicalMaxMireds` | number | 147-500 | Warmest temperature supported (e.g., 454 = ~2200K) |
1871
-
1872
- **Reading State**:
1873
-
1874
- ```typescript
1875
- const mireds = accessory.clusters.colorControl.colorTemperatureMireds
1876
- const kelvin = Math.round(1000000 / mireds)
1877
- ```
1878
-
1879
- **Value Conversions**:
1880
-
1881
- ```typescript
1882
- // Kelvin to Mireds
1883
- const mireds = Math.round(1000000 / kelvin)
1884
-
1885
- // Mireds to Kelvin
1886
- const kelvin = Math.round(1000000 / mireds)
1887
- ```
1888
-
1889
- <details>
1890
- <summary><strong>Handlers</strong></summary>
1891
-
1892
- ```typescript
1893
- handlers: {
1894
- onOff: {
1895
- on: async () => {
1896
- log.info('[CCT Light] Turning ON')
1897
- await myLightAPI.turnOn()
1898
- },
1899
-
1900
- off: async () => {
1901
- log.info('[CCT Light] Turning OFF')
1902
- await myLightAPI.turnOff()
1903
- },
1904
- },
1905
-
1906
- levelControl: {
1907
- moveToLevelWithOnOff: async (request: MatterRequests.MoveToLevel) => {
1908
- const { level } = request
1909
- const brightnessPercent = Math.round((level / 254) * 100)
1910
-
1911
- log.info(`[CCT Light] Setting brightness to ${brightnessPercent}%`)
1912
- await myLightAPI.setBrightness(brightnessPercent)
1913
- },
1914
- },
1915
-
1916
- colorControl: {
1917
- /**
1918
- * Called when user adjusts color temperature via Home app
1919
- */
1920
- moveToColorTemperatureLogic: async (request: { targetMireds: number, transitionTime: number }) => {
1921
- const { targetMireds, transitionTime } = request
1922
- const kelvin = Math.round(1000000 / targetMireds)
1923
-
1924
- log.info(`[CCT Light] Setting color temp to ${kelvin}K (${targetMireds} mireds)`)
1925
- await myLightAPI.setColorTemperature(kelvin, transitionTime)
1926
- },
1927
- },
1928
- }
1929
- ```
1930
-
1931
- </details>
1932
-
1933
- ---
1934
-
1935
- ### Color Light
1936
-
1937
- | Property | Value |
1938
- |--------------------------|--------------------------------------------------------------------------------|
1939
- | **Device Type** | `api.matter.deviceTypes.ExtendedColorLight` |
1940
- | **Description** | A lighting device with on/off, brightness, and color (Hue/Saturation) control. |
1941
- | **Matter Specification** | § 4.4 |
1942
-
1943
- #### Required Clusters
1944
-
1945
- ###### `OnOff` Cluster
1946
-
1947
- Controls the power state of the light.
1948
-
1949
- **Attributes**:
1950
-
1951
- | Attribute | Type | Range/Values | Description |
1952
- |-----------|---------|-----------------|----------------------------------|
1953
- | `onOff` | boolean | `true`, `false` | Power state (true=on, false=off) |
1954
-
1955
- ###### `LevelControl` Cluster
1956
-
1957
- Controls the brightness level of the light.
1958
-
1959
- **Attributes**:
1960
-
1961
- | Attribute | Type | Range/Values | Description |
1962
- |----------------|--------|--------------|--------------------------------------------------|
1963
- | `currentLevel` | number | 1-254 | Current brightness (1 = 0.4%, 254 = 100%) |
1964
- | `minLevel` | number | 1-254 | Minimum brightness level |
1965
- | `maxLevel` | number | 1-254 | Maximum brightness level |
1966
-
1967
- ###### `ColorControl` Cluster
1968
-
1969
- Controls the color (Hue/Saturation or XY) of the light.
1970
-
1971
- **Attributes**:
1972
-
1973
- | Attribute | Type | Range/Values | Description |
1974
- |---------------------|--------|--------------|-------------------------------------|
1975
- | `colorMode` | number | 0-2 | Current color mode (0 = HS, 1 = XY) |
1976
- | `currentHue` | number | 0-254 | Current hue (maps to 0-360 degrees) |
1977
- | `currentSaturation` | number | 0-254 | Current saturation (maps to 0-100%) |
1978
- | `currentX` | number | 0-65535 | CIE 1931 x coordinate |
1979
- | `currentY` | number | 0-65535 | CIE 1931 y coordinate |
1980
-
1981
- **Reading State**:
1982
-
1983
- ```typescript
1984
- const hue = accessory.clusters.colorControl.currentHue
1985
- const saturation = accessory.clusters.colorControl.currentSaturation
1986
-
1987
- // Convert to degrees/percentage
1988
- const hueDegrees = Math.round((hue / 254) * 360)
1989
- const saturationPercent = Math.round((saturation / 254) * 100)
1990
- ```
1991
-
1992
- **Value Conversions**:
1993
-
1994
- ```typescript
1995
- // Hue: Degrees (0-360) to Matter (0-254)
1996
- const matterHue = Math.round((degrees / 360) * 254)
1997
- const degrees = Math.round((matterHue / 254) * 360)
1998
-
1999
- // Saturation: Percent (0-100) to Matter (0-254)
2000
- const matterSat = Math.round((percent / 100) * 254)
2001
- const percent = Math.round((matterSat / 254) * 100)
2002
-
2003
- // XY: Float (0.0-1.0) to Matter (0-65535)
2004
- const matterX = Math.round(floatX * 65535)
2005
- const floatX = matterX / 65535
2006
- ```
2007
-
2008
- <details>
2009
- <summary><strong>Handlers</strong></summary>
2010
-
2011
- ```typescript
2012
- handlers: {
2013
- onOff: {
2014
- on: async () => {
2015
- log.info('[Color Light] Turning ON')
2016
- await myLightAPI.turnOn()
2017
- },
2018
-
2019
- off: async () => {
2020
- log.info('[Color Light] Turning OFF')
2021
- await myLightAPI.turnOff()
2022
- },
2023
- },
2024
-
2025
- levelControl: {
2026
- moveToLevelWithOnOff: async (request: MatterRequests.MoveToLevel) => {
2027
- const { level } = request
2028
- const brightnessPercent = Math.round((level / 254) * 100)
2029
-
2030
- log.info(`[Color Light] Setting brightness to ${brightnessPercent}%`)
2031
- await myLightAPI.setBrightness(brightnessPercent)
2032
- },
2033
- },
2034
-
2035
- colorControl: {
2036
- /**
2037
- * Called when user adjusts color via XY coordinates in Home app
2038
- */
2039
- moveToColorLogic: async (request: { targetX: number, targetY: number, transitionTime: number }) => {
2040
- const { targetX, targetY, transitionTime } = request
2041
- const xFloat = (targetX / 65535).toFixed(4)
2042
- const yFloat = (targetY / 65535).toFixed(4)
2043
-
2044
- log.info(`[Color Light] Setting XY color to (${xFloat}, ${yFloat})`)
2045
- await myLightAPI.setColorXY(xFloat, yFloat, transitionTime)
2046
- },
2047
-
2048
- /**
2049
- * Called when user adjusts color via Hue/Saturation in Home app
2050
- */
2051
- moveToHueAndSaturationLogic: async (request: { targetHue: number, targetSaturation: number, transitionTime: number }) => {
2052
- const { targetHue, targetSaturation, transitionTime } = request
2053
- const hueDegrees = Math.round((targetHue / 254) * 360)
2054
- const saturationPercent = Math.round((targetSaturation / 254) * 100)
2055
-
2056
- log.info(`[Color Light] Setting color to ${hueDegrees}°, ${saturationPercent}%`)
2057
- await myLightAPI.setColorHS(hueDegrees, saturationPercent, transitionTime)
2058
- },
2059
- },
2060
- }
2061
- ```
2062
-
2063
- </details>
2064
-
2065
- ---
2066
-
2067
- ### Extended Color Light
2068
-
2069
- | Property | Value |
2070
- |--------------------------|---------------------------------------------------------------------------------------------------|
2071
- | **Device Type** | `api.matter.deviceTypes.ExtendedColorLight` |
2072
- | **Description** | A lighting device with on/off, brightness, color (Hue/Saturation), and color temperature control. |
2073
- | **Matter Specification** | § 4.4 |
2074
-
2075
- #### Required Clusters
2076
-
2077
- ###### `OnOff` Cluster
2078
-
2079
- Controls the power state of the light.
2080
-
2081
- **Attributes**:
2082
-
2083
- | Attribute | Type | Range/Values | Description |
2084
- |-----------|---------|-----------------|----------------------------------|
2085
- | `onOff` | boolean | `true`, `false` | Power state (true=on, false=off) |
2086
-
2087
- ###### `LevelControl` Cluster
2088
-
2089
- Controls the brightness level of the light.
2090
-
2091
- **Attributes**:
2092
-
2093
- | Attribute | Type | Range/Values | Description |
2094
- |----------------|--------|--------------|--------------------------------------------------|
2095
- | `currentLevel` | number | 1-254 | Current brightness (1 = 0.4%, 254 = 100%) |
2096
- | `minLevel` | number | 1-254 | Minimum brightness level |
2097
- | `maxLevel` | number | 1-254 | Maximum brightness level |
2098
-
2099
- ###### `ColorControl` Cluster
2100
-
2101
- Controls both color (Hue/Saturation or XY) and color temperature of the light.
2102
-
2103
- When updating state for Extended Color Light (Flow B), always update the `colorMode` attribute along with the color/temperature values to indicate which mode is active.
2104
-
2105
- **Attributes**:
2106
-
2107
- | Attribute | Type | Range/Values | Description |
2108
- |---------------------------------|--------|--------------|-------------------------------------------------------|
2109
- | `colorMode` | number | 0-2 | Current color mode (0 = HS, 1 = XY, 2 = ColorTemp) |
2110
- | `currentHue` | number | 0-254 | Current hue (maps to 0-360 degrees) |
2111
- | `currentSaturation` | number | 0-254 | Current saturation (maps to 0-100%) |
2112
- | `currentX` | number | 0-65535 | CIE 1931 x coordinate |
2113
- | `currentY` | number | 0-65535 | CIE 1931 y coordinate |
2114
- | `colorTemperatureMireds` | number | 147-454 | Color temp in mireds (when in ColorTemp mode) |
2115
- | `colorTempPhysicalMinMireds` | number | 147-500 | Coolest temperature supported |
2116
- | `colorTempPhysicalMaxMireds` | number | 147-500 | Warmest temperature supported |
2117
-
2118
- **Reading State**:
2119
-
2120
- ```typescript
2121
- // Check current mode
2122
- const mode = accessory.clusters.colorControl.colorMode
2123
- const ColorMode = api.matter.types.ColorControl.ColorMode
2124
-
2125
- if (mode === ColorMode.CurrentHueAndCurrentSaturation || mode === ColorMode.CurrentXAndCurrentY) {
2126
- // Light is in color mode
2127
- const hue = accessory.clusters.colorControl.currentHue
2128
- const sat = accessory.clusters.colorControl.currentSaturation
2129
- } else if (mode === ColorMode.ColorTemperatureMireds) {
2130
- // Light is in white/CCT mode
2131
- const mireds = accessory.clusters.colorControl.colorTemperatureMireds
2132
- const kelvin = Math.round(1000000 / mireds)
2133
- }
2134
- ```
2135
-
2136
- <details>
2137
- <summary><strong>Handlers</strong></summary>
2138
-
2139
- ```typescript
2140
- handlers: {
2141
- onOff: {
2142
- on: async () => {
2143
- log.info('[Extended Color Light] Turning ON')
2144
- await myLightAPI.turnOn()
2145
- },
2146
-
2147
- off: async () => {
2148
- log.info('[Extended Color Light] Turning OFF')
2149
- await myLightAPI.turnOff()
2150
- },
2151
- },
2152
-
2153
- levelControl: {
2154
- moveToLevelWithOnOff: async (request: MatterRequests.MoveToLevel) => {
2155
- const { level } = request
2156
- const brightnessPercent = Math.round((level / 254) * 100)
2157
-
2158
- log.info(`[Extended Color Light] Setting brightness to ${brightnessPercent}%`)
2159
- await myLightAPI.setBrightness(brightnessPercent)
2160
- },
2161
- },
2162
-
2163
- colorControl: {
2164
- /**
2165
- * Called when user adjusts color via XY coordinates in Home app
2166
- */
2167
- moveToColorLogic: async (request: { targetX: number, targetY: number, transitionTime: number }) => {
2168
- const { targetX, targetY, transitionTime } = request
2169
- const xFloat = (targetX / 65535).toFixed(4)
2170
- const yFloat = (targetY / 65535).toFixed(4)
2171
-
2172
- log.info(`[Extended Color Light] Setting XY color to (${xFloat}, ${yFloat})`)
2173
- await myLightAPI.setColorXY(xFloat, yFloat, transitionTime)
2174
- },
2175
-
2176
- /**
2177
- * Called when user adjusts color via Hue/Saturation in Home app
2178
- */
2179
- moveToHueAndSaturationLogic: async (request: { targetHue: number, targetSaturation: number, transitionTime: number }) => {
2180
- const { targetHue, targetSaturation, transitionTime } = request
2181
- const hueDegrees = Math.round((targetHue / 254) * 360)
2182
- const saturationPercent = Math.round((targetSaturation / 254) * 100)
2183
-
2184
- log.info(`[Extended Color Light] Setting color to ${hueDegrees}°, ${saturationPercent}%`)
2185
- await myLightAPI.setColorHS(hueDegrees, saturationPercent, transitionTime)
2186
- },
2187
-
2188
- /**
2189
- * Called when user adjusts color temperature via Home app
2190
- */
2191
- moveToColorTemperatureLogic: async (request: { targetMireds: number, transitionTime: number }) => {
2192
- const { targetMireds, transitionTime } = request
2193
- const kelvin = Math.round(1000000 / targetMireds)
2194
-
2195
- log.info(`[Extended Color Light] Setting color temp to ${kelvin}K (${targetMireds} mireds)`)
2196
- await myLightAPI.setColorTemperature(kelvin, transitionTime)
2197
- },
2198
- },
2199
- }
2200
- ```
2201
-
2202
- </details>
2203
-
2204
- ---
2205
-
2206
- ### On/Off Outlet
2207
-
2208
- | Property | Value |
2209
- |--------------------------|---------------------------------------------------------------------------|
2210
- | **Device Type** | `api.matter.deviceTypes.OnOffOutlet` |
2211
- | **Description** | A plug-in unit (smart plug) capable of being switched on or off. |
2212
- | **Matter Specification** | § 5.1 |
2213
-
2214
- #### Required Clusters
2215
-
2216
- ###### `OnOff` Cluster
2217
-
2218
- Controls the power state of the outlet.
2219
-
2220
- **Attributes**:
2221
-
2222
- | Attribute | Type | Range/Values | Description |
2223
- |-----------|---------|-----------------|----------------------------------|
2224
- | `onOff` | boolean | `true`, `false` | Power state (true=on, false=off) |
2225
-
2226
- **Reading State**:
2227
-
2228
- ```typescript
2229
- const isOn = accessory.clusters.onOff.onOff
2230
- ```
2231
-
2232
- <details>
2233
- <summary><strong>Handlers</strong></summary>
2234
-
2235
- ```typescript
2236
- handlers: {
2237
- onOff: {
2238
- /**
2239
- * Called when user turns outlet ON via Home app
2240
- */
2241
- on: async () => {
2242
- // Control your physical device
2243
- await myOutletAPI.turnOn()
2244
- // State automatically updated by Homebridge
2245
- },
2246
-
2247
- /**
2248
- * Called when user turns outlet OFF via Home app
2249
- */
2250
- off: async () => {
2251
- // Control your physical device
2252
- await myOutletAPI.turnOff()
2253
- // State automatically updated by Homebridge
2254
- },
2255
- },
2256
- }
2257
- ```
2258
-
2259
- </details>
2260
-
2261
- ---
2262
-
2263
- ### On/Off Switch
2264
-
2265
- | Property | Value |
2266
- |--------------------------|---------------------------------------------------------------------------|
2267
- | **Device Type** | `api.matter.deviceTypes.OnOffSwitch` |
2268
- | **Description** | A switch capable of being switched on or off. |
2269
- | **Matter Specification** | § 6.1 |
2270
-
2271
- #### Required Clusters
2272
-
2273
- ###### `OnOff` Cluster
2274
-
2275
- Controls the power state of the switch.
2276
-
2277
- **Attributes**:
2278
-
2279
- | Attribute | Type | Range/Values | Description |
2280
- |-----------|---------|-----------------|----------------------------------|
2281
- | `onOff` | boolean | `true`, `false` | Power state (true=on, false=off) |
2282
-
2283
- **Reading State**:
2284
-
2285
- ```typescript
2286
- const isOn = accessory.clusters.onOff.onOff
2287
- ```
2288
-
2289
- <details>
2290
- <summary><strong>Handlers</strong></summary>
2291
-
2292
- ```typescript
2293
- handlers: {
2294
- onOff: {
2295
- /**
2296
- * Called when user turns switch ON via Home app
2297
- */
2298
- on: async () => {
2299
- // Control your physical device
2300
- await mySwitchAPI.turnOn()
2301
- // State automatically updated by Homebridge
2302
- },
2303
-
2304
- /**
2305
- * Called when user turns switch OFF via Home app
2306
- */
2307
- off: async () => {
2308
- // Control your physical device
2309
- await mySwitchAPI.turnOff()
2310
- // State automatically updated by Homebridge
2311
- },
2312
- },
2313
- }
2314
- ```
2315
-
2316
- </details>
2317
-
2318
- ---
2319
-
2320
- ### Contact Sensor
2321
-
2322
- | Property | Value |
2323
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
2324
- | **Device Type** | `api.matter.deviceTypes.ContactSensor` |
2325
- | **Description** | A contact sensor that detects open/closed state (e.g., door sensor, window sensor). |
2326
- | **Matter Specification** | § 7.1 |
2327
-
2328
- #### Required Clusters
2329
-
2330
- ###### `BooleanState` Cluster
2331
-
2332
- Represents the contact sensor state using a boolean value.
2333
-
2334
- **IMPORTANT - Inverted Semantics**: The BooleanState cluster for contact sensors uses **inverted logic**:
2335
- - `true` = Contact **closed** / Normal state (door/window is closed)
2336
- - `false` = Contact **open** / Triggered state (door/window is open)
2337
-
2338
- This is opposite from intuitive expectation, so you must invert values when updating state.
2339
-
2340
- **Attributes**:
2341
-
2342
- | Attribute | Type | Range/Values | Description |
2343
- |--------------|---------|-----------------|----------------------------------------------------------|
2344
- | `stateValue` | boolean | `true`, `false` | Contact state (true=closed/normal, false=open/triggered) |
2345
-
2346
- **Reading State**:
2347
-
2348
- ```typescript
2349
- const stateValue = accessory.clusters.booleanState.stateValue
2350
- // true = closed/normal, false = open/triggered
2351
- const isOpen = !stateValue // Invert to get intuitive open/closed
2352
- ```
2353
-
2354
- **Updating State** (Flow B):
2355
-
2356
- ```typescript
2357
- // When your physical sensor reports state change
2358
- function updateContactState(isOpen: boolean) {
2359
- // IMPORTANT: Invert the value!
2360
- // Matter BooleanState: false = open/triggered, true = closed/normal
2361
- api.matter.updateAccessoryState(
2362
- uuid,
2363
- api.matter.clusterNames.BooleanState,
2364
- { stateValue: !isOpen } // Invert!
2365
- )
2366
-
2367
- log.info(`Contact state: ${isOpen ? 'OPEN' : 'CLOSED'}`)
2368
- }
2369
-
2370
- // Example: Door opened
2371
- updateContactState(true) // Sends stateValue: false to Matter
2372
-
2373
- // Example: Door closed
2374
- updateContactState(false) // Sends stateValue: true to Matter
2375
- ```
2376
-
2377
- **Initial State**:
2378
-
2379
- ```typescript
2380
- clusters: {
2381
- booleanState: {
2382
- stateValue: true, // true = closed/normal (safe default)
2383
- },
2384
- }
2385
- ```
2386
-
2387
- **Handler**: Contact sensors are read-only (no handlers needed).
2388
-
2389
- ---
2390
-
2391
- ### Occupancy Sensor
2392
-
2393
- | Property | Value |
2394
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
2395
- | **Device Type** | `api.matter.deviceTypes.MotionSensor` (with OccupancySensing cluster) |
2396
- | **Description** | A sensor that detects occupancy/motion using PIR, ultrasonic, or physical contact methods. |
2397
- | **Matter Specification** | § 7.3 |
2398
-
2399
- **Note**: Matter.js API calls this device type "MotionSensor" but it's actually an **Occupancy Sensor** - this is how it appears in Apple Home and other Matter controllers.
2400
-
2401
- #### Required Clusters
2402
-
2403
- ###### `OccupancySensing` Cluster
2404
-
2405
- Detects occupancy using various sensing methods.
2406
-
2407
- **Attributes**:
2408
-
2409
- | Attribute | Type | Range/Values | Description |
2410
- |-----------------------------|---------|-----------------|-------------------------------------------------|
2411
- | `occupancy.occupied` | boolean | `true`, `false` | Occupancy detected (true=occupied, false=clear) |
2412
- | `occupancySensorType` | number | 0-2 | Sensor type (0=PIR, 1=Ultrasonic, 2=Physical) |
2413
- | `occupancySensorTypeBitmap` | object | See below | Bitmap of supported sensor types |
2414
-
2415
- **Occupancy Sensor Type Bitmap**:
2416
-
2417
- ```typescript
2418
- {
2419
- pir: true, // Passive infrared
2420
- ultrasonic: false, // Ultrasonic
2421
- physicalContact: false, // Physical contact
2422
- }
2423
- ```
2424
-
2425
- **Reading State**:
2426
-
2427
- ```typescript
2428
- const isOccupied = accessory.clusters.occupancySensing.occupancy.occupied
2429
- ```
2430
-
2431
- **Updating State** (Flow B):
2432
-
2433
- ```typescript
2434
- // When your physical sensor detects motion/occupancy
2435
- mqttClient.on('message', (topic, message) => {
2436
- const detected = message.toString() === 'motion'
2437
-
2438
- api.matter.updateAccessoryState(
2439
- uuid,
2440
- api.matter.clusterNames.OccupancySensing,
2441
- { occupancy: { occupied: detected } }
2442
- )
2443
-
2444
- log.info(`Occupancy: ${detected ? 'detected' : 'clear'}`)
2445
- })
2446
- ```
2447
-
2448
- **Initial State with PIR Sensor**:
2449
-
2450
- ```typescript
2451
- // Configure OccupancySensor with PIR feature
2452
- const OccupancySensingServer = api.matter.deviceTypes.MotionSensor.requirements.OccupancySensingServer
2453
- const OccupancySensorWithPIR = api.matter.deviceTypes.MotionSensor.with(
2454
- OccupancySensingServer.with('PassiveInfrared'),
2455
- )
2456
-
2457
- {
2458
- deviceType: OccupancySensorWithPIR,
2459
- clusters: {
2460
- occupancySensing: {
2461
- occupancy: {
2462
- occupied: false, // No occupancy detected initially
2463
- },
2464
- occupancySensorType: 0, // PIR
2465
- occupancySensorTypeBitmap: {
2466
- pir: true,
2467
- ultrasonic: false,
2468
- physicalContact: false,
2469
- },
2470
- },
2471
- },
2472
- }
2473
- ```
2474
-
2475
- **Handler**: Occupancy sensors are read-only (no handlers needed).
2476
-
2477
- ---
2478
-
2479
- ### Window Covering
2480
-
2481
- | Property | Value |
2482
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
2483
- | **Device Type** | `api.matter.deviceTypes.WindowCovering` |
2484
- | **Description** | A motorized window covering with lift position control (e.g., blinds, shades, curtains). |
2485
- | **Matter Specification** | § 8.3 |
2486
-
2487
- #### Required Clusters
2488
-
2489
- ###### `WindowCovering` Cluster
2490
-
2491
- Controls lift position (and optionally tilt) of window coverings.
2492
-
2493
- **IMPORTANT - Inverted Position Semantics**: The WindowCovering cluster uses **inverted percentage** values:
2494
- - `0` = Fully **open** (100% open)
2495
- - `10000` = Fully **closed** (0% open)
2496
-
2497
- This is opposite from intuitive expectation. You must convert between "open percentage" and Matter's "closed percentage" in both directions.
2498
-
2499
- **Attributes**:
2500
-
2501
- | Attribute | Type | Range/Values | Description |
2502
- |-------------------------------------|--------|--------------|------------------------------------------------------------------|
2503
- | `currentPositionLiftPercent100ths` | number | 0-10000 | Current lift position (0=open, 10000=closed, in hundredths) |
2504
- | `targetPositionLiftPercent100ths` | number | 0-10000 | Target lift position (0=open, 10000=closed, in hundredths) |
2505
- | `currentPositionTiltPercent100ths` | number | 0-10000 | Current tilt angle (0=horizontal/open, 10000=vertical/closed) ¹ |
2506
- | `targetPositionTiltPercent100ths` | number | 0-10000 | Target tilt angle (0=horizontal/open, 10000=vertical/closed) ¹ |
2507
-
2508
- ¹ Tilt attributes only present on Venetian blinds with tilt control
2509
-
2510
- **Reading State**:
2511
-
2512
- ```typescript
2513
- // Read lift position (convert to open percentage)
2514
- const closedPercent100ths = accessory.clusters.windowCovering.currentPositionLiftPercent100ths
2515
- const closedPercent = closedPercent100ths / 100
2516
- const openPercent = 100 - closedPercent // Invert!
2517
-
2518
- log.info(`Blind is ${openPercent}% open`)
2519
-
2520
- // Read tilt position (convert to degrees for Venetian blinds)
2521
- const tiltPercent100ths = accessory.clusters.windowCovering.currentPositionTiltPercent100ths
2522
- const degrees = Math.round((tiltPercent100ths / 10000) * 90)
2523
- log.info(`Tilt angle: ${degrees}°`) // 0° = horizontal, 90° = vertical
2524
- ```
2525
-
2526
- **Value Conversions**:
2527
-
2528
- ```typescript
2529
- // Open percentage (0-100) to Matter value (0-10000)
2530
- function openPercentToMatter(openPercent: number): number {
2531
- const closedPercent = 100 - openPercent // Invert!
2532
- return Math.round(closedPercent * 100)
2533
- }
2534
-
2535
- // Matter value (0-10000) to open percentage (0-100)
2536
- function matterToOpenPercent(value: number): number {
2537
- const closedPercent = value / 100
2538
- return 100 - closedPercent // Invert!
2539
- }
2540
-
2541
- // Tilt degrees (0-90) to Matter value (0-10000)
2542
- function degreesToMatter(degrees: number): number {
2543
- return Math.round((degrees / 90) * 10000)
2544
- }
2545
-
2546
- // Matter value (0-10000) to tilt degrees (0-90)
2547
- function matterToDegrees(value: number): number {
2548
- return Math.round((value / 10000) * 90)
2549
- }
2550
- ```
2551
-
2552
- <details>
2553
- <summary><strong>Handlers</strong></summary>
2554
-
2555
- ```typescript
2556
- import type { MatterRequests } from 'homebridge'
2557
-
2558
- handlers: {
2559
- windowCovering: {
2560
- /**
2561
- * Called when user adjusts lift position via Home app
2562
- */
2563
- goToLiftPercentage: async (request: MatterRequests.GoToLiftPercentage) => {
2564
- const { liftPercent100thsValue } = request
2565
-
2566
- // Matter uses 0=open, 10000=closed, so invert to get open percentage
2567
- const closedPercent = liftPercent100thsValue / 100
2568
- const openPercent = 100 - closedPercent // Invert!
2569
-
2570
- log.info(`Moving to ${openPercent}% open`)
2571
- await myBlindAPI.setPosition(openPercent)
2572
- // State automatically updated by Homebridge
2573
- },
2574
-
2575
- /**
2576
- * Called when user presses UP button in Home app
2577
- */
2578
- upOrOpen: async () => {
2579
- log.info('Opening blind')
2580
- await myBlindAPI.open()
2581
- // Update state after physical device confirms
2582
- api.matter.updateAccessoryState(
2583
- uuid,
2584
- api.matter.clusterNames.WindowCovering,
2585
- {
2586
- currentPositionLiftPercent100ths: 0, // 0 = fully open
2587
- targetPositionLiftPercent100ths: 0,
2588
- }
2589
- )
2590
- },
2591
-
2592
- /**
2593
- * Called when user presses DOWN button in Home app
2594
- */
2595
- downOrClose: async () => {
2596
- log.info('Closing blind')
2597
- await myBlindAPI.close()
2598
- // Update state after physical device confirms
2599
- api.matter.updateAccessoryState(
2600
- uuid,
2601
- api.matter.clusterNames.WindowCovering,
2602
- {
2603
- currentPositionLiftPercent100ths: 10000, // 10000 = fully closed
2604
- targetPositionLiftPercent100ths: 10000,
2605
- }
2606
- )
2607
- },
2608
-
2609
- /**
2610
- * Called when user presses STOP button in Home app
2611
- */
2612
- stopMotion: async () => {
2613
- log.info('Stopping blind movement')
2614
- await myBlindAPI.stop()
2615
- },
2616
- },
2617
- }
2618
- ```
2619
-
2620
- </details>
2621
-
2622
- **Updating State** (Flow B):
2623
-
2624
- ```typescript
2625
- // When your physical blind moves to a new position
2626
- function updateBlindPosition(openPercent: number) {
2627
- // Convert open percentage to Matter's closed percentage (0=open, 10000=closed)
2628
- const closedPercent = 100 - openPercent // Invert!
2629
- const value = Math.round(closedPercent * 100)
2630
-
2631
- api.matter.updateAccessoryState(
2632
- uuid,
2633
- api.matter.clusterNames.WindowCovering,
2634
- {
2635
- currentPositionLiftPercent100ths: value,
2636
- targetPositionLiftPercent100ths: value,
2637
- }
2638
- )
2639
-
2640
- log.info(`Lift position: ${openPercent}% open`)
2641
- }
2642
-
2643
- // Example: Blind is 40% open
2644
- updateBlindPosition(40) // Sends value: 6000 to Matter (60% closed)
2645
- ```
2646
-
2647
- **For Venetian Blinds with Tilt**:
2648
-
2649
- Add tilt control handler and update method:
2650
-
2651
- ```typescript
2652
- // Handler for tilt control
2653
- handlers: {
2654
- windowCovering: {
2655
- // ... lift handlers above ...
2656
-
2657
- /**
2658
- * Called when user adjusts tilt angle via Home app
2659
- * Tilt is shown as 0-90° in Home app (0=horizontal, 90=vertical)
2660
- */
2661
- goToTiltPercentage: async (request: MatterRequests.GoToTiltPercentage) => {
2662
- const { tiltPercent100thsValue } = request
2663
-
2664
- // Matter tilt: 0=horizontal/open (0°), 10000=vertical/closed (90°)
2665
- const degrees = Math.round((tiltPercent100thsValue / 10000) * 90)
2666
-
2667
- log.info(`Tilting to ${degrees}°`)
2668
- await myBlindAPI.setTiltAngle(degrees)
2669
- // State automatically updated by Homebridge
2670
- },
2671
- },
2672
- }
2673
-
2674
- // Update tilt position (Flow B)
2675
- function updateTiltPosition(degrees: number) {
2676
- // Convert degrees (0-90) to Matter's tilt percentage (0=horizontal, 10000=vertical)
2677
- const value = Math.round((degrees / 90) * 10000)
2678
-
2679
- api.matter.updateAccessoryState(
2680
- uuid,
2681
- api.matter.clusterNames.WindowCovering,
2682
- {
2683
- currentPositionTiltPercent100ths: value,
2684
- targetPositionTiltPercent100ths: value,
2685
- }
2686
- )
2687
-
2688
- log.info(`Tilt position: ${degrees}°`)
2689
- }
2690
- ```
2691
-
2692
- ---
2693
-
2694
- ### Thermostat
2695
-
2696
- | Property | Value |
2697
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
2698
- | **Device Type** | `api.matter.deviceTypes.Thermostat` |
2699
- | **Description** | A thermostat with heating and/or cooling control, temperature setpoints, and system mode management. |
2700
- | **Matter Specification** | § 9.1 |
2701
-
2702
- #### Required Clusters
2703
-
2704
- ###### `Thermostat` Cluster
2705
-
2706
- Controls HVAC system mode, setpoints, and reports current temperature.
2707
-
2708
- **Attributes**:
2709
-
2710
- | Attribute | Type | Range/Values | Description |
2711
- |------------------------------|--------|--------------|-----------------------------------------------------------|
2712
- | `localTemperature` | number | -27315-32767 | Current temperature (hundredths of °C) ¹ |
2713
- | `occupiedHeatingSetpoint` | number | 700-3000 | Target heating temperature (hundredths of °C) |
2714
- | `occupiedCoolingSetpoint` | number | 1600-3200 | Target cooling temperature (hundredths of °C) |
2715
- | `systemMode` | number | 0-7 | Current system mode (0=Off, 1=Auto, 3=Cool, 4=Heat, etc.) |
2716
- | `controlSequenceOfOperation` | number | 0-5 | Supported modes (2=Heating only, 4=Cooling and Heating) |
2717
- | `minHeatSetpointLimit` | number | 700-3000 | Minimum heating setpoint |
2718
- | `maxHeatSetpointLimit` | number | 700-3000 | Maximum heating setpoint |
2719
- | `minCoolSetpointLimit` | number | 1600-3200 | Minimum cooling setpoint |
2720
- | `maxCoolSetpointLimit` | number | 1600-3200 | Maximum cooling setpoint |
2721
-
2722
- ¹ Temperature values are in **hundredths of degrees Celsius**: `2500` = `25.00°C`
2723
-
2724
- **System Mode Values**:
2725
-
2726
- | Value | Mode | Description |
2727
- |-------|-------------------|------------------------------------------------|
2728
- | 0 | Off | System is off |
2729
- | 1 | Auto | Automatic heating/cooling based on temperature |
2730
- | 2 | Reserved | Reserved (not used) |
2731
- | 3 | Cool | Cooling mode |
2732
- | 4 | Heat | Heating mode |
2733
- | 5 | Emergency Heating | Emergency heat mode |
2734
- | 6 | Precooling | Precooling mode |
2735
- | 7 | Fan Only | Fan only (no heating/cooling) |
2736
-
2737
- **Control Sequence of Operation Values**:
2738
-
2739
- | Value | Description |
2740
- |-------|---------------------------------|
2741
- | 0 | Cooling only |
2742
- | 1 | Cooling with reheat |
2743
- | 2 | Heating only |
2744
- | 3 | Heating with reheat |
2745
- | 4 | Cooling and heating |
2746
- | 5 | Cooling and heating with reheat |
2747
-
2748
- **Reading State**:
2749
-
2750
- ```typescript
2751
- // Read current temperature
2752
- const tempHundredths = accessory.clusters.thermostat.localTemperature
2753
- const celsius = tempHundredths / 100
2754
- log.info(`Current temperature: ${celsius}°C`)
2755
-
2756
- // Read heating setpoint
2757
- const heatSetpoint = accessory.clusters.thermostat.occupiedHeatingSetpoint / 100
2758
- log.info(`Heating setpoint: ${heatSetpoint}°C`)
2759
-
2760
- // Read cooling setpoint (if supported)
2761
- const coolSetpoint = accessory.clusters.thermostat.occupiedCoolingSetpoint / 100
2762
- log.info(`Cooling setpoint: ${coolSetpoint}°C`)
2763
-
2764
- // Read system mode
2765
- const mode = accessory.clusters.thermostat.systemMode
2766
- const modeNames = ['Off', 'Auto', 'Reserved', 'Cool', 'Heat', 'Emergency Heating', 'Precooling', 'Fan Only']
2767
- log.info(`System mode: ${modeNames[mode]}`)
2768
- ```
2769
-
2770
- **Initial State** (Heating and Cooling):
2771
-
2772
- ```typescript
2773
- clusters: {
2774
- thermostat: {
2775
- localTemperature: 2100, // 21.00°C
2776
- occupiedHeatingSetpoint: 2000, // 20.00°C
2777
- occupiedCoolingSetpoint: 2400, // 24.00°C
2778
- minHeatSetpointLimit: 700, // 7.00°C
2779
- maxHeatSetpointLimit: 3000, // 30.00°C
2780
- minCoolSetpointLimit: 1600, // 16.00°C
2781
- maxCoolSetpointLimit: 3200, // 32.00°C
2782
- controlSequenceOfOperation: 4, // Cooling and Heating
2783
- systemMode: 0, // Off
2784
- },
2785
- }
2786
- ```
2787
-
2788
- <details>
2789
- <summary><strong>Handlers</strong></summary>
2790
-
2791
- ```typescript
2792
- import type { MatterRequests } from 'homebridge'
2793
-
2794
- handlers: {
2795
- thermostat: {
2796
- /**
2797
- * Called when user changes system mode in Home app
2798
- */
2799
- systemModeChange: async (request: { systemMode: number, oldSystemMode: number }) => {
2800
- const { systemMode, oldSystemMode } = request
2801
-
2802
- // Matter Thermostat SystemMode enum: 0=Off, 1=Auto, 3=Cool, 4=Heat, 5=EmergencyHeat, 6=Precooling, 7=FanOnly
2803
- const modeNames = ['Off', 'Auto', 'Reserved', 'Cool', 'Heat', 'Emergency Heating', 'Precooling', 'Fan Only']
2804
- const modeName = modeNames[systemMode] || `Unknown (${systemMode})`
2805
-
2806
- log.info(`System mode changed to: ${modeName}`)
2807
- await myThermostatAPI.setSystemMode(systemMode)
2808
- // State automatically updated by Homebridge
2809
- },
2810
-
2811
- /**
2812
- * Called when user adjusts heating setpoint in Home app
2813
- */
2814
- heatingSetpointChange: async (request: { heatingSetpoint: number, oldHeatingSetpoint: number }) => {
2815
- const celsius = request.heatingSetpoint / 100
2816
-
2817
- log.info(`Heating setpoint changed to: ${celsius}°C`)
2818
- await myThermostatAPI.setHeatingSetpoint(celsius)
2819
- // State automatically updated by Homebridge
2820
- },
2821
-
2822
- /**
2823
- * Called when user adjusts cooling setpoint in Home app
2824
- */
2825
- coolingSetpointChange: async (request: { coolingSetpoint: number, oldCoolingSetpoint: number }) => {
2826
- const celsius = request.coolingSetpoint / 100
2827
-
2828
- log.info(`Cooling setpoint changed to: ${celsius}°C`)
2829
- await myThermostatAPI.setCoolingSetpoint(celsius)
2830
- // State automatically updated by Homebridge
2831
- },
2832
- },
2833
- }
2834
- ```
2835
-
2836
- </details>
2837
-
2838
- **Updating State** (Flow B):
2839
-
2840
- ```typescript
2841
- // Update current temperature
2842
- function updateTemperature(celsius: number) {
2843
- const value = Math.round(celsius * 100)
2844
-
2845
- api.matter.updateAccessoryState(
2846
- uuid,
2847
- api.matter.clusterNames.Thermostat,
2848
- { localTemperature: value }
2849
- )
2850
-
2851
- log.info(`Temperature: ${celsius}°C`)
2852
- }
2853
-
2854
- // Update heating setpoint
2855
- function updateHeatingSetpoint(celsius: number) {
2856
- const value = Math.round(celsius * 100)
2857
-
2858
- api.matter.updateAccessoryState(
2859
- uuid,
2860
- api.matter.clusterNames.Thermostat,
2861
- { occupiedHeatingSetpoint: value }
2862
- )
2863
-
2864
- log.info(`Heating setpoint: ${celsius}°C`)
2865
- }
2866
-
2867
- // Update cooling setpoint
2868
- function updateCoolingSetpoint(celsius: number) {
2869
- const value = Math.round(celsius * 100)
2870
-
2871
- api.matter.updateAccessoryState(
2872
- uuid,
2873
- api.matter.clusterNames.Thermostat,
2874
- { occupiedCoolingSetpoint: value }
2875
- )
2876
-
2877
- log.info(`Cooling setpoint: ${celsius}°C`)
2878
- }
2879
-
2880
- // Update system mode
2881
- function updateSystemMode(mode: number) {
2882
- api.matter.updateAccessoryState(
2883
- uuid,
2884
- api.matter.clusterNames.Thermostat,
2885
- { systemMode: mode }
2886
- )
2887
-
2888
- const modeNames = ['Off', 'Auto', 'Reserved', 'Cool', 'Heat', 'Emergency Heating', 'Precooling', 'Fan Only']
2889
- log.info(`System mode: ${modeNames[mode]}`)
2890
- }
2891
- ```
2892
-
2893
- ---
2894
-
2895
- ### Fan
2896
-
2897
- | Property | Value |
2898
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
2899
- | **Device Type** | `api.matter.deviceTypes.Fan` |
2900
- | **Description** | A fan with variable speed control and multiple fan modes. |
2901
- | **Matter Specification** | § 9.2 |
2902
-
2903
- #### Required Clusters
2904
-
2905
- ###### `FanControl` Cluster
2906
-
2907
- Controls fan mode and speed.
2908
-
2909
- **Attributes**:
2910
-
2911
- | Attribute | Type | Range/Values | Description |
2912
- |--------------------|--------|--------------|---------------------------------------------------------------|
2913
- | `fanMode` | number | 0-6 | Current fan mode (0=Off, 1=Low, 2=Medium, 3=High, 4=On, etc.) |
2914
- | `fanModeSequence` | number | 0-4 | Supported fan mode sequence |
2915
- | `percentSetting` | number | 0-100 | Target fan speed percentage (0=off, 1-100=on with speed) |
2916
- | `percentCurrent` | number | 0-100 | Current fan speed percentage |
2917
-
2918
- **Fan Mode Values**:
2919
-
2920
- | Value | Mode | Description |
2921
- |-------|--------|--------------------|
2922
- | 0 | Off | Fan is off |
2923
- | 1 | Low | Low speed |
2924
- | 2 | Medium | Medium speed |
2925
- | 3 | High | High speed |
2926
- | 4 | On | On (no speed info) |
2927
- | 5 | Auto | Automatic mode |
2928
- | 6 | Smart | Smart mode |
2929
-
2930
- **Fan Mode Sequence Values**:
2931
-
2932
- | Value | Description | Available Modes |
2933
- |-------|-----------------------|---------------------------|
2934
- | 0 | Off/Low/Med/High | Off, Low, Med, High |
2935
- | 1 | Off/Low/High | Off, Low, High |
2936
- | 2 | Off/Low/Med/High/Auto | Off, Low, Med, High, Auto |
2937
- | 3 | Off/Low/High/Auto | Off, Low, High, Auto |
2938
- | 4 | Off/On/Auto | Off, On, Auto |
2939
-
2940
- **Reading State**:
2941
-
2942
- ```typescript
2943
- const mode = accessory.clusters.fanControl.fanMode
2944
- const speed = accessory.clusters.fanControl.percentSetting
2945
-
2946
- const modeNames = ['Off', 'Low', 'Medium', 'High', 'On', 'Auto', 'Smart']
2947
- log.info(`Fan mode: ${modeNames[mode]}, Speed: ${speed}%`)
2948
- ```
2949
-
2950
- **Detecting On/Off vs Speed Changes**:
2951
-
2952
- The `percentSetting` attribute is used for both on/off control and speed adjustment. To distinguish between the two:
2953
-
2954
- ```typescript
2955
- // In percentSettingChange handler
2956
- fanControl: {
2957
- percentSettingChange: async (request: { percentSetting: number | null, oldPercentSetting: number | null }) => {
2958
- const percent = request.percentSetting ?? 0
2959
- const isOff = percent === 0
2960
- const wasOff = (request.oldPercentSetting ?? 0) === 0
2961
-
2962
- // Check if on/off state changed
2963
- if (isOff !== wasOff) {
2964
- log.info(`Fan turned ${isOff ? 'off' : 'on'}`)
2965
- await myFanAPI.setPower(!isOff)
2966
- }
2967
-
2968
- // Update speed (only if not turning off)
2969
- if (!isOff) {
2970
- log.info(`Fan speed changed to: ${percent}%`)
2971
- await myFanAPI.setSpeed(percent)
2972
- }
2973
- },
2974
- }
2975
- ```
2976
-
2977
- <details>
2978
- <summary><strong>Handlers</strong></summary>
2979
-
2980
- ```typescript
2981
- import type { MatterRequests } from 'homebridge'
2982
-
2983
- handlers: {
2984
- fanControl: {
2985
- /**
2986
- * Called when user uses step control (increase/decrease button)
2987
- */
2988
- step: async (request: MatterRequests.FanStep) => {
2989
- const { direction, wrap, lowestOff } = request
2990
- const dirStr = direction === 0 ? 'increase' : 'decrease'
2991
-
2992
- log.info(`Fan step ${dirStr} (wrap: ${wrap}, lowestOff: ${lowestOff})`)
2993
- await myFanAPI.step(direction, wrap, lowestOff)
2994
- // State automatically updated by Homebridge
2995
- },
2996
-
2997
- /**
2998
- * Called when user changes fan mode via Home app
2999
- */
3000
- fanModeChange: async (request: { fanMode: number, oldFanMode: number }) => {
3001
- const modeNames = ['Off', 'Low', 'Medium', 'High', 'On', 'Auto', 'Smart']
3002
- const modeName = modeNames[request.fanMode] || `Unknown (${request.fanMode})`
3003
-
3004
- log.info(`Fan mode changed to: ${modeName}`)
3005
- await myFanAPI.setMode(request.fanMode)
3006
- // State automatically updated by Homebridge
3007
- },
3008
-
3009
- /**
3010
- * Called when user adjusts fan speed via Home app
3011
- * Also handles on/off transitions
3012
- */
3013
- percentSettingChange: async (request: { percentSetting: number | null, oldPercentSetting: number | null }) => {
3014
- const percent = request.percentSetting ?? 0
3015
- const isOff = percent === 0
3016
- const wasOff = (request.oldPercentSetting ?? 0) === 0
3017
-
3018
- // Detect on/off state change
3019
- if (isOff !== wasOff) {
3020
- log.info(`Fan turned ${isOff ? 'off' : 'on'}`)
3021
- await myFanAPI.setPower(!isOff)
3022
- }
3023
-
3024
- // Update speed (only when not off)
3025
- if (!isOff) {
3026
- log.info(`Fan speed changed to: ${percent}%`)
3027
- await myFanAPI.setSpeed(percent)
3028
- }
3029
- // State automatically updated by Homebridge
3030
- },
3031
- },
3032
- }
3033
- ```
3034
-
3035
- </details>
3036
-
3037
- **Updating State** (Flow B):
3038
-
3039
- ```typescript
3040
- // Update fan mode
3041
- function updateFanMode(mode: number) {
3042
- api.matter.updateAccessoryState(
3043
- uuid,
3044
- api.matter.clusterNames.FanControl,
3045
- { fanMode: mode }
3046
- )
3047
-
3048
- const modeNames = ['Off', 'Low', 'Medium', 'High', 'On', 'Auto', 'Smart']
3049
- log.info(`Fan mode: ${modeNames[mode]}`)
3050
- }
3051
-
3052
- // Update fan speed
3053
- function updateFanSpeed(percent: number) {
3054
- api.matter.updateAccessoryState(
3055
- uuid,
3056
- api.matter.clusterNames.FanControl,
3057
- {
3058
- percentSetting: percent,
3059
- percentCurrent: percent,
3060
- }
3061
- )
3062
-
3063
- log.info(`Fan speed: ${percent}%`)
3064
- }
3065
- ```
3066
-
3067
- ---
3068
-
3069
- ### Light Sensor
3070
-
3071
- | Property | Value |
3072
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
3073
- | **Device Type** | `api.matter.deviceTypes.LightSensor` |
3074
- | **Description** | A sensor that measures ambient light levels. |
3075
- | **Matter Specification** | § 7.2 |
3076
-
3077
- #### Required Clusters
3078
-
3079
- ###### `IlluminanceMeasurement` Cluster
3080
-
3081
- Measures illuminance (light level) in lux.
3082
-
3083
- **Attributes**:
3084
-
3085
- | Attribute | Type | Range/Values | Description |
3086
- |--------------------|--------|--------------|-------------------------------------------|
3087
- | `measuredValue` | number | 0-65534 | Current light level (logarithmic scale) ¹ |
3088
- | `minMeasuredValue` | number | 1-65533 | Minimum measurable light level |
3089
- | `maxMeasuredValue` | number | 2-65534 | Maximum measurable light level |
3090
-
3091
- ¹ The `measuredValue` uses a logarithmic scale: `value = 10000 × log₁₀(lux)`
3092
-
3093
- **Value Conversion**:
3094
-
3095
- ```typescript
3096
- // Lux to Matter value
3097
- const matterValue = Math.round(10000 * Math.log10(lux))
3098
-
3099
- // Matter value to Lux
3100
- const lux = 10 ** (matterValue / 10000)
3101
- ```
3102
-
3103
- **Reading State**:
3104
-
3105
- ```typescript
3106
- const matterValue = accessory.clusters.illuminanceMeasurement.measuredValue
3107
- const lux = 10 ** (matterValue / 10000)
3108
- log.info(`Light level: ${lux.toFixed(1)} lux`)
3109
- ```
3110
-
3111
- **Updating State** (Flow B):
3112
-
3113
- ```typescript
3114
- // When your physical sensor reports new light level
3115
- function updateIlluminance(lux: number) {
3116
- const value = Math.round(10000 * Math.log10(lux))
3117
-
3118
- api.matter.updateAccessoryState(
3119
- uuid,
3120
- api.matter.clusterNames.IlluminanceMeasurement,
3121
- { measuredValue: value }
3122
- )
3123
-
3124
- log.info(`Illuminance: ${lux} lux`)
3125
- }
3126
-
3127
- // Example: 500 lux
3128
- updateIlluminance(500) // Sends value: ~27000
3129
- ```
3130
-
3131
- **Initial State**:
3132
-
3133
- ```typescript
3134
- clusters: {
3135
- illuminanceMeasurement: {
3136
- measuredValue: 5000, // ~3.16 lux
3137
- minMeasuredValue: 1, // Minimum
3138
- maxMeasuredValue: 65534, // Maximum
3139
- },
3140
- }
3141
- ```
3142
-
3143
- **Handler**: Light sensors are read-only (no handlers needed).
3144
-
3145
- ---
3146
-
3147
- ### Temperature Sensor
3148
-
3149
- | Property | Value |
3150
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
3151
- | **Device Type** | `api.matter.deviceTypes.TemperatureSensor` |
3152
- | **Description** | A sensor that measures ambient temperature. |
3153
- | **Matter Specification** | § 7.4 |
3154
-
3155
- #### Required Clusters
3156
-
3157
- ###### `TemperatureMeasurement` Cluster
3158
-
3159
- Measures temperature in degrees Celsius.
3160
-
3161
- **Attributes**:
3162
-
3163
- | Attribute | Type | Range/Values | Description |
3164
- |--------------------|--------|--------------|------------------------------------------|
3165
- | `measuredValue` | number | -27315-32767 | Current temperature (hundredths of °C) ¹ |
3166
- | `minMeasuredValue` | number | -27315-32767 | Minimum measurable temperature |
3167
- | `maxMeasuredValue` | number | -27315-32767 | Maximum measurable temperature |
3168
-
3169
- ¹ Temperature values are in **hundredths of degrees Celsius**: `2100` = `21.00°C`
3170
-
3171
- **Reading State**:
3172
-
3173
- ```typescript
3174
- const tempHundredths = accessory.clusters.temperatureMeasurement.measuredValue
3175
- const celsius = tempHundredths / 100
3176
- log.info(`Temperature: ${celsius}°C`)
3177
- ```
3178
-
3179
- **Updating State** (Flow B):
3180
-
3181
- ```typescript
3182
- // When your physical sensor reports new temperature
3183
- function updateTemperature(celsius: number) {
3184
- const value = Math.round(celsius * 100)
3185
-
3186
- api.matter.updateAccessoryState(
3187
- uuid,
3188
- api.matter.clusterNames.TemperatureMeasurement,
3189
- { measuredValue: value }
3190
- )
3191
-
3192
- log.info(`Temperature: ${celsius}°C`)
3193
- }
3194
-
3195
- // Example: 21.5°C
3196
- updateTemperature(21.5) // Sends value: 2150
3197
- ```
3198
-
3199
- **Initial State**:
3200
-
3201
- ```typescript
3202
- clusters: {
3203
- temperatureMeasurement: {
3204
- measuredValue: 2100, // 21.00°C
3205
- minMeasuredValue: -5000, // -50.00°C
3206
- maxMeasuredValue: 10000, // 100.00°C
3207
- },
3208
- }
3209
- ```
3210
-
3211
- **Handler**: Temperature sensors are read-only (no handlers needed).
3212
-
3213
- ---
3214
-
3215
- ### Humidity Sensor
3216
-
3217
- | Property | Value |
3218
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
3219
- | **Device Type** | `api.matter.deviceTypes.HumiditySensor` |
3220
- | **Description** | A sensor that measures relative humidity. |
3221
- | **Matter Specification** | § 7.7 |
3222
-
3223
- #### Required Clusters
3224
-
3225
- ###### `RelativeHumidityMeasurement` Cluster
3226
-
3227
- Measures relative humidity as a percentage.
3228
-
3229
- **Attributes**:
3230
-
3231
- | Attribute | Type | Range/Values | Description |
3232
- |--------------------|--------|--------------|----------------------------------------------|
3233
- | `measuredValue` | number | 0-10000 | Current humidity (hundredths of a percent) ¹ |
3234
- | `minMeasuredValue` | number | 0-9999 | Minimum measurable humidity |
3235
- | `maxMeasuredValue` | number | 1-10000 | Maximum measurable humidity |
3236
-
3237
- ¹ Humidity values are in **hundredths of a percent**: `5500` = `55.00%`
3238
-
3239
- **Reading State**:
3240
-
3241
- ```typescript
3242
- const humidityHundredths = accessory.clusters.relativeHumidityMeasurement.measuredValue
3243
- const percent = humidityHundredths / 100
3244
- log.info(`Humidity: ${percent}%`)
3245
- ```
3246
-
3247
- **Updating State** (Flow B):
3248
-
3249
- ```typescript
3250
- // When your physical sensor reports new humidity
3251
- function updateHumidity(percent: number) {
3252
- const value = Math.round(percent * 100)
3253
-
3254
- api.matter.updateAccessoryState(
3255
- uuid,
3256
- api.matter.clusterNames.RelativeHumidityMeasurement,
3257
- { measuredValue: value }
3258
- )
3259
-
3260
- log.info(`Humidity: ${percent}%`)
3261
- }
3262
-
3263
- // Example: 65.5%
3264
- updateHumidity(65.5) // Sends value: 6550
3265
- ```
3266
-
3267
- **Initial State**:
3268
-
3269
- ```typescript
3270
- clusters: {
3271
- relativeHumidityMeasurement: {
3272
- measuredValue: 5500, // 55%
3273
- minMeasuredValue: 0, // 0%
3274
- maxMeasuredValue: 10000, // 100%
3275
- },
3276
- }
3277
- ```
3278
-
3279
- **Handler**: Humidity sensors are read-only (no handlers needed).
3280
-
3281
- ---
3282
-
3283
- ### Smoke/CO Alarm
3284
-
3285
- | Property | Value |
3286
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
3287
- | **Device Type** | `api.matter.deviceTypes.SmokeSensor` (with SmokeAlarm and CoAlarm features) |
3288
- | **Description** | A combined smoke and carbon monoxide alarm sensor. |
3289
- | **Matter Specification** | § 7.9 |
3290
-
3291
- #### Required Clusters
3292
-
3293
- ###### `SmokeCoAlarm` Cluster
3294
-
3295
- Detects smoke and carbon monoxide with three alarm states.
3296
-
3297
- **Attributes**:
3298
-
3299
- | Attribute | Type | Range/Values | Description |
3300
- |---------------------------|---------|--------------|-------------------------------------------------------|
3301
- | `smokeState` | number | 0-2 | Smoke alarm state (0=Normal, 1=Warning, 2=Critical) |
3302
- | `coState` | number | 0-2 | CO alarm state (0=Normal, 1=Warning, 2=Critical) |
3303
- | `batteryAlert` | number | 0-2 | Battery level alert |
3304
- | `deviceMuted` | number | 0-2 | Device mute status |
3305
- | `testInProgress` | boolean | true/false | Whether self-test is running |
3306
- | `hardwareFaultAlert` | boolean | true/false | Hardware fault detected |
3307
- | `endOfServiceAlert` | number | 0-2 | End of service life alert |
3308
- | `interconnectSmokeAlarm` | number | 0-2 | Interconnected smoke alarm status |
3309
- | `interconnectCoAlarm` | number | 0-2 | Interconnected CO alarm status |
3310
- | `contaminationState` | number | 0-2 | Sensor contamination state |
3311
- | `smokeSensitivityLevel` | number | 0-2 | Smoke sensitivity level |
3312
- | `expressedState` | number | 0-10 | Overall alarm state |
3313
-
3314
- **Alarm State Values**:
3315
-
3316
- | Value | State | Description |
3317
- |-------|----------|-------------------------|
3318
- | 0 | Normal | No alarm detected |
3319
- | 1 | Warning | Warning level detected |
3320
- | 2 | Critical | Critical level detected |
3321
-
3322
- **Reading State**:
3323
-
3324
- ```typescript
3325
- const smokeState = accessory.clusters.smokeCoAlarm.smokeState
3326
- const coState = accessory.clusters.smokeCoAlarm.coState
3327
-
3328
- const stateNames = ['Normal', 'Warning', 'Critical']
3329
- log.info(`Smoke: ${stateNames[smokeState]}, CO: ${stateNames[coState]}`)
3330
- ```
3331
-
3332
- **Updating State** (Flow B):
3333
-
3334
- ```typescript
3335
- // Update smoke alarm state
3336
- function updateSmokeState(state: 0 | 1 | 2) {
3337
- api.matter.updateAccessoryState(
3338
- uuid,
3339
- api.matter.clusterNames.SmokeCoAlarm,
3340
- { smokeState: state }
3341
- )
3342
-
3343
- const stateStr = ['Normal', 'Warning', 'Critical'][state]
3344
- log.info(`Smoke state: ${stateStr}`)
3345
- }
3346
-
3347
- // Update CO alarm state
3348
- function updateCOState(state: 0 | 1 | 2) {
3349
- api.matter.updateAccessoryState(
3350
- uuid,
3351
- api.matter.clusterNames.SmokeCoAlarm,
3352
- { coState: state }
3353
- )
3354
-
3355
- const stateStr = ['Normal', 'Warning', 'Critical'][state]
3356
- log.info(`CO state: ${stateStr}`)
3357
- }
3358
-
3359
- // Example: Smoke detected
3360
- updateSmokeState(2) // Critical
3361
- ```
3362
-
3363
- **Initial State with Both Features**:
3364
-
3365
- ```typescript
3366
- // Configure Smoke/CO Alarm with both features
3367
- const SmokeCoAlarmServer = api.matter.deviceTypes.SmokeSensor.requirements.SmokeCoAlarmServer
3368
- const SmokeSensorWithBoth = api.matter.deviceTypes.SmokeSensor.with(
3369
- SmokeCoAlarmServer.with('SmokeAlarm', 'CoAlarm'),
3370
- )
3371
-
3372
- {
3373
- deviceType: SmokeSensorWithBoth,
3374
- clusters: {
3375
- smokeCoAlarm: {
3376
- smokeState: 0, // Normal
3377
- coState: 0, // Normal
3378
- batteryAlert: 0, // Normal
3379
- deviceMuted: 0, // Not muted
3380
- testInProgress: false, // No test running
3381
- hardwareFaultAlert: false, // No fault
3382
- endOfServiceAlert: 0, // Normal
3383
- interconnectSmokeAlarm: 0, // Normal
3384
- interconnectCoAlarm: 0, // Normal
3385
- contaminationState: 0, // Normal
3386
- smokeSensitivityLevel: 1, // Standard sensitivity
3387
- expressedState: 0, // Normal
3388
- },
3389
- },
3390
- }
3391
- ```
3392
-
3393
- **Handler**: Smoke/CO alarms are read-only (no handlers needed).
3394
-
3395
- ---
3396
-
3397
- ### Water Leak Detector
3398
-
3399
- | Property | Value |
3400
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
3401
- | **Device Type** | `api.matter.deviceTypes.LeakSensor` |
3402
- | **Description** | A sensor that detects water leaks. |
3403
- | **Matter Specification** | § 7.12 |
3404
-
3405
- #### Required Clusters
3406
-
3407
- ###### `BooleanState` Cluster
3408
-
3409
- Represents water leak detection state using a boolean value.
3410
-
3411
- **NOTE**: Unlike contact sensors, leak detectors use **standard (non-inverted) semantics**:
3412
- - `false` = No leak detected / Dry (normal state)
3413
- - `true` = Leak detected / Wet (triggered state)
3414
-
3415
- **Attributes**:
3416
-
3417
- | Attribute | Type | Range/Values | Description |
3418
- |--------------|---------|-----------------|----------------------------------------------|
3419
- | `stateValue` | boolean | `true`, `false` | Leak state (true=leak, false=dry) |
3420
-
3421
- **Reading State**:
3422
-
3423
- ```typescript
3424
- const leakDetected = accessory.clusters.booleanState.stateValue
3425
- log.info(`Leak: ${leakDetected ? 'DETECTED' : 'None'}`)
3426
- ```
3427
-
3428
- **Updating State** (Flow B):
3429
-
3430
- ```typescript
3431
- // When your physical sensor reports leak state
3432
- function updateLeakState(leakDetected: boolean) {
3433
- api.matter.updateAccessoryState(
3434
- uuid,
3435
- api.matter.clusterNames.BooleanState,
3436
- { stateValue: leakDetected }
3437
- )
3438
-
3439
- log.info(`Leak: ${leakDetected ? 'detected' : 'none'}`)
3440
- }
3441
-
3442
- // Example: Leak detected
3443
- updateLeakState(true) // Sends stateValue: true
3444
-
3445
- // Example: Leak cleared
3446
- updateLeakState(false) // Sends stateValue: false
3447
- ```
3448
-
3449
- **Initial State**:
3450
-
3451
- ```typescript
3452
- clusters: {
3453
- booleanState: {
3454
- stateValue: false, // false = dry/normal (safe default)
3455
- },
3456
- }
3457
- ```
3458
-
3459
- **Handler**: Leak sensors are read-only (no handlers needed).
3460
-
3461
- ---
3462
-
3463
- ### Door Lock
3464
-
3465
- | Property | Value |
3466
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
3467
- | **Device Type** | `api.matter.deviceTypes.DoorLock` |
3468
- | **Description** | A smart lock that can be locked and unlocked, optionally with PIN code support. |
3469
- | **Matter Specification** | § 8.1 |
3470
-
3471
- #### Required Clusters
3472
-
3473
- ###### `DoorLock` Cluster
3474
-
3475
- Controls door lock/unlock operations.
3476
-
3477
- **Attributes**:
3478
-
3479
- | Attribute | Type | Range/Values | Description |
3480
- |-------------------|---------|--------------|-------------------------------------------------------------|
3481
- | `lockState` | number | 0-2 | Current lock state (0=NotFullyLocked, 1=Locked, 2=Unlocked) |
3482
- | `lockType` | number | 0-11 | Type of lock mechanism |
3483
- | `actuatorEnabled` | boolean | true/false | Whether lock actuator is enabled |
3484
- | `operatingMode` | number | 0-4 | Current operating mode |
3485
-
3486
- **Lock State Values**:
3487
-
3488
- | Value | State | Description |
3489
- |-------|-----------------|--------------------------------|
3490
- | 0 | NotFullyLocked | Lock is not fully engaged |
3491
- | 1 | Locked | Lock is fully engaged |
3492
- | 2 | Unlocked | Lock is disengaged |
3493
-
3494
- **Lock Type Values** (common types):
3495
-
3496
- | Value | Type | Description |
3497
- |-------|------------|----------------------|
3498
- | 0 | DeadBolt | Standard deadbolt |
3499
- | 1 | Magnetic | Magnetic lock |
3500
- | 2 | Other | Other type |
3501
-
3502
- Access all lock types via `api.matter.types.DoorLock.LockType`:
3503
-
3504
- ```typescript
3505
- api.matter.types.DoorLock.LockType.DeadBolt
3506
- api.matter.types.DoorLock.LockType.Magnetic
3507
- api.matter.types.DoorLock.LockType.Mortise
3508
- // etc.
3509
- ```
3510
-
3511
- **Reading State**:
3512
-
3513
- ```typescript
3514
- const lockState = accessory.clusters.doorLock.lockState
3515
- const stateNames = ['NotFullyLocked', 'Locked', 'Unlocked']
3516
- log.info(`Lock state: ${stateNames[lockState]}`)
3517
- ```
3518
-
3519
- <details>
3520
- <summary><strong>Handlers</strong></summary>
3521
-
3522
- ```typescript
3523
- import type { MatterRequests } from 'homebridge'
3524
-
3525
- handlers: {
3526
- doorLock: {
3527
- /**
3528
- * Called when user locks the door via Home app
3529
- */
3530
- lockDoor: async (request?: MatterRequests.LockDoor) => {
3531
- const pinCode = request?.pinCode // Optional PIN code
3532
-
3533
- if (pinCode) {
3534
- log.info(`Locking door with PIN: ${Buffer.from(pinCode).toString()}`)
3535
- } else {
3536
- log.info('Locking door')
3537
- }
3538
-
3539
- await myLockAPI.lock()
3540
- // State automatically updated by Homebridge
3541
- },
3542
-
3543
- /**
3544
- * Called when user unlocks the door via Home app
3545
- */
3546
- unlockDoor: async (request?: MatterRequests.UnlockDoor) => {
3547
- const pinCode = request?.pinCode // Optional PIN code
3548
-
3549
- if (pinCode) {
3550
- log.info(`Unlocking door with PIN: ${Buffer.from(pinCode).toString()}`)
3551
- } else {
3552
- log.info('Unlocking door')
3553
- }
3554
-
3555
- await myLockAPI.unlock()
3556
- // State automatically updated by Homebridge
3557
- },
3558
- },
3559
- }
3560
- ```
3561
-
3562
- </details>
3563
-
3564
- **Updating State** (Flow B):
3565
-
3566
- ```typescript
3567
- // Update lock state
3568
- function updateLockState(state: 0 | 1 | 2) {
3569
- api.matter.updateAccessoryState(
3570
- uuid,
3571
- api.matter.clusterNames.DoorLock,
3572
- { lockState: state }
3573
- )
3574
-
3575
- const stateStr = ['NotFullyLocked', 'Locked', 'Unlocked'][state]
3576
- log.info(`Lock state: ${stateStr}`)
3577
- }
3578
-
3579
- // Example: Lock engaged
3580
- updateLockState(1) // Locked
3581
-
3582
- // Example: Lock disengaged
3583
- updateLockState(2) // Unlocked
3584
- ```
3585
-
3586
- **Initial State**:
3587
-
3588
- ```typescript
3589
- clusters: {
3590
- doorLock: {
3591
- lockState: api.matter.types.DoorLock.LockState.Unlocked, // 2
3592
- lockType: api.matter.types.DoorLock.LockType.DeadBolt, // 0
3593
- actuatorEnabled: true,
3594
- operatingMode: api.matter.types.DoorLock.OperatingMode.Normal, // 0
3595
- },
3596
- }
3597
- ```
3598
-
3599
- ---
3600
-
3601
- ### Robotic Vacuum Cleaner
3602
-
3603
- | Property | Value |
3604
- |--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
3605
- | **Device Type** | `api.matter.deviceTypes.RoboticVacuumCleaner` |
3606
- | **Description** | A robotic vacuum cleaner with run modes, operational states, and cleaning modes. |
3607
- | **Matter Specification** | § 12.1 |
3608
-
3609
- **IMPORTANT**: Robotic vacuum cleaners **must** be published on a dedicated Matter bridge using `api.matter.publishExternalAccessories()` for Apple Home compatibility.
3610
-
3611
- #### Required Clusters
3612
-
3613
- ###### `RvcRunMode` Cluster
3614
-
3615
- Controls the vacuum's run mode (Idle, Cleaning, etc.).
3616
-
3617
- **Attributes**:
3618
-
3619
- | Attribute | Type | Description |
3620
- |------------------|--------|------------------------------------------|
3621
- | `supportedModes` | array | List of supported run modes |
3622
- | `currentMode` | number | Current run mode |
3623
-
3624
- **Common Run Mode Tags**:
3625
-
3626
- | Tag Value | Name | Description |
3627
- |-----------|----------|--------------------------------|
3628
- | 16384 | Idle | Vacuum is idle/docked |
3629
- | 16385 | Cleaning | Vacuum is cleaning |
3630
- | 16386 | Mapping | Vacuum is mapping the space |
3631
-
3632
- ###### `RvcCleanMode` Cluster
3633
-
3634
- Controls the vacuum's cleaning mode (Vacuum, Mop, etc.).
3635
-
3636
- **Attributes**:
3637
-
3638
- | Attribute | Type | Description |
3639
- |------------------|--------|------------------------------------------|
3640
- | `supportedModes` | array | List of supported clean modes |
3641
- | `currentMode` | number | Current clean mode |
3642
-
3643
- **Common Clean Mode Tags**:
3644
-
3645
- | Tag Value | Name | Description |
3646
- |-----------|--------|--------------------------------|
3647
- | 16384 | Vacuum | Vacuum only mode |
3648
- | 16385 | Mop | Mop only mode |
3649
- | 16386 | Both | Vacuum and mop simultaneously |
3650
-
3651
- ###### `RvcOperationalState` Cluster
3652
-
3653
- Reports the vacuum's operational state and provides control commands.
3654
-
3655
- **Attributes**:
3656
-
3657
- | Attribute | Type | Description |
3658
- |------------------------|--------|--------------------------------------|
3659
- | `operationalStateList` | array | List of supported operational states |
3660
- | `operationalState` | number | Current operational state ID |
3661
-
3662
- **Common Operational State IDs**:
3663
-
3664
- | State ID | State | Description |
3665
- |----------|---------|--------------------------------|
3666
- | 0 | Stopped | Vacuum is stopped |
3667
- | 1 | Running | Vacuum is actively cleaning |
3668
- | 2 | Paused | Vacuum is paused |
3669
- | 3 | Error | Vacuum encountered an error |
3670
-
3671
- **Reading State**:
3672
-
3673
- ```typescript
3674
- const runMode = accessory.clusters.rvcRunMode.currentMode
3675
- const cleanMode = accessory.clusters.rvcCleanMode.currentMode
3676
- const opState = accessory.clusters.rvcOperationalState.operationalState
3677
-
3678
- const runModes = ['Idle', 'Cleaning']
3679
- const cleanModes = ['Vacuum', 'Mop']
3680
- const opStates = ['Stopped', 'Running', 'Paused', 'Error']
3681
-
3682
- log.info(`Run: ${runModes[runMode]}, Clean: ${cleanModes[cleanMode]}, State: ${opStates[opState]}`)
3683
- ```
3684
-
3685
- <details>
3686
- <summary><strong>Handlers</strong></summary>
3687
-
3688
- ```typescript
3689
- import type { MatterRequests } from 'homebridge'
3690
-
3691
- handlers: {
3692
- rvcRunMode: {
3693
- /**
3694
- * Called when user changes run mode via Home app
3695
- */
3696
- changeToMode: async (request: MatterRequests.ChangeToMode) => {
3697
- const { newMode } = request
3698
- const modeStr = ['Idle', 'Cleaning'][newMode] || 'Unknown'
3699
-
3700
- log.info(`Changing run mode to: ${modeStr}`)
3701
- await myVacuumAPI.setRunMode(newMode)
3702
- // State automatically updated by Homebridge
3703
- },
3704
- },
3705
-
3706
- rvcCleanMode: {
3707
- /**
3708
- * Called when user changes clean mode via Home app
3709
- */
3710
- changeToMode: async (request: MatterRequests.ChangeToMode) => {
3711
- const { newMode } = request
3712
- const modeStr = ['Vacuum', 'Mop'][newMode] || 'Unknown'
3713
-
3714
- log.info(`Changing clean mode to: ${modeStr}`)
3715
- await myVacuumAPI.setCleanMode(newMode)
3716
- // State automatically updated by Homebridge
3717
- },
3718
- },
3719
-
3720
- rvcOperationalState: {
3721
- /**
3722
- * Called when user starts the vacuum
3723
- */
3724
- start: async () => {
3725
- log.info('Starting vacuum')
3726
- await myVacuumAPI.start()
3727
- // Update state to Running (1)
3728
- api.matter.updateAccessoryState(
3729
- uuid,
3730
- api.matter.clusterNames.RvcOperationalState,
3731
- { operationalState: 1 }
3732
- )
3733
- },
3734
-
3735
- /**
3736
- * Called when user pauses the vacuum
3737
- */
3738
- pause: async () => {
3739
- log.info('Pausing vacuum')
3740
- await myVacuumAPI.pause()
3741
- // Update state to Paused (2)
3742
- api.matter.updateAccessoryState(
3743
- uuid,
3744
- api.matter.clusterNames.RvcOperationalState,
3745
- { operationalState: 2 }
3746
- )
3747
- },
3748
-
3749
- /**
3750
- * Called when user stops the vacuum
3751
- */
3752
- stop: async () => {
3753
- log.info('Stopping vacuum')
3754
- await myVacuumAPI.stop()
3755
- // Update state to Stopped (0)
3756
- api.matter.updateAccessoryState(
3757
- uuid,
3758
- api.matter.clusterNames.RvcOperationalState,
3759
- { operationalState: 0 }
3760
- )
3761
- },
3762
-
3763
- /**
3764
- * Called when user resumes the vacuum
3765
- */
3766
- resume: async () => {
3767
- log.info('Resuming vacuum')
3768
- await myVacuumAPI.resume()
3769
- // Update state to Running (1)
3770
- api.matter.updateAccessoryState(
3771
- uuid,
3772
- api.matter.clusterNames.RvcOperationalState,
3773
- { operationalState: 1 }
3774
- )
3775
- },
3776
- },
3777
- }
3778
- ```
3779
-
3780
- </details>
3781
-
3782
- **Updating State** (Flow B):
3783
-
3784
- ```typescript
3785
- // Update operational state
3786
- function updateOperationalState(state: number) {
3787
- api.matter.updateAccessoryState(
3788
- uuid,
3789
- api.matter.clusterNames.RvcOperationalState,
3790
- { operationalState: state }
3791
- )
3792
-
3793
- const states = ['Stopped', 'Running', 'Paused', 'Error']
3794
- log.info(`Operational state: ${states[state]}`)
3795
- }
3796
-
3797
- // Update run mode
3798
- function updateRunMode(mode: number) {
3799
- api.matter.updateAccessoryState(
3800
- uuid,
3801
- api.matter.clusterNames.RvcRunMode,
3802
- { currentMode: mode }
3803
- )
3804
-
3805
- const modes = ['Idle', 'Cleaning']
3806
- log.info(`Run mode: ${modes[mode]}`)
3807
- }
3808
-
3809
- // Update clean mode
3810
- function updateCleanMode(mode: number) {
3811
- api.matter.updateAccessoryState(
3812
- uuid,
3813
- api.matter.clusterNames.RvcCleanMode,
3814
- { currentMode: mode }
3815
- )
3816
-
3817
- const modes = ['Vacuum', 'Mop']
3818
- log.info(`Clean mode: ${modes[mode]}`)
3819
- }
3820
- ```
3821
-
3822
- **Initial State**:
3823
-
3824
- ```typescript
3825
- clusters: {
3826
- rvcRunMode: {
3827
- supportedModes: [
3828
- { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] },
3829
- { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] },
3830
- ],
3831
- currentMode: 0, // Idle
3832
- },
3833
- rvcCleanMode: {
3834
- supportedModes: [
3835
- { label: 'Vacuum', mode: 0, modeTags: [{ value: 16384 }] },
3836
- { label: 'Mop', mode: 1, modeTags: [{ value: 16385 }] },
3837
- ],
3838
- currentMode: 0, // Vacuum
3839
- },
3840
- rvcOperationalState: {
3841
- operationalStateList: [
3842
- { operationalStateId: 0, operationalStateLabel: 'Stopped' },
3843
- { operationalStateId: 1, operationalStateLabel: 'Running' },
3844
- { operationalStateId: 2, operationalStateLabel: 'Paused' },
3845
- { operationalStateId: 3, operationalStateLabel: 'Error' },
3846
- ],
3847
- operationalState: 0, // Stopped
3848
- },
3849
- }
3850
- ```
3851
-
3852
- **Publishing** (Required for Apple Home):
3853
-
3854
- ```typescript
3855
- // IMPORTANT: Robotic vacuums must be published externally
3856
- const accessories = [
3857
- {
3858
- uuid: api.matter.uuid.generate('robot-vacuum'),
3859
- displayName: 'Robot Vacuum',
3860
- deviceType: api.matter.deviceTypes.RoboticVacuumCleaner,
3861
- // ... configuration
3862
- }
3863
- ]
3864
-
3865
- // Use publishExternalAccessories instead of registerPlatformAccessories
3866
- api.matter.publishExternalAccessories(PLUGIN_NAME, accessories)
3867
-
3868
- // This creates a dedicated Matter bridge with its own QR code
3869
- ```