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%) * Tilt position and percentage control (0-100%) * 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 and covering type.

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

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

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

This function will return true if successful, false otherwise.

Note: Lift percentage 0 means fully closed, 100 means fully open. Tilt percentage 0 means fully closed, 100 means fully open. The covering type must be specified during initialization to ensure the correct features (lift and/or tilt) are enabled.

end

Stops processing Matter window covering events.

void end();

Lift Position Control

setLiftPosition

Sets the window covering lift position.

bool setLiftPosition(uint16_t liftPosition);
  • liftPosition - Lift position value

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 closed, 100 is fully open)

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.

getLiftPercentage

Gets the current lift percentage.

uint8_t getLiftPercentage();

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

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 closed, 100 is fully open)

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.

getTiltPercentage

Gets the current tilt percentage.

uint8_t getTiltPercentage();

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

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

setInstalledOpenLimitLift

Sets the installed open limit for lift (centimeters). This defines the physical position when the window covering is fully open.

bool setInstalledOpenLimitLift(uint16_t openLimit);
  • openLimit - Open limit position (centimeters)

This function will return true if successful, false otherwise.

getInstalledOpenLimitLift

Gets the installed open limit for lift.

uint16_t getInstalledOpenLimitLift();

This function will return the installed open limit for lift (centimeters).

setInstalledClosedLimitLift

Sets the installed closed limit for lift (centimeters). This defines the physical position when the window covering is fully closed.

bool setInstalledClosedLimitLift(uint16_t closedLimit);
  • closedLimit - Closed limit position (centimeters)

This function will return true if successful, false otherwise.

getInstalledClosedLimitLift

Gets the installed closed limit for lift.

uint16_t getInstalledClosedLimitLift();

This function will return the installed closed limit for lift (centimeters).

setInstalledOpenLimitTilt

Sets the installed open limit for tilt (absolute value for conversion, not a physical unit). This is used for converting between absolute position and percentage.

bool setInstalledOpenLimitTilt(uint16_t openLimit);
  • openLimit - Open limit absolute value

This function will return true if successful, false otherwise.

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

getInstalledOpenLimitTilt

Gets the installed open limit for tilt.

uint16_t getInstalledOpenLimitTilt();

This function will return the installed open limit for tilt (absolute value).

setInstalledClosedLimitTilt

Sets the installed closed limit for tilt (absolute value for conversion, not a physical unit). This is used for converting between absolute position and percentage.

bool setInstalledClosedLimitTilt(uint16_t closedLimit);
  • closedLimit - Closed limit absolute value

This function will return true if successful, false otherwise.

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

getInstalledClosedLimitTilt

Gets the installed closed limit for tilt.

uint16_t getInstalledClosedLimitTilt();

This function will return the installed closed limit for tilt (absolute value).

Target Position Control

setTargetLiftPercent100ths

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

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 closed, 10000 is fully open).

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 closed, 100 is fully open)

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 closed, 100 is fully open)

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

// Window covering limits
// Lift limits in centimeters (physical position)
const uint16_t MAX_LIFT = 200;  // Maximum lift position (fully open)
const uint16_t MIN_LIFT = 0;    // Minimum lift position (fully closed)

// Tilt limits (absolute values for conversion, not physical units)
// Tilt is a rotation, not a linear measurement
const uint16_t MAX_TILT = 90;  // Maximum tilt absolute value
const uint16_t MIN_TILT = 0;   // Minimum tilt absolute value

// Current window covering state
// These will be initialized in setup() based on installed limits and saved percentages
uint16_t currentLift = 0;  // Lift position in cm
uint8_t currentLiftPercent = 100;
uint8_t currentTiltPercent = 0;  // Tilt rotation percentage (0-100%)

// Visualize window covering position using RGB LED
// Lift percentage controls brightness (0% = off, 100% = full brightness)
#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)
  float brightness = (float)liftPercent / 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 brightnessValue = map(liftPercent, 0, 100, 0, 255);
  analogWrite(ledPin, brightnessValue);
