MatterWindowCovering

About

The MatterWindowCovering class provides a window covering endpoint for Matter networks. This endpoint implements the Matter window covering standard for motorized blinds, shades, and other window coverings with lift and tilt control.

Features: * Lift position and percentage control (0-100%, Matter: 0 = open, 100 = closed) * Lift and tilt percent100ths control (0-10000, direct Matter attribute mapping) * Local motor calibration for physical-unit to percentage conversion * Multiple window covering types support * Callback support for open, close, lift, tilt, and stop commands * Integration with Apple HomeKit, Amazon Alexa, and Google Home * Matter standard compliance

Supported Window Covering Types: * ROLLERSHADE - Lift support * ROLLERSHADE_2_MOTOR - Lift support * ROLLERSHADE_EXTERIOR - Lift support * ROLLERSHADE_EXTERIOR_2_MOTOR - Lift support * DRAPERY - Lift support * AWNING - Lift support * SHUTTER - Tilt support * BLIND_TILT_ONLY - Tilt support * BLIND_LIFT_AND_TILT - Lift and Tilt support * PROJECTOR_SCREEN - Lift support

Use Cases: * Motorized blinds * Automated shades * Smart window coverings * Projector screens * Awnings and drapes

API Reference

Constructor

MatterWindowCovering

Creates a new Matter window covering endpoint.

MatterWindowCovering();

Initialization

begin

Initializes the Matter window covering endpoint with optional initial positions, covering type, and local motor calibration.

bool begin(
  uint8_t liftPercent = 0,
  uint8_t tiltPercent = 0,
  WindowCoveringType_t coveringType = ROLLERSHADE,
  const PositionCalibration *liftCalibration = nullptr,
  const PositionCalibration *tiltCalibration = nullptr
);
  • liftPercent - Initial lift percentage (0-100, default: 0 = fully open)

  • tiltPercent - Initial tilt percentage (0-100, default: 0 = fully open)

  • coveringType - Window covering type (default: ROLLERSHADE). This determines which features (lift, tilt, or both) are enabled.

  • liftCalibration - Optional local motor range for lift (open/closed physical units). Not exposed as a Matter attribute in ESP-Matter 1.5.

  • tiltCalibration - Optional local motor range for tilt. Not exposed as a Matter attribute in ESP-Matter 1.5.

This function will return true if successful, false otherwise.

Note: Matter percentage semantics apply throughout this API: 0 = fully open, 100 = fully closed. The covering type must be specified during initialization to ensure the correct features (lift and/or tilt) are enabled.

PositionCalibration

Local motor range used to convert between physical motor units and Matter percentages. This is stored in firmware only and is not published as a Matter cluster attribute in ESP-Matter 1.5.

struct PositionCalibration {
  uint16_t open = 0;
  uint16_t closed = 65534;
};

end

Stops processing Matter window covering events.

void end();

Lift Position Control

setLiftPosition

Sets the window covering lift position in local motor units. Converts to Matter percent100ths using the local motor calibration range.

bool setLiftPosition(uint16_t liftPosition);
  • liftPosition - Lift position in local motor units (e.g. centimeters)

This function will return true if successful, false otherwise.

getLiftPosition

Gets the current lift position.

uint16_t getLiftPosition();

This function will return the current lift position.

setLiftPercentage

Sets the window covering lift position as a percentage. This method updates the CurrentPositionLiftPercent100ths attribute, which reflects the device’s actual position. The TargetPositionLiftPercent100ths attribute is set by Matter commands/apps when a new target is requested.

bool setLiftPercentage(uint8_t liftPercent);
  • liftPercent - Lift percentage (0-100, where 0 is fully open, 100 is fully closed)

This function will return true if successful, false otherwise.

Note: When the device reaches the target position, call setOperationalState(LIFT, STALL) to indicate that movement is complete. Prefer setCurrentLiftPercent100ths() when sub-percent precision is needed.

getLiftPercentage

Gets the current lift percentage.

uint8_t getLiftPercentage();

This function will return the current lift percentage (0-100, Matter semantics).

setCurrentLiftPercent100ths

Sets the current lift position using Matter percent100ths (0-10000). This is the primary API for reporting actual position to commissioners.

bool setCurrentLiftPercent100ths(uint16_t liftPercent100ths);
  • liftPercent100ths - Current lift position (0 = open, 10000 = closed)

getCurrentLiftPercent100ths

Gets the current lift position in percent100ths.

