feat(gateway): add support for DALI scene handling and relative brightness adjustments

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-16 04:38:15 +08:00
parent 323ff24c04
commit 82142dd46c
4 changed files with 476 additions and 39 deletions
@@ -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();
@@ -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<GatewayKnxDaliBinding>& 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<uint16_t, std::vector<GatewayKnxDaliBinding>> ets_bindings_by_group_address_;
bool commissioning_scan_done_{true};
bool commissioning_assign_done_{true};
@@ -229,6 +229,21 @@ EtsMemorySnapshot EtsDeviceRuntime::snapshot() const {
return out;
}
uint8_t EtsDeviceRuntime::paramByte(uint32_t addr) const {
auto& device = const_cast<Bau07B0&>(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);
+453 -39
View File
@@ -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<int> 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<uint8_t> RawCommandAddressForTarget(const GatewayKnxDaliTarget& target) {
switch (target.kind) {
case GatewayKnxDaliTargetKind::kBroadcast:
return static_cast<uint8_t>(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<uint8_t>(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<int> 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<uint8_t>((search_address >> 16) & 0xff), sequence) &&
SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRM,
static_cast<uint8_t>((search_address >> 8) & 0xff), sequence) &&
SendRaw(engine, DALI_CMD_SPECIAL_SEARCHADDRL,
static_cast<uint8_t>(search_address & 0xff), sequence);
}
std::optional<bool> 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<uint32_t> 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<uint8_t> 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<uint8_t>((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<bool, 64> QueryUsedShortAddresses(DaliBridgeEngine& engine) {
std::array<bool, 64> used{};
for (int short_address = 0; short_address < static_cast<int>(used.size()); ++short_address) {
used[short_address] = QueryShort(engine, static_cast<uint8_t>(short_address),
DALI_CMD_QUERY_STATUS,
"knx-function-scan-query-used")
.has_value();
}
return used;
}
std::optional<uint8_t> NextFreeShortAddress(const std::array<bool, 64>& used) {
for (size_t index = 0; index < used.size(); ++index) {
if (!used[index]) {
return static_cast<uint8_t>(index);
}
}
return std::nullopt;
}
uint8_t Reg1SceneTypeForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
const uint32_t addr = kReg1SceneParamBlockOffset +
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
return static_cast<uint8_t>((runtime.paramByte(addr) >> 6) & 0x03);
}
bool Reg1SceneSaveAllowedForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
const uint32_t addr = kReg1SceneParamBlockOffset +
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
return runtime.paramBit(addr, 2);
}
uint8_t Reg1KnxSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
const uint32_t addr = kReg1SceneParamBlockOffset +
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index)) + 1;
return static_cast<uint8_t>((runtime.paramByte(addr) >> 1) & 0x7f);
}
uint8_t Reg1DaliSceneNumberForEntry(const openknx::EtsDeviceRuntime& runtime, size_t index) {
const uint32_t addr = kReg1SceneParamBlockOffset +
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
return static_cast<uint8_t>((runtime.paramByte(addr) >> 1) & 0x0f);
}
std::optional<GatewayKnxDaliTarget> Reg1SceneTargetForEntry(
const openknx::EtsDeviceRuntime& runtime, size_t index) {
const uint32_t base = kReg1SceneParamBlockOffset +
(kReg1SceneParamBlockSize * static_cast<uint32_t>(index));
switch (Reg1SceneTypeForEntry(runtime, index)) {
case kReg1SceneTypeAddress:
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kShortAddress,
static_cast<int>((runtime.paramByte(base + 2) >> 2) & 0x3f)};
case kReg1SceneTypeGroup:
return GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kGroup,
static_cast<int>((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<const sockaddr*>(&remote),
sizeof(remote)) == static_cast<int>(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<GatewayKnxDaliBinding> 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<int>(object_number) - kGwReg1AdrKoOffset;
if (adr_relative >= 0 && adr_relative < kGwReg1AdrKoBlockSize * 64) {
@@ -1019,6 +1247,10 @@ std::optional<GatewayKnxDaliBinding> 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<GatewayKnxDaliBinding> 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<GatewayKnxDaliBinding> 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<GatewayKnxDaliBinding> 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<uint16_t>(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<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() cons
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}) {
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<bool, 64> 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<uint8_t>(short_address.value());
ballast.high = static_cast<uint8_t>(
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_H,
"knx-function-scan-rand-h")
.value_or(0));
ballast.middle = static_cast<uint8_t>(
QueryShort(engine_, ballast.short_address, DALI_CMD_QUERY_RANDOM_ADDRESS_M,
"knx-function-scan-rand-m")
.value_or(0));
ballast.low = static_cast<uint8_t>(
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<uint8_t>((random_address.value() >> 16) & 0xff);
ballast.middle = static_cast<uint8_t>((random_address.value() >> 8) & 0xff);
ballast.low = static_cast<uint8_t>(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<unsigned>(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<unsigned>(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<unsigned>(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<unsigned>(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<int>(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<uint8_t>(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<int>(index);
copy_result.metadata["sceneNumber"] = static_cast<int>(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<int>(index);
store_result.metadata["sceneNumber"] = static_cast<int>(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<int>(index);
recall_result.metadata["sceneNumber"] = static_cast<int>(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<int>(matched_entries);
result.metadata["matchedSceneEntries"] = static_cast<int>(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<double>(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<int>(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<IpParameterObject>(
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<unsigned>(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);
}