2c1aa28d4f
Co-authored-by: Copilot <copilot@github.com>
1214 lines
42 KiB
C++
1214 lines
42 KiB
C++
#include "gateway_controller.hpp"
|
|
|
|
#include "dali_domain.hpp"
|
|
#include "esp_log.h"
|
|
#include "esp_system.h"
|
|
#include "gateway_runtime.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <utility>
|
|
|
|
namespace gateway {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kTag = "gateway_controller";
|
|
constexpr const char* kStorageNamespace = "gateway_rt";
|
|
constexpr size_t kMaxNameBytes = 32;
|
|
|
|
std::string ShortKey(const char* prefix, uint8_t gateway_id, uint8_t slot) {
|
|
char key[16] = {0};
|
|
std::snprintf(key, sizeof(key), "%s%u_%u", prefix, gateway_id, slot);
|
|
return std::string(key);
|
|
}
|
|
|
|
std::vector<int> ParseCsv(std::string_view raw) {
|
|
std::vector<int> values;
|
|
size_t start = 0;
|
|
while (start < raw.size()) {
|
|
const size_t comma = raw.find(',', start);
|
|
const size_t end = comma == std::string_view::npos ? raw.size() : comma;
|
|
if (end > start) {
|
|
values.push_back(static_cast<int>(std::strtol(std::string(raw.substr(start, end - start)).c_str(),
|
|
nullptr, 10)));
|
|
}
|
|
if (comma == std::string_view::npos) {
|
|
break;
|
|
}
|
|
start = comma + 1;
|
|
}
|
|
return values;
|
|
}
|
|
|
|
std::string NormalizeName(std::string_view name) {
|
|
std::string normalized(name);
|
|
if (normalized.size() > kMaxNameBytes) {
|
|
normalized.resize(kMaxNameBytes);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
std::string AppendGatewaySuffix(std::string_view name, uint8_t gateway_id) {
|
|
std::string normalized = NormalizeName(name);
|
|
const std::string suffix = "_" + std::to_string(gateway_id);
|
|
if (normalized.size() > suffix.size() &&
|
|
normalized.compare(normalized.size() - suffix.size(), suffix.size(), suffix) == 0) {
|
|
return normalized;
|
|
}
|
|
const size_t max_base_len = kMaxNameBytes > suffix.size() ? kMaxNameBytes - suffix.size() : 0;
|
|
if (normalized.size() > max_base_len) {
|
|
normalized.resize(max_base_len);
|
|
}
|
|
normalized += suffix;
|
|
return normalized;
|
|
}
|
|
|
|
void AppendStringBytes(std::vector<uint8_t>& out, std::string_view value) {
|
|
for (const auto ch : value) {
|
|
out.push_back(static_cast<uint8_t>(ch));
|
|
}
|
|
}
|
|
|
|
void AppendPaddedName(std::vector<uint8_t>& out, std::string_view name) {
|
|
const auto normalized = NormalizeName(name);
|
|
out.push_back(static_cast<uint8_t>(normalized.size()));
|
|
AppendStringBytes(out, normalized);
|
|
while (out.size() < 5 + kMaxNameBytes) {
|
|
out.push_back(0x00);
|
|
}
|
|
}
|
|
|
|
const char* PhyKindToString(DaliPhyKind phy_kind) {
|
|
switch (phy_kind) {
|
|
case DaliPhyKind::kNativeHardware:
|
|
return "native";
|
|
case DaliPhyKind::kSerialUart:
|
|
return "serial";
|
|
case DaliPhyKind::kCustom:
|
|
default:
|
|
return "custom";
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
GatewayController::GatewayController(GatewayRuntime& runtime, DaliDomainService& dali_domain,
|
|
GatewayControllerConfig config)
|
|
: runtime_(runtime), dali_domain_(dali_domain), config_(config) {}
|
|
|
|
GatewayController::~GatewayController() {
|
|
closeStorage();
|
|
}
|
|
|
|
esp_err_t GatewayController::start() {
|
|
const esp_err_t err = openStorage() ? ESP_OK : ESP_FAIL;
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
|
|
const auto device_info = runtime_.deviceInfo();
|
|
ble_enabled_ = device_info.ble_enabled;
|
|
refreshRuntimeGatewayNames();
|
|
runtime_.setCommandAddressResolver([this](uint8_t gateway_id, uint8_t raw_addr) {
|
|
return resolveInternalGroupRawAddress(gateway_id, raw_addr);
|
|
});
|
|
dali_domain_.addRawFrameSink([this](const DaliRawFrame& frame) { handleDaliRawFrame(frame); });
|
|
|
|
for (const auto& channel : dali_domain_.channelInfo()) {
|
|
sceneStore(channel.gateway_id);
|
|
groupStore(channel.gateway_id);
|
|
dali_domain_.resetBus(channel.gateway_id);
|
|
publishPayload(channel.gateway_id, {0x02, channel.gateway_id, 0x88});
|
|
}
|
|
|
|
if (task_handle_ != nullptr) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
const BaseType_t created = xTaskCreate(&GatewayController::TaskEntry, "gateway_ctrl",
|
|
config_.task_stack_size, this, config_.task_priority,
|
|
&task_handle_);
|
|
if (created != pdPASS) {
|
|
task_handle_ = nullptr;
|
|
ESP_LOGE(kTag, "failed to create controller task");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
ESP_LOGI(kTag, "controller started channels=%u", static_cast<unsigned>(dali_domain_.channelCount()));
|
|
return ESP_OK;
|
|
}
|
|
|
|
bool GatewayController::enqueueCommandFrame(const std::vector<uint8_t>& frame) {
|
|
if (!GatewayRuntime::isGatewayCommandFrame(frame) || !GatewayRuntime::hasValidChecksum(frame)) {
|
|
ESP_LOGW(kTag, "dropped invalid command frame len=%u", static_cast<unsigned>(frame.size()));
|
|
return false;
|
|
}
|
|
if (!runtime_.enqueueCommand(frame)) {
|
|
if (runtime_.lastEnqueueDropReason() != GatewayRuntime::CommandDropReason::kDuplicate) {
|
|
ESP_LOGW(kTag, "dropped command frame reason=%d",
|
|
static_cast<int>(runtime_.lastEnqueueDropReason()));
|
|
}
|
|
return false;
|
|
}
|
|
if (task_handle_ != nullptr) {
|
|
xTaskNotifyGive(task_handle_);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void GatewayController::addNotificationSink(NotificationSink sink) {
|
|
if (sink) {
|
|
notification_sinks_.push_back(std::move(sink));
|
|
}
|
|
}
|
|
|
|
void GatewayController::addBleStateSink(BleStateSink sink) {
|
|
if (sink) {
|
|
ble_state_sinks_.push_back(std::move(sink));
|
|
}
|
|
}
|
|
|
|
void GatewayController::addWifiStateSink(WifiStateSink sink) {
|
|
if (sink) {
|
|
wifi_state_sinks_.push_back(std::move(sink));
|
|
}
|
|
}
|
|
|
|
void GatewayController::addGatewayNameSink(GatewayNameSink sink) {
|
|
if (sink) {
|
|
gateway_name_sinks_.push_back(std::move(sink));
|
|
}
|
|
}
|
|
|
|
bool GatewayController::setupMode() const {
|
|
return setup_mode_;
|
|
}
|
|
|
|
bool GatewayController::wirelessSetupMode() const {
|
|
return wireless_setup_mode_;
|
|
}
|
|
|
|
void GatewayController::setWirelessSetupMode(bool enabled) {
|
|
wireless_setup_mode_ = enabled;
|
|
}
|
|
|
|
bool GatewayController::bleEnabled() const {
|
|
return ble_enabled_;
|
|
}
|
|
|
|
bool GatewayController::wifiEnabled() const {
|
|
return wifi_enabled_;
|
|
}
|
|
|
|
bool GatewayController::ipRouterEnabled() const {
|
|
return ip_router_enabled_;
|
|
}
|
|
|
|
GatewayControllerSnapshot GatewayController::snapshot() {
|
|
GatewayControllerSnapshot out;
|
|
out.setup_mode = setup_mode_;
|
|
out.wireless_setup_mode = wireless_setup_mode_;
|
|
out.ble_enabled = ble_enabled_;
|
|
out.wifi_enabled = wifi_enabled_;
|
|
out.ip_router_enabled = ip_router_enabled_;
|
|
out.internal_scene_supported = config_.internal_scene_supported;
|
|
out.internal_group_supported = config_.internal_group_supported;
|
|
|
|
const auto channels = dali_domain_.channelInfo();
|
|
out.channels.reserve(channels.size());
|
|
for (const auto& channel : channels) {
|
|
const auto [scene_low, scene_high] = sceneMask(channel.gateway_id);
|
|
const auto [group_low, group_high] = groupMask(channel.gateway_id);
|
|
GatewayChannelSnapshot channel_snapshot;
|
|
channel_snapshot.channel_index = channel.channel_index;
|
|
channel_snapshot.gateway_id = channel.gateway_id;
|
|
channel_snapshot.name = channel.name;
|
|
channel_snapshot.phy = PhyKindToString(channel.phy_kind);
|
|
channel_snapshot.scene_mask_low = scene_low;
|
|
channel_snapshot.scene_mask_high = scene_high;
|
|
channel_snapshot.group_mask_low = group_low;
|
|
channel_snapshot.group_mask_high = group_high;
|
|
channel_snapshot.allocating = dali_domain_.isAllocAddr(channel.gateway_id);
|
|
channel_snapshot.last_alloc_addr = dali_domain_.lastAllocAddr(channel.gateway_id);
|
|
out.channels.push_back(std::move(channel_snapshot));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void GatewayController::TaskEntry(void* arg) {
|
|
static_cast<GatewayController*>(arg)->taskLoop();
|
|
}
|
|
|
|
void GatewayController::taskLoop() {
|
|
while (true) {
|
|
bool drained = false;
|
|
while (auto command = runtime_.popNextCommand()) {
|
|
drained = true;
|
|
dispatchCommand(*command);
|
|
runtime_.completeCurrentCommand();
|
|
}
|
|
if (!drained) {
|
|
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GatewayController::dispatchCommand(const std::vector<uint8_t>& command) {
|
|
if (command.size() < 7) {
|
|
ESP_LOGW(kTag, "command too short len=%u", static_cast<unsigned>(command.size()));
|
|
return;
|
|
}
|
|
|
|
const uint8_t gateway_id = command[2];
|
|
const uint8_t opcode = command[3];
|
|
const uint8_t addr = command[4];
|
|
const uint8_t data = command[5];
|
|
if (!hasGateway(gateway_id)) {
|
|
ESP_LOGW(kTag, "command for unknown gateway=%u opcode=0x%02x", gateway_id, opcode);
|
|
return;
|
|
}
|
|
|
|
switch (opcode) {
|
|
case 0x00:
|
|
esp_restart();
|
|
break;
|
|
case 0x01:
|
|
setup_mode_ = true;
|
|
break;
|
|
case 0x02:
|
|
ESP_LOGI(kTag, "legacy opcode 0x02 requested for gateway=%u", gateway_id);
|
|
break;
|
|
case 0x03:
|
|
if (!config_.ble_supported) {
|
|
publishPayload(gateway_id, {0x03, gateway_id, 0xff});
|
|
} else if (addr == 0) {
|
|
if (runtime_.setBleEnabled(false)) {
|
|
ble_enabled_ = runtime_.bleEnabled();
|
|
for (const auto& sink : ble_state_sinks_) {
|
|
sink(ble_enabled_);
|
|
}
|
|
publishPayload(gateway_id, {0x03, gateway_id, 0x00});
|
|
} else {
|
|
publishPayload(gateway_id, {0x03, gateway_id,
|
|
static_cast<uint8_t>(ble_enabled_ ? 1 : 0)});
|
|
}
|
|
} else if (addr == 1) {
|
|
if (runtime_.setBleEnabled(true)) {
|
|
ble_enabled_ = runtime_.bleEnabled();
|
|
for (const auto& sink : ble_state_sinks_) {
|
|
sink(ble_enabled_);
|
|
}
|
|
publishPayload(gateway_id, {0x03, gateway_id, 0x01});
|
|
} else {
|
|
publishPayload(gateway_id, {0x03, gateway_id,
|
|
static_cast<uint8_t>(ble_enabled_ ? 1 : 0)});
|
|
}
|
|
} else if (addr == 2) {
|
|
publishPayload(gateway_id, {0x03, gateway_id, static_cast<uint8_t>(ble_enabled_ ? 1 : 0)});
|
|
}
|
|
break;
|
|
case 0x04:
|
|
if (addr == 0) {
|
|
wifi_enabled_ = false;
|
|
wireless_setup_mode_ = false;
|
|
} else if (addr == 1) {
|
|
wifi_enabled_ = true;
|
|
wireless_setup_mode_ = false;
|
|
} else if (addr == 100 || addr == 101) {
|
|
wifi_enabled_ = true;
|
|
wireless_setup_mode_ = true;
|
|
}
|
|
for (const auto& sink : wifi_state_sinks_) {
|
|
sink(addr);
|
|
}
|
|
break;
|
|
case 0x05:
|
|
handleGatewayNameCommand(gateway_id, command);
|
|
break;
|
|
case 0x06: {
|
|
uint8_t feature = 0;
|
|
if (setup_mode_ && config_.setup_supported) {
|
|
feature |= 0x01;
|
|
}
|
|
if (wireless_setup_mode_ && config_.wifi_supported) {
|
|
feature |= 0x02;
|
|
}
|
|
if (config_.ip_router_supported && ip_router_enabled_) {
|
|
feature |= 0x08;
|
|
}
|
|
if (config_.internal_scene_supported) {
|
|
feature |= 0x10;
|
|
}
|
|
if (config_.internal_group_supported) {
|
|
feature |= 0x20;
|
|
}
|
|
publishPayload(gateway_id, {0x03, gateway_id, feature});
|
|
break;
|
|
}
|
|
case 0x07:
|
|
case 0x08:
|
|
dali_domain_.sendRaw(gateway_id, addr, data);
|
|
break;
|
|
case 0x09: {
|
|
const auto ids = gatewayIds();
|
|
if (addr >= 1 && addr <= ids.size()) {
|
|
const auto selected_gateway = ids[addr - 1];
|
|
publishPayload(gateway_id, {0x03, selected_gateway, selected_gateway});
|
|
} else {
|
|
publishPayload(gateway_id, {0x04, 0xff, 0x00});
|
|
}
|
|
break;
|
|
}
|
|
case 0x0A:
|
|
handleGatewayIdentityCommand(gateway_id, addr);
|
|
break;
|
|
case 0x10:
|
|
case 0x11:
|
|
dali_domain_.sendRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
|
|
break;
|
|
case 0x12:
|
|
if (addr == 0xff && data >= 0x10 && data <= 0x1f) {
|
|
const uint8_t scene_id = static_cast<uint8_t>(data - 0x10);
|
|
if (!executeScene(gateway_id, shortAddressFromRaw(addr), scene_id)) {
|
|
dali_domain_.sendRaw(gateway_id, addr, data);
|
|
}
|
|
} else {
|
|
dali_domain_.sendRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
|
|
}
|
|
break;
|
|
case 0x13:
|
|
dali_domain_.sendExtRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
|
|
break;
|
|
case 0x14: {
|
|
const auto result = dali_domain_.queryRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
|
|
if (result.has_value()) {
|
|
publishPayload(gateway_id, {0x03, gateway_id, result.value()});
|
|
} else {
|
|
publishPayload(gateway_id, {0x04, gateway_id, 0x00});
|
|
}
|
|
break;
|
|
}
|
|
case 0x15:
|
|
dali_domain_.queryRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), data);
|
|
break;
|
|
case 0x16:
|
|
dali_domain_.queryRaw(gateway_id, 0, 0);
|
|
break;
|
|
case 0x17:
|
|
if (command.size() >= 8) {
|
|
const int mirek = command[5] * 256 + command[6];
|
|
const uint8_t target = resolveInternalGroupRawAddress(gateway_id, addr);
|
|
dali_domain_.setColTempRaw(gateway_id, shortAddressFromRaw(target), mirek);
|
|
}
|
|
break;
|
|
case 0x18:
|
|
if (command.size() >= 10) {
|
|
const int x = command[5] * 256 + command[6];
|
|
const int y = command[7] * 256 + command[8];
|
|
dali_domain_.setColourRaw(gateway_id, resolveInternalGroupRawAddress(gateway_id, addr), x, y);
|
|
}
|
|
break;
|
|
case 0x30:
|
|
handleAllocationCommand(gateway_id, command);
|
|
break;
|
|
case 0x31:
|
|
break;
|
|
case 0x32:
|
|
dali_domain_.resetAndAllocAddr(gateway_id);
|
|
break;
|
|
case 0x37:
|
|
if (command.size() >= 8) {
|
|
int kelvin = command[5] * 256 + command[6];
|
|
const uint8_t target = resolveInternalGroupRawAddress(gateway_id, addr);
|
|
if (config_.color_temperature_max < config_.color_temperature_min) {
|
|
kelvin = reverseInRange(kelvin, config_.color_temperature_min,
|
|
config_.color_temperature_max);
|
|
}
|
|
dali_domain_.setColTemp(gateway_id, shortAddressFromRaw(target), kelvin);
|
|
}
|
|
break;
|
|
case 0x38:
|
|
if (command.size() >= 9) {
|
|
const uint8_t r = command[5];
|
|
const uint8_t g = command[6];
|
|
const uint8_t b = command[7];
|
|
const int target = shortAddressFromRaw(resolveInternalGroupRawAddress(gateway_id, addr));
|
|
if (r == 0 && g == 0 && b == 0) {
|
|
dali_domain_.off(gateway_id, target);
|
|
} else {
|
|
dali_domain_.setColourRGB(gateway_id, target, r, g, b);
|
|
}
|
|
}
|
|
break;
|
|
case 0xA0:
|
|
handleInternalSceneCommand(gateway_id, command);
|
|
break;
|
|
case 0xA2:
|
|
handleInternalGroupCommand(gateway_id, command);
|
|
break;
|
|
default:
|
|
ESP_LOGW(kTag, "unhandled opcode=0x%02x gateway=%u", opcode, gateway_id);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool GatewayController::hasGateway(uint8_t gateway_id) const {
|
|
const auto channels = dali_domain_.channelInfo();
|
|
return std::any_of(channels.begin(), channels.end(), [gateway_id](const DaliChannelInfo& channel) {
|
|
return channel.gateway_id == gateway_id;
|
|
});
|
|
}
|
|
|
|
std::vector<uint8_t> GatewayController::gatewayIds() const {
|
|
std::vector<uint8_t> ids;
|
|
const auto channels = dali_domain_.channelInfo();
|
|
ids.reserve(channels.size());
|
|
for (const auto& channel : channels) {
|
|
ids.push_back(channel.gateway_id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
std::string GatewayController::gatewayName(uint8_t gateway_id) const {
|
|
const auto channels = dali_domain_.channelInfo();
|
|
const auto it = std::find_if(channels.begin(), channels.end(), [gateway_id](const auto& channel) {
|
|
return channel.gateway_id == gateway_id;
|
|
});
|
|
if (it != channels.end()) {
|
|
return it->name;
|
|
}
|
|
return runtime_.gatewayName(gateway_id);
|
|
}
|
|
|
|
void GatewayController::refreshRuntimeGatewayNames() {
|
|
std::vector<std::string> assigned_names;
|
|
assigned_names.reserve(dali_domain_.channelCount());
|
|
|
|
for (const auto& channel : dali_domain_.channelInfo()) {
|
|
std::string runtime_name = NormalizeName(runtime_.gatewayName(channel.gateway_id));
|
|
if (std::any_of(assigned_names.begin(), assigned_names.end(),
|
|
[&runtime_name](const std::string& assigned_name) {
|
|
return assigned_name == runtime_name;
|
|
})) {
|
|
runtime_name = AppendGatewaySuffix(runtime_name, channel.gateway_id);
|
|
}
|
|
dali_domain_.updateChannelName(channel.gateway_id, runtime_name);
|
|
assigned_names.push_back(runtime_name);
|
|
}
|
|
}
|
|
|
|
void GatewayController::publishPayload(uint8_t, const std::vector<uint8_t>& payload) {
|
|
publishFrame(GatewayRuntime::buildNotificationFrame(payload));
|
|
}
|
|
|
|
void GatewayController::publishFrame(const std::vector<uint8_t>& frame) {
|
|
for (const auto& sink : notification_sinks_) {
|
|
sink(frame);
|
|
}
|
|
}
|
|
|
|
void GatewayController::handleDaliRawFrame(const DaliRawFrame& frame) {
|
|
if (frame.data.size() != 2 && frame.data.size() != 3) {
|
|
return;
|
|
}
|
|
if (setup_mode_ || dali_domain_.isAllocAddr(frame.gateway_id) ||
|
|
runtime_.hasActiveQueryCommand(frame.gateway_id)) {
|
|
return;
|
|
}
|
|
|
|
uint8_t addr = 0;
|
|
uint8_t data = 0;
|
|
if (frame.data.size() == 2) {
|
|
addr = frame.data[0];
|
|
data = frame.data[1];
|
|
if (addr == 0xBE) {
|
|
return;
|
|
}
|
|
} else {
|
|
addr = frame.data[1];
|
|
data = frame.data[2];
|
|
}
|
|
|
|
publishPayload(frame.gateway_id, {0x01, frame.gateway_id, addr, data});
|
|
}
|
|
|
|
uint8_t GatewayController::resolveInternalGroupRawAddress(uint8_t gateway_id, uint8_t raw_addr) {
|
|
if (raw_addr < 0x80 || raw_addr > 0x9f) {
|
|
return raw_addr;
|
|
}
|
|
const uint8_t slot = static_cast<uint8_t>((raw_addr - 0x80) / 2);
|
|
auto* group_data = group(gateway_id, slot);
|
|
if (group_data == nullptr || !group_data->enabled) {
|
|
return raw_addr;
|
|
}
|
|
return internalGroupRawTargetAddress(group_data->target_type, group_data->target_value, raw_addr);
|
|
}
|
|
|
|
uint8_t GatewayController::normalizeGroupTargetType(uint8_t target_type) {
|
|
return target_type <= 2 ? target_type : 2;
|
|
}
|
|
|
|
uint8_t GatewayController::normalizeGroupTargetValue(uint8_t target_type, uint8_t target_value) {
|
|
const uint8_t normalized_type = normalizeGroupTargetType(target_type);
|
|
if (normalized_type == 0) {
|
|
return std::min<uint8_t>(target_value, 63);
|
|
}
|
|
if (normalized_type == 1) {
|
|
return std::min<uint8_t>(target_value, 15);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint8_t GatewayController::internalGroupRawTargetAddress(uint8_t target_type, uint8_t target_value,
|
|
uint8_t raw_addr) {
|
|
const uint8_t normalized_type = normalizeGroupTargetType(target_type);
|
|
const uint8_t normalized_value = normalizeGroupTargetValue(normalized_type, target_value);
|
|
const uint8_t lsb = raw_addr & 0x01;
|
|
if (normalized_type == 0) {
|
|
return static_cast<uint8_t>(normalized_value * 2 + lsb);
|
|
}
|
|
if (normalized_type == 1) {
|
|
return static_cast<uint8_t>(0x80 + normalized_value * 2 + lsb);
|
|
}
|
|
return lsb == 0 ? 0xfe : 0xff;
|
|
}
|
|
|
|
int GatewayController::internalGroupDecTargetAddress(uint8_t target_type, uint8_t target_value) {
|
|
const uint8_t normalized_type = normalizeGroupTargetType(target_type);
|
|
const uint8_t normalized_value = normalizeGroupTargetValue(normalized_type, target_value);
|
|
if (normalized_type == 0) {
|
|
return normalized_value;
|
|
}
|
|
if (normalized_type == 1) {
|
|
return 64 + normalized_value;
|
|
}
|
|
return 127;
|
|
}
|
|
|
|
int GatewayController::shortAddressFromRaw(uint8_t raw_addr) {
|
|
return raw_addr / 2;
|
|
}
|
|
|
|
int GatewayController::reverseInRange(int value, int min_value, int max_value) {
|
|
return min_value + max_value - value;
|
|
}
|
|
|
|
GatewayController::SceneStore& GatewayController::sceneStore(uint8_t gateway_id) {
|
|
auto [it, inserted] = scenes_.try_emplace(gateway_id);
|
|
if (inserted) {
|
|
loadSceneStore(gateway_id, it->second);
|
|
}
|
|
return it->second;
|
|
}
|
|
|
|
GatewayController::GroupStore& GatewayController::groupStore(uint8_t gateway_id) {
|
|
auto [it, inserted] = groups_.try_emplace(gateway_id);
|
|
if (inserted) {
|
|
loadGroupStore(gateway_id, it->second);
|
|
}
|
|
return it->second;
|
|
}
|
|
|
|
GatewayController::InternalScene* GatewayController::scene(uint8_t gateway_id, uint8_t scene_id) {
|
|
if (scene_id >= 16) {
|
|
return nullptr;
|
|
}
|
|
return &sceneStore(gateway_id)[scene_id];
|
|
}
|
|
|
|
GatewayController::InternalGroup* GatewayController::group(uint8_t gateway_id, uint8_t group_id) {
|
|
if (group_id >= 16) {
|
|
return nullptr;
|
|
}
|
|
return &groupStore(gateway_id)[group_id];
|
|
}
|
|
|
|
bool GatewayController::setSceneEnabled(uint8_t gateway_id, uint8_t scene_id, bool enabled) {
|
|
auto* scene_data = scene(gateway_id, scene_id);
|
|
if (scene_data == nullptr) {
|
|
return false;
|
|
}
|
|
if (scene_data->enabled == enabled) {
|
|
return true;
|
|
}
|
|
scene_data->enabled = enabled;
|
|
return saveScene(gateway_id, scene_id);
|
|
}
|
|
|
|
bool GatewayController::setSceneDetail(uint8_t gateway_id, uint8_t scene_id, uint8_t brightness,
|
|
uint8_t color_mode, uint8_t data1, uint8_t data2,
|
|
uint8_t data3) {
|
|
auto* scene_data = scene(gateway_id, scene_id);
|
|
if (scene_data == nullptr) {
|
|
return false;
|
|
}
|
|
const uint8_t next_brightness = std::min<uint8_t>(brightness, 254);
|
|
const uint8_t next_color_mode = std::min<uint8_t>(color_mode, 2);
|
|
uint8_t next_data1 = 0;
|
|
uint8_t next_data2 = 0;
|
|
uint8_t next_data3 = 0;
|
|
if (next_color_mode == 0) {
|
|
next_data1 = data1;
|
|
next_data2 = data2;
|
|
} else if (next_color_mode == 1) {
|
|
next_data1 = data1;
|
|
next_data2 = data2;
|
|
next_data3 = data3;
|
|
}
|
|
if (scene_data->brightness == next_brightness && scene_data->color_mode == next_color_mode &&
|
|
scene_data->data1 == next_data1 && scene_data->data2 == next_data2 &&
|
|
scene_data->data3 == next_data3) {
|
|
return true;
|
|
}
|
|
scene_data->brightness = next_brightness;
|
|
scene_data->color_mode = next_color_mode;
|
|
scene_data->data1 = next_data1;
|
|
scene_data->data2 = next_data2;
|
|
scene_data->data3 = next_data3;
|
|
return saveScene(gateway_id, scene_id);
|
|
}
|
|
|
|
bool GatewayController::setSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) {
|
|
auto* scene_data = scene(gateway_id, scene_id);
|
|
if (scene_data == nullptr) {
|
|
return false;
|
|
}
|
|
const auto normalized = NormalizeName(name);
|
|
if (scene_data->name == normalized) {
|
|
return true;
|
|
}
|
|
scene_data->name = normalized;
|
|
return saveSceneName(gateway_id, scene_id, scene_data->name);
|
|
}
|
|
|
|
bool GatewayController::deleteScene(uint8_t gateway_id, uint8_t scene_id) {
|
|
auto* scene_data = scene(gateway_id, scene_id);
|
|
if (scene_data == nullptr) {
|
|
return false;
|
|
}
|
|
const bool already_default = !scene_data->enabled && scene_data->brightness == 254 &&
|
|
scene_data->color_mode == 2 && scene_data->data1 == 0 &&
|
|
scene_data->data2 == 0 && scene_data->data3 == 0 &&
|
|
scene_data->name.empty();
|
|
if (already_default) {
|
|
return true;
|
|
}
|
|
*scene_data = InternalScene{};
|
|
deleteSceneStorage(gateway_id, scene_id);
|
|
deleteSceneNameStorage(gateway_id, scene_id);
|
|
return true;
|
|
}
|
|
|
|
std::pair<uint8_t, uint8_t> GatewayController::sceneMask(uint8_t gateway_id) {
|
|
const auto& scenes = sceneStore(gateway_id);
|
|
uint16_t mask = 0;
|
|
for (size_t index = 0; index < scenes.size(); ++index) {
|
|
if (scenes[index].enabled) {
|
|
mask |= static_cast<uint16_t>(1U << index);
|
|
}
|
|
}
|
|
return {static_cast<uint8_t>(mask & 0xff), static_cast<uint8_t>((mask >> 8) & 0xff)};
|
|
}
|
|
|
|
bool GatewayController::executeScene(uint8_t gateway_id, int short_address, uint8_t scene_id) {
|
|
auto* scene_data = scene(gateway_id, scene_id);
|
|
if (scene_data == nullptr || !scene_data->enabled) {
|
|
return false;
|
|
}
|
|
if (scene_data->brightness <= 0) {
|
|
dali_domain_.off(gateway_id, short_address);
|
|
} else {
|
|
dali_domain_.setBright(gateway_id, short_address, scene_data->brightness);
|
|
}
|
|
if (scene_data->color_mode == 0) {
|
|
int kelvin = scene_data->data1 * 256 + scene_data->data2;
|
|
if (kelvin > 0) {
|
|
if (config_.color_temperature_max < config_.color_temperature_min) {
|
|
kelvin = reverseInRange(kelvin, config_.color_temperature_min,
|
|
config_.color_temperature_max);
|
|
}
|
|
dali_domain_.setColTemp(gateway_id, short_address, kelvin);
|
|
}
|
|
} else if (scene_data->color_mode == 1) {
|
|
if (scene_data->data1 != 0 || scene_data->data2 != 0 || scene_data->data3 != 0) {
|
|
dali_domain_.setColourRGB(gateway_id, short_address, scene_data->data1, scene_data->data2,
|
|
scene_data->data3);
|
|
} else if (scene_data->brightness <= 0) {
|
|
dali_domain_.off(gateway_id, short_address);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GatewayController::setGroupEnabled(uint8_t gateway_id, uint8_t group_id, bool enabled) {
|
|
auto* group_data = group(gateway_id, group_id);
|
|
if (group_data == nullptr) {
|
|
return false;
|
|
}
|
|
if (group_data->enabled == enabled) {
|
|
return true;
|
|
}
|
|
group_data->enabled = enabled;
|
|
return saveGroup(gateway_id, group_id);
|
|
}
|
|
|
|
bool GatewayController::setGroupDetail(uint8_t gateway_id, uint8_t group_id, uint8_t target_type,
|
|
uint8_t target_value) {
|
|
auto* group_data = group(gateway_id, group_id);
|
|
if (group_data == nullptr) {
|
|
return false;
|
|
}
|
|
const uint8_t next_target_type = normalizeGroupTargetType(target_type);
|
|
const uint8_t next_target_value = normalizeGroupTargetValue(next_target_type, target_value);
|
|
if (group_data->target_type == next_target_type && group_data->target_value == next_target_value) {
|
|
return true;
|
|
}
|
|
group_data->target_type = next_target_type;
|
|
group_data->target_value = next_target_value;
|
|
return saveGroup(gateway_id, group_id);
|
|
}
|
|
|
|
bool GatewayController::setGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) {
|
|
auto* group_data = group(gateway_id, group_id);
|
|
if (group_data == nullptr) {
|
|
return false;
|
|
}
|
|
const auto normalized = NormalizeName(name);
|
|
if (group_data->name == normalized) {
|
|
return true;
|
|
}
|
|
group_data->name = normalized;
|
|
return saveGroupName(gateway_id, group_id, group_data->name);
|
|
}
|
|
|
|
bool GatewayController::deleteGroup(uint8_t gateway_id, uint8_t group_id) {
|
|
auto* group_data = group(gateway_id, group_id);
|
|
if (group_data == nullptr) {
|
|
return false;
|
|
}
|
|
const bool already_default = !group_data->enabled && group_data->target_type == 2 &&
|
|
group_data->target_value == 0 && group_data->name.empty();
|
|
if (already_default) {
|
|
return true;
|
|
}
|
|
*group_data = InternalGroup{};
|
|
deleteGroupStorage(gateway_id, group_id);
|
|
deleteGroupNameStorage(gateway_id, group_id);
|
|
return true;
|
|
}
|
|
|
|
std::pair<uint8_t, uint8_t> GatewayController::groupMask(uint8_t gateway_id) {
|
|
const auto& groups = groupStore(gateway_id);
|
|
uint16_t mask = 0;
|
|
for (size_t index = 0; index < groups.size(); ++index) {
|
|
if (groups[index].enabled) {
|
|
mask |= static_cast<uint16_t>(1U << index);
|
|
}
|
|
}
|
|
return {static_cast<uint8_t>(mask & 0xff), static_cast<uint8_t>((mask >> 8) & 0xff)};
|
|
}
|
|
|
|
bool GatewayController::executeGroup(uint8_t gateway_id, uint8_t group_id) {
|
|
auto* group_data = group(gateway_id, group_id);
|
|
if (group_data == nullptr || !group_data->enabled) {
|
|
return false;
|
|
}
|
|
return dali_domain_.on(gateway_id,
|
|
internalGroupDecTargetAddress(group_data->target_type,
|
|
group_data->target_value));
|
|
}
|
|
|
|
void GatewayController::handleGatewayNameCommand(uint8_t gateway_id,
|
|
const std::vector<uint8_t>& command) {
|
|
const uint8_t op = command[4];
|
|
if (op == 0x00) {
|
|
const auto name = gatewayName(gateway_id);
|
|
std::vector<uint8_t> payload{0x05, gateway_id, op,
|
|
static_cast<uint8_t>(std::min(name.size(), kMaxNameBytes))};
|
|
AppendStringBytes(payload, NormalizeName(name));
|
|
publishPayload(gateway_id, payload);
|
|
return;
|
|
}
|
|
if (op != 0x01) {
|
|
return;
|
|
}
|
|
|
|
if (command.size() < 7) {
|
|
publishPayload(gateway_id, {0x05, gateway_id, op, 0x00});
|
|
return;
|
|
}
|
|
const size_t payload_end = command.size() - 1;
|
|
const size_t requested_len = std::min<size_t>(command[5], kMaxNameBytes);
|
|
const size_t available = payload_end > 6 ? payload_end - 6 : 0;
|
|
const size_t actual_len = std::min(requested_len, available);
|
|
std::string name;
|
|
name.reserve(actual_len);
|
|
for (size_t index = 0; index < actual_len; ++index) {
|
|
name.push_back(static_cast<char>(command[6 + index]));
|
|
}
|
|
if (runtime_.setGatewayName(gateway_id, name)) {
|
|
refreshRuntimeGatewayNames();
|
|
for (const auto& sink : gateway_name_sinks_) {
|
|
sink(gateway_id);
|
|
}
|
|
publishPayload(gateway_id, {0x05, gateway_id, op, 0x01});
|
|
} else {
|
|
publishPayload(gateway_id, {0x05, gateway_id, op, 0x00});
|
|
}
|
|
}
|
|
|
|
void GatewayController::handleGatewayIdentityCommand(uint8_t gateway_id, uint8_t op) {
|
|
std::string value;
|
|
if (op == 0x00) {
|
|
value = gatewayName(gateway_id);
|
|
} else if (op == 0x01) {
|
|
value = runtime_.gatewaySerialHex(gateway_id);
|
|
} else if (op == 0x02) {
|
|
value = runtime_.bleMacHex();
|
|
} else {
|
|
publishPayload(gateway_id, {0x0A, gateway_id, op, 0x00, 0x00});
|
|
return;
|
|
}
|
|
std::vector<uint8_t> payload{0x0A, gateway_id, op,
|
|
static_cast<uint8_t>(std::min(value.size(), kMaxNameBytes))};
|
|
AppendStringBytes(payload, value);
|
|
publishPayload(gateway_id, payload);
|
|
}
|
|
|
|
void GatewayController::handleAllocationCommand(uint8_t gateway_id,
|
|
const std::vector<uint8_t>& command) {
|
|
const uint8_t mode = command[4];
|
|
const uint8_t start_addr = command[5];
|
|
if (mode == 0) {
|
|
dali_domain_.stopAllocAddr(gateway_id);
|
|
} else if (mode == 1) {
|
|
dali_domain_.allocateAllAddr(gateway_id, start_addr);
|
|
} else if (mode == 2) {
|
|
dali_domain_.resetAndAllocAddr(gateway_id);
|
|
} else if (mode == 3) {
|
|
publishPayload(gateway_id, {0x30, gateway_id,
|
|
static_cast<uint8_t>(dali_domain_.isAllocAddr(gateway_id) ? 1 : 0),
|
|
static_cast<uint8_t>(dali_domain_.lastAllocAddr(gateway_id) & 0xff)});
|
|
}
|
|
}
|
|
|
|
void GatewayController::handleInternalSceneCommand(uint8_t gateway_id,
|
|
const std::vector<uint8_t>& command) {
|
|
if (command.size() < 7) {
|
|
publishPayload(gateway_id, {0xA1, gateway_id, 0xff, 0xff, 0x01});
|
|
return;
|
|
}
|
|
const uint8_t op = command[4];
|
|
const uint8_t scene_id = command[5];
|
|
if (scene_id > 15) {
|
|
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x01});
|
|
return;
|
|
}
|
|
|
|
auto* scene_data = scene(gateway_id, scene_id);
|
|
switch (op) {
|
|
case 0x00:
|
|
publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, true)
|
|
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
|
|
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
|
|
break;
|
|
case 0x01:
|
|
publishPayload(gateway_id, setSceneEnabled(gateway_id, scene_id, false)
|
|
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
|
|
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
|
|
break;
|
|
case 0x02:
|
|
publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id,
|
|
static_cast<uint8_t>(scene_data->enabled ? 1 : 0)});
|
|
break;
|
|
case 0x03: {
|
|
const auto [low, high] = sceneMask(gateway_id);
|
|
publishPayload(gateway_id, {0xA0, gateway_id, op, low, high});
|
|
break;
|
|
}
|
|
case 0x04:
|
|
publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, scene_data->brightness,
|
|
scene_data->color_mode, scene_data->data1, scene_data->data2,
|
|
scene_data->data3});
|
|
break;
|
|
case 0x05:
|
|
if (command.size() >= 11 && setSceneDetail(gateway_id, scene_id, command[6], command[7],
|
|
command[8], command[9], command[10])) {
|
|
publishPayload(gateway_id, {0xA0, gateway_id, op, scene_id, 0x01});
|
|
} else {
|
|
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x02});
|
|
}
|
|
break;
|
|
case 0x06:
|
|
publishPayload(gateway_id, deleteScene(gateway_id, scene_id)
|
|
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
|
|
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
|
|
break;
|
|
case 0x07: {
|
|
std::vector<uint8_t> payload{0xA0, gateway_id, op, scene_id};
|
|
AppendPaddedName(payload, scene_data->name);
|
|
publishPayload(gateway_id, payload);
|
|
break;
|
|
}
|
|
case 0x08: {
|
|
if (command.size() < 8) {
|
|
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x02});
|
|
break;
|
|
}
|
|
const size_t payload_end = command.size() - 1;
|
|
const size_t requested_len = std::min<size_t>(command[6], kMaxNameBytes);
|
|
const size_t available = payload_end > 7 ? payload_end - 7 : 0;
|
|
const size_t actual_len = std::min(requested_len, available);
|
|
std::string name;
|
|
for (size_t index = 0; index < actual_len; ++index) {
|
|
name.push_back(static_cast<char>(command[7 + index]));
|
|
}
|
|
publishPayload(gateway_id, setSceneName(gateway_id, scene_id, name)
|
|
? std::vector<uint8_t>{0xA0, gateway_id, op, scene_id, 0x01}
|
|
: std::vector<uint8_t>{0xA1, gateway_id, op, scene_id, 0x02});
|
|
break;
|
|
}
|
|
default:
|
|
publishPayload(gateway_id, {0xA1, gateway_id, op, scene_id, 0x03});
|
|
break;
|
|
}
|
|
}
|
|
|
|
void GatewayController::handleInternalGroupCommand(uint8_t gateway_id,
|
|
const std::vector<uint8_t>& command) {
|
|
if (command.size() < 7) {
|
|
publishPayload(gateway_id, {0xA3, gateway_id, 0xff, 0xff, 0x01});
|
|
return;
|
|
}
|
|
const uint8_t op = command[4];
|
|
const uint8_t group_id = command[5];
|
|
if (group_id > 15) {
|
|
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x01});
|
|
return;
|
|
}
|
|
|
|
auto* group_data = group(gateway_id, group_id);
|
|
switch (op) {
|
|
case 0x00:
|
|
publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, true)
|
|
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
|
|
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
|
|
break;
|
|
case 0x01:
|
|
publishPayload(gateway_id, setGroupEnabled(gateway_id, group_id, false)
|
|
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
|
|
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
|
|
break;
|
|
case 0x02:
|
|
publishPayload(gateway_id, {0xA2, gateway_id, op, group_id,
|
|
static_cast<uint8_t>(group_data->enabled ? 1 : 0)});
|
|
break;
|
|
case 0x03: {
|
|
const auto [low, high] = groupMask(gateway_id);
|
|
publishPayload(gateway_id, {0xA2, gateway_id, op, low, high});
|
|
break;
|
|
}
|
|
case 0x04:
|
|
publishPayload(gateway_id, {0xA2, gateway_id, op, group_id,
|
|
normalizeGroupTargetType(group_data->target_type),
|
|
normalizeGroupTargetValue(group_data->target_type,
|
|
group_data->target_value)});
|
|
break;
|
|
case 0x05:
|
|
if (command.size() >= 9 && setGroupDetail(gateway_id, group_id, command[6], command[7])) {
|
|
publishPayload(gateway_id, {0xA2, gateway_id, op, group_id, 0x01});
|
|
} else {
|
|
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x02});
|
|
}
|
|
break;
|
|
case 0x06:
|
|
publishPayload(gateway_id, deleteGroup(gateway_id, group_id)
|
|
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
|
|
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
|
|
break;
|
|
case 0x07: {
|
|
std::vector<uint8_t> payload{0xA2, gateway_id, op, group_id};
|
|
AppendPaddedName(payload, group_data->name);
|
|
publishPayload(gateway_id, payload);
|
|
break;
|
|
}
|
|
case 0x08: {
|
|
if (command.size() < 8) {
|
|
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x02});
|
|
break;
|
|
}
|
|
const size_t payload_end = command.size() - 1;
|
|
const size_t requested_len = std::min<size_t>(command[6], kMaxNameBytes);
|
|
const size_t available = payload_end > 7 ? payload_end - 7 : 0;
|
|
const size_t actual_len = std::min(requested_len, available);
|
|
std::string name;
|
|
for (size_t index = 0; index < actual_len; ++index) {
|
|
name.push_back(static_cast<char>(command[7 + index]));
|
|
}
|
|
publishPayload(gateway_id, setGroupName(gateway_id, group_id, name)
|
|
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
|
|
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
|
|
break;
|
|
}
|
|
case 0x09:
|
|
publishPayload(gateway_id, executeGroup(gateway_id, group_id)
|
|
? std::vector<uint8_t>{0xA2, gateway_id, op, group_id, 0x01}
|
|
: std::vector<uint8_t>{0xA3, gateway_id, op, group_id, 0x02});
|
|
break;
|
|
default:
|
|
publishPayload(gateway_id, {0xA3, gateway_id, op, group_id, 0x03});
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool GatewayController::openStorage() {
|
|
if (storage_ != 0) {
|
|
return true;
|
|
}
|
|
const esp_err_t err = nvs_open(kStorageNamespace, NVS_READWRITE, &storage_);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(kTag, "failed to open controller storage: %s", esp_err_to_name(err));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void GatewayController::closeStorage() {
|
|
if (storage_ != 0) {
|
|
nvs_close(storage_);
|
|
storage_ = 0;
|
|
}
|
|
}
|
|
|
|
void GatewayController::loadSceneStore(uint8_t gateway_id, SceneStore& scenes) {
|
|
for (uint8_t scene_id = 0; scene_id < scenes.size(); ++scene_id) {
|
|
const auto raw = readString(ShortKey("sc", gateway_id, scene_id));
|
|
const auto values = ParseCsv(raw);
|
|
if (values.size() >= 6) {
|
|
scenes[scene_id].enabled = values[0] != 0;
|
|
scenes[scene_id].brightness = static_cast<uint8_t>(std::clamp(values[1], 0, 254));
|
|
scenes[scene_id].color_mode = static_cast<uint8_t>(std::clamp(values[2], 0, 2));
|
|
scenes[scene_id].data1 = static_cast<uint8_t>(std::clamp(values[3], 0, 255));
|
|
scenes[scene_id].data2 = static_cast<uint8_t>(std::clamp(values[4], 0, 255));
|
|
scenes[scene_id].data3 = static_cast<uint8_t>(std::clamp(values[5], 0, 255));
|
|
}
|
|
scenes[scene_id].name = NormalizeName(readString(ShortKey("sn", gateway_id, scene_id)));
|
|
}
|
|
}
|
|
|
|
void GatewayController::loadGroupStore(uint8_t gateway_id, GroupStore& groups) {
|
|
for (uint8_t group_id = 0; group_id < groups.size(); ++group_id) {
|
|
const auto raw = readString(ShortKey("gr", gateway_id, group_id));
|
|
const auto values = ParseCsv(raw);
|
|
if (values.size() >= 2) {
|
|
groups[group_id].enabled = values[0] != 0;
|
|
uint8_t target_type = static_cast<uint8_t>(std::clamp(values[1], 0, 255));
|
|
uint8_t target_value = values.size() >= 3 ? static_cast<uint8_t>(std::clamp(values[2], 0, 255))
|
|
: target_type;
|
|
if (values.size() < 3) {
|
|
target_type = 2;
|
|
}
|
|
groups[group_id].target_type = normalizeGroupTargetType(target_type);
|
|
groups[group_id].target_value = normalizeGroupTargetValue(groups[group_id].target_type,
|
|
target_value);
|
|
}
|
|
groups[group_id].name = NormalizeName(readString(ShortKey("gn", gateway_id, group_id)));
|
|
}
|
|
}
|
|
|
|
bool GatewayController::saveScene(uint8_t gateway_id, uint8_t scene_id) {
|
|
const auto* scene_data = scene(gateway_id, scene_id);
|
|
if (scene_data == nullptr) {
|
|
return false;
|
|
}
|
|
char payload[32] = {0};
|
|
std::snprintf(payload, sizeof(payload), "%u,%u,%u,%u,%u,%u", scene_data->enabled ? 1 : 0,
|
|
scene_data->brightness, scene_data->color_mode, scene_data->data1,
|
|
scene_data->data2, scene_data->data3);
|
|
return writeString(ShortKey("sc", gateway_id, scene_id), payload);
|
|
}
|
|
|
|
bool GatewayController::deleteSceneStorage(uint8_t gateway_id, uint8_t scene_id) {
|
|
return eraseKey(ShortKey("sc", gateway_id, scene_id));
|
|
}
|
|
|
|
bool GatewayController::saveSceneName(uint8_t gateway_id, uint8_t scene_id, std::string_view name) {
|
|
if (name.empty()) {
|
|
return deleteSceneNameStorage(gateway_id, scene_id);
|
|
}
|
|
return writeString(ShortKey("sn", gateway_id, scene_id), NormalizeName(name));
|
|
}
|
|
|
|
bool GatewayController::deleteSceneNameStorage(uint8_t gateway_id, uint8_t scene_id) {
|
|
return eraseKey(ShortKey("sn", gateway_id, scene_id));
|
|
}
|
|
|
|
bool GatewayController::saveGroup(uint8_t gateway_id, uint8_t group_id) {
|
|
const auto* group_data = group(gateway_id, group_id);
|
|
if (group_data == nullptr) {
|
|
return false;
|
|
}
|
|
char payload[24] = {0};
|
|
std::snprintf(payload, sizeof(payload), "%u,%u,%u", group_data->enabled ? 1 : 0,
|
|
normalizeGroupTargetType(group_data->target_type),
|
|
normalizeGroupTargetValue(group_data->target_type, group_data->target_value));
|
|
return writeString(ShortKey("gr", gateway_id, group_id), payload);
|
|
}
|
|
|
|
bool GatewayController::deleteGroupStorage(uint8_t gateway_id, uint8_t group_id) {
|
|
return eraseKey(ShortKey("gr", gateway_id, group_id));
|
|
}
|
|
|
|
bool GatewayController::saveGroupName(uint8_t gateway_id, uint8_t group_id, std::string_view name) {
|
|
if (name.empty()) {
|
|
return deleteGroupNameStorage(gateway_id, group_id);
|
|
}
|
|
return writeString(ShortKey("gn", gateway_id, group_id), NormalizeName(name));
|
|
}
|
|
|
|
bool GatewayController::deleteGroupNameStorage(uint8_t gateway_id, uint8_t group_id) {
|
|
return eraseKey(ShortKey("gn", gateway_id, group_id));
|
|
}
|
|
|
|
std::string GatewayController::readString(std::string_view key) const {
|
|
if (storage_ == 0) {
|
|
return {};
|
|
}
|
|
size_t required_size = 0;
|
|
if (nvs_get_str(storage_, std::string(key).c_str(), nullptr, &required_size) != ESP_OK ||
|
|
required_size == 0) {
|
|
return {};
|
|
}
|
|
std::string value(required_size - 1, '\0');
|
|
if (nvs_get_str(storage_, std::string(key).c_str(), value.data(), &required_size) != ESP_OK) {
|
|
return {};
|
|
}
|
|
return value;
|
|
}
|
|
|
|
bool GatewayController::writeString(std::string_view key, std::string_view value) {
|
|
if (storage_ == 0) {
|
|
return false;
|
|
}
|
|
return nvs_set_str(storage_, std::string(key).c_str(), std::string(value).c_str()) == ESP_OK &&
|
|
nvs_commit(storage_) == ESP_OK;
|
|
}
|
|
|
|
bool GatewayController::eraseKey(std::string_view key) {
|
|
if (storage_ == 0) {
|
|
return false;
|
|
}
|
|
const esp_err_t err = nvs_erase_key(storage_, std::string(key).c_str());
|
|
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
|
return true;
|
|
}
|
|
if (err != ESP_OK) {
|
|
return false;
|
|
}
|
|
return nvs_commit(storage_) == ESP_OK;
|
|
}
|
|
|
|
} // namespace gateway
|