uint16_t getCurrentLiftPercent100ths();

Tilt Position Control

setTiltPosition

Sets the window covering tilt position. Note that tilt is a rotation, not a linear measurement. This method converts the absolute position to percentage using the installed limits.

bool setTiltPosition(uint16_t tiltPosition);
  • tiltPosition - Tilt position value (absolute value for conversion, not a physical unit)

This function will return true if successful, false otherwise.

getTiltPosition

Gets the current tilt position. Note that tilt is a rotation, not a linear measurement.

uint16_t getTiltPosition();

This function will return the current tilt position (absolute value for conversion, not a physical unit).

setTiltPercentage

Sets the window covering tilt position as a percentage. This method updates the CurrentPositionTiltPercent100ths attribute, which reflects the device’s actual position. The TargetPositionTiltPercent100ths attribute is set by Matter commands/apps when a new target is requested.

bool setTiltPercentage(uint8_t tiltPercent);
  • tiltPercent - Tilt percentage (0-100, where 0 is fully open, 100 is fully closed)

This function will return true if successful, false otherwise.

Note: When the device reaches the target position, call setOperationalState(TILT, STALL) to indicate that movement is complete. Prefer setCurrentTiltPercent100ths() when sub-percent precision is needed.

getTiltPercentage

Gets the current tilt percentage.

uint8_t getTiltPercentage();

This function will return the current tilt percentage (0-100, Matter semantics).

setCurrentTiltPercent100ths

Sets the current tilt position using Matter percent100ths (0-10000).

bool setCurrentTiltPercent100ths(uint16_t tiltPercent100ths);

getCurrentTiltPercent100ths

Gets the current tilt position in percent100ths.

uint16_t getCurrentTiltPercent100ths();

Window Covering Type

setCoveringType

Sets the window covering type.

bool setCoveringType(WindowCoveringType_t coveringType);
  • coveringType - Window covering type (see Window Covering Types enum)

This function will return true if successful, false otherwise.

getCoveringType

Gets the current window covering type.

WindowCoveringType_t getCoveringType();

This function will return the current window covering type.

Installed Limit Control

These methods configure local motor calibration for converting between physical motor units and Matter percentages. They are not exposed as Matter cluster attributes in ESP-Matter 1.5. Prefer setLiftCalibration() / setTiltCalibration() or pass PositionCalibration to begin().

setLiftCalibration

Sets the local lift motor calibration range.

bool setLiftCalibration(const PositionCalibration &calibration);

getLiftCalibration

Gets the local lift motor calibration range.

PositionCalibration getLiftCalibration();

setTiltCalibration

Sets the local tilt motor calibration range.

bool setTiltCalibration(const PositionCalibration &calibration);

getTiltCalibration

Gets the local tilt motor calibration range.

PositionCalibration getTiltCalibration();

setInstalledOpenLimitLift

Sets the local open limit for lift motor calibration (physical units when fully open).

bool setInstalledOpenLimitLift(uint16_t openLimit);
  • openLimit - Open limit position in your motor units (e.g. centimeters)

This function will return true if successful, false otherwise.

getInstalledOpenLimitLift

Gets the local open limit for lift motor calibration.

uint16_t getInstalledOpenLimitLift();

This function will return the open limit for lift motor calibration.

setInstalledClosedLimitLift

Sets the local closed limit for lift motor calibration (physical units when fully closed).

bool setInstalledClosedLimitLift(uint16_t closedLimit);
  • closedLimit - Closed limit position in your motor units (e.g. centimeters)

This function will return true if successful, false otherwise.

getInstalledClosedLimitLift

Gets the local closed limit for lift motor calibration.

uint16_t getInstalledClosedLimitLift();

This function will return the closed limit for lift motor calibration.

setInstalledOpenLimitTilt

Sets the local open limit for tilt motor calibration.

bool setInstalledOpenLimitTilt(uint16_t openLimit);
  • openLimit - Open limit value for tilt conversion

This function will return true if successful, false otherwise.

Note: Tilt is a rotation, not a linear measurement. These limits are used for local position conversion only.

getInstalledOpenLimitTilt

Gets the local open limit for tilt motor calibration.

uint16_t getInstalledOpenLimitTilt();

This function will return the open limit for tilt motor calibration.

setInstalledClosedLimitTilt

Sets the local closed limit for tilt motor calibration.

