From 82142dd46cae361a4b6dea3b09e456979efa3e27 Mon Sep 17 00:00:00 2001 From: Tony Date: Sat, 16 May 2026 04:38:15 +0800 Subject: [PATCH] feat(gateway): add support for DALI scene handling and relative brightness adjustments Signed-off-by: Tony --- .../gateway_knx/include/ets_device_runtime.h | 2 + .../gateway_knx/include/gateway_knx.hpp | 6 + .../gateway_knx/src/ets_device_runtime.cpp | 15 + components/gateway_knx/src/gateway_knx.cpp | 492 ++++++++++++++++-- 4 files changed, 476 insertions(+), 39 deletions(-) diff --git a/components/gateway_knx/include/ets_device_runtime.h b/components/gateway_knx/include/ets_device_runtime.h index b5f3a38..8d300ad 100644 --- a/components/gateway_knx/include/ets_device_runtime.h +++ b/components/gateway_knx/include/ets_device_runtime.h @@ -43,6 +43,8 @@ class EtsDeviceRuntime { void setProgrammingMode(bool enabled); void toggleProgrammingMode(); EtsMemorySnapshot snapshot() const; + uint8_t paramByte(uint32_t addr) const; + bool paramBit(uint32_t addr, uint8_t shift) const; // Accessors for OpenKNX integration (DIB construction, IP parameter object). DeviceObject& deviceObject(); diff --git a/components/gateway_knx/include/gateway_knx.hpp b/components/gateway_knx/include/gateway_knx.hpp index 1464b57..b509a18 100644 --- a/components/gateway_knx/include/gateway_knx.hpp +++ b/components/gateway_knx/include/gateway_knx.hpp @@ -86,6 +86,8 @@ enum class GatewayKnxDaliDataType : uint8_t { kBrightness = 2, kColorTemperature = 3, kRgb = 4, + kBrightnessRelative = 5, + kScene = 6, }; enum class GatewayKnxDaliTargetKind : uint8_t { @@ -143,6 +145,7 @@ class GatewayKnxBridge { explicit GatewayKnxBridge(DaliBridgeEngine& engine); void setConfig(const GatewayKnxConfig& config); + void setRuntimeContext(const openknx::EtsDeviceRuntime* runtime); const GatewayKnxConfig& config() const; size_t etsBindingCount() const; @@ -164,6 +167,8 @@ class GatewayKnxBridge { GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target, const uint8_t* data, size_t len); + DaliBridgeResult executeReg1SceneWrite(uint16_t group_address, const uint8_t* data, + size_t len); DaliBridgeResult executeEtsBindings(uint16_t group_address, const std::vector& bindings, const uint8_t* data, size_t len); @@ -194,6 +199,7 @@ class GatewayKnxBridge { DaliBridgeEngine& engine_; GatewayKnxConfig config_; + const openknx::EtsDeviceRuntime* runtime_{nullptr}; std::map> ets_bindings_by_group_address_; bool commissioning_scan_done_{true}; bool commissioning_assign_done_{true}; diff --git a/components/gateway_knx/src/ets_device_runtime.cpp b/components/gateway_knx/src/ets_device_runtime.cpp index 51a8147..41d5f4b 100644 --- a/components/gateway_knx/src/ets_device_runtime.cpp +++ b/components/gateway_knx/src/ets_device_runtime.cpp @@ -229,6 +229,21 @@ EtsMemorySnapshot EtsDeviceRuntime::snapshot() const { return out; } +uint8_t EtsDeviceRuntime::paramByte(uint32_t addr) const { + auto& device = const_cast(device_); + if (!device.configured()) { + return 0; + } + return device.parameters().getByte(addr); +} + +bool EtsDeviceRuntime::paramBit(uint32_t addr, uint8_t shift) const { + if (shift > 7) { + return false; + } + return ((paramByte(addr) >> (7 - shift)) & 0x01U) != 0; +} + void EtsDeviceRuntime::setFunctionPropertyHandlers(FunctionPropertyHandler command_handler, FunctionPropertyHandler state_handler) { command_handler_ = std::move(command_handler); diff --git a/components/gateway_knx/src/gateway_knx.cpp b/components/gateway_knx/src/gateway_knx.cpp index 0909ca4..81d9360 100644 --- a/components/gateway_knx/src/gateway_knx.cpp +++ b/components/gateway_knx/src/gateway_knx.cpp @@ -107,11 +107,25 @@ constexpr uint16_t kGwReg1GrpKoOffset = 1164; constexpr uint16_t kGwReg1GrpKoBlockSize = 17; constexpr uint16_t kGwReg1AppKoBroadcastSwitch = 1; constexpr uint16_t kGwReg1AppKoBroadcastDimm = 2; +constexpr uint16_t kGwReg1AppKoScene = 5; constexpr uint8_t kGwReg1KoSwitch = 0; +constexpr uint8_t kGwReg1KoDimmRelative = 2; constexpr uint8_t kGwReg1KoDimmAbsolute = 3; constexpr uint8_t kGwReg1KoColor = 6; constexpr uint8_t kGwReg1KoSwitchState = 1; constexpr uint8_t kGwReg1KoDimmState = 4; +constexpr uint8_t kReg1SceneTelegramNumberMask = 0x3f; +constexpr uint8_t kReg1SceneTelegramStoreMask = 0x80; +constexpr size_t kReg1SceneEntryCount = 64; +constexpr uint32_t kReg1SceneParamBlockOffset = 47; +constexpr uint32_t kReg1SceneParamBlockSize = 4; +constexpr uint8_t kReg1SceneTypeNone = 0; +constexpr uint8_t kReg1SceneTypeAddress = 1; +constexpr uint8_t kReg1SceneTypeGroup = 2; +constexpr uint8_t kReg1SceneTypeBroadcast = 3; +constexpr uint8_t kDaliCmdStepDownOff = 0x07; +constexpr uint8_t kDaliCmdOnStepUp = 0x08; +constexpr uint8_t kDaliCmdStopFade = 0xff; constexpr uint8_t kReg1DaliFunctionObjectIndex = 160; constexpr uint8_t kReg1DaliFunctionPropertyId = 1; constexpr uint8_t kReg1FunctionType = 2; @@ -463,10 +477,14 @@ std::string DataTypeName(GatewayKnxDaliDataType data_type) { return "Switch"; case GatewayKnxDaliDataType::kBrightness: return "Dimmer"; + case GatewayKnxDaliDataType::kBrightnessRelative: + return "Dimmer Relative"; case GatewayKnxDaliDataType::kColorTemperature: return "Color Temperature"; case GatewayKnxDaliDataType::kRgb: return "RGB"; + case GatewayKnxDaliDataType::kScene: + return "Scene"; case GatewayKnxDaliDataType::kUnknown: default: return "Unknown"; @@ -479,10 +497,14 @@ const char* DataTypeDpt(GatewayKnxDaliDataType data_type) { return "DPST-1-1"; case GatewayKnxDaliDataType::kBrightness: return "DPST-5-1"; + case GatewayKnxDaliDataType::kBrightnessRelative: + return "DPST-3-7"; case GatewayKnxDaliDataType::kColorTemperature: return "DPST-7-600"; case GatewayKnxDaliDataType::kRgb: return "DPST-232-600"; + case GatewayKnxDaliDataType::kScene: + return "DPST-17-1"; case GatewayKnxDaliDataType::kUnknown: default: return ""; @@ -606,6 +628,75 @@ bool SendRawExt(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, const char* return ExecuteRaw(engine, BridgeOperation::sendExt, addr, cmd, sequence).ok; } +std::optional ExecuteRawQuery(DaliBridgeEngine& engine, uint8_t addr, uint8_t cmd, + const char* sequence) { + const auto result = ExecuteRaw(engine, BridgeOperation::query, addr, cmd, sequence); + if (!result.ok || !result.data.has_value()) { + return std::nullopt; + } + return result.data.value(); +} + +std::optional RawCommandAddressForTarget(const GatewayKnxDaliTarget& target) { + switch (target.kind) { + case GatewayKnxDaliTargetKind::kBroadcast: + return static_cast(0xff); + case GatewayKnxDaliTargetKind::kShortAddress: + if (target.address < 0 || target.address > 63) { + return std::nullopt; + } + return DaliComm::toCmdAddr(target.address); + case GatewayKnxDaliTargetKind::kGroup: + if (target.address < 0 || target.address > 15) { + return std::nullopt; + } + return static_cast(0x80 + (target.address * 2) + 1); + case GatewayKnxDaliTargetKind::kNone: + default: + return std::nullopt; + } +} + +DaliBridgeResult SendRawForTarget(DaliBridgeEngine& engine, uint16_t group_address, + const GatewayKnxDaliTarget& target, uint8_t cmd) { + const auto raw_addr = RawCommandAddressForTarget(target); + if (!raw_addr.has_value()) { + DaliBridgeResult result; + result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); + result.error = "invalid DALI target for raw command"; + return result; + } + DaliBridgeRequest request; + request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); + request.operation = BridgeOperation::send; + request.rawAddress = raw_addr.value(); + request.rawCommand = cmd; + request.metadata["sourceProtocol"] = "knx"; + request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); + request.metadata["daliTarget"] = TargetName(target); + return engine.execute(request); +} + +DaliBridgeResult SendRawExtForTarget(DaliBridgeEngine& engine, uint16_t group_address, + const GatewayKnxDaliTarget& target, uint8_t cmd) { + const auto raw_addr = RawCommandAddressForTarget(target); + if (!raw_addr.has_value()) { + DaliBridgeResult result; + result.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); + result.error = "invalid DALI target for raw command"; + return result; + } + DaliBridgeRequest request; + request.sequence = "knx-" + GatewayKnxGroupAddressString(group_address); + request.operation = BridgeOperation::sendExt; + request.rawAddress = raw_addr.value(); + request.rawCommand = cmd; + request.metadata["sourceProtocol"] = "knx"; + request.metadata["knxGroupAddress"] = GatewayKnxGroupAddressString(group_address); + request.metadata["daliTarget"] = TargetName(target); + return engine.execute(request); +} + std::optional MetadataInt(const DaliBridgeResult& result, const std::string& key) { return getObjectInt(result.metadata, key); } @@ -670,6 +761,134 @@ DaliBridgeResult IgnoredResult(uint16_t group_address, uint16_t group_object_num return result; } +bool SetSearchAddress(DaliBridgeEngine& engine, uint32_t search_address, const char* sequence) { + return SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRH, + static_cast((search_address >> 16) & 0xff), sequence) && + SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRM, + static_cast((search_address >> 8) & 0xff), sequence) && + SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRL, + static_cast(search_address & 0xff), sequence); +} + +std::optional CompareSelectedSearchAddress(DaliBridgeEngine& engine, uint32_t search_address, + const char* sequence) { + if (!SetSearchAddress(engine, search_address, sequence)) { + return std::nullopt; + } + const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_COMPARE, DALI_CMD_OFF, sequence); + if (!raw.has_value()) { + return std::nullopt; + } + return raw.value() == 0xff; +} + +std::optional FindLowestSelectedRandomAddress(DaliBridgeEngine& engine) { + const auto any = CompareSelectedSearchAddress(engine, 0x00ffffffu, + "knx-function-scan-compare-any"); + if (!any.has_value() || !any.value()) { + return std::nullopt; + } + + uint32_t low = 0; + uint32_t high = 0x00ffffffu; + while (low < high) { + const uint32_t mid = low + ((high - low) / 2); + const auto match = CompareSelectedSearchAddress(engine, mid, + "knx-function-scan-compare-binary"); + if (!match.has_value()) { + return std::nullopt; + } + if (match.value()) { + high = mid; + } else { + low = mid + 1; + } + } + if (!SetSearchAddress(engine, low, "knx-function-scan-compare-final")) { + return std::nullopt; + } + return low; +} + +std::optional QuerySelectedShortAddress(DaliBridgeEngine& engine) { + const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_QUERY_SHORT_ADDRESS, DALI_CMD_OFF, + "knx-function-scan-query-short"); + if (!raw.has_value() || raw.value() < 0 || raw.value() > 0xff || raw.value() == 0xff) { + return std::nullopt; + } + return static_cast((raw.value() >> 1) & 0x3f); +} + +bool VerifyShortAddress(DaliBridgeEngine& engine, uint8_t short_address) { + const auto raw = ExecuteRawQuery(engine, DALI_CMD_SPECIAL_VERIFY_SHORT_ADDRESS, + DaliComm::toCmdAddr(short_address), + "knx-function-scan-verify-short"); + return raw.has_value() && raw.value() == 0xff; +} + +std::array QueryUsedShortAddresses(DaliBridgeEngine& engine) { + std::array used{}; + for (int short_address = 0; short_address < static_cast(used.size()); ++short_address) { + used[short_address] = QueryShort(engine, static_cast(short_address), + DALI_CMD_QUERY_STATUS, + "knx-function-scan-query-used") + .has_value(); + } + return used; +} + +std::optional NextFreeShortAddress(const std::array& used) { + for (size_t index = 0; index < used.size(); ++index) { + if (!used[index]) { + return static_cast(index); + } + } + return std::nullopt; +} + +uint8_t Reg1SceneTypeForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { + const uint32_t addr = kReg1SceneParamBlockOffset + + (kReg1SceneParamBlockSize * static_cast(index)); + return static_cast((runtime.paramByte(addr) >> 6) & 0x03); +} + +bool Reg1SceneSaveAllowedForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { + const uint32_t addr = kReg1SceneParamBlockOffset + + (kReg1SceneParamBlockSize * static_cast(index)); + return runtime.paramBit(addr, 2); +} + +uint8_t Reg1KnxSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { + const uint32_t addr = kReg1SceneParamBlockOffset + + (kReg1SceneParamBlockSize * static_cast(index)) + 1; + return static_cast((runtime.paramByte(addr) >> 1) & 0x7f); +} + +uint8_t Reg1DaliSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) { + const uint32_t addr = kReg1SceneParamBlockOffset + + (kReg1SceneParamBlockSize * static_cast(index)); + return static_cast((runtime.paramByte(addr) >> 1) & 0x0f); +} + +std::optional Reg1SceneTargetForEntry( + const openknx::EtsDeviceRuntime& runtime, size_t index) { + const uint32_t base = kReg1SceneParamBlockOffset + + (kReg1SceneParamBlockSize * static_cast(index)); + switch (Reg1SceneTypeForEntry(runtime, index)) { + case kReg1SceneTypeAddress: + return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress, + static_cast((runtime.paramByte(base + 2) >> 2) & 0x3f)}; + case kReg1SceneTypeGroup: + return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup, + static_cast((runtime.paramByte(base + 3) >> 4) & 0x0f)}; + case kReg1SceneTypeBroadcast: + return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127}; + case kReg1SceneTypeNone: + default: + return std::nullopt; + } +} + bool SendAll(int sock, const uint8_t* data, size_t len, const sockaddr_in& remote) { return sendto(sock, data, len, 0, reinterpret_cast(&remote), sizeof(remote)) == static_cast(len); @@ -898,10 +1117,14 @@ const char* GatewayKnxDataTypeToString(GatewayKnxDaliDataType data_type) { return "switch"; case GatewayKnxDaliDataType::kBrightness: return "brightness"; + case GatewayKnxDaliDataType::kBrightnessRelative: + return "brightness_relative"; case GatewayKnxDaliDataType::kColorTemperature: return "color_temperature"; case GatewayKnxDaliDataType::kRgb: return "rgb"; + case GatewayKnxDaliDataType::kScene: + return "scene"; case GatewayKnxDaliDataType::kUnknown: default: return "unknown"; @@ -1009,6 +1232,11 @@ std::optional GwReg1BindingForObject(uint8_t main_group, 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) { @@ -1019,6 +1247,10 @@ std::optional GwReg1BindingForObject(uint8_t main_group, 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); @@ -1038,6 +1270,10 @@ std::optional GwReg1BindingForObject(uint8_t main_group, 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); @@ -1073,6 +1309,10 @@ void GatewayKnxBridge::setConfig(const GatewayKnxConfig& config) { rebuildEtsBindings(); } +void GatewayKnxBridge::setRuntimeContext(const openknx::EtsDeviceRuntime* runtime) { + runtime_ = runtime; +} + const GatewayKnxConfig& GatewayKnxBridge::config() const { return config_; } size_t GatewayKnxBridge::etsBindingCount() const { @@ -1093,7 +1333,7 @@ std::vector GatewayKnxBridge::describeDaliBindings() cons } } if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) { - bindings.reserve(2 + (64 * 3) + (16 * 3)); + 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) { @@ -1106,10 +1346,16 @@ std::vector GatewayKnxBridge::describeDaliBindings() cons 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, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { + 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()); @@ -1120,7 +1366,8 @@ std::vector GatewayKnxBridge::describeDaliBindings() cons for (int group = 0; group < 16; ++group) { const uint16_t base = static_cast(kGwReg1GrpKoOffset + (group * kGwReg1GrpKoBlockSize)); - for (const uint8_t slot : {kGwReg1KoSwitch, kGwReg1KoDimmAbsolute, kGwReg1KoColor}) { + 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()); @@ -1364,48 +1611,109 @@ bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len, commissioning_scan_done_ = false; commissioning_found_ballasts_.clear(); + const bool only_new = data[1] == 1; + const bool randomize = data[2] == 1; const bool delete_all = data[3] == 1; const bool assign = data[4] == 1; - if (assign || delete_all) { - DaliBridgeRequest allocate = FunctionRequest( - "knx-function-scan-allocate", - delete_all ? BridgeOperation::resetAndAllocateShortAddresses - : BridgeOperation::allocateAllShortAddresses); - allocate.value = DaliValue::Object{{"start", 0}, {"removeAddrFirst", delete_all}}; - engine_.execute(allocate); + ESP_LOGI(kTag, "REG1-Dali scan start onlyNew=%d randomize=%d deleteAll=%d assign=%d", + only_new, randomize, delete_all, assign); + + std::array used_addresses{}; + if (assign && !delete_all) { + used_addresses = QueryUsedShortAddresses(engine_); } - DaliBridgeRequest search = FunctionRequest("knx-function-scan-search", BridgeOperation::searchAddressRange); - search.value = DaliValue::Object{{"start", 0}, {"end", 63}}; - const auto search_result = engine_.execute(search); - if (search_result.ok) { - if (const auto* addresses_value = getObjectValue(search_result.metadata, "addresses")) { - if (const auto* addresses = addresses_value->asArray()) { - for (const auto& address_value : *addresses) { - const auto short_address = address_value.asInt(); - if (!short_address.has_value() || short_address.value() < 0 || short_address.value() > 63) { - continue; - } - GatewayKnxCommissioningBallast ballast; - ballast.short_address = static_cast(short_address.value()); - ballast.high = static_cast( - QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_H, - "knx-function-scan-rand-h") - .value_or(0)); - ballast.middle = static_cast( - QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_M, - "knx-function-scan-rand-m") - .value_or(0)); - ballast.low = static_cast( - QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_L, - "knx-function-scan-rand-l") - .value_or(0)); - commissioning_found_ballasts_.push_back(ballast); - } - } + const bool initialized = SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, + "knx-function-scan-terminate-prev") && + SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, + only_new ? DALI_CMD_STOP_FADE : DALI_CMD_OFF, + "knx-function-scan-init") && + SendRawExt(engine_, DALI_CMD_SPECIAL_INITIALIZE, + 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"); + commissioning_scan_done_ = true; + response->clear(); + return true; + } + + if (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"); + commissioning_scan_done_ = true; + response->clear(); + return true; } } + + if (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"); + commissioning_scan_done_ = true; + response->clear(); + return true; + } + } + + while (true) { + 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; + + if (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 for 0x%06x", + static_cast(random_address.value())); + break; + } + if (!SendRaw(engine_, DALI_CMD_SPECIAL_PROGRAM_SHORT_ADDRESS, + DaliComm::toCmdAddr(next_address.value()), + "knx-function-scan-program-short") || + !VerifyShortAddress(engine_, next_address.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; + ballast.short_address = next_address.value(); + } else { + ballast.short_address = QuerySelectedShortAddress(engine_).value_or(0xff); + } + + commissioning_found_ballasts_.push_back(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)); + + if (!SendRaw(engine_, DALI_CMD_SPECIAL_WITHDRAW, DALI_CMD_OFF, + "knx-function-scan-withdraw")) { + ESP_LOGW(kTag, "REG1-Dali scan failed while withdrawing matched device"); + break; + } + } + + SendRaw(engine_, DALI_CMD_SPECIAL_TERMINATE, DALI_CMD_OFF, + "knx-function-scan-terminate"); commissioning_scan_done_ = true; + ESP_LOGI(kTag, "REG1-Dali scan completed count=%u", + static_cast(commissioning_found_ballasts_.size())); response->clear(); return true; } @@ -1699,6 +2007,84 @@ DaliBridgeResult GatewayKnxBridge::executeEtsBindings( 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) { @@ -1714,7 +2100,8 @@ DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address GatewayKnxDaliDataType data_type, GatewayKnxDaliTarget target, const uint8_t* data, size_t len) { - if (target.kind == GatewayKnxDaliTargetKind::kNone) { + if (target.kind == GatewayKnxDaliTargetKind::kNone && + data_type != GatewayKnxDaliDataType::kScene) { return ErrorResult(group_address, "missing DALI target"); } switch (data_type) { @@ -1735,6 +2122,22 @@ DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address 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"); @@ -1757,6 +2160,8 @@ DaliBridgeResult GatewayKnxBridge::executeForDecodedWrite(uint16_t group_address 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"); @@ -1948,6 +2353,7 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { config_.individual_address, effectiveTunnelAddress(), std::move(tp_uart_interface)); + bridge_.setRuntimeContext(ets_device_.get()); knx_ip_parameters_ = std::make_unique( ets_device_->deviceObject(), ets_device_->platform()); openknx_configured_.store(ets_device_->configured()); @@ -1988,6 +2394,12 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { }); ets_device_->setGroupObjectWriteHandler( [this](uint16_t group_object_number, const uint8_t* data, size_t len) { + if (!shouldRouteDaliApplicationFrames()) { + return IgnoredResult( + GwReg1GroupAddressForObject(config_.main_group, group_object_number), + group_object_number, + "routing blocked by commissioning-only state"); + } const DaliBridgeResult result = group_object_write_handler_ ? group_object_write_handler_(group_object_number, data, len) @@ -2002,6 +2414,7 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() { ESP_LOGW(kTag, "OpenKNX group object %u not routed to DALI: %s", static_cast(group_object_number), result.error.c_str()); } + return result; }); ets_device_->setBusFrameSender([this](const uint8_t* data, size_t len) { sendTunnelIndication(data, len); @@ -2116,6 +2529,7 @@ void GatewayKnxTpIpRouter::finishTask() { SemaphoreGuard guard(openknx_lock_); setProgrammingLed(false); knx_ip_parameters_.reset(); + bridge_.setRuntimeContext(nullptr); ets_device_.reset(); openknx_configured_.store(false); }