@homebridge-plugins/homebridge-matter 0.2.0-beta.8 → 0.2.0
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/CHANGELOG.md +1 -1
- package/MATTER_API.md +1553 -0
- package/README.md +1 -1
- package/config.schema.json +4 -4
- package/dist/devices/ContactSensorAccessory.js +3 -2
- package/dist/devices/ContactSensorAccessory.js.map +1 -1
- package/dist/devices/FanAccessory.js +2 -2
- package/dist/devices/FanAccessory.js.map +1 -1
- package/dist/devices/OccupancySensorAccessory.d.ts +9 -0
- package/dist/devices/{MotionSensorAccessory.js → OccupancySensorAccessory.js} +10 -9
- package/dist/devices/OccupancySensorAccessory.js.map +1 -0
- package/dist/devices/index.d.ts +1 -1
- package/dist/devices/index.js +1 -1
- package/dist/devices/index.js.map +1 -1
- package/dist/platform.js +5 -5
- package/dist/platform.js.map +1 -1
- package/package.json +3 -3
- package/dist/devices/MotionSensorAccessory.d.ts +0 -9
- package/dist/devices/MotionSensorAccessory.js.map +0 -1
package/MATTER_API.md
CHANGED
|
@@ -2314,3 +2314,1556 @@ handlers: {
|
|
|
2314
2314
|
```
|
|
2315
2315
|
|
|
2316
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
|
+
```
|