@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/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
+ ```