bool setInstalledClosedLimitTilt(uint16_t closedLimit);
  • closedLimit - Closed limit value for tilt conversion

This function will return true if successful, false otherwise.

Note: Tilt is a rotation, not a linear measurement. These limits are used for local position conversion only.

getInstalledClosedLimitTilt

Gets the local closed limit for tilt motor calibration.

uint16_t getInstalledClosedLimitTilt();

This function will return the closed limit for tilt motor calibration.

Target Position Control

setTargetLiftPercent100ths

Sets the target lift position in percent100ths (0-10000, where 0 is fully open, 10000 is fully closed).

bool setTargetLiftPercent100ths(uint16_t liftPercent100ths);
  • liftPercent100ths - Target lift position in percent100ths (0-10000)

This function will return true if successful, false otherwise.

Note: This sets the target position that the device should move towards. The actual position should be updated using setLiftPercentage().

getTargetLiftPercent100ths

Gets the current target lift position in percent100ths.

uint16_t getTargetLiftPercent100ths();

This function will return the current target lift position in percent100ths (0-10000).

setTargetTiltPercent100ths

Sets the target tilt position in percent100ths (0-10000, where 0 is fully open, 10000 is fully closed).

bool setTargetTiltPercent100ths(uint16_t tiltPercent100ths);
  • tiltPercent100ths - Target tilt position in percent100ths (0-10000)

This function will return true if successful, false otherwise.

Note: This sets the target position that the device should move towards. The actual position should be updated using setTiltPercentage().

getTargetTiltPercent100ths

Gets the current target tilt position in percent100ths.

uint16_t getTargetTiltPercent100ths();

This function will return the current target tilt position in percent100ths (0-10000).

Operational Status Control

setOperationalStatus

Sets the full operational status bitmap.

bool setOperationalStatus(uint8_t operationalStatus);
  • operationalStatus - Full operational status bitmap value

This function will return true if successful, false otherwise.

Note: It is recommended to use setOperationalState() to set individual field states instead of setting the full bitmap directly.

getOperationalStatus

Gets the full operational status bitmap.

uint8_t getOperationalStatus();

This function will return the current operational status bitmap value.

setOperationalState

Sets the operational state for a specific field (LIFT or TILT). The GLOBAL field is automatically updated based on priority (LIFT > TILT).

bool setOperationalState(OperationalStatusField_t field, OperationalState_t state);
  • field - Field to set (LIFT or TILT). GLOBAL cannot be set directly.

  • state - Operational state (STALL, MOVING_UP_OR_OPEN, or MOVING_DOWN_OR_CLOSE)

This function will return true if successful, false otherwise.

Note: Only LIFT and TILT fields can be set directly. The GLOBAL field is automatically updated based on the active field (LIFT has priority over TILT).

getOperationalState

Gets the operational state for a specific field.

OperationalState_t getOperationalState(OperationalStatusField_t field);
  • field - Field to get (GLOBAL, LIFT, or TILT)

This function will return the operational state for the specified field (STALL, MOVING_UP_OR_OPEN, or MOVING_DOWN_OR_CLOSE).

Event Handling

The MatterWindowCovering class automatically detects Matter commands and calls the appropriate callbacks when registered. There are two types of callbacks:

Target Position Callbacks (triggered when TargetPosition attributes change): * onOpen() - called when UpOrOpen command is received (sets target to 0% = fully open) * onClose() - called when DownOrClose command is received (sets target to 100% = fully closed) * onStop() - called when StopMotion command is received (sets target to current position) * onGoToLiftPercentage() - called when TargetPositionLiftPercent100ths changes (from any command, setTargetLiftPercent100ths(), or direct attribute write) * onGoToTiltPercentage() - called when TargetPositionTiltPercent100ths changes (from any command, setTargetTiltPercent100ths(), or direct attribute write)

Current Position Callback (triggered when CurrentPosition attributes change): * onChange() - called when CurrentPositionLiftPercent100ths or CurrentPositionTiltPercent100ths change (after setLiftPercentage()/setTiltPercentage() are called or when a Matter controller updates these attributes directly)

Important: onChange() is not automatically called when Matter commands are executed. Commands modify TargetPosition, not CurrentPosition. To trigger onChange(), your onGoToLiftPercentage() or onGoToTiltPercentage() callback must call setLiftPercentage() or setTiltPercentage() when the physical device actually moves.

Note: All callbacks are optional. If a specific callback is not registered, only the generic onGoToLiftPercentage() or onGoToTiltPercentage() callbacks will be called (if registered).