#endif
}

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

  // Update CurrentPosition to reflect actual position (setLiftPercentage now only updates CurrentPosition)
  WindowBlinds.setLiftPercentage(currentLiftPercent);

  // 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
  uint16_t closedLimit = WindowBlinds.getInstalledClosedLimitLift();
  currentLift = closedLimit;
  currentLiftPercent = 0;
  Serial.printf("Closing window covering to full close (position: %d cm)\r\n", currentLift);

  // Update CurrentPosition to reflect actual position (setLiftPercentage now only updates CurrentPosition)
  WindowBlinds.setLiftPercentage(currentLiftPercent);

  // 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 OPEN
    WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::MOVING_UP_OR_OPEN);
  }
  if (liftPercent < currentLiftPercent) {
    // Set operational status to CLOSE
    WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::MOVING_DOWN_OR_CLOSE);
  }

  // This is where you would trigger your motor to go towards liftPercent
  // For simulation, we update instantly
  // Calculate absolute position based on installed limits
  uint16_t openLimit = WindowBlinds.getInstalledOpenLimitLift();
  uint16_t closedLimit = WindowBlinds.getInstalledClosedLimitLift();

  // Linear interpolation: 0% = openLimit, 100% = closedLimit
  if (openLimit < closedLimit) {
    currentLift = openLimit + ((closedLimit - openLimit) * liftPercent) / 100;
  } else {
    currentLift = openLimit - ((openLimit - closedLimit) * liftPercent) / 100;
  }
  currentLiftPercent = liftPercent;
  Serial.printf("Moving lift to %d%% (position: %d cm)\r\n", currentLiftPercent, currentLift);

  // Update CurrentPosition to reflect actual position (setLiftPercentage now only updates CurrentPosition)
  WindowBlinds.setLiftPercentage(currentLiftPercent);

  // 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 OPEN
    WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::MOVING_UP_OR_OPEN);
  }
  if (tiltPercent > currentTiltPercent) {
    // Set operational status to CLOSE
    WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::MOVING_DOWN_OR_CLOSE);
  }

  // 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 %d%%\r\n", currentTiltPercent);

  // Update CurrentPosition to reflect actual position
  WindowBlinds.setTiltPercentage(currentTiltPercent);

  // 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
  // (setLiftPercentage and setTiltPercentage now only update CurrentPosition)
  WindowBlinds.setLiftPercentage(currentLiftPercent);
  WindowBlinds.setTiltPercentage(currentTiltPercent);

  // 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 100% (fully open) if not stored before
  uint8_t lastLiftPercent = matterPref.getUChar(liftPercentPrefKey, 100);
  // 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
  WindowBlinds.begin(lastLiftPercent, lastTiltPercent, MatterWindowCovering::BLIND_LIFT_AND_TILT);

  // Configure installed limits for lift and tilt
  WindowBlinds.setInstalledOpenLimitLift(MIN_LIFT);
  WindowBlinds.setInstalledClosedLimitLift(MAX_LIFT);
  WindowBlinds.setInstalledOpenLimitTilt(MIN_TILT);
  WindowBlinds.setInstalledClosedLimitTilt(MAX_TILT);

  // Initialize current positions based on percentages and installed limits
  uint16_t openLimitLift = WindowBlinds.getInstalledOpenLimitLift();
  uint16_t closedLimitLift = WindowBlinds.getInstalledClosedLimitLift();
  currentLiftPercent = lastLiftPercent;
  if (openLimitLift < closedLimitLift) {
    currentLift = openLimitLift + ((closedLimitLift - openLimitLift) * lastLiftPercent) / 100;
  } else {
    currentLift = openLimitLift - ((openLimitLift - closedLimitLift) * lastLiftPercent) / 100;
  }

  currentTiltPercent = lastTiltPercent;

  Serial.printf(
    "Window Covering limits configured: Lift [%d-%d cm], Tilt [%d-%d]\r\n", WindowBlinds.getInstalledOpenLimitLift(),
    WindowBlinds.getInstalledClosedLimitLift(), WindowBlinds.getInstalledOpenLimitTilt(), WindowBlinds.getInstalledClosedLimitTilt()
  );
  Serial.printf("Initial positions: Lift=%d cm (%d%%), Tilt=%d%%\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=%d%%, Tilt=%d%%\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=%d%%, Tilt=%d%%\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=%d%%, Tilt=%d%%\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 %d%%\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.setLiftPercentage(0);  // close the covering
    Matter.decommission();
    button_time_stamp = millis();  // avoid running decommissioning again, reboot takes a second or so
  }
}