#include "gateway_knx_private.hpp" namespace gateway { namespace { 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(object_number); binding.channel_index = channel_index; binding.object_role = object_role; binding.main_group = main_group; binding.middle_group = static_cast((object_number >> 8) & 0x07); binding.sub_group = static_cast(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 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}); } if (object_number == kGwReg1AppKoScene) { return MakeGwReg1Binding(main_group, object_number, -1, "scene", GatewayKnxDaliDataType::kScene, GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kNone, -1}); } const int adr_relative = static_cast(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 == kGwReg1KoDimmRelative) { return MakeGwReg1Binding(main_group, object_number, channel, "dimm_relative", GatewayKnxDaliDataType::kBrightnessRelative, 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(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 == kGwReg1KoDimmRelative) { return MakeGwReg1Binding(main_group, object_number, group, "dimm_relative", GatewayKnxDaliDataType::kBrightnessRelative, 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 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) { commissioning_lock_ = xSemaphoreCreateMutex(); if (commissioning_lock_ == nullptr) { ESP_LOGE(kTag, "Failed to create REG1-Dali commissioning mutex"); } } GatewayKnxBridge::~GatewayKnxBridge() { commissioning_scan_cancel_requested_.store(true, std::memory_order_release); if (commissioning_lock_ == nullptr) { return; } while (true) { TaskHandle_t task_handle = nullptr; { SemaphoreGuard guard(commissioning_lock_); task_handle = commissioning_scan_task_; } if (task_handle == nullptr) { break; } vTaskDelay(pdMS_TO_TICKS(10)); } vSemaphoreDelete(commissioning_lock_); commissioning_lock_ = nullptr; } void GatewayKnxBridge::CommissioningScanTaskEntry(void* arg) { auto* bridge = static_cast(arg); if (bridge != nullptr) { bridge->runCommissioningScanTask(); } vTaskDelete(nullptr); } void GatewayKnxBridge::runCommissioningScanTask() { if (commissioning_lock_ == nullptr) { return; } GatewayKnxReg1ScanOptions options; { SemaphoreGuard guard(commissioning_lock_); options = commissioning_scan_options_; } auto is_cancelled = [this]() { return commissioning_scan_cancel_requested_.load(std::memory_order_acquire); }; auto record_ballast = [this](const GatewayKnxCommissioningBallast& ballast) { if (commissioning_lock_ == nullptr) { return; } SemaphoreGuard guard(commissioning_lock_); if (commissioning_scan_cancel_requested_.load(std::memory_order_acquire)) { return; } commissioning_found_ballasts_.push_back(ballast); }; auto finish_scan = [this](bool clear_results) { if (commissioning_lock_ == nullptr) { return; } SemaphoreGuard guard(commissioning_lock_); if (clear_results) { commissioning_found_ballasts_.clear(); } commissioning_scan_done_ = true; commissioning_scan_cancel_requested_.store(false, std::memory_order_release); commissioning_scan_task_ = nullptr; }; ESP_LOGI(kTag, "REG1-Dali scan start onlyNew=%d randomize=%d deleteAll=%d assign=%d", options.only_new, options.randomize, options.delete_all, options.assign); bool clear_results = false; std::array used_addresses{}; do { if (is_cancelled()) { clear_results = true; break; } if (options.assign && !options.delete_all) { used_addresses = QueryUsedShortAddresses(engine_); } if (is_cancelled()) { clear_results = true; break; } const bool initialized = SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, "knx-function-scan-terminate-prev") && SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, options.only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, "knx-function-scan-init") && SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, options.only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, "knx-function-scan-init-repeat"); if (!initialized) { ESP_LOGW(kTag, "REG1-Dali scan failed during initialize"); break; } if (is_cancelled()) { clear_results = true; break; } if (options.delete_all) { const bool removed = SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xff, "knx-function-scan-clear-short-dtr") && SendRawExt(engine_, 0xff, DALI_CMD_STORE_DTR_AS_SHORT_ADDRESS, "knx-function-scan-clear-short"); if (!removed) { ESP_LOGW(kTag, "REG1-Dali scan failed while clearing short addresses"); break; } } if (is_cancelled()) { clear_results = true; break; } if (options.randomize) { const bool randomized = SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, "knx-function-scan-randomize") && SendRawExt(engine_, DALI_CMD_SPECIAL_RANDOMIZE, DALI_CMD_OFF, "knx-function-scan-randomize-repeat"); if (!randomized) { ESP_LOGW(kTag, "REG1-Dali scan failed while randomizing addresses"); break; } } while (!is_cancelled()) { auto log_and_record_ballast = [&](const GatewayKnxCommissioningBallast& ballast) { record_ballast(ballast); ESP_LOGI(kTag, "REG1-Dali scan found random=0x%02X%02X%02X short=%u", ballast.high, ballast.middle, ballast.low, static_cast(ballast.short_address)); }; auto withdraw_selected = [&]() { return SendRaw(engine_, DALI_CMD_SPECIAL_WITHDRAW, DALI_CMD_OFF, "knx-function-scan-withdraw"); }; auto program_selected_ballast = [&](uint8_t short_address, uint32_t fallback_random_address) -> std::optional { if (!ProgramShortAddressAndConfirm(engine_, short_address)) { return std::nullopt; } const auto random_address = QueryRandomAddressForShortAddress(engine_, short_address).value_or(fallback_random_address); GatewayKnxCommissioningBallast ballast; ballast.high = static_cast((random_address >> 16) & 0xff); ballast.middle = static_cast((random_address >> 8) & 0xff); ballast.low = static_cast(random_address & 0xff); ballast.short_address = short_address; return ballast; }; if (options.assign) { const auto next_address = NextFreeShortAddress(used_addresses); if (!next_address.has_value()) { ESP_LOGW(kTag, "REG1-Dali scan has no free short address left"); break; } const auto compare_base = FindSelectedCommissioningCompareBase(engine_); if (!compare_base.has_value()) { break; } const auto first_ballast = program_selected_ballast(next_address.value(), compare_base.value()); if (!first_ballast.has_value()) { ESP_LOGW(kTag, "REG1-Dali scan failed to program short address %u", static_cast(next_address.value())); break; } used_addresses[next_address.value()] = true; log_and_record_ballast(first_ballast.value()); if (is_cancelled()) { clear_results = true; break; } if (!withdraw_selected()) { ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device"); break; } auto compare_cursor = IncrementRandomAddress(compare_base.value()); bool compare_multi_failed = false; while (!is_cancelled() && compare_cursor.has_value()) { const auto contiguous_short = NextFreeShortAddress(used_addresses); if (!contiguous_short.has_value()) { break; } const auto next_search = IncrementRandomAddress(compare_cursor.value()); if (!next_search.has_value()) { break; } const auto matched = CompareSelectedSearchAddress( engine_, next_search.value(), "knx-function-scan-compare-multi"); if (!matched.has_value()) { compare_multi_failed = true; break; } if (!matched.value()) { break; } compare_cursor = next_search.value(); const auto ballast = program_selected_ballast(contiguous_short.value(), compare_cursor.value()); if (!ballast.has_value()) { ESP_LOGW(kTag, "REG1-Dali scan failed to program short address %u", static_cast(contiguous_short.value())); compare_multi_failed = true; break; } used_addresses[contiguous_short.value()] = true; log_and_record_ballast(ballast.value()); if (is_cancelled()) { clear_results = true; break; } if (!withdraw_selected()) { ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device"); compare_multi_failed = true; break; } } if (compare_multi_failed) { break; } if (is_cancelled()) { clear_results = true; break; } continue; } const auto random_address = FindLowestSelectedRandomAddress(engine_); if (!random_address.has_value()) { break; } GatewayKnxCommissioningBallast ballast; ballast.high = static_cast((random_address.value() >> 16) & 0xff); ballast.middle = static_cast((random_address.value() >> 8) & 0xff); ballast.low = static_cast(random_address.value() & 0xff); ballast.short_address = 0xff; ballast.short_address = QuerySelectedShortAddress(engine_).value_or(0xff); log_and_record_ballast(ballast); if (is_cancelled()) { clear_results = true; break; } if (!withdraw_selected()) { ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device"); break; } } if (is_cancelled()) { clear_results = true; } } while (false); SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, "knx-function-scan-terminate"); finish_scan(clear_results); if (clear_results) { ESP_LOGI(kTag, "REG1-Dali scan cancelled"); } else { size_t found_count = 0; { SemaphoreGuard guard(commissioning_lock_); found_count = commissioning_found_ballasts_.size(); } ESP_LOGI(kTag, "REG1-Dali scan completed count=%u", static_cast(found_count)); } } void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { config_ = config; rebuildEtsBindings(); } void GatewayKnxBridge::setRuntimeContext(const openknx::EtsDeviceRuntime* runtime) { runtime_ = runtime; } 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 GatewayKnxBridge::describeDaliBindings() const { std::vector bindings; std::set 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 * 4) + (16 * 4)); 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()); } } if (const auto binding = GwReg1BindingForObject(config_.main_group, kGwReg1AppKoScene)) { 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(kGwReg1AdrKoOffset + (address * kGwReg1AdrKoBlockSize)); for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmRelative, 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(kGwReg1GrpKoOffset + (group * kGwReg1GrpKoBlockSize)); for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmRelative, 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); if (!data_type.has_value()) { continue; } for (uint8_t sub = 0; sub <= 80; ++sub) { const auto target = GatewayKnxDaliTargetForSubgroup(sub); if (!target.has_value()) { continue; } GatewayKnxDaliBinding binding; binding.mapping_mode = GatewayKnxMappingMode::kFormula; binding.main_group = config_.main_group; binding.middle_group = middle; binding.sub_group = sub; binding.group_address = GatewayKnxGroupAddress(config_.main_group, middle, sub); 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)); } } return bindings; } bool GatewayKnxBridge::matchesGroupAddress(uint16_t group_address) const { if (!config_.dali_router_enabled) { return false; } if (config_.ets_database_enabled && ets_bindings_by_group_address_.find(group_address) != ets_bindings_by_group_address_.end()) { return true; } const uint8_t main = static_cast((group_address >> 11) & 0x1f); const uint8_t middle = static_cast((group_address >> 8) & 0x07); const uint8_t sub = static_cast(group_address & 0xff); if (main != config_.main_group) { return false; } if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { const uint16_t object_number = static_cast((middle << 8) | sub); return GwReg1BindingForObject(config_.main_group, object_number).has_value(); } if (config_.mapping_mode == GatewayKnxMappingMode::kManual) { return false; } return GatewayKnxDaliDataTypeForMiddleGroup(middle).has_value() && GatewayKnxDaliTargetForSubgroup(sub).has_value(); } DaliBridgeResult GatewayKnxBridge::handleGroupWrite(uint16_t group_address, const uint8_t* data, size_t len) { 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((group_address >> 11) & 0x1f); const uint8_t middle = static_cast((group_address >> 8) & 0x07); const uint8_t sub = static_cast(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((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()) { return ErrorResult(group_address, "unmapped KNX group address"); } return executeForDecodedWrite(group_address, data_type.value(), target.value(), data, len); } DaliBridgeResult GatewayKnxBridge::handleGroupObjectWrite(uint16_t group_object_number, const uint8_t* data, size_t len) { const uint16_t group_address = GwReg1GroupAddressForObject(config_.main_group, group_object_number); const std::string payload = HexBytes(data, len); ESP_LOGI(kTag, "OpenKNX KO write ko=%u derivedGa=%s len=%u payload=%s", static_cast(group_object_number), GatewayKnxGroupAddressString(group_address).c_str(), static_cast(len), payload.c_str()); if (!config_.dali_router_enabled) { return ErrorResult(group_address, "KNX to DALI router disabled"); } const auto binding = GwReg1BindingForObject(config_.main_group, group_object_number); if (!binding.has_value()) { ESP_LOGW(kTag, "OpenKNX KO write ignored ko=%u: unsupported GW-REG1 object", static_cast(group_object_number)); return IgnoredResult(group_address, group_object_number, "unsupported GW-REG1 group object"); } DaliBridgeResult result = executeForDecodedWrite(binding->group_address, binding->data_type, binding->target, data, len); result.metadata["source"] = "openknx_group_object"; result.metadata["groupObjectNumber"] = static_cast(group_object_number); result.metadata["objectRole"] = binding->object_role; if (result.ok) { ESP_LOGI(kTag, "OpenKNX KO write routed ko=%u role=%s target=%s", static_cast(group_object_number), binding->object_role.c_str(), TargetName(binding->target).c_str()); } else { ESP_LOGW(kTag, "OpenKNX KO write failed ko=%u role=%s error=%s", static_cast(group_object_number), binding->object_role.c_str(), result.error.c_str()); } return result; } bool GatewayKnxBridge::handleFunctionPropertyCommand(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId || data == nullptr || len == 0 || response == nullptr) { return false; } switch (data[0]) { case kReg1FunctionType: return handleReg1TypeCommand(data, len, response); case kReg1FunctionScan: return handleReg1ScanCommand(data, len, response); case kReg1FunctionAssign: return handleReg1AssignCommand(data, len, response); case kReg1FunctionEvgWrite: return handleReg1EvgWriteCommand(data, len, response); case kReg1FunctionEvgRead: return handleReg1EvgReadCommand(data, len, response); case kReg1FunctionSetScene: return handleReg1SetSceneCommand(data, len, response); case kReg1FunctionGetScene: return handleReg1GetSceneCommand(data, len, response); case kReg1FunctionIdentify: return handleReg1IdentifyCommand(data, len, response); default: return false; } } bool GatewayKnxBridge::handleFunctionPropertyState(uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len, std::vector* response) { if (object_index != kReg1DaliFunctionObjectIndex || property_id != kReg1DaliFunctionPropertyId || data == nullptr || len == 0 || response == nullptr) { return false; } switch (data[0]) { case kReg1FunctionScan: case 5: return handleReg1ScanState(data, len, response); case kReg1FunctionAssign: return handleReg1AssignState(data, len, response); case 7: return handleReg1FoundEvgsState(data, len, response); default: return false; } } bool GatewayKnxBridge::handleReg1TypeCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } const uint8_t short_address = data[1]; const auto type_response = QueryShort(engine_, short_address, DALI_CMD_QUERY_DEVICE_TYPE, "knx-function-type"); if (!type_response.has_value()) { *response = {0x01}; return true; } uint8_t device_type = static_cast(type_response.value()); if (device_type == kDaliDeviceTypeMultiple) { for (int index = 0; index < 16; ++index) { const auto next_type = QueryShort(engine_, short_address, DALI_CMD_QUERY_NEXT_DEVICE_TYPE, "knx-function-next-device-type"); if (!next_type.has_value()) { *response = {0x01}; return true; } if (next_type.value() == kDaliDeviceTypeNone) { break; } if (next_type.value() < 20) { device_type = static_cast(next_type.value()); } } } *response = {0x00, device_type}; if (device_type == kReg1DeviceTypeDt8) { if (!SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-dt8-select")) { *response = {0x02}; return true; } const auto color_features = QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_TYPE, "knx-function-color-type"); if (!color_features.has_value()) { *response = {0x02}; return true; } response->push_back(static_cast(color_features.value())); } return true; } bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } if (commissioning_lock_ == nullptr) { ESP_LOGE(kTag, "REG1-Dali scan unavailable: commissioning mutex missing"); response->clear(); return true; } SemaphoreGuard guard(commissioning_lock_); if (commissioning_scan_task_ != nullptr) { ESP_LOGW(kTag, "REG1-Dali scan request ignored while a scan is already running"); response->clear(); return true; } commissioning_scan_options_.only_new = data[1] == 1; commissioning_scan_options_.randomize = data[2] == 1; commissioning_scan_options_.delete_all = data[3] == 1; commissioning_scan_options_.assign = data[4] == 1; commissioning_scan_cancel_requested_.store(false, std::memory_order_release); commissioning_scan_done_ = false; commissioning_found_ballasts_.clear(); if (xTaskCreate(&GatewayKnxBridge::CommissioningScanTaskEntry, "gw_knx_scan", kCommissioningScanTaskStackSize, this, tskIDLE_PRIORITY + 1, &commissioning_scan_task_) != pdPASS) { commissioning_scan_done_ = true; ESP_LOGE(kTag, "Failed to start REG1-Dali commissioning scan task"); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1AssignCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } commissioning_assign_done_ = false; const uint8_t short_address = data[1] == 99 ? 0xff : data[1]; const bool ok = SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, 0x00, "knx-function-assign-init") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRL, data[4], "knx-function-assign-search-l") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRM, data[3], "knx-function-assign-search-m") && SendRaw(engine_, DALI_CMD_SPECIAL_SEARCHADDRH, data[2], "knx-function-assign-search-h") && SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, short_address == 0xff ? 0xff : DaliComm::toCmdAddr(short_address), "knx-function-assign-program") && SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, 0x00, "knx-function-assign-terminate"); commissioning_assign_done_ = true; if (!ok) { ESP_LOGW(kTag, "REG1-Dali assign command failed while programming short address %u", short_address); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1EvgWriteCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 10 || response == nullptr) { return false; } const uint8_t short_address = data[1]; DaliBridgeRequest settings = FunctionRequest("knx-function-evg-write-settings", BridgeOperation::setAddressSettings); settings.shortAddress = short_address; settings.value = DaliValue::Object{ {"minLevel", Reg1PercentToArc(data[2])}, {"maxLevel", Reg1PercentToArc(data[3])}, {"powerOnLevel", Reg1PercentToArc(data[4])}, {"systemFailureLevel", Reg1PercentToArc(data[5])}, {"fadeTime", static_cast((data[6] >> 4) & 0x0f)}, {"fadeRate", static_cast(data[6] & 0x0f)}, }; const bool settings_ok = engine_.execute(settings).ok; DaliBridgeRequest groups = FunctionRequest("knx-function-evg-write-groups", BridgeOperation::setGroupMask); groups.shortAddress = short_address; groups.value = static_cast(static_cast(data[8]) | (static_cast(data[9]) << 8)); const bool groups_ok = engine_.execute(groups).ok; if (!settings_ok || !groups_ok) { ESP_LOGW(kTag, "REG1-Dali EVG write command failed for short address %u", short_address); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1EvgReadCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } const uint8_t short_address = data[1]; response->assign(12, 0x00); (*response)[0] = 0x00; uint8_t error_byte = 0; DaliBridgeRequest settings = FunctionRequest("knx-function-evg-read-settings", BridgeOperation::getAddressSettings); settings.shortAddress = short_address; const auto settings_result = engine_.execute(settings); const auto set_level = [&](size_t index, const char* key, uint8_t error_mask) { const auto value = MetadataInt(settings_result, key); if (!settings_result.ok || !value.has_value()) { error_byte |= error_mask; (*response)[index] = 0xff; return; } (*response)[index] = Reg1ArcToPercent(static_cast(std::clamp(value.value(), 0, 255))); }; set_level(1, "minLevel", 0b00000001); set_level(2, "maxLevel", 0b00000010); set_level(3, "powerOnLevel", 0b00000100); set_level(4, "systemFailureLevel", 0b00001000); const auto fade_time = MetadataInt(settings_result, "fadeTime"); const auto fade_rate = MetadataInt(settings_result, "fadeRate"); if (!settings_result.ok || !fade_time.has_value() || !fade_rate.has_value()) { error_byte |= 0b00010000; (*response)[5] = 0xff; } else { (*response)[5] = static_cast(((fade_rate.value() & 0x0f) << 4) | (fade_time.value() & 0x0f)); } DaliBridgeRequest groups = FunctionRequest("knx-function-evg-read-groups", BridgeOperation::getGroupMask); groups.shortAddress = short_address; const auto groups_result = engine_.execute(groups); if (!groups_result.ok || !groups_result.data.has_value()) { error_byte |= 0b11000000; } else { const uint16_t mask = static_cast(groups_result.data.value()); (*response)[7] = static_cast(mask & 0xff); (*response)[8] = static_cast((mask >> 8) & 0xff); } (*response)[9] = error_byte; return true; } bool GatewayKnxBridge::handleReg1SetSceneCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 10 || response == nullptr) { return false; } const GatewayKnxDaliTarget target = Reg1SceneTarget(data[1]); const uint8_t scene = data[2] & 0x0f; const bool enabled = data[3] != 0; DaliBridgeRequest request = FunctionRequest( enabled ? "knx-function-set-scene" : "knx-function-remove-scene", enabled ? (data[4] == kReg1DeviceTypeDt8 ? BridgeOperation::storeDt8SceneSnapshot : BridgeOperation::setSceneLevel) : BridgeOperation::removeSceneLevel); ApplyTargetToRequest(target, &request); DaliValue::Object value{{"scene", static_cast(scene)}}; if (enabled) { value["brightness"] = static_cast(Reg1PercentToArc(data[6])); if (data[4] == kReg1DeviceTypeDt8) { if (data[5] == kReg1ColorTypeTw) { const uint16_t kelvin = ReadBe16(data + 7); value["colorMode"] = "color_temperature"; value["colorTemperature"] = static_cast(kelvin); } else { value["colorMode"] = "rgb"; value["r"] = static_cast(data[7]); value["g"] = static_cast(data[8]); value["b"] = static_cast(data[9]); } } } request.value = std::move(value); const auto result = engine_.execute(request); if (!result.ok) { ESP_LOGW(kTag, "REG1-Dali set scene command failed for scene %u", scene); } response->clear(); return true; } bool GatewayKnxBridge::handleReg1GetSceneCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 5 || response == nullptr) { return false; } const uint8_t short_address = data[1]; const uint8_t scene = data[2] & 0x0f; DaliBridgeRequest request = FunctionRequest("knx-function-get-scene", BridgeOperation::getSceneLevel); request.shortAddress = short_address; request.value = DaliValue::Object{{"scene", static_cast(scene)}}; const auto result = engine_.execute(request); if (!result.ok || !result.data.has_value()) { *response = {0xff}; return true; } const uint8_t raw_level = static_cast(std::clamp(result.data.value(), 0, 255)); *response = {static_cast(raw_level == 0xff ? 0xff : Reg1ArcToPercent(raw_level))}; if (raw_level != 0xff && data[3] == kReg1DeviceTypeDt8) { if (data[4] == kReg1ColorTypeTw) { response->resize(3, 0); SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, 0xe2, "knx-function-get-scene-ct-selector"); SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-get-scene-ct-dt-select"); const uint16_t mirek = static_cast( (QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE, "knx-function-get-scene-mirek-h") .value_or(0) << 8) | QueryShort(engine_, short_address, DALI_CMD_QUERY_CONTENT_DTR, "knx-function-get-scene-mirek-l") .value_or(0)); const uint16_t kelvin = mirek == 0 ? 0 : static_cast(1000000U / mirek); (*response)[1] = static_cast((kelvin >> 8) & 0xff); (*response)[2] = static_cast(kelvin & 0xff); } else { response->resize(4, 0); const std::array selectors{0xe9, 0xea, 0xeb}; for (size_t index = 0; index < selectors.size(); ++index) { SendRaw(engine_, DALI_CMD_SPECIAL_SET_DTR0, selectors[index], "knx-function-get-scene-rgb-selector"); SendRaw(engine_, DALI_CMD_SPECIAL_DT_SELECT, kReg1DeviceTypeDt8, "knx-function-get-scene-rgb-dt-select"); (*response)[index + 1] = static_cast( QueryShort(engine_, short_address, DALI_CMD_QUERY_COLOR_VALUE, "knx-function-get-scene-rgb-value") .value_or(0)); } } } return true; } bool GatewayKnxBridge::handleReg1IdentifyCommand(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } DaliBridgeRequest off = FunctionRequest("knx-function-identify-broadcast-off", BridgeOperation::off); off.metadata["broadcast"] = true; engine_.execute(off); DaliBridgeRequest identify = FunctionRequest("knx-function-identify-recall-max", BridgeOperation::recallMaxLevel); identify.shortAddress = data[1]; engine_.execute(identify); response->clear(); return true; } bool GatewayKnxBridge::handleReg1ScanState(const uint8_t* data, size_t len, std::vector* response) { if (len < 1 || response == nullptr) { return false; } response->clear(); if (commissioning_lock_ != nullptr) { SemaphoreGuard guard(commissioning_lock_); response->push_back(commissioning_scan_done_ ? 1 : 0); if (data[0] == kReg1FunctionScan) { response->push_back(static_cast( std::min(commissioning_found_ballasts_.size(), 0xff))); } return true; } response->push_back(commissioning_scan_done_ ? 1 : 0); if (data[0] == kReg1FunctionScan) { response->push_back(static_cast( std::min(commissioning_found_ballasts_.size(), 0xff))); } return true; } bool GatewayKnxBridge::handleReg1AssignState(const uint8_t* data, size_t len, std::vector* response) { if (len < 1 || response == nullptr) { return false; } *response = {static_cast(commissioning_assign_done_ ? 1 : 0)}; return true; } bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len, std::vector* response) { if (len < 2 || response == nullptr) { return false; } if (commissioning_lock_ != nullptr) { SemaphoreGuard guard(commissioning_lock_); if (data[1] == 254) { commissioning_scan_cancel_requested_.store(true, std::memory_order_release); commissioning_scan_done_ = true; commissioning_found_ballasts_.clear(); response->clear(); return true; } const size_t index = data[1]; response->clear(); response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0); if (index < commissioning_found_ballasts_.size()) { const auto& ballast = commissioning_found_ballasts_[index]; response->push_back(ballast.high); response->push_back(ballast.middle); response->push_back(ballast.low); response->push_back(ballast.short_address); } return true; } if (data[1] == 254) { commissioning_found_ballasts_.clear(); response->clear(); return true; } const size_t index = data[1]; response->clear(); response->push_back(index < commissioning_found_ballasts_.size() ? 1 : 0); if (index < commissioning_found_ballasts_.size()) { const auto& ballast = commissioning_found_ballasts_[index]; response->push_back(ballast.high); response->push_back(ballast.middle); response->push_back(ballast.low); response->push_back(ballast.short_address); } return true; } DaliBridgeResult GatewayKnxBridge::executeEtsBindings( uint16_t group_address, const std::vector& 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(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(result.results.size()); if (!result.ok) { result.error = "one or more ETS KNX bindings failed"; } return result; } DaliBridgeResult GatewayKnxBridge::executeReg1SceneWrite(uint16_t group_address, const uint8_t* data, size_t len) { if (runtime_ == nullptr || !runtime_->configured()) { return ErrorResult(group_address, "REG1 scene parameters are unavailable"); } if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing KNX scene payload"); } const uint8_t knx_scene = data[0] & kReg1SceneTelegramNumberMask; const bool store_scene = (data[0] & kReg1SceneTelegramStoreMask) != 0; DaliBridgeResult result; result.ok = true; result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); result.metadata["sourceProtocol"] = "knx"; result.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); result.metadata["sceneNumber"] = static_cast(knx_scene); result.metadata["sceneAction"] = std::string(store_scene ? "store" : "recall"); size_t matched_entries = 0; for (size_t index = 0; index < kReg1SceneEntryCount; ++index) { if (Reg1SceneTypeForEntry(*runtime_, index) == kReg1SceneTypeNone) { continue; } const uint8_t configured_knx_scene = Reg1KnxSceneNumberForEntry(*runtime_, index); if (configured_knx_scene == 0 || knx_scene != static_cast(configured_knx_scene - 1)) { continue; } if (store_scene && !Reg1SceneSaveAllowedForEntry(*runtime_, index)) { continue; } const auto target = Reg1SceneTargetForEntry(*runtime_, index); if (!target.has_value()) { continue; } ++matched_entries; const uint8_t dali_scene = Reg1DaliSceneNumberForEntry(*runtime_, index); if (store_scene) { DaliBridgeResult copy_result = SendRawExtForTarget(engine_, group_address, target.value(), DALI_CMD_STORE_ACTUAL_LEVEL_IN_THE_DTR); copy_result.metadata["sceneTableIndex"] = static_cast(index); copy_result.metadata["sceneNumber"] = static_cast(dali_scene); result.results.emplace_back(copy_result.toJson()); result.ok = result.ok && copy_result.ok; DaliBridgeResult store_result = SendRawExtForTarget(engine_, group_address, target.value(), DALI_CMD_SET_SCENE(dali_scene)); store_result.metadata["sceneTableIndex"] = static_cast(index); store_result.metadata["sceneNumber"] = static_cast(dali_scene); result.results.emplace_back(store_result.toJson()); result.ok = result.ok && store_result.ok; } else { DaliBridgeResult recall_result = SendRawForTarget(engine_, group_address, target.value(), DALI_CMD_GO_TO_SCENE(dali_scene)); recall_result.metadata["sceneTableIndex"] = static_cast(index); recall_result.metadata["sceneNumber"] = static_cast(dali_scene); result.results.emplace_back(recall_result.toJson()); result.ok = result.ok && recall_result.ok; } } if (matched_entries == 0) { result.ok = false; result.error = "no configured REG1 scene mapping matched KNX scene"; return result; } result.data = static_cast(matched_entries); result.metadata["matchedSceneEntries"] = static_cast(matched_entries); if (!result.ok) { result.error = "one or more REG1 scene operations 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, const uint8_t* data, size_t len) { if (target.kind == GatewayKnxDaliTargetKind::kNone && data_type != GatewayKnxDaliDataType::kScene) { return ErrorResult(group_address, "missing DALI target"); } switch (data_type) { case GatewayKnxDaliDataType::kSwitch: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT1 switch payload"); } DaliBridgeRequest request = RequestForTarget( group_address, target, (data[0] & 0x01) != 0 ? BridgeOperation::on : BridgeOperation::off); return engine_.execute(request); } case GatewayKnxDaliDataType::kBrightness: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT5 brightness payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setBrightnessPercent); request.value = (static_cast(data[0]) * 100.0) / 255.0; return engine_.execute(request); } case GatewayKnxDaliDataType::kBrightnessRelative: { if (data == nullptr || len < 1) { return ErrorResult(group_address, "missing DPT3 relative dimming payload"); } const uint8_t payload = data[0]; const uint8_t step_code = payload & 0x07; const bool dim_up = (payload & 0x10) != 0; const uint8_t cmd = step_code == 0 ? kDaliCmdStopFade : (dim_up ? kDaliCmdOnStepUp : kDaliCmdStepDownOff); DaliBridgeResult result = SendRawForTarget(engine_, group_address, target, cmd); result.metadata["knxRelativeStepCode"] = static_cast(step_code); result.metadata["knxRelativeDirection"] = step_code == 0 ? std::string("stop") : std::string(dim_up ? "up" : "down"); return result; } case GatewayKnxDaliDataType::kColorTemperature: { if (data == nullptr || len < 2) { return ErrorResult(group_address, "missing DPT7 color temperature payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setColorTemperature); request.value = static_cast(ReadBe16(data)); return engine_.execute(request); } case GatewayKnxDaliDataType::kRgb: { if (data == nullptr || len < 3) { return ErrorResult(group_address, "missing DPT232 RGB payload"); } DaliBridgeRequest request = RequestForTarget(group_address, target, BridgeOperation::setColourRGB); DaliValue::Object rgb; rgb["r"] = static_cast(data[0]); rgb["g"] = static_cast(data[1]); rgb["b"] = static_cast(data[2]); request.value = std::move(rgb); return engine_.execute(request); } case GatewayKnxDaliDataType::kScene: return executeReg1SceneWrite(group_address, data, len); case GatewayKnxDaliDataType::kUnknown: default: return ErrorResult(group_address, "unsupported KNX data type"); } } } // namespace gateway