#include "PowerNodeWidget.hpp" #include "../Fonts.hpp" #include #include PowerNodeWidget::PowerNodeWidget(const WidgetConfig& config) : Widget(config) { labelText_[0] = '\0'; valueFormat_[0] = '\0'; leftFormat_[0] = '\0'; rightFormat_[0] = '\0'; } static bool set_label_text_if_changed(lv_obj_t* label, const char* text) { if (!label || !text) return false; const char* current = lv_label_get_text(label); if (current && strcmp(current, text) == 0) return false; lv_label_set_text(label, text); return true; } static void set_obj_name(lv_obj_t* obj, const char* base, uint8_t id, const char* suffix) { #if LV_USE_OBJ_NAME if (!obj || !base) return; char name[48]; if (suffix && suffix[0] != '\0') { snprintf(name, sizeof(name), "%s#%u_%s", base, id, suffix); } else { snprintf(name, sizeof(name), "%s#%u", base, id); } lv_obj_set_name(obj, name); #else (void)obj; (void)base; (void)id; (void)suffix; #endif } int PowerNodeWidget::encodeUtf8(uint32_t codepoint, char* buf) { if (codepoint < 0x80) { buf[0] = static_cast(codepoint); buf[1] = '\0'; return 1; } else if (codepoint < 0x800) { buf[0] = static_cast(0xC0 | (codepoint >> 6)); buf[1] = static_cast(0x80 | (codepoint & 0x3F)); buf[2] = '\0'; return 2; } else if (codepoint < 0x10000) { buf[0] = static_cast(0xE0 | (codepoint >> 12)); buf[1] = static_cast(0x80 | ((codepoint >> 6) & 0x3F)); buf[2] = static_cast(0x80 | (codepoint & 0x3F)); buf[3] = '\0'; return 3; } else if (codepoint < 0x110000) { buf[0] = static_cast(0xF0 | (codepoint >> 18)); buf[1] = static_cast(0x80 | ((codepoint >> 12) & 0x3F)); buf[2] = static_cast(0x80 | ((codepoint >> 6) & 0x3F)); buf[3] = static_cast(0x80 | (codepoint & 0x3F)); buf[4] = '\0'; return 4; } buf[0] = '\0'; return 0; } void PowerNodeWidget::parseText() { labelText_[0] = '\0'; valueFormat_[0] = '\0'; leftFormat_[0] = '\0'; rightFormat_[0] = '\0'; // Parse primary text (label\nformat) const char* text = config_.text; if (text && text[0] != '\0') { const char* newline = strchr(text, '\n'); if (newline) { size_t labelLen = static_cast(newline - text); if (labelLen >= MAX_TEXT_LEN) labelLen = MAX_TEXT_LEN - 1; memcpy(labelText_, text, labelLen); labelText_[labelLen] = '\0'; strncpy(valueFormat_, newline + 1, MAX_TEXT_LEN - 1); valueFormat_[MAX_TEXT_LEN - 1] = '\0'; } else { strncpy(labelText_, text, MAX_TEXT_LEN - 1); labelText_[MAX_TEXT_LEN - 1] = '\0'; } } // Copy left format from text2 if (config_.text2[0] != '\0') { strncpy(leftFormat_, config_.text2, MAX_FORMAT_LEN - 1); leftFormat_[MAX_FORMAT_LEN - 1] = '\0'; } // Copy right format from text3 if (config_.text3[0] != '\0') { strncpy(rightFormat_, config_.text3, MAX_FORMAT_LEN - 1); rightFormat_[MAX_FORMAT_LEN - 1] = '\0'; } } lv_obj_t* PowerNodeWidget::create(lv_obj_t* parent) { parseText(); obj_ = lv_obj_create(parent); if (obj_ == nullptr) { return nullptr; } set_obj_name(obj_, "PowerNode", config_.id, nullptr); lv_obj_remove_style_all(obj_); lv_obj_set_pos(obj_, config_.x, config_.y); lv_obj_set_size(obj_, config_.width > 0 ? config_.width : 120, config_.height > 0 ? config_.height : 120); lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(obj_, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(obj_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_all(obj_, 6, 0); lv_obj_set_style_pad_gap(obj_, 2, 0); // Top label (title) if (labelText_[0] != '\0') { labelLabel_ = lv_label_create(obj_); set_obj_name(labelLabel_, "PowerNode", config_.id, "label"); lv_label_set_text(labelLabel_, labelText_); lv_obj_clear_flag(labelLabel_, LV_OBJ_FLAG_CLICKABLE); } // Check if we have left/right values (dual-value mode) bool hasDualValues = (config_.knxAddress2 > 0 || config_.knxAddress3 > 0); if (hasDualValues) { // Create middle row container for: left | icon | right middleRow_ = lv_obj_create(obj_); lv_obj_remove_style_all(middleRow_); set_obj_name(middleRow_, "PowerNode", config_.id, "middleRow"); lv_obj_set_size(middleRow_, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_clear_flag(middleRow_, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_flex_flow(middleRow_, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(middleRow_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_gap(middleRow_, 8, 0); // Left value label (secondary) if (config_.knxAddress2 > 0) { leftLabel_ = lv_label_create(middleRow_); set_obj_name(leftLabel_, "PowerNode", config_.id, "left"); lv_label_set_text(leftLabel_, leftFormat_[0] != '\0' ? leftFormat_ : ""); lv_obj_clear_flag(leftLabel_, LV_OBJ_FLAG_CLICKABLE); } // Center icon if (config_.iconCodepoint > 0 && Fonts::hasIconFont()) { iconLabel_ = lv_label_create(middleRow_); set_obj_name(iconLabel_, "PowerNode", config_.id, "icon"); char iconText[5]; encodeUtf8(config_.iconCodepoint, iconText); lv_label_set_text(iconLabel_, iconText); lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE); } // Right value label (tertiary) if (config_.knxAddress3 > 0) { rightLabel_ = lv_label_create(middleRow_); set_obj_name(rightLabel_, "PowerNode", config_.id, "right"); lv_label_set_text(rightLabel_, rightFormat_[0] != '\0' ? rightFormat_ : ""); lv_obj_clear_flag(rightLabel_, LV_OBJ_FLAG_CLICKABLE); } } else { // Original layout: icon in column flow if (config_.iconCodepoint > 0 && Fonts::hasIconFont()) { iconLabel_ = lv_label_create(obj_); set_obj_name(iconLabel_, "PowerNode", config_.id, "icon"); char iconText[5]; encodeUtf8(config_.iconCodepoint, iconText); lv_label_set_text(iconLabel_, iconText); lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE); } } // Bottom value label (primary) valueLabel_ = lv_label_create(obj_); set_obj_name(valueLabel_, "PowerNode", config_.id, "value"); if (valueFormat_[0] != '\0') { lv_label_set_text(valueLabel_, valueFormat_); } else { lv_label_set_text(valueLabel_, ""); } lv_obj_clear_flag(valueLabel_, LV_OBJ_FLAG_CLICKABLE); return obj_; } void PowerNodeWidget::applyStyle() { if (obj_ == nullptr) return; lv_obj_set_style_bg_color(obj_, lv_color_white(), 0); lv_obj_set_style_bg_opa(obj_, LV_OPA_COVER, 0); lv_obj_set_style_radius(obj_, LV_RADIUS_CIRCLE, 0); int16_t minSide = config_.width < config_.height ? config_.width : config_.height; int16_t ring = minSide / 12; if (ring < 2) ring = 2; if (ring > 12) ring = 12; lv_obj_set_style_border_width(obj_, ring, 0); lv_obj_set_style_border_color(obj_, lv_color_make( config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0); lv_obj_set_style_border_opa(obj_, config_.bgOpacity, 0); applyShadowStyle(); uint8_t valueSizeIdx = config_.fontSize; uint8_t labelSizeIdx = valueSizeIdx > 0 ? static_cast(valueSizeIdx - 1) : valueSizeIdx; uint8_t sideValueSizeIdx = labelSizeIdx; // Left/right values use smaller font if (labelLabel_ != nullptr) { lv_obj_set_style_text_color(labelLabel_, lv_color_make( config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); lv_obj_set_style_text_font(labelLabel_, Fonts::bySizeIndex(labelSizeIdx), 0); lv_obj_set_style_text_align(labelLabel_, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_opa(labelLabel_, LV_OPA_70, 0); } if (iconLabel_ != nullptr) { 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 : valueSizeIdx; lv_obj_set_style_text_font(iconLabel_, Fonts::iconFont(sizeIdx), 0); lv_obj_set_style_text_align(iconLabel_, LV_TEXT_ALIGN_CENTER, 0); } if (leftLabel_ != nullptr) { lv_obj_set_style_text_color(leftLabel_, lv_color_make( config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); lv_obj_set_style_text_font(leftLabel_, Fonts::bySizeIndex(sideValueSizeIdx), 0); lv_obj_set_style_text_align(leftLabel_, LV_TEXT_ALIGN_RIGHT, 0); } if (rightLabel_ != nullptr) { lv_obj_set_style_text_color(rightLabel_, lv_color_make( config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); lv_obj_set_style_text_font(rightLabel_, Fonts::bySizeIndex(sideValueSizeIdx), 0); lv_obj_set_style_text_align(rightLabel_, LV_TEXT_ALIGN_LEFT, 0); } if (valueLabel_ != nullptr) { lv_obj_set_style_text_color(valueLabel_, lv_color_make( config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); lv_obj_set_style_text_font(valueLabel_, Fonts::bySizeIndex(valueSizeIdx), 0); lv_obj_set_style_text_align(valueLabel_, LV_TEXT_ALIGN_CENTER, 0); } } void PowerNodeWidget::updateValueText(const char* text) { if (valueLabel_ == nullptr || text == nullptr) return; set_label_text_if_changed(valueLabel_, text); } void PowerNodeWidget::updateLeftText(const char* text) { if (leftLabel_ == nullptr || text == nullptr) return; set_label_text_if_changed(leftLabel_, text); } void PowerNodeWidget::updateRightText(const char* text) { if (rightLabel_ == nullptr || text == nullptr) return; set_label_text_if_changed(rightLabel_, text); } void PowerNodeWidget::updateIcon(uint32_t codepoint) { if (iconLabel_ == nullptr || codepoint == 0) return; char iconText[5]; encodeUtf8(codepoint, iconText); set_label_text_if_changed(iconLabel_, iconText); } static bool isNumericSource(TextSource source) { return source == TextSource::KNX_DPT_TEMP || source == TextSource::KNX_DPT_PERCENT || source == TextSource::KNX_DPT_POWER || source == TextSource::KNX_DPT_ENERGY || source == TextSource::KNX_DPT_DECIMALFACTOR; } static void formatValue(char* buf, size_t bufSize, float value, const char* fmt, TextSource source) { const char* useFmt = (fmt && fmt[0] != '\0') ? fmt : "%0.1f"; if (source == TextSource::KNX_DPT_PERCENT || source == TextSource::KNX_DPT_DECIMALFACTOR) { int intVal = static_cast(value + 0.5f); snprintf(buf, bufSize, useFmt, intVal); } else { snprintf(buf, bufSize, useFmt, value); } } void PowerNodeWidget::onKnxValue(float value) { // Cache and call base for condition evaluation cachedPrimaryValue_ = value; hasCachedPrimary_ = true; if (valueLabel_ == nullptr) return; if (!isNumericSource(config_.textSource)) return; char buf[32]; formatValue(buf, sizeof(buf), value, valueFormat_, config_.textSource); updateValueText(buf); // Evaluate conditions after updating value evaluateConditions(cachedPrimaryValue_, cachedSecondaryValue_, cachedTertiaryValue_); } void PowerNodeWidget::onKnxValue2(float value) { cachedSecondaryValue_ = value; hasCachedSecondary_ = true; if (leftLabel_ == nullptr) return; if (!isNumericSource(config_.textSource2)) return; char buf[32]; formatValue(buf, sizeof(buf), value, leftFormat_, config_.textSource2); updateLeftText(buf); // Evaluate conditions after updating value evaluateConditions(cachedPrimaryValue_, cachedSecondaryValue_, cachedTertiaryValue_); } void PowerNodeWidget::onKnxValue3(float value) { cachedTertiaryValue_ = value; hasCachedTertiary_ = true; if (rightLabel_ == nullptr) return; if (!isNumericSource(config_.textSource3)) return; char buf[32]; formatValue(buf, sizeof(buf), value, rightFormat_, config_.textSource3); updateRightText(buf); // Evaluate conditions after updating value evaluateConditions(cachedPrimaryValue_, cachedSecondaryValue_, cachedTertiaryValue_); } void PowerNodeWidget::onKnxSwitch(bool value) { if (valueLabel_ == nullptr) return; if (config_.textSource != TextSource::KNX_DPT_SWITCH) return; updateValueText(value ? "EIN" : "AUS"); } void PowerNodeWidget::onKnxText(const char* text) { if (valueLabel_ == nullptr) return; if (config_.textSource != TextSource::KNX_DPT_TEXT) return; updateValueText(text); } bool PowerNodeWidget::evaluateConditions(float primaryValue, float secondaryValue, float tertiaryValue) { if (config_.conditionCount == 0) return false; const StyleCondition* bestMatch = nullptr; uint8_t bestPriority = 255; for (uint8_t i = 0; i < config_.conditionCount && i < MAX_CONDITIONS; ++i) { const StyleCondition& cond = config_.conditions[i]; if (!cond.enabled) continue; // Get the value to check based on source float checkValue = 0.0f; bool hasValue = false; switch (cond.source) { case ConditionSource::PRIMARY: checkValue = primaryValue; hasValue = hasCachedPrimary_; break; case ConditionSource::SECONDARY: checkValue = secondaryValue; hasValue = hasCachedSecondary_; break; case ConditionSource::TERTIARY: checkValue = tertiaryValue; hasValue = hasCachedTertiary_; break; } if (!hasValue) continue; // Evaluate condition bool matches = false; switch (cond.op) { case ConditionOp::LESS: matches = checkValue < cond.threshold; break; case ConditionOp::LESS_EQUAL: matches = checkValue <= cond.threshold; break; case ConditionOp::EQUAL: matches = checkValue == cond.threshold; break; case ConditionOp::GREATER_EQUAL: matches = checkValue >= cond.threshold; break; case ConditionOp::GREATER: matches = checkValue > cond.threshold; break; case ConditionOp::NOT_EQUAL: matches = checkValue != cond.threshold; break; } if (matches && cond.priority < bestPriority) { bestMatch = &cond; bestPriority = cond.priority; } } if (bestMatch) { // Apply icon change if (bestMatch->style.iconCodepoint != 0 && bestMatch->style.iconCodepoint != currentConditionIcon_) { updateIcon(bestMatch->style.iconCodepoint); currentConditionIcon_ = bestMatch->style.iconCodepoint; } // Apply text color to icon if ((bestMatch->style.flags & ConditionStyle::FLAG_USE_TEXT_COLOR) && iconLabel_) { lv_obj_set_style_text_color(iconLabel_, lv_color_make( bestMatch->style.textColor.r, bestMatch->style.textColor.g, bestMatch->style.textColor.b), 0); } // Apply border color change (bgColor affects border in PowerNode) if (bestMatch->style.flags & ConditionStyle::FLAG_USE_BG_COLOR) { lv_obj_set_style_border_color(obj_, lv_color_make( bestMatch->style.bgColor.r, bestMatch->style.bgColor.g, bestMatch->style.bgColor.b), 0); } // Handle hide flag if (bestMatch->style.flags & ConditionStyle::FLAG_HIDE) { lv_obj_add_flag(obj_, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_clear_flag(obj_, LV_OBJ_FLAG_HIDDEN); } return true; } return false; }