diff --git a/libraries/Matter/examples/matter_water_valve/matter_water_valve.ino b/libraries/Matter/examples/matter_water_valve/matter_water_valve.ino new file mode 100644 index 00000000..11736e78 --- /dev/null +++ b/libraries/Matter/examples/matter_water_valve/matter_water_valve.ino @@ -0,0 +1,89 @@ +/* + Matter water valve example + + The example shows how to create a water valve device with the Arduino Matter API. + + The example creates a Matter water valve device that can be opened (optionally for a set + duration) or closed from a Matter hub, and toggles the builtin LED based on its current state. + The device has to be commissioned to a Matter hub first. + + Compatible boards: + - Arduino Nano Matter + - SparkFun Thing Plus MGM240P + - xG24 Explorer Kit + - xG24 Dev Kit + - Seeed Studio XIAO MG24 (Sense) + + Author: Ludovic BOUÉ + */ +#include +#include + +MatterWaterValve matter_water_valve; + +void setup() +{ + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE); + Serial.begin(115200); + Matter.begin(); + matter_water_valve.begin(); + + Serial.println("Matter water valve"); + + if (!Matter.isDeviceCommissioned()) { + Serial.println("Matter device is not commissioned"); + Serial.println("Commission it to your Matter hub with the manual pairing code or QR code"); + Serial.printf("Manual pairing code: %s\n", Matter.getManualPairingCode().c_str()); + Serial.printf("QR code URL: %s\n", Matter.getOnboardingQRCodeUrl().c_str()); + } + while (!Matter.isDeviceCommissioned()) { + delay(200); + } + + Serial.println("Waiting for Thread network..."); + while (!Matter.isDeviceThreadConnected()) { + delay(200); + } + Serial.println("Connected to Thread network"); + + Serial.println("Waiting for Matter device discovery..."); + while (!matter_water_valve.is_online()) { + delay(200); + } + Serial.println("Matter device is now online"); +} + +void loop() +{ + // Run down the countdown of a timed open operation, log the remaining time, and close the valve once it elapses + static unsigned long last_tick_ms = 0; + if ((matter_water_valve.get_remaining_duration() > 0) && (millis() - last_tick_ms >= 1000)) { + last_tick_ms = millis(); + uint32_t remaining_duration = matter_water_valve.get_remaining_duration() - 1; + matter_water_valve.set_remaining_duration(remaining_duration); + Serial.printf("Water valve remaining duration: %lu s\n", (unsigned long)remaining_duration); + } + + // Log the default open duration whenever it's changed remotely, e.g. from a Matter hub + static uint32_t default_open_duration_prev = matter_water_valve.get_default_open_duration(); + uint32_t default_open_duration = matter_water_valve.get_default_open_duration(); + if (default_open_duration_prev != default_open_duration) { + Serial.printf("Water valve default open duration changed: %lu s\n", (unsigned long)default_open_duration); + default_open_duration_prev = default_open_duration; + } + + // Toggle the LED on/off based on the current valve state + static MatterWaterValve::valve_state_t state_prev = MatterWaterValve::valve_state_t::VALVE_STATE_CLOSED; + MatterWaterValve::valve_state_t state = matter_water_valve.get_current_state(); + if (state_prev != state) { + if (state == MatterWaterValve::valve_state_t::VALVE_STATE_OPEN) { + Serial.println("Water valve state: OPEN"); + digitalWrite(LED_BUILTIN, LED_BUILTIN_ACTIVE); + } else { + Serial.println("Water valve state: CLOSED"); + digitalWrite(LED_BUILTIN, LED_BUILTIN_INACTIVE); + } + state_prev = state; + } +} diff --git a/libraries/Matter/src/MatterEndpoint.h b/libraries/Matter/src/MatterEndpoint.h index dadaac92..ba872661 100644 --- a/libraries/Matter/src/MatterEndpoint.h +++ b/libraries/Matter/src/MatterEndpoint.h @@ -55,6 +55,7 @@ #define DEVICE_TYPE_DOOR_LOCK 0x000A #define DEVICE_TYPE_AIR_PURIFIER 0x002D #define DEVICE_TYPE_RAIN_SENSOR 0x0044 +#define DEVICE_TYPE_WATER_VALVE 0x0042 // Device Version for dynamic endpoints #define DEVICE_VERSION_DEFAULT 1 diff --git a/libraries/Matter/src/MatterWaterValve.cpp b/libraries/Matter/src/MatterWaterValve.cpp new file mode 100644 index 00000000..8d6bbcdc --- /dev/null +++ b/libraries/Matter/src/MatterWaterValve.cpp @@ -0,0 +1,359 @@ +/* + * This file is part of the Silicon Labs Arduino Core + * + * The MIT License (MIT) + * + * Copyright 2026 Silicon Laboratories Inc. www.silabs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "MatterWaterValve.h" +#include +#include +#include + +using namespace ::chip; +using namespace ::chip::Platform; +using namespace ::chip::Credentials; +using namespace ::chip::app::Clusters; +using namespace ::chip::app::Clusters::ValveConfigurationAndControl; +using namespace ::chip::app::DataModel; + +const EmberAfDeviceType gWaterValveDeviceTypes[] = { { DEVICE_TYPE_WATER_VALVE, DEVICE_VERSION_DEFAULT } }; + +constexpr CommandId waterValveIncomingCommands[] = { + app::Clusters::ValveConfigurationAndControl::Commands::Open::Id, + app::Clusters::ValveConfigurationAndControl::Commands::Close::Id, + kInvalidCommandId, +}; + +// Handles the Valve Configuration and Control cluster's Open and Close commands. +class WaterValveCommandHandler : public app::CommandHandlerInterface { +public: + WaterValveCommandHandler(EndpointId endpoint, DeviceWaterValve* device) : + app::CommandHandlerInterface(MakeOptional(endpoint), ValveConfigurationAndControl::Id), + device(device) + { + ; + } + + void InvokeCommand(HandlerContext& handlerContext) override + { + HandleCommand( + handlerContext, [this](HandlerContext& ctx, const Commands::Open::DecodableType& req) { + if (req.openDuration.HasValue() && !req.openDuration.Value().IsNull()) { + this->device->Open(true, req.openDuration.Value().Value()); + } else { + this->device->Open(false, 0); + } + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Protocols::InteractionModel::Status::Success); + }); + HandleCommand( + handlerContext, [this](HandlerContext& ctx, const Commands::Close::DecodableType& req) { + (void)req; + this->device->Close(); + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Protocols::InteractionModel::Status::Success); + }); + } + +private: + DeviceWaterValve* device; +}; + +// Valve Configuration and Control cluster attributes +DECLARE_DYNAMIC_ATTRIBUTE_LIST_BEGIN(waterValveConfigurationAndControlAttrs) +DECLARE_DYNAMIC_ATTRIBUTE(ValveConfigurationAndControl::Attributes::OpenDuration::Id, INT32U, 4, ATTRIBUTE_MASK_NULLABLE), /* Open Duration */ +DECLARE_DYNAMIC_ATTRIBUTE(ValveConfigurationAndControl::Attributes::DefaultOpenDuration::Id, INT32U, 4, ATTRIBUTE_MASK_WRITABLE_NULLABLE), /* Default Open Duration */ +DECLARE_DYNAMIC_ATTRIBUTE(ValveConfigurationAndControl::Attributes::RemainingDuration::Id, INT32U, 4, ATTRIBUTE_MASK_NULLABLE), /* Remaining Duration */ +DECLARE_DYNAMIC_ATTRIBUTE(ValveConfigurationAndControl::Attributes::CurrentState::Id, ENUM8, 1, ATTRIBUTE_MASK_NULLABLE), /* Current State */ +DECLARE_DYNAMIC_ATTRIBUTE(ValveConfigurationAndControl::Attributes::TargetState::Id, ENUM8, 1, ATTRIBUTE_MASK_NULLABLE), /* Target State */ +DECLARE_DYNAMIC_ATTRIBUTE(ValveConfigurationAndControl::Attributes::ValveFault::Id, BITMAP16, 2, 0), /* Valve Fault */ +DECLARE_DYNAMIC_ATTRIBUTE(ValveConfigurationAndControl::Attributes::FeatureMap::Id, BITMAP32, 4, 0), /* FeatureMap */ +DECLARE_DYNAMIC_ATTRIBUTE_LIST_END(); /* ClusterRevision auto added by LIST_END */ + +// Water valve endpoint cluster list +DECLARE_DYNAMIC_CLUSTER_LIST_BEGIN(waterValveEndpointClusters) +DECLARE_DYNAMIC_CLUSTER(ValveConfigurationAndControl::Id, waterValveConfigurationAndControlAttrs, ZAP_CLUSTER_MASK(SERVER), waterValveIncomingCommands, nullptr), +DECLARE_DYNAMIC_CLUSTER(Descriptor::Id, descriptorAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), +DECLARE_DYNAMIC_CLUSTER(BridgedDeviceBasicInformation::Id, bridgedDeviceBasicAttrs, ZAP_CLUSTER_MASK(SERVER), nullptr, nullptr), +DECLARE_DYNAMIC_CLUSTER(Identify::Id, identifyAttrs, ZAP_CLUSTER_MASK(SERVER), identifyIncomingCommands, nullptr) +DECLARE_DYNAMIC_CLUSTER_LIST_END; + +/***************************************************************************//** + * Constructor for MatterWaterValve + ******************************************************************************/ +MatterWaterValve::MatterWaterValve() : + water_valve_device(nullptr), + device_endpoint(nullptr), + endpoint_dataversion_storage(nullptr), + command_handler(nullptr), + initialized(false) +{ + ; +} + +/***************************************************************************//** + * Destructor for MatterWaterValve + ******************************************************************************/ +MatterWaterValve::~MatterWaterValve() +{ + this->end(); +} + +/***************************************************************************//** + * Initializes the MatterWaterValve instance + * + * @return true if the initialization succeeded, false otherwise + ******************************************************************************/ +bool MatterWaterValve::begin() +{ + if (this->initialized) { + return false; + } + + // Create new device + DeviceWaterValve* new_water_valve_device = new (std::nothrow)DeviceWaterValve("Water Valve"); + if (new_water_valve_device == nullptr) { + return false; + } + new_water_valve_device->SetReachable(true); + new_water_valve_device->SetProductName("Water Valve"); + + // Set the device instance pointer in the base class + this->base_matter_device = new_water_valve_device; + + // Create new endpoint + EmberAfEndpointType* new_endpoint = (EmberAfEndpointType*)malloc(sizeof(EmberAfEndpointType)); + if (new_endpoint == nullptr) { + delete(new_water_valve_device); + return false; + } + new_endpoint->cluster = waterValveEndpointClusters; + new_endpoint->clusterCount = ArraySize(waterValveEndpointClusters); + new_endpoint->endpointSize = 0; + + // Create data version storage for the endpoint + size_t dataversion_size = ArraySize(waterValveEndpointClusters) * sizeof(DataVersion); + DataVersion* new_sensor_data_version = (DataVersion*)malloc(dataversion_size); + if (new_sensor_data_version == nullptr) { + delete(new_water_valve_device); + free(new_endpoint); + return false; + } + + // Add new endpoint + int result = AddDeviceEndpoint(new_water_valve_device, + new_endpoint, + Span(gWaterValveDeviceTypes), + Span(new_sensor_data_version, ArraySize(waterValveEndpointClusters)), + 1); + if (result < 0) { + delete(new_water_valve_device); + free(new_endpoint); + free(new_sensor_data_version); + return false; + } + + this->water_valve_device = new_water_valve_device; + this->device_endpoint = new_endpoint; + this->endpoint_dataversion_storage = new_sensor_data_version; + + // Register the Valve Configuration and Control cluster's Open/Close command handler - + // this can't be serviced through the plain ember external-attribute mechanism. + EndpointId assigned_endpoint_id = new_water_valve_device->GetEndpointId(); + this->command_handler = new (std::nothrow)WaterValveCommandHandler(assigned_endpoint_id, new_water_valve_device); + if (this->command_handler != nullptr) { + app::CommandHandlerInterfaceRegistry::Instance().RegisterCommandHandler(this->command_handler); + } + + this->initialized = true; + return true; +} + +/***************************************************************************//** + * Deinitializes the MatterWaterValve instance + ******************************************************************************/ +void MatterWaterValve::end() +{ + if (!this->initialized) { + return; + } + if (this->command_handler != nullptr) { + (void)app::CommandHandlerInterfaceRegistry::Instance().UnregisterCommandHandler(this->command_handler); + delete(this->command_handler); + this->command_handler = nullptr; + } + (void)RemoveDeviceEndpoint(this->water_valve_device); + free(this->device_endpoint); + free(this->endpoint_dataversion_storage); + delete(this->water_valve_device); + this->initialized = false; +} + +/***************************************************************************//** + * Opens the valve indefinitely, or until the DefaultOpenDuration times it out if one is set + ******************************************************************************/ +void MatterWaterValve::open() +{ + if (!this->initialized) { + return; + } + PlatformMgr().LockChipStack(); + this->water_valve_device->Open(false, 0); + PlatformMgr().UnlockChipStack(); +} + +/***************************************************************************//** + * Opens the valve for the given duration in seconds, after which it closes automatically - + * requires the sketch to keep calling set_remaining_duration() to run the countdown down to 0 + * + * @param[in] duration_seconds the duration in seconds after which the valve should close + ******************************************************************************/ +void MatterWaterValve::open(uint32_t duration_seconds) +{ + if (!this->initialized) { + return; + } + PlatformMgr().LockChipStack(); + this->water_valve_device->Open(true, duration_seconds); + PlatformMgr().UnlockChipStack(); +} + +/***************************************************************************//** + * Closes the valve + ******************************************************************************/ +void MatterWaterValve::close() +{ + if (!this->initialized) { + return; + } + PlatformMgr().LockChipStack(); + this->water_valve_device->Close(); + PlatformMgr().UnlockChipStack(); +} + +/***************************************************************************//** + * Gets the valve's current state + * + * @return the current valve state + ******************************************************************************/ +MatterWaterValve::valve_state_t MatterWaterValve::get_current_state() +{ + return (valve_state_t)this->water_valve_device->GetCurrentState(); +} + +/***************************************************************************//** + * Gets the valve's target state + * + * @return the target valve state + ******************************************************************************/ +MatterWaterValve::valve_state_t MatterWaterValve::get_target_state() +{ + return (valve_state_t)this->water_valve_device->GetTargetState(); +} + +/***************************************************************************//** + * Gets the duration in seconds used for the current/last timed open operation + * The value is 0 if the valve was opened indefinitely. + * + * @return the open duration in seconds + ******************************************************************************/ +uint32_t MatterWaterValve::get_open_duration() +{ + return this->water_valve_device->GetOpenDuration(); +} + +/***************************************************************************//** + * Gets the default open duration in seconds used when Open is called without an explicit duration + * The value is 0 if no default duration is configured (the valve opens indefinitely by default). + * + * @return the default open duration in seconds + ******************************************************************************/ +uint32_t MatterWaterValve::get_default_open_duration() +{ + return this->water_valve_device->GetDefaultOpenDuration(); +} + +/***************************************************************************//** + * Sets the default open duration in seconds used when Open is called without an explicit duration + * + * @param[in] duration_seconds the default open duration in seconds, 0 to disable it + ******************************************************************************/ +void MatterWaterValve::set_default_open_duration(uint32_t duration_seconds) +{ + if (!this->initialized) { + return; + } + PlatformMgr().LockChipStack(); + this->water_valve_device->SetDefaultOpenDuration(duration_seconds); + PlatformMgr().UnlockChipStack(); +} + +/***************************************************************************//** + * Gets the remaining duration in seconds of the current timed open operation + * + * @return the remaining duration in seconds + ******************************************************************************/ +uint32_t MatterWaterValve::get_remaining_duration() +{ + return this->water_valve_device->GetRemainingDuration(); +} + +/***************************************************************************//** + * Sets the remaining duration in seconds of the current timed open operation + * Reaching 0 while the valve is open with a timed duration active automatically closes it - + * call this periodically from the sketch's loop() to run the countdown started by open(duration_seconds). + * + * @param[in] remaining_duration_seconds the remaining duration in seconds + ******************************************************************************/ +void MatterWaterValve::set_remaining_duration(uint32_t remaining_duration_seconds) +{ + if (!this->initialized) { + return; + } + PlatformMgr().LockChipStack(); + this->water_valve_device->SetRemainingDuration(remaining_duration_seconds); + PlatformMgr().UnlockChipStack(); +} + +/***************************************************************************//** + * Gets the valve's currently reported fault bitmap + * + * @return the valve fault bitmap, a combination of the valve_fault_t values + ******************************************************************************/ +uint16_t MatterWaterValve::get_valve_fault() +{ + return this->water_valve_device->GetValveFault(); +} + +/***************************************************************************//** + * Sets the valve's fault bitmap + * + * @param[in] valve_fault the valve fault to set + ******************************************************************************/ +void MatterWaterValve::set_valve_fault(valve_fault_t valve_fault) +{ + if (!this->initialized) { + return; + } + PlatformMgr().LockChipStack(); + this->water_valve_device->SetValveFault((uint16_t)valve_fault); + PlatformMgr().UnlockChipStack(); +} diff --git a/libraries/Matter/src/MatterWaterValve.h b/libraries/Matter/src/MatterWaterValve.h new file mode 100644 index 00000000..2bb43c95 --- /dev/null +++ b/libraries/Matter/src/MatterWaterValve.h @@ -0,0 +1,89 @@ +/* + * This file is part of the Silicon Labs Arduino Core + * + * The MIT License (MIT) + * + * Copyright 2026 Silicon Laboratories Inc. www.silabs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef MATTER_WATER_VALVE_H +#define MATTER_WATER_VALVE_H + +#include "Matter.h" +#include "devices/DeviceWaterValve.h" +#include +#include + +using namespace chip; +using namespace ::chip::DeviceLayer; + +class WaterValveCommandHandler; + +class MatterWaterValve : public ArduinoMatterAppliance { +public: + + enum valve_state_t { + VALVE_STATE_CLOSED = 0, + VALVE_STATE_OPEN = 1, + VALVE_STATE_TRANSITIONING = 2 + }; + + enum valve_fault_t { + VALVE_FAULT_GENERAL_FAULT = 0x01, + VALVE_FAULT_BLOCKED = 0x02, + VALVE_FAULT_LEAKING = 0x04, + VALVE_FAULT_NOT_CONNECTED = 0x08, + VALVE_FAULT_SHORT_CIRCUIT = 0x10, + VALVE_FAULT_CURRENT_EXCEEDED = 0x20 + }; + + MatterWaterValve(); + ~MatterWaterValve(); + bool begin(); + void end(); + + void open(); + void open(uint32_t duration_seconds); + void close(); + + valve_state_t get_current_state(); + valve_state_t get_target_state(); + + uint32_t get_open_duration(); + + uint32_t get_default_open_duration(); + void set_default_open_duration(uint32_t duration_seconds); + + uint32_t get_remaining_duration(); + void set_remaining_duration(uint32_t remaining_duration_seconds); + + uint16_t get_valve_fault(); + void set_valve_fault(valve_fault_t valve_fault); + +private: + DeviceWaterValve* water_valve_device; + EmberAfEndpointType* device_endpoint; + DataVersion* endpoint_dataversion_storage; + WaterValveCommandHandler* command_handler; + bool initialized; +}; + +#endif // MATTER_WATER_VALVE_H diff --git a/libraries/Matter/src/devices/DeviceWaterValve.cpp b/libraries/Matter/src/devices/DeviceWaterValve.cpp new file mode 100644 index 00000000..b2e2e549 --- /dev/null +++ b/libraries/Matter/src/devices/DeviceWaterValve.cpp @@ -0,0 +1,296 @@ +/* + * This file is part of the Silicon Labs Arduino Core + * + * The MIT License (MIT) + * + * Copyright 2026 Silicon Laboratories Inc. www.silabs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DeviceWaterValve.h" + +// The Matter spec requires OpenDuration/DefaultOpenDuration/RemainingDuration to be at least 1s when +// set, so "no duration configured" (internal 0) must be reported/accepted as null on the wire, using +// the ZCL convention of an all-ones value to represent null for unsigned integer attributes. +static constexpr uint32_t kNullDurationValue = 0xFFFFFFFFu; + +DeviceWaterValve::DeviceWaterValve(const char* device_name) : + Device(device_name), + current_state(VALVE_STATE_CLOSED), + target_state(VALVE_STATE_CLOSED), + open_duration(0), + default_open_duration(0), + remaining_duration(0), + valve_fault(0) +{ + this->SetDeviceType(device_type_t::kDeviceType_WaterValve); +} + +uint8_t DeviceWaterValve::GetCurrentState() +{ + return this->current_state; +} + +uint8_t DeviceWaterValve::GetTargetState() +{ + return this->target_state; +} + +uint32_t DeviceWaterValve::GetOpenDuration() +{ + return this->open_duration; +} + +uint32_t DeviceWaterValve::GetDefaultOpenDuration() +{ + return this->default_open_duration; +} + +void DeviceWaterValve::SetDefaultOpenDuration(uint32_t default_open_duration) +{ + bool changed = this->default_open_duration != default_open_duration; + this->default_open_duration = default_open_duration; + + if (changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_DefaultOpenDurationValue); + CallDeviceChangeCallback(); + } +} + +uint32_t DeviceWaterValve::GetRemainingDuration() +{ + return this->remaining_duration; +} + +void DeviceWaterValve::SetRemainingDuration(uint32_t remaining_duration) +{ + bool changed = this->remaining_duration != remaining_duration; + this->remaining_duration = remaining_duration; + + if (changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_RemainingDurationValue); + CallDeviceChangeCallback(); + } + + // A running timed-open countdown reaching zero closes the valve automatically. + if ((remaining_duration == 0) && (this->current_state == VALVE_STATE_OPEN) && (this->open_duration != 0)) { + this->Close(); + } +} + +uint16_t DeviceWaterValve::GetValveFault() +{ + return this->valve_fault; +} + +void DeviceWaterValve::SetValveFault(uint16_t valve_fault) +{ + bool changed = this->valve_fault != valve_fault; + ChipLogProgress(DeviceLayer, "WaterValveDevice[%s]: new valve fault='%u'", this->device_name, valve_fault); + this->valve_fault = valve_fault; + + if (changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_ValveFaultValue); + CallDeviceChangeCallback(); + } +} + +void DeviceWaterValve::Open(bool has_duration, uint32_t open_duration_seconds) +{ + ChipLogProgress(DeviceLayer, "WaterValveDevice[%s]: opening", this->device_name); + + uint32_t new_open_duration = 0; + if (has_duration) { + new_open_duration = open_duration_seconds; + } else if (this->default_open_duration != 0) { + new_open_duration = this->default_open_duration; + } + + bool open_duration_changed = this->open_duration != new_open_duration; + this->open_duration = new_open_duration; + + bool remaining_duration_changed = this->remaining_duration != new_open_duration; + this->remaining_duration = new_open_duration; + + bool current_state_changed = this->current_state != VALVE_STATE_OPEN; + this->current_state = VALVE_STATE_OPEN; + + bool target_state_changed = this->target_state != VALVE_STATE_OPEN; + this->target_state = VALVE_STATE_OPEN; + + if (current_state_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_CurrentStateValue); + } + if (target_state_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_TargetStateValue); + } + if (open_duration_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_OpenDurationValue); + } + if (remaining_duration_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_RemainingDurationValue); + } + CallDeviceChangeCallback(); +} + +void DeviceWaterValve::Close() +{ + ChipLogProgress(DeviceLayer, "WaterValveDevice[%s]: closing", this->device_name); + + bool current_state_changed = this->current_state != VALVE_STATE_CLOSED; + this->current_state = VALVE_STATE_CLOSED; + + bool target_state_changed = this->target_state != VALVE_STATE_CLOSED; + this->target_state = VALVE_STATE_CLOSED; + + bool open_duration_changed = this->open_duration != 0; + this->open_duration = 0; + + bool remaining_duration_changed = this->remaining_duration != 0; + this->remaining_duration = 0; + + if (current_state_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_CurrentStateValue); + } + if (target_state_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_TargetStateValue); + } + if (open_duration_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_OpenDurationValue); + } + if (remaining_duration_changed) { + this->HandleWaterValveDeviceStatusChanged(kChanged_RemainingDurationValue); + } + CallDeviceChangeCallback(); +} + +uint32_t DeviceWaterValve::GetValveConfigurationAndControlClusterFeatureMap() +{ + return this->valve_configuration_and_control_cluster_feature_map; +} + +uint16_t DeviceWaterValve::GetValveConfigurationAndControlClusterRevision() +{ + return this->valve_configuration_and_control_cluster_revision; +} + +CHIP_ERROR DeviceWaterValve::HandleReadEmberAfAttribute(ClusterId clusterId, + chip::AttributeId attributeId, + uint8_t* buffer, + uint16_t maxReadLength) +{ + if (!this->reachable) { + return CHIP_ERROR_INTERNAL; + } + + using namespace ::chip::app::Clusters; + ChipLogProgress(DeviceLayer, "HandleReadWaterValveAttribute: clusterId=%lu attrId=%ld", clusterId, attributeId); + + if (clusterId == chip::app::Clusters::BridgedDeviceBasicInformation::Id) { + return this->HandleReadBridgedDeviceBasicAttribute(clusterId, attributeId, buffer, maxReadLength); + } + + if (clusterId == chip::app::Clusters::ValveConfigurationAndControl::Id) { + using namespace ::chip::app::Clusters::ValveConfigurationAndControl::Attributes; + if ((attributeId == OpenDuration::Id) && (maxReadLength == 4)) { + uint32_t openDuration = this->GetOpenDuration(); + uint32_t wireOpenDuration = (openDuration == 0) ? kNullDurationValue : openDuration; + memcpy(buffer, &wireOpenDuration, sizeof(wireOpenDuration)); + } else if ((attributeId == DefaultOpenDuration::Id) && (maxReadLength == 4)) { + uint32_t defaultOpenDuration = this->GetDefaultOpenDuration(); + uint32_t wireDefaultOpenDuration = (defaultOpenDuration == 0) ? kNullDurationValue : defaultOpenDuration; + memcpy(buffer, &wireDefaultOpenDuration, sizeof(wireDefaultOpenDuration)); + } else if ((attributeId == RemainingDuration::Id) && (maxReadLength == 4)) { + uint32_t remainingDuration = this->GetRemainingDuration(); + uint32_t wireRemainingDuration = (remainingDuration == 0) ? kNullDurationValue : remainingDuration; + memcpy(buffer, &wireRemainingDuration, sizeof(wireRemainingDuration)); + } else if ((attributeId == CurrentState::Id) && (maxReadLength == 1)) { + uint8_t currentState = this->GetCurrentState(); + memcpy(buffer, ¤tState, sizeof(currentState)); + } else if ((attributeId == TargetState::Id) && (maxReadLength == 1)) { + uint8_t targetState = this->GetTargetState(); + memcpy(buffer, &targetState, sizeof(targetState)); + } else if ((attributeId == ValveFault::Id) && (maxReadLength == 2)) { + uint16_t valveFault = this->GetValveFault(); + memcpy(buffer, &valveFault, sizeof(valveFault)); + } else if ((attributeId == ValveConfigurationAndControl::Attributes::FeatureMap::Id) && (maxReadLength == 4)) { + uint32_t featureMap = this->GetValveConfigurationAndControlClusterFeatureMap(); + memcpy(buffer, &featureMap, sizeof(featureMap)); + } else if ((attributeId == ValveConfigurationAndControl::Attributes::ClusterRevision::Id) && (maxReadLength == 2)) { + uint16_t clusterRevision = this->GetValveConfigurationAndControlClusterRevision(); + memcpy(buffer, &clusterRevision, sizeof(clusterRevision)); + } else { + return CHIP_ERROR_INVALID_ARGUMENT; + } + return CHIP_NO_ERROR; + } + + return CHIP_ERROR_INVALID_ARGUMENT; +} + +CHIP_ERROR DeviceWaterValve::HandleWriteEmberAfAttribute(ClusterId clusterId, + chip::AttributeId attributeId, + uint8_t* buffer) +{ + if (!this->reachable) { + return CHIP_ERROR_INTERNAL; + } + + using namespace ::chip::app::Clusters; + ChipLogProgress(DeviceLayer, "HandleWriteWaterValveAttribute: clusterId=%lu attrId=%ld", clusterId, attributeId); + + if (clusterId != chip::app::Clusters::ValveConfigurationAndControl::Id) { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + using namespace ::chip::app::Clusters::ValveConfigurationAndControl::Attributes; + if (attributeId == DefaultOpenDuration::Id) { + uint32_t wireDefaultOpenDuration = *((uint32_t*)buffer); + this->SetDefaultOpenDuration((wireDefaultOpenDuration == kNullDurationValue) ? 0 : wireDefaultOpenDuration); + } else { + return CHIP_ERROR_INVALID_ARGUMENT; + } + + return CHIP_NO_ERROR; +} + +void DeviceWaterValve::HandleWaterValveDeviceStatusChanged(Changed_t itemChangedMask) +{ + using namespace ::chip::app::Clusters; + + if (itemChangedMask & kChanged_CurrentStateValue) { + ScheduleMatterReportingCallback(this->endpoint_id, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::CurrentState::Id); + } + if (itemChangedMask & kChanged_TargetStateValue) { + ScheduleMatterReportingCallback(this->endpoint_id, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::TargetState::Id); + } + if (itemChangedMask & kChanged_OpenDurationValue) { + ScheduleMatterReportingCallback(this->endpoint_id, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::OpenDuration::Id); + } + if (itemChangedMask & kChanged_DefaultOpenDurationValue) { + ScheduleMatterReportingCallback(this->endpoint_id, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::DefaultOpenDuration::Id); + } + if (itemChangedMask & kChanged_RemainingDurationValue) { + ScheduleMatterReportingCallback(this->endpoint_id, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::RemainingDuration::Id); + } + if (itemChangedMask & kChanged_ValveFaultValue) { + ScheduleMatterReportingCallback(this->endpoint_id, ValveConfigurationAndControl::Id, ValveConfigurationAndControl::Attributes::ValveFault::Id); + } +} diff --git a/libraries/Matter/src/devices/DeviceWaterValve.h b/libraries/Matter/src/devices/DeviceWaterValve.h new file mode 100644 index 00000000..30358dca --- /dev/null +++ b/libraries/Matter/src/devices/DeviceWaterValve.h @@ -0,0 +1,93 @@ +/* + * This file is part of the Silicon Labs Arduino Core + * + * The MIT License (MIT) + * + * Copyright 2026 Silicon Laboratories Inc. www.silabs.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include "MatterDevice.h" + +class DeviceWaterValve : public Device +{ +public: + enum Changed_t{ + kChanged_CurrentStateValue = kChanged_Last << 1, + kChanged_TargetStateValue = kChanged_Last << 2, + kChanged_OpenDurationValue = kChanged_Last << 3, + kChanged_DefaultOpenDurationValue = kChanged_Last << 4, + kChanged_RemainingDurationValue = kChanged_Last << 5, + kChanged_ValveFaultValue = kChanged_Last << 6, + } Changed; + + // ValveStateEnum values + enum valve_state_t { + VALVE_STATE_CLOSED = 0, + VALVE_STATE_OPEN = 1, + VALVE_STATE_TRANSITIONING = 2 + }; + + DeviceWaterValve(const char* device_name); + + uint8_t GetCurrentState(); + uint8_t GetTargetState(); + + uint32_t GetOpenDuration(); + uint32_t GetDefaultOpenDuration(); + void SetDefaultOpenDuration(uint32_t default_open_duration); + uint32_t GetRemainingDuration(); + void SetRemainingDuration(uint32_t remaining_duration); + + uint16_t GetValveFault(); + void SetValveFault(uint16_t valve_fault); + + // Opens the valve. If has_duration is false the valve stays open until Close() + // is called or the DefaultOpenDuration attribute times it out. + void Open(bool has_duration, uint32_t open_duration_seconds); + void Close(); + + uint32_t GetValveConfigurationAndControlClusterFeatureMap(); + uint16_t GetValveConfigurationAndControlClusterRevision(); + + CHIP_ERROR HandleReadEmberAfAttribute(ClusterId clusterId, + chip::AttributeId attributeId, + uint8_t* buffer, + uint16_t maxReadLength) override; + + CHIP_ERROR HandleWriteEmberAfAttribute(ClusterId clusterId, + chip::AttributeId attributeId, + uint8_t* buffer) override; + +private: + void HandleWaterValveDeviceStatusChanged(Changed_t itemChangedMask); + + uint8_t current_state; + uint8_t target_state; + uint32_t open_duration; // 0 means "no timed duration" (open indefinitely) + uint32_t default_open_duration; // 0 means "no default duration" configured + uint32_t remaining_duration; + uint16_t valve_fault; + + static const uint32_t valve_configuration_and_control_cluster_feature_map = 0u; + static const uint16_t valve_configuration_and_control_cluster_revision = 1u; +}; diff --git a/libraries/Matter/src/devices/MatterDevice.h b/libraries/Matter/src/devices/MatterDevice.h index 93225e30..ca9c7114 100644 --- a/libraries/Matter/src/devices/MatterDevice.h +++ b/libraries/Matter/src/devices/MatterDevice.h @@ -73,6 +73,7 @@ class Device kDeviceType_COSensor = 0x0012, kDeviceType_TVOCSensor = 0x0013, kDeviceType_PowerSource = 0x0014, + kDeviceType_WaterValve = 0x0016, kDeviceType_Unspecified = 0xFFFF }; diff --git a/test/build/test_build.py b/test/build/test_build.py index bea246ee..c5495b35 100644 --- a/test/build/test_build.py +++ b/test/build/test_build.py @@ -254,6 +254,7 @@ "../../libraries/Matter/examples/matter_temp_sensor/matter_temp_sensor.ino": all_matter, "../../libraries/Matter/examples/matter_thermostat/matter_thermostat.ino": all_matter, "../../libraries/Matter/examples/matter_tvoc_sensor/matter_tvoc_sensor.ino": all_matter, + "../../libraries/Matter/examples/matter_water_valve/matter_water_valve.ino": all_matter, "../../libraries/Matter/examples/matter_window_covering/matter_window_covering.ino": all_matter, "../../libraries/Matter/examples/nano_matter_certified_lightbulb/nano_matter_certified_lightbulb.ino": nano_matter_matter, "../../libraries/Matter/examples/nano_matter_lightbulb_color/nano_matter_lightbulb_color.ino": nano_matter_matter,