MatterOccupancySensor

About

The MatterOccupancySensor class provides an occupancy sensor endpoint for Matter networks. This endpoint implements the Matter occupancy sensing standard for detecting occupied/unoccupied states (e.g., motion sensors, PIR sensors).

Features: * Occupancy state reporting (occupied/unoccupied) * Multiple sensor type support (PIR, Ultrasonic, Physical Contact) * HoldTime attribute (seconds) stored and reportable to controllers * HoldTimeLimits (min, max, default) required before a controller can write a non-zero HoldTime * HoldTime change callback for real-time updates from Matter controllers * Simple boolean state * Read-only sensor (no control functionality) * Automatic state updates * Integration with Home Assistant, Apple Home, Amazon Alexa, and Google Home * Matter standard compliance

Use Cases: * Motion sensors (PIR) * Occupancy detection * Security systems * Smart lighting automation * Energy management (turn off lights when unoccupied)

API Reference

Constructor

MatterOccupancySensor

Creates a new Matter occupancy sensor endpoint.

MatterOccupancySensor();

Initialization

begin

Initializes the Matter occupancy sensor endpoint with an initial occupancy state and sensor type.

bool begin(bool _occupancyState = false, OccupancySensorType_t _occupancySensorType = OCCUPANCY_SENSOR_TYPE_PIR);
  • _occupancyState - Initial occupancy state (true = occupied, false = unoccupied, default: false)

  • _occupancySensorType - Sensor type (default: OCCUPANCY_SENSOR_TYPE_PIR)

This function will return true if successful, false otherwise.

end

Stops processing Matter occupancy sensor events.

void end();

Sensor Types

OccupancySensorType_t

Occupancy sensor type enumeration:

  • OCCUPANCY_SENSOR_TYPE_PIR - Passive Infrared (PIR) sensor

  • OCCUPANCY_SENSOR_TYPE_ULTRASONIC - Ultrasonic sensor

  • OCCUPANCY_SENSOR_TYPE_PIR_AND_ULTRASONIC - Combined PIR and Ultrasonic

  • OCCUPANCY_SENSOR_TYPE_PHYSICAL_CONTACT - Physical contact sensor

getOccupancySensorType

Returns the sensor type passed to begin(). CHIP cluster init reads this via halOccupancyGetSensorType().

OccupancySensorType_t getOccupancySensorType();

Occupancy State Control

setOccupancy

Sets the occupancy state.

bool setOccupancy(bool _occupancyState);
  • _occupancyState - Occupancy state (true = occupied, false = unoccupied)

This function will return true if successful, false otherwise.

getOccupancy

Gets the current occupancy state.

bool getOccupancy();

This function will return true if occupied, false if unoccupied.

HoldTime Control

setHoldTime

Sets the HoldTime attribute (in seconds). CHIP stores and reports this value. setOccupancy(false) is not delayed by HoldTime — the sketch (or HAL) must keep occupied until the hold expires. See the HoldTime example.

bool setHoldTime(uint16_t _holdTime_seconds);
  • _holdTime_seconds - HoldTime value in seconds

Important: This function must be called after Matter.begin() has been called, as it requires the Matter event loop to be running. When limits are configured (holdTimeMax > 0), the value must fall in that range.

This function will return true if successful, false otherwise.

getHoldTime

Gets the current HoldTime value (in seconds).

uint16_t getHoldTime();

This function will return the current HoldTime value in seconds.

setHoldTimeLimits

Sets the HoldTime limits (minimum, maximum, and default values). These limits define the valid range for HoldTime values and provide metadata for Matter controllers.

bool setHoldTimeLimits(uint16_t _holdTimeMin_seconds, uint16_t _holdTimeMax_seconds, uint16_t _holdTimeDefault_seconds);
  • _holdTimeMin_seconds - Minimum HoldTime value in seconds

  • _holdTimeMax_seconds - Maximum HoldTime value in seconds

  • _holdTimeDefault_seconds - Default/recommended HoldTime value in seconds (informational metadata for controllers)

Important: * This function must be called after Matter.begin() has been called, as it requires the Matter event loop to be running. * The holdTimeDefault_seconds parameter is informational metadata for Matter controllers (recommended default value). It does NOT automatically set the HoldTime attribute - use setHoldTime() to set the actual value. * If the current HoldTime value is outside the new limits, it will be automatically adjusted to the nearest limit (minimum or maximum). * Call this before a Matter controller writes HoldTime. Until max > 0, CHIP rejects any non-zero controller HoldTime write (limits start at 0/0/0).