onOpen

Sets a callback function to be called when the UpOrOpen command is received from a Matter controller. This command sets the target position to 0% (fully open).

void onOpen(EndPointOpenCB onChangeCB);
  • onChangeCB - Function to call when UpOrOpen command is received

The callback signature is:

bool onChangeCallback();

onClose

Sets a callback function to be called when the DownOrClose command is received from a Matter controller. This command sets the target position to 100% (fully closed).

void onClose(EndPointCloseCB onChangeCB);
  • onChangeCB - Function to call when DownOrClose command is received

The callback signature is:

bool onChangeCallback();

onGoToLiftPercentage

Sets a callback function to be called when TargetPositionLiftPercent100ths changes. This is triggered by: * Matter commands: UpOrOpen, DownOrClose, StopMotion, GoToLiftPercentage * Calling setTargetLiftPercent100ths() * Direct attribute writes to TargetPositionLiftPercent100ths

This callback is always called when the target lift position changes, regardless of which command or method was used to change it.

Note: This callback receives the target position. To update the current position (which triggers onChange()), call setLiftPercentage() when the physical device actually moves.

void onGoToLiftPercentage(EndPointLiftCB onChangeCB);
  • onChangeCB - Function to call when target lift percentage changes

The callback signature is:

bool onChangeCallback(uint8_t liftPercent);
  • liftPercent - Target lift percentage (0-100, where 0 is fully open, 100 is fully closed)

onGoToTiltPercentage

Sets a callback function to be called when TargetPositionTiltPercent100ths changes. This is triggered by: * Matter commands: UpOrOpen, DownOrClose, StopMotion, GoToTiltPercentage * Calling setTargetTiltPercent100ths() * Direct attribute writes to TargetPositionTiltPercent100ths

This callback is always called when the target tilt position changes, regardless of which command or method was used to change it.

Note: This callback receives the target position. To update the current position (which triggers onChange()), call setTiltPercentage() when the physical device actually moves.

void onGoToTiltPercentage(EndPointTiltCB onChangeCB);
  • onChangeCB - Function to call when target tilt percentage changes

The callback signature is:

bool onChangeCallback(uint8_t tiltPercent);
  • tiltPercent - Target tilt percentage (0-100, where 0 is fully open, 100 is fully closed)

onStop

Sets a callback function to be called when the StopMotion command is received from a Matter controller. This command sets the target position to the current position, effectively stopping any movement.

void onStop(EndPointStopCB onChangeCB);
  • onChangeCB - Function to call when StopMotion command is received

The callback signature is:

bool onChangeCallback();

onChange

Sets a callback function to be called when CurrentPositionLiftPercent100ths or CurrentPositionTiltPercent100ths attributes change. This is different from onGoToLiftPercentage() and onGoToTiltPercentage(), which are called when TargetPosition attributes change.

When ``onChange()`` is called: * When CurrentPositionLiftPercent100ths changes (after setLiftPercentage() is called or when a Matter controller updates this attribute directly) * When CurrentPositionTiltPercent100ths changes (after setTiltPercentage() is called or when a Matter controller updates this attribute directly)

Important: onChange() is not automatically called when Matter commands are executed. Commands modify TargetPosition attributes, which trigger onGoToLiftPercentage() or onGoToTiltPercentage() callbacks instead. To trigger onChange() after a command, your onGoToLiftPercentage() or onGoToTiltPercentage() callback must call setLiftPercentage() or setTiltPercentage() to update the CurrentPosition attributes when the physical device actually moves.

void onChange(EndPointCB onChangeCB);
  • onChangeCB - Function to call when current position attributes change

The callback signature is:

bool onChangeCallback(uint8_t liftPercent, uint8_t tiltPercent);
  • liftPercent - Current lift percentage (0-100)

  • tiltPercent - Current tilt percentage (0-100)

updateAccessory

Updates the state of the window covering using the current Matter internal state.

void updateAccessory();

This function will call the registered callback with the current state.

Example

Window Covering

// 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.

// Matter Manager
#include <Arduino.h>
#include <Matter.h>
#if !CONFIG_ENABLE_CHIPOBLE
// if the device can be commissioned using BLE, WiFi is not used - save flash space
#include <WiFi.h>
#endif
#include <Preferences.h>

// List of Matter Endpoints for this Node
// Window Covering Endpoint
MatterWindowCovering WindowBlinds;

// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network
#if !CONFIG_ENABLE_CHIPOBLE
// WiFi is manually set and started
const char *ssid = "your-ssid";          // Change this to your WiFi SSID
const char *password = "your-password";  // Change this to your WiFi password
#endif

// it will keep last Lift & Tilt state stored, using Preferences
Preferences matterPref;
const char *liftPercentPrefKey = "LiftPercent";
const char *tiltPercentPrefKey = "TiltPercent";

// set your board USER BUTTON pin here
const uint8_t buttonPin = BOOT_PIN;  // Set your pin here. Using BOOT Button.

// Button control
uint32_t button_time_stamp = 0;                // debouncing control
bool button_state = false;                     // false = released | true = pressed
const uint32_t debounceTime = 250;             // button debouncing time (ms)
const uint32_t decommissioningTimeout = 5000;  // keep the button pressed for 5s, or longer, to decommission

// Local motor calibration (not exposed as Matter attributes in ESP-Matter 1.5)
// Lift limits in centimeters (physical position at open/closed ends)
// Matter percent: 0 = open at open limit, 100 = closed at closed limit
const MatterWindowCovering::PositionCalibration LIFT_CALIBRATION = {.open = 0, .closed = 200};

// Tilt limits (absolute values for conversion, not physical units)
// Tilt is a rotation, not a linear measurement
const MatterWindowCovering::PositionCalibration TILT_CALIBRATION = {.open = 0, .closed = 90};

// Current window covering state
// These will be initialized in setup() based on motor calibration and saved percentages
// Matter percent: 0 = fully open, 100 = fully closed
uint16_t currentLift = LIFT_CALIBRATION.open;  // Lift position in cm
uint8_t currentLiftPercent = 0;
uint8_t currentTiltPercent = 0;  // Tilt rotation percentage (0-100%)

// Visualize window covering position using RGB LED
// Brightness follows openness (inverted Matter percent: more open = brighter)
#ifdef RGB_BUILTIN
const uint8_t ledPin = RGB_BUILTIN;
#else
const uint8_t ledPin = 2;  // Set your pin here if your board has not defined RGB_BUILTIN
#warning "Do not forget to set the RGB LED pin"
#endif

void visualizeWindowBlinds(uint8_t liftPercent, uint8_t tiltPercent) {
#ifdef RGB_BUILTIN
  // Use RGB LED to visualize lift position (brightness) and tilt (color shift)
  // Brighter when more open (lower Matter percent)
  uint8_t openness = 100 - liftPercent;
  float brightness = (float)openness / 100.0;  // 0.0 to 1.0
  // Tilt affects color: 0% = red, 100% = blue
  uint8_t red = (uint8_t)(map(tiltPercent, 0, 100, 255, 0) * brightness);
  uint8_t blue = (uint8_t)(map(tiltPercent, 0, 100, 0, 255) * brightness);
  uint8_t green = 0;
  rgbLedWrite(ledPin, red, green, blue);
#else
  // For non-RGB boards, just use brightness
  uint8_t openness = 100 - liftPercent;
  uint8_t brightnessValue = map(openness, 0, 100, 0, 255);
  analogWrite(ledPin, brightnessValue);
#endif
}

// Convert Matter lift percent (0 = open, 100 = closed) to local motor units (cm)
static uint16_t liftPercentToCm(uint8_t liftPercent) {
  const auto cal = WindowBlinds.getLiftCalibration();
  // Linear interpolation: 0% = open limit, 100% = closed limit
  if (cal.open < cal.closed) {
    return cal.open + ((cal.closed - cal.open) * liftPercent) / 100;
  }
  return cal.open - ((cal.open - cal.closed) * liftPercent) / 100;
}

// Window Covering Callbacks
bool fullOpen() {
  // This is where you would trigger your motor to go to full open state
  // For simulation, we update instantly
  currentLift = WindowBlinds.getLiftCalibration().open;
  currentLiftPercent = 0;
  Serial.printf("Opening window covering to full open (position: %u cm)\r\n", currentLift);

  // Update CurrentPosition to reflect actual position (Percent100ths on the Matter cluster)
  WindowBlinds.setCurrentLiftPercent100ths(0);

  // Set operational status to STALL when movement is complete
  WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL);

  // Store state
  matterPref.putUChar(liftPercentPrefKey, currentLiftPercent);

  return true;
}

