diff --git a/doc/Applikationsbeschreibung-SmartHomeBridge.md b/doc/Applikationsbeschreibung-SmartHomeBridge.md index 0b8bf26..7de324f 100644 --- a/doc/Applikationsbeschreibung-SmartHomeBridge.md +++ b/doc/Applikationsbeschreibung-SmartHomeBridge.md @@ -16,12 +16,34 @@ In den Basiseinstellungen wird die Auswahl der angebunden Smart Home System vorg Name der Smart Home Bridge. Bei manchen Smart Home Systemen wird dieser beim Koppeln angezeigt. - +## Unterstütze Smarthomesysteme + Folgende Smart Home Systeme werden unterstüzt: -#### Apple HomeKit +### Matter + +Über die Matter Unterstützung können KNX Geräte über Matter fähige Geräte gesteuert werden. +Z.B. Google Home, Apple Home + + + +#### Kopplungscode + +Über Apple HomeKit oder Google Home können die KNX Geräte gesteuert werden. +Der Kopplungscode wird nur beim erstmaligen Koppeln der Bridge verwendet. +Das Koppeln von weiteren Apps erfolgt in der App, in der die erste Kopplung durchgeführt wurde. + +Koppelung in Apple Home: + +- "Gerät hinzufügen" wählen +- "Weitere Optionen..." wählen +- Nun sollte die Bridge sichtbar sein. Den Kopplungscode der in ETS eingestellt wurde (Standardwert 29710235) eingeben und die Meldung das es sich um ein nicht zertifiziertes Gerät handelt bestätigen. +- Danach den Setup-Wizard für alle Geräte durchführen. + + +### Apple HomeKit Über Apple HomeKit können die KNX Geräte gesteuert werden. Es werden bis zu 149 Geräte unterstützt, jedoch derzeit nur maximal 60 empfohlen. Das Limit ergibt sich aus eine Beschränkung der Anzahl der Accessories bei einem HomeKit Gerät mit maximal 150. Eines davon repräsentiert das Bridge Gerät. @@ -34,6 +56,21 @@ Nachdem die Bridge über ETS programmiert wurde, kann sie mit Apple Homekit verb Sollte nachträglich eine Gerätetype sich ändern, z.B. eine Lampe wird zu einer Jalousie, oder eine Untertype ändert sich z.B. ein CO2 Sensor wird zu einem Kontakt, muss zuerst das Gerät deaktiviert werden. Danach die in der Home App warten bis das Gerät verschwunden ist und danach kann das Gerät wieder aktiviert werden. + + +#### Kopplungscode + +Über Apple HomeKit können die KNX Geräte gesteuert werden. Es werden bis zu 149 Geräte unterstützt. Das Limit ergibt sich aus eine Beschränkung der Anzahl der Accessories bei einem HomeKit Gerät mit maximal 150. Eines davon repräsentiert das Bridge Gerät. + +Nachdem die Bridge über ETS programmiert wurde, kann sie mit Apple Homekit verbunden: + +- "Gerät hinzufügen" wählen +- "Weitere Optionen..." wählen +- Nun sollte die Bridge sichtbar sein. Den Kopplungscode der in ETS eingestellt wurde (Standardwert 46637726) eingeben und die Meldung das es sich um ein nicht zertifiziertes Gerät handelt bestätigen. +- Danach den Setup-Wizard für alle Geräte durchführen. + +Sollte nachträglich eine Gerätetype sich ändern, z.B. eine Lampe wird zu einer Jalousie, oder eine Untertype ändert sich z.B. ein CO2 Sensor wird zu einem Kontakt, muss zuerst das Gerät deaktiviert werden. Danach die in der Home App warten bis das Gerät verschwunden ist und danach kann das Gerät wieder aktiviert werden. + ### Hue Emulation (Nur für Alexa) @@ -62,20 +99,6 @@ Die Philips HUE Emulation unterstützt nur Lampen. Geräte mit dieser Einstellun Die Philips HUE Emulation unterstützt nur Lampen. Geräte mit dieser Einstellung können aber als Dimmbare Lampe in HUE emuliert werde, um trotzdem eine Steuerung über die Smart Home Bridge zu Ermöglichen. - -### Kopplungscode - -Über Apple HomeKit können die KNX Geräte gesteuert werden. Es werden bis zu 149 Geräte unterstützt. Das Limit ergibt sich aus eine Beschränkung der Anzahl der Accessories bei einem HomeKit Gerät mit maximal 150. Eines davon repräsentiert das Bridge Gerät. - -Nachdem die Bridge über ETS programmiert wurde, kann sie mit Apple Homekit verbunden: - -- "Gerät hinzufügen" wählen -- "Weitere Optionen..." wählen -- Nun sollte die Bridge sichtbar sein. Den Kopplungscode der in ETS eingestellt wurde (Standardwert 46637726) eingeben und die Meldung das es sich um ein nicht zertifiziertes Gerät handelt bestätigen. -- Danach den Setup-Wizard für alle Geräte durchführen. - -Sollte nachträglich eine Gerätetype sich ändern, z.B. eine Lampe wird zu einer Jalousie, oder eine Untertype ändert sich z.B. ein CO2 Sensor wird zu einem Kontakt, muss zuerst das Gerät deaktiviert werden. Danach die in der Home App warten bis das Gerät verschwunden ist und danach kann das Gerät wieder aktiviert werden. - # Gerätetypen diff --git a/src/Alarm/HomeKitAlarm.cpp b/src/Alarm/HomeKitAlarm.cpp index a52dc44..185a50d 100644 --- a/src/Alarm/HomeKitAlarm.cpp +++ b/src/Alarm/HomeKitAlarm.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitAlarm.h" HomeKitAlarm::HomeKitAlarm(int device) : diff --git a/src/Alarm/KnxChannelAlarm.cpp b/src/Alarm/KnxChannelAlarm.cpp index 7d87fa3..4fc1e33 100644 --- a/src/Alarm/KnxChannelAlarm.cpp +++ b/src/Alarm/KnxChannelAlarm.cpp @@ -28,8 +28,23 @@ void KnxChannelAlarm::add(AlarmBridge* sensorBridge) { sensorBridges.push_back(sensorBridge); sensorBridge->initialize(this); - auto value = mainFunctionValue(); - sensorBridge->setDetected(value); +} + +void KnxChannelAlarm::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto sensorBridge = static_cast(bridge); + sensorBridge->setDetected(mainFunctionValue()); +} + +void KnxChannelAlarm::syncAllBridgeStates() +{ + for (auto it = sensorBridges.begin(); it != sensorBridges.end(); ++it) + { + syncBridgeState(*it); + } } void KnxChannelAlarm::remove(AlarmBridge* sensorBridge) diff --git a/src/Alarm/KnxChannelAlarm.h b/src/Alarm/KnxChannelAlarm.h index 30d40fc..0e92ce9 100644 --- a/src/Alarm/KnxChannelAlarm.h +++ b/src/Alarm/KnxChannelAlarm.h @@ -31,6 +31,8 @@ class KnxChannelAlarm : public KnxChannelBase void remove(AlarmBridge* sensorBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; AlarmType getAlarmType(); protected: virtual void setup() override; diff --git a/src/Alarm/MatterAlarm.cpp b/src/Alarm/MatterAlarm.cpp new file mode 100644 index 0000000..7cd83f6 --- /dev/null +++ b/src/Alarm/MatterAlarm.cpp @@ -0,0 +1,89 @@ +#include "Alarm/MatterAlarm.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +MatterAlarmBridge::MatterAlarmBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterAlarmBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + uint32_t matterType = esp_matter::endpoint::occupancy_sensor::get_device_type_id(); + switch (_channel->getAlarmType()) + { + case AlarmTypeContact: + matterType = esp_matter::endpoint::contact_sensor::get_device_type_id(); + break; + case AlarmTypeCarbonMonoxid: + case AlarmTypeSmoke: + matterType = esp_matter::endpoint::smoke_co_alarm::get_device_type_id(); + break; + case AlarmTypeCarbonDioxid: + matterType = esp_matter::endpoint::air_quality_sensor::get_device_type_id(); + break; + case AlarmTypeLeak: + matterType = esp_matter::endpoint::water_leak_detector::get_device_type_id(); + break; + default: + matterType = esp_matter::endpoint::occupancy_sensor::get_device_type_id(); + break; + } + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), matterType, + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setDetected(_channel->mainFunctionValue()); + } +} + +void MatterAlarmBridge::setDetected(bool detected) +{ + if (_device == nullptr) + return; + + auto alarmType = _channel->getAlarmType(); + if (alarmType == AlarmTypeContact || alarmType == AlarmTypeLeak) + { + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::boolStateClusterId, + matterbridge::boolStateAttrId, detected); + } + else if (alarmType == AlarmTypeSmoke) + { + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::smokeCoAlarmClusterId, + matterbridge::smokeStateAttrId, + detected ? static_cast(chip::app::Clusters::SmokeCoAlarm::AlarmStateEnum::kCritical) + : static_cast(chip::app::Clusters::SmokeCoAlarm::AlarmStateEnum::kNormal)); + } + else if (alarmType == AlarmTypeCarbonMonoxid) + { + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::smokeCoAlarmClusterId, + matterbridge::coStateAttrId, + detected ? static_cast(chip::app::Clusters::SmokeCoAlarm::AlarmStateEnum::kCritical) + : static_cast(chip::app::Clusters::SmokeCoAlarm::AlarmStateEnum::kNormal)); + } + else if (alarmType == AlarmTypeCarbonDioxid) + { + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::airQualityClusterId, + matterbridge::measuredAirQualityAttrId, + detected ? static_cast(chip::app::Clusters::AirQuality::AirQualityEnum::kPoor) + : static_cast(chip::app::Clusters::AirQuality::AirQualityEnum::kGood)); + } + else + { + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::occupancyClusterId, + matterbridge::occupancyAttrId, detected ? 1 : 0); + } +} + +void MatterAlarmBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t, + uint32_t, esp_matter_attr_val_t *) +{ +} \ No newline at end of file diff --git a/src/Alarm/MatterAlarm.h b/src/Alarm/MatterAlarm.h new file mode 100644 index 0000000..8419f6d --- /dev/null +++ b/src/Alarm/MatterAlarm.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Alarm/KnxChannelAlarm.h" + +class MatterAlarmBridge final : public MatterBridgeDeviceBase, public AlarmBridge +{ +public: + explicit MatterAlarmBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setDetected(bool detected) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/Baggages/Help_de/BRI-Apple-HomeKit.md b/src/Baggages/Help_de/BRI-Apple-HomeKit.md index 6c9a9a7..29dc8a2 100644 --- a/src/Baggages/Help_de/BRI-Apple-HomeKit.md +++ b/src/Baggages/Help_de/BRI-Apple-HomeKit.md @@ -11,3 +11,4 @@ Nachdem die Bridge über ETS programmiert wurde, kann sie mit Apple Homekit verb Sollte nachträglich eine Gerätetype sich ändern, z.B. eine Lampe wird zu einer Jalousie, oder eine Untertype ändert sich z.B. ein CO2 Sensor wird zu einem Kontakt, muss zuerst das Gerät deaktiviert werden. Danach die in der Home App warten bis das Gerät verschwunden ist und danach kann das Gerät wieder aktiviert werden. + diff --git a/src/Baggages/Help_de/BRI-Matter.md b/src/Baggages/Help_de/BRI-Matter.md new file mode 100644 index 0000000..fcd0887 --- /dev/null +++ b/src/Baggages/Help_de/BRI-Matter.md @@ -0,0 +1,6 @@ +### Matter + +Über die Matter Unterstützung können KNX Geräte über Matter fähige Geräte gesteuert werden. +Z.B. Google Home, Apple Home + + diff --git a/src/Baggages/Help_de/BRI-Name.md b/src/Baggages/Help_de/BRI-Name.md index 3b52cc6..caa90d0 100644 --- a/src/Baggages/Help_de/BRI-Name.md +++ b/src/Baggages/Help_de/BRI-Name.md @@ -3,4 +3,3 @@ Name der Smart Home Bridge. Bei manchen Smart Home Systemen wird dieser beim Koppeln angezeigt. - diff --git a/src/Baggages/Help_de/BRI-Kopplungscode.md b/src/Baggages/Help_de/BRI-PairingCodeHomeKit.md similarity index 100% rename from src/Baggages/Help_de/BRI-Kopplungscode.md rename to src/Baggages/Help_de/BRI-PairingCodeHomeKit.md diff --git a/src/Baggages/Help_de/BRI-PairingCodeMatter.md b/src/Baggages/Help_de/BRI-PairingCodeMatter.md new file mode 100644 index 0000000..9a47e6a --- /dev/null +++ b/src/Baggages/Help_de/BRI-PairingCodeMatter.md @@ -0,0 +1,13 @@ +### Kopplungscode + +Über Apple HomeKit oder Google Home können die KNX Geräte gesteuert werden. +Der Kopplungscode wird nur beim erstmaligen Koppeln der Bridge verwendet. +Das Koppeln von weiteren Apps erfolgt in der App, in der die erste Kopplung durchgeführt wurde. + +Koppelung in Apple Home: + +- "Gerät hinzufügen" wählen +- "Weitere Optionen..." wählen +- Nun sollte die Bridge sichtbar sein. Den Kopplungscode der in ETS eingestellt wurde (Standardwert 29710235) eingeben und die Meldung das es sich um ein nicht zertifiziertes Gerät handelt bestätigen. +- Danach den Setup-Wizard für alle Geräte durchführen. + diff --git a/src/ChannelOwnerModule.h b/src/ChannelOwnerModule.h index 1f4b940..450302d 100644 --- a/src/ChannelOwnerModule.h +++ b/src/ChannelOwnerModule.h @@ -10,6 +10,7 @@ class ChannelOwnerModule : public OpenKNX::Module OpenKNX::Channel** _pChannels = nullptr; public: void setNumberOfChannels(uint8_t numberOfChannels); + uint8_t numberOfChannels() const { return _numberOfChannels; } ~ChannelOwnerModule(); virtual OpenKNX::Channel* createChannel(uint8_t _channelIndex /* this parameter is used in macros, do not rename */); diff --git a/src/Dimmer/HomeKitDimmer.cpp b/src/Dimmer/HomeKitDimmer.cpp index 3478c3f..77da57a 100644 --- a/src/Dimmer/HomeKitDimmer.cpp +++ b/src/Dimmer/HomeKitDimmer.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitDimmer.h" diff --git a/src/Dimmer/KnxChannelDimmer.cpp b/src/Dimmer/KnxChannelDimmer.cpp index 20ac41a..1d3fd8d 100644 --- a/src/Dimmer/KnxChannelDimmer.cpp +++ b/src/Dimmer/KnxChannelDimmer.cpp @@ -35,9 +35,25 @@ void KnxChannelDimmer::add(DimmerBridge *dimmerBridge) { dimmerBridges.push_back(dimmerBridge); dimmerBridge->initialize(this); +} + +void KnxChannelDimmer::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto dimmerBridge = static_cast(bridge); dimmerBridge->setBrightness(koGet(KO_DIMMER_FEEDBACK)); } +void KnxChannelDimmer::syncAllBridgeStates() +{ + for (auto it = dimmerBridges.begin(); it != dimmerBridges.end(); ++it) + { + syncBridgeState(*it); + } +} + void KnxChannelDimmer::remove(DimmerBridge *dimmerBridge) { dimmerBridges.remove(dimmerBridge); diff --git a/src/Dimmer/KnxChannelDimmer.h b/src/Dimmer/KnxChannelDimmer.h index d5a11fd..186b582 100644 --- a/src/Dimmer/KnxChannelDimmer.h +++ b/src/Dimmer/KnxChannelDimmer.h @@ -18,6 +18,8 @@ class KnxChannelDimmer : public KnxChannelBase void remove(DimmerBridge* dimmerBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: uint8_t lastBrighness = 100; diff --git a/src/Dimmer/MatterDimmer.cpp b/src/Dimmer/MatterDimmer.cpp new file mode 100644 index 0000000..7bef4cd --- /dev/null +++ b/src/Dimmer/MatterDimmer.cpp @@ -0,0 +1,68 @@ +#include "Dimmer/MatterDimmer.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include "knxprod.h" +#include + +MatterDimmerBridge::MatterDimmerBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +namespace +{ +uint8_t percentToMatterLevel(uint8_t percent) +{ + uint16_t clamped = static_cast(std::clamp(percent, 0, 100)); + return static_cast((clamped * 254u + 50u) / 100u); +} + +uint8_t matterLevelToPercent(uint8_t level) +{ + uint16_t clamped = static_cast(std::clamp(level, 0, 254)); + return static_cast((clamped * 100u + 127u) / 254u); +} +} + +void MatterDimmerBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + uint32_t matterType = ParamBRI_CHDeviceType == 20 + ? esp_matter::endpoint::dimmable_light::get_device_type_id() + : esp_matter::endpoint::dimmable_plugin_unit::get_device_type_id(); + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), matterType, + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setBrightness(_channel->mainFunctionValue() ? 100 : 0); + } +} + +void MatterDimmerBridge::setBrightness(uint8_t brightness) +{ + if (_device == nullptr) + return; + + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::onOffClusterId, + matterbridge::onOffAttrId, brightness > 0); + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::levelControlClusterId, + matterbridge::currentLevelAttrId, percentToMatterLevel(brightness)); +} + +void MatterDimmerBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::onOffClusterId && attributeId == matterbridge::onOffAttrId) + { + _channel->commandPower(this, val->val.b); + } + else if (clusterId == matterbridge::levelControlClusterId && attributeId == matterbridge::currentLevelAttrId) + { + _channel->commandBrightness(this, matterLevelToPercent(val->val.u8)); + } +} \ No newline at end of file diff --git a/src/Dimmer/MatterDimmer.h b/src/Dimmer/MatterDimmer.h new file mode 100644 index 0000000..af2f621 --- /dev/null +++ b/src/Dimmer/MatterDimmer.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Dimmer/KnxChannelDimmer.h" + +class MatterDimmerBridge final : public MatterBridgeDeviceBase, public DimmerBridge +{ +public: + explicit MatterDimmerBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setBrightness(uint8_t brightness) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/Display/HomeKitDisplay.cpp b/src/Display/HomeKitDisplay.cpp index 66af28d..327f7bc 100644 --- a/src/Display/HomeKitDisplay.cpp +++ b/src/Display/HomeKitDisplay.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitDisplay.h" diff --git a/src/Display/KnxChannelDisplay.cpp b/src/Display/KnxChannelDisplay.cpp index 5a0bc92..3d3584d 100644 --- a/src/Display/KnxChannelDisplay.cpp +++ b/src/Display/KnxChannelDisplay.cpp @@ -87,13 +87,29 @@ void KnxChannelDisplay::add(DeviceBridge *DeviceBridge) { DeviceBridges.push_back(DeviceBridge); DeviceBridge->initialize(this); +} + +void KnxChannelDisplay::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto deviceBridge = static_cast(bridge); if (getDisplayType() == DisplayType::DisplayTypeText) { - DeviceBridge->setValue(lastStringValue.c_str()); + deviceBridge->setValue(lastStringValue.c_str()); } else { - DeviceBridge->setValue(lastValue); + deviceBridge->setValue(lastValue); + } +} + +void KnxChannelDisplay::syncAllBridgeStates() +{ + for (auto it = DeviceBridges.begin(); it != DeviceBridges.end(); ++it) + { + syncBridgeState(*it); } } diff --git a/src/Display/KnxChannelDisplay.h b/src/Display/KnxChannelDisplay.h index 4888744..202423e 100644 --- a/src/Display/KnxChannelDisplay.h +++ b/src/Display/KnxChannelDisplay.h @@ -39,6 +39,8 @@ class KnxChannelDisplay : public KnxChannelBase void remove(DeviceBridge* DeviceBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; diff --git a/src/Display/MatterDisplay.cpp b/src/Display/MatterDisplay.cpp new file mode 100644 index 0000000..58aaca3 --- /dev/null +++ b/src/Display/MatterDisplay.cpp @@ -0,0 +1,107 @@ +#include "Display/MatterDisplay.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +MatterDisplayBridge::MatterDisplayBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterDisplayBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _displayType = _channel->getDisplayType(); + switch (_displayType) + { + case DisplayTypeTemperature: + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::temperature_sensor::get_device_type_id(), + static_cast(this)); + break; + case DisplayTypeHumidity: + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::humidity_sensor::get_device_type_id(), + static_cast(this)); + break; + case DisplayTypeLux: + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::light_sensor::get_device_type_id(), + static_cast(this)); + break; + case DisplayTypeRain: + case DisplayTypeSnow: + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::rain_sensor::get_device_type_id(), + static_cast(this)); + break; + case DisplayTypeWind: + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::air_quality_sensor::get_device_type_id(), + static_cast(this)); + break; + default: + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::occupancy_sensor::get_device_type_id(), + static_cast(this)); + break; + } + + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + } +} + +void MatterDisplayBridge::setValue(double value) +{ + if (_device == nullptr) + return; + + switch (_displayType) + { + case DisplayTypeTemperature: + matterbridge::reportI16(_device->persistent_info.device_endpoint_id, matterbridge::temperatureClusterId, + matterbridge::measuredTemperatureAttrId, + static_cast(std::lround(value * 100.0))); + break; + case DisplayTypeHumidity: + matterbridge::reportU16(_device->persistent_info.device_endpoint_id, matterbridge::humidityClusterId, + matterbridge::measuredHumidityAttrId, + static_cast(std::clamp(std::lround(value * 100.0), 0, 10000))); + break; + case DisplayTypeLux: + matterbridge::reportU16(_device->persistent_info.device_endpoint_id, matterbridge::illuminanceClusterId, + matterbridge::measuredIlluminanceAttrId, + static_cast(std::clamp(std::lround(value), 0, 65534))); + break; + case DisplayTypeRain: + case DisplayTypeSnow: + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::boolStateClusterId, + matterbridge::boolStateAttrId, value > 0.0); + break; + case DisplayTypeWind: + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::airQualityClusterId, + matterbridge::measuredAirQualityAttrId, + value > 0.0 ? static_cast(chip::app::Clusters::AirQuality::AirQualityEnum::kFair) + : static_cast(chip::app::Clusters::AirQuality::AirQualityEnum::kGood)); + break; + default: + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::occupancyClusterId, + matterbridge::occupancyAttrId, + static_cast(std::clamp(std::lround(value), 0, 1))); + break; + } +} + +void MatterDisplayBridge::setValue(const char *) +{ +} + +void MatterDisplayBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t, + uint32_t, esp_matter_attr_val_t *) +{ +} \ No newline at end of file diff --git a/src/Display/MatterDisplay.h b/src/Display/MatterDisplay.h new file mode 100644 index 0000000..283579c --- /dev/null +++ b/src/Display/MatterDisplay.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Display/KnxChannelDisplay.h" + +class MatterDisplayBridge final : public MatterBridgeDeviceBase, public DeviceBridge +{ + DisplayType _displayType = DisplayTypeText; + +public: + explicit MatterDisplayBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setValue(double value) override; + void setValue(const char *value) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/DoorWindow/HomeKitDoorWindow.cpp b/src/DoorWindow/HomeKitDoorWindow.cpp index 517c10a..fb751cb 100644 --- a/src/DoorWindow/HomeKitDoorWindow.cpp +++ b/src/DoorWindow/HomeKitDoorWindow.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitDoorWindow.h" diff --git a/src/DoorWindow/KnxChannelDoorWindow.cpp b/src/DoorWindow/KnxChannelDoorWindow.cpp index 9a76cfc..1c4e8a0 100644 --- a/src/DoorWindow/KnxChannelDoorWindow.cpp +++ b/src/DoorWindow/KnxChannelDoorWindow.cpp @@ -39,6 +39,14 @@ void KnxChannelDoorWindow::add(DoorWindowBridge* interface) { interfaces.push_back(interface); interface->initialize(this); +} + +void KnxChannelDoorWindow::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto interface = static_cast(bridge); switch ((KnxChannelDoorWindowFeedback) ParamBRI_CHDoorWindowFeedbackType) { case KnxChannelDoorWindowFeedback::DoorWindowFeedbackPercentage: @@ -49,7 +57,7 @@ void KnxChannelDoorWindow::add(DoorWindowBridge* interface) break; case KnxChannelDoorWindowFeedback::DoorWindowFeedbackClosed: interface->setPosition(koGet(KO_FEEDBACK_BIT) ? 0 : 100); - break; + break; } interface->setMovement(_currentMovement); if (ParamBRI_CHDoorWindowObstructionDetection) @@ -61,7 +69,14 @@ void KnxChannelDoorWindow::add(DoorWindowBridge* interface) interface->setObstructionDetected(false); } interface->mainFunctionValueChanged(); +} +void KnxChannelDoorWindow::syncAllBridgeStates() +{ + for (auto it = interfaces.begin(); it != interfaces.end(); ++it) + { + syncBridgeState(*it); + } } void KnxChannelDoorWindow::remove(DoorWindowBridge* interface) @@ -141,7 +156,7 @@ bool KnxChannelDoorWindow::commandPosition(DoorWindowBridge* interface, uint8_t return true; } logDebugP("Received changed. Position: %d", position); - + bool sendPosition = true; if (position == 0 || position == 100) { @@ -156,7 +171,7 @@ bool KnxChannelDoorWindow::commandPosition(DoorWindowBridge* interface, uint8_t return true; } } - + switch (getDoorWindowHandling()) { case DoorWindowHandling::DoorWindowHandlingSendOpenAndClose: @@ -238,7 +253,7 @@ void KnxChannelDoorWindow::processInputKo(GroupObject &ko) { if (isKo(ko, KO_FEEDBACK_PERCENT)) { - uint8_t position = currentPosition(); + uint8_t position = koGet(KO_FEEDBACK_PERCENT); if (ParamBRI_CHDoorWindowUsePercent) koSetWithoutSend(KO_POSITION, position); for (auto it = interfaces.begin(); it != interfaces.end(); ++it) @@ -249,35 +264,27 @@ void KnxChannelDoorWindow::processInputKo(GroupObject &ko) mainFunctionValueChanged(); } - else if (isKo(ko, KO_OBSTRUCTION_DETECTED)) + else if (isKo(ko, KO_FEEDBACK_BIT)) { - bool obstructionDetected = koGet(KO_OBSTRUCTION_DETECTED); + uint8_t position = currentPosition(); + if (ParamBRI_CHDoorWindowUsePercent) + koSetWithoutSend(KO_POSITION, position); for (auto it = interfaces.begin(); it != interfaces.end(); ++it) { - (*it)->setObstructionDetected(obstructionDetected); + (*it)->setPosition(position); + (*it)->mainFunctionValueChanged(); } + mainFunctionValueChanged(); } else if (isKo(ko, KO_CLOSING_FEEDBACK) || isKo(ko, KO_OPENING_FEEDBACK)) { - bool down = koGet(KO_CLOSING_FEEDBACK); - bool up = koGet(KO_OPENING_FEEDBACK); - auto value = DoorWindowMoveState::DoorWindowMoveStateHold; - if (down) - { + bool closing = koGet(KO_CLOSING_FEEDBACK); + bool opening = koGet(KO_OPENING_FEEDBACK); + DoorWindowMoveState value = DoorWindowMoveState::DoorWindowMoveStateHold; + if (closing && !opening) value = DoorWindowMoveState::DoorWindowMoveStateClosing; - koSetWithoutSend(KO_OPENING_FEEDBACK, false); - logDebugP("Moving down"); - } - else if(up) - { + else if (!closing && opening) value = DoorWindowMoveState::DoorWindowMoveStateOpening; - koSetWithoutSend(KO_CLOSING_FEEDBACK, false); - logDebugP("Moving up"); - } - else - { - logDebugP("Stopping move"); - } _currentMovement = value; for (auto it = interfaces.begin(); it != interfaces.end(); ++it) { @@ -285,7 +292,16 @@ void KnxChannelDoorWindow::processInputKo(GroupObject &ko) (*it)->mainFunctionValueChanged(); } mainFunctionValueChanged(); - + } + else if (isKo(ko, KO_OBSTRUCTION_DETECTED)) + { + bool value = koGet(KO_OBSTRUCTION_DETECTED); + for (auto it = interfaces.begin(); it != interfaces.end(); ++it) + { + (*it)->setObstructionDetected(value); + (*it)->mainFunctionValueChanged(); + } + mainFunctionValueChanged(); } } diff --git a/src/DoorWindow/KnxChannelDoorWindow.h b/src/DoorWindow/KnxChannelDoorWindow.h index 79ac9d4..0e32900 100644 --- a/src/DoorWindow/KnxChannelDoorWindow.h +++ b/src/DoorWindow/KnxChannelDoorWindow.h @@ -36,6 +36,8 @@ class KnxChannelDoorWindow : public KnxChannelBase void remove(DoorWindowBridge* interface); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: volatile bool updatePosition = false; diff --git a/src/DoorWindow/MatterDoorWindow.cpp b/src/DoorWindow/MatterDoorWindow.cpp new file mode 100644 index 0000000..b79baed --- /dev/null +++ b/src/DoorWindow/MatterDoorWindow.cpp @@ -0,0 +1,57 @@ +#include "DoorWindow/MatterDoorWindow.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +MatterDoorWindowBridge::MatterDoorWindowBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterDoorWindowBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::window_covering_device::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setPosition(_channel->mainFunctionValue() ? 100 : 0); + } +} + +void MatterDoorWindowBridge::setPosition(uint8_t position) +{ + if (_device == nullptr) + return; + + auto matterPosition = static_cast(std::clamp(position, 0, 100) * 100); + auto currentVal = matterbridge::u16Value(matterPosition); + auto targetVal = matterbridge::u16Value(matterPosition); + esp_matter::attribute::report(_device->persistent_info.device_endpoint_id, matterbridge::windowCoveringClusterId, + matterbridge::currentPositionLiftPercent100thsAttrId, ¤tVal); + esp_matter::attribute::report(_device->persistent_info.device_endpoint_id, matterbridge::windowCoveringClusterId, + matterbridge::targetPositionLiftPercent100thsAttrId, &targetVal); +} + +void MatterDoorWindowBridge::setMovement(DoorWindowMoveState) +{ +} + +void MatterDoorWindowBridge::setObstructionDetected(bool) +{ +} + +void MatterDoorWindowBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::windowCoveringClusterId && + attributeId == matterbridge::targetPositionLiftPercent100thsAttrId) + { + _channel->commandPosition(this, static_cast(val->val.u16 / 100)); + } +} \ No newline at end of file diff --git a/src/DoorWindow/MatterDoorWindow.h b/src/DoorWindow/MatterDoorWindow.h new file mode 100644 index 0000000..168fa4a --- /dev/null +++ b/src/DoorWindow/MatterDoorWindow.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "DoorWindow/KnxChannelDoorWindow.h" + +class MatterDoorWindowBridge final : public MatterBridgeDeviceBase, public DoorWindowBridge +{ +public: + explicit MatterDoorWindowBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setPosition(uint8_t position) override; + void setMovement(DoorWindowMoveState movement) override; + void setObstructionDetected(bool obstructionDetected) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/Espalexa.h b/src/Espalexa.h index 6513c44..92cd59f 100644 --- a/src/Espalexa.h +++ b/src/Espalexa.h @@ -140,7 +140,7 @@ class Espalexa : public RequestHandler { // construct 'globally unique' Json dict key fitting into signed int inline int encodeLightKey(uint8_t idx) { - static_assert(ESPALEXA_MAXDEVICES <= 128, ""); + // static_assert(ESPALEXA_MAXDEVICES <= 128, ""); return (mac24<<7) | idx; } diff --git a/src/Fan/HomeKitFan.cpp b/src/Fan/HomeKitFan.cpp index 121e2f3..f348453 100644 --- a/src/Fan/HomeKitFan.cpp +++ b/src/Fan/HomeKitFan.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitFan.h" diff --git a/src/Fan/KnxChannelFan.cpp b/src/Fan/KnxChannelFan.cpp index 06a9073..14a38c0 100644 --- a/src/Fan/KnxChannelFan.cpp +++ b/src/Fan/KnxChannelFan.cpp @@ -28,8 +28,28 @@ void KnxChannelFan::add(FanBridge *fanBridge) { fanBridges.push_back(fanBridge); fanBridge->initialize(this); +} + +void KnxChannelFan::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + bool automatic = koGet(KO_AUTOMATIC_FEEDBACK); + if (ParamBRI_CHFanKoAutomaticFeedback == 1) + automatic = !automatic; + + auto fanBridge = static_cast(bridge); fanBridge->setPower(koGet(KO_SWITCH_FEEDBACK)); - fanBridge->setAutomatic(koGet(KO_AUTOMATIC_FEEDBACK)); + fanBridge->setAutomatic(automatic); +} + +void KnxChannelFan::syncAllBridgeStates() +{ + for (auto it = fanBridges.begin(); it != fanBridges.end(); ++it) + { + syncBridgeState(*it); + } } void KnxChannelFan::remove(FanBridge *fanBridge) diff --git a/src/Fan/KnxChannelFan.h b/src/Fan/KnxChannelFan.h index 89ec71e..e1767cf 100644 --- a/src/Fan/KnxChannelFan.h +++ b/src/Fan/KnxChannelFan.h @@ -19,6 +19,8 @@ class KnxChannelFan : public KnxChannelBase void remove(FanBridge* fanBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; diff --git a/src/Fan/MatterFan.cpp b/src/Fan/MatterFan.cpp new file mode 100644 index 0000000..54b8d1a --- /dev/null +++ b/src/Fan/MatterFan.cpp @@ -0,0 +1,64 @@ +#include "Fan/MatterFan.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include "knxprod.h" +#include + +MatterFanBridge::MatterFanBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterFanBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::fan::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setPower(_channel->mainFunctionValue()); + setAutomatic(_automatic); + } +} + +void MatterFanBridge::setAutomatic(bool automatic) +{ + uint8_t _channelIndex = _channel != nullptr ? _channel->channelIndex() : 0; + if (ParamBRI_CHFanAutomatic == 0) + return; + + _automatic = automatic; + if (_device == nullptr) + return; + + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::fanControlClusterId, + matterbridge::fanModeAttrId, automatic ? 0 : 1); +} + +void MatterFanBridge::setPower(bool on) +{ + _power = on; + if (_device == nullptr) + return; + + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::onOffClusterId, + matterbridge::onOffAttrId, on); + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::fanControlClusterId, + matterbridge::percentCurrentAttrId, on ? 100 : 0); + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::fanControlClusterId, + matterbridge::percentSettingAttrId, on ? 100 : 0); +} + +void MatterFanBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::onOffClusterId && attributeId == matterbridge::onOffAttrId) + _channel->commandPower(this, val->val.b); + else if (clusterId == matterbridge::fanControlClusterId && attributeId == matterbridge::fanModeAttrId) + _channel->commandAutomatic(this, val->val.u8 == 0); +} \ No newline at end of file diff --git a/src/Fan/MatterFan.h b/src/Fan/MatterFan.h new file mode 100644 index 0000000..3b6c2fe --- /dev/null +++ b/src/Fan/MatterFan.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Fan/KnxChannelFan.h" + +class MatterFanBridge final : public MatterBridgeDeviceBase, public FanBridge +{ + bool _automatic = false; + bool _power = false; + +public: + explicit MatterFanBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setAutomatic(bool automatic) override; + void setPower(bool on) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/HomeKitBridge.cpp b/src/HomeKitBridge.cpp index 40934ed..4771f68 100644 --- a/src/HomeKitBridge.cpp +++ b/src/HomeKitBridge.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitBridge.h" #include "SmartHomeBridgeModule.h" @@ -37,7 +37,7 @@ void HomeKitBridge::initialize(SmartHomeBridgeModule *bridge) _bridge = bridge; homeSpan.useEthernet(); homeSpan.setSerialInputDisable(true); - homeSpan.setPairingCode((const char *)ParamBRI_PairingCode); + homeSpan.setPairingCode(ParamBRI_PairingCodeHomeKitStr.c_str()); homeSpan.setPortNum(8080); homeSpan.begin(Category::Bridges, bridge->getNameInUTF8()); new SpanAccessory(); diff --git a/src/HomeKitBridge.h b/src/HomeKitBridge.h index e66bf5f..07c482e 100644 --- a/src/HomeKitBridge.h +++ b/src/HomeKitBridge.h @@ -1,5 +1,5 @@ #pragma once -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeSpan.h" #include "SmartHomeBridgeModule.h" diff --git a/src/ISO8859_15ToUTF8.cpp b/src/ISO8859_15ToUTF8.cpp index b3676f4..bf22395 100644 --- a/src/ISO8859_15ToUTF8.cpp +++ b/src/ISO8859_15ToUTF8.cpp @@ -432,7 +432,9 @@ const char8_t* cp1252_UTF8[128] = { const char8_t** defaultConverter = DEFAULT_CHAR_MAPPING; -const char* convertISO8859_15ToUTF8(const char* iso) +namespace +{ +const char* convertISO8859_15ToUTF8_impl(const char* iso, bool allwaysMakeCopy = false) { int bufferlength = 0; int i = 0; @@ -452,7 +454,7 @@ const char* convertISO8859_15ToUTF8(const char* iso) break; } } - if (!replacementNeeded) + if (!replacementNeeded && !allwaysMakeCopy) return iso; char* cUtf8 = (char*) HS_MALLOC(bufferlength); size_t bufferIndex = 0; @@ -476,10 +478,16 @@ const char* convertISO8859_15ToUTF8(const char* iso) } return cUtf8; } +} + +const char* convertISO8859_15ToUTF8(const char* iso) +{ + return convertISO8859_15ToUTF8_impl(iso); +} std::string convertISO8859_15ToUTF8_string(const char* c1252) { - const char* converted = convertISO8859_15ToUTF8(c1252); + const char* converted = convertISO8859_15ToUTF8_impl(c1252); std::string result(converted); if (converted != c1252) { diff --git a/src/Jalousie/HomeKitJalousie.cpp b/src/Jalousie/HomeKitJalousie.cpp index b1d8e35..4f13f03 100644 --- a/src/Jalousie/HomeKitJalousie.cpp +++ b/src/Jalousie/HomeKitJalousie.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitJalousie.h" diff --git a/src/Jalousie/KnxChannelJalousie.cpp b/src/Jalousie/KnxChannelJalousie.cpp index e13b44f..b768e50 100644 --- a/src/Jalousie/KnxChannelJalousie.cpp +++ b/src/Jalousie/KnxChannelJalousie.cpp @@ -26,9 +26,26 @@ void KnxChannelJalousie::deleteBridgeDevice(ChannelBridge *device) void KnxChannelJalousie::add(RolladenBridge* interface) { KnxChannelRolladen::add(interface); +} + +void KnxChannelJalousie::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + KnxChannelRolladen::syncBridgeState(bridge); + auto interface = static_cast(bridge); interface->setSlatPosition(koGet(KO_SLAT_POSITION_FEEDBACK)); } +void KnxChannelJalousie::syncAllBridgeStates() +{ + for (auto it = interfaces.begin(); it != interfaces.end(); ++it) + { + syncBridgeState(*it); + } +} + void KnxChannelJalousie::remove(RolladenBridge* interface) { interfaces.remove(interface); diff --git a/src/Jalousie/KnxChannelJalousie.h b/src/Jalousie/KnxChannelJalousie.h index a8c0cf9..0c24266 100644 --- a/src/Jalousie/KnxChannelJalousie.h +++ b/src/Jalousie/KnxChannelJalousie.h @@ -12,6 +12,8 @@ class KnxChannelJalousie : public KnxChannelRolladen void remove(RolladenBridge* interface); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; diff --git a/src/Jalousie/MatterJalousie.cpp b/src/Jalousie/MatterJalousie.cpp new file mode 100644 index 0000000..e35ca6f --- /dev/null +++ b/src/Jalousie/MatterJalousie.cpp @@ -0,0 +1,51 @@ +#include "Jalousie/MatterJalousie.h" + +#include "Matter/MatterBridgeCommon.h" + +MatterJalousieBridge::MatterJalousieBridge(MatterBridge *bridge) : MatterRolladenBridge(bridge) +{ +} + +void MatterJalousieBridge::setup(uint8_t channelIndex) +{ + MatterRolladenBridge::setup(channelIndex); + + if (_device == nullptr) + return; + + // Create optional Tilt Percent100ths attributes (not created by window_covering_device). + auto ep = _device->persistent_info.device_endpoint_id; + auto wcCluster = esp_matter::cluster::get(ep, matterbridge::windowCoveringClusterId); + if (wcCluster != nullptr) + { + esp_matter::cluster::window_covering::attribute::create_current_position_tilt_percent_100ths( + wcCluster, nullable()); + esp_matter::cluster::window_covering::attribute::create_target_position_tilt_percent_100ths( + wcCluster, nullable()); + } +} + +void MatterJalousieBridge::setSlatPosition(uint8_t slatPosition) +{ + if (_device == nullptr) + return; + + auto ep = _device->persistent_info.device_endpoint_id; + auto tilt = static_cast(std::clamp(slatPosition, 0, 100) * 100); + matterbridge::reportU16(ep, matterbridge::windowCoveringClusterId, + matterbridge::currentPositionTiltPercent100thsAttrId, tilt); + matterbridge::reportU16(ep, matterbridge::windowCoveringClusterId, + matterbridge::targetPositionTiltPercent100thsAttrId, tilt); +} + +void MatterJalousieBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + MatterRolladenBridge::handleMatterAttribute(type, clusterId, attributeId, val); + + if (clusterId == matterbridge::windowCoveringClusterId && + attributeId == matterbridge::targetPositionTiltPercent100thsAttrId) + { + static_cast(_channel)->commandSlatPosition(this, static_cast(val->val.u16 / 100)); + } +} \ No newline at end of file diff --git a/src/Jalousie/MatterJalousie.h b/src/Jalousie/MatterJalousie.h new file mode 100644 index 0000000..8960060 --- /dev/null +++ b/src/Jalousie/MatterJalousie.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Rolladen/MatterRolladen.h" +#include "Jalousie/KnxChannelJalousie.h" + +class MatterJalousieBridge final : public MatterRolladenBridge +{ +public: + explicit MatterJalousieBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setSlatPosition(uint8_t slatPosition) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/KnxChannelBase.h b/src/KnxChannelBase.h index 300887a..ee212ff 100644 --- a/src/KnxChannelBase.h +++ b/src/KnxChannelBase.h @@ -84,6 +84,8 @@ class KnxChannelBase : public OpenKNX::Channel, public Component const char* getNameInUTF8(); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) = 0; virtual void deleteBridgeDevice(ChannelBridge* device) = 0; + virtual void syncBridgeState(ChannelBridge* bridge) {} + virtual void syncAllBridgeStates() {} virtual void commandMainFunctionClick() = 0; virtual bool supportMainFunctionClick() { return true; } virtual std::string currentValueAsString() = 0; diff --git a/src/Lock/HomeKitLock.cpp b/src/Lock/HomeKitLock.cpp index 230d8dd..beb2966 100644 --- a/src/Lock/HomeKitLock.cpp +++ b/src/Lock/HomeKitLock.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitLock.h" diff --git a/src/Lock/KnxChannelLock.cpp b/src/Lock/KnxChannelLock.cpp index 41778a9..3f22b24 100644 --- a/src/Lock/KnxChannelLock.cpp +++ b/src/Lock/KnxChannelLock.cpp @@ -29,10 +29,26 @@ void KnxChannelLock::add(LockBridge *lockBridge) { lockBridges.push_back(lockBridge); lockBridge->initialize(this); +} + +void KnxChannelLock::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto lockBridge = static_cast(bridge); lockBridge->setLocked(isLocked()); lockBridge->setBlocked(koGet(KO_BLOCKED_FEEDBACK)); } +void KnxChannelLock::syncAllBridgeStates() +{ + for (auto it = lockBridges.begin(); it != lockBridges.end(); ++it) + { + syncBridgeState(*it); + } +} + void KnxChannelLock::remove(LockBridge *lockBridge) { lockBridges.remove(lockBridge); diff --git a/src/Lock/KnxChannelLock.h b/src/Lock/KnxChannelLock.h index 4493814..454d6a4 100644 --- a/src/Lock/KnxChannelLock.h +++ b/src/Lock/KnxChannelLock.h @@ -27,6 +27,8 @@ class KnxChannelLock : public KnxChannelBase void remove(LockBridge* lockBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; virtual void processInputKo(GroupObject& ko) override; diff --git a/src/Lock/MatterLock.cpp b/src/Lock/MatterLock.cpp new file mode 100644 index 0000000..801183a --- /dev/null +++ b/src/Lock/MatterLock.cpp @@ -0,0 +1,61 @@ +#include "Lock/MatterLock.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +MatterLockBridge::MatterLockBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterLockBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::door_lock::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setLocked(_channel->mainFunctionValue()); + } +} + +void MatterLockBridge::setLocked(bool lock) +{ + if (_device == nullptr) + return; + + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::doorLockClusterId, + matterbridge::lockStateAttrId, + lock ? static_cast(chip::app::Clusters::DoorLock::DlLockState::kLocked) + : static_cast(chip::app::Clusters::DoorLock::DlLockState::kUnlocked)); +} + +void MatterLockBridge::setBlocked(bool) +{ +} + +void MatterLockBridge::setUnlocking(bool) +{ +} + +void MatterLockBridge::setLocking(bool) +{ +} + +void MatterLockBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (attributeId == matterbridge::lockStateAttrId) + { + const auto lockState = static_cast(val->val.u8); + if (lockState == static_cast(chip::app::Clusters::DoorLock::DlLockState::kLocked)) + _channel->commandLock(this, true); + else if (lockState == static_cast(chip::app::Clusters::DoorLock::DlLockState::kUnlocked)) + _channel->commandLock(this, false); + } +} \ No newline at end of file diff --git a/src/Lock/MatterLock.h b/src/Lock/MatterLock.h new file mode 100644 index 0000000..2a15dae --- /dev/null +++ b/src/Lock/MatterLock.h @@ -0,0 +1,18 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Lock/KnxChannelLock.h" + +class MatterLockBridge final : public MatterBridgeDeviceBase, public LockBridge +{ +public: + explicit MatterLockBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setLocked(bool lock) override; + void setBlocked(bool lock) override; + void setUnlocking(bool unlocking) override; + void setLocking(bool locking) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/Matter/MatterBridgeCommon.h b/src/Matter/MatterBridgeCommon.h new file mode 100644 index 0000000..bb98422 --- /dev/null +++ b/src/Matter/MatterBridgeCommon.h @@ -0,0 +1,266 @@ +#pragma once + +#include +#include +#include +#include + +#ifdef INADDR_NONE +#undef INADDR_NONE +#endif + +#include +#include +#include +#include + +namespace matterbridge +{ +constexpr uint32_t onOffClusterId = chip::app::Clusters::OnOff::Id; +constexpr uint32_t levelControlClusterId = chip::app::Clusters::LevelControl::Id; +constexpr uint32_t colorControlClusterId = chip::app::Clusters::ColorControl::Id; +constexpr uint32_t fanControlClusterId = chip::app::Clusters::FanControl::Id; +constexpr uint32_t thermostatClusterId = chip::app::Clusters::Thermostat::Id; +constexpr uint32_t doorLockClusterId = chip::app::Clusters::DoorLock::Id; +constexpr uint32_t windowCoveringClusterId = chip::app::Clusters::WindowCovering::Id; +constexpr uint32_t basicInformationClusterId = chip::app::Clusters::BasicInformation::Id; +constexpr uint32_t bridgedDeviceBasicInformationClusterId = chip::app::Clusters::BridgedDeviceBasicInformation::Id; +constexpr uint32_t occupancyClusterId = chip::app::Clusters::OccupancySensing::Id; +constexpr uint32_t smokeCoAlarmClusterId = chip::app::Clusters::SmokeCoAlarm::Id; +constexpr uint32_t humidityClusterId = chip::app::Clusters::RelativeHumidityMeasurement::Id; +constexpr uint32_t temperatureClusterId = chip::app::Clusters::TemperatureMeasurement::Id; +constexpr uint32_t illuminanceClusterId = chip::app::Clusters::IlluminanceMeasurement::Id; +constexpr uint32_t airQualityClusterId = chip::app::Clusters::AirQuality::Id; +constexpr uint32_t boolStateClusterId = chip::app::Clusters::BooleanState::Id; + +constexpr uint32_t onOffAttrId = chip::app::Clusters::OnOff::Attributes::OnOff::Id; +constexpr uint32_t currentLevelAttrId = chip::app::Clusters::LevelControl::Attributes::CurrentLevel::Id; +constexpr uint32_t currentHueAttrId = chip::app::Clusters::ColorControl::Attributes::CurrentHue::Id; +constexpr uint32_t currentSaturationAttrId = chip::app::Clusters::ColorControl::Attributes::CurrentSaturation::Id; +constexpr uint32_t fanModeAttrId = chip::app::Clusters::FanControl::Attributes::FanMode::Id; +constexpr uint32_t percentSettingAttrId = chip::app::Clusters::FanControl::Attributes::PercentSetting::Id; +constexpr uint32_t percentCurrentAttrId = chip::app::Clusters::FanControl::Attributes::PercentCurrent::Id; +constexpr uint32_t systemModeAttrId = chip::app::Clusters::Thermostat::Attributes::SystemMode::Id; +// RunningMode may be absent in some generated CHIP namespace variants; use the spec attribute id directly. +constexpr uint32_t runningModeAttrId = 0x001E; +constexpr uint32_t localTemperatureAttrId = chip::app::Clusters::Thermostat::Attributes::LocalTemperature::Id; +constexpr uint32_t heatingSetpointAttrId = chip::app::Clusters::Thermostat::Attributes::OccupiedHeatingSetpoint::Id; +constexpr uint32_t coolingSetpointAttrId = chip::app::Clusters::Thermostat::Attributes::OccupiedCoolingSetpoint::Id; +constexpr uint32_t lockStateAttrId = chip::app::Clusters::DoorLock::Attributes::LockState::Id; +constexpr uint32_t targetPositionTiltPercent100thsAttrId = chip::app::Clusters::WindowCovering::Attributes::TargetPositionTiltPercent100ths::Id; +constexpr uint32_t currentPositionTiltPercent100thsAttrId = chip::app::Clusters::WindowCovering::Attributes::CurrentPositionTiltPercent100ths::Id; +constexpr uint32_t currentPositionLiftPercent100thsAttrId = chip::app::Clusters::WindowCovering::Attributes::CurrentPositionLiftPercent100ths::Id; +constexpr uint32_t targetPositionLiftPercent100thsAttrId = chip::app::Clusters::WindowCovering::Attributes::TargetPositionLiftPercent100ths::Id; +constexpr uint32_t occupancyAttrId = chip::app::Clusters::OccupancySensing::Attributes::Occupancy::Id; +constexpr uint32_t boolStateAttrId = chip::app::Clusters::BooleanState::Attributes::StateValue::Id; +constexpr uint32_t smokeStateAttrId = chip::app::Clusters::SmokeCoAlarm::Attributes::SmokeState::Id; +constexpr uint32_t coStateAttrId = chip::app::Clusters::SmokeCoAlarm::Attributes::COState::Id; +constexpr uint32_t measuredTemperatureAttrId = chip::app::Clusters::TemperatureMeasurement::Attributes::MeasuredValue::Id; +constexpr uint32_t measuredHumidityAttrId = chip::app::Clusters::RelativeHumidityMeasurement::Attributes::MeasuredValue::Id; +constexpr uint32_t measuredIlluminanceAttrId = chip::app::Clusters::IlluminanceMeasurement::Attributes::MeasuredValue::Id; +constexpr uint32_t measuredAirQualityAttrId = chip::app::Clusters::AirQuality::Attributes::AirQuality::Id; +constexpr uint32_t nodeLabelAttrId = chip::app::Clusters::BasicInformation::Attributes::NodeLabel::Id; +constexpr uint32_t productNameAttrId = chip::app::Clusters::BasicInformation::Attributes::ProductName::Id; +constexpr uint32_t bridgedNodeLabelAttrId = chip::app::Clusters::BridgedDeviceBasicInformation::Attributes::NodeLabel::Id; + +inline esp_matter_attr_val_t boolValue(bool value) +{ + esp_matter_attr_val_t result{}; + result.type = ESP_MATTER_VAL_TYPE_BOOLEAN; + result.val.b = value; + return result; +} + +inline esp_matter_attr_val_t u8Value(uint8_t value) +{ + esp_matter_attr_val_t result{}; + result.type = ESP_MATTER_VAL_TYPE_UINT8; + result.val.u8 = value; + return result; +} + +inline esp_matter_attr_val_t i16Value(int16_t value) +{ + esp_matter_attr_val_t result{}; + result.type = ESP_MATTER_VAL_TYPE_INT16; + result.val.i16 = value; + return result; +} + +inline esp_matter_attr_val_t u16Value(uint16_t value) +{ + esp_matter_attr_val_t result{}; + result.type = ESP_MATTER_VAL_TYPE_UINT16; + result.val.u16 = value; + return result; +} + +inline esp_matter_attr_val_t textValue(const char *value) +{ + const char *safeValue = value != nullptr ? value : ""; + return esp_matter_char_str(const_cast(safeValue), static_cast(std::strlen(safeValue))); +} + +inline bool hasAttribute(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId) +{ + if (esp_matter::cluster::get(endpointId, clusterId) == nullptr) + return false; + + return esp_matter::attribute::get(endpointId, clusterId, attributeId) != nullptr; +} + +inline esp_err_t reportBool(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId, bool value) +{ + if (!hasAttribute(endpointId, clusterId, attributeId)) + return ESP_ERR_NOT_FOUND; + + auto reported = boolValue(value); + return esp_matter::attribute::report(endpointId, clusterId, attributeId, &reported); +} + +inline esp_err_t reportU8(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId, uint8_t value) +{ + if (!hasAttribute(endpointId, clusterId, attributeId)) + return ESP_ERR_NOT_FOUND; + + auto attribute = esp_matter::attribute::get(endpointId, clusterId, attributeId); + + esp_matter_attr_val_t current{}; + esp_matter_attr_val_t reported = u8Value(value); + if (esp_matter::attribute::get_val(attribute, ¤t) == ESP_OK && + current.type == ESP_MATTER_VAL_TYPE_NULLABLE_UINT8) + { + reported = esp_matter_nullable_uint8(value); + } + + return esp_matter::attribute::report(endpointId, clusterId, attributeId, &reported); +} + +inline esp_err_t reportI16(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId, int16_t value) +{ + if (!hasAttribute(endpointId, clusterId, attributeId)) + return ESP_ERR_NOT_FOUND; + + auto reported = i16Value(value); + return esp_matter::attribute::report(endpointId, clusterId, attributeId, &reported); +} + +inline esp_err_t reportU16(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId, uint16_t value) +{ + if (!hasAttribute(endpointId, clusterId, attributeId)) + return ESP_ERR_NOT_FOUND; + + auto attribute = esp_matter::attribute::get(endpointId, clusterId, attributeId); + + esp_matter_attr_val_t current{}; + esp_matter_attr_val_t reported = u16Value(value); + if (esp_matter::attribute::get_val(attribute, ¤t) == ESP_OK && + current.type == ESP_MATTER_VAL_TYPE_NULLABLE_UINT16) + { + reported = esp_matter_nullable_uint16(nullable(value)); + } + + return esp_matter::attribute::report(endpointId, clusterId, attributeId, &reported); +} + +inline esp_err_t reportText(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId, const char *value) +{ + if (!hasAttribute(endpointId, clusterId, attributeId)) + return ESP_ERR_NOT_FOUND; + + auto reported = textValue(value); + return esp_matter::attribute::report(endpointId, clusterId, attributeId, &reported); +} + +inline esp_err_t updateText(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId, const char *value) +{ + if (!hasAttribute(endpointId, clusterId, attributeId)) + return ESP_ERR_NOT_FOUND; + + auto updated = textValue(value); + return esp_matter::attribute::update(endpointId, clusterId, attributeId, &updated); +} + +inline void ensureOptionalNameAttributes(uint16_t endpointId) +{ + static char empty[] = ""; + + auto bridgedCluster = esp_matter::cluster::get(endpointId, bridgedDeviceBasicInformationClusterId); + if (bridgedCluster != nullptr) + { + if (esp_matter::attribute::get(endpointId, bridgedDeviceBasicInformationClusterId, bridgedNodeLabelAttrId) == nullptr) + esp_matter::cluster::bridged_device_basic_information::attribute::create_node_label(bridgedCluster, empty, 0); + + if (esp_matter::attribute::get(endpointId, bridgedDeviceBasicInformationClusterId, productNameAttrId) == nullptr) + esp_matter::cluster::bridged_device_basic_information::attribute::create_product_name(bridgedCluster, empty, 0); + } + + auto basicCluster = esp_matter::cluster::get(endpointId, basicInformationClusterId); + if (basicCluster != nullptr) + { + if (esp_matter::attribute::get(endpointId, basicInformationClusterId, nodeLabelAttrId) == nullptr) + esp_matter::cluster::basic_information::attribute::create_node_label(basicCluster, empty, 0); + + if (esp_matter::attribute::get(endpointId, basicInformationClusterId, productNameAttrId) == nullptr) + esp_matter::cluster::basic_information::attribute::create_product_name(basicCluster, empty, 0); + } +} + +inline esp_err_t setAndUpdateText(uint16_t endpointId, uint32_t clusterId, uint32_t attributeId, const char *value) +{ + if (!hasAttribute(endpointId, clusterId, attributeId)) + return ESP_ERR_NOT_FOUND; + + auto attribute = esp_matter::attribute::get(endpointId, clusterId, attributeId); + auto initial = textValue(value); + esp_err_t err = esp_matter::attribute::set_val(attribute, &initial); + if (err != ESP_OK) + return err; + + return updateText(endpointId, clusterId, attributeId, value); +} + +inline esp_err_t setDeviceName(esp_matter_bridge::device_t *device, const char *name) +{ + if (device == nullptr || name == nullptr) + return ESP_ERR_INVALID_ARG; + + uint16_t endpointId = device->persistent_info.device_endpoint_id; + ensureOptionalNameAttributes(endpointId); + + esp_err_t bridgedErr = setAndUpdateText(endpointId, bridgedDeviceBasicInformationClusterId, bridgedNodeLabelAttrId, name); + esp_err_t basicErr = setAndUpdateText(endpointId, basicInformationClusterId, nodeLabelAttrId, name); + esp_err_t bridgedProductErr = setAndUpdateText(endpointId, bridgedDeviceBasicInformationClusterId, productNameAttrId, name); + esp_err_t basicProductErr = setAndUpdateText(endpointId, basicInformationClusterId, productNameAttrId, name); + + if (bridgedErr == ESP_OK || basicErr == ESP_OK || bridgedProductErr == ESP_OK || basicProductErr == ESP_OK) + return ESP_OK; + + if (bridgedErr != ESP_ERR_NOT_FOUND) + return bridgedErr; + if (basicErr != ESP_ERR_NOT_FOUND) + return basicErr; + if (bridgedProductErr != ESP_ERR_NOT_FOUND) + return bridgedProductErr; + if (basicProductErr != ESP_ERR_NOT_FOUND) + return basicProductErr; + + return ESP_ERR_NOT_FOUND; +} + +inline esp_err_t setEndpointName(uint16_t endpointId, const char *name) +{ + if (name == nullptr) + return ESP_ERR_INVALID_ARG; + + return updateText(endpointId, basicInformationClusterId, nodeLabelAttrId, name); +} + +inline esp_err_t setEndpointProductName(uint16_t endpointId, const char *name) +{ + if (name == nullptr) + return ESP_ERR_INVALID_ARG; + + return updateText(endpointId, basicInformationClusterId, productNameAttrId, name); +} +} \ No newline at end of file diff --git a/src/Matter/MatterBridgeDeviceBase.h b/src/Matter/MatterBridgeDeviceBase.h new file mode 100644 index 0000000..02b4901 --- /dev/null +++ b/src/Matter/MatterBridgeDeviceBase.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include "MatterBridgeCommon.h" +#include +#include + +class MatterBridge; + +class MatterBridgeDeviceBase +{ +protected: + MatterBridge *_bridge = nullptr; + esp_matter_bridge::device_t *_device = nullptr; + +public: + explicit MatterBridgeDeviceBase(MatterBridge *bridge) : _bridge(bridge) {} + + virtual ~MatterBridgeDeviceBase() + { + if (_device != nullptr) + esp_matter_bridge::remove_device(_device); + } + + uint16_t endpointId() const + { + return _device != nullptr ? _device->persistent_info.device_endpoint_id : 0; + } + + virtual void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) = 0; +}; diff --git a/src/MatterBridge.cpp b/src/MatterBridge.cpp new file mode 100644 index 0000000..a24dc73 --- /dev/null +++ b/src/MatterBridge.cpp @@ -0,0 +1,697 @@ +#include "MatterBridge.h" + +#include "SmartHomeBridgeModule.h" + +#include "Matter/MatterBridgeCommon.h" +#include "Matter/MatterBridgeDeviceBase.h" + +#include "Switch/MatterSwitch.h" +#include "Dimmer/MatterDimmer.h" +#include "RGB/MatterRGB.h" +#include "Jalousie/MatterJalousie.h" +#include "Rolladen/MatterRolladen.h" +#include "Thermostat/MatterThermostat.h" +#include "Display/MatterDisplay.h" +#include "Alarm/MatterAlarm.h" +#include "Fan/MatterFan.h" +#include "DoorWindow/MatterDoorWindow.h" +#include "Scene/MatterScene.h" +#include "Lock/MatterLock.h" +#include "Media/MatterMedia.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace +{ +constexpr uint32_t kDefaultMatterSetupPasscode = 20202021; + +void logMatterEndpointBudget() +{ +#ifdef CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT + constexpr unsigned dynamicCount = CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; +#else + constexpr unsigned dynamicCount = 0; +#endif + +#ifdef FIXED_ENDPOINT_COUNT + constexpr unsigned fixedCount = FIXED_ENDPOINT_COUNT; +#else + constexpr unsigned fixedCount = 0; +#endif + +#ifdef MAX_ENDPOINT_COUNT + constexpr unsigned maxCount = MAX_ENDPOINT_COUNT; +#else + constexpr unsigned maxCount = dynamicCount + fixedCount; +#endif + +#ifdef CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT + constexpr int sdkconfigDynamic = CONFIG_ESP_MATTER_MAX_DYNAMIC_ENDPOINT_COUNT; +#else + constexpr int sdkconfigDynamic = -1; +#endif + + logInfo("MatterBridge", + "Matter endpoint budget: dynamic=%u fixed=%u max=%u sdkconfig_dynamic=%d", + dynamicCount, fixedCount, maxCount, sdkconfigDynamic); +} + +class RuntimeCommissionableDataProvider : public chip::DeviceLayer::CommissionableDataProvider +{ +public: + CHIP_ERROR SetBackingProvider(chip::DeviceLayer::CommissionableDataProvider *provider) + { + _backingProvider = provider; + return CHIP_NO_ERROR; + } + + CHIP_ERROR SetRuntimePasscode(uint32_t passcode) + { + if (!chip::PayloadContents::IsValidSetupPIN(passcode)) + return CHIP_ERROR_INVALID_ARGUMENT; + + _passcode = passcode; + for (size_t i = 0; i < sizeof(_salt); ++i) + _salt[i] = static_cast(esp_random() & 0xFF); + + chip::Crypto::Spake2pVerifier verifier; + CHIP_ERROR err = verifier.Generate(_iterationCount, chip::ByteSpan(_salt, sizeof(_salt)), _passcode); + if (err != CHIP_NO_ERROR) + return err; + + chip::MutableByteSpan serialized(_verifier); + err = verifier.Serialize(serialized); + if (err != CHIP_NO_ERROR) + return err; + + _verifierLen = serialized.size(); + + _isInitialized = true; + return CHIP_NO_ERROR; + } + + CHIP_ERROR GetSetupDiscriminator(uint16_t &setupDiscriminator) override + { + if (_backingProvider != nullptr) + return _backingProvider->GetSetupDiscriminator(setupDiscriminator); + + setupDiscriminator = 3840; + return CHIP_NO_ERROR; + } + + CHIP_ERROR SetSetupDiscriminator(uint16_t setupDiscriminator) override + { + if (_backingProvider != nullptr) + return _backingProvider->SetSetupDiscriminator(setupDiscriminator); + + (void)setupDiscriminator; + return CHIP_ERROR_NOT_IMPLEMENTED; + } + + CHIP_ERROR GetSpake2pIterationCount(uint32_t &iterationCount) override + { + VerifyOrReturnError(_isInitialized, CHIP_ERROR_INCORRECT_STATE); + iterationCount = _iterationCount; + return CHIP_NO_ERROR; + } + + CHIP_ERROR GetSpake2pSalt(chip::MutableByteSpan &saltBuf) override + { + VerifyOrReturnError(_isInitialized, CHIP_ERROR_INCORRECT_STATE); + if (saltBuf.size() < sizeof(_salt)) + return CHIP_ERROR_BUFFER_TOO_SMALL; + + memcpy(saltBuf.data(), _salt, sizeof(_salt)); + saltBuf.reduce_size(sizeof(_salt)); + return CHIP_NO_ERROR; + } + + CHIP_ERROR GetSpake2pVerifier(chip::MutableByteSpan &verifierBuf, size_t &outVerifierLen) override + { + VerifyOrReturnError(_isInitialized, CHIP_ERROR_INCORRECT_STATE); + outVerifierLen = _verifierLen; + if (verifierBuf.size() < outVerifierLen) + return CHIP_ERROR_BUFFER_TOO_SMALL; + + memcpy(verifierBuf.data(), _verifier, outVerifierLen); + verifierBuf.reduce_size(outVerifierLen); + return CHIP_NO_ERROR; + } + + CHIP_ERROR GetSetupPasscode(uint32_t &setupPasscode) override + { + VerifyOrReturnError(_isInitialized, CHIP_ERROR_INCORRECT_STATE); + setupPasscode = _passcode; + return CHIP_NO_ERROR; + } + + CHIP_ERROR SetSetupPasscode(uint32_t setupPasscode) override + { + return SetRuntimePasscode(setupPasscode); + } + +private: + chip::DeviceLayer::CommissionableDataProvider *_backingProvider = nullptr; + uint32_t _passcode = kDefaultMatterSetupPasscode; + uint32_t _iterationCount = 10000; + uint8_t _salt[16]{}; + uint8_t _verifier[chip::Crypto::kSpake2p_VerifierSerialized_Length]{}; + size_t _verifierLen = 0; + bool _isInitialized = false; +}; + +RuntimeCommissionableDataProvider sRuntimeCommissionableDataProvider; + +bool tryParseMatterPasscode(const std::string &text, uint32_t &out) +{ + if (text.size() != 8) + return false; + + for (unsigned char c : text) + { + if (!std::isdigit(c)) + return false; + } + + try + { + size_t consumed = 0; + unsigned long parsed = std::stoul(text, &consumed, 10); + if (consumed != text.size()) + return false; + out = static_cast(parsed); + return true; + } + catch (...) + { + return false; + } +} + + +esp_err_t addSwitchEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::on_off_switch::config_t config; + return esp_matter::endpoint::on_off_switch::add(endpoint, &config); +} + +esp_err_t addOutletEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::on_off_plugin_unit::config_t config; + return esp_matter::endpoint::on_off_plugin_unit::add(endpoint, &config); +} + +esp_err_t addOnOffLightEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::on_off_light::config_t config; + return esp_matter::endpoint::on_off_light::add(endpoint, &config); +} + +esp_err_t addDimmerEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::dimmable_plugin_unit::config_t config; + return esp_matter::endpoint::dimmable_plugin_unit::add(endpoint, &config); +} + +esp_err_t addDimmableLightEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::dimmable_light::config_t config; + return esp_matter::endpoint::dimmable_light::add(endpoint, &config); +} + +esp_err_t addRGBEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::extended_color_light::config_t config; + return esp_matter::endpoint::extended_color_light::add(endpoint, &config); +} + +esp_err_t addRolladenEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::window_covering_device::config_t config; + return esp_matter::endpoint::window_covering_device::add(endpoint, &config); +} + +esp_err_t addThermostatEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::thermostat::config_t config; + return esp_matter::endpoint::thermostat::add(endpoint, &config); +} + +esp_err_t addFanEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::fan::config_t config; + return esp_matter::endpoint::fan::add(endpoint, &config); +} + +esp_err_t addLockEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::door_lock::config_t config; + return esp_matter::endpoint::door_lock::add(endpoint, &config); +} + +esp_err_t addTemperatureEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::temperature_sensor::config_t config; + return esp_matter::endpoint::temperature_sensor::add(endpoint, &config); +} + +esp_err_t addHumidityEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::humidity_sensor::config_t config; + return esp_matter::endpoint::humidity_sensor::add(endpoint, &config); +} + +esp_err_t addLightEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::light_sensor::config_t config; + return esp_matter::endpoint::light_sensor::add(endpoint, &config); +} + +esp_err_t addOccupancyEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::occupancy_sensor::config_t config; + return esp_matter::endpoint::occupancy_sensor::add(endpoint, &config); +} + +esp_err_t addContactEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::contact_sensor::config_t config; + return esp_matter::endpoint::contact_sensor::add(endpoint, &config); +} + +esp_err_t addLeakEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::water_leak_detector::config_t config; + return esp_matter::endpoint::water_leak_detector::add(endpoint, &config); +} + +esp_err_t addSmokeEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::smoke_co_alarm::config_t config; + return esp_matter::endpoint::smoke_co_alarm::add(endpoint, &config); +} + +esp_err_t addAirQualityEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::air_quality_sensor::config_t config; + return esp_matter::endpoint::air_quality_sensor::add(endpoint, &config); +} + +esp_err_t addRainEndpoint(esp_matter::endpoint_t *endpoint) +{ + esp_matter::endpoint::rain_sensor::config_t config; + return esp_matter::endpoint::rain_sensor::add(endpoint, &config); +} + +esp_err_t addForDeviceType(esp_matter::endpoint_t *endpoint, uint32_t deviceTypeId) +{ + if (deviceTypeId == esp_matter::endpoint::on_off_switch::get_device_type_id()) + return addSwitchEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::on_off_plugin_unit::get_device_type_id()) + return addOutletEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::on_off_light::get_device_type_id()) + return addOnOffLightEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::dimmable_plugin_unit::get_device_type_id()) + return addDimmerEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::dimmable_light::get_device_type_id()) + return addDimmableLightEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::extended_color_light::get_device_type_id()) + return addRGBEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::window_covering_device::get_device_type_id()) + return addRolladenEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::thermostat::get_device_type_id()) + return addThermostatEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::fan::get_device_type_id()) + return addFanEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::door_lock::get_device_type_id()) + return addLockEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::temperature_sensor::get_device_type_id()) + return addTemperatureEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::humidity_sensor::get_device_type_id()) + return addHumidityEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::light_sensor::get_device_type_id()) + return addLightEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::occupancy_sensor::get_device_type_id()) + return addOccupancyEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::contact_sensor::get_device_type_id()) + return addContactEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::water_leak_detector::get_device_type_id()) + return addLeakEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::smoke_co_alarm::get_device_type_id()) + return addSmokeEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::air_quality_sensor::get_device_type_id()) + return addAirQualityEndpoint(endpoint); + if (deviceTypeId == esp_matter::endpoint::rain_sensor::get_device_type_id()) + return addRainEndpoint(endpoint); + + return ESP_OK; +} + +MatterBridgeDeviceBase *asDeviceBase(void *privData) +{ + return reinterpret_cast(privData); +} +} // namespace + +esp_matter::node_t *MatterBridge::node() const +{ + return _node; +} + +uint16_t MatterBridge::parentEndpointId() const +{ + return _aggregatorEndpointId; +} + +const std::string MatterBridge::name() +{ + return "MatterBridge"; +} + +esp_err_t MatterBridge::configureNode(esp_matter::node_t *node) +{ + esp_matter::attribute::set_callback(attributeCallback); + + esp_matter::endpoint::aggregator::config_t aggregatorConfig{}; + esp_matter::endpoint_t *aggregatorEndpoint = + esp_matter::endpoint::aggregator::create(node, &aggregatorConfig, esp_matter::ENDPOINT_FLAG_NONE, nullptr); + if (aggregatorEndpoint == nullptr) + { + logErrorP("Matter aggregator endpoint creation failed"); + return ESP_FAIL; + } + + _aggregatorEndpointId = esp_matter::endpoint::get_id(aggregatorEndpoint); + return esp_matter_bridge::initialize(node, deviceTypeCallback); +} + +void MatterBridge::initialize(SmartHomeBridgeModule *bridge) +{ + BridgeBase::initialize(bridge); + logMatterEndpointBudget(); + + // Resolve intended passcode + uint32_t matterSetupPasscode = kDefaultMatterSetupPasscode; + if (!tryParseMatterPasscode(ParamBRI_PairingCodeMatterStr, matterSetupPasscode)) + { + logErrorP("Matter setup code invalid ('%s'), using fallback %u", + ParamBRI_PairingCodeMatterStr.c_str(), (unsigned)kDefaultMatterSetupPasscode); + matterSetupPasscode = kDefaultMatterSetupPasscode; + } + else if (!chip::PayloadContents::IsValidSetupPIN(matterSetupPasscode)) + { + logErrorP("Matter setup code not allowed by Matter rules (%u), using fallback %u", + (unsigned)matterSetupPasscode, (unsigned)kDefaultMatterSetupPasscode); + matterSetupPasscode = kDefaultMatterSetupPasscode; + } + _matterSetupPasscode = matterSetupPasscode; + + // NVS must be updated BEFORE InitChipStack(), because the + // LegacyTemporaryCommissionableDataProvider reads and caches salt/verifier + // from NVS during ConfigurationManagerImpl::Init() (called by InitChipStack). + // Clearing the old verifier here forces CHIP to recompute it from the new passcode. + { + using cfg = chip::DeviceLayer::Internal::ESP32Config; + uint32_t nvsPasscode = 0; + bool nvsHasPin = (cfg::ReadConfigValue(cfg::kConfigKey_SetupPinCode, nvsPasscode) == CHIP_NO_ERROR); + if (!nvsHasPin || nvsPasscode != _matterSetupPasscode) + { + logInfoP("Matter passcode changed (%u -> %u): updating NVS before CHIP init", + (unsigned)nvsPasscode, (unsigned)_matterSetupPasscode); + cfg::WriteConfigValue(cfg::kConfigKey_SetupPinCode, _matterSetupPasscode); + cfg::ClearConfigValue(cfg::kConfigKey_Spake2pVerifier); + cfg::ClearConfigValue(cfg::kConfigKey_Spake2pSalt); + cfg::ClearConfigValue(cfg::kConfigKey_Spake2pIterationCount); + } + else + { + logInfoP("Matter passcode unchanged (%u), NVS kept", (unsigned)_matterSetupPasscode); + } + } + + // Pre-init CHIP before node::create() can register connectivity handlers. + // This avoids PostEventOrDie() on a NULL chip event queue when WiFi events + // arrive between initialize() and start(). + CHIP_ERROR chipErr = chip::Platform::MemoryInit(); + if (chipErr != CHIP_NO_ERROR && chipErr != CHIP_ERROR_INCORRECT_STATE) + { + logErrorP("Matter MemoryInit failed: %" CHIP_ERROR_FORMAT, chipErr.Format()); + return; + } + + chipErr = chip::DeviceLayer::PlatformMgr().InitChipStack(); + if (chipErr != CHIP_NO_ERROR && chipErr != CHIP_ERROR_INCORRECT_STATE) + { + logErrorP("Matter InitChipStack failed: %" CHIP_ERROR_FORMAT, chipErr.Format()); + return; + } + + // Belt-and-suspenders: also set the in-memory passcode on the provider + // that was registered by InitChipStack. + if (chip::DeviceLayer::CommissionableDataProvider *provider = chip::DeviceLayer::GetCommissionableDataProvider(); + provider != nullptr) + { + chipErr = provider->SetSetupPasscode(_matterSetupPasscode); + if (chipErr != CHIP_NO_ERROR) + logInfoP("Matter SetSetupPasscode (in-memory) not supported: %" CHIP_ERROR_FORMAT, chipErr.Format()); + else + logInfoP("Matter setup code set: %u", (unsigned)_matterSetupPasscode); + } + else + { + logErrorP("Matter CommissionableDataProvider missing"); + } + + esp_matter::node::config_t config{}; + _bridgeNodeLabel = bridge != nullptr ? bridge->getNameInUTF8() : ""; + if (_bridgeNodeLabel.empty()) + _bridgeNodeLabel = "SmartHomeBridge"; + logInfoP("Matter bridge node label: '%s'", _bridgeNodeLabel.c_str()); + std::strncpy(config.root_node.basic_information.node_label, _bridgeNodeLabel.c_str(), + sizeof(config.root_node.basic_information.node_label) - 1); + + _node = esp_matter::node::create(&config, attributeCallback, nullptr, this); + if (_node == nullptr) + { + logErrorP("Matter node creation failed"); + return; + } + + if (configureNode(_node) != ESP_OK) + logErrorP("Matter bridge initialization failed"); + else + { + logInfoP("Matter bridge initialized (aggregator endpoint=%u)", (unsigned)_aggregatorEndpointId); + } +} + +void MatterBridge::start(SmartHomeBridgeModule *bridge) +{ + BridgeBase::start(bridge); + if (startMatter() != ESP_OK) + logErrorP("Matter start failed"); +} + +esp_err_t MatterBridge::startMatter() +{ + // Arduino/OpenKNX may already run mDNS. ESP-Matter discovery initializes + // its own advertiser and fails if mDNS is already active. + MDNS.end(); + + esp_err_t err = esp_matter::start(nullptr); + if (err != ESP_OK) + return err; + + // After start(), set NodeLabel and ProductName on ep 0. + // esp_matter::start() restores persistent attribute values from NVS and may + // overwrite what was set via set_val before start. Calling update() after + // start() ensures Apple Home reads the correct bridge name during commissioning. + matterbridge::updateText(0, matterbridge::basicInformationClusterId, + matterbridge::nodeLabelAttrId, _bridgeNodeLabel.c_str()); + matterbridge::updateText(0, matterbridge::basicInformationClusterId, + matterbridge::productNameAttrId, _bridgeNodeLabel.c_str()); + + if (chip::DeviceLayer::CommissionableDataProvider *provider = chip::DeviceLayer::GetCommissionableDataProvider(); + provider != nullptr) + { + CHIP_ERROR chipErr = sRuntimeCommissionableDataProvider.SetBackingProvider(provider); + if (chipErr != CHIP_NO_ERROR) + logErrorP("Matter runtime provider backing set failed: %" CHIP_ERROR_FORMAT, chipErr.Format()); + + chipErr = sRuntimeCommissionableDataProvider.SetRuntimePasscode(_matterSetupPasscode); + if (chipErr != CHIP_NO_ERROR) + logErrorP("Matter runtime provider passcode setup failed: %" CHIP_ERROR_FORMAT, chipErr.Format()); + else + { + chip::DeviceLayer::SetCommissionableDataProvider(&sRuntimeCommissionableDataProvider); + logInfoP("Matter runtime commissionable provider installed"); + } + + uint32_t effectivePasscode = 0; + chipErr = chip::DeviceLayer::GetCommissionableDataProvider()->GetSetupPasscode(effectivePasscode); + if (chipErr == CHIP_NO_ERROR) + logInfoP("Matter effective setup code after start: %u", (unsigned)effectivePasscode); + else + logInfoP("Matter GetSetupPasscode not supported after start: %" CHIP_ERROR_FORMAT, chipErr.Format()); + } + + return ESP_OK; +} + +void MatterBridge::loop() +{ +} + +void MatterBridge::processInputKo(GroupObject &) +{ +} + +void MatterBridge::getInformation(String &result) +{ + result += "

