Backup
This commit is contained in:
parent
75a7e18913
commit
8e90872c75
@ -41,6 +41,16 @@ GroupObjectTableObject& BauSystemBDevice::groupObjectTable()
|
|||||||
return _groupObjTable;
|
return _groupObjTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AddressTableObject& BauSystemBDevice::addressTable()
|
||||||
|
{
|
||||||
|
return _addrTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssociationTableObject& BauSystemBDevice::associationTable()
|
||||||
|
{
|
||||||
|
return _assocTable;
|
||||||
|
}
|
||||||
|
|
||||||
void BauSystemBDevice::loop()
|
void BauSystemBDevice::loop()
|
||||||
{
|
{
|
||||||
_transLayer.loop();
|
_transLayer.loop();
|
||||||
|
|||||||
@ -23,6 +23,8 @@ class BauSystemBDevice : public BauSystemB
|
|||||||
void loop() override;
|
void loop() override;
|
||||||
bool configured() override;
|
bool configured() override;
|
||||||
GroupObjectTableObject& groupObjectTable();
|
GroupObjectTableObject& groupObjectTable();
|
||||||
|
AddressTableObject& addressTable();
|
||||||
|
AssociationTableObject& associationTable();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
ApplicationLayer& applicationLayer() override;
|
ApplicationLayer& applicationLayer() override;
|
||||||
|
|||||||
@ -47,5 +47,5 @@ void Gui::create()
|
|||||||
void Gui::updateTemperature(float temp)
|
void Gui::updateTemperature(float temp)
|
||||||
{
|
{
|
||||||
// Delegate to WidgetManager for KNX group address 1
|
// 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 "KnxWorker.hpp"
|
||||||
#include "Gui.hpp"
|
#include "WidgetManager.hpp"
|
||||||
#include "esp32_idf_platform.h"
|
#include "esp32_idf_platform.h"
|
||||||
#include "knx_facade.h"
|
#include "knx_facade.h"
|
||||||
#include "knx/bau07B0.h"
|
#include "knx/bau07B0.h"
|
||||||
@ -8,7 +8,10 @@
|
|||||||
#include "knx/dpt.h"
|
#include "knx/dpt.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include <esp_timer.h>
|
#include <esp_timer.h>
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "nvs.h"
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#define TAG "KNXWORKER"
|
#define TAG "KNXWORKER"
|
||||||
#define MASK_VERSION 0x07B0
|
#define MASK_VERSION 0x07B0
|
||||||
@ -22,6 +25,65 @@ KnxFacade<Esp32IdfPlatform, Bau07B0> knx(knxBau);
|
|||||||
|
|
||||||
KnxWorker::KnxWorker() {}
|
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() {
|
void KnxWorker::init() {
|
||||||
ESP_LOGI(TAG, "INIT");
|
ESP_LOGI(TAG, "INIT");
|
||||||
@ -31,16 +93,83 @@ void KnxWorker::init() {
|
|||||||
knxPlatform.setupUart();
|
knxPlatform.setupUart();
|
||||||
|
|
||||||
#if !UART_DEBUG_MODE
|
#if !UART_DEBUG_MODE
|
||||||
|
knx.bau().deviceObject().hardwareType(kKnxHardwareType);
|
||||||
|
knx.bau().deviceObject().version(kKnxHardwareVersion);
|
||||||
|
|
||||||
knx.readMemory();
|
knx.readMemory();
|
||||||
|
|
||||||
// Register callback for GroupObject 1 (Temperature)
|
uint32_t bauNumberOverride = 0;
|
||||||
GroupObject& go1 = knx.getGroupObject(1);
|
if (loadKnxBauNumber(bauNumberOverride)) {
|
||||||
go1.dataPointType(DPT_Value_Temp);
|
knx.bau().deviceObject().manufacturerId(0x00FA);
|
||||||
go1.callback([](GroupObject& go) {
|
knx.bau().deviceObject().bauNumber(bauNumberOverride);
|
||||||
float temp = (float)go.value(DPT_Value_Temp);
|
ESP_LOGI(TAG, "Applied KNX serial override: %04X%08lX", 0x00FA, (unsigned long)bauNumberOverride);
|
||||||
ESP_LOGI(TAG, "Temperature received: %.1f °C", temp);
|
}
|
||||||
Gui::updateTemperature(temp);
|
|
||||||
});
|
// 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();
|
knx.start();
|
||||||
ESP_LOGI(TAG, "FINISH");
|
ESP_LOGI(TAG, "FINISH");
|
||||||
@ -60,6 +189,39 @@ void KnxWorker::toggleProgMode() {
|
|||||||
#endif
|
#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() {
|
void KnxWorker::loop() {
|
||||||
#if UART_DEBUG_MODE
|
#if UART_DEBUG_MODE
|
||||||
// Periodically send U_STATE_REQ to test TX direction
|
// Periodically send U_STATE_REQ to test TX direction
|
||||||
@ -87,6 +249,19 @@ void KnxWorker::loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#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();
|
knx.loop();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -119,8 +294,8 @@ bool KnxWorker::getGroupObjectInfo(size_t index, KnxGroupObjectInfo& info) {
|
|||||||
info.readFlag = go.readEnable();
|
info.readFlag = go.readEnable();
|
||||||
info.writeFlag = go.writeEnable();
|
info.writeFlag = go.writeEnable();
|
||||||
|
|
||||||
// Get ASAP - this is the index we use for addressing
|
// Resolve the primary group address via association/address tables
|
||||||
info.groupAddress = go.asap();
|
info.groupAddress = resolveGroupAddress(static_cast<uint16_t>(index));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
@ -136,4 +311,4 @@ void KnxWorker::formatGroupAddress(uint16_t addr, char* buf, size_t bufSize) {
|
|||||||
uint8_t middle = (addr >> 8) & 0x07;
|
uint8_t middle = (addr >> 8) & 0x07;
|
||||||
uint8_t sub = addr & 0xFF;
|
uint8_t sub = addr & 0xFF;
|
||||||
snprintf(buf, bufSize, "%d/%d/%d", main, middle, sub);
|
snprintf(buf, bufSize, "%d/%d/%d", main, middle, sub);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
// KNX Group Object Info für Web-API
|
// KNX Group Object Info für Web-API
|
||||||
struct KnxGroupObjectInfo {
|
struct KnxGroupObjectInfo {
|
||||||
uint16_t goIndex; // Group Object Index (1-based)
|
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 dptMain; // DPT Haupttyp
|
||||||
uint8_t dptSub; // DPT Subtyp
|
uint8_t dptSub; // DPT Subtyp
|
||||||
bool commFlag; // Kommunikations-Flag
|
bool commFlag; // Kommunikations-Flag
|
||||||
@ -19,6 +19,9 @@ public:
|
|||||||
KnxWorker();
|
KnxWorker();
|
||||||
void init();
|
void init();
|
||||||
void toggleProgMode();
|
void toggleProgMode();
|
||||||
|
bool getProgMode();
|
||||||
|
void setProgMode(bool enabled);
|
||||||
|
void clearSettings();
|
||||||
void loop();
|
void loop();
|
||||||
|
|
||||||
// KNX Gruppenadressen auslesen
|
// KNX Gruppenadressen auslesen
|
||||||
@ -27,4 +30,8 @@ public:
|
|||||||
|
|
||||||
// Gruppenadresse als String formatieren (z.B. "1/2/3")
|
// Gruppenadresse als String formatieren (z.B. "1/2/3")
|
||||||
static void formatGroupAddress(uint16_t addr, char* buf, size_t bufSize);
|
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_SWITCH = 2, // KNX Switch (DPT 1.001)
|
||||||
KNX_DPT_PERCENT = 3, // KNX Percent (DPT 5.001)
|
KNX_DPT_PERCENT = 3, // KNX Percent (DPT 5.001)
|
||||||
KNX_DPT_TEXT = 4, // KNX Text (DPT 16.000)
|
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 {
|
enum class TextAlign : uint8_t {
|
||||||
@ -94,7 +97,7 @@ struct WidgetConfig {
|
|||||||
// Text properties
|
// Text properties
|
||||||
TextSource textSource;
|
TextSource textSource;
|
||||||
char text[MAX_TEXT_LEN]; // Static text or format string
|
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 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
|
uint8_t textAlign; // TextAlign: 0=left, 1=center, 2=right
|
||||||
bool isContainer; // For buttons: use as container (no internal label/icon)
|
bool isContainer; // For buttons: use as container (no internal label/icon)
|
||||||
@ -110,7 +113,7 @@ struct WidgetConfig {
|
|||||||
|
|
||||||
// Button specific
|
// Button specific
|
||||||
bool isToggle; // For buttons: toggle mode
|
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)
|
ButtonAction action; // Button action (KNX, Jump, Back)
|
||||||
uint8_t targetScreen; // Target screen ID for jump
|
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 = {};
|
UiEvent event = {};
|
||||||
event.type = UiEventType::KNX_VALUE;
|
event.type = UiEventType::KNX_VALUE;
|
||||||
event.groupAddr = groupAddr;
|
event.groupAddr = groupAddr;
|
||||||
|
event.textSource = source;
|
||||||
event.value = value;
|
event.value = value;
|
||||||
enqueueUiEvent(event);
|
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) {
|
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
|
||||||
UiEvent event = {};
|
UiEvent event = {};
|
||||||
event.type = UiEventType::KNX_SWITCH;
|
event.type = UiEventType::KNX_SWITCH;
|
||||||
@ -798,7 +803,7 @@ void WidgetManager::processUiQueue() {
|
|||||||
xQueueReceive(uiQueue_, &event, 0) == pdTRUE) {
|
xQueueReceive(uiQueue_, &event, 0) == pdTRUE) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case UiEventType::KNX_VALUE:
|
case UiEventType::KNX_VALUE:
|
||||||
applyKnxValue(event.groupAddr, event.value);
|
applyKnxValue(event.groupAddr, event.value, event.textSource);
|
||||||
break;
|
break;
|
||||||
case UiEventType::KNX_SWITCH:
|
case UiEventType::KNX_SWITCH:
|
||||||
applyKnxSwitch(event.groupAddr, event.state);
|
applyKnxSwitch(event.groupAddr, event.state);
|
||||||
@ -813,9 +818,10 @@ void WidgetManager::processUiQueue() {
|
|||||||
esp_lv_adapter_unlock();
|
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_) {
|
for (auto& widget : widgets_) {
|
||||||
if (widget && widget->getKnxAddress() == groupAddr) {
|
if (widget && widget->getKnxAddress() == groupAddr &&
|
||||||
|
widget->getTextSource() == source) {
|
||||||
widget->onKnxValue(value);
|
widget->onKnxValue(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ public:
|
|||||||
void onUserActivity();
|
void onUserActivity();
|
||||||
|
|
||||||
// Thread-safe KNX updates (queued to UI thread)
|
// 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 onKnxValue(uint16_t groupAddr, float value);
|
||||||
void onKnxSwitch(uint16_t groupAddr, bool value);
|
void onKnxSwitch(uint16_t groupAddr, bool value);
|
||||||
void onKnxText(uint16_t groupAddr, const char* text);
|
void onKnxText(uint16_t groupAddr, const char* text);
|
||||||
@ -69,6 +70,7 @@ private:
|
|||||||
struct UiEvent {
|
struct UiEvent {
|
||||||
UiEventType type;
|
UiEventType type;
|
||||||
uint16_t groupAddr;
|
uint16_t groupAddr;
|
||||||
|
TextSource textSource;
|
||||||
float value;
|
float value;
|
||||||
bool state;
|
bool state;
|
||||||
char text[UI_EVENT_TEXT_LEN];
|
char text[UI_EVENT_TEXT_LEN];
|
||||||
@ -80,7 +82,7 @@ private:
|
|||||||
void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent);
|
void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent);
|
||||||
bool enqueueUiEvent(const UiEvent& event);
|
bool enqueueUiEvent(const UiEvent& event);
|
||||||
void processUiQueue();
|
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 applyKnxSwitch(uint16_t groupAddr, bool value);
|
||||||
void applyKnxText(uint16_t groupAddr, const char* text);
|
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++) {
|
for (size_t i = 1; i <= count; i++) {
|
||||||
KnxGroupObjectInfo info;
|
KnxGroupObjectInfo info;
|
||||||
if (knxWorker.getGroupObjectInfo(i, info)) {
|
if (knxWorker.getGroupObjectInfo(i, info)) {
|
||||||
|
if (info.groupAddress == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
char addrStr[16];
|
char addrStr[16];
|
||||||
KnxWorker::formatGroupAddress(info.groupAddress, addrStr, sizeof(addrStr));
|
KnxWorker::formatGroupAddress(info.groupAddress, addrStr, sizeof(addrStr));
|
||||||
|
|
||||||
@ -33,3 +36,83 @@ esp_err_t WebServer::getKnxAddressesHandler(httpd_req_t* req) {
|
|||||||
cJSON_Delete(arr);
|
cJSON_Delete(arr);
|
||||||
return ESP_OK;
|
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();
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||||
config.stack_size = 8192;
|
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);
|
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_uri_t getKnxAddresses = { .uri = "/api/knx/addresses", .method = HTTP_GET, .handler = getKnxAddressesHandler, .user_ctx = nullptr };
|
||||||
httpd_register_uri_handler(server_, &getKnxAddresses);
|
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
|
// Status routes
|
||||||
httpd_uri_t postUsbMode = { .uri = "/api/usb-mode", .method = HTTP_POST, .handler = postUsbModeHandler, .user_ctx = nullptr };
|
httpd_uri_t postUsbMode = { .uri = "/api/usb-mode", .method = HTTP_POST, .handler = postUsbModeHandler, .user_ctx = nullptr };
|
||||||
httpd_register_uri_handler(server_, &postUsbMode);
|
httpd_register_uri_handler(server_, &postUsbMode);
|
||||||
|
|||||||
@ -32,6 +32,9 @@ private:
|
|||||||
|
|
||||||
// KNX handlers (KnxHandlers.cpp)
|
// KNX handlers (KnxHandlers.cpp)
|
||||||
static esp_err_t getKnxAddressesHandler(httpd_req_t* req);
|
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)
|
// Status handlers (StatusHandlers.cpp)
|
||||||
static esp_err_t postUsbModeHandler(httpd_req_t* req);
|
static esp_err_t postUsbModeHandler(httpd_req_t* req);
|
||||||
|
|||||||
@ -187,10 +187,22 @@ void LabelWidget::applyStyle() {
|
|||||||
void LabelWidget::onKnxValue(float value) {
|
void LabelWidget::onKnxValue(float value) {
|
||||||
lv_obj_t* label = textLabel_ ? textLabel_ : obj_;
|
lv_obj_t* label = textLabel_ ? textLabel_ : obj_;
|
||||||
if (label == nullptr) return;
|
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];
|
char buf[32];
|
||||||
snprintf(buf, sizeof(buf), config_.text, value);
|
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);
|
lv_label_set_text(label, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -247,7 +247,10 @@ void PowerLinkWidget::updateAnimation(float speed, bool reverse) {
|
|||||||
|
|
||||||
void PowerLinkWidget::onKnxValue(float value) {
|
void PowerLinkWidget::onKnxValue(float value) {
|
||||||
if (config_.textSource != TextSource::KNX_DPT_TEMP &&
|
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 factor = parseFloatOr(config_.text, 1.0f);
|
||||||
float speed = std::fabs(value) * factor;
|
float speed = std::fabs(value) * factor;
|
||||||
|
|||||||
@ -158,12 +158,16 @@ void PowerNodeWidget::updateValueText(const char* text) {
|
|||||||
void PowerNodeWidget::onKnxValue(float value) {
|
void PowerNodeWidget::onKnxValue(float value) {
|
||||||
if (valueLabel_ == nullptr) return;
|
if (valueLabel_ == nullptr) return;
|
||||||
if (config_.textSource != TextSource::KNX_DPT_TEMP &&
|
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];
|
char buf[32];
|
||||||
const char* fmt = valueFormat_[0] != '\0' ? valueFormat_ : "%0.1f";
|
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);
|
int percent = static_cast<int>(value + 0.5f);
|
||||||
snprintf(buf, sizeof(buf), fmt, percent);
|
snprintf(buf, sizeof(buf), fmt, percent);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -114,8 +114,16 @@
|
|||||||
color: #ffd1d1;
|
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:hover { transform: translateY(-1px); }
|
||||||
.btn:active { transform: translateY(1px); }
|
.btn:active { transform: translateY(1px); }
|
||||||
|
.btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
||||||
|
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
@ -599,6 +607,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="top-actions">
|
<div class="top-actions">
|
||||||
<button class="btn ghost" onclick="enableUsbMode()">USB-Modus</button>
|
<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 ghost danger" onclick="resetConfig()">Zuruecksetzen</button>
|
||||||
<button class="btn primary" onclick="saveConfig()">Speichern & Anwenden</button>
|
<button class="btn primary" onclick="saveConfig()">Speichern & Anwenden</button>
|
||||||
</div>
|
</div>
|
||||||
@ -754,11 +764,24 @@
|
|||||||
1: 'KNX Temperatur',
|
1: 'KNX Temperatur',
|
||||||
2: 'KNX Schalter',
|
2: 'KNX Schalter',
|
||||||
3: 'KNX Prozent',
|
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 = {
|
const sourceOptions = {
|
||||||
label: [0, 1, 2, 3, 4],
|
label: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||||
button: [0],
|
button: [0],
|
||||||
led: [0, 2]
|
led: [0, 2]
|
||||||
};
|
};
|
||||||
@ -769,7 +792,10 @@
|
|||||||
1: '%.1f °C',
|
1: '%.1f °C',
|
||||||
2: '%s',
|
2: '%s',
|
||||||
3: '%d %%',
|
3: '%d %%',
|
||||||
4: '%s'
|
4: '%s',
|
||||||
|
5: '%.1f W',
|
||||||
|
6: '%.0f kWh',
|
||||||
|
7: '%d'
|
||||||
};
|
};
|
||||||
|
|
||||||
const WIDGET_DEFAULTS = {
|
const WIDGET_DEFAULTS = {
|
||||||
@ -837,6 +863,8 @@
|
|||||||
let nextWidgetId = 0;
|
let nextWidgetId = 0;
|
||||||
let nextScreenId = 0;
|
let nextScreenId = 0;
|
||||||
let knxAddresses = [];
|
let knxAddresses = [];
|
||||||
|
let knxProgMode = false;
|
||||||
|
let knxProgBusy = false;
|
||||||
let canvasScale = 0.6;
|
let canvasScale = 0.6;
|
||||||
let showGrid = true;
|
let showGrid = true;
|
||||||
let activeScreenId = 0;
|
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() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
await loadKnxAddresses();
|
await loadKnxAddresses();
|
||||||
@ -975,6 +1060,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mapLegacyKnxAddresses();
|
||||||
|
|
||||||
if (config.standby.screen >= 255) {
|
if (config.standby.screen >= 255) {
|
||||||
config.standby.screen = -1;
|
config.standby.screen = -1;
|
||||||
}
|
}
|
||||||
@ -1203,6 +1290,19 @@
|
|||||||
document.getElementById('treeCount').textContent = screen.widgets.length;
|
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() {
|
function renderProperties() {
|
||||||
const panel = document.getElementById('properties');
|
const panel = document.getElementById('properties');
|
||||||
const screen = getActiveScreen();
|
const screen = getActiveScreen();
|
||||||
@ -1215,17 +1315,14 @@
|
|||||||
|
|
||||||
const key = typeKeyFor(w.type);
|
const key = typeKeyFor(w.type);
|
||||||
const sourceList = sourceOptions[key] || [0];
|
const sourceList = sourceOptions[key] || [0];
|
||||||
const sourceOptionsHtml = sourceList.map((value) => {
|
const sourceOptionsHtml = buildTextSourceOptions(sourceList, w.textSrc);
|
||||||
const label = textSources[value] || 'Unbekannt';
|
|
||||||
return `<option value="${value}" ${w.textSrc == value ? 'selected' : ''}>${label}</option>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const knxOptions = knxAddresses.map((a) =>
|
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('');
|
).join('');
|
||||||
|
|
||||||
const knxWriteOptions = knxAddresses.filter(a => a.write).map((a) =>
|
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('');
|
).join('');
|
||||||
|
|
||||||
const screenOptions = config.screens.map((screenItem) =>
|
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() {
|
async function enableUsbMode() {
|
||||||
if (!confirm('USB-Modus aktivieren?\n\nDie SD-Karte wird als USB-Laufwerk verfuegbar.\nZum Beenden: Geraet neu starten.')) return;
|
if (!confirm('USB-Modus aktivieren?\n\nDie SD-Karte wird als USB-Laufwerk verfuegbar.\nZum Beenden: Geraet neu starten.')) return;
|
||||||
try {
|
try {
|
||||||
@ -1782,6 +1928,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
requestAnimationFrame(() => document.body.classList.add('loaded'));
|
requestAnimationFrame(() => document.body.classList.add('loaded'));
|
||||||
|
loadKnxProgMode();
|
||||||
loadConfig();
|
loadConfig();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -19,7 +19,9 @@
|
|||||||
<h4 :class="headingClass">Inhalt</h4>
|
<h4 :class="headingClass">Inhalt</h4>
|
||||||
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
||||||
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="w.textSrc === 0" :class="rowClass">
|
<div v-if="w.textSrc === 0" :class="rowClass">
|
||||||
@ -30,8 +32,8 @@
|
|||||||
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
||||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||||
<option :value="0">-- Waehlen --</option>
|
<option :value="0">-- Waehlen --</option>
|
||||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -47,15 +49,17 @@
|
|||||||
<h4 :class="headingClass">LED</h4>
|
<h4 :class="headingClass">LED</h4>
|
||||||
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
||||||
<select :class="inputClass" v-model.number="w.textSrc">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="w.textSrc === 2" :class="rowClass">
|
<div v-if="w.textSrc === 2" :class="rowClass">
|
||||||
<label :class="labelClass">KNX Objekt</label>
|
<label :class="labelClass">KNX Objekt</label>
|
||||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||||
<option :value="0">-- Waehlen --</option>
|
<option :value="0">-- Waehlen --</option>
|
||||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -79,15 +83,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
||||||
<select :class="inputClass" v-model.number="w.textSrc">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="w.textSrc === 2" :class="rowClass">
|
<div v-if="w.textSrc === 2" :class="rowClass">
|
||||||
<label :class="labelClass">KNX Objekt</label>
|
<label :class="labelClass">KNX Objekt</label>
|
||||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||||
<option :value="0">-- Waehlen --</option>
|
<option :value="0">-- Waehlen --</option>
|
||||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -147,7 +153,9 @@
|
|||||||
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
||||||
<label class="w-[70px] text-[11px] text-muted">Speed</label>
|
<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">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="link.widget.textSrc === 0" class="flex items-center gap-2 mb-2 text-[11px] text-muted">
|
<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>
|
<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">
|
<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 :value="0">-- Waehlen --</option>
|
||||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">Label</label><input :class="inputClass" type="text" v-model="powerNodeLabel"></div>
|
||||||
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
||||||
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="w.textSrc === 0" :class="rowClass">
|
<div v-if="w.textSrc === 0" :class="rowClass">
|
||||||
@ -190,8 +200,8 @@
|
|||||||
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
||||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||||
<option :value="0">-- Waehlen --</option>
|
<option :value="0">-- Waehlen --</option>
|
||||||
<option v-for="addr in store.knxAddresses" :key="addr.index" :value="addr.index">
|
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -342,8 +352,8 @@
|
|||||||
<div :class="rowClass"><label :class="labelClass">KNX Schreib</label>
|
<div :class="rowClass"><label :class="labelClass">KNX Schreib</label>
|
||||||
<select :class="inputClass" v-model.number="w.knxAddrWrite">
|
<select :class="inputClass" v-model.number="w.knxAddrWrite">
|
||||||
<option :value="0">-- Keine --</option>
|
<option :value="0">-- Keine --</option>
|
||||||
<option v-for="addr in writeableAddresses" :key="addr.index" :value="addr.index">
|
<option v-for="addr in writeableAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
GO{{ addr.index }} ({{ addr.addrStr }})
|
GA {{ addr.addrStr }} (GO{{ addr.index }})
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -369,7 +379,7 @@
|
|||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useEditorStore } from '../stores/editor';
|
import { useEditorStore } from '../stores/editor';
|
||||||
import { typeKeyFor } from '../utils';
|
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';
|
import IconPicker from './IconPicker.vue';
|
||||||
|
|
||||||
const store = useEditorStore();
|
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 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]';
|
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() {
|
function addPowerNode() {
|
||||||
store.addWidget('powernode');
|
store.addWidget('powernode');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,30 @@
|
|||||||
<div class="flex items-center gap-2.5 flex-wrap justify-end">
|
<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="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 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-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>
|
<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>
|
</div>
|
||||||
@ -17,10 +41,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
import { useEditorStore } from '../stores/editor';
|
import { useEditorStore } from '../stores/editor';
|
||||||
|
|
||||||
const store = useEditorStore();
|
const store = useEditorStore();
|
||||||
const emit = defineEmits(['open-settings']);
|
const emit = defineEmits(['open-settings']);
|
||||||
|
const knxProgMode = ref(false);
|
||||||
|
const knxProgPending = ref(false);
|
||||||
|
const knxResetPending = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadKnxProgMode();
|
||||||
|
});
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
try {
|
try {
|
||||||
@ -38,7 +70,7 @@ async function handleReset() {
|
|||||||
await store.resetConfig();
|
await store.resetConfig();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert('Fehler');
|
alert('Fehler');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function enableUsbMode() {
|
async function enableUsbMode() {
|
||||||
@ -55,4 +87,68 @@ async function enableUsbMode() {
|
|||||||
alert('Fehler beim Aktivieren');
|
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>
|
</script>
|
||||||
|
|||||||
@ -63,16 +63,29 @@ export const textSources = {
|
|||||||
1: 'KNX Temperatur',
|
1: 'KNX Temperatur',
|
||||||
2: 'KNX Schalter',
|
2: 'KNX Schalter',
|
||||||
3: 'KNX Prozent',
|
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 = {
|
export const sourceOptions = {
|
||||||
label: [0, 1, 2, 3, 4],
|
label: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||||
button: [0],
|
button: [0],
|
||||||
led: [0, 2],
|
led: [0, 2],
|
||||||
icon: [0, 2],
|
icon: [0, 2],
|
||||||
powernode: [0, 1, 2, 3, 4],
|
powernode: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||||
powerlink: [0, 1, 3]
|
powerlink: [0, 1, 3, 5, 6, 7]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ICON_DEFAULTS = {
|
export const ICON_DEFAULTS = {
|
||||||
@ -88,7 +101,10 @@ export const defaultFormats = {
|
|||||||
1: '%.1f °C',
|
1: '%.1f °C',
|
||||||
2: '%s',
|
2: '%s',
|
||||||
3: '%d %%',
|
3: '%d %%',
|
||||||
4: '%s'
|
4: '%s',
|
||||||
|
5: '%.1f W',
|
||||||
|
6: '%.0f kWh',
|
||||||
|
7: '%d'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WIDGET_DEFAULTS = {
|
export const WIDGET_DEFAULTS = {
|
||||||
|
|||||||
@ -22,6 +22,34 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const nextScreenId = ref(0);
|
const nextScreenId = ref(0);
|
||||||
const nextWidgetId = 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(() => {
|
const activeScreen = computed(() => {
|
||||||
return config.screens.find(s => s.id === activeScreenId.value) || config.screens[0];
|
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');
|
const resp = await fetch('/api/knx/addresses');
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
knxAddresses.value = await resp.json();
|
knxAddresses.value = await resp.json();
|
||||||
|
mapLegacyKnxAddresses();
|
||||||
} else {
|
} else {
|
||||||
knxAddresses.value = [];
|
knxAddresses.value = [];
|
||||||
}
|
}
|
||||||
@ -152,6 +181,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
if (!config.standby) {
|
if (!config.standby) {
|
||||||
config.standby = { enabled: false, screen: -1, minutes: 5 };
|
config.standby = { enabled: false, screen: -1, minutes: 5 };
|
||||||
}
|
}
|
||||||
|
mapLegacyKnxAddresses();
|
||||||
|
|
||||||
// Recalculate IDs
|
// Recalculate IDs
|
||||||
nextWidgetId.value = 0;
|
nextWidgetId.value = 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user