20642e5ec3
This commit add comat parameter for some old DALI-1 control gear. Signed-off-by: Tony <tonylu@tony-cloud.com>
1229 lines
49 KiB
C++
1229 lines
49 KiB
C++
#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<int>(object_number);
|
|
binding.channel_index = channel_index;
|
|
binding.object_role = object_role;
|
|
binding.main_group = main_group;
|
|
binding.middle_group = static_cast<uint8_t>((object_number >> 8) & 0x07);
|
|
binding.sub_group = static_cast<uint8_t>(object_number & 0xff);
|
|
binding.group_address = GwReg1GroupAddressForObject(main_group, object_number);
|
|
binding.address = GatewayKnxGroupAddressString(binding.group_address);
|
|
binding.data_type = data_type;
|
|
binding.target = target;
|
|
binding.datapoint_type = DataTypeDpt(data_type);
|
|
binding.name = std::string("GW-REG1 ") + TargetName(target) + " - " +
|
|
DataTypeName(data_type);
|
|
return binding;
|
|
}
|
|
|
|
std::optional<GatewayKnxDaliBinding> GwReg1BindingForObject(uint8_t main_group,
|
|
uint16_t object_number) {
|
|
if (object_number == kGwReg1AppKoBroadcastSwitch) {
|
|
return MakeGwReg1Binding(
|
|
main_group, object_number, -1, "broadcast_switch", GatewayKnxDaliDataType::kSwitch,
|
|
GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127});
|
|
}
|
|
if (object_number == kGwReg1AppKoBroadcastDimm) {
|
|
return MakeGwReg1Binding(
|
|
main_group, object_number, -1, "broadcast_dimm_absolute",
|
|
GatewayKnxDaliDataType::kBrightness,
|
|
GatewayKnxDaliTarget{GatewayKnxDaliTargetKind::kBroadcast, 127});
|
|
}
|
|
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) {
|
|
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<int>(object_number) - kGwReg1GrpKoOffset;
|
|
if (group_relative >= 0 && group_relative < kGwReg1GrpKoBlockSize * 16) {
|
|
const int group = group_relative / kGwReg1GrpKoBlockSize;
|
|
const int slot = group_relative % kGwReg1GrpKoBlockSize;
|
|
const GatewayKnxDaliTarget target{GatewayKnxDaliTargetKind::kGroup, group};
|
|
if (slot == kGwReg1KoSwitch) {
|
|
return MakeGwReg1Binding(main_group, object_number, group, "switch",
|
|
GatewayKnxDaliDataType::kSwitch, target);
|
|
}
|
|
if (slot == 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<GatewayKnxDaliBinding> EtsBindingForAssociation(uint8_t main_group,
|
|
const GatewayKnxEtsAssociation& association) {
|
|
auto binding = GwReg1BindingForObject(main_group, association.group_object_number);
|
|
if (!binding.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
binding->mapping_mode = GatewayKnxMappingMode::kEtsDatabase;
|
|
binding->group_address = association.group_address;
|
|
binding->address = GatewayKnxGroupAddressString(association.group_address);
|
|
binding->name = std::string("ETS ") + binding->name;
|
|
return binding;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
GatewayKnxBridge::GatewayKnxBridge(DaliBridgeEngine& engine) : engine_(engine) {
|
|
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<GatewayKnxBridge*>(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<bool, 64> 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<unsigned>(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<GatewayKnxCommissioningBallast> {
|
|
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<uint8_t>((random_address >> 16) & 0xff);
|
|
ballast.middle = static_cast<uint8_t>((random_address >> 8) & 0xff);
|
|
ballast.low = static_cast<uint8_t>(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<unsigned>(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<unsigned>(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<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;
|
|
|
|
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<unsigned>(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<GatewayKnxDaliBinding> GatewayKnxBridge::describeDaliBindings() const {
|
|
std::vector<GatewayKnxDaliBinding> bindings;
|
|
std::set<uint16_t> ets_group_addresses;
|
|
if (config_.ets_database_enabled) {
|
|
for (const auto& entry : ets_bindings_by_group_address_) {
|
|
ets_group_addresses.insert(entry.first);
|
|
bindings.insert(bindings.end(), entry.second.begin(), entry.second.end());
|
|
}
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
|
|
bindings.reserve(2 + (64 * 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<uint16_t>(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<uint16_t>(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<uint8_t>((group_address >> 11) & 0x1f);
|
|
const uint8_t middle = static_cast<uint8_t>((group_address >> 8) & 0x07);
|
|
const uint8_t sub = static_cast<uint8_t>(group_address & 0xff);
|
|
if (main != config_.main_group) {
|
|
return false;
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
|
|
const uint16_t object_number = static_cast<uint16_t>((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<uint8_t>((group_address >> 11) & 0x1f);
|
|
const uint8_t middle = static_cast<uint8_t>((group_address >> 8) & 0x07);
|
|
const uint8_t sub = static_cast<uint8_t>(group_address & 0xff);
|
|
if (main != config_.main_group) {
|
|
return ErrorResult(group_address, "KNX main group does not match gateway config");
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kGwReg1Direct) {
|
|
const uint16_t object_number = static_cast<uint16_t>((middle << 8) | sub);
|
|
const auto binding = GwReg1BindingForObject(config_.main_group, object_number);
|
|
if (!binding.has_value()) {
|
|
return ErrorResult(group_address, "unmapped GW-REG1 KNX object address");
|
|
}
|
|
return executeForDecodedWrite(group_address, binding->data_type, binding->target, data, len);
|
|
}
|
|
if (config_.mapping_mode == GatewayKnxMappingMode::kManual) {
|
|
return ErrorResult(group_address, "manual KNX mapping dataset is not configured");
|
|
}
|
|
const auto data_type = GatewayKnxDaliDataTypeForMiddleGroup(middle);
|
|
const auto target = GatewayKnxDaliTargetForSubgroup(sub);
|
|
if (!data_type.has_value() || !target.has_value()) {
|
|
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<unsigned>(group_object_number),
|
|
GatewayKnxGroupAddressString(group_address).c_str(), static_cast<unsigned>(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<unsigned>(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<int>(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<unsigned>(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<unsigned>(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<uint8_t>* 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<uint8_t>* 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<uint8_t>* 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<uint8_t>(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<uint8_t>(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<uint8_t>(color_features.value()));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1ScanCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* 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<uint8_t>* 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<uint8_t>* 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<int>((data[6] >> 4) & 0x0f)},
|
|
{"fadeRate", static_cast<int>(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<int>(static_cast<uint16_t>(data[8]) |
|
|
(static_cast<uint16_t>(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<uint8_t>* 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<uint8_t>(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<uint8_t>(((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<uint16_t>(groups_result.data.value());
|
|
(*response)[7] = static_cast<uint8_t>(mask & 0xff);
|
|
(*response)[8] = static_cast<uint8_t>((mask >> 8) & 0xff);
|
|
}
|
|
(*response)[9] = error_byte;
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1SetSceneCommand(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* 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<int>(scene)}};
|
|
if (enabled) {
|
|
value["brightness"] = static_cast<int>(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<int>(kelvin);
|
|
} else {
|
|
value["colorMode"] = "rgb";
|
|
value["r"] = static_cast<int>(data[7]);
|
|
value["g"] = static_cast<int>(data[8]);
|
|
value["b"] = static_cast<int>(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<uint8_t>* 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<int>(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<uint8_t>(std::clamp(result.data.value(), 0, 255));
|
|
*response = {static_cast<uint8_t>(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<uint16_t>(
|
|
(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<uint16_t>(1000000U / mirek);
|
|
(*response)[1] = static_cast<uint8_t>((kelvin >> 8) & 0xff);
|
|
(*response)[2] = static_cast<uint8_t>(kelvin & 0xff);
|
|
} else {
|
|
response->resize(4, 0);
|
|
const std::array<uint8_t, 3> 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<uint8_t>(
|
|
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<uint8_t>* 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<uint8_t>* 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<uint8_t>(
|
|
std::min<size_t>(commissioning_found_ballasts_.size(), 0xff)));
|
|
}
|
|
return true;
|
|
}
|
|
response->push_back(commissioning_scan_done_ ? 1 : 0);
|
|
if (data[0] == kReg1FunctionScan) {
|
|
response->push_back(static_cast<uint8_t>(
|
|
std::min<size_t>(commissioning_found_ballasts_.size(), 0xff)));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1AssignState(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* response) {
|
|
if (len < 1 || response == nullptr) {
|
|
return false;
|
|
}
|
|
*response = {static_cast<uint8_t>(commissioning_assign_done_ ? 1 : 0)};
|
|
return true;
|
|
}
|
|
|
|
bool GatewayKnxBridge::handleReg1FoundEvgsState(const uint8_t* data, size_t len,
|
|
std::vector<uint8_t>* 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<GatewayKnxDaliBinding>& bindings,
|
|
const uint8_t* data, size_t len) {
|
|
if (bindings.empty()) {
|
|
return ErrorResult(group_address, "unmapped ETS KNX group address");
|
|
}
|
|
DaliBridgeResult result;
|
|
result.ok = true;
|
|
result.metadata["source"] = "ets_database";
|
|
result.metadata["groupAddress"] = GatewayKnxGroupAddressString(group_address);
|
|
result.metadata["bindingCount"] = static_cast<int>(bindings.size());
|
|
for (const auto& binding : bindings) {
|
|
DaliBridgeResult child = executeForDecodedWrite(group_address, binding.data_type,
|
|
binding.target, data, len);
|
|
result.ok = result.ok && child.ok;
|
|
result.results.emplace_back(child.toJson());
|
|
}
|
|
result.data = static_cast<int>(result.results.size());
|
|
if (!result.ok) {
|
|
result.error = "one or more ETS KNX bindings failed";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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) {
|
|
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<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");
|
|
}
|
|
DaliBridgeRequest request = RequestForTarget(group_address, target,
|
|
BridgeOperation::setColorTemperature);
|
|
request.value = static_cast<int>(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<int>(data[0]);
|
|
rgb["g"] = static_cast<int>(data[1]);
|
|
rgb["b"] = static_cast<int>(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
|