bool fullClose() {
  // This is where you would trigger your motor to go to full close state
  // For simulation, we update instantly
  currentLift = WindowBlinds.getLiftCalibration().closed;
  currentLiftPercent = 100;
  Serial.printf("Closing window covering to full close (position: %u cm)\r\n", currentLift);

  // Update CurrentPosition to reflect actual position (Percent100ths on the Matter cluster)
  WindowBlinds.setCurrentLiftPercent100ths(10000);

  // Set operational status to STALL when movement is complete
  WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL);

  // Store state
  matterPref.putUChar(liftPercentPrefKey, currentLiftPercent);

  return true;
}

bool goToLiftPercentage(uint8_t liftPercent) {
  // update Lift operational state
  if (liftPercent > currentLiftPercent) {
    // Set operational status to CLOSE
    WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::MOVING_DOWN_OR_CLOSE);
  } else if (liftPercent < currentLiftPercent) {
    // Set operational status to OPEN
    WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::MOVING_UP_OR_OPEN);
  }

  // This is where you would trigger your motor to go towards liftPercent
  // For simulation, we update instantly
  currentLift = liftPercentToCm(liftPercent);
  currentLiftPercent = liftPercent;
  Serial.printf("Moving lift to %u%% (position: %u cm)\r\n", currentLiftPercent, currentLift);

  // Update CurrentPosition to reflect actual position (Percent100ths on the Matter cluster)
  WindowBlinds.setCurrentLiftPercent100ths(liftPercent * 100);

  // Set operational status to STALL when movement is complete
  WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL);

  // Store state
  matterPref.putUChar(liftPercentPrefKey, currentLiftPercent);

  return true;
}

bool goToTiltPercentage(uint8_t tiltPercent) {
  // update Tilt operational state
  if (tiltPercent > currentTiltPercent) {
    // Set operational status to CLOSE
    WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::MOVING_DOWN_OR_CLOSE);
  } else if (tiltPercent < currentTiltPercent) {
    // Set operational status to OPEN
    WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::MOVING_UP_OR_OPEN);
  }

  // This is where you would trigger your motor to rotate the shade to tiltPercent
  // For simulation, we update instantly
  currentTiltPercent = tiltPercent;
  Serial.printf("Rotating tilt to %u%%\r\n", currentTiltPercent);

  // Update CurrentPosition to reflect actual position (Percent100ths on the Matter cluster)
  WindowBlinds.setCurrentTiltPercent100ths(tiltPercent * 100);

  // Set operational status to STALL when movement is complete
  WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::STALL);

  // Store state
  matterPref.putUChar(tiltPercentPrefKey, currentTiltPercent);

  return true;
}

bool stopMotor() {
  // Motor can be stopped while moving cover toward current target
  Serial.println("Stopping window covering motor");

  // Update CurrentPosition to reflect actual position when stopped
  WindowBlinds.setCurrentLiftPercent100ths(currentLiftPercent * 100);
  WindowBlinds.setCurrentTiltPercent100ths(currentTiltPercent * 100);

  // Set operational status to STALL for both lift and tilt
  WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL);
  WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::STALL);

  return true;
}

void setup() {
  // Initialize the USER BUTTON (Boot button) GPIO
  pinMode(buttonPin, INPUT_PULLUP);
  // Initialize the RGB LED GPIO
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, LOW);

  Serial.begin(115200);

// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network
#if !CONFIG_ENABLE_CHIPOBLE
  // We start by connecting to a WiFi network
  Serial.print("Connecting to ");
  Serial.println(ssid);
  // Manually connect to WiFi
  WiFi.begin(ssid, password);
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\r\nWiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  delay(500);
#endif

  // Initialize Matter EndPoint
  matterPref.begin("MatterPrefs", false);
  // default lift percentage is 0% (fully open) if not stored before — Matter semantics
  uint8_t lastLiftPercent = matterPref.getUChar(liftPercentPrefKey, 0);
  // default tilt percentage is 0% if not stored before
  uint8_t lastTiltPercent = matterPref.getUChar(tiltPercentPrefKey, 0);

  // Initialize window covering with BLIND_LIFT_AND_TILT type and local motor calibration
  WindowBlinds.begin(lastLiftPercent, lastTiltPercent, MatterWindowCovering::BLIND_LIFT_AND_TILT, &LIFT_CALIBRATION, &TILT_CALIBRATION);

  // Initialize current positions based on percentages and motor calibration
  currentLiftPercent = lastLiftPercent;
  currentLift = liftPercentToCm(lastLiftPercent);
  currentTiltPercent = lastTiltPercent;

  Serial.printf(
    "Motor calibration: Lift [%u-%u cm], Tilt [%u-%u]\r\n", WindowBlinds.getLiftCalibration().open, WindowBlinds.getLiftCalibration().closed,
    WindowBlinds.getTiltCalibration().open, WindowBlinds.getTiltCalibration().closed
  );
  Serial.printf("Initial positions: Lift=%u cm (%u%%), Tilt=%u%%\r\n", currentLift, currentLiftPercent, currentTiltPercent);

  // Set callback functions
  WindowBlinds.onOpen(fullOpen);
  WindowBlinds.onClose(fullClose);
  WindowBlinds.onGoToLiftPercentage(goToLiftPercentage);
  WindowBlinds.onGoToTiltPercentage(goToTiltPercentage);
  WindowBlinds.onStop(stopMotor);

  // Generic callback for Lift or Tilt change
  WindowBlinds.onChange([](uint8_t liftPercent, uint8_t tiltPercent) {
    Serial.printf("Window Covering changed: Lift=%u%%, Tilt=%u%%\r\n", liftPercent, tiltPercent);
    visualizeWindowBlinds(liftPercent, tiltPercent);
    return true;
  });

  // Matter beginning - Last step, after all EndPoints are initialized
  Matter.begin();
  // This may be a restart of a already commissioned Matter accessory
  if (Matter.isDeviceCommissioned()) {
    Serial.println("Matter Node is commissioned and connected to the network. Ready for use.");
    Serial.printf("Initial state: Lift=%u%%, Tilt=%u%%\r\n", WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage());
    // Update visualization based on initial state
    visualizeWindowBlinds(WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage());
  }
}

void loop() {
  // Check Matter Window Covering Commissioning state, which may change during execution of loop()
  if (!Matter.isDeviceCommissioned()) {
    Serial.println("");
    Serial.println("Matter Node is not commissioned yet.");
    Serial.println("Initiate the device discovery in your Matter environment.");
    Serial.println("Commission it to your Matter hub with the manual pairing code or QR code");
    Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str());
    Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str());
    // waits for Matter Window Covering Commissioning.
    uint32_t timeCount = 0;
    while (!Matter.isDeviceCommissioned()) {
      delay(100);
      if ((timeCount++ % 50) == 0) {  // 50*100ms = 5 sec
        Serial.println("Matter Node not commissioned yet. Waiting for commissioning.");
      }
    }
    Serial.printf("Initial state: Lift=%u%%, Tilt=%u%%\r\n", WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage());
    // Update visualization based on initial state
    visualizeWindowBlinds(WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage());
    Serial.println("Matter Node is commissioned and connected to the network. Ready for use.");
  }

  // A button is also used to control the window covering
  // Check if the button has been pressed
  if (digitalRead(buttonPin) == LOW && !button_state) {
    // deals with button debouncing
    button_time_stamp = millis();  // record the time while the button is pressed.
    button_state = true;           // pressed.
  }

  // Onboard User Button is used to manually change lift percentage or to decommission
  uint32_t time_diff = millis() - button_time_stamp;
  if (digitalRead(buttonPin) == HIGH && button_state && time_diff > debounceTime) {
    // Button is released - cycle lift percentage by 20%
    button_state = false;  // released
    uint8_t targetLiftPercent = currentLiftPercent;
    // go to the closest next 20% or move 20% more
    if ((targetLiftPercent % 20) != 0) {
      targetLiftPercent = ((targetLiftPercent / 20) + 1) * 20;
    } else {
      targetLiftPercent += 20;
    }
    if (targetLiftPercent > 100) {
      targetLiftPercent = 0;
    }
    Serial.printf("User button released. Setting lift to %u%%\r\n", targetLiftPercent);
    WindowBlinds.setTargetLiftPercent100ths(targetLiftPercent * 100);
  }

  // Onboard User Button is kept pressed for longer than 5 seconds in order to decommission matter node
  if (button_state && time_diff > decommissioningTimeout) {
    Serial.println("Decommissioning the Window Covering Matter Accessory. It shall be commissioned again.");
    WindowBlinds.setCurrentLiftPercent100ths(10000);  // fully closed
    Matter.decommission();
    button_time_stamp = millis();  // avoid running decommissioning again, reboot takes a second or so
  }
}