Backup
This commit is contained in:
parent
75a7e18913
commit
8e90872c75
@ -41,6 +41,16 @@ GroupObjectTableObject& BauSystemBDevice::groupObjectTable()
|
||||
return _groupObjTable;
|
||||
}
|
||||
|
||||
AddressTableObject& BauSystemBDevice::addressTable()
|
||||
{
|
||||
return _addrTable;
|
||||
}
|
||||
|
||||
AssociationTableObject& BauSystemBDevice::associationTable()
|
||||
{
|
||||
return _assocTable;
|
||||
}
|
||||
|
||||
void BauSystemBDevice::loop()
|
||||
{
|
||||
_transLayer.loop();
|
||||
|
||||
@ -23,6 +23,8 @@ class BauSystemBDevice : public BauSystemB
|
||||
void loop() override;
|
||||
bool configured() override;
|
||||
GroupObjectTableObject& groupObjectTable();
|
||||
AddressTableObject& addressTable();
|
||||
AssociationTableObject& associationTable();
|
||||
|
||||
protected:
|
||||
ApplicationLayer& applicationLayer() override;
|
||||
|
||||
@ -47,5 +47,5 @@ void Gui::create()
|
||||
void Gui::updateTemperature(float temp)
|
||||
{
|
||||
// Delegate to WidgetManager for KNX group address 1
|
||||
WidgetManager::instance().onKnxValue(1, temp);
|
||||
WidgetManager::instance().onKnxValue(1, temp, TextSource::KNX_DPT_TEMP);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include "KnxWorker.hpp"
|
||||
#include "Gui.hpp"
|
||||
#include "WidgetManager.hpp"
|
||||
#include "esp32_idf_platform.h"
|
||||
#include "knx_facade.h"
|
||||
#include "knx/bau07B0.h"
|
||||
@ -8,7 +8,10 @@
|
||||
#include "knx/dpt.h"
|
||||
#include "esp_log.h"
|
||||
#include <esp_timer.h>
|
||||
#include "esp_system.h"
|
||||
#include "nvs.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "KNXWORKER"
|
||||
#define MASK_VERSION 0x07B0
|
||||
@ -22,6 +25,65 @@ KnxFacade<Esp32IdfPlatform, Bau07B0> knx(knxBau);
|
||||
|
||||
KnxWorker::KnxWorker() {}
|
||||
|
||||
namespace {
|
||||
constexpr char kKnxNvsNamespace[] = "knx";
|
||||
constexpr char kKnxSerialKey[] = "serial_bau";
|
||||
constexpr uint8_t kKnxHardwareType[6] = {0x00, 0x00, 0xAB, 0xCE, 0x03, 0x00};
|
||||
constexpr uint16_t kKnxHardwareVersion = 1;
|
||||
|
||||
bool loadKnxBauNumber(uint32_t& outValue) {
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(kKnxNvsNamespace, NVS_READONLY, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t value = 0;
|
||||
err = nvs_get_u32(handle, kKnxSerialKey, &value);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outValue = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool saveKnxBauNumber(uint32_t value) {
|
||||
nvs_handle_t handle;
|
||||
esp_err_t err = nvs_open(kKnxNvsNamespace, NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
err = nvs_set_u32(handle, kKnxSerialKey, value);
|
||||
if (err == ESP_OK) {
|
||||
err = nvs_commit(handle);
|
||||
}
|
||||
nvs_close(handle);
|
||||
return err == ESP_OK;
|
||||
}
|
||||
|
||||
uint32_t generateRandomBauNumber() {
|
||||
uint32_t value = esp_random();
|
||||
if (value == 0) {
|
||||
value = 1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
uint16_t resolveGroupAddress(uint16_t asap) {
|
||||
if (!knxBau.configured()) {
|
||||
return 0;
|
||||
}
|
||||
int32_t tsap = knxBau.associationTable().translateAsap(asap);
|
||||
if (tsap < 0) {
|
||||
return 0;
|
||||
}
|
||||
return knxBau.addressTable().getGroupAddress(static_cast<uint16_t>(tsap));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
void KnxWorker::init() {
|
||||
ESP_LOGI(TAG, "INIT");
|
||||
@ -31,16 +93,83 @@ void KnxWorker::init() {
|
||||
knxPlatform.setupUart();
|
||||
|
||||
#if !UART_DEBUG_MODE
|
||||
knx.bau().deviceObject().hardwareType(kKnxHardwareType);
|
||||
knx.bau().deviceObject().version(kKnxHardwareVersion);
|
||||
|
||||
knx.readMemory();
|
||||
|
||||
// Register callback for GroupObject 1 (Temperature)
|
||||
GroupObject& go1 = knx.getGroupObject(1);
|
||||
go1.dataPointType(DPT_Value_Temp);
|
||||
go1.callback([](GroupObject& go) {
|
||||
float temp = (float)go.value(DPT_Value_Temp);
|
||||
ESP_LOGI(TAG, "Temperature received: %.1f °C", temp);
|
||||
Gui::updateTemperature(temp);
|
||||
uint32_t bauNumberOverride = 0;
|
||||
if (loadKnxBauNumber(bauNumberOverride)) {
|
||||
knx.bau().deviceObject().manufacturerId(0x00FA);
|
||||
knx.bau().deviceObject().bauNumber(bauNumberOverride);
|
||||
ESP_LOGI(TAG, "Applied KNX serial override: %04X%08lX", 0x00FA, (unsigned long)bauNumberOverride);
|
||||
}
|
||||
|
||||
// Register callbacks for all group objects to forward updates to the GUI
|
||||
size_t goCount = getGroupObjectCount();
|
||||
if (goCount == 0) {
|
||||
ESP_LOGW(TAG, "No KNX group objects configured; skipping callbacks");
|
||||
} else {
|
||||
for (size_t i = 1; i <= goCount; i++) {
|
||||
GroupObject& go = knx.getGroupObject(i);
|
||||
go.callback([](GroupObject& go) {
|
||||
uint16_t groupAddr = resolveGroupAddress(go.asap());
|
||||
if (groupAddr == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
KNXValue switchValue = false;
|
||||
if (go.tryValue(switchValue, DPT_Switch)) {
|
||||
WidgetManager::instance().onKnxSwitch(groupAddr, static_cast<bool>(switchValue));
|
||||
}
|
||||
|
||||
KNXValue tempValue = 0.0f;
|
||||
if (go.tryValue(tempValue, DPT_Value_Temp)) {
|
||||
WidgetManager::instance().onKnxValue(groupAddr, static_cast<float>(tempValue),
|
||||
TextSource::KNX_DPT_TEMP);
|
||||
}
|
||||
|
||||
KNXValue percentValue = 0.0f;
|
||||
if (go.tryValue(percentValue, DPT_Scaling)) {
|
||||
WidgetManager::instance().onKnxValue(groupAddr, static_cast<float>(percentValue),
|
||||
TextSource::KNX_DPT_PERCENT);
|
||||
}
|
||||
|
||||
KNXValue factorValue = (uint8_t)0;
|
||||
if (go.tryValue(factorValue, DPT_DecimalFactor)) {
|
||||
WidgetManager::instance().onKnxValue(groupAddr, static_cast<float>(factorValue),
|
||||
TextSource::KNX_DPT_DECIMALFACTOR);
|
||||
}
|
||||
|
||||
KNXValue powerValue = 0.0f;
|
||||
if (go.tryValue(powerValue, DPT_Value_Power)) {
|
||||
WidgetManager::instance().onKnxValue(groupAddr, static_cast<float>(powerValue),
|
||||
TextSource::KNX_DPT_POWER);
|
||||
}
|
||||
|
||||
KNXValue energyValue = (int32_t)0;
|
||||
if (go.tryValue(energyValue, DPT_ActiveEnergy_kWh)) {
|
||||
WidgetManager::instance().onKnxValue(groupAddr, static_cast<float>(energyValue),
|
||||
TextSource::KNX_DPT_ENERGY);
|
||||
}
|
||||
|
||||
KNXValue textValue = "";
|
||||
if (go.tryValue(textValue, DPT_String_8859_1) ||
|
||||
go.tryValue(textValue, DPT_String_ASCII)) {
|
||||
const char* raw = static_cast<const char*>(textValue);
|
||||
size_t maxLen = go.valueSize();
|
||||
if (maxLen > 14) {
|
||||
maxLen = 14;
|
||||
}
|
||||
size_t len = strnlen(raw, maxLen);
|
||||
char buf[15];
|
||||
memcpy(buf, raw, len);
|
||||
buf[len] = '\0';
|
||||
WidgetManager::instance().onKnxText(groupAddr, buf);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
knx.start();
|
||||
ESP_LOGI(TAG, "FINISH");
|
||||
@ -60,6 +189,39 @@ void KnxWorker::toggleProgMode() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool KnxWorker::getProgMode() {
|
||||
#if !UART_DEBUG_MODE
|
||||
return knx.progMode();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void KnxWorker::setProgMode(bool enabled) {
|
||||
#if !UART_DEBUG_MODE
|
||||
knx.progMode(enabled);
|
||||
#else
|
||||
(void)enabled;
|
||||
#endif
|
||||
}
|
||||
|
||||
void KnxWorker::clearSettings() {
|
||||
#if !UART_DEBUG_MODE
|
||||
if (knxResetState_ == 0) {
|
||||
uint32_t bauNumber = generateRandomBauNumber();
|
||||
bool stored = saveKnxBauNumber(bauNumber);
|
||||
knx.bau().deviceObject().manufacturerId(0x00FA);
|
||||
knx.bau().deviceObject().bauNumber(bauNumber);
|
||||
if (stored) {
|
||||
ESP_LOGI(TAG, "KNX serial randomized to %04X%08lX", 0x00FA, (unsigned long)bauNumber);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to persist randomized KNX serial");
|
||||
}
|
||||
knxResetState_ = 1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void KnxWorker::loop() {
|
||||
#if UART_DEBUG_MODE
|
||||
// Periodically send U_STATE_REQ to test TX direction
|
||||
@ -87,6 +249,19 @@ void KnxWorker::loop() {
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (knxResetState_ != 0) {
|
||||
uint32_t nowMs = (uint32_t)(esp_timer_get_time() / 1000);
|
||||
if (knxResetState_ == 1) {
|
||||
knx.bau().memory().clearMemory();
|
||||
knxResetAtMs_ = nowMs + 300;
|
||||
knxResetState_ = 2;
|
||||
} else if (knxResetState_ == 2) {
|
||||
if ((int32_t)(nowMs - knxResetAtMs_) >= 0) {
|
||||
knxResetState_ = 0;
|
||||
knx.bau().platform().restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
knx.loop();
|
||||
#endif
|
||||
}
|
||||
@ -119,8 +294,8 @@ bool KnxWorker::getGroupObjectInfo(size_t index, KnxGroupObjectInfo& info) {
|
||||
info.readFlag = go.readEnable();
|
||||
info.writeFlag = go.writeEnable();
|
||||
|
||||
// Get ASAP - this is the index we use for addressing
|
||||
info.groupAddress = go.asap();
|
||||
// Resolve the primary group address via association/address tables
|
||||
info.groupAddress = resolveGroupAddress(static_cast<uint16_t>(index));
|
||||
|
||||
return true;
|
||||
#else
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
// KNX Group Object Info für Web-API
|
||||
struct KnxGroupObjectInfo {
|
||||
uint16_t goIndex; // Group Object Index (1-based)
|
||||
uint16_t groupAddress; // Gruppenadresse (z.B. 1/2/3 = 0x0A03)
|
||||
uint16_t groupAddress; // Gruppenadresse (z.B. 1/2/3 = 0x0A03), 0 wenn nicht zugeordnet
|
||||
uint8_t dptMain; // DPT Haupttyp
|
||||
uint8_t dptSub; // DPT Subtyp
|
||||
bool commFlag; // Kommunikations-Flag
|
||||
@ -19,6 +19,9 @@ public:
|
||||
KnxWorker();
|
||||
void init();
|
||||
void toggleProgMode();
|
||||
bool getProgMode();
|
||||
void setProgMode(bool enabled);
|
||||
void clearSettings();
|
||||
void loop();
|
||||
|
||||
// KNX Gruppenadressen auslesen
|
||||
@ -27,4 +30,8 @@ public:
|
||||
|
||||
// Gruppenadresse als String formatieren (z.B. "1/2/3")
|
||||
static void formatGroupAddress(uint16_t addr, char* buf, size_t bufSize);
|
||||
|
||||
private:
|
||||
volatile uint8_t knxResetState_ = 0;
|
||||
volatile uint32_t knxResetAtMs_ = 0;
|
||||
};
|
||||
|
||||
@ -47,6 +47,9 @@ enum class TextSource : uint8_t {
|
||||
KNX_DPT_SWITCH = 2, // KNX Switch (DPT 1.001)
|
||||
KNX_DPT_PERCENT = 3, // KNX Percent (DPT 5.001)
|
||||
KNX_DPT_TEXT = 4, // KNX Text (DPT 16.000)
|
||||
KNX_DPT_POWER = 5, // KNX Power (DPT 14.056)
|
||||
KNX_DPT_ENERGY = 6, // KNX Energy (DPT 13.013)
|
||||
KNX_DPT_DECIMALFACTOR = 7, // KNX Decimal Factor (DPT 5.005)
|
||||
};
|
||||
|
||||
enum class TextAlign : uint8_t {
|
||||
@ -94,7 +97,7 @@ struct WidgetConfig {
|
||||
// Text properties
|
||||
TextSource textSource;
|
||||
char text[MAX_TEXT_LEN]; // Static text or format string
|
||||
uint16_t knxAddress; // KNX group address (if textSource != STATIC)
|
||||
uint16_t knxAddress; // KNX group address (GA) for read binding
|
||||
uint8_t fontSize; // Font size index (0=14, 1=18, 2=22, 3=28, 4=36, 5=48)
|
||||
uint8_t textAlign; // TextAlign: 0=left, 1=center, 2=right
|
||||
bool isContainer; // For buttons: use as container (no internal label/icon)
|
||||
@ -110,7 +113,7 @@ struct WidgetConfig {
|
||||
|
||||
// Button specific
|
||||
bool isToggle; // For buttons: toggle mode
|
||||
uint16_t knxAddressWrite; // KNX address to write on click
|
||||
uint16_t knxAddressWrite; // KNX group address (GA) to write on click
|
||||
ButtonAction action; // Button action (KNX, Jump, Back)
|
||||
uint8_t targetScreen; // Target screen ID for jump
|
||||
|
||||
|
||||
@ -745,14 +745,19 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
|
||||
void WidgetManager::onKnxValue(uint16_t groupAddr, float value, TextSource source) {
|
||||
UiEvent event = {};
|
||||
event.type = UiEventType::KNX_VALUE;
|
||||
event.groupAddr = groupAddr;
|
||||
event.textSource = source;
|
||||
event.value = value;
|
||||
enqueueUiEvent(event);
|
||||
}
|
||||
|
||||
void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
|
||||
onKnxValue(groupAddr, value, TextSource::KNX_DPT_TEMP);
|
||||
}
|
||||
|
||||
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
|
||||
UiEvent event = {};
|
||||
event.type = UiEventType::KNX_SWITCH;
|
||||
@ -798,7 +803,7 @@ void WidgetManager::processUiQueue() {
|
||||
xQueueReceive(uiQueue_, &event, 0) == pdTRUE) {
|
||||
switch (event.type) {
|
||||
case UiEventType::KNX_VALUE:
|
||||
applyKnxValue(event.groupAddr, event.value);
|
||||
applyKnxValue(event.groupAddr, event.value, event.textSource);
|
||||
break;
|
||||
case UiEventType::KNX_SWITCH:
|
||||
applyKnxSwitch(event.groupAddr, event.state);
|
||||
@ -813,9 +818,10 @@ void WidgetManager::processUiQueue() {
|
||||
esp_lv_adapter_unlock();
|
||||
}
|
||||
|
||||
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value) {
|
||||
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) {
|
||||
for (auto& widget : widgets_) {
|
||||
if (widget && widget->getKnxAddress() == groupAddr) {
|
||||
if (widget && widget->getKnxAddress() == groupAddr &&
|
||||
widget->getTextSource() == source) {
|
||||
widget->onKnxValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ public:
|
||||
void onUserActivity();
|
||||
|
||||
// Thread-safe KNX updates (queued to UI thread)
|
||||
void onKnxValue(uint16_t groupAddr, float value, TextSource source);
|
||||
void onKnxValue(uint16_t groupAddr, float value);
|
||||
void onKnxSwitch(uint16_t groupAddr, bool value);
|
||||
void onKnxText(uint16_t groupAddr, const char* text);
|
||||
@ -69,6 +70,7 @@ private:
|
||||
struct UiEvent {
|
||||
UiEventType type;
|
||||
uint16_t groupAddr;
|
||||
TextSource textSource;
|
||||
float value;
|
||||
bool state;
|
||||
char text[UI_EVENT_TEXT_LEN];
|
||||
@ -80,7 +82,7 @@ private:
|
||||
void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent);
|
||||
bool enqueueUiEvent(const UiEvent& event);
|
||||
void processUiQueue();
|
||||
void applyKnxValue(uint16_t groupAddr, float value);
|
||||
void applyKnxValue(uint16_t groupAddr, float value, TextSource source);
|
||||
void applyKnxSwitch(uint16_t groupAddr, bool value);
|
||||
void applyKnxText(uint16_t groupAddr, const char* text);
|
||||
|
||||
|
||||
@ -12,6 +12,9 @@ esp_err_t WebServer::getKnxAddressesHandler(httpd_req_t* req) {
|
||||
for (size_t i = 1; i <= count; i++) {
|
||||
KnxGroupObjectInfo info;
|
||||
if (knxWorker.getGroupObjectInfo(i, info)) {
|
||||
if (info.groupAddress == 0) {
|
||||
continue;
|
||||
}
|
||||
char addrStr[16];
|
||||
KnxWorker::formatGroupAddress(info.groupAddress, addrStr, sizeof(addrStr));
|
||||
|
||||
@ -33,3 +36,83 @@ esp_err_t WebServer::getKnxAddressesHandler(httpd_req_t* req) {
|
||||
cJSON_Delete(arr);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t WebServer::getKnxProgHandler(httpd_req_t* req) {
|
||||
KnxWorker& knxWorker = Gui::knxWorker;
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddBoolToObject(json, "progMode", knxWorker.getProgMode());
|
||||
return sendJsonObject(req, json);
|
||||
}
|
||||
|
||||
esp_err_t WebServer::postKnxProgHandler(httpd_req_t* req) {
|
||||
KnxWorker& knxWorker = Gui::knxWorker;
|
||||
bool current = knxWorker.getProgMode();
|
||||
bool target = current;
|
||||
bool hasTarget = false;
|
||||
|
||||
int total_len = req->content_len;
|
||||
if (total_len > 0) {
|
||||
if (total_len > 256) {
|
||||
return sendJsonError(req, "Content too large");
|
||||
}
|
||||
|
||||
char* buf = new char[total_len + 1];
|
||||
if (!buf) {
|
||||
return sendJsonError(req, "Out of memory");
|
||||
}
|
||||
|
||||
int received = 0;
|
||||
while (received < total_len) {
|
||||
int ret = httpd_req_recv(req, buf + received, total_len - received);
|
||||
if (ret <= 0) {
|
||||
delete[] buf;
|
||||
return sendJsonError(req, "Receive failed");
|
||||
}
|
||||
received += ret;
|
||||
}
|
||||
buf[received] = '\0';
|
||||
|
||||
cJSON* json = cJSON_Parse(buf);
|
||||
delete[] buf;
|
||||
|
||||
if (!json) {
|
||||
return sendJsonError(req, "Invalid JSON");
|
||||
}
|
||||
|
||||
cJSON* enabled = cJSON_GetObjectItemCaseSensitive(json, "enabled");
|
||||
if (!enabled) {
|
||||
enabled = cJSON_GetObjectItemCaseSensitive(json, "progMode");
|
||||
}
|
||||
|
||||
if (cJSON_IsBool(enabled)) {
|
||||
target = cJSON_IsTrue(enabled);
|
||||
hasTarget = true;
|
||||
}
|
||||
|
||||
cJSON_Delete(json);
|
||||
|
||||
if (!hasTarget) {
|
||||
return sendJsonError(req, "Missing 'enabled'");
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTarget) {
|
||||
target = !current;
|
||||
}
|
||||
knxWorker.setProgMode(target);
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "status", "ok");
|
||||
cJSON_AddBoolToObject(json, "progMode", target);
|
||||
return sendJsonObject(req, json);
|
||||
}
|
||||
|
||||
esp_err_t WebServer::postKnxResetHandler(httpd_req_t* req) {
|
||||
KnxWorker& knxWorker = Gui::knxWorker;
|
||||
knxWorker.clearSettings();
|
||||
|
||||
cJSON* json = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(json, "status", "ok");
|
||||
cJSON_AddStringToObject(json, "message", "KNX settings cleared, rebooting");
|
||||
return sendJsonObject(req, json);
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ void WebServer::start() {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
config.stack_size = 8192;
|
||||
config.max_uri_handlers = 20;
|
||||
config.max_uri_handlers = 24;
|
||||
|
||||
ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port);
|
||||
|
||||
@ -61,6 +61,15 @@ void WebServer::start() {
|
||||
httpd_uri_t getKnxAddresses = { .uri = "/api/knx/addresses", .method = HTTP_GET, .handler = getKnxAddressesHandler, .user_ctx = nullptr };
|
||||
httpd_register_uri_handler(server_, &getKnxAddresses);
|
||||
|
||||
httpd_uri_t getKnxProg = { .uri = "/api/knx/prog", .method = HTTP_GET, .handler = getKnxProgHandler, .user_ctx = nullptr };
|
||||
httpd_register_uri_handler(server_, &getKnxProg);
|
||||
|
||||
httpd_uri_t postKnxProg = { .uri = "/api/knx/prog", .method = HTTP_POST, .handler = postKnxProgHandler, .user_ctx = nullptr };
|
||||
httpd_register_uri_handler(server_, &postKnxProg);
|
||||
|
||||
httpd_uri_t postKnxReset = { .uri = "/api/knx/reset", .method = HTTP_POST, .handler = postKnxResetHandler, .user_ctx = nullptr };
|
||||
httpd_register_uri_handler(server_, &postKnxReset);
|
||||
|
||||
// Status routes
|
||||
httpd_uri_t postUsbMode = { .uri = "/api/usb-mode", .method = HTTP_POST, .handler = postUsbModeHandler, .user_ctx = nullptr };
|
||||
httpd_register_uri_handler(server_, &postUsbMode);
|
||||
|
||||
@ -32,6 +32,9 @@ private:
|
||||
|
||||
// KNX handlers (KnxHandlers.cpp)
|
||||
static esp_err_t getKnxAddressesHandler(httpd_req_t* req);
|
||||
static esp_err_t getKnxProgHandler(httpd_req_t* req);
|
||||
static esp_err_t postKnxProgHandler(httpd_req_t* req);
|
||||
static esp_err_t postKnxResetHandler(httpd_req_t* req);
|
||||
|
||||
// Status handlers (StatusHandlers.cpp)
|
||||
static esp_err_t postUsbModeHandler(httpd_req_t* req);
|
||||
|
||||
@ -187,10 +187,22 @@ void LabelWidget::applyStyle() {
|
||||
void LabelWidget::onKnxValue(float value) {
|
||||
lv_obj_t* label = textLabel_ ? textLabel_ : obj_;
|
||||
if (label == nullptr) return;
|
||||
if (config_.textSource != TextSource::KNX_DPT_TEMP) return;
|
||||
if (config_.textSource != TextSource::KNX_DPT_TEMP &&
|
||||
config_.textSource != TextSource::KNX_DPT_PERCENT &&
|
||||
config_.textSource != TextSource::KNX_DPT_POWER &&
|
||||
config_.textSource != TextSource::KNX_DPT_ENERGY &&
|
||||
config_.textSource != TextSource::KNX_DPT_DECIMALFACTOR) {
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[32];
|
||||
if (config_.textSource == TextSource::KNX_DPT_PERCENT ||
|
||||
config_.textSource == TextSource::KNX_DPT_DECIMALFACTOR) {
|
||||
int intValue = static_cast<int>(value + 0.5f);
|
||||
snprintf(buf, sizeof(buf), config_.text, intValue);
|
||||
} else {
|
||||
snprintf(buf, sizeof(buf), config_.text, value);
|
||||
}
|
||||
lv_label_set_text(label, buf);
|
||||
}
|
||||
|
||||
|
||||
@ -247,7 +247,10 @@ void PowerLinkWidget::updateAnimation(float speed, bool reverse) {
|
||||
|
||||
void PowerLinkWidget::onKnxValue(float value) {
|
||||
if (config_.textSource != TextSource::KNX_DPT_TEMP &&
|
||||
config_.textSource != TextSource::KNX_DPT_PERCENT) return;
|
||||
config_.textSource != TextSource::KNX_DPT_PERCENT &&
|
||||
config_.textSource != TextSource::KNX_DPT_POWER &&
|
||||
config_.textSource != TextSource::KNX_DPT_ENERGY &&
|
||||
config_.textSource != TextSource::KNX_DPT_DECIMALFACTOR) return;
|
||||
|
||||
float factor = parseFloatOr(config_.text, 1.0f);
|
||||
float speed = std::fabs(value) * factor;
|
||||
|
||||
@ -158,12 +158,16 @@ void PowerNodeWidget::updateValueText(const char* text) {
|
||||
void PowerNodeWidget::onKnxValue(float value) {
|
||||
if (valueLabel_ == nullptr) return;
|
||||
if (config_.textSource != TextSource::KNX_DPT_TEMP &&
|
||||
config_.textSource != TextSource::KNX_DPT_PERCENT) return;
|
||||
config_.textSource != TextSource::KNX_DPT_PERCENT &&
|
||||
config_.textSource != TextSource::KNX_DPT_POWER &&
|
||||
config_.textSource != TextSource::KNX_DPT_ENERGY &&
|
||||
config_.textSource != TextSource::KNX_DPT_DECIMALFACTOR) return;
|
||||
|
||||
char buf[32];
|
||||
const char* fmt = valueFormat_[0] != '\0' ? valueFormat_ : "%0.1f";
|
||||
|
||||
if (config_.textSource == TextSource::KNX_DPT_PERCENT) {
|
||||
if (config_.textSource == TextSource::KNX_DPT_PERCENT ||
|
||||
config_.textSource == TextSource::KNX_DPT_DECIMALFACTOR) {
|
||||
int percent = static_cast<int>(value + 0.5f);
|
||||
snprintf(buf, sizeof(buf), fmt, percent);
|
||||
} else {
|
||||
|
||||
@ -114,8 +114,16 @@
|
||||
color: #ffd1d1;
|
||||
}
|
||||
|
||||
.btn.prog.active {
|
||||
background: rgba(255, 107, 107, 0.18);
|
||||
border-color: rgba(255, 107, 107, 0.6);
|
||||
color: #ffd1d1;
|
||||
box-shadow: 0 8px 18px rgba(255, 107, 107, 0.2);
|
||||
}
|
||||
|
||||
.btn:hover { transform: translateY(-1px); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
||||
|
||||
.icon-btn {
|
||||
width: 28px;
|
||||
@ -599,6 +607,8 @@
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<button class="btn ghost" onclick="enableUsbMode()">USB-Modus</button>
|
||||
<button class="btn ghost prog" id="knxProgBtn" onclick="toggleKnxProg()" aria-pressed="false" title="KNX Programmiermodus">KNX Prog AUS</button>
|
||||
<button class="btn ghost danger" onclick="resetKnxSettings()">KNX Reset</button>
|
||||
<button class="btn ghost danger" onclick="resetConfig()">Zuruecksetzen</button>
|
||||
<button class="btn primary" onclick="saveConfig()">Speichern & Anwenden</button>
|
||||
</div>
|
||||
@ -754,11 +764,24 @@
|
||||
1: 'KNX Temperatur',
|
||||
2: 'KNX Schalter',
|
||||
3: 'KNX Prozent',
|
||||
4: 'KNX Text'
|
||||
4: 'KNX Text',
|
||||
5: 'KNX Leistung (DPT 14.056)',
|
||||
6: 'KNX Energie (DPT 13.013)',
|
||||
7: 'KNX Dezimalfaktor (DPT 5.005)'
|
||||
};
|
||||
|
||||
const textSourceGroups = [
|
||||
{ label: 'Statisch', values: [0] },
|
||||
{ label: 'DPT 1.x', values: [2] },
|
||||
{ label: 'DPT 5.x', values: [3, 7] },
|
||||
{ label: 'DPT 9.x', values: [1] },
|
||||
{ label: 'DPT 13.x', values: [6] },
|
||||
{ label: 'DPT 14.x', values: [5] },
|
||||
{ label: 'DPT 16.x', values: [4] }
|
||||
];
|
||||
|
||||
const sourceOptions = {
|
||||
label: [0, 1, 2, 3, 4],
|
||||
label: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
button: [0],
|
||||
led: [0, 2]
|
||||
};
|
||||
@ -769,7 +792,10 @@
|
||||
1: '%.1f °C',
|
||||
2: '%s',
|
||||
3: '%d %%',
|
||||
4: '%s'
|
||||
4: '%s',
|
||||
5: '%.1f W',
|
||||
6: '%.0f kWh',
|
||||
7: '%d'
|
||||
};
|
||||
|
||||
const WIDGET_DEFAULTS = {
|
||||
@ -837,6 +863,8 @@
|
||||
let nextWidgetId = 0;
|
||||
let nextScreenId = 0;
|
||||
let knxAddresses = [];
|
||||
let knxProgMode = false;
|
||||
let knxProgBusy = false;
|
||||
let canvasScale = 0.6;
|
||||
let showGrid = true;
|
||||
let activeScreenId = 0;
|
||||
@ -932,6 +960,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
function mapLegacyKnxAddresses() {
|
||||
if (!knxAddresses.length || !config || !Array.isArray(config.screens)) return;
|
||||
const addrByIndex = new Map();
|
||||
const gaSet = new Set();
|
||||
knxAddresses.forEach((addr) => {
|
||||
if (typeof addr.index === 'number' && typeof addr.addr === 'number') {
|
||||
addrByIndex.set(addr.index, addr.addr);
|
||||
gaSet.add(addr.addr);
|
||||
}
|
||||
});
|
||||
|
||||
config.screens.forEach((screen) => {
|
||||
if (!Array.isArray(screen.widgets)) return;
|
||||
screen.widgets.forEach((w) => {
|
||||
if (typeof w.knxAddr === 'number' && w.knxAddr > 0) {
|
||||
if (!gaSet.has(w.knxAddr) && addrByIndex.has(w.knxAddr)) {
|
||||
w.knxAddr = addrByIndex.get(w.knxAddr);
|
||||
}
|
||||
}
|
||||
if (typeof w.knxAddrWrite === 'number' && w.knxAddrWrite > 0) {
|
||||
if (!gaSet.has(w.knxAddrWrite) && addrByIndex.has(w.knxAddrWrite)) {
|
||||
w.knxAddrWrite = addrByIndex.get(w.knxAddrWrite);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateKnxProgButton() {
|
||||
const btn = document.getElementById('knxProgBtn');
|
||||
if (!btn) return;
|
||||
btn.classList.toggle('active', knxProgMode);
|
||||
btn.setAttribute('aria-pressed', knxProgMode ? 'true' : 'false');
|
||||
btn.textContent = knxProgMode ? 'KNX Prog AN' : 'KNX Prog AUS';
|
||||
btn.title = knxProgMode ? 'KNX Programmiermodus aktiv' : 'KNX Programmiermodus aus';
|
||||
btn.disabled = knxProgBusy;
|
||||
}
|
||||
|
||||
async function loadKnxProgMode() {
|
||||
try {
|
||||
const resp = await fetch('/api/knx/prog');
|
||||
if (!resp.ok) {
|
||||
updateKnxProgButton();
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (typeof data.progMode === 'boolean') {
|
||||
knxProgMode = data.progMode;
|
||||
} else if (typeof data.enabled === 'boolean') {
|
||||
knxProgMode = data.enabled;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore when API is not available.
|
||||
}
|
||||
updateKnxProgButton();
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
await loadKnxAddresses();
|
||||
@ -975,6 +1060,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
mapLegacyKnxAddresses();
|
||||
|
||||
if (config.standby.screen >= 255) {
|
||||
config.standby.screen = -1;
|
||||
}
|
||||
@ -1203,6 +1290,19 @@
|
||||
document.getElementById('treeCount').textContent = screen.widgets.length;
|
||||
}
|
||||
|
||||
function buildTextSourceOptions(sourceList, selectedValue) {
|
||||
const allowed = new Set(sourceList || []);
|
||||
return textSourceGroups.map((group) => {
|
||||
const values = group.values.filter((value) => allowed.has(value));
|
||||
if (!values.length) return '';
|
||||
const options = values.map((value) => {
|
||||
const label = textSources[value] || 'Unbekannt';
|
||||
return `<option value="${value}" ${selectedValue == value ? 'selected' : ''}>${label}</option>`;
|
||||
}).join('');
|
||||
return `<optgroup label="${group.label}">${options}</optgroup>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderProperties() {
|
||||
const panel = document.getElementById('properties');
|
||||
const screen = getActiveScreen();
|
||||
@ -1215,17 +1315,14 @@
|
||||
|
||||
const key = typeKeyFor(w.type);
|
||||
const sourceList = sourceOptions[key] || [0];
|
||||
const sourceOptionsHtml = sourceList.map((value) => {
|
||||
const label = textSources[value] || 'Unbekannt';
|
||||
return `<option value="${value}" ${w.textSrc == value ? 'selected' : ''}>${label}</option>`;
|
||||
}).join('');
|
||||
const sourceOptionsHtml = buildTextSourceOptions(sourceList, w.textSrc);
|
||||
|
||||
const knxOptions = knxAddresses.map((a) =>
|
||||
`<option value="${a.index}" ${w.knxAddr == a.index ? 'selected' : ''}>GO${a.index} (${a.addrStr})</option>`
|
||||
`<option value="${a.addr}" ${w.knxAddr == a.addr ? 'selected' : ''}>GA ${a.addrStr} (GO${a.index})</option>`
|
||||
).join('');
|
||||
|
||||
const knxWriteOptions = knxAddresses.filter(a => a.write).map((a) =>
|
||||
`<option value="${a.index}" ${w.knxAddrWrite == a.index ? 'selected' : ''}>GO${a.index} (${a.addrStr})</option>`
|
||||
`<option value="${a.addr}" ${w.knxAddrWrite == a.addr ? 'selected' : ''}>GA ${a.addrStr} (GO${a.index})</option>`
|
||||
).join('');
|
||||
|
||||
const screenOptions = config.screens.map((screenItem) =>
|
||||
@ -1705,6 +1802,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function resetKnxSettings() {
|
||||
if (!confirm('KNX Einstellungen wirklich loeschen?\n\nDas Geraet startet neu und muss in ETS neu programmiert werden.')) return;
|
||||
try {
|
||||
const resp = await fetch('/api/knx/reset', { method: 'POST' });
|
||||
if (!resp.ok) {
|
||||
showStatus('KNX Reset fehlgeschlagen', true);
|
||||
return;
|
||||
}
|
||||
showStatus('KNX Reset...');
|
||||
alert('KNX Einstellungen geloescht. Geraet startet neu.');
|
||||
} catch (e) {
|
||||
showStatus('KNX Reset fehlgeschlagen', true);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleKnxProg() {
|
||||
if (knxProgBusy) return;
|
||||
knxProgBusy = true;
|
||||
const next = !knxProgMode;
|
||||
updateKnxProgButton();
|
||||
try {
|
||||
const resp = await fetch('/api/knx/prog', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: next })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
showStatus('KNX Prog fehlgeschlagen', true);
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const updated =
|
||||
(typeof data.progMode === 'boolean')
|
||||
? data.progMode
|
||||
: (typeof data.enabled === 'boolean' ? data.enabled : null);
|
||||
if (updated === null) {
|
||||
showStatus('KNX Prog fehlgeschlagen', true);
|
||||
return;
|
||||
}
|
||||
knxProgMode = updated;
|
||||
showStatus(knxProgMode ? 'KNX Prog AN' : 'KNX Prog AUS');
|
||||
} catch (e) {
|
||||
showStatus('KNX Prog fehlgeschlagen', true);
|
||||
} finally {
|
||||
knxProgBusy = false;
|
||||
updateKnxProgButton();
|
||||
}
|
||||
}
|
||||
|
||||
async function enableUsbMode() {
|
||||
if (!confirm('USB-Modus aktivieren?\n\nDie SD-Karte wird als USB-Laufwerk verfuegbar.\nZum Beenden: Geraet neu starten.')) return;
|
||||
try {
|
||||
@ -1782,6 +1928,7 @@
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => document.body.classList.add('loaded'));
|
||||
loadKnxProgMode();
|
||||
loadConfig();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -19,7 +19,9 @@
|
||||
<h4 :class="headingClass">Inhalt</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
||||
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
||||
<option v-for="opt in sourceOptions.label" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
<optgroup v-for="group in groupedSources(sourceOptions.label)" :key="group.label" :label="group.label">
|
||||
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="w.textSrc === 0" :class="rowClass">
|
||||
@ -30,8 +32,8 @@
|
||||
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||
<option :value="0">-- Waehlen --</option>
|
||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
||||
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -47,15 +49,17 @@
|
||||
<h4 :class="headingClass">LED</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
||||
<select :class="inputClass" v-model.number="w.textSrc">
|
||||
<option v-for="opt in sourceOptions.led" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
<optgroup v-for="group in groupedSources(sourceOptions.led)" :key="group.label" :label="group.label">
|
||||
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="w.textSrc === 2" :class="rowClass">
|
||||
<label :class="labelClass">KNX Objekt</label>
|
||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||
<option :value="0">-- Waehlen --</option>
|
||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
||||
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -79,15 +83,17 @@
|
||||
</div>
|
||||
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
||||
<select :class="inputClass" v-model.number="w.textSrc">
|
||||
<option v-for="opt in sourceOptions.icon" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
<optgroup v-for="group in groupedSources(sourceOptions.icon)" :key="group.label" :label="group.label">
|
||||
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="w.textSrc === 2" :class="rowClass">
|
||||
<label :class="labelClass">KNX Objekt</label>
|
||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||
<option :value="0">-- Waehlen --</option>
|
||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
||||
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -147,7 +153,9 @@
|
||||
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
||||
<label class="w-[70px] text-[11px] text-muted">Speed</label>
|
||||
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="link.widget.textSrc">
|
||||
<option v-for="opt in sourceOptions.powerlink" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
<optgroup v-for="group in groupedSources(sourceOptions.powerlink)" :key="group.label" :label="group.label">
|
||||
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="link.widget.textSrc === 0" class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
||||
@ -163,8 +171,8 @@
|
||||
<label class="w-[70px] text-[11px] text-muted">KNX</label>
|
||||
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="link.widget.knxAddr">
|
||||
<option :value="0">-- Waehlen --</option>
|
||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
||||
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -179,7 +187,9 @@
|
||||
<div :class="rowClass"><label :class="labelClass">Label</label><input :class="inputClass" type="text" v-model="powerNodeLabel"></div>
|
||||
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
||||
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
||||
<option v-for="opt in sourceOptions.powernode" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
<optgroup v-for="group in groupedSources(sourceOptions.powernode)" :key="group.label" :label="group.label">
|
||||
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="w.textSrc === 0" :class="rowClass">
|
||||
@ -190,8 +200,8 @@
|
||||
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||
<option :value="0">-- Waehlen --</option>
|
||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
||||
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -342,8 +352,8 @@
|
||||
<div :class="rowClass"><label :class="labelClass">KNX Schreib</label>
|
||||
<select :class="inputClass" v-model.number="w.knxAddrWrite">
|
||||
<option :value="0">-- Keine --</option>
|
||||
<option v-for="addr in writeableAddresses" :key="addr.index" :value="addr.index">
|
||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
||||
<option v-for="addr in writeableAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -369,7 +379,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { useEditorStore } from '../stores/editor';
|
||||
import { typeKeyFor } from '../utils';
|
||||
import { sourceOptions, textSources, fontSizes, BUTTON_ACTIONS, defaultFormats, WIDGET_TYPES } from '../constants';
|
||||
import { sourceOptions, textSources, textSourceGroups, fontSizes, BUTTON_ACTIONS, defaultFormats, WIDGET_TYPES } from '../constants';
|
||||
import IconPicker from './IconPicker.vue';
|
||||
|
||||
const store = useEditorStore();
|
||||
@ -444,6 +454,16 @@ const noteClass = 'text-[11px] text-muted leading-tight';
|
||||
const iconSelectClass = 'flex-1 bg-panel-2 border border-border rounded-lg px-2.5 py-1.5 text-text text-[12px] flex items-center justify-center gap-1.5 cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]';
|
||||
const iconRemoveClass = 'w-7 h-7 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[12px] cursor-pointer hover:bg-[#f2cfcf]';
|
||||
|
||||
function groupedSources(options) {
|
||||
const allowed = new Set(options || []);
|
||||
return textSourceGroups
|
||||
.map((group) => ({
|
||||
label: group.label,
|
||||
values: group.values.filter((value) => allowed.has(value))
|
||||
}))
|
||||
.filter((group) => group.values.length > 0);
|
||||
}
|
||||
|
||||
function addPowerNode() {
|
||||
store.addWidget('powernode');
|
||||
}
|
||||
|
||||
@ -10,6 +10,30 @@
|
||||
<div class="flex items-center gap-2.5 flex-wrap justify-end">
|
||||
<button class="border border-border bg-panel-2 text-text px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#e4ebf2] active:translate-y-0.5" @click="emit('open-settings')">Einstellungen</button>
|
||||
<button class="border border-border bg-panel-2 text-text px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#e4ebf2] active:translate-y-0.5" @click="enableUsbMode">USB-Modus</button>
|
||||
<button
|
||||
:class="[
|
||||
'border px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 active:translate-y-0.5',
|
||||
knxProgMode ? 'border-red-200 bg-[#f7dede] text-[#b3261e] hover:bg-[#f2cfcf] shadow-[0_6px_14px_rgba(179,38,30,0.2)]' : 'border-border bg-panel-2 text-text hover:bg-[#e4ebf2]',
|
||||
knxProgPending ? 'opacity-60 cursor-not-allowed' : ''
|
||||
]"
|
||||
:aria-pressed="knxProgMode"
|
||||
:disabled="knxProgPending"
|
||||
:title="knxProgMode ? 'KNX Programmiermodus aktiv' : 'KNX Programmiermodus aus'"
|
||||
@click="toggleKnxProg"
|
||||
>
|
||||
{{ knxProgMode ? 'KNX Prog AN' : 'KNX Prog AUS' }}
|
||||
</button>
|
||||
<button
|
||||
:class="[
|
||||
'border border-red-200 bg-[#f7dede] text-[#b3261e] px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#f2cfcf] active:translate-y-0.5',
|
||||
knxResetPending ? 'opacity-60 cursor-not-allowed' : ''
|
||||
]"
|
||||
:disabled="knxResetPending"
|
||||
title="KNX Einstellungen loeschen"
|
||||
@click="resetKnxSettings"
|
||||
>
|
||||
KNX Reset
|
||||
</button>
|
||||
<button class="border border-red-200 bg-[#f7dede] text-[#b3261e] px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#f2cfcf] active:translate-y-0.5" @click="handleReset">Zuruecksetzen</button>
|
||||
<button class="border border-[#2b62a5] bg-[#2f6db8] text-white px-3.5 py-2 rounded-[10px] text-[13px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#2b62a5] active:translate-y-0.5 shadow-[0_8px_18px_rgba(47,109,184,0.3)]" @click="handleSave">Speichern & Anwenden</button>
|
||||
</div>
|
||||
@ -17,10 +41,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useEditorStore } from '../stores/editor';
|
||||
|
||||
const store = useEditorStore();
|
||||
const emit = defineEmits(['open-settings']);
|
||||
const knxProgMode = ref(false);
|
||||
const knxProgPending = ref(false);
|
||||
const knxResetPending = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
loadKnxProgMode();
|
||||
});
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
@ -55,4 +87,68 @@ async function enableUsbMode() {
|
||||
alert('Fehler beim Aktivieren');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKnxProgMode() {
|
||||
try {
|
||||
const resp = await fetch('/api/knx/prog');
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
if (typeof data.progMode === 'boolean') {
|
||||
knxProgMode.value = data.progMode;
|
||||
} else if (typeof data.enabled === 'boolean') {
|
||||
knxProgMode.value = data.enabled;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore when API is not available.
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleKnxProg() {
|
||||
if (knxProgPending.value) return;
|
||||
const next = !knxProgMode.value;
|
||||
knxProgPending.value = true;
|
||||
try {
|
||||
const resp = await fetch('/api/knx/prog', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: next })
|
||||
});
|
||||
if (!resp.ok) {
|
||||
alert('KNX Prog fehlgeschlagen');
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const updated =
|
||||
typeof data.progMode === 'boolean'
|
||||
? data.progMode
|
||||
: (typeof data.enabled === 'boolean' ? data.enabled : null);
|
||||
if (updated === null) {
|
||||
alert('KNX Prog fehlgeschlagen');
|
||||
return;
|
||||
}
|
||||
knxProgMode.value = updated;
|
||||
} catch (e) {
|
||||
alert('Fehler beim Umschalten');
|
||||
} finally {
|
||||
knxProgPending.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resetKnxSettings() {
|
||||
if (knxResetPending.value) return;
|
||||
if (!confirm('KNX Einstellungen wirklich loeschen?\n\nDas Geraet startet neu und muss in ETS neu programmiert werden.')) return;
|
||||
knxResetPending.value = true;
|
||||
try {
|
||||
const resp = await fetch('/api/knx/reset', { method: 'POST' });
|
||||
if (!resp.ok) {
|
||||
alert('KNX Reset fehlgeschlagen');
|
||||
return;
|
||||
}
|
||||
alert('KNX Einstellungen geloescht. Geraet startet neu.');
|
||||
} catch (e) {
|
||||
alert('KNX Reset fehlgeschlagen');
|
||||
} finally {
|
||||
knxResetPending.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -63,16 +63,29 @@ export const textSources = {
|
||||
1: 'KNX Temperatur',
|
||||
2: 'KNX Schalter',
|
||||
3: 'KNX Prozent',
|
||||
4: 'KNX Text'
|
||||
4: 'KNX Text',
|
||||
5: 'KNX Leistung (DPT 14.056)',
|
||||
6: 'KNX Energie (DPT 13.013)',
|
||||
7: 'KNX Dezimalfaktor (DPT 5.005)'
|
||||
};
|
||||
|
||||
export const textSourceGroups = [
|
||||
{ label: 'Statisch', values: [0] },
|
||||
{ label: 'DPT 1.x', values: [2] },
|
||||
{ label: 'DPT 5.x', values: [3, 7] },
|
||||
{ label: 'DPT 9.x', values: [1] },
|
||||
{ label: 'DPT 13.x', values: [6] },
|
||||
{ label: 'DPT 14.x', values: [5] },
|
||||
{ label: 'DPT 16.x', values: [4] }
|
||||
];
|
||||
|
||||
export const sourceOptions = {
|
||||
label: [0, 1, 2, 3, 4],
|
||||
label: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
button: [0],
|
||||
led: [0, 2],
|
||||
icon: [0, 2],
|
||||
powernode: [0, 1, 2, 3, 4],
|
||||
powerlink: [0, 1, 3]
|
||||
powernode: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
powerlink: [0, 1, 3, 5, 6, 7]
|
||||
};
|
||||
|
||||
export const ICON_DEFAULTS = {
|
||||
@ -88,7 +101,10 @@ export const defaultFormats = {
|
||||
1: '%.1f °C',
|
||||
2: '%s',
|
||||
3: '%d %%',
|
||||
4: '%s'
|
||||
4: '%s',
|
||||
5: '%.1f W',
|
||||
6: '%.0f kWh',
|
||||
7: '%d'
|
||||
};
|
||||
|
||||
export const WIDGET_DEFAULTS = {
|
||||
|
||||
@ -22,6 +22,34 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
const nextScreenId = ref(0);
|
||||
const nextWidgetId = ref(0);
|
||||
|
||||
function mapLegacyKnxAddresses() {
|
||||
if (!knxAddresses.value.length || !Array.isArray(config.screens)) return;
|
||||
const addrByIndex = new Map();
|
||||
const gaSet = new Set();
|
||||
knxAddresses.value.forEach((addr) => {
|
||||
if (typeof addr.index === 'number' && typeof addr.addr === 'number') {
|
||||
addrByIndex.set(addr.index, addr.addr);
|
||||
gaSet.add(addr.addr);
|
||||
}
|
||||
});
|
||||
|
||||
config.screens.forEach((screen) => {
|
||||
if (!Array.isArray(screen.widgets)) return;
|
||||
screen.widgets.forEach((w) => {
|
||||
if (typeof w.knxAddr === 'number' && w.knxAddr > 0) {
|
||||
if (!gaSet.has(w.knxAddr) && addrByIndex.has(w.knxAddr)) {
|
||||
w.knxAddr = addrByIndex.get(w.knxAddr);
|
||||
}
|
||||
}
|
||||
if (typeof w.knxAddrWrite === 'number' && w.knxAddrWrite > 0) {
|
||||
if (!gaSet.has(w.knxAddrWrite) && addrByIndex.has(w.knxAddrWrite)) {
|
||||
w.knxAddrWrite = addrByIndex.get(w.knxAddrWrite);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const activeScreen = computed(() => {
|
||||
return config.screens.find(s => s.id === activeScreenId.value) || config.screens[0];
|
||||
});
|
||||
@ -110,6 +138,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
const resp = await fetch('/api/knx/addresses');
|
||||
if (resp.ok) {
|
||||
knxAddresses.value = await resp.json();
|
||||
mapLegacyKnxAddresses();
|
||||
} else {
|
||||
knxAddresses.value = [];
|
||||
}
|
||||
@ -152,6 +181,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
if (!config.standby) {
|
||||
config.standby = { enabled: false, screen: -1, minutes: 5 };
|
||||
}
|
||||
mapLegacyKnxAddresses();
|
||||
|
||||
// Recalculate IDs
|
||||
nextWidgetId.value = 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user