From ac71355cfb1d465c6accb4b08b3df9649848e49e Mon Sep 17 00:00:00 2001 From: SuGlider Date: Sat, 6 Dec 2025 23:21:46 -0300 Subject: [PATCH 01/28] feat(matter): adds window covering matter endpoint --- CMakeLists.txt | 1 + docs/en/matter/ep_window_covering.rst | 561 ++++++++++++ docs/en/matter/matter.rst | 1 + .../MatterWindowCovering.ino | 356 ++++++++ .../examples/MatterWindowCovering/README.md | 228 +++++ .../examples/MatterWindowCovering/ci.yml | 4 + libraries/Matter/keywords.txt | 40 + libraries/Matter/src/Matter.h | 2 + .../MatterEndpoints/MatterWindowCovering.cpp | 857 ++++++++++++++++++ .../MatterEndpoints/MatterWindowCovering.h | 162 ++++ 10 files changed, 2212 insertions(+) create mode 100644 docs/en/matter/ep_window_covering.rst create mode 100644 libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino create mode 100644 libraries/Matter/examples/MatterWindowCovering/README.md create mode 100644 libraries/Matter/examples/MatterWindowCovering/ci.yml create mode 100644 libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp create mode 100644 libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 22f67c370e1..7528d687f2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,6 +199,7 @@ set(ARDUINO_LIBRARY_Matter_SRCS libraries/Matter/src/MatterEndpoints/MatterOccupancySensor.cpp libraries/Matter/src/MatterEndpoints/MatterOnOffPlugin.cpp libraries/Matter/src/MatterEndpoints/MatterThermostat.cpp + libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp libraries/Matter/src/Matter.cpp libraries/Matter/src/MatterEndPoint.cpp) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst new file mode 100644 index 00000000000..a7414286f90 --- /dev/null +++ b/docs/en/matter/ep_window_covering.rst @@ -0,0 +1,561 @@ +#################### +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. + +.. code-block:: arduino + + MatterWindowCovering(); + +Initialization +************** + +begin +^^^^^ + +Initializes the Matter window covering endpoint with optional initial positions and covering type. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + void end(); + +Lift Position Control +********************* + +setLiftPosition +^^^^^^^^^^^^^^^ + +Sets the window covering lift position. + +.. code-block:: arduino + + bool setLiftPosition(uint16_t liftPosition); + +* ``liftPosition`` - Lift position value + +This function will return ``true`` if successful, ``false`` otherwise. + +getLiftPosition +^^^^^^^^^^^^^^^ + +Gets the current lift position. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + uint8_t getTiltPercentage(); + +This function will return the current tilt percentage (0-100). + +Window Covering Type +******************** + +setCoveringType +^^^^^^^^^^^^^^^ + +Sets the window covering type. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + WindowCoveringType_t getCoveringType(); + +This function will return the current window covering type. + +Installed Limit Control +*********************** + +setInstalledOpenLimitLift +^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the installed open limit for lift (in centimeters). This defines the physical position when the window covering is fully open. + +.. code-block:: arduino + + bool setInstalledOpenLimitLift(uint16_t openLimit); + +* ``openLimit`` - Open limit position in centimeters + +This function will return ``true`` if successful, ``false`` otherwise. + +getInstalledOpenLimitLift +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Gets the installed open limit for lift. + +.. code-block:: arduino + + uint16_t getInstalledOpenLimitLift(); + +This function will return the installed open limit for lift in centimeters. + +setInstalledClosedLimitLift +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the installed closed limit for lift (in centimeters). This defines the physical position when the window covering is fully closed. + +.. code-block:: arduino + + bool setInstalledClosedLimitLift(uint16_t closedLimit); + +* ``closedLimit`` - Closed limit position in centimeters + +This function will return ``true`` if successful, ``false`` otherwise. + +getInstalledClosedLimitLift +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Gets the installed closed limit for lift. + +.. code-block:: arduino + + uint16_t getInstalledClosedLimitLift(); + +This function will return the installed closed limit for lift in 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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). + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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). + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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). + +.. code-block:: arduino + + 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. + +.. code-block:: arduino + + 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 +************** + +onOpen +^^^^^^ + +Sets a callback function to be called when the window covering is opened. + +.. code-block:: arduino + + void onOpen(EndPointOpenCB onChangeCB); + +* ``onChangeCB`` - Function to call when window covering is opened + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(); + +onClose +^^^^^^^ + +Sets a callback function to be called when the window covering is closed. + +.. code-block:: arduino + + void onClose(EndPointCloseCB onChangeCB); + +* ``onChangeCB`` - Function to call when window covering is closed + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(); + +onGoToLiftPercentage +^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when the lift percentage changes. + +.. code-block:: arduino + + void onGoToLiftPercentage(EndPointLiftCB onChangeCB); + +* ``onChangeCB`` - Function to call when lift percentage changes + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(uint8_t liftPercent); + +* ``liftPercent`` - New lift percentage (0-100) + +onGoToTiltPercentage +^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when the tilt percentage changes. + +.. code-block:: arduino + + void onGoToTiltPercentage(EndPointTiltCB onChangeCB); + +* ``onChangeCB`` - Function to call when tilt percentage changes + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(uint8_t tiltPercent); + +* ``tiltPercent`` - New tilt percentage (0-100) + +onStop +^^^^^^ + +Sets a callback function to be called when the window covering movement is stopped. + +.. code-block:: arduino + + void onStop(EndPointStopCB onChangeCB); + +* ``onChangeCB`` - Function to call when window covering is stopped + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(); + +onChange +^^^^^^^^ + +Sets a callback function to be called when any parameter changes. + +.. code-block:: arduino + + void onChange(EndPointCB onChangeCB); + +* ``onChangeCB`` - Function to call when state changes + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(uint8_t liftPercent, uint8_t tiltPercent); + +* ``liftPercent`` - New lift percentage (0-100) +* ``tiltPercent`` - New tilt percentage (0-100) + +updateAccessory +^^^^^^^^^^^^^^^ + +Updates the state of the window covering using the current Matter internal state. + +.. code-block:: arduino + + void updateAccessory(); + +This function will call the registered callback with the current state. + +Example +------- + +Window Covering +*************** + +.. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino + :language: arduino + diff --git a/docs/en/matter/matter.rst b/docs/en/matter/matter.rst index 278606d4e81..9520eb606b2 100644 --- a/docs/en/matter/matter.rst +++ b/docs/en/matter/matter.rst @@ -136,6 +136,7 @@ The library provides specialized endpoint classes for different device types. Ea * ``MatterThermostat``: Thermostat with temperature control and setpoints * ``MatterOnOffPlugin``: On/off plugin unit (power outlet/relay) * ``MatterGenericSwitch``: Generic switch endpoint (smart button) +* ``MatterWindowCovering``: Window covering with lift and tilt control (blinds, shades) .. toctree:: :maxdepth: 1 diff --git a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino new file mode 100644 index 00000000000..142a36d4cd6 --- /dev/null +++ b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino @@ -0,0 +1,356 @@ +// 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 +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space +#include +#endif +#include + +// 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 = 40; // 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) { + // 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; + +#ifdef RGB_BUILTIN + 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 myTargetLiftPercent = currentLiftPercent; + // go to the closest next 20% or move 20% more + if ((myTargetLiftPercent % 20) != 0) { + myTargetLiftPercent = ((myTargetLiftPercent / 20) + 1) * 20; + } else { + myTargetLiftPercent += 20; + } + if (myTargetLiftPercent > 100) { + myTargetLiftPercent = 0; + } + Serial.printf("User button released. Setting lift to %d%%\r\n", myTargetLiftPercent); + WindowBlinds.setTargetLiftPercent100ths(myTargetLiftPercent * 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 + } +} diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md new file mode 100644 index 00000000000..0a1e411154b --- /dev/null +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -0,0 +1,228 @@ +# Matter Window Covering Example + +This example demonstrates how to create a Matter-compatible window covering device using an ESP32 SoC microcontroller. The application showcases Matter commissioning, device control via smart home ecosystems, and manual control using a physical button. + +## Supported Targets + +| SoC | Wi-Fi | Thread | BLE Commissioning | Status | +| --- | ---- | ------ | ----------------- | ------ | +| ESP32 | ✅ | ❌ | ❌ | Fully supported | +| ESP32-S2 | ✅ | ❌ | ❌ | Fully supported | +| ESP32-S3 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C3 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C5 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C6 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-H2 | ❌ | ✅ | ✅ | Supported (Thread only) | + +### Note on Commissioning: + +- **ESP32 & ESP32-S2** do not support commissioning over Bluetooth LE. For these chips, you must provide Wi-Fi credentials directly in the sketch code so they can connect to your network manually. +- **ESP32-C6** Although it has Thread support, the ESP32 Arduino Matter Library has been pre compiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. +- **ESP32-C5** Although it has Thread support, the ESP32 Arduino Matter Library has been pre compiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. + +## Features + +- Matter protocol implementation for a window covering device +- Support for both Wi-Fi and Thread(*) connectivity +- Lift position and percentage control (0-100%) - Lift represents the physical position in centimeters +- Tilt rotation and percentage control (0-100%) - Tilt represents rotation of the shade, not a linear measurement +- Multiple window covering types support +- State persistence using `Preferences` library +- Button control for manual lift adjustment and factory reset +- RGB LED visualization of lift (brightness) and tilt (color) positions +- Installed limit configuration for lift (in cm) and tilt (absolute values) +- Matter commissioning via QR code or manual pairing code +- Integration with Apple HomeKit, Amazon Alexa, and Google Home +(*) It is necessary to compile the project using Arduino as IDF Component. + +## Hardware Requirements + +- ESP32 compatible development board (see supported targets table) +- RGB LED for visualization (uses built-in RGB LED if available) +- User button for manual control (uses BOOT button by default) + +## Pin Configuration + +- **RGB LED**: Uses `RGB_BUILTIN` if defined (for visualization), otherwise pin 2. The LED brightness represents lift position (0% = off, 100% = full brightness), and color represents tilt rotation (red = 0%, blue = 100%). +- **Button**: Uses `BOOT_PIN` by default + +## Software Setup + +### Prerequisites + +1. Install the Arduino IDE (2.0 or newer recommended) +2. Install ESP32 Arduino Core with Matter support +3. ESP32 Arduino libraries: + - `Matter` + - `Preferences` + - `Wi-Fi` (only for ESP32 and ESP32-S2) + +### Configuration + +Before uploading the sketch, configure the following: + +1. **Wi-Fi credentials** (if not using BLE commissioning - mandatory for ESP32 | ESP32-S2): + ```cpp + const char *ssid = "your-ssid"; // Change to your Wi-Fi SSID + const char *password = "your-password"; // Change to your Wi-Fi password + ``` + +2. **RGB LED pin configuration** (if not using built-in RGB LED): + ```cpp + const uint8_t ledPin = 2; // Set your RGB LED pin here + ``` + +3. **Button pin configuration** (optional): + By default, the `BOOT` button (GPIO 0) is used for manual lift control and factory reset. You can change this to a different pin if needed. + ```cpp + const uint8_t buttonPin = BOOT_PIN; // Set your button pin here + ``` + +## Building and Flashing + +1. Open the `MatterWindowCovering.ino` sketch in the Arduino IDE. +2. Select your ESP32 board from the **Tools > Board** menu. +3. Connect your ESP32 board to your computer via USB. +4. Click the **Upload** button to compile and flash the sketch. + +## Expected Output + +Once the sketch is running, open the Serial Monitor at a baud rate of **115200**. The Wi-Fi connection messages will be displayed only for ESP32 and ESP32-S2. Other targets will use Matter CHIPoBLE to automatically setup the IP Network. You should see output similar to the following: + +``` +Connecting to your-wifi-ssid +....... +WiFi connected +IP address: 192.168.1.100 + +Matter Node is not commissioned yet. +Initiate the device discovery in your Matter environment. +Commission it to your Matter hub with the manual pairing code or QR code +Manual pairing code: 34970112332 +QR code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT%3A6FCJ142C00KA0648G00 +Matter Node not commissioned yet. Waiting for commissioning. +... +Initial state: Lift=100%, Tilt=0% +Matter Node is commissioned and connected to the network. Ready for use. +Window Covering changed: Lift=100%, Tilt=0% +Moving lift to 50% (position: 100 cm) +Window Covering changed: Lift=50%, Tilt=0% +``` + +## Using the Device + +### Manual Control + +The user button (BOOT button by default) provides manual control: + +- **Short press of the button**: Cycle lift percentage by 20% increments. If the current position is not a multiple of 20%, it will round up to the next multiple of 20%. Otherwise, it will add 20% (0% → 20% → 40% → ... → 100% → 0%) +- **Long press (>5 seconds)**: Factory reset the device (decommission) + +### State Persistence + +The device saves the last known lift and tilt percentages using the `Preferences` library. After a power cycle or restart: + +- The device will restore to the last saved lift and tilt percentages +- Default state is 100% lift (fully open) and 0% tilt if no previous state was saved +- The Matter controller will be notified of the restored state +- The RGB LED will reflect the restored state + +### RGB LED Visualization + +The RGB LED provides visual feedback: + +- **Brightness**: Represents lift position (0% = off, 100% = full brightness) +- **Color**: Represents tilt position (red = 0% tilt, blue = 100% tilt) +- For boards without RGB LED, only brightness is used + +### Window Covering Integration + +For production use with a motorized window covering: + +1. **Motor Control**: + - Connect your motor driver to your ESP32 + - Update the callback functions (`fullOpen()`, `fullClose()`, `goToLiftPercentage()`, `goToTiltPercentage()`, `stopMotor()`) to control your actual motor + - The example currently simulates movement instantly - replace with actual motor control code + +2. **Position Feedback**: + - Use encoders or limit switches to provide position feedback + - For lift: Update `currentLift` (in cm) based on actual motor position + - For tilt: Update `currentTiltPercent` (rotation percentage) based on actual motor rotation + - Call `setLiftPercentage()` and `setTiltPercentage()` to update `CurrentPosition` attributes (these methods update the device's actual position, not the target) + - Call `setOperationalState(LIFT, STALL)` or `setOperationalState(TILT, STALL)` when movement is complete to indicate the device has reached the target position + - Configure installed limits using `setInstalledOpenLimitLift()`, `setInstalledClosedLimitLift()`, `setInstalledOpenLimitTilt()`, and `setInstalledClosedLimitTilt()` to define the physical range of your window covering + +3. **Window Covering Type**: + - Pass the covering type to `begin()` to configure the appropriate type (e.g., `BLIND_LIFT_AND_TILT`, `ROLLERSHADE`, etc.) + - Different types support different features (lift only, tilt only, or both) + - The covering type must be specified during initialization to ensure the correct features are enabled + +### Smart Home Integration + +Use a Matter-compatible hub (like an Apple HomePod, Google Nest Hub, or Amazon Echo) to commission the device. + +#### Apple Home + +1. Open the Home app on your iOS device +2. Tap the "+" button > Add Accessory +3. Scan the QR code displayed in the Serial Monitor, or +4. Tap "I Don't Have a Code or Cannot Scan" and enter the manual pairing code +5. Follow the prompts to complete setup +6. The device will appear as a window covering/blind in your Home app +7. You can control both lift and tilt positions using sliders + +#### Amazon Alexa + +1. Open the Alexa app +2. Tap More > Add Device > Matter +3. Select "Scan QR code" or "Enter code manually" +4. Complete the setup process +5. The window covering will appear in your Alexa app +6. You can control positions using voice commands like "Alexa, set blinds to 50 percent" + +#### Google Home + +1. Open the Google Home app +2. Tap "+" > Set up device > New device +3. Choose "Matter device" +4. Scan the QR code or enter the manual pairing code +5. Follow the prompts to complete setup +6. You can control positions using voice commands or sliders in the app + +## Code Structure + +The MatterWindowCovering example consists of the following main components: + +1. **`setup()`**: Initializes hardware (button, RGB LED), configures Wi-Fi (if needed), initializes `Preferences` library, sets up the Matter window covering endpoint with the last saved state, registers callback functions, and starts the Matter stack. + +2. **`loop()`**: Checks the Matter commissioning state, handles button input for manual lift control and factory reset, and allows the Matter stack to process events. + +3. **Callbacks**: + - `fullOpen()`: Handles open command - moves window covering to fully open (100% lift), updates `CurrentPosition`, and sets operational state to `STALL` + - `fullClose()`: Handles close command - moves window covering to fully closed (0% lift), updates `CurrentPosition`, and sets operational state to `STALL` + - `goToLiftPercentage()`: Handles lift percentage changes (0-100%), calculates absolute position in cm based on installed limits, updates `CurrentPosition`, and sets operational state to `STALL` when movement is complete + - `goToTiltPercentage()`: Handles tilt rotation percentage changes (0-100%), updates `CurrentPosition`, and sets operational state to `STALL` when movement is complete + - `stopMotor()`: Handles stop command - stops any ongoing movement, updates `CurrentPosition` for both lift and tilt, and sets operational state to `STALL` for both + - `visualizeWindowBlinds()`: Updates RGB LED to reflect current lift and tilt positions + +## Troubleshooting + +- **Device not visible during commissioning**: Ensure Wi-Fi or Thread connectivity is properly configured +- **Window covering not responding**: Verify callback functions are properly implemented and motor control is working +- **Position not updating**: Check that `setLiftPercentage()` and `setTiltPercentage()` are being called with correct values +- **State not persisting**: Check that the `Preferences` library is properly initialized and that flash memory is not corrupted +- **RGB LED not working**: For RGB LED, verify the pin supports RGB LED control. For non-RGB boards, ensure the pin supports PWM (analogWrite) +- **Tilt not working**: Ensure the covering type supports tilt (e.g., `BLIND_LIFT_AND_TILT`, `SHUTTER`, or `BLIND_TILT_ONLY`) and that it is specified in `begin()` +- **Failed to commission**: Try factory resetting the device by long-pressing the button. Other option would be to erase the SoC Flash Memory by using `Arduino IDE Menu` -> `Tools` -> `Erase All Flash Before Sketch Upload: "Enabled"` or directly with `esptool.py --port erase_flash` +- **No serial output**: Check baudrate (115200) and USB connection + +## Related Documentation + +- [Matter Overview](https://docs.espressif.com/projects/arduino-esp32/en/latest/matter/matter.html) +- [Matter Endpoint Base Class](https://docs.espressif.com/projects/arduino-esp32/en/latest/matter/matter_ep.html) +- [Matter Window Covering Endpoint](https://docs.espressif.com/projects/arduino-esp32/en/latest/matter/ep_window_covering.html) + +## License + +This example is licensed under the Apache License, Version 2.0. + diff --git a/libraries/Matter/examples/MatterWindowCovering/ci.yml b/libraries/Matter/examples/MatterWindowCovering/ci.yml new file mode 100644 index 00000000000..09074596de8 --- /dev/null +++ b/libraries/Matter/examples/MatterWindowCovering/ci.yml @@ -0,0 +1,4 @@ +fqbn_append: PartitionScheme=huge_app + +requires: + - CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y \ No newline at end of file diff --git a/libraries/Matter/keywords.txt b/libraries/Matter/keywords.txt index fc8ccf082f0..3d8e7861776 100644 --- a/libraries/Matter/keywords.txt +++ b/libraries/Matter/keywords.txt @@ -27,9 +27,18 @@ MatterPressureSensor KEYWORD1 MatterOccupancySensor KEYWORD1 MatterOnOffPlugin KEYWORD1 MatterThermostat KEYWORD1 +MatterWindowCovering KEYWORD1 ControlSequenceOfOperation_t KEYWORD1 ThermostatMode_t KEYWORD1 +WindowCoveringType_t KEYWORD1 +OperationalState_t KEYWORD1 +OperationalStatusField_t KEYWORD1 EndPointCB KEYWORD1 +EndPointOpenCB KEYWORD1 +EndPointCloseCB KEYWORD1 +EndPointLiftCB KEYWORD1 +EndPointTiltCB KEYWORD1 +EndPointStopCB KEYWORD1 EndPointHeatingSetpointCB KEYWORD1 EndPointCoolingSetpointCB KEYWORD1 EndPointTemperatureCB KEYWORD1 @@ -118,6 +127,37 @@ onChangeMode KEYWORD2 onChangeLocalTemperature KEYWORD2 onChangeCoolingSetpoint KEYWORD2 onChangeHeatingSetpoint KEYWORD2 +setLiftPosition KEYWORD2 +getLiftPosition KEYWORD2 +setLiftPercentage KEYWORD2 +getLiftPercentage KEYWORD2 +setTargetLiftPercent100ths KEYWORD2 +getTargetLiftPercent100ths KEYWORD2 +setInstalledOpenLimitLift KEYWORD2 +getInstalledOpenLimitLift KEYWORD2 +setInstalledClosedLimitLift KEYWORD2 +getInstalledClosedLimitLift KEYWORD2 +setTiltPosition KEYWORD2 +getTiltPosition KEYWORD2 +setTiltPercentage KEYWORD2 +getTiltPercentage KEYWORD2 +setTargetTiltPercent100ths KEYWORD2 +getTargetTiltPercent100ths KEYWORD2 +setInstalledOpenLimitTilt KEYWORD2 +getInstalledOpenLimitTilt KEYWORD2 +setInstalledClosedLimitTilt KEYWORD2 +getInstalledClosedLimitTilt KEYWORD2 +setCoveringType KEYWORD2 +getCoveringType KEYWORD2 +setOperationalStatus KEYWORD2 +getOperationalStatus KEYWORD2 +setOperationalState KEYWORD2 +getOperationalState KEYWORD2 +onOpen KEYWORD2 +onClose KEYWORD2 +onGoToLiftPercentage KEYWORD2 +onGoToTiltPercentage KEYWORD2 +onStop KEYWORD2 onEvent KEYWORD2 setEndPointId KEYWORD2 getEndPointId KEYWORD2 diff --git a/libraries/Matter/src/Matter.h b/libraries/Matter/src/Matter.h index a5afab14de1..66570555250 100644 --- a/libraries/Matter/src/Matter.h +++ b/libraries/Matter/src/Matter.h @@ -35,6 +35,7 @@ #include #include #include +#include // Matter Event types used when there is a user callback for Matter Events enum matterEvent_t { @@ -202,6 +203,7 @@ class ArduinoMatter { friend class MatterOccupancySensor; friend class MatterOnOffPlugin; friend class MatterThermostat; + friend class MatterWindowCovering; protected: static void _init(); diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp new file mode 100644 index 00000000000..321a8ee8268 --- /dev/null +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -0,0 +1,857 @@ +// 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. + +#include +#ifdef CONFIG_ESP_MATTER_ENABLE_DATA_MODEL + +#include +#include + +using namespace esp_matter; +using namespace esp_matter::endpoint; +using namespace esp_matter::cluster; +using namespace esp_matter::cluster::window_covering; +using namespace esp_matter::cluster::window_covering::command; +using namespace esp_matter::cluster::window_covering::feature; +using namespace chip::app::Clusters; + +MatterWindowCovering::MatterWindowCovering() {} + +MatterWindowCovering::~MatterWindowCovering() { + end(); +} + +bool MatterWindowCovering::begin(uint8_t liftPercent, uint8_t tiltPercent, WindowCoveringType_t _coveringType) { + ArduinoMatter::_init(); + + if (getEndPointId() != 0) { + log_e("Matter Window Covering with Endpoint Id %d device has already been created.", getEndPointId()); + return false; + } + + coveringType = (_coveringType == 0) ? ROLLERSHADE : _coveringType; + + window_covering_device::config_t window_covering_config(0); + window_covering_config.window_covering.type = (uint8_t)coveringType; + window_covering_config.window_covering.config_status = 0; + window_covering_config.window_covering.operational_status = 0; + + currentLiftPercent = liftPercent; + currentTiltPercent = tiltPercent; + currentLiftPosition = 0; + currentTiltPosition = 0; + + endpoint_t *endpoint = window_covering_device::create(node::get(), &window_covering_config, ENDPOINT_FLAG_NONE, (void *)this); + if (endpoint == nullptr) { + log_e("Failed to create window covering endpoint"); + return false; + } + + setEndPointId(endpoint::get_id(endpoint)); + log_i("Window Covering created with endpoint_id %d", getEndPointId()); + + // Get the Window Covering cluster and add features and commands + cluster_t *window_covering_cluster = cluster::get(endpoint, WindowCovering::Id); + if (window_covering_cluster != nullptr) { + // Add Lift feature + feature::lift::config_t lift_config; + lift_config.number_of_actuations_lift = 0; + if (feature::lift::add(window_covering_cluster, &lift_config) != ESP_OK) { + log_e("Failed to add Lift feature"); + } + + // Add Position Aware Lift feature + feature::position_aware_lift::config_t position_aware_lift_config; + position_aware_lift_config.current_position_lift_percentage = nullable(0); + position_aware_lift_config.target_position_lift_percent_100ths = nullable(liftPercent * 100); + position_aware_lift_config.current_position_lift_percent_100ths = nullable(liftPercent * 100); + if (feature::position_aware_lift::add(window_covering_cluster, &position_aware_lift_config) != ESP_OK) { + log_e("Failed to add Position Aware Lift feature"); + } + + // Add Tilt feature if the covering type supports it + bool supportsTilt = (coveringType == SHUTTER || coveringType == BLIND_TILT_ONLY || coveringType == BLIND_LIFT_AND_TILT); + if (supportsTilt) { + feature::tilt::config_t tilt_config; + tilt_config.number_of_actuations_tilt = 0; + if (feature::tilt::add(window_covering_cluster, &tilt_config) != ESP_OK) { + log_e("Failed to add Tilt feature"); + } + + // Add Position Aware Tilt feature + feature::position_aware_tilt::config_t position_aware_tilt_config; + position_aware_tilt_config.current_position_tilt_percentage = nullable(0); + position_aware_tilt_config.target_position_tilt_percent_100ths = nullable(tiltPercent * 100); + position_aware_tilt_config.current_position_tilt_percent_100ths = nullable(tiltPercent * 100); + if (feature::position_aware_tilt::add(window_covering_cluster, &position_aware_tilt_config) != ESP_OK) { + log_e("Failed to add Position Aware Tilt feature"); + } + } + + // Add Absolute Position feature (creates InstalledOpenLimitLift/ClosedLimitLift/Tilt attributes) + // Must be added AFTER all lift and tilt features for all attributes to be created + feature::absolute_position::config_t absolute_position_config; + absolute_position_config.installed_open_limit_lift = 0; + absolute_position_config.installed_closed_limit_lift = 65534; + absolute_position_config.installed_open_limit_tilt = 0; + absolute_position_config.installed_closed_limit_tilt = 65534; + if (feature::absolute_position::add(window_covering_cluster, &absolute_position_config) != ESP_OK) { + log_e("Failed to add Absolute Position feature"); + } + + // Create Window Covering commands + create_up_or_open(window_covering_cluster); + create_down_or_close(window_covering_cluster); + create_stop_motion(window_covering_cluster); + create_go_to_lift_value(window_covering_cluster); + create_go_to_lift_percentage(window_covering_cluster); + if (supportsTilt) { + create_go_to_tilt_value(window_covering_cluster); + create_go_to_tilt_percentage(window_covering_cluster); + } + } else { + log_e("Failed to get Window Covering cluster for feature and command creation"); + } + + started = true; + + // Set initial lift and tilt percentages + if (liftPercent > 0) { + setLiftPercentage(liftPercent); + } + if (tiltPercent > 0) { + setTiltPercentage(tiltPercent); + } + + return true; +} + +void MatterWindowCovering::end() { + started = false; +} + +bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val) { + bool ret = true; + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + log_d("Window Covering Attr update callback: endpoint: %u, cluster: %u, attribute: %u", endpoint_id, cluster_id, attribute_id); + + if (endpoint_id == getEndPointId() && cluster_id == WindowCovering::Id) { + switch (attribute_id) { + // Current position attributes (read-only, updated by device) + case WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id: { + uint16_t liftPercent100ths = val->val.u16; + uint8_t liftPercent = (uint8_t)(liftPercent100ths / 100); + log_d("Window Covering Lift Percentage changed to %d%%", liftPercent); + if (currentLiftPercent != liftPercent) { + if (_onChangeCB != NULL) { + ret &= _onChangeCB(liftPercent, currentTiltPercent); + } + if (ret == true) { + currentLiftPercent = liftPercent; + } + } + break; + } + case WindowCovering::Attributes::CurrentPositionTiltPercent100ths::Id: { + uint16_t tiltPercent100ths = val->val.u16; + uint8_t tiltPercent = (uint8_t)(tiltPercent100ths / 100); + log_d("Window Covering Tilt Percentage changed to %d%%", tiltPercent); + if (currentTiltPercent != tiltPercent) { + if (_onChangeCB != NULL) { + ret &= _onChangeCB(currentLiftPercent, tiltPercent); + } + if (ret == true) { + currentTiltPercent = tiltPercent; + } + } + break; + } + case WindowCovering::Attributes::CurrentPositionLift::Id: + log_d("Window Covering Lift Position changed to %d", val->val.u16); + currentLiftPosition = val->val.u16; + break; + case WindowCovering::Attributes::CurrentPositionTilt::Id: + log_d("Window Covering Tilt Position changed to %d", val->val.u16); + currentTiltPosition = val->val.u16; + break; + case WindowCovering::Attributes::CurrentPositionLiftPercentage::Id: + log_d("Window Covering Lift Percentage (legacy) changed to %d%%", val->val.u8); + break; + case WindowCovering::Attributes::CurrentPositionTiltPercentage::Id: + log_d("Window Covering Tilt Percentage (legacy) changed to %d%%", val->val.u8); + break; + + // Target position attributes (writable, trigger movement) + // Note: TargetPosition is where the device SHOULD go, not where it is. + // CurrentPosition should only be updated when the physical device actually moves. + case WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id: { + if (!chip::app::NumericAttributeTraits::IsNullValue(val->val.u16)) { + uint16_t targetLiftPercent100ths = val->val.u16; + uint8_t targetLiftPercent = (uint8_t)(targetLiftPercent100ths / 100); + log_d("Window Covering Target Lift Percentage changed to %d%%", targetLiftPercent); + // Call callback to trigger movement - do NOT update currentLiftPercent here + // CurrentPosition will be updated by the application when the device actually moves + if (_onGoToLiftPercentageCB != NULL) { + ret &= _onGoToLiftPercentageCB(targetLiftPercent); + } + } else { + log_d("Window Covering Target Lift Percentage set to NULL"); + } + break; + } + case WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id: { + if (!chip::app::NumericAttributeTraits::IsNullValue(val->val.u16)) { + uint16_t targetTiltPercent100ths = val->val.u16; + uint8_t targetTiltPercent = (uint8_t)(targetTiltPercent100ths / 100); + log_d("Window Covering Target Tilt Percentage changed to %d%%", targetTiltPercent); + // Call callback to trigger movement - do NOT update currentTiltPercent here + // CurrentPosition will be updated by the application when the device actually moves + if (_onGoToTiltPercentageCB != NULL) { + ret &= _onGoToTiltPercentageCB(targetTiltPercent); + } + } else { + log_d("Window Covering Target Tilt Percentage set to NULL"); + } + break; + } + + // Configuration attributes + case WindowCovering::Attributes::Type::Id: + log_d("Window Covering Type changed to %d", val->val.u8); + coveringType = (WindowCoveringType_t)val->val.u8; + break; + case WindowCovering::Attributes::EndProductType::Id: + log_d("Window Covering End Product Type changed to %d", val->val.u8); + break; + case WindowCovering::Attributes::ConfigStatus::Id: + log_d("Window Covering Config Status changed to 0x%02X", val->val.u8); + break; + case WindowCovering::Attributes::Mode::Id: + log_d("Window Covering Mode changed to 0x%02X", val->val.u8); + break; + + // Operational status attributes + case WindowCovering::Attributes::OperationalStatus::Id: + log_d("Window Covering Operational Status changed to 0x%02X", val->val.u8); + break; + case WindowCovering::Attributes::SafetyStatus::Id: + log_d("Window Covering Safety Status changed to 0x%04X", val->val.u16); + break; + + // Limit attributes + case WindowCovering::Attributes::PhysicalClosedLimitLift::Id: + log_d("Window Covering Physical Closed Limit Lift changed to %d", val->val.u16); + break; + case WindowCovering::Attributes::PhysicalClosedLimitTilt::Id: + log_d("Window Covering Physical Closed Limit Tilt changed to %d", val->val.u16); + break; + case WindowCovering::Attributes::InstalledOpenLimitLift::Id: + log_d("Window Covering Installed Open Limit Lift changed to %d", val->val.u16); + break; + case WindowCovering::Attributes::InstalledClosedLimitLift::Id: + log_d("Window Covering Installed Closed Limit Lift changed to %d", val->val.u16); + break; + case WindowCovering::Attributes::InstalledOpenLimitTilt::Id: + log_d("Window Covering Installed Open Limit Tilt changed to %d", val->val.u16); + break; + case WindowCovering::Attributes::InstalledClosedLimitTilt::Id: + log_d("Window Covering Installed Closed Limit Tilt changed to %d", val->val.u16); + break; + + // Actuation count attributes + case WindowCovering::Attributes::NumberOfActuationsLift::Id: + log_d("Window Covering Number of Actuations Lift changed to %d", val->val.u16); + break; + case WindowCovering::Attributes::NumberOfActuationsTilt::Id: + log_d("Window Covering Number of Actuations Tilt changed to %d", val->val.u16); + break; + + default: + log_d("Window Covering Unknown attribute %u changed", attribute_id); + break; + } + } + return ret; +} + +bool MatterWindowCovering::setLiftPosition(uint16_t liftPosition) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + if (currentLiftPosition == liftPosition) { + return true; + } + + // Get InstalledOpenLimitLift and InstalledClosedLimitLift for conversion + uint16_t openLimit = 0; + uint16_t closedLimit = 0; + esp_matter_attr_val_t limitVal = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitLift::Id, &limitVal)) { + openLimit = limitVal.val.u16; + } + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitLift::Id, &limitVal)) { + closedLimit = limitVal.val.u16; + } + + // Convert absolute position to percent100ths + // Using the same logic as ESP-Matter's LiftToPercent100ths + uint16_t liftPercent100ths = 0; + if (openLimit != closedLimit) { + // Linear interpolation between open (0% = 0) and closed (100% = 10000) + if (openLimit < closedLimit) { + // Normal: open < closed + if (liftPosition <= openLimit) { + liftPercent100ths = 0; // Fully open + } else if (liftPosition >= closedLimit) { + liftPercent100ths = 10000; // Fully closed + } else { + liftPercent100ths = ((liftPosition - openLimit) * 10000) / (closedLimit - openLimit); + } + } else { + // Inverted: open > closed + if (liftPosition >= openLimit) { + liftPercent100ths = 0; // Fully open + } else if (liftPosition <= closedLimit) { + liftPercent100ths = 10000; // Fully closed + } else { + liftPercent100ths = ((openLimit - liftPosition) * 10000) / (openLimit - closedLimit); + } + } + } + + // Update CurrentPositionLift (absolute) + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionLift::Id, &val)) { + log_e("Failed to get Lift Position Attribute."); + return false; + } + + if (val.val.u16 != liftPosition) { + val.val.u16 = liftPosition; + bool ret = updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionLift::Id, &val); + if (ret) { + currentLiftPosition = liftPosition; + // Also update CurrentPositionLiftPercent100ths to keep attributes in sync + // This matches ESP-Matter's LiftPositionSet() behavior + setLiftPercentage((uint8_t)(liftPercent100ths / 100)); + } + return ret; + } + return true; +} + +uint16_t MatterWindowCovering::getLiftPosition() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionLift::Id, &val)) { + currentLiftPosition = val.val.u16; + } + return currentLiftPosition; +} + +bool MatterWindowCovering::setLiftPercentage(uint8_t liftPercent) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + if (liftPercent > 100) { + log_e("Lift percentage must be between 0 and 100"); + return false; + } + + if (currentLiftPercent == liftPercent) { + return true; + } + + // Matter uses percent100ths (0-10000 for 0-100%) + uint16_t liftPercent100ths = liftPercent * 100; + + // Update only CurrentPosition, not TargetPosition + // TargetPosition is set by Matter commands/apps, CurrentPosition reflects actual position + esp_matter_attr_val_t currentVal = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id, ¤tVal)) { + log_e("Failed to get Current Lift Percentage Attribute."); + return false; + } + + if (currentVal.val.u16 != liftPercent100ths) { + currentVal.val.u16 = liftPercent100ths; + bool ret = updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id, ¤tVal); + if (ret) { + currentLiftPercent = liftPercent; + } + return ret; + } + return true; +} + +uint8_t MatterWindowCovering::getLiftPercentage() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id, &val)) { + currentLiftPercent = (uint8_t)(val.val.u16 / 100); + } + return currentLiftPercent; +} + +bool MatterWindowCovering::setTargetLiftPercent100ths(uint16_t liftPercent100ths) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + if (liftPercent100ths > 10000) { + log_e("Lift percent100ths must be between 0 and 10000"); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id, &val)) { + log_e("Failed to get Target Lift Percentage Attribute."); + return false; + } + + if (val.val.u16 != liftPercent100ths) { + val.val.u16 = liftPercent100ths; + return updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id, &val); + } + return true; +} + +uint16_t MatterWindowCovering::getTargetLiftPercent100ths() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id, &val)) { + if (!chip::app::NumericAttributeTraits::IsNullValue(val.val.u16)) { + return val.val.u16; + } + } + return 0; +} + +bool MatterWindowCovering::setTiltPosition(uint16_t tiltPosition) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + if (currentTiltPosition == tiltPosition) { + return true; + } + + // Get InstalledOpenLimitTilt and InstalledClosedLimitTilt for conversion + uint16_t openLimit = 0; + uint16_t closedLimit = 0; + esp_matter_attr_val_t limitVal = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitTilt::Id, &limitVal)) { + openLimit = limitVal.val.u16; + } + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitTilt::Id, &limitVal)) { + closedLimit = limitVal.val.u16; + } + + // Convert absolute position to percent100ths + // Using the same logic as ESP-Matter's TiltToPercent100ths + uint16_t tiltPercent100ths = 0; + if (openLimit != closedLimit) { + // Linear interpolation between open (0% = 0) and closed (100% = 10000) + if (openLimit < closedLimit) { + // Normal: open < closed + if (tiltPosition <= openLimit) { + tiltPercent100ths = 0; // Fully open + } else if (tiltPosition >= closedLimit) { + tiltPercent100ths = 10000; // Fully closed + } else { + tiltPercent100ths = ((tiltPosition - openLimit) * 10000) / (closedLimit - openLimit); + } + } else { + // Inverted: open > closed + if (tiltPosition >= openLimit) { + tiltPercent100ths = 0; // Fully open + } else if (tiltPosition <= closedLimit) { + tiltPercent100ths = 10000; // Fully closed + } else { + tiltPercent100ths = ((openLimit - tiltPosition) * 10000) / (openLimit - closedLimit); + } + } + } + + // Update CurrentPositionTilt (absolute) + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionTilt::Id, &val)) { + log_e("Failed to get Tilt Position Attribute."); + return false; + } + + if (val.val.u16 != tiltPosition) { + val.val.u16 = tiltPosition; + bool ret = updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionTilt::Id, &val); + if (ret) { + currentTiltPosition = tiltPosition; + // Also update CurrentPositionTiltPercent100ths to keep attributes in sync + // This matches ESP-Matter's TiltPositionSet() behavior + setTiltPercentage((uint8_t)(tiltPercent100ths / 100)); + } + return ret; + } + return true; +} + +uint16_t MatterWindowCovering::getTiltPosition() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionTilt::Id, &val)) { + currentTiltPosition = val.val.u16; + } + return currentTiltPosition; +} + +bool MatterWindowCovering::setTiltPercentage(uint8_t tiltPercent) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + if (tiltPercent > 100) { + log_e("Tilt percentage must be between 0 and 100"); + return false; + } + + if (currentTiltPercent == tiltPercent) { + return true; + } + + // Matter uses percent100ths (0-10000 for 0-100%) + uint16_t tiltPercent100ths = tiltPercent * 100; + + // Update only CurrentPosition, not TargetPosition + // TargetPosition is set by Matter commands/apps, CurrentPosition reflects actual position + esp_matter_attr_val_t currentVal = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionTiltPercent100ths::Id, ¤tVal)) { + log_e("Failed to get Current Tilt Percentage Attribute."); + return false; + } + + if (currentVal.val.u16 != tiltPercent100ths) { + currentVal.val.u16 = tiltPercent100ths; + bool ret = updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionTiltPercent100ths::Id, ¤tVal); + if (ret) { + currentTiltPercent = tiltPercent; + } + return ret; + } + return true; +} + +uint8_t MatterWindowCovering::getTiltPercentage() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionTiltPercent100ths::Id, &val)) { + currentTiltPercent = (uint8_t)(val.val.u16 / 100); + } + return currentTiltPercent; +} + +bool MatterWindowCovering::setTargetTiltPercent100ths(uint16_t tiltPercent100ths) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + if (tiltPercent100ths > 10000) { + log_e("Tilt percent100ths must be between 0 and 10000"); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id, &val)) { + log_e("Failed to get Target Tilt Percentage Attribute."); + return false; + } + + if (val.val.u16 != tiltPercent100ths) { + val.val.u16 = tiltPercent100ths; + return updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id, &val); + } + return true; +} + +uint16_t MatterWindowCovering::getTargetTiltPercent100ths() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id, &val)) { + if (!chip::app::NumericAttributeTraits::IsNullValue(val.val.u16)) { + return val.val.u16; + } + } + return 0; +} + +bool MatterWindowCovering::setInstalledOpenLimitLift(uint16_t openLimit) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitLift::Id, &val)) { + log_e("Failed to get Installed Open Limit Lift Attribute"); + return false; + } + + if (val.val.u16 != openLimit) { + val.val.u16 = openLimit; + return setAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitLift::Id, &val); + } + return true; +} + +uint16_t MatterWindowCovering::getInstalledOpenLimitLift() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitLift::Id, &val)) { + return val.val.u16; + } + return 0; +} + +bool MatterWindowCovering::setInstalledClosedLimitLift(uint16_t closedLimit) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitLift::Id, &val)) { + log_e("Failed to get Installed Closed Limit Lift Attribute."); + return false; + } + + if (val.val.u16 != closedLimit) { + val.val.u16 = closedLimit; + return setAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitLift::Id, &val); + } + return true; +} + +uint16_t MatterWindowCovering::getInstalledClosedLimitLift() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitLift::Id, &val)) { + return val.val.u16; + } + return 0; +} + +bool MatterWindowCovering::setInstalledOpenLimitTilt(uint16_t openLimit) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitTilt::Id, &val)) { + log_e("Failed to get Installed Open Limit Tilt Attribute."); + return false; + } + + if (val.val.u16 != openLimit) { + val.val.u16 = openLimit; + return setAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitTilt::Id, &val); + } + return true; +} + +uint16_t MatterWindowCovering::getInstalledOpenLimitTilt() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledOpenLimitTilt::Id, &val)) { + return val.val.u16; + } + return 0; +} + +bool MatterWindowCovering::setInstalledClosedLimitTilt(uint16_t closedLimit) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitTilt::Id, &val)) { + log_e("Failed to get Installed Closed Limit Tilt Attribute."); + return false; + } + + if (val.val.u16 != closedLimit) { + val.val.u16 = closedLimit; + return setAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitTilt::Id, &val); + } + return true; +} + +uint16_t MatterWindowCovering::getInstalledClosedLimitTilt() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::InstalledClosedLimitTilt::Id, &val)) { + return val.val.u16; + } + return 0; +} + +bool MatterWindowCovering::setCoveringType(WindowCoveringType_t coveringType) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::Type::Id, &val)) { + log_e("Failed to get Window Covering Type Attribute."); + return false; + } + + if (val.val.u8 != (uint8_t)coveringType) { + val.val.u8 = (uint8_t)coveringType; + bool ret = updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::Type::Id, &val); + if (ret) { + this->coveringType = coveringType; + } + return ret; + } + return true; +} + +MatterWindowCovering::WindowCoveringType_t MatterWindowCovering::getCoveringType() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::Type::Id, &val)) { + coveringType = (WindowCoveringType_t)val.val.u8; + } + return coveringType; +} + +bool MatterWindowCovering::setOperationalStatus(uint8_t operationalStatus) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::OperationalStatus::Id, &val)) { + log_e("Failed to get Operational Status Attribute."); + return false; + } + + if (val.val.u8 != operationalStatus) { + val.val.u8 = operationalStatus; + return updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::OperationalStatus::Id, &val); + } + return true; +} + +uint8_t MatterWindowCovering::getOperationalStatus() { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::OperationalStatus::Id, &val)) { + return val.val.u8; + } + return 0; +} + +bool MatterWindowCovering::setOperationalState(OperationalStatusField_t field, OperationalState_t state) { + if (!started) { + log_e("Matter Window Covering device has not begun."); + return false; + } + + // ESP-Matter only allows setting Lift or Tilt, not Global directly + // Global is automatically updated based on Lift (priority) or Tilt + if (field == GLOBAL) { + log_e("Cannot set Global operational state directly. Set Lift or Tilt instead."); + return false; + } + + if (field != LIFT && field != TILT) { + log_e("Invalid Operational Status Field. Only LIFT or TILT are allowed."); + return false; + } + + // Get current operational status + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::OperationalStatus::Id, &val)) { + log_e("Failed to get Operational Status Attribute."); + return false; + } + + uint8_t currentStatus = val.val.u8; + uint8_t fieldMask = (uint8_t)field; + uint8_t fieldShift = (field == LIFT) ? 2 : 4; // LIFT: bits 2-3, TILT: bits 4-5 + + // Extract current state for this field + uint8_t currentFieldState = (currentStatus & fieldMask) >> fieldShift; + + // Only update if state changed + if (currentFieldState != (uint8_t)state) { + // Clear the field and set new state + currentStatus = (currentStatus & ~fieldMask) | (((uint8_t)state << fieldShift) & fieldMask); + + // Following ESP-Matter behavior: + // 1. Set the field (Lift or Tilt) to the new state + // 2. Temporarily set Global to the same state + // 3. Recalculate Global based on priority: Lift (if not Stall) > Tilt + uint8_t liftState = (currentStatus & LIFT) >> 2; + uint8_t tiltState = (currentStatus & TILT) >> 4; + + // Global follows Lift by priority, or Tilt if Lift is not active (Stall = 0) + uint8_t globalState = (liftState != STALL) ? liftState : tiltState; + + // Update Global field (bits 0-1) + currentStatus = (currentStatus & ~GLOBAL) | (globalState << 0); + + val.val.u8 = currentStatus; + return updateAttributeVal(WindowCovering::Id, WindowCovering::Attributes::OperationalStatus::Id, &val); + } + return true; +} + +MatterWindowCovering::OperationalState_t MatterWindowCovering::getOperationalState(OperationalStatusField_t field) { + esp_matter_attr_val_t val = esp_matter_invalid(NULL); + if (!getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::OperationalStatus::Id, &val)) { + return STALL; + } + + uint8_t operationalStatus = val.val.u8; + uint8_t fieldMask = (uint8_t)field; + uint8_t fieldShift = 0; + + // Determine shift based on field + if (field == GLOBAL) { + fieldShift = 0; // Bits 0-1 + } else if (field == LIFT) { + fieldShift = 2; // Bits 2-3 + } else if (field == TILT) { + fieldShift = 4; // Bits 4-5 + } else { + return STALL; + } + + // Extract state for this field + uint8_t fieldState = (operationalStatus & fieldMask) >> fieldShift; + return (OperationalState_t)fieldState; +} + +void MatterWindowCovering::updateAccessory() { + if (_onChangeCB != NULL) { + _onChangeCB(currentLiftPercent, currentTiltPercent); + } +} + +#endif /* CONFIG_ESP_MATTER_ENABLE_DATA_MODEL */ + diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h new file mode 100644 index 00000000000..5777617f70d --- /dev/null +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h @@ -0,0 +1,162 @@ +// 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. + +#pragma once +#include +#ifdef CONFIG_ESP_MATTER_ENABLE_DATA_MODEL + +#include +#include +#include +#include + +using namespace chip::app::Clusters; + +// Matter Window Covering endpoint with Lift and Tilt control + +class MatterWindowCovering : public MatterEndPoint { +public: + // Window Covering Type constants (from Matter spec) + enum WindowCoveringType_t { + ROLLERSHADE = (uint8_t) WindowCovering::Type::kRollerShade, // Roller Shade (LIFT support) + ROLLERSHADE_2_MOTOR = (uint8_t) WindowCovering::Type::kRollerShade2Motor, // Roller Shade 2 Motor (LIFT support) + ROLLERSHADE_EXTERIOR = (uint8_t) WindowCovering::Type::kRollerShadeExterior, // Roller Shade Exterior (LIFT support) + ROLLERSHADE_EXTERIOR_2_MOTOR = (uint8_t) WindowCovering::Type::kRollerShadeExterior2Motor, // Roller Shade Exterior 2 Motor (LIFT support) + DRAPERY = (uint8_t) WindowCovering::Type::kDrapery, // Drapery (LIFT support) + AWNING = (uint8_t) WindowCovering::Type::kAwning, // Awning (LIFT support) + SHUTTER = (uint8_t) WindowCovering::Type::kShutter, // Shutter (TILT support) + BLIND_TILT_ONLY = (uint8_t) WindowCovering::Type::kTiltBlindTiltOnly, // Blind Tilt Only (TILT support) + BLIND_LIFT_AND_TILT = (uint8_t) WindowCovering::Type::kTiltBlindLiftAndTilt, // Blind Lift and Tilt (LIFT and TILT support) + PROJECTOR_SCREEN = (uint8_t) WindowCovering::Type::kProjectorScreen, // Projector Screen (LIFT support) + }; + + // Operational State constants (from Matter spec) + enum OperationalState_t { + STALL = (uint8_t)WindowCovering::OperationalState::Stall, // Currently not moving + MOVING_UP_OR_OPEN = (uint8_t)WindowCovering::OperationalState::MovingUpOrOpen, // Is currently opening + MOVING_DOWN_OR_CLOSE = (uint8_t)WindowCovering::OperationalState::MovingDownOrClose, // Is currently closing + }; + + // Operational Status field constants (from Matter spec) + enum OperationalStatusField_t { + GLOBAL = (uint8_t) WindowCovering::OperationalStatus::kGlobal, // Global operational state field 0x03 (bits 0-1) + LIFT = (uint8_t) WindowCovering::OperationalStatus::kLift, // Lift operational state field 0x0C (bits 2-3) + TILT = (uint8_t) WindowCovering::OperationalStatus::kTilt, // Tilt operational state field 0x30 (bits 4-5) + }; + + MatterWindowCovering(); + ~MatterWindowCovering(); + virtual bool begin(uint8_t liftPercent = 100, uint8_t tiltPercent = 0, WindowCoveringType_t coveringType = ROLLERSHADE); + void end(); // this will just stop processing Matter events + + // Lift position control + bool setLiftPosition(uint16_t liftPosition); + uint16_t getLiftPosition(); + bool setLiftPercentage(uint8_t liftPercent); + uint8_t getLiftPercentage(); + bool setTargetLiftPercent100ths(uint16_t liftPercent100ths); + uint16_t getTargetLiftPercent100ths(); + + // Lift limit control + bool setInstalledOpenLimitLift(uint16_t openLimit); + uint16_t getInstalledOpenLimitLift(); + bool setInstalledClosedLimitLift(uint16_t closedLimit); + uint16_t getInstalledClosedLimitLift(); + + // Tilt position control + bool setTiltPosition(uint16_t tiltPosition); + uint16_t getTiltPosition(); + bool setTiltPercentage(uint8_t tiltPercent); + uint8_t getTiltPercentage(); + bool setTargetTiltPercent100ths(uint16_t tiltPercent100ths); + uint16_t getTargetTiltPercent100ths(); + + // Tilt limit control + bool setInstalledOpenLimitTilt(uint16_t openLimit); + uint16_t getInstalledOpenLimitTilt(); + bool setInstalledClosedLimitTilt(uint16_t closedLimit); + uint16_t getInstalledClosedLimitTilt(); + + // Window covering type + bool setCoveringType(WindowCoveringType_t coveringType); + WindowCoveringType_t getCoveringType(); + + // Operational status control (full bitmap) + bool setOperationalStatus(uint8_t operationalStatus); + uint8_t getOperationalStatus(); + + // Operational state control (individual fields) + bool setOperationalState(OperationalStatusField_t field, OperationalState_t state); + OperationalState_t getOperationalState(OperationalStatusField_t field); + + // User Callback for whenever the window covering is opened + using EndPointOpenCB = std::function; + void onOpen(EndPointOpenCB onChangeCB) { + _onOpenCB = onChangeCB; + } + + // User Callback for whenever the window covering is closed + using EndPointCloseCB = std::function; + void onClose(EndPointCloseCB onChangeCB) { + _onCloseCB = onChangeCB; + } + + // User Callback for whenever the lift percentage is changed + using EndPointLiftCB = std::function; + void onGoToLiftPercentage(EndPointLiftCB onChangeCB) { + _onGoToLiftPercentageCB = onChangeCB; + } + + // User Callback for whenever the tilt percentage is changed + using EndPointTiltCB = std::function; + void onGoToTiltPercentage(EndPointTiltCB onChangeCB) { + _onGoToTiltPercentageCB = onChangeCB; + } + + // User Callback for whenever the window covering is stopped + using EndPointStopCB = std::function; + void onStop(EndPointStopCB onChangeCB) { + _onStopCB = onChangeCB; + } + + // User Callback for whenever any parameter is changed + using EndPointCB = std::function; + void onChange(EndPointCB onChangeCB) { + _onChangeCB = onChangeCB; + } + + // used to update the state of the window covering using the current Matter internal state + // It is necessary to set a user callback function using onChange() to handle the physical window covering state + void updateAccessory(); + + // this function is called by Matter internal event processor. It could be overwritten by the application, if necessary. + bool attributeChangeCB(uint16_t endpoint_id, uint32_t cluster_id, uint32_t attribute_id, esp_matter_attr_val_t *val); + +protected: + bool started = false; + uint8_t currentLiftPercent = 0; + uint16_t currentLiftPosition = 0; + uint8_t currentTiltPercent = 0; + uint16_t currentTiltPosition = 0; + WindowCoveringType_t coveringType = ROLLERSHADE; + + EndPointOpenCB _onOpenCB = NULL; + EndPointCloseCB _onCloseCB = NULL; + EndPointLiftCB _onGoToLiftPercentageCB = NULL; + EndPointTiltCB _onGoToTiltPercentageCB = NULL; + EndPointStopCB _onStopCB = NULL; + EndPointCB _onChangeCB = NULL; +}; +#endif /* CONFIG_ESP_MATTER_ENABLE_DATA_MODEL */ + From 4e4477d37aadbf1b56b29d669a7059b3159bb053 Mon Sep 17 00:00:00 2001 From: SuGlider Date: Sun, 7 Dec 2025 00:09:26 -0300 Subject: [PATCH 02/28] feat(matter): improves the example --- .../examples/MatterWindowCovering/MatterWindowCovering.ino | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino index 142a36d4cd6..8977c8e6d3d 100644 --- a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino +++ b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino @@ -52,7 +52,7 @@ 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 = 40; // Maximum tilt absolute value +const uint16_t MAX_TILT = 90; // Maximum tilt absolute value const uint16_t MIN_TILT = 0; // Minimum tilt absolute value // Current window covering state @@ -73,12 +73,11 @@ const uint8_t ledPin = 2; // Set your pin here if your board has not defined RG void visualizeWindowBlinds(uint8_t liftPercent, uint8_t tiltPercent) { // Use RGB LED to visualize lift position (brightness) and tilt (color shift) float brightness = (float)liftPercent / 100.0; // 0.0 to 1.0 +#ifdef RGB_BUILTIN // 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; - -#ifdef RGB_BUILTIN rgbLedWrite(ledPin, red, green, blue); #else // For non-RGB boards, just use brightness From 8fce4ab9081ec88b1eca110fc5cc58fce175a07c Mon Sep 17 00:00:00 2001 From: SuGlider Date: Sun, 7 Dec 2025 00:30:25 -0300 Subject: [PATCH 03/28] feat(matter): solves not used variables --- .../examples/MatterWindowCovering/MatterWindowCovering.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino index 8977c8e6d3d..869c6b82650 100644 --- a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino +++ b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino @@ -71,9 +71,9 @@ const uint8_t ledPin = 2; // Set your pin here if your board has not defined RG #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 -#ifdef RGB_BUILTIN // 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); From 62ef5051143e43d4be12361e0d73e7548ffeb223 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Mon, 8 Dec 2025 08:21:00 -0300 Subject: [PATCH 04/28] fix(matter): code formatting Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h index 5777617f70d..9f1bebc4350 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h @@ -58,7 +58,7 @@ class MatterWindowCovering : public MatterEndPoint { MatterWindowCovering(); ~MatterWindowCovering(); virtual bool begin(uint8_t liftPercent = 100, uint8_t tiltPercent = 0, WindowCoveringType_t coveringType = ROLLERSHADE); - void end(); // this will just stop processing Matter events + void end(); // this will just stop processing Matter events. // Lift position control bool setLiftPosition(uint16_t liftPosition); From 77d3fda4b37ce59b48bad20563c97859ccf77313 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Mon, 8 Dec 2025 08:22:28 -0300 Subject: [PATCH 05/28] fix(matter): better commentary Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp index 321a8ee8268..924df90b217 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -792,7 +792,8 @@ bool MatterWindowCovering::setOperationalState(OperationalStatusField_t field, O uint8_t currentStatus = val.val.u8; uint8_t fieldMask = (uint8_t)field; - uint8_t fieldShift = (field == LIFT) ? 2 : 4; // LIFT: bits 2-3, TILT: bits 4-5 + // For clarity: LIFT uses shift 2 (bits 2-3), TILT uses shift 4 (bits 4-5) + uint8_t fieldShift = (field == LIFT) ? 2 : 4; // Extract current state for this field uint8_t currentFieldState = (currentStatus & fieldMask) >> fieldShift; From db0062af9403523b3d95aa2b42c9499fa145f19e Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Mon, 8 Dec 2025 08:23:22 -0300 Subject: [PATCH 06/28] fix(matter): better variable name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../MatterWindowCovering.ino | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino index 869c6b82650..28f458b61b3 100644 --- a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino +++ b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino @@ -331,18 +331,18 @@ void loop() { if (digitalRead(buttonPin) == HIGH && button_state && time_diff > debounceTime) { // Button is released - cycle lift percentage by 20% button_state = false; // released - uint8_t myTargetLiftPercent = currentLiftPercent; + uint8_t targetLiftPercent = currentLiftPercent; // go to the closest next 20% or move 20% more - if ((myTargetLiftPercent % 20) != 0) { - myTargetLiftPercent = ((myTargetLiftPercent / 20) + 1) * 20; + if ((targetLiftPercent % 20) != 0) { + targetLiftPercent = ((targetLiftPercent / 20) + 1) * 20; } else { - myTargetLiftPercent += 20; + targetLiftPercent += 20; } - if (myTargetLiftPercent > 100) { - myTargetLiftPercent = 0; + if (targetLiftPercent > 100) { + targetLiftPercent = 0; } - Serial.printf("User button released. Setting lift to %d%%\r\n", myTargetLiftPercent); - WindowBlinds.setTargetLiftPercent100ths(myTargetLiftPercent * 100); + 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 From b83cff77daee8dce84a4397bcdcd720e8f4a5a7c Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Mon, 8 Dec 2025 08:25:01 -0300 Subject: [PATCH 07/28] fix(matter): better commentary Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp index 924df90b217..89dbf3175d9 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -152,7 +152,7 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus if (endpoint_id == getEndPointId() && cluster_id == WindowCovering::Id) { switch (attribute_id) { - // Current position attributes (read-only, updated by device) + // Current position attributes (read-only to external Matter controllers; updated internally by device) case WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id: { uint16_t liftPercent100ths = val->val.u16; uint8_t liftPercent = (uint8_t)(liftPercent100ths / 100); From a6054c49a4fc85c089622a3dd46c31b08f2284d0 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Mon, 8 Dec 2025 08:26:12 -0300 Subject: [PATCH 08/28] fix(matter): fix commentary Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp index 89dbf3175d9..ca59cdf09ec 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -205,7 +205,7 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus uint8_t targetLiftPercent = (uint8_t)(targetLiftPercent100ths / 100); log_d("Window Covering Target Lift Percentage changed to %d%%", targetLiftPercent); // Call callback to trigger movement - do NOT update currentLiftPercent here - // CurrentPosition will be updated by the application when the device actually moves + // `CurrentPosition` will be updated by the application when the device actually moves if (_onGoToLiftPercentageCB != NULL) { ret &= _onGoToLiftPercentageCB(targetLiftPercent); } From 795a7f9f967adb50c53fc0d40d8800d7b2835136 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:02:32 +0000 Subject: [PATCH 09/28] ci(pre-commit): Apply automatic fixes --- docs/en/matter/ep_window_covering.rst | 1 - .../MatterWindowCovering.ino | 76 ++++++++-------- .../examples/MatterWindowCovering/README.md | 1 - .../examples/MatterWindowCovering/ci.yml | 2 +- .../MatterEndpoints/MatterWindowCovering.cpp | 91 +++++++------------ .../MatterEndpoints/MatterWindowCovering.h | 33 ++++--- 6 files changed, 86 insertions(+), 118 deletions(-) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst index a7414286f90..becac299917 100644 --- a/docs/en/matter/ep_window_covering.rst +++ b/docs/en/matter/ep_window_covering.rst @@ -558,4 +558,3 @@ Window Covering .. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino :language: arduino - diff --git a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino index 28f458b61b3..c63915a7fdf 100644 --- a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino +++ b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino @@ -42,22 +42,22 @@ 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 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) +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 +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 +uint16_t currentLift = 0; // Lift position in cm uint8_t currentLiftPercent = 100; uint8_t currentTiltPercent = 0; // Tilt rotation percentage (0-100%) @@ -94,16 +94,16 @@ bool fullOpen() { 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; } @@ -114,16 +114,16 @@ bool fullClose() { 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; } @@ -143,7 +143,7 @@ bool goToLiftPercentage(uint8_t liftPercent) { // 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; @@ -152,16 +152,16 @@ bool goToLiftPercentage(uint8_t liftPercent) { } 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; } @@ -180,32 +180,32 @@ bool goToTiltPercentage(uint8_t 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; } @@ -242,16 +242,16 @@ void setup() { 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(); @@ -261,25 +261,25 @@ void setup() { } 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); - + + 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); + Serial.printf("Window Covering changed: Lift=%d%%, Tilt=%d%%\r\n", liftPercent, tiltPercent); visualizeWindowBlinds(liftPercent, tiltPercent); return true; }); diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md index 0a1e411154b..492d9f0e6f7 100644 --- a/libraries/Matter/examples/MatterWindowCovering/README.md +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -225,4 +225,3 @@ The MatterWindowCovering example consists of the following main components: ## License This example is licensed under the Apache License, Version 2.0. - diff --git a/libraries/Matter/examples/MatterWindowCovering/ci.yml b/libraries/Matter/examples/MatterWindowCovering/ci.yml index 09074596de8..050a80ff543 100644 --- a/libraries/Matter/examples/MatterWindowCovering/ci.yml +++ b/libraries/Matter/examples/MatterWindowCovering/ci.yml @@ -1,4 +1,4 @@ fqbn_append: PartitionScheme=huge_app requires: - - CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y \ No newline at end of file + - CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp index ca59cdf09ec..54ae83c456d 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -70,7 +70,7 @@ bool MatterWindowCovering::begin(uint8_t liftPercent, uint8_t tiltPercent, Windo if (feature::lift::add(window_covering_cluster, &lift_config) != ESP_OK) { log_e("Failed to add Lift feature"); } - + // Add Position Aware Lift feature feature::position_aware_lift::config_t position_aware_lift_config; position_aware_lift_config.current_position_lift_percentage = nullable(0); @@ -79,7 +79,7 @@ bool MatterWindowCovering::begin(uint8_t liftPercent, uint8_t tiltPercent, Windo if (feature::position_aware_lift::add(window_covering_cluster, &position_aware_lift_config) != ESP_OK) { log_e("Failed to add Position Aware Lift feature"); } - + // Add Tilt feature if the covering type supports it bool supportsTilt = (coveringType == SHUTTER || coveringType == BLIND_TILT_ONLY || coveringType == BLIND_LIFT_AND_TILT); if (supportsTilt) { @@ -88,7 +88,7 @@ bool MatterWindowCovering::begin(uint8_t liftPercent, uint8_t tiltPercent, Windo if (feature::tilt::add(window_covering_cluster, &tilt_config) != ESP_OK) { log_e("Failed to add Tilt feature"); } - + // Add Position Aware Tilt feature feature::position_aware_tilt::config_t position_aware_tilt_config; position_aware_tilt_config.current_position_tilt_percentage = nullable(0); @@ -98,7 +98,7 @@ bool MatterWindowCovering::begin(uint8_t liftPercent, uint8_t tiltPercent, Windo log_e("Failed to add Position Aware Tilt feature"); } } - + // Add Absolute Position feature (creates InstalledOpenLimitLift/ClosedLimitLift/Tilt attributes) // Must be added AFTER all lift and tilt features for all attributes to be created feature::absolute_position::config_t absolute_position_config; @@ -109,7 +109,7 @@ bool MatterWindowCovering::begin(uint8_t liftPercent, uint8_t tiltPercent, Windo if (feature::absolute_position::add(window_covering_cluster, &absolute_position_config) != ESP_OK) { log_e("Failed to add Absolute Position feature"); } - + // Create Window Covering commands create_up_or_open(window_covering_cluster); create_down_or_close(window_covering_cluster); @@ -153,7 +153,8 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus if (endpoint_id == getEndPointId() && cluster_id == WindowCovering::Id) { switch (attribute_id) { // Current position attributes (read-only to external Matter controllers; updated internally by device) - case WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id: { + case WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id: + { uint16_t liftPercent100ths = val->val.u16; uint8_t liftPercent = (uint8_t)(liftPercent100ths / 100); log_d("Window Covering Lift Percentage changed to %d%%", liftPercent); @@ -167,7 +168,8 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus } break; } - case WindowCovering::Attributes::CurrentPositionTiltPercent100ths::Id: { + case WindowCovering::Attributes::CurrentPositionTiltPercent100ths::Id: + { uint16_t tiltPercent100ths = val->val.u16; uint8_t tiltPercent = (uint8_t)(tiltPercent100ths / 100); log_d("Window Covering Tilt Percentage changed to %d%%", tiltPercent); @@ -189,17 +191,14 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus log_d("Window Covering Tilt Position changed to %d", val->val.u16); currentTiltPosition = val->val.u16; break; - case WindowCovering::Attributes::CurrentPositionLiftPercentage::Id: - log_d("Window Covering Lift Percentage (legacy) changed to %d%%", val->val.u8); - break; - case WindowCovering::Attributes::CurrentPositionTiltPercentage::Id: - log_d("Window Covering Tilt Percentage (legacy) changed to %d%%", val->val.u8); - break; + case WindowCovering::Attributes::CurrentPositionLiftPercentage::Id: log_d("Window Covering Lift Percentage (legacy) changed to %d%%", val->val.u8); break; + case WindowCovering::Attributes::CurrentPositionTiltPercentage::Id: log_d("Window Covering Tilt Percentage (legacy) changed to %d%%", val->val.u8); break; // Target position attributes (writable, trigger movement) // Note: TargetPosition is where the device SHOULD go, not where it is. // CurrentPosition should only be updated when the physical device actually moves. - case WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id: { + case WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id: + { if (!chip::app::NumericAttributeTraits::IsNullValue(val->val.u16)) { uint16_t targetLiftPercent100ths = val->val.u16; uint8_t targetLiftPercent = (uint8_t)(targetLiftPercent100ths / 100); @@ -214,7 +213,8 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus } break; } - case WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id: { + case WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id: + { if (!chip::app::NumericAttributeTraits::IsNullValue(val->val.u16)) { uint16_t targetTiltPercent100ths = val->val.u16; uint8_t targetTiltPercent = (uint8_t)(targetTiltPercent100ths / 100); @@ -235,55 +235,27 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus log_d("Window Covering Type changed to %d", val->val.u8); coveringType = (WindowCoveringType_t)val->val.u8; break; - case WindowCovering::Attributes::EndProductType::Id: - log_d("Window Covering End Product Type changed to %d", val->val.u8); - break; - case WindowCovering::Attributes::ConfigStatus::Id: - log_d("Window Covering Config Status changed to 0x%02X", val->val.u8); - break; - case WindowCovering::Attributes::Mode::Id: - log_d("Window Covering Mode changed to 0x%02X", val->val.u8); - break; + case WindowCovering::Attributes::EndProductType::Id: log_d("Window Covering End Product Type changed to %d", val->val.u8); break; + case WindowCovering::Attributes::ConfigStatus::Id: log_d("Window Covering Config Status changed to 0x%02X", val->val.u8); break; + case WindowCovering::Attributes::Mode::Id: log_d("Window Covering Mode changed to 0x%02X", val->val.u8); break; // Operational status attributes - case WindowCovering::Attributes::OperationalStatus::Id: - log_d("Window Covering Operational Status changed to 0x%02X", val->val.u8); - break; - case WindowCovering::Attributes::SafetyStatus::Id: - log_d("Window Covering Safety Status changed to 0x%04X", val->val.u16); - break; + case WindowCovering::Attributes::OperationalStatus::Id: log_d("Window Covering Operational Status changed to 0x%02X", val->val.u8); break; + case WindowCovering::Attributes::SafetyStatus::Id: log_d("Window Covering Safety Status changed to 0x%04X", val->val.u16); break; // Limit attributes - case WindowCovering::Attributes::PhysicalClosedLimitLift::Id: - log_d("Window Covering Physical Closed Limit Lift changed to %d", val->val.u16); - break; - case WindowCovering::Attributes::PhysicalClosedLimitTilt::Id: - log_d("Window Covering Physical Closed Limit Tilt changed to %d", val->val.u16); - break; - case WindowCovering::Attributes::InstalledOpenLimitLift::Id: - log_d("Window Covering Installed Open Limit Lift changed to %d", val->val.u16); - break; - case WindowCovering::Attributes::InstalledClosedLimitLift::Id: - log_d("Window Covering Installed Closed Limit Lift changed to %d", val->val.u16); - break; - case WindowCovering::Attributes::InstalledOpenLimitTilt::Id: - log_d("Window Covering Installed Open Limit Tilt changed to %d", val->val.u16); - break; - case WindowCovering::Attributes::InstalledClosedLimitTilt::Id: - log_d("Window Covering Installed Closed Limit Tilt changed to %d", val->val.u16); - break; + case WindowCovering::Attributes::PhysicalClosedLimitLift::Id: log_d("Window Covering Physical Closed Limit Lift changed to %d", val->val.u16); break; + case WindowCovering::Attributes::PhysicalClosedLimitTilt::Id: log_d("Window Covering Physical Closed Limit Tilt changed to %d", val->val.u16); break; + case WindowCovering::Attributes::InstalledOpenLimitLift::Id: log_d("Window Covering Installed Open Limit Lift changed to %d", val->val.u16); break; + case WindowCovering::Attributes::InstalledClosedLimitLift::Id: log_d("Window Covering Installed Closed Limit Lift changed to %d", val->val.u16); break; + case WindowCovering::Attributes::InstalledOpenLimitTilt::Id: log_d("Window Covering Installed Open Limit Tilt changed to %d", val->val.u16); break; + case WindowCovering::Attributes::InstalledClosedLimitTilt::Id: log_d("Window Covering Installed Closed Limit Tilt changed to %d", val->val.u16); break; // Actuation count attributes - case WindowCovering::Attributes::NumberOfActuationsLift::Id: - log_d("Window Covering Number of Actuations Lift changed to %d", val->val.u16); - break; - case WindowCovering::Attributes::NumberOfActuationsTilt::Id: - log_d("Window Covering Number of Actuations Tilt changed to %d", val->val.u16); - break; + case WindowCovering::Attributes::NumberOfActuationsLift::Id: log_d("Window Covering Number of Actuations Lift changed to %d", val->val.u16); break; + case WindowCovering::Attributes::NumberOfActuationsTilt::Id: log_d("Window Covering Number of Actuations Tilt changed to %d", val->val.u16); break; - default: - log_d("Window Covering Unknown attribute %u changed", attribute_id); - break; + default: log_d("Window Covering Unknown attribute %u changed", attribute_id); break; } } return ret; @@ -809,10 +781,10 @@ bool MatterWindowCovering::setOperationalState(OperationalStatusField_t field, O // 3. Recalculate Global based on priority: Lift (if not Stall) > Tilt uint8_t liftState = (currentStatus & LIFT) >> 2; uint8_t tiltState = (currentStatus & TILT) >> 4; - + // Global follows Lift by priority, or Tilt if Lift is not active (Stall = 0) uint8_t globalState = (liftState != STALL) ? liftState : tiltState; - + // Update Global field (bits 0-1) currentStatus = (currentStatus & ~GLOBAL) | (globalState << 0); @@ -855,4 +827,3 @@ void MatterWindowCovering::updateAccessory() { } #endif /* CONFIG_ESP_MATTER_ENABLE_DATA_MODEL */ - diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h index 9f1bebc4350..740729df43a 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h @@ -29,30 +29,30 @@ class MatterWindowCovering : public MatterEndPoint { public: // Window Covering Type constants (from Matter spec) enum WindowCoveringType_t { - ROLLERSHADE = (uint8_t) WindowCovering::Type::kRollerShade, // Roller Shade (LIFT support) - ROLLERSHADE_2_MOTOR = (uint8_t) WindowCovering::Type::kRollerShade2Motor, // Roller Shade 2 Motor (LIFT support) - ROLLERSHADE_EXTERIOR = (uint8_t) WindowCovering::Type::kRollerShadeExterior, // Roller Shade Exterior (LIFT support) - ROLLERSHADE_EXTERIOR_2_MOTOR = (uint8_t) WindowCovering::Type::kRollerShadeExterior2Motor, // Roller Shade Exterior 2 Motor (LIFT support) - DRAPERY = (uint8_t) WindowCovering::Type::kDrapery, // Drapery (LIFT support) - AWNING = (uint8_t) WindowCovering::Type::kAwning, // Awning (LIFT support) - SHUTTER = (uint8_t) WindowCovering::Type::kShutter, // Shutter (TILT support) - BLIND_TILT_ONLY = (uint8_t) WindowCovering::Type::kTiltBlindTiltOnly, // Blind Tilt Only (TILT support) - BLIND_LIFT_AND_TILT = (uint8_t) WindowCovering::Type::kTiltBlindLiftAndTilt, // Blind Lift and Tilt (LIFT and TILT support) - PROJECTOR_SCREEN = (uint8_t) WindowCovering::Type::kProjectorScreen, // Projector Screen (LIFT support) + ROLLERSHADE = (uint8_t)WindowCovering::Type::kRollerShade, // Roller Shade (LIFT support) + ROLLERSHADE_2_MOTOR = (uint8_t)WindowCovering::Type::kRollerShade2Motor, // Roller Shade 2 Motor (LIFT support) + ROLLERSHADE_EXTERIOR = (uint8_t)WindowCovering::Type::kRollerShadeExterior, // Roller Shade Exterior (LIFT support) + ROLLERSHADE_EXTERIOR_2_MOTOR = (uint8_t)WindowCovering::Type::kRollerShadeExterior2Motor, // Roller Shade Exterior 2 Motor (LIFT support) + DRAPERY = (uint8_t)WindowCovering::Type::kDrapery, // Drapery (LIFT support) + AWNING = (uint8_t)WindowCovering::Type::kAwning, // Awning (LIFT support) + SHUTTER = (uint8_t)WindowCovering::Type::kShutter, // Shutter (TILT support) + BLIND_TILT_ONLY = (uint8_t)WindowCovering::Type::kTiltBlindTiltOnly, // Blind Tilt Only (TILT support) + BLIND_LIFT_AND_TILT = (uint8_t)WindowCovering::Type::kTiltBlindLiftAndTilt, // Blind Lift and Tilt (LIFT and TILT support) + PROJECTOR_SCREEN = (uint8_t)WindowCovering::Type::kProjectorScreen, // Projector Screen (LIFT support) }; // Operational State constants (from Matter spec) enum OperationalState_t { - STALL = (uint8_t)WindowCovering::OperationalState::Stall, // Currently not moving - MOVING_UP_OR_OPEN = (uint8_t)WindowCovering::OperationalState::MovingUpOrOpen, // Is currently opening - MOVING_DOWN_OR_CLOSE = (uint8_t)WindowCovering::OperationalState::MovingDownOrClose, // Is currently closing + STALL = (uint8_t)WindowCovering::OperationalState::Stall, // Currently not moving + MOVING_UP_OR_OPEN = (uint8_t)WindowCovering::OperationalState::MovingUpOrOpen, // Is currently opening + MOVING_DOWN_OR_CLOSE = (uint8_t)WindowCovering::OperationalState::MovingDownOrClose, // Is currently closing }; // Operational Status field constants (from Matter spec) enum OperationalStatusField_t { - GLOBAL = (uint8_t) WindowCovering::OperationalStatus::kGlobal, // Global operational state field 0x03 (bits 0-1) - LIFT = (uint8_t) WindowCovering::OperationalStatus::kLift, // Lift operational state field 0x0C (bits 2-3) - TILT = (uint8_t) WindowCovering::OperationalStatus::kTilt, // Tilt operational state field 0x30 (bits 4-5) + GLOBAL = (uint8_t)WindowCovering::OperationalStatus::kGlobal, // Global operational state field 0x03 (bits 0-1) + LIFT = (uint8_t)WindowCovering::OperationalStatus::kLift, // Lift operational state field 0x0C (bits 2-3) + TILT = (uint8_t)WindowCovering::OperationalStatus::kTilt, // Tilt operational state field 0x30 (bits 4-5) }; MatterWindowCovering(); @@ -159,4 +159,3 @@ class MatterWindowCovering : public MatterEndPoint { EndPointCB _onChangeCB = NULL; }; #endif /* CONFIG_ESP_MATTER_ENABLE_DATA_MODEL */ - From c75a2163f0d76b7ce4280fbbbdc0c5cfb077d77f Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 12:10:13 -0300 Subject: [PATCH 10/28] fix(matter): vale CI check --- libraries/Matter/examples/MatterWindowCovering/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md index 492d9f0e6f7..7a14cbbc9e7 100644 --- a/libraries/Matter/examples/MatterWindowCovering/README.md +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -24,8 +24,8 @@ This example demonstrates how to create a Matter-compatible window covering devi - Matter protocol implementation for a window covering device - Support for both Wi-Fi and Thread(*) connectivity -- Lift position and percentage control (0-100%) - Lift represents the physical position in centimeters -- Tilt rotation and percentage control (0-100%) - Tilt represents rotation of the shade, not a linear measurement +- Lift position and percentage control — Lift can be specified either as a physical position (in centimeters) or as a percentage (0–100%). +- Tilt rotation and percentage control — Lift can be specified either as a physical position (in centimeters) or as a percentage (0–100%). - Multiple window covering types support - State persistence using `Preferences` library - Button control for manual lift adjustment and factory reset From bb1d060f5ef1dccf6a70ca6e5d7ee50f988bedfb Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 12:16:09 -0300 Subject: [PATCH 11/28] fix(matter_doc): CI Vale check --- docs/en/matter/ep_window_covering.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst index becac299917..1fe108936c2 100644 --- a/docs/en/matter/ep_window_covering.rst +++ b/docs/en/matter/ep_window_covering.rst @@ -217,13 +217,13 @@ Installed Limit Control setInstalledOpenLimitLift ^^^^^^^^^^^^^^^^^^^^^^^^^ -Sets the installed open limit for lift (in centimeters). This defines the physical position when the window covering is fully open. +Sets the installed open limit for lift in centimeters. This defines the physical position when the window covering is fully open. .. code-block:: arduino bool setInstalledOpenLimitLift(uint16_t openLimit); -* ``openLimit`` - Open limit position in centimeters +* ``openLimit`` - Open limit position (centimeters) This function will return ``true`` if successful, ``false`` otherwise. @@ -236,18 +236,18 @@ Gets the installed open limit for lift. uint16_t getInstalledOpenLimitLift(); -This function will return the installed open limit for lift in centimeters. +This function will return the installed open limit for lift (centimeters). setInstalledClosedLimitLift ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Sets the installed closed limit for lift (in centimeters). This defines the physical position when the window covering is fully closed. +Sets the installed closed limit for lift in centimeters. This defines the physical position when the window covering is fully closed. .. code-block:: arduino bool setInstalledClosedLimitLift(uint16_t closedLimit); -* ``closedLimit`` - Closed limit position in centimeters +* ``closedLimit`` - Closed limit position (centimeters) This function will return ``true`` if successful, ``false`` otherwise. @@ -260,7 +260,7 @@ Gets the installed closed limit for lift. uint16_t getInstalledClosedLimitLift(); -This function will return the installed closed limit for lift in centimeters. +This function will return the installed closed limit for lift (centimeters). setInstalledOpenLimitTilt ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -558,3 +558,4 @@ Window Covering .. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino :language: arduino + From b6132b448c3e30ab0b910b1f497f00b5cdc75bcd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:17:05 +0000 Subject: [PATCH 12/28] ci(pre-commit): Apply automatic fixes --- docs/en/matter/ep_window_covering.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst index 1fe108936c2..0f6c4387601 100644 --- a/docs/en/matter/ep_window_covering.rst +++ b/docs/en/matter/ep_window_covering.rst @@ -558,4 +558,3 @@ Window Covering .. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino :language: arduino - From db1472dc881f46a6a3f6ba5d82a908692b1c3fe1 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 12:17:20 -0300 Subject: [PATCH 13/28] fix(matter_docs): CI Vale check Updated README.md for MatterWindowCovering example to clarify ESP32-C6 and ESP32-C5 Thread support and improved descriptions for lift and tilt controls. --- .../examples/MatterWindowCovering/README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md index 7a14cbbc9e7..45cb8718617 100644 --- a/libraries/Matter/examples/MatterWindowCovering/README.md +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -17,20 +17,20 @@ This example demonstrates how to create a Matter-compatible window covering devi ### Note on Commissioning: - **ESP32 & ESP32-S2** do not support commissioning over Bluetooth LE. For these chips, you must provide Wi-Fi credentials directly in the sketch code so they can connect to your network manually. -- **ESP32-C6** Although it has Thread support, the ESP32 Arduino Matter Library has been pre compiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. -- **ESP32-C5** Although it has Thread support, the ESP32 Arduino Matter Library has been pre compiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. +- **ESP32-C6** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. +- **ESP32-C5** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. ## Features - Matter protocol implementation for a window covering device - Support for both Wi-Fi and Thread(*) connectivity -- Lift position and percentage control — Lift can be specified either as a physical position (in centimeters) or as a percentage (0–100%). -- Tilt rotation and percentage control — Lift can be specified either as a physical position (in centimeters) or as a percentage (0–100%). +- Lift position and percentage control (0-100%) - Lift represents the physical position (centimeters) +- Tilt rotation and percentage control (0-100%) - Tilt represents rotation of the shade, not a linear measurement - Multiple window covering types support - State persistence using `Preferences` library - Button control for manual lift adjustment and factory reset - RGB LED visualization of lift (brightness) and tilt (color) positions -- Installed limit configuration for lift (in cm) and tilt (absolute values) +- Installed limit configuration for lift (cm) and tilt (absolute values) - Matter commissioning via QR code or manual pairing code - Integration with Apple HomeKit, Amazon Alexa, and Google Home (*) It is necessary to compile the project using Arduino as IDF Component. @@ -146,7 +146,7 @@ For production use with a motorized window covering: 2. **Position Feedback**: - Use encoders or limit switches to provide position feedback - - For lift: Update `currentLift` (in cm) based on actual motor position + - For lift: Update `currentLift` (cm) based on actual motor position - For tilt: Update `currentTiltPercent` (rotation percentage) based on actual motor rotation - Call `setLiftPercentage()` and `setTiltPercentage()` to update `CurrentPosition` attributes (these methods update the device's actual position, not the target) - Call `setOperationalState(LIFT, STALL)` or `setOperationalState(TILT, STALL)` when movement is complete to indicate the device has reached the target position @@ -200,7 +200,7 @@ The MatterWindowCovering example consists of the following main components: 3. **Callbacks**: - `fullOpen()`: Handles open command - moves window covering to fully open (100% lift), updates `CurrentPosition`, and sets operational state to `STALL` - `fullClose()`: Handles close command - moves window covering to fully closed (0% lift), updates `CurrentPosition`, and sets operational state to `STALL` - - `goToLiftPercentage()`: Handles lift percentage changes (0-100%), calculates absolute position in cm based on installed limits, updates `CurrentPosition`, and sets operational state to `STALL` when movement is complete + - `goToLiftPercentage()`: Handles lift percentage changes (0-100%), calculates absolute position (cm) based on installed limits, updates `CurrentPosition`, and sets operational state to `STALL` when movement is complete - `goToTiltPercentage()`: Handles tilt rotation percentage changes (0-100%), updates `CurrentPosition`, and sets operational state to `STALL` when movement is complete - `stopMotor()`: Handles stop command - stops any ongoing movement, updates `CurrentPosition` for both lift and tilt, and sets operational state to `STALL` for both - `visualizeWindowBlinds()`: Updates RGB LED to reflect current lift and tilt positions @@ -225,3 +225,4 @@ The MatterWindowCovering example consists of the following main components: ## License This example is licensed under the Apache License, Version 2.0. + From 2d4ad337f3be2c24e8b324b5f044b6d0a58f6daa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:18:17 +0000 Subject: [PATCH 14/28] ci(pre-commit): Apply automatic fixes --- libraries/Matter/examples/MatterWindowCovering/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md index 45cb8718617..7f9726330f7 100644 --- a/libraries/Matter/examples/MatterWindowCovering/README.md +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -225,4 +225,3 @@ The MatterWindowCovering example consists of the following main components: ## License This example is licensed under the Apache License, Version 2.0. - From c8bb4ee995fbe16fce3420f51ac1996a49adbda4 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 12:26:24 -0300 Subject: [PATCH 15/28] fix(matter_docs): CI Vale check --- docs/en/matter/ep_window_covering.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst index 0f6c4387601..c531c7b3350 100644 --- a/docs/en/matter/ep_window_covering.rst +++ b/docs/en/matter/ep_window_covering.rst @@ -217,7 +217,7 @@ Installed Limit Control setInstalledOpenLimitLift ^^^^^^^^^^^^^^^^^^^^^^^^^ -Sets the installed open limit for lift in centimeters. This defines the physical position when the window covering is fully open. +Sets the installed open limit for lift (centimeters). This defines the physical position when the window covering is fully open. .. code-block:: arduino @@ -241,7 +241,7 @@ This function will return the installed open limit for lift (centimeters). setInstalledClosedLimitLift ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Sets the installed closed limit for lift in centimeters. This defines the physical position when the window covering is fully closed. +Sets the installed closed limit for lift (centimeters). This defines the physical position when the window covering is fully closed. .. code-block:: arduino @@ -558,3 +558,4 @@ Window Covering .. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino :language: arduino + From c9dc7c0ad3ac434115ad17d9adc34ff322a827ff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:27:05 +0000 Subject: [PATCH 16/28] ci(pre-commit): Apply automatic fixes --- docs/en/matter/ep_window_covering.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst index c531c7b3350..60b34fb8b17 100644 --- a/docs/en/matter/ep_window_covering.rst +++ b/docs/en/matter/ep_window_covering.rst @@ -558,4 +558,3 @@ Window Covering .. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino :language: arduino - From 916878453a184b067d825beec759e6a7f51ab8c0 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 15:09:16 -0300 Subject: [PATCH 17/28] feat(matter): adds a simple Window Blind example This example is the simplest possible in order to demonstrate how to control a Window blind. --- .../MatterSimpleWidowsBlind.ino | 92 ++++++++++++ .../MatterSimpleWidowsBlind/README.md | 136 ++++++++++++++++++ .../examples/MatterSimpleWidowsBlind/ci.yml | 4 + 3 files changed, 232 insertions(+) create mode 100644 libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino create mode 100644 libraries/Matter/examples/MatterSimpleWidowsBlind/README.md create mode 100644 libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino new file mode 100644 index 00000000000..40b1d2d4269 --- /dev/null +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino @@ -0,0 +1,92 @@ +// 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 Simple Window Blinds Example +// This is a minimal example that only controls Lift percentage using a single onGoToLiftPercentage() callback + +#include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space +#include +#endif + +// 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 + +// Simple callback - handles window Lift change request +bool onBlindLift(uint8_t liftPercent) { + // This example only uses lift + Serial.printf("Window Covering change request: Lift=%d%%\r\n", liftPercent); + + // Returning true will store the new Lift value into the Matter Cluster + return true; + } + + void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("\n========================================"); + Serial.println("Matter Simple Window Blinds Example"); + Serial.println("========================================\n"); + + // 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); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(""); + Serial.println("WiFi connected"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + #endif + + // Initialize Window Covering endpoint + // Using ROLLERSHADE type (lift only, no tilt) + WindowBlinds.begin(100, 0, MatterWindowCovering::ROLLERSHADE); + + // Set up the onGoToLiftPercentage callback - this handles all window covering changes requested by the Matter Controller + WindowBlinds.onGoToLiftPercentage(onBlindLift); + + // Start Matter + Matter.begin(); + Serial.println("Matter started"); + Serial.println(); + + // Print commissioning information + 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()); + Serial.println("========================================"); + } + + void loop() { + delay(100); + } diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md b/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md new file mode 100644 index 00000000000..0e0053da1a2 --- /dev/null +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md @@ -0,0 +1,136 @@ +# Matter Simple Window Blinds Example + +This is a minimal example demonstrating how to create a Matter-compatible window covering device with lift control only. This example uses a single `onGoToLiftPercentage()` callback to handle all window covering lift changes, making it ideal for simple implementations. + +## Supported Targets + +| SoC | Wi-Fi | Thread | BLE Commissioning | Status | +| --- | ---- | ------ | ----------------- | ------ | +| ESP32 | ✅ | ❌ | ❌ | Fully supported | +| ESP32-S2 | ✅ | ❌ | ❌ | Fully supported | +| ESP32-S3 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C3 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C5 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C6 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-H2 | ❌ | ✅ | ✅ | Supported (Thread only) | + +### Note on Commissioning: + +- **ESP32 & ESP32-S2** do not support commissioning over Bluetooth LE. For these chips, you must provide Wi-Fi credentials directly in the sketch code so they can connect to your network manually. +- **ESP32-C6** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. +- **ESP32-C5** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. + +## Features + +- Matter protocol implementation for a window covering device +- **Lift control only** (0-100%) - simplified implementation +- **Single `onGoToLiftPercentage()` callback** - handles all window covering lift changes when `TargetPositionLiftPercent100ths` changes +- Matter commissioning via QR code or manual pairing code +- Integration with Apple HomeKit, Amazon Alexa, and Google Home + +## Hardware Requirements + +- ESP32 compatible development board (see supported targets table) +- Window covering motor/actuator (optional for testing - example simulates movement) + +## Software Setup + +### Prerequisites + +1. Install the Arduino IDE (2.0 or newer recommended) +2. Install ESP32 Arduino Core with Matter support +3. ESP32 Arduino libraries: + - `Matter` + - `Wi-Fi` (only for ESP32 and ESP32-S2) + +### Configuration + +Before uploading the sketch, configure the following: + +1. **Wi-Fi Credentials** (for ESP32 and ESP32-S2 only): + ```cpp + const char *ssid = "your-ssid"; + const char *password = "your-password"; + ``` + +## Expected Output + +``` +======================================== +Matter Simple Window Blinds Example +======================================== + +Connecting to your-ssid +WiFi connected +IP address: 192.168.1.100 +Matter started + +======================================== +Matter Node is not commissioned yet. +Initiate the device discovery in your Matter environment. +Commission it to your Matter hub with the manual pairing code or QR code +Manual pairing code: 34970112332 +QR code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT:Y.K9042C00KA0648G00 +======================================== +``` + +When a command is received from the Matter controller: +``` +Window Covering change request: Lift=50% +``` + +## Usage + +1. **Commissioning**: Use the QR code or manual pairing code to commission the device to your Matter hub (Apple Home, Google Home, or Amazon Alexa). + +2. **Control**: Once commissioned, you can control the window covering lift percentage (0-100%) from your smart home app. The `onGoToLiftPercentage()` callback will be triggered whenever the target lift percentage changes. + +## Code Structure + +- **`onBlindLift()`**: Callback function that handles window covering lift changes. This is registered with `WindowBlinds.onGoToLiftPercentage()` and is triggered when `TargetPositionLiftPercent100ths` changes. The callback receives the target lift percentage (0-100%). +- **`setup()`**: Initializes WiFi (if needed), Window Covering endpoint with `ROLLERSHADE` type, registers the callback, and starts Matter. +- **`loop()`**: Empty - all control is handled via Matter callbacks. + +## Customization + +### Adding Motor Control + +In the `onBlindLift()` callback, replace the simulation code with actual motor control: + +```cpp +bool onBlindLift(uint8_t liftPercent) { + Serial.printf("Moving window covering to %d%%\r\n", liftPercent); + + // Here you would control your actual motor/actuator + // For example: + // - Calculate target position based on liftPercent and installed limits (if configured) + // - Move motor to target position + // - When movement is complete, update current position: + // WindowBlinds.setLiftPercentage(finalLiftPercent); + // WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL); + + // For this minimal example, we just return true to accept the command + return true; // Indicate command was accepted +} +``` + +## Troubleshooting + +1. **Device not discoverable**: Ensure WiFi is connected (for ESP32/ESP32-S2) or BLE is enabled (for other chips). + +2. **Lift percentage not updating**: Check that `onGoToLiftPercentage()` callback is properly registered and that `setLiftPercentage()` is called when movement is complete to update the `CurrentPosition` attribute. + +3. **Commands not working**: Ensure the callback returns `true` to accept the command. If it returns `false`, the command will be rejected. + +4. **Motor not responding**: Replace the simulation code in `onBlindLift()` with your actual motor control implementation. Remember to update `CurrentPosition` and set `OperationalState` to `STALL` when movement is complete. + +## Notes + +- This example uses `ROLLERSHADE` window covering type (lift only, no tilt). +- The example accepts commands but doesn't actually move a motor. In a real implementation, you should: + 1. Move the motor to the target position in the callback + 2. Update `CurrentPositionLiftPercent100ths` using `setLiftPercentage()` when movement is complete + 3. Set `OperationalState` to `STALL` using `setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL)` to indicate the device has reached the target position +- **Important**: `onGoToLiftPercentage()` is called when `TargetPositionLiftPercent100ths` changes. This happens when commands are executed or when a Matter controller writes directly to the target position attribute. +- Commands modify `TargetPosition`, not `CurrentPosition`. The application is responsible for updating `CurrentPosition` when the physical device actually moves. + diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml b/libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml new file mode 100644 index 00000000000..166b1b67930 --- /dev/null +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml @@ -0,0 +1,4 @@ +fqbn_append: PartitionScheme=huge_app + +requires: + - CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y From 7304cea89bc8a380cdafe9da49661df567c9fe27 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:10:05 +0000 Subject: [PATCH 18/28] ci(pre-commit): Apply automatic fixes --- .../MatterSimpleWidowsBlind.ino | 184 ++++++------ .../MatterSimpleWidowsBlind/README.md | 271 +++++++++--------- .../examples/MatterSimpleWidowsBlind/ci.yml | 8 +- 3 files changed, 231 insertions(+), 232 deletions(-) diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino index 40b1d2d4269..51c4192d7f2 100644 --- a/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino @@ -1,92 +1,92 @@ -// 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 Simple Window Blinds Example -// This is a minimal example that only controls Lift percentage using a single onGoToLiftPercentage() callback - -#include -#if !CONFIG_ENABLE_CHIPOBLE -// if the device can be commissioned using BLE, WiFi is not used - save flash space -#include -#endif - -// 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 - -// Simple callback - handles window Lift change request -bool onBlindLift(uint8_t liftPercent) { - // This example only uses lift - Serial.printf("Window Covering change request: Lift=%d%%\r\n", liftPercent); - - // Returning true will store the new Lift value into the Matter Cluster - return true; - } - - void setup() { - Serial.begin(115200); - delay(1000); - Serial.println("\n========================================"); - Serial.println("Matter Simple Window Blinds Example"); - Serial.println("========================================\n"); - - // 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); - WiFi.mode(WIFI_STA); - WiFi.begin(ssid, password); - while (WiFi.status() != WL_CONNECTED) { - delay(500); - Serial.print("."); - } - Serial.println(""); - Serial.println("WiFi connected"); - Serial.print("IP address: "); - Serial.println(WiFi.localIP()); - #endif - - // Initialize Window Covering endpoint - // Using ROLLERSHADE type (lift only, no tilt) - WindowBlinds.begin(100, 0, MatterWindowCovering::ROLLERSHADE); - - // Set up the onGoToLiftPercentage callback - this handles all window covering changes requested by the Matter Controller - WindowBlinds.onGoToLiftPercentage(onBlindLift); - - // Start Matter - Matter.begin(); - Serial.println("Matter started"); - Serial.println(); - - // Print commissioning information - 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()); - Serial.println("========================================"); - } - - void loop() { - delay(100); - } +// 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 Simple Window Blinds Example +// This is a minimal example that only controls Lift percentage using a single onGoToLiftPercentage() callback + +#include +#if !CONFIG_ENABLE_CHIPOBLE +// if the device can be commissioned using BLE, WiFi is not used - save flash space +#include +#endif + +// 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 + +// Simple callback - handles window Lift change request +bool onBlindLift(uint8_t liftPercent) { + // This example only uses lift + Serial.printf("Window Covering change request: Lift=%d%%\r\n", liftPercent); + + // Returning true will store the new Lift value into the Matter Cluster + return true; +} + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println("\n========================================"); + Serial.println("Matter Simple Window Blinds Example"); + Serial.println("========================================\n"); + +// 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); + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println(""); + Serial.println("WiFi connected"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); +#endif + + // Initialize Window Covering endpoint + // Using ROLLERSHADE type (lift only, no tilt) + WindowBlinds.begin(100, 0, MatterWindowCovering::ROLLERSHADE); + + // Set up the onGoToLiftPercentage callback - this handles all window covering changes requested by the Matter Controller + WindowBlinds.onGoToLiftPercentage(onBlindLift); + + // Start Matter + Matter.begin(); + Serial.println("Matter started"); + Serial.println(); + + // Print commissioning information + 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()); + Serial.println("========================================"); +} + +void loop() { + delay(100); +} diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md b/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md index 0e0053da1a2..3f417916d3f 100644 --- a/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md @@ -1,136 +1,135 @@ -# Matter Simple Window Blinds Example - -This is a minimal example demonstrating how to create a Matter-compatible window covering device with lift control only. This example uses a single `onGoToLiftPercentage()` callback to handle all window covering lift changes, making it ideal for simple implementations. - -## Supported Targets - -| SoC | Wi-Fi | Thread | BLE Commissioning | Status | -| --- | ---- | ------ | ----------------- | ------ | -| ESP32 | ✅ | ❌ | ❌ | Fully supported | -| ESP32-S2 | ✅ | ❌ | ❌ | Fully supported | -| ESP32-S3 | ✅ | ❌ | ✅ | Fully supported | -| ESP32-C3 | ✅ | ❌ | ✅ | Fully supported | -| ESP32-C5 | ✅ | ❌ | ✅ | Fully supported | -| ESP32-C6 | ✅ | ❌ | ✅ | Fully supported | -| ESP32-H2 | ❌ | ✅ | ✅ | Supported (Thread only) | - -### Note on Commissioning: - -- **ESP32 & ESP32-S2** do not support commissioning over Bluetooth LE. For these chips, you must provide Wi-Fi credentials directly in the sketch code so they can connect to your network manually. -- **ESP32-C6** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. -- **ESP32-C5** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. - -## Features - -- Matter protocol implementation for a window covering device -- **Lift control only** (0-100%) - simplified implementation -- **Single `onGoToLiftPercentage()` callback** - handles all window covering lift changes when `TargetPositionLiftPercent100ths` changes -- Matter commissioning via QR code or manual pairing code -- Integration with Apple HomeKit, Amazon Alexa, and Google Home - -## Hardware Requirements - -- ESP32 compatible development board (see supported targets table) -- Window covering motor/actuator (optional for testing - example simulates movement) - -## Software Setup - -### Prerequisites - -1. Install the Arduino IDE (2.0 or newer recommended) -2. Install ESP32 Arduino Core with Matter support -3. ESP32 Arduino libraries: - - `Matter` - - `Wi-Fi` (only for ESP32 and ESP32-S2) - -### Configuration - -Before uploading the sketch, configure the following: - -1. **Wi-Fi Credentials** (for ESP32 and ESP32-S2 only): - ```cpp - const char *ssid = "your-ssid"; - const char *password = "your-password"; - ``` - -## Expected Output - -``` -======================================== -Matter Simple Window Blinds Example -======================================== - -Connecting to your-ssid -WiFi connected -IP address: 192.168.1.100 -Matter started - -======================================== -Matter Node is not commissioned yet. -Initiate the device discovery in your Matter environment. -Commission it to your Matter hub with the manual pairing code or QR code -Manual pairing code: 34970112332 -QR code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT:Y.K9042C00KA0648G00 -======================================== -``` - -When a command is received from the Matter controller: -``` -Window Covering change request: Lift=50% -``` - -## Usage - -1. **Commissioning**: Use the QR code or manual pairing code to commission the device to your Matter hub (Apple Home, Google Home, or Amazon Alexa). - -2. **Control**: Once commissioned, you can control the window covering lift percentage (0-100%) from your smart home app. The `onGoToLiftPercentage()` callback will be triggered whenever the target lift percentage changes. - -## Code Structure - -- **`onBlindLift()`**: Callback function that handles window covering lift changes. This is registered with `WindowBlinds.onGoToLiftPercentage()` and is triggered when `TargetPositionLiftPercent100ths` changes. The callback receives the target lift percentage (0-100%). -- **`setup()`**: Initializes WiFi (if needed), Window Covering endpoint with `ROLLERSHADE` type, registers the callback, and starts Matter. -- **`loop()`**: Empty - all control is handled via Matter callbacks. - -## Customization - -### Adding Motor Control - -In the `onBlindLift()` callback, replace the simulation code with actual motor control: - -```cpp -bool onBlindLift(uint8_t liftPercent) { - Serial.printf("Moving window covering to %d%%\r\n", liftPercent); - - // Here you would control your actual motor/actuator - // For example: - // - Calculate target position based on liftPercent and installed limits (if configured) - // - Move motor to target position - // - When movement is complete, update current position: - // WindowBlinds.setLiftPercentage(finalLiftPercent); - // WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL); - - // For this minimal example, we just return true to accept the command - return true; // Indicate command was accepted -} -``` - -## Troubleshooting - -1. **Device not discoverable**: Ensure WiFi is connected (for ESP32/ESP32-S2) or BLE is enabled (for other chips). - -2. **Lift percentage not updating**: Check that `onGoToLiftPercentage()` callback is properly registered and that `setLiftPercentage()` is called when movement is complete to update the `CurrentPosition` attribute. - -3. **Commands not working**: Ensure the callback returns `true` to accept the command. If it returns `false`, the command will be rejected. - -4. **Motor not responding**: Replace the simulation code in `onBlindLift()` with your actual motor control implementation. Remember to update `CurrentPosition` and set `OperationalState` to `STALL` when movement is complete. - -## Notes - -- This example uses `ROLLERSHADE` window covering type (lift only, no tilt). -- The example accepts commands but doesn't actually move a motor. In a real implementation, you should: - 1. Move the motor to the target position in the callback - 2. Update `CurrentPositionLiftPercent100ths` using `setLiftPercentage()` when movement is complete - 3. Set `OperationalState` to `STALL` using `setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL)` to indicate the device has reached the target position -- **Important**: `onGoToLiftPercentage()` is called when `TargetPositionLiftPercent100ths` changes. This happens when commands are executed or when a Matter controller writes directly to the target position attribute. -- Commands modify `TargetPosition`, not `CurrentPosition`. The application is responsible for updating `CurrentPosition` when the physical device actually moves. - +# Matter Simple Window Blinds Example + +This is a minimal example demonstrating how to create a Matter-compatible window covering device with lift control only. This example uses a single `onGoToLiftPercentage()` callback to handle all window covering lift changes, making it ideal for simple implementations. + +## Supported Targets + +| SoC | Wi-Fi | Thread | BLE Commissioning | Status | +| --- | ---- | ------ | ----------------- | ------ | +| ESP32 | ✅ | ❌ | ❌ | Fully supported | +| ESP32-S2 | ✅ | ❌ | ❌ | Fully supported | +| ESP32-S3 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C3 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C5 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-C6 | ✅ | ❌ | ✅ | Fully supported | +| ESP32-H2 | ❌ | ✅ | ✅ | Supported (Thread only) | + +### Note on Commissioning: + +- **ESP32 & ESP32-S2** do not support commissioning over Bluetooth LE. For these chips, you must provide Wi-Fi credentials directly in the sketch code so they can connect to your network manually. +- **ESP32-C6** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. +- **ESP32-C5** Although it has Thread support, the ESP32 Arduino Matter Library has been precompiled using Wi-Fi only. In order to configure it for Thread-only operation it is necessary to build the project as an ESP-IDF component and to disable the Matter Wi-Fi station feature. + +## Features + +- Matter protocol implementation for a window covering device +- **Lift control only** (0-100%) - simplified implementation +- **Single `onGoToLiftPercentage()` callback** - handles all window covering lift changes when `TargetPositionLiftPercent100ths` changes +- Matter commissioning via QR code or manual pairing code +- Integration with Apple HomeKit, Amazon Alexa, and Google Home + +## Hardware Requirements + +- ESP32 compatible development board (see supported targets table) +- Window covering motor/actuator (optional for testing - example simulates movement) + +## Software Setup + +### Prerequisites + +1. Install the Arduino IDE (2.0 or newer recommended) +2. Install ESP32 Arduino Core with Matter support +3. ESP32 Arduino libraries: + - `Matter` + - `Wi-Fi` (only for ESP32 and ESP32-S2) + +### Configuration + +Before uploading the sketch, configure the following: + +1. **Wi-Fi Credentials** (for ESP32 and ESP32-S2 only): + ```cpp + const char *ssid = "your-ssid"; + const char *password = "your-password"; + ``` + +## Expected Output + +``` +======================================== +Matter Simple Window Blinds Example +======================================== + +Connecting to your-ssid +WiFi connected +IP address: 192.168.1.100 +Matter started + +======================================== +Matter Node is not commissioned yet. +Initiate the device discovery in your Matter environment. +Commission it to your Matter hub with the manual pairing code or QR code +Manual pairing code: 34970112332 +QR code URL: https://project-chip.github.io/connectedhomeip/qrcode.html?data=MT:Y.K9042C00KA0648G00 +======================================== +``` + +When a command is received from the Matter controller: +``` +Window Covering change request: Lift=50% +``` + +## Usage + +1. **Commissioning**: Use the QR code or manual pairing code to commission the device to your Matter hub (Apple Home, Google Home, or Amazon Alexa). + +2. **Control**: Once commissioned, you can control the window covering lift percentage (0-100%) from your smart home app. The `onGoToLiftPercentage()` callback will be triggered whenever the target lift percentage changes. + +## Code Structure + +- **`onBlindLift()`**: Callback function that handles window covering lift changes. This is registered with `WindowBlinds.onGoToLiftPercentage()` and is triggered when `TargetPositionLiftPercent100ths` changes. The callback receives the target lift percentage (0-100%). +- **`setup()`**: Initializes WiFi (if needed), Window Covering endpoint with `ROLLERSHADE` type, registers the callback, and starts Matter. +- **`loop()`**: Empty - all control is handled via Matter callbacks. + +## Customization + +### Adding Motor Control + +In the `onBlindLift()` callback, replace the simulation code with actual motor control: + +```cpp +bool onBlindLift(uint8_t liftPercent) { + Serial.printf("Moving window covering to %d%%\r\n", liftPercent); + + // Here you would control your actual motor/actuator + // For example: + // - Calculate target position based on liftPercent and installed limits (if configured) + // - Move motor to target position + // - When movement is complete, update current position: + // WindowBlinds.setLiftPercentage(finalLiftPercent); + // WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL); + + // For this minimal example, we just return true to accept the command + return true; // Indicate command was accepted +} +``` + +## Troubleshooting + +1. **Device not discoverable**: Ensure WiFi is connected (for ESP32/ESP32-S2) or BLE is enabled (for other chips). + +2. **Lift percentage not updating**: Check that `onGoToLiftPercentage()` callback is properly registered and that `setLiftPercentage()` is called when movement is complete to update the `CurrentPosition` attribute. + +3. **Commands not working**: Ensure the callback returns `true` to accept the command. If it returns `false`, the command will be rejected. + +4. **Motor not responding**: Replace the simulation code in `onBlindLift()` with your actual motor control implementation. Remember to update `CurrentPosition` and set `OperationalState` to `STALL` when movement is complete. + +## Notes + +- This example uses `ROLLERSHADE` window covering type (lift only, no tilt). +- The example accepts commands but doesn't actually move a motor. In a real implementation, you should: + 1. Move the motor to the target position in the callback + 2. Update `CurrentPositionLiftPercent100ths` using `setLiftPercentage()` when movement is complete + 3. Set `OperationalState` to `STALL` using `setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL)` to indicate the device has reached the target position +- **Important**: `onGoToLiftPercentage()` is called when `TargetPositionLiftPercent100ths` changes. This happens when commands are executed or when a Matter controller writes directly to the target position attribute. +- Commands modify `TargetPosition`, not `CurrentPosition`. The application is responsible for updating `CurrentPosition` when the physical device actually moves. diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml b/libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml index 166b1b67930..050a80ff543 100644 --- a/libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/ci.yml @@ -1,4 +1,4 @@ -fqbn_append: PartitionScheme=huge_app - -requires: - - CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y +fqbn_append: PartitionScheme=huge_app + +requires: + - CONFIG_ESP_MATTER_ENABLE_DATA_MODEL=y From 93f3bc76c8c32a2d219c96fb5c9621634cc6bb43 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 15:13:44 -0300 Subject: [PATCH 19/28] fix(matter): formatting in MatterSimpleWidowsBlind example --- .../MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino index 51c4192d7f2..34b18df3215 100644 --- a/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino @@ -37,7 +37,7 @@ bool onBlindLift(uint8_t liftPercent) { // This example only uses lift Serial.printf("Window Covering change request: Lift=%d%%\r\n", liftPercent); - // Returning true will store the new Lift value into the Matter Cluster + // Returning true will store the new Lift value into the Matter Cluster return true; } From 1f9399078926e4b6e635adbcd547b596cfdf083c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:14:32 +0000 Subject: [PATCH 20/28] ci(pre-commit): Apply automatic fixes --- .../MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino index 34b18df3215..51c4192d7f2 100644 --- a/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino @@ -37,7 +37,7 @@ bool onBlindLift(uint8_t liftPercent) { // This example only uses lift Serial.printf("Window Covering change request: Lift=%d%%\r\n", liftPercent); - // Returning true will store the new Lift value into the Matter Cluster + // Returning true will store the new Lift value into the Matter Cluster return true; } From 3ad6107fb75fbe7cc2c271a490364dc675100805 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 15:22:58 -0300 Subject: [PATCH 21/28] fix(matter): adds necessary command changes from ESP-Matter --- .../MatterEndpoints/MatterWindowCovering.cpp | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp index 54ae83c456d..25271b5fbb1 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -205,6 +205,48 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus log_d("Window Covering Target Lift Percentage changed to %d%%", targetLiftPercent); // Call callback to trigger movement - do NOT update currentLiftPercent here // `CurrentPosition` will be updated by the application when the device actually moves + // Get current position to detect StopMotion command + uint16_t currentLiftPercent100ths = 0; + esp_matter_attr_val_t currentVal = esp_matter_invalid(NULL); + if (getAttributeVal(WindowCovering::Id, WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id, ¤tVal)) { + if (!chip::app::NumericAttributeTraits::IsNullValue(currentVal.val.u16)) { + currentLiftPercent100ths = currentVal.val.u16; + log_d("Window Covering Current Lift Percentage is %d%%", (uint8_t)(currentLiftPercent100ths / 100)); + } + } + + // Detect command type based on target value and call appropriate callbacks + // Commands modify TargetPositionLiftPercent100ths: + // - UpOrOpen: sets TargetPosition = 0 (WC_PERCENT100THS_MIN_OPEN) + // - DownOrClose: sets TargetPosition = 10000 (WC_PERCENT100THS_MAX_CLOSED) + // - StopMotion: sets TargetPosition = CurrentPosition + // Priority: UpOrOpen/DownOrClose > StopMotion > GoToLiftPercentage + // Note: If StopMotion is executed when CurrentPosition is at 0 or 10000, + // it will be detected as UpOrOpen/DownOrClose (acceptable behavior) + if (targetLiftPercent100ths == 0) { + // UpOrOpen command - fully open (priority check) + log_d("Window Covering: UpOrOpen command detected"); + if (_onOpenCB != NULL) { + ret &= _onOpenCB(); + } + } else if (targetLiftPercent100ths == 10000) { + // DownOrClose command - fully closed (priority check) + log_d("Window Covering: DownOrClose command detected"); + if (_onCloseCB != NULL) { + ret &= _onCloseCB(); + } + } else if (targetLiftPercent100ths == currentLiftPercent100ths && currentLiftPercent100ths != 0 && currentLiftPercent100ths != 10000) { + // StopMotion command - target equals current position (but not at limits) + // This detects StopMotion when TargetPosition is set to CurrentPosition + // and CurrentPosition is not at the limits (0 or 10000) + log_d("Window Covering: StopMotion command detected"); + if (_onStopCB != NULL) { + ret &= _onStopCB(); + } + } + + // Always call the generic onGoToLiftPercentage callback for compatibility + // This handles all target position changes, including commands and direct attribute writes if (_onGoToLiftPercentageCB != NULL) { ret &= _onGoToLiftPercentageCB(targetLiftPercent); } From 10b77bbeeaadcaa01323e167c74debecb2754b8b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:23:40 +0000 Subject: [PATCH 22/28] ci(pre-commit): Apply automatic fixes --- libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp index 25271b5fbb1..fc293c5c2da 100644 --- a/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -214,7 +214,7 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus log_d("Window Covering Current Lift Percentage is %d%%", (uint8_t)(currentLiftPercent100ths / 100)); } } - + // Detect command type based on target value and call appropriate callbacks // Commands modify TargetPositionLiftPercent100ths: // - UpOrOpen: sets TargetPosition = 0 (WC_PERCENT100THS_MIN_OPEN) @@ -244,7 +244,7 @@ bool MatterWindowCovering::attributeChangeCB(uint16_t endpoint_id, uint32_t clus ret &= _onStopCB(); } } - + // Always call the generic onGoToLiftPercentage callback for compatibility // This handles all target position changes, including commands and direct attribute writes if (_onGoToLiftPercentageCB != NULL) { From ed47deb941e2647ee27eea509f641fc442a65fc4 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 15:34:22 -0300 Subject: [PATCH 23/28] feat(matter_docs): improves event handling documentation for MatterWindowCovering Updated callback descriptions and clarified command triggers for window covering events. --- docs/en/matter/ep_window_covering.rst | 69 ++++++++++++++++++++------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst index 60b34fb8b17..6b6da62f690 100644 --- a/docs/en/matter/ep_window_covering.rst +++ b/docs/en/matter/ep_window_covering.rst @@ -430,16 +430,32 @@ This function will return the operational state for the specified field (``STALL 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 window covering is opened. +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). .. code-block:: arduino void onOpen(EndPointOpenCB onChangeCB); -* ``onChangeCB`` - Function to call when window covering is opened +* ``onChangeCB`` - Function to call when ``UpOrOpen`` command is received The callback signature is: @@ -450,13 +466,13 @@ The callback signature is: onClose ^^^^^^^ -Sets a callback function to be called when the window covering is closed. +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). .. code-block:: arduino void onClose(EndPointCloseCB onChangeCB); -* ``onChangeCB`` - Function to call when window covering is closed +* ``onChangeCB`` - Function to call when ``DownOrClose`` command is received The callback signature is: @@ -467,13 +483,20 @@ The callback signature is: onGoToLiftPercentage ^^^^^^^^^^^^^^^^^^^^ -Sets a callback function to be called when the lift percentage changes. +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. .. code-block:: arduino void onGoToLiftPercentage(EndPointLiftCB onChangeCB); -* ``onChangeCB`` - Function to call when lift percentage changes +* ``onChangeCB`` - Function to call when target lift percentage changes The callback signature is: @@ -481,18 +504,25 @@ The callback signature is: bool onChangeCallback(uint8_t liftPercent); -* ``liftPercent`` - New lift percentage (0-100) +* ``liftPercent`` - Target lift percentage (0-100, where 0 is fully closed, 100 is fully open) onGoToTiltPercentage ^^^^^^^^^^^^^^^^^^^^ -Sets a callback function to be called when the tilt percentage changes. +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. .. code-block:: arduino void onGoToTiltPercentage(EndPointTiltCB onChangeCB); -* ``onChangeCB`` - Function to call when tilt percentage changes +* ``onChangeCB`` - Function to call when target tilt percentage changes The callback signature is: @@ -500,18 +530,18 @@ The callback signature is: bool onChangeCallback(uint8_t tiltPercent); -* ``tiltPercent`` - New tilt percentage (0-100) +* ``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 window covering movement is stopped. +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. .. code-block:: arduino void onStop(EndPointStopCB onChangeCB); -* ``onChangeCB`` - Function to call when window covering is stopped +* ``onChangeCB`` - Function to call when ``StopMotion`` command is received The callback signature is: @@ -522,13 +552,19 @@ The callback signature is: onChange ^^^^^^^^ -Sets a callback function to be called when any parameter changes. +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. .. code-block:: arduino void onChange(EndPointCB onChangeCB); -* ``onChangeCB`` - Function to call when state changes +* ``onChangeCB`` - Function to call when current position attributes change The callback signature is: @@ -536,8 +572,8 @@ The callback signature is: bool onChangeCallback(uint8_t liftPercent, uint8_t tiltPercent); -* ``liftPercent`` - New lift percentage (0-100) -* ``tiltPercent`` - New tilt percentage (0-100) +* ``liftPercent`` - Current lift percentage (0-100) +* ``tiltPercent`` - Current tilt percentage (0-100) updateAccessory ^^^^^^^^^^^^^^^ @@ -558,3 +594,4 @@ Window Covering .. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino :language: arduino + From af00cc725043c6267aea709e6429973c5fd061f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:35:09 +0000 Subject: [PATCH 24/28] ci(pre-commit): Apply automatic fixes --- docs/en/matter/ep_window_covering.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/en/matter/ep_window_covering.rst b/docs/en/matter/ep_window_covering.rst index 6b6da62f690..3d976951691 100644 --- a/docs/en/matter/ep_window_covering.rst +++ b/docs/en/matter/ep_window_covering.rst @@ -594,4 +594,3 @@ Window Covering .. literalinclude:: ../../../libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino :language: arduino - From 3c587c01dd2fec9763f6f3f761963019e456a741 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 15:36:53 -0300 Subject: [PATCH 25/28] feat(matter_docs): Enhance README with callback usage details Clarify the importance of calling setLiftPercentage() and setTiltPercentage() in callbacks to update CurrentPosition attributes. Expand on callback flow and provide details on target position callbacks. --- .../examples/MatterWindowCovering/README.md | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md index 7f9726330f7..0aac3e53660 100644 --- a/libraries/Matter/examples/MatterWindowCovering/README.md +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -148,9 +148,14 @@ For production use with a motorized window covering: - Use encoders or limit switches to provide position feedback - For lift: Update `currentLift` (cm) based on actual motor position - For tilt: Update `currentTiltPercent` (rotation percentage) based on actual motor rotation - - Call `setLiftPercentage()` and `setTiltPercentage()` to update `CurrentPosition` attributes (these methods update the device's actual position, not the target) + - **Important**: Call `setLiftPercentage()` and `setTiltPercentage()` in your `onGoToLiftPercentage()` or `onGoToTiltPercentage()` callbacks to update `CurrentPosition` attributes when the physical device actually moves. This will trigger the `onChange()` callback if registered. - Call `setOperationalState(LIFT, STALL)` or `setOperationalState(TILT, STALL)` when movement is complete to indicate the device has reached the target position - Configure installed limits using `setInstalledOpenLimitLift()`, `setInstalledClosedLimitLift()`, `setInstalledOpenLimitTilt()`, and `setInstalledClosedLimitTilt()` to define the physical range of your window covering + + **Callback Flow:** + - Matter command → `TargetPosition` changes → `onGoToLiftPercentage()`/`onGoToTiltPercentage()` called + - Your callback moves the motor → When movement completes, call `setLiftPercentage()`/`setTiltPercentage()` + - `setLiftPercentage()`/`setTiltPercentage()` update `CurrentPosition` → `onChange()` called (if registered) 3. **Window Covering Type**: - Pass the covering type to `begin()` to configure the appropriate type (e.g., `BLIND_LIFT_AND_TILT`, `ROLLERSHADE`, etc.) @@ -198,12 +203,18 @@ The MatterWindowCovering example consists of the following main components: 2. **`loop()`**: Checks the Matter commissioning state, handles button input for manual lift control and factory reset, and allows the Matter stack to process events. 3. **Callbacks**: - - `fullOpen()`: Handles open command - moves window covering to fully open (100% lift), updates `CurrentPosition`, and sets operational state to `STALL` - - `fullClose()`: Handles close command - moves window covering to fully closed (0% lift), updates `CurrentPosition`, and sets operational state to `STALL` - - `goToLiftPercentage()`: Handles lift percentage changes (0-100%), calculates absolute position (cm) based on installed limits, updates `CurrentPosition`, and sets operational state to `STALL` when movement is complete - - `goToTiltPercentage()`: Handles tilt rotation percentage changes (0-100%), updates `CurrentPosition`, and sets operational state to `STALL` when movement is complete - - `stopMotor()`: Handles stop command - stops any ongoing movement, updates `CurrentPosition` for both lift and tilt, and sets operational state to `STALL` for both - - `visualizeWindowBlinds()`: Updates RGB LED to reflect current lift and tilt positions + + **Target Position Callbacks** (called when `TargetPosition` attributes change): + - `fullOpen()`: Registered with `onOpen()` - called when `UpOrOpen` command is received. Moves window covering to fully open (100% lift), calls `setLiftPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` + - `fullClose()`: Registered with `onClose()` - called when `DownOrClose` command is received. Moves window covering to fully closed (0% lift), calls `setLiftPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` + - `goToLiftPercentage()`: Registered with `onGoToLiftPercentage()` - called when `TargetPositionLiftPercent100ths` changes (from commands, `setTargetLiftPercent100ths()`, or direct attribute writes). Calculates absolute position (cm) based on installed limits, calls `setLiftPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` when movement is complete + - `goToTiltPercentage()`: Registered with `onGoToTiltPercentage()` - called when `TargetPositionTiltPercent100ths` changes. Calls `setTiltPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` when movement is complete + - `stopMotor()`: Registered with `onStop()` - called when `StopMotion` command is received. Stops any ongoing movement, calls `setLiftPercentage()` and `setTiltPercentage()` to update `CurrentPosition` for both, and sets operational state to `STALL` for both + + **Current Position Callback** (called when `CurrentPosition` attributes change): + - `onChange()`: Registered with `onChange()` - called when `CurrentPositionLiftPercent100ths` or `CurrentPositionTiltPercent100ths` change (after `setLiftPercentage()` or `setTiltPercentage()` are called). Updates RGB LED visualization to reflect current positions + + **Note:** The Target Position callbacks (`fullOpen()`, `fullClose()`, `goToLiftPercentage()`, `goToTiltPercentage()`, `stopMotor()`) call `setLiftPercentage()` or `setTiltPercentage()` to update the `CurrentPosition` attributes. This triggers the `onChange()` callback, which updates the visualization. ## Troubleshooting @@ -225,3 +236,4 @@ The MatterWindowCovering example consists of the following main components: ## License This example is licensed under the Apache License, Version 2.0. + From 562c25dd02c1e02a1fe69183a9b677c71a3a7d3f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:37:37 +0000 Subject: [PATCH 26/28] ci(pre-commit): Apply automatic fixes --- libraries/Matter/examples/MatterWindowCovering/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md index 0aac3e53660..8e18539c65a 100644 --- a/libraries/Matter/examples/MatterWindowCovering/README.md +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -151,7 +151,7 @@ For production use with a motorized window covering: - **Important**: Call `setLiftPercentage()` and `setTiltPercentage()` in your `onGoToLiftPercentage()` or `onGoToTiltPercentage()` callbacks to update `CurrentPosition` attributes when the physical device actually moves. This will trigger the `onChange()` callback if registered. - Call `setOperationalState(LIFT, STALL)` or `setOperationalState(TILT, STALL)` when movement is complete to indicate the device has reached the target position - Configure installed limits using `setInstalledOpenLimitLift()`, `setInstalledClosedLimitLift()`, `setInstalledOpenLimitTilt()`, and `setInstalledClosedLimitTilt()` to define the physical range of your window covering - + **Callback Flow:** - Matter command → `TargetPosition` changes → `onGoToLiftPercentage()`/`onGoToTiltPercentage()` called - Your callback moves the motor → When movement completes, call `setLiftPercentage()`/`setTiltPercentage()` @@ -203,17 +203,17 @@ The MatterWindowCovering example consists of the following main components: 2. **`loop()`**: Checks the Matter commissioning state, handles button input for manual lift control and factory reset, and allows the Matter stack to process events. 3. **Callbacks**: - + **Target Position Callbacks** (called when `TargetPosition` attributes change): - `fullOpen()`: Registered with `onOpen()` - called when `UpOrOpen` command is received. Moves window covering to fully open (100% lift), calls `setLiftPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` - `fullClose()`: Registered with `onClose()` - called when `DownOrClose` command is received. Moves window covering to fully closed (0% lift), calls `setLiftPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` - `goToLiftPercentage()`: Registered with `onGoToLiftPercentage()` - called when `TargetPositionLiftPercent100ths` changes (from commands, `setTargetLiftPercent100ths()`, or direct attribute writes). Calculates absolute position (cm) based on installed limits, calls `setLiftPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` when movement is complete - `goToTiltPercentage()`: Registered with `onGoToTiltPercentage()` - called when `TargetPositionTiltPercent100ths` changes. Calls `setTiltPercentage()` to update `CurrentPosition`, and sets operational state to `STALL` when movement is complete - `stopMotor()`: Registered with `onStop()` - called when `StopMotion` command is received. Stops any ongoing movement, calls `setLiftPercentage()` and `setTiltPercentage()` to update `CurrentPosition` for both, and sets operational state to `STALL` for both - + **Current Position Callback** (called when `CurrentPosition` attributes change): - `onChange()`: Registered with `onChange()` - called when `CurrentPositionLiftPercent100ths` or `CurrentPositionTiltPercent100ths` change (after `setLiftPercentage()` or `setTiltPercentage()` are called). Updates RGB LED visualization to reflect current positions - + **Note:** The Target Position callbacks (`fullOpen()`, `fullClose()`, `goToLiftPercentage()`, `goToTiltPercentage()`, `stopMotor()`) call `setLiftPercentage()` or `setTiltPercentage()` to update the `CurrentPosition` attributes. This triggers the `onChange()` callback, which updates the visualization. ## Troubleshooting @@ -236,4 +236,3 @@ The MatterWindowCovering example consists of the following main components: ## License This example is licensed under the Apache License, Version 2.0. - From ea82ad84ebc407c28c13df97e82b74bebae24c19 Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 15:40:55 -0300 Subject: [PATCH 27/28] fix(matter_docs): typos in README.md for MatterSimpleWidowsBlind --- libraries/Matter/examples/MatterSimpleWidowsBlind/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md b/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md index 3f417916d3f..637d2a4b398 100644 --- a/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md @@ -88,7 +88,7 @@ Window Covering change request: Lift=50% ## Code Structure - **`onBlindLift()`**: Callback function that handles window covering lift changes. This is registered with `WindowBlinds.onGoToLiftPercentage()` and is triggered when `TargetPositionLiftPercent100ths` changes. The callback receives the target lift percentage (0-100%). -- **`setup()`**: Initializes WiFi (if needed), Window Covering endpoint with `ROLLERSHADE` type, registers the callback, and starts Matter. +- **`setup()`**: Initializes Wi-Fi (if needed), Window Covering endpoint with `ROLLERSHADE` type, registers the callback, and starts Matter. - **`loop()`**: Empty - all control is handled via Matter callbacks. ## Customization @@ -116,7 +116,7 @@ bool onBlindLift(uint8_t liftPercent) { ## Troubleshooting -1. **Device not discoverable**: Ensure WiFi is connected (for ESP32/ESP32-S2) or BLE is enabled (for other chips). +1. **Device not discoverable**: Ensure Wi-Fi is connected (for ESP32/ESP32-S2) or BLE is enabled (for other chips). 2. **Lift percentage not updating**: Check that `onGoToLiftPercentage()` callback is properly registered and that `setLiftPercentage()` is called when movement is complete to update the `CurrentPosition` attribute. From 52b04af255fe35670e8026f674c783d8895470af Mon Sep 17 00:00:00 2001 From: Sugar Glider Date: Tue, 9 Dec 2025 16:24:02 -0300 Subject: [PATCH 28/28] fix(matter): Add new keywords for window treatments --- libraries/Matter/keywords.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/libraries/Matter/keywords.txt b/libraries/Matter/keywords.txt index 3d8e7861776..88c005976ca 100644 --- a/libraries/Matter/keywords.txt +++ b/libraries/Matter/keywords.txt @@ -235,3 +235,19 @@ MATTER_FABRIC_REMOVED LITERAL1 MATTER_FABRIC_COMMITTED LITERAL1 MATTER_FABRIC_UPDATED LITERAL1 MATTER_ESP32_PUBLIC_SPECIFIC_EVENT LITERAL1 +ROLLERSHADE LITERAL1 +ROLLERSHADE_2_MOTOR LITERAL1 +ROLLERSHADE_EXTERIOR LITERAL1 +ROLLERSHADE_EXTERIOR_2_MOTOR LITERAL1 +DRAPERY LITERAL1 +AWNING LITERAL1 +SHUTTER LITERAL1 +BLIND_TILT_ONLY LITERAL1 +BLIND_LIFT_AND_TILT LITERAL1 +PROJECTOR_SCREEN LITERAL1 +STALL LITERAL1 +MOVING_UP_OR_OPEN LITERAL1 +MOVING_DOWN_OR_CLOSE LITERAL1 +GLOBAL LITERAL1 +LIFT LITERAL1 +TILT LITERAL1