This commit is contained in:
Thomas Peterson 2026-02-09 15:28:12 +01:00
parent 1a7f29b363
commit a430110f94
21 changed files with 220 additions and 53 deletions

View File

@ -321,6 +321,7 @@ bool KnxWorker::getGroupObjectInfo(size_t index, KnxGroupObjectInfo& info) {
info.commFlag = go.commFlag();
info.readFlag = go.readEnable();
info.writeFlag = go.writeEnable();
info.transmitFlag = go.transmitEnable();
// Resolve the primary group address via association/address tables
info.groupAddress = resolveGroupAddress(static_cast<uint16_t>(index));
@ -333,6 +334,47 @@ bool KnxWorker::getGroupObjectInfo(size_t index, KnxGroupObjectInfo& info) {
#endif
}
bool KnxWorker::sendSwitch(uint16_t groupAddr, bool value) {
#if !UART_DEBUG_MODE
if (!knxBau.configured()) {
ESP_LOGW(TAG, "sendSwitch: KNX not configured");
return false;
}
// Find the GroupObject index for this group address
size_t goCount = getGroupObjectCount();
for (size_t i = 1; i <= goCount; i++) {
uint16_t addr = resolveGroupAddress(static_cast<uint16_t>(i));
if (addr == groupAddr) {
GroupObject& go = knx.getGroupObject(i);
// Check if this GO can transmit
if (!go.transmitEnable()) {
ESP_LOGW(TAG, "sendSwitch: GO %d cannot transmit", (int)i);
return false;
}
// Set value and trigger send
KNXValue knxVal = value;
if (go.value(knxVal, DPT_Switch)) {
ESP_LOGI(TAG, "sendSwitch: GA=%d GO=%d value=%d", groupAddr, (int)i, value);
return true;
} else {
ESP_LOGW(TAG, "sendSwitch: Failed to set value for GO %d", (int)i);
return false;
}
}
}
ESP_LOGW(TAG, "sendSwitch: No GO found for GA=%d", groupAddr);
return false;
#else
(void)groupAddr;
(void)value;
return false;
#endif
}
void KnxWorker::formatGroupAddress(uint16_t addr, char* buf, size_t bufSize) {
// Format: main/middle/sub (5/3/8 bit)
uint8_t main = (addr >> 11) & 0x1F;

View File

@ -11,7 +11,8 @@ struct KnxGroupObjectInfo {
uint8_t dptSub; // DPT Subtyp
bool commFlag; // Kommunikations-Flag
bool readFlag; // Lese-Flag
bool writeFlag; // Schreib-Flag
bool writeFlag; // Schreib-Flag (empfangen vom Bus)
bool transmitFlag; // Sende-Flag (senden auf den Bus)
};
class KnxWorker {
@ -28,6 +29,9 @@ public:
size_t getGroupObjectCount();
bool getGroupObjectInfo(size_t index, KnxGroupObjectInfo& info);
// KNX Telegramm senden
bool sendSwitch(uint16_t groupAddr, bool value);
// Gruppenadresse als String formatieren (z.B. "1/2/3")
static void formatGroupAddress(uint16_t addr, char* buf, size_t bufSize);

View File

@ -119,10 +119,14 @@ void WidgetConfig::serialize(uint8_t* buf) const {
buf[pos++] = cardStyle;
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
const SubButtonConfig& sb = subButtons[i];
buf[pos++] = sb.iconCodepoint & 0xFF;
buf[pos++] = (sb.iconCodepoint >> 8) & 0xFF;
buf[pos++] = (sb.iconCodepoint >> 16) & 0xFF;
buf[pos++] = (sb.iconCodepoint >> 24) & 0xFF;
buf[pos++] = sb.iconCodepointOff & 0xFF;
buf[pos++] = (sb.iconCodepointOff >> 8) & 0xFF;
buf[pos++] = (sb.iconCodepointOff >> 16) & 0xFF;
buf[pos++] = (sb.iconCodepointOff >> 24) & 0xFF;
buf[pos++] = sb.iconCodepointOn & 0xFF;
buf[pos++] = (sb.iconCodepointOn >> 8) & 0xFF;
buf[pos++] = (sb.iconCodepointOn >> 16) & 0xFF;
buf[pos++] = (sb.iconCodepointOn >> 24) & 0xFF;
buf[pos++] = sb.knxAddrRead & 0xFF;
buf[pos++] = (sb.knxAddrRead >> 8) & 0xFF;
buf[pos++] = sb.knxAddrWrite & 0xFF;
@ -303,8 +307,11 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) {
SubButtonConfig& sb = subButtons[i];
if (pos + 20 <= SERIALIZED_SIZE) {
sb.iconCodepoint = buf[pos] | (buf[pos + 1] << 8) |
if (pos + 24 <= SERIALIZED_SIZE) {
sb.iconCodepointOff = buf[pos] | (buf[pos + 1] << 8) |
(buf[pos + 2] << 16) | (buf[pos + 3] << 24);
pos += 4;
sb.iconCodepointOn = buf[pos] | (buf[pos + 1] << 8) |
(buf[pos + 2] << 16) | (buf[pos + 3] << 24);
pos += 4;
sb.knxAddrRead = buf[pos] | (buf[pos + 1] << 8); pos += 2;

View File

@ -169,9 +169,10 @@ enum class SubButtonAction : uint8_t {
NAVIGATE = 1, // Navigate to screen
};
// Sub-button configuration for RoomCard (20 bytes)
// Sub-button configuration for RoomCard (24 bytes)
struct SubButtonConfig {
uint32_t iconCodepoint; // 4 bytes - Icon codepoint
uint32_t iconCodepointOff; // 4 bytes - Icon codepoint when OFF
uint32_t iconCodepointOn; // 4 bytes - Icon codepoint when ON (0 = use iconCodepointOff)
uint16_t knxAddrRead; // 2 bytes - KNX address to read status
uint16_t knxAddrWrite; // 2 bytes - KNX address to write on click
Color colorOn; // 3 bytes - Color when ON
@ -181,7 +182,7 @@ struct SubButtonConfig {
uint8_t targetScreen; // 1 byte - Target screen for navigate
bool enabled; // 1 byte - Is this sub-button active?
uint8_t _padding[2]; // 2 bytes - Alignment padding
// Total: 20 bytes per SubButton
// Total: 24 bytes per SubButton
};
// Text line configuration for RoomCard (24 bytes)
@ -296,8 +297,8 @@ struct WidgetConfig {
uint8_t arcValueFontSize; // Center value font size index
// Serialization size (fixed for NVS storage)
// 331 + 14 (arcMin + arcMax + arcUnit + arcShowValue + arcScaleOffset + arcScaleColor + arcValueColor + arcValueFontSize) = 345
static constexpr size_t SERIALIZED_SIZE = 345;
// 345 + 24 (6 subbuttons * 4 bytes for iconCodepointOn) = 369
static constexpr size_t SERIALIZED_SIZE = 369;
void serialize(uint8_t* buf) const;
void deserialize(const uint8_t* buf);

View File

@ -4,6 +4,7 @@
#include "widgets/RoomCardTileWidget.hpp"
#include "HistoryStore.hpp"
#include "SdCard.hpp"
#include "Gui.hpp"
#include "esp_lv_adapter.h"
#include "esp_log.h"
#include "esp_timer.h"
@ -604,13 +605,31 @@ void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target
case ButtonAction::KNX:
default: {
if (cfg.knxAddressWrite > 0) {
bool state = false;
bool currentState = false;
if (target) {
state = (lv_obj_get_state(target) & LV_STATE_CHECKED) != 0;
currentState = (lv_obj_get_state(target) & LV_STATE_CHECKED) != 0;
}
// For toggle buttons: send the opposite of current state
// For non-toggle buttons: send ON (true)
bool sendValue = cfg.isToggle ? !currentState : true;
ESP_LOGI(TAG, "Button %d clicked, KNX write to %d, value=%d (toggle=%d, currentState=%d)",
cfg.id, cfg.knxAddressWrite, sendValue, cfg.isToggle, currentState);
// Send KNX telegram
if (Gui::knxWorker.sendSwitch(cfg.knxAddressWrite, sendValue)) {
// Update local UI state immediately for responsive feedback
if (cfg.isToggle && target) {
if (sendValue) {
lv_obj_add_state(target, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(target, LV_STATE_CHECKED);
}
}
// Also update the cache
cacheKnxSwitch(cfg.knxAddressWrite, sendValue);
}
ESP_LOGI(TAG, "Button %d clicked, KNX write to %d, state=%d",
cfg.id, cfg.knxAddressWrite, state);
// TODO: Send KNX telegram
}
break;
}
@ -680,10 +699,13 @@ void WidgetManager::navigateBack() {
void WidgetManager::sendKnxSwitch(uint16_t groupAddr, bool value) {
ESP_LOGI(TAG, "sendKnxSwitch: GA=%d, value=%d", groupAddr, value);
// TODO: Send actual KNX telegram via KnxWorker
// For now, just log and update cache so UI reflects the change
// Send actual KNX telegram via KnxWorker
if (Gui::knxWorker.sendSwitch(groupAddr, value)) {
// Update cache so UI reflects the change
cacheKnxSwitch(groupAddr, value);
}
}
void WidgetManager::enterStandby() {
if (!config_->standbyEnabled || config_->standbyMinutes == 0) return;
@ -1693,7 +1715,8 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
cJSON* sbJson = cJSON_CreateObject();
cJSON_AddNumberToObject(sbJson, "pos", static_cast<int>(sb.position));
cJSON_AddNumberToObject(sbJson, "icon", sb.iconCodepoint);
cJSON_AddNumberToObject(sbJson, "iconOff", sb.iconCodepointOff);
cJSON_AddNumberToObject(sbJson, "iconOn", sb.iconCodepointOn);
cJSON_AddNumberToObject(sbJson, "knxRead", sb.knxAddrRead);
cJSON_AddNumberToObject(sbJson, "knxWrite", sb.knxAddrWrite);
cJSON_AddNumberToObject(sbJson, "action", static_cast<int>(sb.action));
@ -2035,9 +2058,10 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
cond.style.flags |= ConditionStyle::FLAG_USE_TEXT_COLOR;
}
// Background color
// Background color (only if not empty and starts with #)
cJSON* bgColor = cJSON_GetObjectItem(condItem, "bgColor");
if (cJSON_IsString(bgColor)) {
if (cJSON_IsString(bgColor) && bgColor->valuestring &&
bgColor->valuestring[0] == '#' && strlen(bgColor->valuestring) >= 4) {
cond.style.bgColor = Color::fromHex(parseHexColor(bgColor->valuestring));
cond.style.flags |= ConditionStyle::FLAG_USE_BG_COLOR;
}
@ -2139,9 +2163,20 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
sb.position = static_cast<SubButtonPosition>(pos->valueint);
}
cJSON* iconOff = cJSON_GetObjectItem(sbItem, "iconOff");
if (cJSON_IsNumber(iconOff)) {
sb.iconCodepointOff = static_cast<uint32_t>(iconOff->valuedouble);
} else {
// Backward compatibility: try old "icon" field
cJSON* icon = cJSON_GetObjectItem(sbItem, "icon");
if (cJSON_IsNumber(icon)) {
sb.iconCodepoint = static_cast<uint32_t>(icon->valuedouble);
sb.iconCodepointOff = static_cast<uint32_t>(icon->valuedouble);
}
}
cJSON* iconOn = cJSON_GetObjectItem(sbItem, "iconOn");
if (cJSON_IsNumber(iconOn)) {
sb.iconCodepointOn = static_cast<uint32_t>(iconOn->valuedouble);
}
cJSON* knxRead = cJSON_GetObjectItem(sbItem, "knxRead");

View File

@ -24,7 +24,7 @@ esp_err_t WebServer::getKnxAddressesHandler(httpd_req_t* req) {
cJSON_AddStringToObject(obj, "addrStr", addrStr);
cJSON_AddBoolToObject(obj, "comm", info.commFlag);
cJSON_AddBoolToObject(obj, "read", info.readFlag);
cJSON_AddBoolToObject(obj, "write", info.writeFlag);
cJSON_AddBoolToObject(obj, "write", info.transmitFlag); // transmit = kann auf Bus schreiben
cJSON_AddItemToArray(arr, obj);
}
}

View File

@ -235,7 +235,9 @@ void ButtonWidget::applyStyle() {
lv_obj_set_style_text_color(iconLabel_, lv_color_make(
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
uint8_t sizeIdx = config_.iconSize < 6 ? config_.iconSize : config_.fontSize;
// Icon fonts support sizes 0-13 (14px to 260px)
uint8_t sizeIdx = config_.iconSize;
if (sizeIdx > 13) sizeIdx = 13;
lv_obj_set_style_text_font(iconLabel_, Fonts::iconFont(sizeIdx), 0);
}
}
@ -336,10 +338,16 @@ bool ButtonWidget::evaluateConditions(float primaryValue, float secondaryValue,
}
if (bestMatch->style.flags & ConditionStyle::FLAG_USE_BG_COLOR) {
lv_obj_set_style_bg_color(obj_, lv_color_make(
lv_color_t bgColor = lv_color_make(
bestMatch->style.bgColor.r,
bestMatch->style.bgColor.g,
bestMatch->style.bgColor.b), 0);
bestMatch->style.bgColor.b);
// Set for default state
lv_obj_set_style_bg_color(obj_, bgColor, LV_PART_MAIN | LV_STATE_DEFAULT);
// Also set for checked state (toggle buttons)
lv_obj_set_style_bg_color(obj_, bgColor, LV_PART_MAIN | LV_STATE_CHECKED);
// And for pressed state
lv_obj_set_style_bg_color(obj_, bgColor, LV_PART_MAIN | LV_STATE_PRESSED);
}
if (bestMatch->style.flags & ConditionStyle::FLAG_USE_BG_OPACITY) {
@ -411,8 +419,45 @@ void ButtonWidget::applyFakeShadowStyle() {
}
void ButtonWidget::updateIcon(uint32_t codepoint) {
if (!iconLabel_ || codepoint == 0) return;
if (codepoint == 0) return;
// If no icon label exists yet, we need to create it dynamically
if (!iconLabel_ && obj_ && Fonts::hasIconFont()) {
// For buttons without a content container, we need to create the icon label
if (!contentContainer_) {
// Create a simple icon label directly in the button
iconLabel_ = lv_label_create(obj_);
if (iconLabel_) {
lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE);
// Position it based on icon position setting
if (config_.iconPosition == static_cast<uint8_t>(IconPosition::LEFT)) {
lv_obj_align(iconLabel_, LV_ALIGN_LEFT_MID, config_.iconPositionX, 0);
} else if (config_.iconPosition == static_cast<uint8_t>(IconPosition::RIGHT)) {
lv_obj_align(iconLabel_, LV_ALIGN_RIGHT_MID, -config_.iconPositionX, 0);
} else {
lv_obj_align(iconLabel_, LV_ALIGN_CENTER, 0, 0);
}
}
}
}
if (!iconLabel_) return;
// Set the icon text
char iconText[5];
encodeUtf8(codepoint, iconText);
lv_label_set_text(iconLabel_, iconText);
// Apply the correct font size
// If iconSize is 0 (no icon was originally configured), use fontSize instead
// Default to size 2 (22px) if both are 0
// Icon fonts support sizes 0-13 (14px to 260px)
uint8_t sizeIdx = config_.iconSize > 0 ? config_.iconSize : config_.fontSize;
if (sizeIdx == 0) sizeIdx = 2; // Default to medium size (22px)
if (sizeIdx > 13) sizeIdx = 13; // Cap at max icon font size
lv_obj_set_style_text_font(iconLabel_, Fonts::iconFont(sizeIdx), 0);
// Apply text color from config
lv_obj_set_style_text_color(iconLabel_, lv_color_make(
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
}

View File

@ -115,12 +115,13 @@ void RoomCardWidgetBase::createSubButtonsCommon() {
int16_t absY = config_.y + relY;
lv_obj_set_pos(btn, absX, absY);
// Create icon
if (cfg.iconCodepoint > 0 && Fonts::hasIconFont()) {
// Create icon (use iconCodepointOff initially, will be updated based on state)
uint32_t initialIcon = cfg.iconCodepointOff;
if (initialIcon > 0 && Fonts::hasIconFont()) {
lv_obj_t* icon = lv_label_create(btn);
lv_obj_clear_flag(icon, LV_OBJ_FLAG_CLICKABLE);
char iconText[5];
encodeUtf8(cfg.iconCodepoint, iconText);
encodeUtf8(initialIcon, iconText);
lv_label_set_text(icon, iconText);
lv_obj_center(icon);
subButtonIcons_[i] = icon;
@ -158,9 +159,20 @@ void RoomCardWidgetBase::updateSubButtonColor(uint8_t index) {
if (index >= MAX_SUBBUTTONS || !subButtonObjs_[index]) return;
const SubButtonConfig& cfg = config_.subButtons[index];
const Color& color = subButtonStates_[index] ? cfg.colorOn : cfg.colorOff;
bool isOn = subButtonStates_[index];
const Color& color = isOn ? cfg.colorOn : cfg.colorOff;
lv_obj_set_style_bg_color(subButtonObjs_[index], lv_color_hex(color.toLvColor()), 0);
// Update icon based on state
if (subButtonIcons_[index]) {
uint32_t iconCodepoint = isOn && cfg.iconCodepointOn > 0 ? cfg.iconCodepointOn : cfg.iconCodepointOff;
if (iconCodepoint > 0) {
char iconText[5];
encodeUtf8(iconCodepoint, iconText);
lv_label_set_text(subButtonIcons_[index], iconText);
}
}
}
void RoomCardWidgetBase::updateTemperature(float value) {

View File

@ -2845,8 +2845,8 @@ CONFIG_LV_COLOR_DEPTH=16
#
# Memory Settings
#
CONFIG_LV_USE_BUILTIN_MALLOC=y
# CONFIG_LV_USE_CLIB_MALLOC is not set
# CONFIG_LV_USE_BUILTIN_MALLOC is not set
CONFIG_LV_USE_CLIB_MALLOC=y
# CONFIG_LV_USE_MICROPYTHON_MALLOC is not set
# CONFIG_LV_USE_RTTHREAD_MALLOC is not set
# CONFIG_LV_USE_CUSTOM_MALLOC is not set
@ -2856,9 +2856,6 @@ CONFIG_LV_USE_BUILTIN_STRING=y
CONFIG_LV_USE_BUILTIN_SPRINTF=y
# CONFIG_LV_USE_CLIB_SPRINTF is not set
# CONFIG_LV_USE_CUSTOM_SPRINTF is not set
CONFIG_LV_MEM_SIZE_KILOBYTES=128
CONFIG_LV_MEM_POOL_EXPAND_SIZE_KILOBYTES=0
CONFIG_LV_MEM_ADR=0x0
# end of Memory Settings
#

View File

@ -71,6 +71,7 @@ const key = computed(() => w.value ? typeKeyFor(w.value.type) : 'label');
const showIconPicker = ref(false);
const conditionIconPickerIdx = ref(-1);
const subButtonIconPickerIdx = ref(-1);
const subButtonIconType = ref('off'); // 'off' or 'on'
const textLineIconPickerIdx = ref(-1);
// Map widget types to settings components
@ -110,9 +111,10 @@ function openConditionIconPicker(idx) {
showIconPicker.value = true;
}
function openSubButtonIconPicker(idx) {
function openSubButtonIconPicker(idx, type = 'off') {
conditionIconPickerIdx.value = -1;
subButtonIconPickerIdx.value = idx;
subButtonIconType.value = type;
textLineIconPickerIdx.value = -1;
showIconPicker.value = true;
}
@ -131,7 +133,8 @@ const activeIconCodepoint = computed({
return w.value.conditions[conditionIconPickerIdx.value].icon || 0;
}
if (subButtonIconPickerIdx.value >= 0 && w.value?.subButtons?.[subButtonIconPickerIdx.value]) {
return w.value.subButtons[subButtonIconPickerIdx.value].icon || 0;
const sb = w.value.subButtons[subButtonIconPickerIdx.value];
return subButtonIconType.value === 'on' ? (sb.iconOn || 0) : (sb.iconOff || 0);
}
if (textLineIconPickerIdx.value >= 0 && w.value?.textLines?.[textLineIconPickerIdx.value]) {
return w.value.textLines[textLineIconPickerIdx.value].icon || 0;
@ -142,7 +145,12 @@ const activeIconCodepoint = computed({
if (conditionIconPickerIdx.value >= 0 && w.value?.conditions?.[conditionIconPickerIdx.value]) {
w.value.conditions[conditionIconPickerIdx.value].icon = value;
} else if (subButtonIconPickerIdx.value >= 0 && w.value?.subButtons?.[subButtonIconPickerIdx.value]) {
w.value.subButtons[subButtonIconPickerIdx.value].icon = value;
const sb = w.value.subButtons[subButtonIconPickerIdx.value];
if (subButtonIconType.value === 'on') {
sb.iconOn = value;
} else {
sb.iconOff = value;
}
} else if (textLineIconPickerIdx.value >= 0 && w.value?.textLines?.[textLineIconPickerIdx.value]) {
w.value.textLines[textLineIconPickerIdx.value].icon = value;
} else if (w.value) {
@ -155,6 +163,7 @@ function handleIconPickerClose() {
showIconPicker.value = false;
conditionIconPickerIdx.value = -1;
subButtonIconPickerIdx.value = -1;
subButtonIconType.value = 'off';
textLineIconPickerIdx.value = -1;
}
</script>

View File

@ -147,10 +147,14 @@
</button>
<button v-if="cond.icon" class="w-6 h-6 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[11px] cursor-pointer hover:bg-[#f2cfcf]" @click="cond.icon = 0">x</button>
</div>
<div class="flex items-center gap-2 text-[11px] text-muted">
<label class="w-[50px]">Farbe</label>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[50px]">Textfarbe</label>
<input class="h-[22px] w-[32px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="cond.textColor">
</div>
<div class="flex items-center gap-2 text-[11px] text-muted">
<label class="w-[50px]">Hintergr.</label>
<input class="h-[22px] w-[32px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="cond.bgColor">
</div>
</div>
</div>
</template>
@ -190,7 +194,8 @@ const conditionCount = computed({
op: 'eq',
priority: props.widget.conditions.length,
icon: 0,
textColor: '#FFFFFF'
textColor: '#FFFFFF',
bgColor: ''
});
}
if (props.widget.conditions.length > target) {

View File

@ -166,11 +166,20 @@
</select>
</div>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[50px]">Icon</label>
<button class="flex-1 bg-white border border-border rounded-md px-2.5 py-1 text-[11px] flex items-center justify-center gap-1 cursor-pointer hover:bg-[#e4ebf2]" @click="$emit('open-subbutton-icon-picker', idx)">
<span v-if="sb.icon" class="material-symbols-outlined text-[16px]">{{ String.fromCodePoint(sb.icon) }}</span>
<label class="w-[50px]">Icon Aus</label>
<button class="flex-1 bg-white border border-border rounded-md px-2.5 py-1 text-[11px] flex items-center justify-center gap-1 cursor-pointer hover:bg-[#e4ebf2]" @click="$emit('open-subbutton-icon-picker', idx, 'off')">
<span v-if="sb.iconOff" class="material-symbols-outlined text-[16px]">{{ String.fromCodePoint(sb.iconOff) }}</span>
<span v-else>Kein Icon</span>
</button>
<button v-if="sb.iconOff" class="w-6 h-6 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[11px] cursor-pointer hover:bg-[#f2cfcf]" @click="sb.iconOff = 0">x</button>
</div>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[50px]">Icon An</label>
<button class="flex-1 bg-white border border-border rounded-md px-2.5 py-1 text-[11px] flex items-center justify-center gap-1 cursor-pointer hover:bg-[#e4ebf2]" @click="$emit('open-subbutton-icon-picker', idx, 'on')">
<span v-if="sb.iconOn" class="material-symbols-outlined text-[16px]">{{ String.fromCodePoint(sb.iconOn) }}</span>
<span v-else>Wie Aus</span>
</button>
<button v-if="sb.iconOn" class="w-6 h-6 rounded-md border border-red-200 bg-[#f7dede] text-[#b3261e] grid place-items-center text-[11px] cursor-pointer hover:bg-[#f2cfcf]" @click="sb.iconOn = 0">x</button>
</div>
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[50px]">Aktion</label>
@ -184,7 +193,7 @@
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="sb.knxRead">
<option :value="0">-- Keine --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }}
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
@ -193,7 +202,7 @@
<select class="flex-1 bg-white border border-border rounded-md px-2 py-1 text-[11px]" v-model.number="sb.knxWrite">
<option :value="0">-- Keine --</option>
<option v-for="addr in writeableAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
GA {{ addr.addrStr }}
GA {{ addr.addrStr }} (GO{{ addr.index }})
</option>
</select>
</div>
@ -316,7 +325,8 @@ const subButtonCount = computed({
while (props.widget.subButtons.length < target) {
props.widget.subButtons.push({
pos: props.widget.subButtons.length,
icon: 0,
iconOff: 0,
iconOn: 0,
knxRead: 0,
knxWrite: 0,
action: 0,