This function will return true if successful, false otherwise.

onHoldTimeChange

Sets a callback function that will be called when the HoldTime value is changed by a Matter Controller.

void onHoldTimeChange(HoldTimeChangeCB onHoldTimeChangeCB);
  • onHoldTimeChangeCB - Callback function of type HoldTimeChangeCB

The callback function signature is:

using HoldTimeChangeCB = std::function<bool(uint16_t holdTime_seconds)>;

The callback receives the new HoldTime value and can return true to accept the change or false to reject it.

Example:

OccupancySensor.onHoldTimeChange([](uint16_t holdTime_seconds) -> bool {
    Serial.printf("HoldTime changed to %u seconds\n", holdTime_seconds);
    return true;  // Accept the change
});

Operators

bool operator

Returns the current occupancy state.

operator bool();

Example:

if (mySensor) {
    Serial.println("Room is occupied");
} else {
    Serial.println("Room is unoccupied");
}

Assignment operator

Sets the occupancy state.

void operator=(bool _occupancyState);

Example:

mySensor = true;   // Set to occupied
mySensor = false;  // Set to unoccupied

Example

Basic Occupancy Sensor

// Copyright 2025 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
 * This example is an example code that will create a Matter Device which can be
 * commissioned and controlled from a Matter Environment APP.
 * Additionally the ESP32 will send debug messages indicating the Matter activity.
 * Turning DEBUG Level ON may be useful to following Matter Accessory and Controller messages.
 *
 * The example will create a Matter Occupancy Sensor Device.
 * The Occupancy Sensor will be simulated to change its state every 2 minutes.
 *
 * The onboard button can be kept pressed for 5 seconds to decommission the Matter Node.
 * The example will also show the manual commissioning code and QR code to be used in the Matter environment.
 *
 */

// Matter Manager
#include <Arduino.h>
#include <Matter.h>
// List of Matter Endpoints for this Node
// Matter Occupancy Sensor Endpoint
MatterOccupancySensor OccupancySensor;

// Wi-Fi credentials for this sketch. Fill these in when the board cannot
// commission over BLE (Arduino prebuild on ESP32 / ESP32-S2): the sketch
// joins the AP itself. When Matter commissions over BLE (CHIPoBLE), the
// hub sends SSID and password — leave the placeholders; they are unused.
#define WIFI_SSID     "your-ssid"
#define WIFI_PASSWORD "your-password"

// set your board USER BUTTON pin here - decommissioning only
const uint8_t buttonPin = BOOT_PIN;  // Set your pin here. Using BOOT Button.
MatterButton button;

void setup() {
  // Initialize the USER BUTTON (Boot button) that will be used to decommission the Matter Node
  button.begin(buttonPin);

  Serial.begin(115200);

// CONFIG_ENABLE_CHIPOBLE=n: sketch starts Wi-Fi here; with CHIPoBLE the hub delivers credentials.
#if !CONFIG_ENABLE_CHIPOBLE
  matterConnectWiFi(WIFI_SSID, WIFI_PASSWORD);
#endif

  // set initial occupancy sensor state as false and connected to a PIR sensor type (default)
  OccupancySensor.begin();

  // Matter beginning - Last step, after all EndPoints are initialized
  Matter.begin();
  matterWaitUntilReady();
}

bool simulatedHWOccupancySensor() {
  // Simulated Occupancy Sensor
  static bool occupancyState = false;
  static uint32_t lastTime = millis();
  const uint32_t occupancyTimeout = 120000;  // 2 minutes to toggle the state

  // Simulate a Occupancy Sensor state change every 2 minutes
  if (millis() - lastTime > occupancyTimeout) {
    occupancyState = !occupancyState;
    lastTime = millis();
  }
  return occupancyState;
}

void loop() {
  matterRestartIfNoFabric();

  matterButtonEvent_t ev;
  while ((ev = button.poll()) != MATTER_BUTTON_NONE) {
    if (ev == MATTER_BUTTON_LONG_HOLD) {
      Serial.println("Decommissioning Occupancy Sensor Matter Accessory. It shall be commissioned again.");
      Matter.decommission();
    }
  }

  // Check Simulated Occupancy Sensor and set Matter Attribute
  OccupancySensor.setOccupancy(simulatedHWOccupancySensor());

  delay(50);
}

