Add OpenKNX IDF component with TPUart integration

- Created CMakeLists.txt for the OpenKNX IDF component, ensuring dependencies on OpenKNX and TPUart submodules.
- Implemented Arduino compatibility header for basic functions like millis, delay, pinMode, and digitalRead.
- Developed EspIdfPlatform class for network interface management and multicast communication.
- Added EtsMemoryLoader for loading ETS memory snapshots and managing associations.
- Introduced TpuartUartInterface for UART communication with methods for reading, writing, and managing callbacks.
- Implemented arduino_compat.cpp for Arduino-like functionality on ESP-IDF.
- Created source files for platform and memory loader implementations.
- Updated submodules for knx, knx_dali_gw, and tpuart.

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-11 07:05:40 +08:00
parent 1b8753636f
commit 70367f53ca
20 changed files with 1359 additions and 20 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
idf_component_register(
SRCS "src/gateway_knx.cpp"
INCLUDE_DIRS "include"
REQUIRES dali_cpp esp_driver_uart freertos log lwip
REQUIRES dali_cpp esp_driver_uart freertos log lwip openknx_idf
)
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
@@ -12,6 +12,7 @@
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <vector>
@@ -32,15 +33,30 @@ struct GatewayKnxTpUartConfig {
uint32_t read_timeout_ms{20};
};
enum class GatewayKnxMappingMode : uint8_t {
kFormula = 0,
kGwReg1Direct = 1,
kManual = 2,
kEtsDatabase = 3,
};
struct GatewayKnxEtsAssociation {
uint16_t group_address{0};
uint16_t group_object_number{0};
};
struct GatewayKnxConfig {
bool dali_router_enabled{true};
bool ip_router_enabled{false};
bool tunnel_enabled{true};
bool multicast_enabled{true};
bool ets_database_enabled{true};
GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula};
uint8_t main_group{0};
uint16_t udp_port{kGatewayKnxDefaultUdpPort};
std::string multicast_address{kGatewayKnxDefaultMulticastAddress};
uint16_t individual_address{0x1101};
std::vector<GatewayKnxEtsAssociation> ets_associations;
GatewayKnxTpUartConfig tp_uart;
};
@@ -69,8 +85,12 @@ struct GatewayKnxDaliBinding {
uint8_t main_group{0};
uint8_t middle_group{0};
uint8_t sub_group{0};
GatewayKnxMappingMode mapping_mode{GatewayKnxMappingMode::kFormula};
int group_object_number{-1};
int channel_index{-1};
std::string address;
std::string name;
std::string object_role;
std::string datapoint_type;
GatewayKnxDaliDataType data_type{GatewayKnxDaliDataType::kUnknown};
GatewayKnxDaliTarget target;
@@ -79,6 +99,8 @@ struct GatewayKnxDaliBinding {
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode);
GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value);
const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type);
const char* GatewayKnxTargetKindToString(GatewayKnxDaliTargetKind kind);
std::optional<GatewayKnxDaliDataType> GatewayKnxDaliDataTypeForMiddleGroup(
@@ -94,6 +116,7 @@ class GatewayKnxBridge {
void setConfig(const GatewayKnxConfig& config);
const GatewayKnxConfig& config() const;
size_t etsBindingCount() const;
std::vector<GatewayKnxDaliBinding> describeDaliBindings() const;
DaliBridgeResult handleCemiFrame(const uint8_t* data, size_t len);
@@ -105,9 +128,14 @@ class GatewayKnxBridge {
GatewayKnxDaliDataType data_type,
GatewayKnxDaliTarget target,
const uint8_t* data, size_t len);
DaliBridgeResult executeEtsBindings(uint16_t group_address,
const std::vector<GatewayKnxDaliBinding>& bindings,
const uint8_t* data, size_t len);
void rebuildEtsBindings();
DaliBridgeEngine& engine_;
GatewayKnxConfig config_;
std::map<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
};
class GatewayKnxTpIpRouter {
+375 -1
View File
@@ -7,9 +7,13 @@
#include <algorithm>
#include <array>
#include <cerrno>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <initializer_list>
#include <set>
#include <utility>
#include <unistd.h>
@@ -49,6 +53,15 @@ constexpr uint8_t kTpUartLDataConfirmNegative = 0x0b;
constexpr uint8_t kTpUartLDataStart = 0x80;
constexpr uint8_t kTpUartLDataEnd = 0x40;
constexpr uint8_t kTpUartBusy = 0xc0;
constexpr uint16_t kGwReg1AdrKoOffset = 12;
constexpr uint16_t kGwReg1AdrKoBlockSize = 18;
constexpr uint16_t kGwReg1GrpKoOffset = 1164;
constexpr uint16_t kGwReg1GrpKoBlockSize = 17;
constexpr uint16_t kGwReg1AppKoBroadcastSwitch = 1;
constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2;
constexpr uint8_t kGwReg1KoSwitch = 0;
constexpr uint8_t kGwReg1KoDimmAbsolute = 3;
constexpr uint8_t kGwReg1KoColor = 6;
struct DecodedGroupWrite {
uint16_t group_address{0};
@@ -94,6 +107,108 @@ std::optional<std::string> ObjectStringAny(const DaliValue::Object& object,
return std::nullopt;
}
const DaliValue* ObjectValueAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
if (const auto* value = getObjectValue(object, key)) {
return value;
}
}
return nullptr;
}
std::string NormalizeModeString(std::string value) {
value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) {
return ch == '_' || ch == '-' || std::isspace(ch) != 0;
}),
value.end());
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
std::optional<uint16_t> ParseGroupAddressString(const std::string& value) {
int parts[3] = {-1, -1, -1};
size_t start = 0;
for (int index = 0; index < 3; ++index) {
const size_t slash = value.find('/', start);
const bool last = index == 2;
if ((slash == std::string::npos) != last) {
return std::nullopt;
}
const std::string token = value.substr(start, last ? std::string::npos : slash - start);
if (token.empty()) {
return std::nullopt;
}
char* end = nullptr;
errno = 0;
const long parsed = std::strtol(token.c_str(), &end, 10);
if (errno != 0 || end == token.c_str() || *end != '\0') {
return std::nullopt;
}
parts[index] = static_cast<int>(parsed);
start = slash + 1;
}
if (parts[0] < 0 || parts[0] > 31 || parts[1] < 0 || parts[1] > 7 || parts[2] < 0 ||
parts[2] > 255) {
return std::nullopt;
}
return static_cast<uint16_t>(((parts[0] & 0x1f) << 11) | ((parts[1] & 0x07) << 8) |
(parts[2] & 0xff));
}
std::optional<uint16_t> ObjectGroupAddressAny(const DaliValue::Object& object,
std::initializer_list<const char*> keys) {
for (const char* key : keys) {
const auto* value = getObjectValue(object, key);
if (value == nullptr) {
continue;
}
if (const auto raw = value->asInt()) {
if (raw.value() >= 0 && raw.value() <= 0xffff) {
return static_cast<uint16_t>(raw.value());
}
}
if (const auto raw = value->asString()) {
if (const auto parsed = ParseGroupAddressString(raw.value())) {
return parsed.value();
}
}
}
return std::nullopt;
}
std::vector<GatewayKnxEtsAssociation> ParseEtsAssociations(const DaliValue::Object& object) {
std::vector<GatewayKnxEtsAssociation> associations;
const auto* raw_associations = ObjectValueAny(
object, {"etsAssociations", "ets_associations", "etsBindings", "ets_bindings",
"associationTable", "association_table"});
const auto* array = raw_associations == nullptr ? nullptr : raw_associations->asArray();
if (array == nullptr) {
return associations;
}
associations.reserve(array->size());
for (const auto& item : *array) {
const auto* entry = item.asObject();
if (entry == nullptr) {
continue;
}
const auto group_address = ObjectGroupAddressAny(
*entry, {"groupAddress", "group_address", "address", "rawAddress", "raw_address"});
const auto object_number = ObjectIntAny(
*entry, {"objectNumber", "object_number", "groupObjectNumber", "group_object_number",
"ko", "asap"});
if (!group_address.has_value() || !object_number.has_value() || object_number.value() < 0 ||
object_number.value() > kGwReg1GrpKoOffset + (kGwReg1GrpKoBlockSize * 16)) {
continue;
}
associations.push_back(GatewayKnxEtsAssociation{
group_address.value(), static_cast<uint16_t>(object_number.value())});
}
return associations;
}
std::string TargetName(const GatewayKnxDaliTarget& target) {
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
@@ -386,6 +501,12 @@ std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value
.value_or(config.tunnel_enabled);
config.multicast_enabled = ObjectBoolAny(object, {"multicastEnabled", "multicast_enabled"})
.value_or(config.multicast_enabled);
if (const auto mode = ObjectStringAny(object, {"mappingMode", "mapping_mode"})) {
config.mapping_mode = GatewayKnxMappingModeFromString(mode.value());
}
config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled", "ets_database_enabled"})
.value_or(config.ets_database_enabled);
config.ets_associations = ParseEtsAssociations(object);
config.main_group = static_cast<uint8_t>(
std::clamp(ObjectIntAny(object, {"mainGroup", "main_group"}).value_or(config.main_group),
0, 31));
@@ -431,6 +552,8 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
out["ipRouterEnabled"] = config.ip_router_enabled;
out["tunnelEnabled"] = config.tunnel_enabled;
out["multicastEnabled"] = config.multicast_enabled;
out["etsDatabaseEnabled"] = config.ets_database_enabled;
out["mappingMode"] = GatewayKnxMappingModeToString(config.mapping_mode);
out["mainGroup"] = static_cast<int>(config.main_group);
out["udpPort"] = static_cast<int>(config.udp_port);
out["multicastAddress"] = config.multicast_address;
@@ -444,9 +567,47 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
serial["txBufferSize"] = static_cast<int>(config.tp_uart.tx_buffer_size);
serial["readTimeoutMs"] = static_cast<int>(config.tp_uart.read_timeout_ms);
out["tpUart"] = std::move(serial);
DaliValue::Array ets_associations;
ets_associations.reserve(config.ets_associations.size());
for (const auto& association : config.ets_associations) {
DaliValue::Object entry;
entry["groupAddress"] = static_cast<int>(association.group_address);
entry["groupObjectNumber"] = static_cast<int>(association.group_object_number);
ets_associations.emplace_back(std::move(entry));
}
out["etsAssociations"] = std::move(ets_associations);
return DaliValue(std::move(out));
}
const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode) {
switch (mode) {
case GatewayKnxMappingMode::kEtsDatabase:
return "ets_database";
case GatewayKnxMappingMode::kGwReg1Direct:
return "gw_reg1_direct";
case GatewayKnxMappingMode::kManual:
return "manual";
case GatewayKnxMappingMode::kFormula:
default:
return "formula";
}
}
GatewayKnxMappingMode GatewayKnxMappingModeFromString(const std::string& value) {
const std::string normalized = NormalizeModeString(value);
if (normalized == "gwreg1direct" || normalized == "gwreg1" ||
normalized == "gwreg1channel" || normalized == "channelindex") {
return GatewayKnxMappingMode::kGwReg1Direct;
}
if (normalized == "manual" || normalized == "database" || normalized == "db") {
return GatewayKnxMappingMode::kManual;
}
if (normalized == "etsdatabase" || normalized == "ets" || normalized == "openknx") {
return GatewayKnxMappingMode::kEtsDatabase;
}
return GatewayKnxMappingMode::kFormula;
}
const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) {
switch (data_type) {
case GatewayKnxDaliDataType::kSwitch:
@@ -522,14 +683,170 @@ std::string GatewayKnxGroupAddressString(uint16_t group_address) {
std::to_string(sub);
}
namespace {
uint16_t GwReg1GroupAddressForObject(uint8_t main_group, uint16_t object_number) {
return GatewayKnxGroupAddress(main_group, static_cast<uint8_t>(object_number >> 8),
static_cast<uint8_t>(object_number & 0xff));
}
GatewayKnxDaliBinding MakeGwReg1Binding(uint8_t main_group, uint16_t object_number,
int channel_index, const char* object_role,
GatewayKnxDaliDataType data_type,
GatewayKnxDaliTarget target) {
GatewayKnxDaliBinding binding;
binding.mapping_mode = GatewayKnxMappingMode::kGwReg1Direct;
binding.group_object_number = static_cast<int>(object_number);
binding.channel_index = channel_index;
binding.object_role = object_role;
binding.main_group = main_group;
binding.middle_group = static_cast<uint8_t>((object_number >> 8) & 0x07);
binding.sub_group = static_cast<uint8_t>(object_number & 0xff);
binding.group_address = GwReg1GroupAddressForObject(main_group, object_number);
binding.address = GatewayKnxGroupAddressString(binding.group_address);
binding.data_type = data_type;
binding.target = target;
binding.datapoint_type = DataTypeDpt(data_type);
binding.name = std::string("GW-REG1 ") + TargetName(target) + " - " +
DataTypeName(data_type);
return binding;
}
std::optional<GatewayKnxDaliBinding> GwReg1BindingForObject(uint8_t main_group,
uint16_t object_number) {
if (object_number == kGwReg1AppKoBroadcastSwitch) {
return MakeGwReg1Binding(
main_group, object_number, -1, "broadcast_switch", GatewayKnxDaliDataType::kSwitch,
GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127});
}
if (object_number == kGwReg1AppKoBroadcastDimm) {
return MakeGwReg1Binding(
main_group, object_number, -1, "broadcast_dimm_absolute",
GatewayKnxDaliDataType::kBrightness,
GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127});
}
const int adr_relative = static_cast<int>(object_number) - kGwReg1AdrKoOffset;
if (adr_relative >= 0 && adr_relative < kGwReg1AdrKoBlockSize * 64) {
const int channel = adr_relative / kGwReg1AdrKoBlockSize;
const int slot = adr_relative % kGwReg1AdrKoBlockSize;
const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kShortAddress, channel};
if (slot == kGwReg1KoSwitch) {
return MakeGwReg1Binding(main_group, object_number, channel, "switch",
GatewayKnxDaliDataType::kSwitch, target);
}
if (slot == kGwReg1KoDimmAbsolute) {
return MakeGwReg1Binding(main_group, object_number, channel, "dimm_absolute",
GatewayKnxDaliDataType::kBrightness, target);
}
if (slot == kGwReg1KoColor) {
return MakeGwReg1Binding(main_group, object_number, channel, "color",
GatewayKnxDaliDataType::kRgb, target);
}
}
const int group_relative = static_cast<int>(object_number) - kGwReg1GrpKoOffset;
if (group_relative >= 0 && group_relative < kGwReg1GrpKoBlockSize * 16) {
const int group = group_relative / kGwReg1GrpKoBlockSize;
const int slot = group_relative % kGwReg1GrpKoBlockSize;
const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kGroup, group};
if (slot == kGwReg1KoSwitch) {
return MakeGwReg1Binding(main_group, object_number, group, "switch",
GatewayKnxDaliDataType::kSwitch, target);
}
if (slot == kGwReg1KoDimmAbsolute) {
return MakeGwReg1Binding(main_group, object_number, group, "dimm_absolute",
GatewayKnxDaliDataType::kBrightness, target);
}
if (slot == kGwReg1KoColor) {
return MakeGwReg1Binding(main_group, object_number, group, "color",
GatewayKnxDaliDataType::kRgb, target);
}
}
return std::nullopt;
}
std::optional<GatewayKnxDaliBinding> EtsBindingForAssociation(uint8_t main_group,
const GatewayKnxEtsAssociation& association) {
auto binding = GwReg1BindingForObject(main_group, association.group_object_number);
if (!binding.has_value()) {
return std::nullopt;
}
binding->mapping_mode = GatewayKnxMappingMode::kEtsDatabase;
binding->group_address = association.group_address;
binding->address = GatewayKnxGroupAddressString(association.group_address);
binding->name = std::string("ETS ") + binding->name;
return binding;
}
} // namespace
GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {}
void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; }
void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) {
config_ = config;
rebuildEtsBindings();
}
const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; }
size_t GatewayKnxBridge::etsBindingCount() const {
size_t count = 0;
for (const auto& entry : ets_bindings_by_group_address_) {
count += entry.second.size();
}
return count;
}
std::vector<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() const {
std::vector<GatewayKnxDaliBinding> bindings;
std::set<uint16_t> ets_group_addresses;
if (config_.ets_database_enabled) {
for (const auto& entry : ets_bindings_by_group_address_) {
ets_group_addresses.insert(entry.first);
bindings.insert(bindings.end(), entry.second.begin(), entry.second.end());
}
}
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
bindings.reserve(2 + (64 * 3) + (16 * 3));
if (const auto binding = GwReg1BindingForObject(config_.main_group,
kGwReg1AppKoBroadcastSwitch)) {
if (ets_group_addresses.count(binding->group_address) == 0) {
bindings.push_back(binding.value());
}
}
if (const auto binding = GwReg1BindingForObject(config_.main_group,
kGwReg1AppKoBroadcastDimm)) {
if (ets_group_addresses.count(binding->group_address) == 0) {
bindings.push_back(binding.value());
}
}
for (int address = 0; address < 64; ++address) {
const uint16_t base = static_cast<uint16_t>(kGwReg1AdrKoOffset +
(address * kGwReg1AdrKoBlockSize));
for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) {
if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) {
if (ets_group_addresses.count(binding->group_address) == 0) {
bindings.push_back(binding.value());
}
}
}
}
for (int group = 0; group < 16; ++group) {
const uint16_t base = static_cast<uint16_t>(kGwReg1GrpKoOffset +
(group * kGwReg1GrpKoBlockSize));
for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) {
if (const auto binding = GwReg1BindingForObject(config_.main_group, base + slot)) {
if (ets_group_addresses.count(binding->group_address) == 0) {
bindings.push_back(binding.value());
}
}
}
}
return bindings;
}
bindings.reserve(4 * 81);
for (uint8_t middle = 1; middle <= 4; ++middle) {
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
@@ -542,6 +859,7 @@ std::vector<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() cons
continue;
}
GatewayKnxDaliBinding binding;
binding.mapping_mode = GatewayKnxMappingMode::kFormula;
binding.main_group = config_.main_group;
binding.middle_group = middle;
binding.sub_group = sub;
@@ -549,6 +867,10 @@ std::vector<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() cons
binding.address = GatewayKnxGroupAddressString(binding.group_address);
binding.data_type = data_type.value();
binding.target = target.value();
if (ets_group_addresses.count(binding.group_address) != 0) {
continue;
}
binding.object_role = GatewayKnxDataTypeToString(data_type.value());
binding.datapoint_type = DataTypeDpt(data_type.value());
binding.name = TargetName(target.value()) + " - " + DataTypeName(data_type.value());
bindings.push_back(std::move(binding));
@@ -570,12 +892,29 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons
if (!config_.dali_router_enabled) {
return ErrorResult(group_address, "KNX to DALI router disabled");
}
if (config_.ets_database_enabled) {
const auto ets_bindings = ets_bindings_by_group_address_.find(group_address);
if (ets_bindings != ets_bindings_by_group_address_.end()) {
return executeEtsBindings(group_address, ets_bindings->second, data, len);
}
}
const uint8_t main = static_cast<uint8_t>((group_address >> 11) & 0x1f);
const uint8_t middle = static_cast<uint8_t>((group_address >> 8) & 0x07);
const uint8_t sub = static_cast<uint8_t>(group_address & 0xff);
if (main != config_.main_group) {
return ErrorResult(group_address, "KNX main group does not match gateway config");
}
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
const uint16_t object_number = static_cast<uint16_t>((middle << 8) | sub);
const auto binding = GwReg1BindingForObject(config_.main_group, object_number);
if (!binding.has_value()) {
return ErrorResult(group_address, "unmapped GW-REG1 KNX object address");
}
return executeForDecodedWrite(group_address, binding->data_type, binding->target, data, len);
}
if (config_.mapping_mode == GatewayKnxMappingMode::kManual) {
return ErrorResult(group_address, "manual KNX mapping dataset is not configured");
}
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
if (!data_type.has_value() || !target.has_value()) {
@@ -584,6 +923,41 @@ DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, cons
return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len);
}
DaliBridgeResult GatewayKnxBridge::executeEtsBindings(
uint16_t group_address, const std::vector<GatewayKnxDaliBinding>& bindings,
const uint8_t* data, size_t len) {
if (bindings.empty()) {
return ErrorResult(group_address, "unmapped ETS KNX group address");
}
DaliBridgeResult result;
result.ok = true;
result.metadata["source"] = "ets_database";
result.metadata["groupAddress"] = GatewayKnxGroupAddressString(group_address);
result.metadata["bindingCount"] = static_cast<int>(bindings.size());
for (const auto& binding : bindings) {
DaliBridgeResult child = executeForDecodedWrite(group_address, binding.data_type,
binding.target, data, len);
result.ok = result.ok && child.ok;
result.results.emplace_back(child.toJson());
}
result.data = static_cast<int>(result.results.size());
if (!result.ok) {
result.error = "one or more ETS KNX bindings failed";
}
return result;
}
void GatewayKnxBridge::rebuildEtsBindings() {
ets_bindings_by_group_address_.clear();
for (const auto& association : config_.ets_associations) {
const auto binding = EtsBindingForAssociation(config_.main_group, association);
if (!binding.has_value()) {
continue;
}
ets_bindings_by_group_address_[association.group_address].push_back(binding.value());
}
}
DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address,
GatewayKnxDaliDataType data_type,
GatewayKnxDaliTarget target,