|
|
|
@@ -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);
|
|
|
|
|
}
|
|
|
|
|