Occupancy Sensor with HoldTime

For an example that demonstrates HoldTime functionality, see:

// Copyright 2025 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
 * This example is an example code that will create a Matter Device which can be
 * commissioned and controlled from a Matter Environment APP.
 * Additionally the ESP32 will send debug messages indicating the Matter activity.
 * Turning DEBUG Level ON may be useful to following Matter Accessory and Controller messages.
 *
 * The example will create a Matter Occupancy Sensor Device.
 * Simulated detection every 2 minutes; occupied only until HoldTime expires
 * (default 30 seconds, configurable via the Matter controller).
 *
 * The HoldTime attribute allows you to adjust how long the active status is retained
 * after the person leaves. The HoldTime can be changed by a Matter Controller, and the
 * onHoldTimeChange() callback is used to update the simulated sensor functionality.
 *
 * The HoldTime value is persisted to Preferences (NVS) and restored on reboot, so the
 * last configured HoldTime value is maintained across device restarts.
 *
 * The onboard button can be kept pressed for 5 seconds to decommission the Matter Node.
 * The example will also show the manual commissioning code and QR code to be used in the Matter environment.
 *
 */

// Matter Manager
#include <Arduino.h>
#include <Matter.h>
// Wi-Fi credentials for this sketch. Fill these in when the board cannot
// commission over BLE (Arduino prebuild on ESP32 / ESP32-S2): the sketch
// joins the AP itself. When Matter commissions over BLE (CHIPoBLE), the
// hub sends SSID and password — leave the placeholders; they are unused.
#define WIFI_SSID     "your-ssid"
#define WIFI_PASSWORD "your-password"

#include <Preferences.h>

// HoldTime configuration constants
const uint16_t HOLD_TIME_MIN = 0;       // Minimum HoldTime in seconds
const uint16_t HOLD_TIME_MAX = 3600;    // Maximum HoldTime in seconds (1 hour)
const uint16_t HOLD_TIME_DEFAULT = 30;  // Default HoldTime in seconds

// List of Matter Endpoints for this Node
// Matter Occupancy Sensor Endpoint
MatterOccupancySensor OccupancySensor;

// Preferences to store HoldTime value across reboots
Preferences matterPref;
const char *holdTimePrefKey = "HoldTime";

// set your board USER BUTTON pin here - decommissioning only
const uint8_t buttonPin = BOOT_PIN;  // Set your pin here. Using BOOT Button.
MatterButton button;

// Simulated Occupancy Sensor with HoldTime support
// When occupancy is detected, it holds the "occupied" state for HoldTime seconds
// After HoldTime expires, it automatically switches to "unoccupied"
//
// Behavior for different HoldTime vs detectionInterval relationships:
// - holdTime_ms < detectionInterval: State switches to unoccupied after HoldTime, then waits for next detection
// - holdTime_ms == detectionInterval: If detections keep coming, timer resets (continuous occupancy)
// - holdTime_ms > detectionInterval: If detections keep coming, timer resets (continuous occupancy)
//   If detections stop, HoldTime expires after the last detection
bool simulatedHWOccupancySensor() {
  static bool occupancyState = false;
  static uint32_t lastDetectionTime = 0;
  static uint32_t lastDetectionEvent = millis();
  const uint32_t detectionInterval = 120000;  // Simulate detection every 2 minutes

  // Get current HoldTime from the sensor (can be changed by Matter Controller)
  uint32_t holdTime_ms = OccupancySensor.getHoldTime() * 1000;  // Convert seconds to milliseconds

  // Check HoldTime expiration FIRST (before processing new detections)
  // This ensures HoldTime can expire even if a detection occurs in the same iteration
  if (occupancyState && (millis() - lastDetectionTime > holdTime_ms)) {
    occupancyState = false;
    // Reset detection interval counter so next detection can happen immediately
    // This makes the simulation more responsive after the room becomes unoccupied
    lastDetectionEvent = millis();
    Serial.println("HoldTime expired. Switching to unoccupied state.");
  }

  // Simulate periodic occupancy detection (e.g., motion detected)
  // Check this AFTER HoldTime expiration so new detections can immediately re-trigger occupancy
  if (millis() - lastDetectionEvent > detectionInterval) {
    // New detection event occurred
    lastDetectionEvent = millis();

    if (!occupancyState) {
      // Transition from unoccupied to occupied - start hold timer
      occupancyState = true;
      lastDetectionTime = millis();
      Serial.printf("Occupancy detected! Holding state for %u seconds (HoldTime)\n", OccupancySensor.getHoldTime());
    } else {
      // Already occupied - new detection extends the hold period by resetting the timer
      // This simulates continuous occupancy (person still present)
      lastDetectionTime = millis();
      Serial.printf("Occupancy still detected. Resetting hold timer to %u seconds (HoldTime)\n", OccupancySensor.getHoldTime());
    }
  }

  return occupancyState;
}