Matter

"; + result += "Matter bridge aktiv"; +} + +bool MatterBridge::processCommand(const std::string cmd, bool) +{ + if (cmd == "mr") + { + factoryReset(); + logInfoP("Matter factory reset triggered"); + openknx.restart(); + return true; + } + return false; +} + +void MatterBridge::showHelp() +{ + openknx.console.printHelpLine("mr", "Matter factory reset"); +} + +esp_err_t MatterBridge::factoryReset() +{ + return esp_matter_bridge::factory_reset(); +} + +esp_err_t MatterBridge::deviceTypeCallback(esp_matter::endpoint_t *endpoint, uint32_t deviceTypeId, void *) +{ + return addForDeviceType(endpoint, deviceTypeId); +} + +esp_err_t MatterBridge::attributeCallback(esp_matter::attribute::callback_type_t type, uint16_t endpointId, + uint32_t clusterId, uint32_t attributeId, + esp_matter_attr_val_t *val, void *privData) +{ + auto device = asDeviceBase(privData); + if (device == nullptr || val == nullptr || type == esp_matter::attribute::PRE_UPDATE) + return ESP_OK; + + device->handleMatterAttribute(type, clusterId, attributeId, val); + (void)endpointId; + return ESP_OK; +} + +SwitchBridge *MatterBridge::createSwitch(KnxChannelSwitch &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterSwitchBridge(this); + channel.add(bridge); + return bridge; +} + +DimmerBridge *MatterBridge::createDimmer(KnxChannelDimmer &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterDimmerBridge(this); + channel.add(bridge); + return bridge; +} + +RGBBridge *MatterBridge::createRGB(KnxChannelRGB &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterRGBBridge(this); + channel.add(bridge); + return bridge; +} + +RolladenBridge *MatterBridge::createJalousien(KnxChannelJalousie &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterJalousieBridge(this); + channel.add(bridge); + return bridge; +} + +RolladenBridge *MatterBridge::createRolladen(KnxChannelRolladen &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterRolladenBridge(this); + channel.add(bridge); + return bridge; +} + +ThermostatBridge *MatterBridge::createThermostat(KnxChannelThermostat &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterThermostatBridge(this); + channel.add(bridge); + return bridge; +} + +DeviceBridge *MatterBridge::createDisplay(KnxChannelDisplay &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterDisplayBridge(this); + channel.add(bridge); + return bridge; +} + +AlarmBridge *MatterBridge::createSensor(KnxChannelAlarm &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterAlarmBridge(this); + channel.add(bridge); + return bridge; +} + +FanBridge *MatterBridge::createFan(KnxChannelFan &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterFanBridge(this); + channel.add(bridge); + return bridge; +} + +DoorWindowBridge *MatterBridge::createDoorWindow(KnxChannelDoorWindow &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterDoorWindowBridge(this); + channel.add(bridge); + return bridge; +} + +SceneBridge *MatterBridge::createScene(KnxChannelScene &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterSceneBridge(this); + channel.add(bridge); + return bridge; +} + +LockBridge *MatterBridge::createLock(KnxChannelLock &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterLockBridge(this); + channel.add(bridge); + return bridge; +} + +MediaBridge *MatterBridge::createMedia(KnxChannelMedia &channel, uint8_t, uint8_t) +{ + auto bridge = new MatterMediaBridge(this); + channel.add(bridge); + return bridge; +} diff --git a/src/MatterBridge.h b/src/MatterBridge.h new file mode 100644 index 0000000..a2ef8e4 --- /dev/null +++ b/src/MatterBridge.h @@ -0,0 +1,52 @@ +#pragma once + +#include "BridgeBase.h" +#include +#include + +class MatterBridge : public BridgeBase +{ + esp_matter::node_t *_node = nullptr; + uint16_t _aggregatorEndpointId = 0; + uint32_t _matterSetupPasscode = 20202021; + std::string _bridgeNodeLabel; + +public: + MatterBridge() = default; + + esp_matter::node_t *node() const; + uint16_t parentEndpointId() const; + + virtual SwitchBridge *createSwitch(KnxChannelSwitch &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual DimmerBridge *createDimmer(KnxChannelDimmer &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual RGBBridge *createRGB(KnxChannelRGB &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual RolladenBridge *createJalousien(KnxChannelJalousie &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual RolladenBridge *createRolladen(KnxChannelRolladen &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual ThermostatBridge *createThermostat(KnxChannelThermostat &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual DeviceBridge *createDisplay(KnxChannelDisplay &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual AlarmBridge *createSensor(KnxChannelAlarm &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual FanBridge *createFan(KnxChannelFan &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual DoorWindowBridge *createDoorWindow(KnxChannelDoorWindow &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual SceneBridge *createScene(KnxChannelScene &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual LockBridge *createLock(KnxChannelLock &channel, uint8_t _channelIndex, uint8_t deviceType) override; + virtual MediaBridge *createMedia(KnxChannelMedia &channel, uint8_t _channelIndex, uint8_t deviceType) override; + + virtual const std::string name() override; + virtual void initialize(SmartHomeBridgeModule *bridge) override; + virtual void start(SmartHomeBridgeModule *bridge) override; + virtual void loop() override; + virtual void processInputKo(GroupObject &ko) override; + virtual void getInformation(String &result) override; + virtual bool processCommand(const std::string cmd, bool diagnoseKo) override; + virtual void showHelp() override; + +private: + static esp_err_t deviceTypeCallback(esp_matter::endpoint_t *endpoint, uint32_t deviceTypeId, void *privData); + static esp_err_t attributeCallback(esp_matter::attribute::callback_type_t type, uint16_t endpointId, + uint32_t clusterId, uint32_t attributeId, + esp_matter_attr_val_t *val, void *privData); + + esp_err_t configureNode(esp_matter::node_t *node); + esp_err_t startMatter(); + esp_err_t factoryReset(); +}; diff --git a/src/Media/HomeKitMedia.cpp b/src/Media/HomeKitMedia.cpp index 1e3693c..f69672d 100644 --- a/src/Media/HomeKitMedia.cpp +++ b/src/Media/HomeKitMedia.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitMedia.h" #if 0 diff --git a/src/Media/KnxChannelMedia.cpp b/src/Media/KnxChannelMedia.cpp index c80430d..7a08bd4 100644 --- a/src/Media/KnxChannelMedia.cpp +++ b/src/Media/KnxChannelMedia.cpp @@ -30,11 +30,27 @@ void KnxChannelMedia::add(MediaBridge *mediaBridge) { mediaBridges.push_back(mediaBridge); mediaBridge->initialize(this); +} + +void KnxChannelMedia::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto mediaBridge = static_cast(bridge); mediaBridge->setPlay(koGet(KO_PLAY_FEEDBACK)); mediaBridge->setVolume(koGet(KO_VOLUME_FEEDBACK)); mediaBridge->setTitle(koGet(KO_TITLE)); } +void KnxChannelMedia::syncAllBridgeStates() +{ + for (auto it = mediaBridges.begin(); it != mediaBridges.end(); ++it) + { + syncBridgeState(*it); + } +} + void KnxChannelMedia::remove(MediaBridge *mediaBridge) { mediaBridges.remove(mediaBridge); diff --git a/src/Media/KnxChannelMedia.h b/src/Media/KnxChannelMedia.h index 2fa391e..e3e6d44 100644 --- a/src/Media/KnxChannelMedia.h +++ b/src/Media/KnxChannelMedia.h @@ -20,6 +20,8 @@ class KnxChannelMedia : public KnxChannelBase void remove(MediaBridge* mediaBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; diff --git a/src/Media/MatterMedia.cpp b/src/Media/MatterMedia.cpp new file mode 100644 index 0000000..27c9551 --- /dev/null +++ b/src/Media/MatterMedia.cpp @@ -0,0 +1,60 @@ +#include "Media/MatterMedia.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +MatterMediaBridge::MatterMediaBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterMediaBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::dimmable_light::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + } +} + +void MatterMediaBridge::setPlay(bool play) +{ + if (_device == nullptr) + return; + + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::onOffClusterId, + matterbridge::onOffAttrId, play); +} + +void MatterMediaBridge::setVolume(uint8_t volume) +{ + if (_device == nullptr) + return; + + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::levelControlClusterId, + matterbridge::currentLevelAttrId, volume); +} + +void MatterMediaBridge::setTitle(const char *text) +{ + if (_device == nullptr) + return; + + matterbridge::reportText(_device->persistent_info.device_endpoint_id, matterbridge::basicInformationClusterId, + matterbridge::nodeLabelAttrId, text); +} + +void MatterMediaBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::onOffClusterId && attributeId == matterbridge::onOffAttrId) + _channel->commandPlay(this, val->val.b); + else if (clusterId == matterbridge::levelControlClusterId && attributeId == matterbridge::currentLevelAttrId) + _channel->commandVolume(this, val->val.u8); +} \ No newline at end of file diff --git a/src/Media/MatterMedia.h b/src/Media/MatterMedia.h new file mode 100644 index 0000000..6400281 --- /dev/null +++ b/src/Media/MatterMedia.h @@ -0,0 +1,17 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Media/KnxChannelMedia.h" + +class MatterMediaBridge final : public MatterBridgeDeviceBase, public MediaBridge +{ +public: + explicit MatterMediaBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setPlay(bool play) override; + void setVolume(uint8_t volume) override; + void setTitle(const char *text) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/RGB/HomeKitRGB.cpp b/src/RGB/HomeKitRGB.cpp index 81f4ef8..23c0b44 100644 --- a/src/RGB/HomeKitRGB.cpp +++ b/src/RGB/HomeKitRGB.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitRGB.h" #include "rgbconvert.h" diff --git a/src/RGB/KnxChannelRGB.cpp b/src/RGB/KnxChannelRGB.cpp index 96ff27e..a8e6efc 100644 --- a/src/RGB/KnxChannelRGB.cpp +++ b/src/RGB/KnxChannelRGB.cpp @@ -46,11 +46,27 @@ void KnxChannelRGB::add(RGBBridge* RGBBridge) { RGBBridges.push_back(RGBBridge); RGBBridge->initialize(this); - RGBBridge->setRGB(lastColor); +} + +void KnxChannelRGB::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto rgbBridge = static_cast(bridge); + rgbBridge->setRGB(lastColor); if (ParamBRI_CHLightRGBUseSwitchKO) - RGBBridge->setPower(koGet(KO_POWER_FEEDBACK)); + rgbBridge->setPower(koGet(KO_POWER_FEEDBACK)); else - RGBBridge->setPower((uint32_t) koGet(KO_RGB_FEEDBACK) > 0); + rgbBridge->setPower((uint32_t) koGet(KO_RGB_FEEDBACK) > 0); +} + +void KnxChannelRGB::syncAllBridgeStates() +{ + for (auto it = RGBBridges.begin(); it != RGBBridges.end(); ++it) + { + syncBridgeState(*it); + } } void KnxChannelRGB::remove(RGBBridge* RGBBridge) diff --git a/src/RGB/KnxChannelRGB.h b/src/RGB/KnxChannelRGB.h index 4958c80..35d8550 100644 --- a/src/RGB/KnxChannelRGB.h +++ b/src/RGB/KnxChannelRGB.h @@ -21,6 +21,8 @@ class KnxChannelRGB : public KnxChannelBase void remove(RGBBridge* RGBBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: uint32_t lastColor = 0xFFFFFF; diff --git a/src/RGB/MatterRGB.cpp b/src/RGB/MatterRGB.cpp new file mode 100644 index 0000000..902683d --- /dev/null +++ b/src/RGB/MatterRGB.cpp @@ -0,0 +1,146 @@ +#include "RGB/MatterRGB.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +namespace +{ +uint8_t clampHue(uint32_t rgb) +{ + auto red = static_cast((rgb >> 16) & 0xFF) / 255.0f; + auto green = static_cast((rgb >> 8) & 0xFF) / 255.0f; + auto blue = static_cast(rgb & 0xFF) / 255.0f; + auto maxValue = std::max({red, green, blue}); + auto minValue = std::min({red, green, blue}); + auto delta = maxValue - minValue; + + if (delta == 0.0f) + return 0; + + float hue = 0.0f; + if (maxValue == red) + hue = 60.0f * std::fmod(((green - blue) / delta), 6.0f); + else if (maxValue == green) + hue = 60.0f * (((blue - red) / delta) + 2.0f); + else + hue = 60.0f * (((red - green) / delta) + 4.0f); + + if (hue < 0.0f) + hue += 360.0f; + + return static_cast(std::clamp(static_cast(std::lround(hue * 254.0f / 360.0f)), 0, 254)); +} + +uint8_t clampSaturation(uint32_t rgb) +{ + auto red = static_cast((rgb >> 16) & 0xFF) / 255.0f; + auto green = static_cast((rgb >> 8) & 0xFF) / 255.0f; + auto blue = static_cast(rgb & 0xFF) / 255.0f; + auto maxValue = std::max({red, green, blue}); + auto minValue = std::min({red, green, blue}); + auto saturation = maxValue == 0.0f ? 0.0f : ((maxValue - minValue) / maxValue); + return static_cast(std::clamp(static_cast(std::lround(saturation * 254.0f)), 0, 254)); +} + +uint32_t rgbFromHueSaturation(uint8_t hue, uint8_t saturation, uint8_t brightness) +{ + auto h = static_cast(hue) * 360.0f / 254.0f; + auto s = static_cast(saturation) / 254.0f; + auto v = static_cast(brightness) / 254.0f; + + auto c = v * s; + auto x = c * (1.0f - std::fabs(std::fmod(h / 60.0f, 2.0f) - 1.0f)); + auto m = v - c; + + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; + + if (h < 60.0f) { red = c; green = x; } + else if (h < 120.0f) { red = x; green = c; } + else if (h < 180.0f) { green = c; blue = x; } + else if (h < 240.0f) { green = x; blue = c; } + else if (h < 300.0f) { red = x; blue = c; } + else { red = c; blue = x; } + + auto toByte = [m](float component) { + return static_cast(std::clamp(static_cast(std::lround((component + m) * 255.0f)), 0, 255)); + }; + + return (static_cast(toByte(red)) << 16) | + (static_cast(toByte(green)) << 8) | + static_cast(toByte(blue)); +} +} + +MatterRGBBridge::MatterRGBBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterRGBBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::extended_color_light::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setPower(_channel->mainFunctionValue()); + } +} + +void MatterRGBBridge::setPower(bool on) +{ + if (_device == nullptr) + return; + + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::onOffClusterId, + matterbridge::onOffAttrId, on); +} + +void MatterRGBBridge::setRGB(uint32_t rgb) +{ + if (_device == nullptr) + return; + + _hue = clampHue(rgb); + _saturation = clampSaturation(rgb); + _brightness = std::max(1, static_cast((((rgb >> 16) & 0xFF) + ((rgb >> 8) & 0xFF) + (rgb & 0xFF)) / 3)); + + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::onOffClusterId, + matterbridge::onOffAttrId, rgb != 0); + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::levelControlClusterId, + matterbridge::currentLevelAttrId, _brightness); + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::colorControlClusterId, + matterbridge::currentHueAttrId, _hue); + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::colorControlClusterId, + matterbridge::currentSaturationAttrId, _saturation); +} + +void MatterRGBBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::onOffClusterId && attributeId == matterbridge::onOffAttrId) + { + _channel->commandPower(this, val->val.b); + } + else if (clusterId == matterbridge::levelControlClusterId && attributeId == matterbridge::currentLevelAttrId) + { + _brightness = val->val.u8; + _channel->commandRGB(this, rgbFromHueSaturation(_hue, _saturation, _brightness)); + } + else if (clusterId == matterbridge::colorControlClusterId) + { + if (attributeId == matterbridge::currentHueAttrId) + _hue = val->val.u8; + else if (attributeId == matterbridge::currentSaturationAttrId) + _saturation = val->val.u8; + + _channel->commandRGB(this, rgbFromHueSaturation(_hue, _saturation, _brightness)); + } +} \ No newline at end of file diff --git a/src/RGB/MatterRGB.h b/src/RGB/MatterRGB.h new file mode 100644 index 0000000..1fe0949 --- /dev/null +++ b/src/RGB/MatterRGB.h @@ -0,0 +1,20 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "RGB/KnxChannelRGB.h" + +class MatterRGBBridge final : public MatterBridgeDeviceBase, public RGBBridge +{ + uint8_t _hue = 0; + uint8_t _saturation = 0; + uint8_t _brightness = 254; + +public: + explicit MatterRGBBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setRGB(uint32_t rgb) override; + void setPower(bool on) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/Rolladen/HomeKitRolladen.cpp b/src/Rolladen/HomeKitRolladen.cpp index e74c33a..33a3e49 100644 --- a/src/Rolladen/HomeKitRolladen.cpp +++ b/src/Rolladen/HomeKitRolladen.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitRolladen.h" HomeKitRolladen::HomeKitRolladen(int device) : diff --git a/src/Rolladen/KnxChannelRolladen.cpp b/src/Rolladen/KnxChannelRolladen.cpp index b45b9c0..54052f8 100644 --- a/src/Rolladen/KnxChannelRolladen.cpp +++ b/src/Rolladen/KnxChannelRolladen.cpp @@ -30,9 +30,25 @@ void KnxChannelRolladen::add(RolladenBridge* interface) { interfaces.push_back(interface); interface->initialize(this); +} + +void KnxChannelRolladen::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto interface = static_cast(bridge); interface->setPosition(koGet(KO_POSITION_FEEDBACK)); } +void KnxChannelRolladen::syncAllBridgeStates() +{ + for (auto it = interfaces.begin(); it != interfaces.end(); ++it) + { + syncBridgeState(*it); + } +} + void KnxChannelRolladen::remove(RolladenBridge* interface) { interfaces.remove(interface); diff --git a/src/Rolladen/KnxChannelRolladen.h b/src/Rolladen/KnxChannelRolladen.h index d0c970a..da4d06d 100644 --- a/src/Rolladen/KnxChannelRolladen.h +++ b/src/Rolladen/KnxChannelRolladen.h @@ -36,6 +36,8 @@ class KnxChannelRolladen : public KnxChannelBase void remove(RolladenBridge* interface); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: volatile bool updatePosition = false; diff --git a/src/Rolladen/MatterRolladen.cpp b/src/Rolladen/MatterRolladen.cpp new file mode 100644 index 0000000..1a44df9 --- /dev/null +++ b/src/Rolladen/MatterRolladen.cpp @@ -0,0 +1,102 @@ +#include "Rolladen/MatterRolladen.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +namespace +{ +constexpr uint8_t kDeviceTypeRolladen = 31; +constexpr uint8_t kDeviceTypeMarkise = 32; + +// WindowCovering EndProductType enum values (Matter spec). +constexpr uint8_t kEndProductTypeRollerShade = 0x00; +constexpr uint8_t kEndProductTypeRollerShutter = 0x11; +constexpr uint8_t kEndProductTypeInteriorVenetianBlind = 0x0C; +constexpr uint8_t kEndProductTypeAwningTerracePatio = 0x13; + +void applyWindowCoveringEndProductType(uint16_t endpointId, uint8_t channelDeviceType) +{ + auto wcCluster = esp_matter::cluster::get(endpointId, matterbridge::windowCoveringClusterId); + if (wcCluster == nullptr) + return; + + uint8_t endProductType = kEndProductTypeRollerShade; + if (channelDeviceType == 30) + endProductType = kEndProductTypeInteriorVenetianBlind; + else if (channelDeviceType == 32) + endProductType = kEndProductTypeAwningTerracePatio; + else if (channelDeviceType == 31) + endProductType = kEndProductTypeRollerShutter; + + constexpr uint32_t endProductTypeAttrId = chip::app::Clusters::WindowCovering::Attributes::EndProductType::Id; + if (!matterbridge::hasAttribute(endpointId, matterbridge::windowCoveringClusterId, endProductTypeAttrId)) + { + esp_matter::cluster::window_covering::attribute::create_end_product_type(wcCluster, endProductType); + return; + } + + auto updated = matterbridge::u8Value(endProductType); + esp_matter::attribute::update(endpointId, matterbridge::windowCoveringClusterId, endProductTypeAttrId, &updated); +} +} // namespace + +MatterRolladenBridge::MatterRolladenBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterRolladenBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::window_covering_device::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + + // Create optional Lift Percent100ths attributes (mandatory only under PA feature flag, + // so window_covering_device does not create them automatically). + auto ep = _device->persistent_info.device_endpoint_id; + auto wcCluster = esp_matter::cluster::get(ep, matterbridge::windowCoveringClusterId); + if (wcCluster != nullptr) + { + esp_matter::cluster::window_covering::attribute::create_current_position_lift_percent_100ths( + wcCluster, nullable()); + esp_matter::cluster::window_covering::attribute::create_target_position_lift_percent_100ths( + wcCluster, nullable()); + } + applyWindowCoveringEndProductType(ep, ParamBRI_CHDeviceType); + setPosition(_channel->mainFunctionValue() ? 100 : 0); + } +} + +void MatterRolladenBridge::setPosition(uint8_t position) +{ + if (_device == nullptr) + return; + + auto ep = _device->persistent_info.device_endpoint_id; + auto matterPosition = static_cast(std::clamp(position, 0, 100) * 100); + matterbridge::reportU16(ep, matterbridge::windowCoveringClusterId, + matterbridge::currentPositionLiftPercent100thsAttrId, matterPosition); + matterbridge::reportU16(ep, matterbridge::windowCoveringClusterId, + matterbridge::targetPositionLiftPercent100thsAttrId, matterPosition); +} + +void MatterRolladenBridge::setMovement(MoveState) +{ +} + +void MatterRolladenBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::windowCoveringClusterId && + attributeId == matterbridge::targetPositionLiftPercent100thsAttrId) + { + _channel->commandPosition(this, static_cast(val->val.u16 / 100)); + } +} \ No newline at end of file diff --git a/src/Rolladen/MatterRolladen.h b/src/Rolladen/MatterRolladen.h new file mode 100644 index 0000000..9ef5444 --- /dev/null +++ b/src/Rolladen/MatterRolladen.h @@ -0,0 +1,16 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Rolladen/KnxChannelRolladen.h" + +class MatterRolladenBridge : public MatterBridgeDeviceBase, public RolladenBridge +{ +public: + explicit MatterRolladenBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setPosition(uint8_t position) override; + void setMovement(MoveState movement) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/Scene/HomeKitScene.cpp b/src/Scene/HomeKitScene.cpp index eb68eb6..7ed206b 100644 --- a/src/Scene/HomeKitScene.cpp +++ b/src/Scene/HomeKitScene.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitScene.h" diff --git a/src/Scene/KnxChannelScene.cpp b/src/Scene/KnxChannelScene.cpp index 17b908a..cc8d298 100644 --- a/src/Scene/KnxChannelScene.cpp +++ b/src/Scene/KnxChannelScene.cpp @@ -25,8 +25,23 @@ void KnxChannelScene::add(SceneBridge *sceneBridge) { sceneBridges.push_back(sceneBridge); sceneBridge->initialize(this); - sceneBridge->setActivating(false); +} + +void KnxChannelScene::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto sceneBridge = static_cast(bridge); + sceneBridge->setActivating(_lastActivatiation != 0); +} +void KnxChannelScene::syncAllBridgeStates() +{ + for (auto it = sceneBridges.begin(); it != sceneBridges.end(); ++it) + { + syncBridgeState(*it); + } } void KnxChannelScene::remove(SceneBridge *sceneBridge) diff --git a/src/Scene/KnxChannelScene.h b/src/Scene/KnxChannelScene.h index 902af5e..b598d0e 100644 --- a/src/Scene/KnxChannelScene.h +++ b/src/Scene/KnxChannelScene.h @@ -23,6 +23,8 @@ class KnxChannelScene : public KnxChannelBase void remove(SceneBridge* sceneBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; virtual void loop() override; diff --git a/src/Scene/MatterScene.cpp b/src/Scene/MatterScene.cpp new file mode 100644 index 0000000..205b950 --- /dev/null +++ b/src/Scene/MatterScene.cpp @@ -0,0 +1,41 @@ +#include "Scene/MatterScene.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +MatterSceneBridge::MatterSceneBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterSceneBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::on_off_light::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setActivating(false); + } +} + +void MatterSceneBridge::setActivating(bool activating) +{ + if (_device == nullptr) + return; + + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::onOffClusterId, + matterbridge::onOffAttrId, activating); +} + +void MatterSceneBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::onOffClusterId && attributeId == matterbridge::onOffAttrId && val->val.b) + _channel->commandActivate(this); +} \ No newline at end of file diff --git a/src/Scene/MatterScene.h b/src/Scene/MatterScene.h new file mode 100644 index 0000000..d7dd80e --- /dev/null +++ b/src/Scene/MatterScene.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Scene/KnxChannelScene.h" + +class MatterSceneBridge final : public MatterBridgeDeviceBase, public SceneBridge +{ +public: + explicit MatterSceneBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setActivating(bool activating) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/SmartHomeBridge.share.xml b/src/SmartHomeBridge.share.xml index e920786..5a3a886 100644 --- a/src/SmartHomeBridge.share.xml +++ b/src/SmartHomeBridge.share.xml @@ -8,6 +8,7 @@ + @@ -76,8 +77,11 @@ - - + + + + + @@ -453,15 +457,19 @@ - + + + - - - + + + + + @@ -471,10 +479,12 @@ - + - - + + + + @@ -483,12 +493,17 @@ + + + + + @@ -498,10 +513,6 @@ - - - - @@ -519,6 +530,7 @@ + @@ -529,6 +541,15 @@ + + + + + + + + + @@ -554,12 +575,23 @@ + + + + + + + + + + + - + - - + + diff --git a/src/SmartHomeBridge.templ.xml b/src/SmartHomeBridge.templ.xml index 0e8fb59..7ccf9f3 100644 --- a/src/SmartHomeBridge.templ.xml +++ b/src/SmartHomeBridge.templ.xml @@ -162,7 +162,7 @@ - + diff --git a/src/SmartHomeBridgeModule.cpp b/src/SmartHomeBridgeModule.cpp index 3188496..450947f 100644 --- a/src/SmartHomeBridgeModule.cpp +++ b/src/SmartHomeBridgeModule.cpp @@ -4,8 +4,11 @@ #ifndef SMARTHOMEBRIDGE_DEVICESONLY #include #include +#ifdef SMARTHOMEBRIDGE_HOMEKIT #include "HomeKitBridge.h" +#endif #include "HueBridge.h" +#include "MatterBridge.h" #endif #include "SmartHomeBridgeModule.h" #include "./Switch/KnxChannelSwitch.h" @@ -43,18 +46,9 @@ const std::string SmartHomeBridgeModule::version() return MODULE_SmartHomeBridge_Version; } -SmartHomeBridgeModule::~SmartHomeBridgeModule() -{ - if (_utf8Name != nullptr) - { - delete _utf8Name; - _utf8Name = nullptr; - } -} - const char *SmartHomeBridgeModule::getNameInUTF8() { - return _utf8Name; + return _utf8Name.c_str(); } void SmartHomeBridgeModule::setup(bool configured) @@ -73,17 +67,30 @@ void SmartHomeBridgeModule::setup() #else logDebugP("Setup Bridge"); #endif - _utf8Name = convertISO8859_15ToUTF8((const char *)ParamBRI_BridgeName); + _utf8Name = convertISO8859_15ToUTF8_string(ParamBRI_BridgeNameStr.c_str()); #ifndef SMARTHOMEBRIDGE_DEVICESONLY webServer = new WebServer(webServerPort); + bool matterEnabled = ParamBRI_MatterEnabled; + if (matterEnabled) + { + logDebugP("Matter enabled"); + addBridge(new MatterBridge()); + } + else + { + logDebugP("Matter disabled"); + } + +#ifdef SMARTHOMEBRIDGE_HOMEKIT bool homeKitEnabled = ParamBRI_HomeKitEnabled; if (homeKitEnabled) { logDebugP("Homekit enabled"); addBridge(new HomeKitBridge()); } +#endif bool hueEnabled = ParamBRI_HueEnabled; if (hueEnabled) @@ -244,38 +251,38 @@ void SmartHomeBridgeModule::startBridge() "/update", HTTP_POST, [this]() { webServer->sendHeader("Connection", "close"); - webServer->send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); - ESP.restart(); }, - [this]() - { - HTTPUpload &upload = webServer->upload(); - if (upload.status == UPLOAD_FILE_START) - { - Serial.printf("Update: %s\n", upload.filename.c_str()); - if (!Update.begin(UPDATE_SIZE_UNKNOWN)) - { // start with max available size - Update.printError(Serial); - } - } - else if (upload.status == UPLOAD_FILE_WRITE) - { - /* flashing firmware to ESP*/ - if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) - { - Update.printError(Serial); - } - } - else if (upload.status == UPLOAD_FILE_END) - { - if (Update.end(true)) - { // true to set the size to the current progress - Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); - } - else - { - Update.printError(Serial); - } - } + // webServer->send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + // ESP.restart(); }, + // [this]() + // { + // HTTPUpload &upload = webServer->upload(); + // if (upload.status == UPLOAD_FILE_START) + // { + // Serial.printf("Update: %s\n", upload.filename.c_str()); + // if (!Update.begin(UPDATE_SIZE_UNKNOWN)) + // { // start with max available size + // Update.printError(Serial); + // } + // } + // else if (upload.status == UPLOAD_FILE_WRITE) + // { + // /* flashing firmware to ESP*/ + // if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) + // { + // Update.printError(Serial); + // } + // } + // else if (upload.status == UPLOAD_FILE_END) + // { + // if (Update.end(true)) + // { // true to set the size to the current progress + // Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); + // } + // else + // { + // Update.printError(Serial); + // } + // } }); #endif logDebugP("Initialize briges"); @@ -293,6 +300,18 @@ void SmartHomeBridgeModule::startBridge() for (auto it = bridgeInterfaces->begin(); it != bridgeInterfaces->end(); ++it) (*it)->start(this); + // Push channel feedback state after all bridges are fully started. + // This avoids initial bridge reports during endpoint creation/startup. + if (_pChannels != nullptr) + { + for (uint8_t channelIndex = 0; channelIndex < numberOfChannels(); ++channelIndex) + { + auto channel = static_cast(_pChannels[channelIndex]); + if (channel != nullptr) + channel->syncAllBridgeStates(); + } + } + #ifndef SMARTHOMEBRIDGE_DEVICESONLY webServer->begin(); #endif diff --git a/src/SmartHomeBridgeModule.h b/src/SmartHomeBridgeModule.h index 939298b..823d4e1 100644 --- a/src/SmartHomeBridgeModule.h +++ b/src/SmartHomeBridgeModule.h @@ -19,7 +19,7 @@ class SmartHomeBridgeModule : public ChannelOwnerModule const uint16_t webServerPort = 80; WebServer* webServer = nullptr; #endif - const char* _utf8Name = nullptr; + std::string _utf8Name = ""; DynamicPointerArray* bridgeInterfaces = nullptr; volatile bool started = false; void startBridge(); @@ -38,7 +38,6 @@ class SmartHomeBridgeModule : public ChannelOwnerModule virtual OpenKNX::Channel* createChannel(uint8_t _channelIndex /* this parameter is used in macros, do not rename */); public: SmartHomeBridgeModule(); - ~SmartHomeBridgeModule(); KnxChannelBase* getChannel(uint8_t channelIndex); void addBridge(BridgeBase* bridge); const char* getNameInUTF8(); diff --git a/src/Switch/HomeKitSwitch.cpp b/src/Switch/HomeKitSwitch.cpp index 41be6d8..5e72d9c 100644 --- a/src/Switch/HomeKitSwitch.cpp +++ b/src/Switch/HomeKitSwitch.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitSwitch.h" diff --git a/src/Switch/KnxChannelSwitch.cpp b/src/Switch/KnxChannelSwitch.cpp index 2d56f76..f7beb05 100644 --- a/src/Switch/KnxChannelSwitch.cpp +++ b/src/Switch/KnxChannelSwitch.cpp @@ -26,9 +26,25 @@ void KnxChannelSwitch::add(SwitchBridge *switchBridge) { switchBridges.push_back(switchBridge); switchBridge->initialize(this); +} + +void KnxChannelSwitch::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto switchBridge = static_cast(bridge); switchBridge->setPower(koGet(KO_SWITCH_FEEDBACK)); } +void KnxChannelSwitch::syncAllBridgeStates() +{ + for (auto it = switchBridges.begin(); it != switchBridges.end(); ++it) + { + syncBridgeState(*it); + } +} + void KnxChannelSwitch::remove(SwitchBridge *switchBridge) { switchBridges.remove(switchBridge); diff --git a/src/Switch/KnxChannelSwitch.h b/src/Switch/KnxChannelSwitch.h index 51471b9..b24ffe3 100644 --- a/src/Switch/KnxChannelSwitch.h +++ b/src/Switch/KnxChannelSwitch.h @@ -18,6 +18,8 @@ class KnxChannelSwitch : public KnxChannelBase void remove(SwitchBridge* switchBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; virtual void processInputKo(GroupObject& ko) override; diff --git a/src/Switch/MatterSwitch.cpp b/src/Switch/MatterSwitch.cpp new file mode 100644 index 0000000..7e22116 --- /dev/null +++ b/src/Switch/MatterSwitch.cpp @@ -0,0 +1,49 @@ +#include "Switch/MatterSwitch.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include "knxprod.h" +#include + +MatterSwitchBridge::MatterSwitchBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterSwitchBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + // Use actuator device types for bridged KNX channels. on_off_switch is a + // controller type and is not suitable for Apple Home accessories. + uint32_t matterType = esp_matter::endpoint::on_off_light::get_device_type_id(); + if (ParamBRI_CHDeviceType == 11) + matterType = esp_matter::endpoint::on_off_plugin_unit::get_device_type_id(); + else if (ParamBRI_CHDeviceType == 20) + matterType = esp_matter::endpoint::on_off_light::get_device_type_id(); + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), matterType, + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setPower(_channel->mainFunctionValue()); + } +} + +void MatterSwitchBridge::setPower(bool value) +{ + if (_device == nullptr) + return; + + matterbridge::reportBool(_device->persistent_info.device_endpoint_id, matterbridge::onOffClusterId, + matterbridge::onOffAttrId, value); +} + +void MatterSwitchBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId == matterbridge::onOffClusterId && attributeId == matterbridge::onOffAttrId) + _channel->commandPower(this, val->val.b); +} \ No newline at end of file diff --git a/src/Switch/MatterSwitch.h b/src/Switch/MatterSwitch.h new file mode 100644 index 0000000..bccb1f9 --- /dev/null +++ b/src/Switch/MatterSwitch.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Switch/KnxChannelSwitch.h" + +class MatterSwitchBridge final : public MatterBridgeDeviceBase, public SwitchBridge +{ +public: + explicit MatterSwitchBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setPower(bool value) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file diff --git a/src/Thermostat/HomeKitThermostat.cpp b/src/Thermostat/HomeKitThermostat.cpp index 4bf9e67..128f187 100644 --- a/src/Thermostat/HomeKitThermostat.cpp +++ b/src/Thermostat/HomeKitThermostat.cpp @@ -1,4 +1,4 @@ -#ifndef SMARTHOMEBRIDGE_DEVICESONLY +#if !defined(SMARTHOMEBRIDGE_DEVICESONLY) && defined(SMARTHOMEBRIDGE_HOMEKIT) #include "HomeKitThermostat.h" HomeKitThermostat::HomeKitThermostat(int device) : diff --git a/src/Thermostat/KnxChannelThermostat.cpp b/src/Thermostat/KnxChannelThermostat.cpp index 8b13a9f..bff1b35 100644 --- a/src/Thermostat/KnxChannelThermostat.cpp +++ b/src/Thermostat/KnxChannelThermostat.cpp @@ -39,6 +39,14 @@ void KnxChannelThermostat::add(ThermostatBridge *thermostatBridge) { thermostatBridges.push_back(thermostatBridge); thermostatBridge->initialize(this); +} + +void KnxChannelThermostat::syncBridgeState(ChannelBridge *bridge) +{ + if (bridge == nullptr) + return; + + auto thermostatBridge = static_cast(bridge); thermostatBridge->setCurrentTemperature(koGet(KO_CURRENT_TEMPERATUR_FEEDBACK)); thermostatBridge->setTargetTemperature(koGet(KO_TARGET_TEMPERATURE_FEEDBACK)); if (ParamBRI_CHThermostatFeedbackKoType == 0) @@ -62,6 +70,14 @@ void KnxChannelThermostat::add(ThermostatBridge *thermostatBridge) updateBridgeFromKo(KO_COOLING_ACTIVE_PERCENT_FEEDBACK, thermostatBridge); } +void KnxChannelThermostat::syncAllBridgeStates() +{ + for (auto it = thermostatBridges.begin(); it != thermostatBridges.end(); ++it) + { + syncBridgeState(*it); + } +} + void KnxChannelThermostat::remove(ThermostatBridge *thermostatBridge) { thermostatBridges.remove(thermostatBridge); diff --git a/src/Thermostat/KnxChannelThermostat.h b/src/Thermostat/KnxChannelThermostat.h index 9ded813..907fbef 100644 --- a/src/Thermostat/KnxChannelThermostat.h +++ b/src/Thermostat/KnxChannelThermostat.h @@ -45,6 +45,8 @@ class KnxChannelThermostat : public KnxChannelBase void remove(ThermostatBridge* thermostatBridge); virtual ChannelBridge* createBridgeDevice(BridgeBase& bridge) override; virtual void deleteBridgeDevice(ChannelBridge* device) override; + virtual void syncBridgeState(ChannelBridge* bridge) override; + virtual void syncAllBridgeStates() override; protected: virtual void setup() override; virtual void processInputKo(GroupObject& ko) override; diff --git a/src/Thermostat/MatterThermostat.cpp b/src/Thermostat/MatterThermostat.cpp new file mode 100644 index 0000000..b0fc131 --- /dev/null +++ b/src/Thermostat/MatterThermostat.cpp @@ -0,0 +1,136 @@ +#include "Thermostat/MatterThermostat.h" + +#include "MatterBridge.h" +#include "Matter/MatterBridgeCommon.h" + +#include + +MatterThermostatBridge::MatterThermostatBridge(MatterBridge *bridge) : MatterBridgeDeviceBase(bridge) +{ +} + +void MatterThermostatBridge::setup(uint8_t _channelIndex) +{ + if (_bridge == nullptr || _bridge->node() == nullptr) + return; + + _device = esp_matter_bridge::create_device(_bridge->node(), _bridge->parentEndpointId(), + esp_matter::endpoint::thermostat::get_device_type_id(), + static_cast(this)); + if (_device != nullptr) + { + matterbridge::setDeviceName(_device, _channel->getNameInUTF8()); + setCurrentTemperature(_currentTemperature); + setTargetTemperature(_targetTemperature); + setMode(_mode); + setCurrentState(_currentState); + } +} + +void MatterThermostatBridge::setTargetTemperature(double temperature) +{ + _targetTemperature = temperature; + if (_device == nullptr) + return; + + matterbridge::reportI16(_device->persistent_info.device_endpoint_id, matterbridge::thermostatClusterId, + matterbridge::heatingSetpointAttrId, + static_cast(std::lround(temperature * 100.0))); + matterbridge::reportI16(_device->persistent_info.device_endpoint_id, matterbridge::thermostatClusterId, + matterbridge::coolingSetpointAttrId, + static_cast(std::lround(temperature * 100.0))); +} + +void MatterThermostatBridge::setCurrentTemperature(double temperature) +{ + _currentTemperature = temperature; + if (_device == nullptr) + return; + + matterbridge::reportI16(_device->persistent_info.device_endpoint_id, matterbridge::thermostatClusterId, + matterbridge::localTemperatureAttrId, + static_cast(std::lround(temperature * 100.0))); +} + +void MatterThermostatBridge::setMode(ThermostatMode mode) +{ + _mode = mode; + if (_device == nullptr) + return; + + auto matterMode = chip::app::Clusters::Thermostat::SystemModeEnum::kOff; + switch (mode) + { + case ThermostatModeHeating: + matterMode = chip::app::Clusters::Thermostat::SystemModeEnum::kHeat; + break; + case ThermostatModeCooling: + matterMode = chip::app::Clusters::Thermostat::SystemModeEnum::kCool; + break; + case ThermostatModeAutoHeatingCooling: + matterMode = chip::app::Clusters::Thermostat::SystemModeEnum::kAuto; + break; + case ThermostatModeOff: + default: + matterMode = chip::app::Clusters::Thermostat::SystemModeEnum::kOff; + break; + } + + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::thermostatClusterId, + matterbridge::systemModeAttrId, static_cast(matterMode)); +} + +void MatterThermostatBridge::setCurrentState(ThermostatCurrentState currentState) +{ + _currentState = currentState; + if (_device == nullptr) + return; + + auto matterState = chip::app::Clusters::Thermostat::ThermostatRunningModeEnum::kOff; + switch (currentState) + { + case ThermostatCurrentStateHeating: + matterState = chip::app::Clusters::Thermostat::ThermostatRunningModeEnum::kHeat; + break; + case ThermostatCurrentStateCooling: + matterState = chip::app::Clusters::Thermostat::ThermostatRunningModeEnum::kCool; + break; + case ThermostatCurrentStateOff: + default: + matterState = chip::app::Clusters::Thermostat::ThermostatRunningModeEnum::kOff; + break; + } + + matterbridge::reportU8(_device->persistent_info.device_endpoint_id, matterbridge::thermostatClusterId, + matterbridge::runningModeAttrId, static_cast(matterState)); +} + +void MatterThermostatBridge::handleMatterAttribute(esp_matter::attribute::callback_type_t, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) +{ + if (clusterId != matterbridge::thermostatClusterId) + return; + + if (attributeId == matterbridge::heatingSetpointAttrId || attributeId == matterbridge::coolingSetpointAttrId) + { + _channel->commandTargetTemperature(this, static_cast(val->val.i16) / 100.0); + } + else if (attributeId == matterbridge::systemModeAttrId) + { + switch (val->val.u8) + { + case static_cast(chip::app::Clusters::Thermostat::SystemModeEnum::kHeat): + _channel->commandMode(this, ThermostatModeHeating); + break; + case static_cast(chip::app::Clusters::Thermostat::SystemModeEnum::kCool): + _channel->commandMode(this, ThermostatModeCooling); + break; + case static_cast(chip::app::Clusters::Thermostat::SystemModeEnum::kAuto): + _channel->commandMode(this, ThermostatModeAutoHeatingCooling); + break; + default: + _channel->commandMode(this, ThermostatModeOff); + break; + } + } +} \ No newline at end of file diff --git a/src/Thermostat/MatterThermostat.h b/src/Thermostat/MatterThermostat.h new file mode 100644 index 0000000..d6d0f8b --- /dev/null +++ b/src/Thermostat/MatterThermostat.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Matter/MatterBridgeDeviceBase.h" +#include "Thermostat/KnxChannelThermostat.h" + +class MatterThermostatBridge final : public MatterBridgeDeviceBase, public ThermostatBridge +{ + double _targetTemperature = 21.0; + double _currentTemperature = 21.0; + ThermostatMode _mode = ThermostatModeOff; + ThermostatCurrentState _currentState = ThermostatCurrentStateOff; + +public: + explicit MatterThermostatBridge(MatterBridge *bridge); + + void setup(uint8_t channelIndex) override; + void setTargetTemperature(double temperature) override; + void setCurrentTemperature(double temperature) override; + void setMode(ThermostatMode mode) override; + void setCurrentState(ThermostatCurrentState currentState) override; + void handleMatterAttribute(esp_matter::attribute::callback_type_t type, uint32_t clusterId, + uint32_t attributeId, esp_matter_attr_val_t *val) override; +}; \ No newline at end of file