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..3d976951691 --- /dev/null +++ b/docs/en/matter/ep_window_covering.rst @@ -0,0 +1,596 @@ +#################### +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 (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 (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 (centimeters). + +setInstalledClosedLimitLift +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sets the installed closed limit for lift (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 (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 (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 +************** + +The ``MatterWindowCovering`` class automatically detects Matter commands and calls the appropriate callbacks when registered. There are two types of callbacks: + +**Target Position Callbacks** (triggered when ``TargetPosition`` attributes change): +* ``onOpen()`` - called when ``UpOrOpen`` command is received (sets target to 0% = fully open) +* ``onClose()`` - called when ``DownOrClose`` command is received (sets target to 100% = fully closed) +* ``onStop()`` - called when ``StopMotion`` command is received (sets target to current position) +* ``onGoToLiftPercentage()`` - called when ``TargetPositionLiftPercent100ths`` changes (from any command, ``setTargetLiftPercent100ths()``, or direct attribute write) +* ``onGoToTiltPercentage()`` - called when ``TargetPositionTiltPercent100ths`` changes (from any command, ``setTargetTiltPercent100ths()``, or direct attribute write) + +**Current Position Callback** (triggered when ``CurrentPosition`` attributes change): +* ``onChange()`` - called when ``CurrentPositionLiftPercent100ths`` or ``CurrentPositionTiltPercent100ths`` change (after ``setLiftPercentage()``/``setTiltPercentage()`` are called or when a Matter controller updates these attributes directly) + +**Important:** ``onChange()`` is **not** automatically called when Matter commands are executed. Commands modify ``TargetPosition``, not ``CurrentPosition``. To trigger ``onChange()``, your ``onGoToLiftPercentage()`` or ``onGoToTiltPercentage()`` callback must call ``setLiftPercentage()`` or ``setTiltPercentage()`` when the physical device actually moves. + +**Note:** All callbacks are optional. If a specific callback is not registered, only the generic ``onGoToLiftPercentage()`` or ``onGoToTiltPercentage()`` callbacks will be called (if registered). + +onOpen +^^^^^^ + +Sets a callback function to be called when the ``UpOrOpen`` command is received from a Matter controller. This command sets the target position to 0% (fully open). + +.. code-block:: arduino + + void onOpen(EndPointOpenCB onChangeCB); + +* ``onChangeCB`` - Function to call when ``UpOrOpen`` command is received + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(); + +onClose +^^^^^^^ + +Sets a callback function to be called when the ``DownOrClose`` command is received from a Matter controller. This command sets the target position to 100% (fully closed). + +.. code-block:: arduino + + void onClose(EndPointCloseCB onChangeCB); + +* ``onChangeCB`` - Function to call when ``DownOrClose`` command is received + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(); + +onGoToLiftPercentage +^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when ``TargetPositionLiftPercent100ths`` changes. This is triggered by: +* Matter commands: ``UpOrOpen``, ``DownOrClose``, ``StopMotion``, ``GoToLiftPercentage`` +* Calling ``setTargetLiftPercent100ths()`` +* Direct attribute writes to ``TargetPositionLiftPercent100ths`` + +This callback is always called when the target lift position changes, regardless of which command or method was used to change it. + +**Note:** This callback receives the **target** position. To update the **current** position (which triggers ``onChange()``), call ``setLiftPercentage()`` when the physical device actually moves. + +.. code-block:: arduino + + void onGoToLiftPercentage(EndPointLiftCB onChangeCB); + +* ``onChangeCB`` - Function to call when target lift percentage changes + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(uint8_t liftPercent); + +* ``liftPercent`` - Target lift percentage (0-100, where 0 is fully closed, 100 is fully open) + +onGoToTiltPercentage +^^^^^^^^^^^^^^^^^^^^ + +Sets a callback function to be called when ``TargetPositionTiltPercent100ths`` changes. This is triggered by: +* Matter commands: ``UpOrOpen``, ``DownOrClose``, ``StopMotion``, ``GoToTiltPercentage`` +* Calling ``setTargetTiltPercent100ths()`` +* Direct attribute writes to ``TargetPositionTiltPercent100ths`` + +This callback is always called when the target tilt position changes, regardless of which command or method was used to change it. + +**Note:** This callback receives the **target** position. To update the **current** position (which triggers ``onChange()``), call ``setTiltPercentage()`` when the physical device actually moves. + +.. code-block:: arduino + + void onGoToTiltPercentage(EndPointTiltCB onChangeCB); + +* ``onChangeCB`` - Function to call when target tilt percentage changes + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(uint8_t tiltPercent); + +* ``tiltPercent`` - Target tilt percentage (0-100, where 0 is fully closed, 100 is fully open) + +onStop +^^^^^^ + +Sets a callback function to be called when the ``StopMotion`` command is received from a Matter controller. This command sets the target position to the current position, effectively stopping any movement. + +.. code-block:: arduino + + void onStop(EndPointStopCB onChangeCB); + +* ``onChangeCB`` - Function to call when ``StopMotion`` command is received + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(); + +onChange +^^^^^^^^ + +Sets a callback function to be called when ``CurrentPositionLiftPercent100ths`` or ``CurrentPositionTiltPercent100ths`` attributes change. This is different from ``onGoToLiftPercentage()`` and ``onGoToTiltPercentage()``, which are called when ``TargetPosition`` attributes change. + +**When ``onChange()`` is called:** +* When ``CurrentPositionLiftPercent100ths`` changes (after ``setLiftPercentage()`` is called or when a Matter controller updates this attribute directly) +* When ``CurrentPositionTiltPercent100ths`` changes (after ``setTiltPercentage()`` is called or when a Matter controller updates this attribute directly) + +**Important:** ``onChange()`` is **not** automatically called when Matter commands are executed. Commands modify ``TargetPosition`` attributes, which trigger ``onGoToLiftPercentage()`` or ``onGoToTiltPercentage()`` callbacks instead. To trigger ``onChange()`` after a command, your ``onGoToLiftPercentage()`` or ``onGoToTiltPercentage()`` callback must call ``setLiftPercentage()`` or ``setTiltPercentage()`` to update the ``CurrentPosition`` attributes when the physical device actually moves. + +.. code-block:: arduino + + void onChange(EndPointCB onChangeCB); + +* ``onChangeCB`` - Function to call when current position attributes change + +The callback signature is: + +.. code-block:: arduino + + bool onChangeCallback(uint8_t liftPercent, uint8_t tiltPercent); + +* ``liftPercent`` - Current lift percentage (0-100) +* ``tiltPercent`` - Current tilt percentage (0-100) + +updateAccessory +^^^^^^^^^^^^^^^ + +Updates the state of the window covering using the current Matter internal state. + +.. 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/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino b/libraries/Matter/examples/MatterSimpleWidowsBlind/MatterSimpleWidowsBlind.ino new file mode 100644 index 00000000000..51c4192d7f2 --- /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..637d2a4b398 --- /dev/null +++ b/libraries/Matter/examples/MatterSimpleWidowsBlind/README.md @@ -0,0 +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 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 + +### 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 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. + +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..050a80ff543 --- /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 diff --git a/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino new file mode 100644 index 00000000000..c63915a7fdf --- /dev/null +++ b/libraries/Matter/examples/MatterWindowCovering/MatterWindowCovering.ino @@ -0,0 +1,355 @@ +// 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 = 90; // Maximum tilt absolute value +const uint16_t MIN_TILT = 0; // Minimum tilt absolute value + +// Current window covering state +// These will be initialized in setup() based on installed limits and saved percentages +uint16_t currentLift = 0; // Lift position in cm +uint8_t currentLiftPercent = 100; +uint8_t currentTiltPercent = 0; // Tilt rotation percentage (0-100%) + +// Visualize window covering position using RGB LED +// Lift percentage controls brightness (0% = off, 100% = full brightness) +#ifdef RGB_BUILTIN +const uint8_t ledPin = RGB_BUILTIN; +#else +const uint8_t ledPin = 2; // Set your pin here if your board has not defined RGB_BUILTIN +#warning "Do not forget to set the RGB LED pin" +#endif + +void visualizeWindowBlinds(uint8_t liftPercent, uint8_t tiltPercent) { +#ifdef RGB_BUILTIN + // Use RGB LED to visualize lift position (brightness) and tilt (color shift) + float brightness = (float)liftPercent / 100.0; // 0.0 to 1.0 + // Tilt affects color: 0% = red, 100% = blue + uint8_t red = (uint8_t)(map(tiltPercent, 0, 100, 255, 0) * brightness); + uint8_t blue = (uint8_t)(map(tiltPercent, 0, 100, 0, 255) * brightness); + uint8_t green = 0; + rgbLedWrite(ledPin, red, green, blue); +#else + // For non-RGB boards, just use brightness + uint8_t brightnessValue = map(liftPercent, 0, 100, 0, 255); + analogWrite(ledPin, brightnessValue); +#endif +} + +// Window Covering Callbacks +bool fullOpen() { + // This is where you would trigger your motor to go to full open state + // For simulation, we update instantly + uint16_t openLimit = WindowBlinds.getInstalledOpenLimitLift(); + currentLift = openLimit; + currentLiftPercent = 100; + Serial.printf("Opening window covering to full open (position: %d cm)\r\n", currentLift); + + // Update CurrentPosition to reflect actual position (setLiftPercentage now only updates CurrentPosition) + WindowBlinds.setLiftPercentage(currentLiftPercent); + + // Set operational status to STALL when movement is complete + WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL); + + // Store state + matterPref.putUChar(liftPercentPrefKey, currentLiftPercent); + + return true; +} + +bool fullClose() { + // This is where you would trigger your motor to go to full close state + // For simulation, we update instantly + uint16_t closedLimit = WindowBlinds.getInstalledClosedLimitLift(); + currentLift = closedLimit; + currentLiftPercent = 0; + Serial.printf("Closing window covering to full close (position: %d cm)\r\n", currentLift); + + // Update CurrentPosition to reflect actual position (setLiftPercentage now only updates CurrentPosition) + WindowBlinds.setLiftPercentage(currentLiftPercent); + + // Set operational status to STALL when movement is complete + WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL); + + // Store state + matterPref.putUChar(liftPercentPrefKey, currentLiftPercent); + + return true; +} + +bool goToLiftPercentage(uint8_t liftPercent) { + // update Lift operational state + if (liftPercent > currentLiftPercent) { + // Set operational status to OPEN + WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::MOVING_UP_OR_OPEN); + } + if (liftPercent < currentLiftPercent) { + // Set operational status to CLOSE + WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::MOVING_DOWN_OR_CLOSE); + } + + // This is where you would trigger your motor to go towards liftPercent + // For simulation, we update instantly + // Calculate absolute position based on installed limits + uint16_t openLimit = WindowBlinds.getInstalledOpenLimitLift(); + uint16_t closedLimit = WindowBlinds.getInstalledClosedLimitLift(); + + // Linear interpolation: 0% = openLimit, 100% = closedLimit + if (openLimit < closedLimit) { + currentLift = openLimit + ((closedLimit - openLimit) * liftPercent) / 100; + } else { + currentLift = openLimit - ((openLimit - closedLimit) * liftPercent) / 100; + } + currentLiftPercent = liftPercent; + Serial.printf("Moving lift to %d%% (position: %d cm)\r\n", currentLiftPercent, currentLift); + + // Update CurrentPosition to reflect actual position (setLiftPercentage now only updates CurrentPosition) + WindowBlinds.setLiftPercentage(currentLiftPercent); + + // Set operational status to STALL when movement is complete + WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL); + + // Store state + matterPref.putUChar(liftPercentPrefKey, currentLiftPercent); + + return true; +} + +bool goToTiltPercentage(uint8_t tiltPercent) { + // update Tilt operational state + if (tiltPercent < currentTiltPercent) { + // Set operational status to OPEN + WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::MOVING_UP_OR_OPEN); + } + if (tiltPercent > currentTiltPercent) { + // Set operational status to CLOSE + WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::MOVING_DOWN_OR_CLOSE); + } + + // This is where you would trigger your motor to rotate the shade to tiltPercent + // For simulation, we update instantly + currentTiltPercent = tiltPercent; + Serial.printf("Rotating tilt to %d%%\r\n", currentTiltPercent); + + // Update CurrentPosition to reflect actual position + WindowBlinds.setTiltPercentage(currentTiltPercent); + + // Set operational status to STALL when movement is complete + WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::STALL); + + // Store state + matterPref.putUChar(tiltPercentPrefKey, currentTiltPercent); + + return true; +} + +bool stopMotor() { + // Motor can be stopped while moving cover toward current target + Serial.println("Stopping window covering motor"); + + // Update CurrentPosition to reflect actual position when stopped + // (setLiftPercentage and setTiltPercentage now only update CurrentPosition) + WindowBlinds.setLiftPercentage(currentLiftPercent); + WindowBlinds.setTiltPercentage(currentTiltPercent); + + // Set operational status to STALL for both lift and tilt + WindowBlinds.setOperationalState(MatterWindowCovering::LIFT, MatterWindowCovering::STALL); + WindowBlinds.setOperationalState(MatterWindowCovering::TILT, MatterWindowCovering::STALL); + + return true; +} + +void setup() { + // Initialize the USER BUTTON (Boot button) GPIO + pinMode(buttonPin, INPUT_PULLUP); + // Initialize the RGB LED GPIO + pinMode(ledPin, OUTPUT); + digitalWrite(ledPin, LOW); + + Serial.begin(115200); + +// CONFIG_ENABLE_CHIPOBLE is enabled when BLE is used to commission the Matter Network +#if !CONFIG_ENABLE_CHIPOBLE + // We start by connecting to a WiFi network + Serial.print("Connecting to "); + Serial.println(ssid); + // Manually connect to WiFi + WiFi.begin(ssid, password); + // Wait for connection + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\r\nWiFi connected"); + Serial.println("IP address: "); + Serial.println(WiFi.localIP()); + delay(500); +#endif + + // Initialize Matter EndPoint + matterPref.begin("MatterPrefs", false); + // default lift percentage is 100% (fully open) if not stored before + uint8_t lastLiftPercent = matterPref.getUChar(liftPercentPrefKey, 100); + // default tilt percentage is 0% if not stored before + uint8_t lastTiltPercent = matterPref.getUChar(tiltPercentPrefKey, 0); + + // Initialize window covering with BLIND_LIFT_AND_TILT type + WindowBlinds.begin(lastLiftPercent, lastTiltPercent, MatterWindowCovering::BLIND_LIFT_AND_TILT); + + // Configure installed limits for lift and tilt + WindowBlinds.setInstalledOpenLimitLift(MIN_LIFT); + WindowBlinds.setInstalledClosedLimitLift(MAX_LIFT); + WindowBlinds.setInstalledOpenLimitTilt(MIN_TILT); + WindowBlinds.setInstalledClosedLimitTilt(MAX_TILT); + + // Initialize current positions based on percentages and installed limits + uint16_t openLimitLift = WindowBlinds.getInstalledOpenLimitLift(); + uint16_t closedLimitLift = WindowBlinds.getInstalledClosedLimitLift(); + currentLiftPercent = lastLiftPercent; + if (openLimitLift < closedLimitLift) { + currentLift = openLimitLift + ((closedLimitLift - openLimitLift) * lastLiftPercent) / 100; + } else { + currentLift = openLimitLift - ((openLimitLift - closedLimitLift) * lastLiftPercent) / 100; + } + + currentTiltPercent = lastTiltPercent; + + Serial.printf( + "Window Covering limits configured: Lift [%d-%d cm], Tilt [%d-%d]\r\n", WindowBlinds.getInstalledOpenLimitLift(), + WindowBlinds.getInstalledClosedLimitLift(), WindowBlinds.getInstalledOpenLimitTilt(), WindowBlinds.getInstalledClosedLimitTilt() + ); + Serial.printf("Initial positions: Lift=%d cm (%d%%), Tilt=%d%%\r\n", currentLift, currentLiftPercent, currentTiltPercent); + + // Set callback functions + WindowBlinds.onOpen(fullOpen); + WindowBlinds.onClose(fullClose); + WindowBlinds.onGoToLiftPercentage(goToLiftPercentage); + WindowBlinds.onGoToTiltPercentage(goToTiltPercentage); + WindowBlinds.onStop(stopMotor); + + // Generic callback for Lift or Tilt change + WindowBlinds.onChange([](uint8_t liftPercent, uint8_t tiltPercent) { + Serial.printf("Window Covering changed: Lift=%d%%, Tilt=%d%%\r\n", liftPercent, tiltPercent); + visualizeWindowBlinds(liftPercent, tiltPercent); + return true; + }); + + // Matter beginning - Last step, after all EndPoints are initialized + Matter.begin(); + // This may be a restart of a already commissioned Matter accessory + if (Matter.isDeviceCommissioned()) { + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); + Serial.printf("Initial state: Lift=%d%%, Tilt=%d%%\r\n", WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage()); + // Update visualization based on initial state + visualizeWindowBlinds(WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage()); + } +} + +void loop() { + // Check Matter Window Covering Commissioning state, which may change during execution of loop() + if (!Matter.isDeviceCommissioned()) { + Serial.println(""); + Serial.println("Matter Node is not commissioned yet."); + Serial.println("Initiate the device discovery in your Matter environment."); + Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); + Serial.printf("Manual pairing code: %s\r\n", Matter.getManualPairingCode().c_str()); + Serial.printf("QR code URL: %s\r\n", Matter.getOnboardingQRCodeUrl().c_str()); + // waits for Matter Window Covering Commissioning. + uint32_t timeCount = 0; + while (!Matter.isDeviceCommissioned()) { + delay(100); + if ((timeCount++ % 50) == 0) { // 50*100ms = 5 sec + Serial.println("Matter Node not commissioned yet. Waiting for commissioning."); + } + } + Serial.printf("Initial state: Lift=%d%%, Tilt=%d%%\r\n", WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage()); + // Update visualization based on initial state + visualizeWindowBlinds(WindowBlinds.getLiftPercentage(), WindowBlinds.getTiltPercentage()); + Serial.println("Matter Node is commissioned and connected to the network. Ready for use."); + } + + // A button is also used to control the window covering + // Check if the button has been pressed + if (digitalRead(buttonPin) == LOW && !button_state) { + // deals with button debouncing + button_time_stamp = millis(); // record the time while the button is pressed. + button_state = true; // pressed. + } + + // Onboard User Button is used to manually change lift percentage or to decommission + uint32_t time_diff = millis() - button_time_stamp; + if (digitalRead(buttonPin) == HIGH && button_state && time_diff > debounceTime) { + // Button is released - cycle lift percentage by 20% + button_state = false; // released + uint8_t targetLiftPercent = currentLiftPercent; + // go to the closest next 20% or move 20% more + if ((targetLiftPercent % 20) != 0) { + targetLiftPercent = ((targetLiftPercent / 20) + 1) * 20; + } else { + targetLiftPercent += 20; + } + if (targetLiftPercent > 100) { + targetLiftPercent = 0; + } + Serial.printf("User button released. Setting lift to %d%%\r\n", targetLiftPercent); + WindowBlinds.setTargetLiftPercent100ths(targetLiftPercent * 100); + } + + // Onboard User Button is kept pressed for longer than 5 seconds in order to decommission matter node + if (button_state && time_diff > decommissioningTimeout) { + Serial.println("Decommissioning the Window Covering Matter Accessory. It shall be commissioned again."); + WindowBlinds.setLiftPercentage(0); // close the covering + Matter.decommission(); + button_time_stamp = millis(); // avoid running decommissioning again, reboot takes a second or so + } +} diff --git a/libraries/Matter/examples/MatterWindowCovering/README.md b/libraries/Matter/examples/MatterWindowCovering/README.md new file mode 100644 index 00000000000..8e18539c65a --- /dev/null +++ b/libraries/Matter/examples/MatterWindowCovering/README.md @@ -0,0 +1,238 @@ +# 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 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 (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 (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` (cm) based on actual motor position + - For tilt: Update `currentTiltPercent` (rotation percentage) based on actual motor rotation + - **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.) + - 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**: + + **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 + +- **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..050a80ff543 --- /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 diff --git a/libraries/Matter/keywords.txt b/libraries/Matter/keywords.txt index fc8ccf082f0..88c005976ca 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 @@ -195,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 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..fc293c5c2da --- /dev/null +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.cpp @@ -0,0 +1,871 @@ +// 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 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); + 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 + // 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); + } + } 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; + // 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; + + // 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..740729df43a --- /dev/null +++ b/libraries/Matter/src/MatterEndpoints/MatterWindowCovering.h @@ -0,0 +1,161 @@ +// 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 */