void setup() {
  // Initialize the USER BUTTON (Boot button) that will be used to decommission the Matter Node
  button.begin(buttonPin);

  Serial.begin(115200);

// CONFIG_ENABLE_CHIPOBLE=n: sketch starts Wi-Fi here; with CHIPoBLE the hub delivers credentials.
#if !CONFIG_ENABLE_CHIPOBLE
  matterConnectWiFi(WIFI_SSID, WIFI_PASSWORD);
#endif

  // Initialize Preferences and read stored HoldTime value
  matterPref.begin("MatterPrefs", false);
  uint16_t storedHoldTime = matterPref.getUShort(holdTimePrefKey, HOLD_TIME_DEFAULT);

  // Validate stored value is within limits
  if (storedHoldTime < HOLD_TIME_MIN || storedHoldTime > HOLD_TIME_MAX) {
    uint16_t invalidValue = storedHoldTime;
    storedHoldTime = HOLD_TIME_DEFAULT;
    Serial.printf("Invalid stored HoldTime (%u), using default: %u seconds\n", invalidValue, HOLD_TIME_DEFAULT);
  } else if (storedHoldTime != HOLD_TIME_DEFAULT) {
    Serial.printf("Restored HoldTime from Preferences: %u seconds\n", storedHoldTime);
  }

  // Register callback for HoldTime changes from Matter Controller
  OccupancySensor.onHoldTimeChange([](uint16_t holdTime_seconds) -> bool {
    Serial.printf("HoldTime changed to %u seconds by Matter Controller\n", holdTime_seconds);
    // Store the new HoldTime value to Preferences for persistence across reboots
    matterPref.putUShort(holdTimePrefKey, holdTime_seconds);
    // The callback can return false to reject the change, or true to accept it
    // In this case, we always accept the change and update the simulator
    return true;
  });

  // set initial occupancy sensor state as false and connected to a PIR sensor type (default)
  OccupancySensor.begin();

  // Matter beginning - Last step, after all EndPoints are initialized
  Matter.begin();

  // Set HoldTimeLimits after Matter.begin() (optional, but recommended for validation)
  if (!OccupancySensor.setHoldTimeLimits(HOLD_TIME_MIN, HOLD_TIME_MAX, HOLD_TIME_DEFAULT)) {
    Serial.println("Warning: Failed to set HoldTimeLimits");
  } else {
    Serial.printf("HoldTimeLimits set: Min=%u, Max=%u, Default=%u seconds\n", HOLD_TIME_MIN, HOLD_TIME_MAX, HOLD_TIME_DEFAULT);
  }

  // Set initial HoldTime (use stored value if valid, otherwise use default)
  // This must be done after Matter.begin() because setHoldTime() requires the Matter event loop
  if (!OccupancySensor.setHoldTime(storedHoldTime)) {
    Serial.printf("Warning: Failed to set HoldTime to %u seconds\n", storedHoldTime);
  } else {
    Serial.printf("HoldTime set to: %u seconds\n", storedHoldTime);
  }

  Serial.printf("Initial HoldTime: %u seconds\n", OccupancySensor.getHoldTime());
  matterWaitUntilReady();
}

void loop() {
  matterRestartIfNoFabric();

  matterButtonEvent_t ev;
  while ((ev = button.poll()) != MATTER_BUTTON_NONE) {
    if (ev == MATTER_BUTTON_LONG_HOLD) {
      Serial.println("Decommissioning Occupancy Sensor Matter Accessory. It shall be commissioned again.");
      Matter.decommission();
    }
  }

  // Check Simulated Occupancy Sensor and set Matter Attribute
  OccupancySensor.setOccupancy(simulatedHWOccupancySensor());

  delay(50);
}

This example shows: * How to configure HoldTimeLimits after Matter.begin() * How to set and persist HoldTime values * How to use the onHoldTimeChange() callback * How to implement HoldTime expiration logic in sensor simulation