This commit is contained in:
Thomas Peterson 2026-01-30 13:44:18 +01:00
parent c30a3ef4e7
commit 7e451b4e3b
13 changed files with 160 additions and 2484 deletions

View File

@ -227,9 +227,7 @@ bool HistoryStore::updateLatest(uint16_t groupAddr, TextSource source, float val
}
int64_t HistoryStore::now() const {
int64_t monoSec = esp_timer_get_time() / 1000000LL;
if (!timeSynced_) return monoSec;
return baseEpoch_ + (monoSec - baseMono_);
return (int64_t)time(nullptr);
}
int32_t HistoryStore::periodSeconds(ChartPeriod period) const {
@ -294,9 +292,6 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
bool hasData = false;
int32_t lastValidValue = NO_POINT;
// Optimization: Try to find a starting value from before the window if index 0 is empty?
// For now, simple forward-fill (step) is sufficient to connect sparse points.
for (size_t i = 0; i < outCount; ++i) {
if (counts[i] > 0) {
float avg = sums[i] / counts[i];
@ -305,15 +300,9 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
lastValidValue = val;
hasData = true;
} else {
// No data in this bucket.
// Use the last valid value to draw a connected line (Step Hold).
// If we haven't seen any data yet (lastValidValue == NO_POINT),
// we leave it as NO_POINT (gap at start).
outValues[i] = lastValidValue;
// If we just filled with a valid value, count it as having data
if (lastValidValue != NO_POINT) {
hasData = true; // Ensures y-scaling considers this flat line
hasData = true;
}
}
}
@ -324,8 +313,14 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
bool HistoryStore::tick() {
xSemaphoreTake(mutex_, portMAX_DELAY);
int64_t nowSec = now();
bool added = false;
// Only collect data if time is roughly synced (after 2020)
if (nowSec < 1577836800LL) {
xSemaphoreGive(mutex_);
return false;
}
bool added = false;
for (size_t i = 0; i < series_.size(); ++i) {
HistorySeries& series = series_[i];
if (!series.active || !series.hasLatest) continue;
@ -345,20 +340,18 @@ bool HistoryStore::tick() {
if (added) {
dirty_ = true;
if (timeSynced_) dataEpoch_ = true;
}
xSemaphoreGive(mutex_);
// REMOVED synchronous save from here to avoid blocking UI task
return added;
}
void HistoryStore::performAutoSave() {
xSemaphoreTake(mutex_, portMAX_DELAY);
bool shouldSave = false;
int64_t nowSec = now();
int64_t monoUs = esp_timer_get_time();
if (dirty_ && timeSynced_ && SdCard::instance().isMounted()) {
if (dirty_ && nowSec > 1577836800LL && SdCard::instance().isMounted()) {
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
shouldSave = true;
lastSaveMonoUs_ = monoUs;
@ -371,120 +364,8 @@ void HistoryStore::performAutoSave() {
}
}
void HistoryStore::updateTimeOfDay(const struct tm& value) {
xSemaphoreTake(mutex_, portMAX_DELAY);
hour_ = value.tm_hour;
minute_ = value.tm_min;
second_ = value.tm_sec;
hasTime_ = true;
// applyTimeSync calls internal logic, assume called inside lock or extracted
// But applyTimeSync calls clearAll which touches series.
// So we must be careful about locking.
// Let's unlock before applyTimeSync and re-lock inside?
// No, let's just make applyTimeSync assume lock or lock itself?
// applyTimeSync modifies member vars.
// Refactoring for simplicity: lock around the whole block
// BUT applyTimeSync calls settimeofday which is system call (thread safe?)
// We will inline the logic briefly or handle it carefully.
// For now, assume lock held is OK for short duration.
// We need to implement applyTimeSync correctly with locking.
xSemaphoreGive(mutex_);
applyTimeSync();
}
void HistoryStore::updateDate(const struct tm& value) {
xSemaphoreTake(mutex_, portMAX_DELAY);
year_ = value.tm_year;
month_ = value.tm_mon;
day_ = value.tm_mday;
hasDate_ = true;
xSemaphoreGive(mutex_);
applyTimeSync();
}
void HistoryStore::updateDateTime(const struct tm& value) {
xSemaphoreTake(mutex_, portMAX_DELAY);
year_ = value.tm_year;
month_ = value.tm_mon;
day_ = value.tm_mday;
hour_ = value.tm_hour;
minute_ = value.tm_min;
second_ = value.tm_sec;
hasDate_ = true;
hasTime_ = true;
xSemaphoreGive(mutex_);
applyTimeSync();
}
int64_t HistoryStore::buildEpoch() const {
if (year_ < 1990 || month_ < 1 || month_ > 12 || day_ < 1 || day_ > 31) return -1;
struct tm combined = {};
combined.tm_year = year_ - 1900;
combined.tm_mon = month_ - 1;
combined.tm_mday = day_;
combined.tm_hour = hour_;
combined.tm_min = minute_;
combined.tm_sec = second_;
combined.tm_isdst = -1;
time_t epoch = mktime(&combined);
if (epoch < 0) return -1;
return static_cast<int64_t>(epoch);
}
bool HistoryStore::applyTimeSync() {
xSemaphoreTake(mutex_, portMAX_DELAY);
if (!hasDate_ || !hasTime_) {
xSemaphoreGive(mutex_);
return false;
}
int64_t epoch = buildEpoch();
if (epoch <= 0) {
ESP_LOGW(TAG, "Invalid KNX time/date for sync");
xSemaphoreGive(mutex_);
return false;
}
bool wasSynced = timeSynced_;
timeSynced_ = true;
baseEpoch_ = epoch;
baseMono_ = esp_timer_get_time() / 1000000LL;
// Release lock for system call (optional but safer)
xSemaphoreGive(mutex_);
struct timeval tv = {};
tv.tv_sec = static_cast<time_t>(epoch);
settimeofday(&tv, nullptr);
xSemaphoreTake(mutex_, portMAX_DELAY);
if (!wasSynced) {
bool hasData = false;
for (size_t i = 0; i < series_.size(); ++i) {
const HistorySeries& series = series_[i];
if (!series.active) continue;
if (series.fine.count > 0 || series.coarse.count > 0 || series.hasLatest) {
hasData = true;
break;
}
}
if (!dataEpoch_ && hasData) {
clearAll();
}
dataEpoch_ = true;
}
dirty_ = true;
xSemaphoreGive(mutex_);
ESP_LOGI(TAG, "Time synced: %ld", static_cast<long>(epoch));
return !wasSynced;
}
void HistoryStore::clearAll() {
// Assumes lock is held by caller
xSemaphoreTake(mutex_, portMAX_DELAY);
for (size_t i = 0; i < series_.size(); ++i) {
HistorySeries& series = series_[i];
if (!series.active) continue;
@ -496,17 +377,14 @@ void HistoryStore::clearAll() {
series.lastFineSampleTs = 0;
series.lastCoarseSampleTs = 0;
}
xSemaphoreGive(mutex_);
}
void HistoryStore::saveToSdCard() {
if (!SdCard::instance().isMounted()) return;
xSemaphoreTake(mutex_, portMAX_DELAY);
if (!timeSynced_) {
xSemaphoreGive(mutex_);
return;
}
xSemaphoreGive(mutex_); // Release to open file
// Double check time sync before save
if (now() < 1577836800LL) return;
FILE* f = fopen(HISTORY_FILE, "wb");
if (!f) {
@ -537,8 +415,6 @@ void HistoryStore::saveToSdCard() {
return;
}
// Allocate temp buffer for one series to minimize lock time
// Size: Header + Fine + Coarse
size_t seriesDataSize = sizeof(HistoryFileSeriesHeader) +
sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP);
uint8_t* tempBuf = (uint8_t*)malloc(seriesDataSize);
@ -556,7 +432,6 @@ void HistoryStore::saveToSdCard() {
continue;
}
// Copy to temp buffer
HistoryFileSeriesHeader* sh = (HistoryFileSeriesHeader*)tempBuf;
sh->groupAddr = series.key.addr;
sh->source = static_cast<uint8_t>(series.key.source);
@ -575,7 +450,6 @@ void HistoryStore::saveToSdCard() {
xSemaphoreGive(mutex_);
// Write to file (unlocked)
if (fwrite(tempBuf, 1, seriesDataSize, f) != seriesDataSize) {
ESP_LOGW(TAG, "Failed to write series data");
break;
@ -586,7 +460,6 @@ void HistoryStore::saveToSdCard() {
xSemaphoreTake(mutex_, portMAX_DELAY);
dirty_ = false;
dataEpoch_ = true;
xSemaphoreGive(mutex_);
fclose(f);
@ -648,9 +521,8 @@ void HistoryStore::loadFromSdCard() {
}
}
dirty_ = false;
dataEpoch_ = true;
xSemaphoreGive(mutex_);
fclose(f);
ESP_LOGI(TAG, "History loaded");
}
}

View File

@ -22,11 +22,7 @@ public:
int32_t* outValues, size_t outCount) const;
int64_t now() const;
bool isTimeSynced() const { return timeSynced_; }
void updateTimeOfDay(const struct tm& value);
void updateDate(const struct tm& value);
void updateDateTime(const struct tm& value);
bool isTimeSynced() const { return now() > 1577836800LL; }
void loadFromSdCard();
void saveToSdCard();
@ -101,8 +97,6 @@ private:
static bool keysEqual(const SeriesKey& a, const SeriesKey& b);
int32_t periodSeconds(ChartPeriod period) const;
int64_t buildEpoch() const;
bool applyTimeSync();
void clearAll();
HistorySeries* findSeries(uint16_t groupAddr, TextSource source);
@ -113,21 +107,8 @@ private:
std::array<HistorySeries, HISTORY_MAX_SERIES> series_ = {};
size_t seriesCount_ = 0;
bool timeSynced_ = false;
bool hasDate_ = false;
bool hasTime_ = false;
int year_ = 0;
int month_ = 0;
int day_ = 0;
int hour_ = 0;
int minute_ = 0;
int second_ = 0;
int64_t baseEpoch_ = 0;
int64_t baseMono_ = 0;
bool dirty_ = false;
int64_t lastSaveMonoUs_ = 0;
bool dataEpoch_ = false;
mutable SemaphoreHandle_t mutex_ = nullptr;
};

View File

@ -64,6 +64,9 @@ enum class TextSource : uint8_t {
KNX_DPT_TIME = 8, // KNX Time of day (DPT 10.001)
KNX_DPT_DATE = 9, // KNX Date (DPT 11.001)
KNX_DPT_DATETIME = 10, // KNX DateTime (DPT 19.001)
SYSTEM_TIME = 11, // System Time (RTC)
SYSTEM_DATE = 12, // System Date (RTC)
SYSTEM_DATETIME = 13, // System DateTime (RTC)
};
enum class TextAlign : uint8_t {

View File

@ -481,7 +481,7 @@ void WidgetManager::showModalScreenLocked(const ScreenConfig& screen) {
for (uint8_t i = 0; i < screen.widgetCount; i++) {
const WidgetConfig& cfg = screen.widgets[i];
auto widget = WidgetFactory::create(cfg);
if (widget && cfg.id < MAX_WIDGETS) {
if (widget && cfg.id < 256) {
widget->create(modal);
widget->applyStyle();
widgets_[cfg.id] = std::move(widget);
@ -699,6 +699,8 @@ void WidgetManager::loop() {
refreshChartWidgets();
}
updateSystemTimeWidgets();
if (didUiNav) return;
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
@ -753,7 +755,7 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren
if (cfg.parentId != -1) continue;
auto widget = WidgetFactory::create(cfg);
if (widget && cfg.id < MAX_WIDGETS) {
if (widget && cfg.id < 256) {
widget->create(parent);
widget->applyStyle();
widgets_[cfg.id] = std::move(widget);
@ -773,18 +775,18 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren
const WidgetConfig& cfg = screen.widgets[i];
// Skip if already created
if (widgets_[cfg.id]) continue;
if (cfg.id < 256 && widgets_[cfg.id]) continue;
// Skip if it's a root widget (should be created in Pass 1, but if failed/skipped, ignore)
if (cfg.parentId == -1) continue;
// Check if parent exists
if (cfg.parentId >= 0 && cfg.parentId < MAX_WIDGETS && widgets_[cfg.parentId]) {
if (cfg.parentId >= 0 && cfg.parentId < 256 && widgets_[cfg.parentId]) {
// Parent exists! Get its LVGL object
lv_obj_t* parentObj = widgets_[cfg.parentId]->getObj();
if (parentObj) {
auto widget = WidgetFactory::create(cfg);
if (widget && cfg.id < MAX_WIDGETS) {
if (widget && cfg.id < 256) {
widget->create(parentObj);
widget->applyStyle();
widgets_[cfg.id] = std::move(widget);
@ -955,6 +957,30 @@ void WidgetManager::refreshChartWidgets() {
refreshChartWidgetsLocked();
}
void WidgetManager::updateSystemTimeWidgets() {
int64_t nowUs = esp_timer_get_time();
if (nowUs - lastSystemTimeUpdateUs_ < 500000) { // Update every 500ms
return;
}
lastSystemTimeUpdateUs_ = nowUs;
time_t now;
time(&now);
struct tm timeinfo;
localtime_r(&now, &timeinfo);
// Call from loop() which is ALREADY LOCKED by LVGL timer
for (auto& widget : widgets_) {
if (!widget) continue;
TextSource src = widget->getTextSource();
if (src == TextSource::SYSTEM_TIME ||
src == TextSource::SYSTEM_DATE ||
src == TextSource::SYSTEM_DATETIME) {
widget->onKnxTime(timeinfo, src);
}
}
}
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) {
for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr &&
@ -989,26 +1015,40 @@ void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) {
}
void WidgetManager::applyKnxTime(uint16_t groupAddr, const struct tm& value, KnxTimeType type) {
bool updated = false;
switch (type) {
case KnxTimeType::TIME:
if (config_.knxTimeAddress != 0 && groupAddr == config_.knxTimeAddress) {
HistoryStore::instance().updateTimeOfDay(value);
updated = true;
}
break;
case KnxTimeType::DATE:
if (config_.knxDateAddress != 0 && groupAddr == config_.knxDateAddress) {
HistoryStore::instance().updateDate(value);
updated = true;
}
break;
case KnxTimeType::DATETIME:
if (config_.knxDateTimeAddress != 0 && groupAddr == config_.knxDateTimeAddress) {
HistoryStore::instance().updateDateTime(value);
updated = true;
}
break;
// Simplified system time synchronization
bool isGlobalTime = false;
if (type == KnxTimeType::TIME && config_.knxTimeAddress != 0 && groupAddr == config_.knxTimeAddress) isGlobalTime = true;
if (type == KnxTimeType::DATE && config_.knxDateAddress != 0 && groupAddr == config_.knxDateAddress) isGlobalTime = true;
if (type == KnxTimeType::DATETIME && config_.knxDateTimeAddress != 0 && groupAddr == config_.knxDateTimeAddress) isGlobalTime = true;
if (isGlobalTime) {
time_t now;
time(&now);
struct tm t_new;
localtime_r(&now, &t_new);
if (type == KnxTimeType::TIME) {
t_new.tm_hour = value.tm_hour;
t_new.tm_min = value.tm_min;
t_new.tm_sec = value.tm_sec;
} else if (type == KnxTimeType::DATE) {
t_new.tm_year = value.tm_year;
if (t_new.tm_year < 1900) t_new.tm_year += 1900;
t_new.tm_mon = value.tm_mon;
t_new.tm_mday = value.tm_mday;
} else if (type == KnxTimeType::DATETIME) {
t_new = value;
if (t_new.tm_year < 1900) t_new.tm_year += 1900;
}
// Final normalization for mktime (expects year - 1900)
if (t_new.tm_year >= 1900) t_new.tm_year -= 1900;
time_t t = mktime(&t_new);
if (t != -1) {
struct timeval tv = { .tv_sec = t, .tv_usec = 0 };
settimeofday(&tv, NULL);
}
}
TextSource source = TextSource::STATIC;
@ -1025,9 +1065,7 @@ void WidgetManager::applyKnxTime(uint16_t groupAddr, const struct tm& value, Knx
}
}
if (updated) {
chartRefreshPending_ = true;
}
chartRefreshPending_ = true;
}
void WidgetManager::cacheKnxValue(uint16_t groupAddr, TextSource source, float value) {

View File

@ -45,6 +45,9 @@ public:
// User activity (resets standby timer)
void onUserActivity();
// Update widgets displaying system time
void updateSystemTimeWidgets();
// Thread-safe KNX updates (queued to UI thread)
void onKnxValue(uint16_t groupAddr, float value, TextSource source);
void onKnxValue(uint16_t groupAddr, float value);
@ -170,9 +173,10 @@ private:
uint8_t navTargetScreen_ = 0xFF;
int64_t navRequestUs_ = 0;
int64_t lastActivityUs_ = 0;
int64_t lastSystemTimeUpdateUs_ = 0;
// Runtime widget instances (indexed by widget ID)
std::array<std::unique_ptr<Widget>, MAX_WIDGETS> widgets_;
std::array<std::unique_ptr<Widget>, 256> widgets_;
lv_obj_t* screen_ = nullptr;
lv_obj_t* modalContainer_ = nullptr;
lv_obj_t* modalDimmer_ = nullptr;

View File

@ -174,6 +174,17 @@ lv_obj_t* LabelWidget::create(lv_obj_t* parent) {
if (obj_ != nullptr) {
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
// Initial update for system time widgets to avoid empty display
if (config_.textSource == TextSource::SYSTEM_TIME ||
config_.textSource == TextSource::SYSTEM_DATE ||
config_.textSource == TextSource::SYSTEM_DATETIME) {
time_t now;
time(&now);
struct tm timeinfo;
localtime_r(&now, &timeinfo);
onKnxTime(timeinfo, config_.textSource);
}
}
return obj_;
}
@ -260,16 +271,25 @@ void LabelWidget::onKnxTime(const struct tm& value, TextSource source) {
if (config_.textSource != source) return;
if (source != TextSource::KNX_DPT_TIME &&
source != TextSource::KNX_DPT_DATE &&
source != TextSource::KNX_DPT_DATETIME) {
source != TextSource::KNX_DPT_DATETIME &&
source != TextSource::SYSTEM_TIME &&
source != TextSource::SYSTEM_DATE &&
source != TextSource::SYSTEM_DATETIME) {
return;
}
int year = value.tm_year;
// tm_year is usually year-1900.
// If value comes from KNX raw decode, it might be full year depending on logic.
// Standard tm_year: 123 for 2023.
// Our existing logic handled year < 1900 by adding 1900.
// Let's ensure consistent behavior for SYSTEM time which uses standard tm struct.
if (year > 0 && year < 1900) {
year += 1900;
}
int month = value.tm_mon;
if (month < 1 || month > 12) {
// tm_mon is 0-11
if (month >= 0 && month <= 11) {
month += 1;
}
@ -277,9 +297,9 @@ void LabelWidget::onKnxTime(const struct tm& value, TextSource source) {
const char* fmt = config_.text;
if (!fmt || fmt[0] == '\0' || strchr(fmt, '%') == nullptr) {
if (source == TextSource::KNX_DPT_TIME) {
if (source == TextSource::KNX_DPT_TIME || source == TextSource::SYSTEM_TIME) {
fmt = "%02d:%02d:%02d";
} else if (source == TextSource::KNX_DPT_DATE) {
} else if (source == TextSource::KNX_DPT_DATE || source == TextSource::SYSTEM_DATE) {
fmt = "%02d.%02d.%04d";
} else {
fmt = "%02d.%02d.%04d %02d:%02d:%02d";
@ -287,9 +307,9 @@ void LabelWidget::onKnxTime(const struct tm& value, TextSource source) {
}
char buf[32];
if (source == TextSource::KNX_DPT_TIME) {
if (source == TextSource::KNX_DPT_TIME || source == TextSource::SYSTEM_TIME) {
snprintf(buf, sizeof(buf), fmt, value.tm_hour, value.tm_min, value.tm_sec);
} else if (source == TextSource::KNX_DPT_DATE) {
} else if (source == TextSource::KNX_DPT_DATE || source == TextSource::SYSTEM_DATE) {
snprintf(buf, sizeof(buf), fmt, value.tm_mday, month, year);
} else {
snprintf(buf, sizeof(buf), fmt, value.tm_mday, month, year,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -29,7 +29,7 @@
</div>
<template v-else>
<div :class="rowClass"><label :class="labelClass">Format</label><input :class="inputClass" type="text" v-model="w.text"></div>
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
<div v-if="w.textSrc < 11" :class="rowClass"><label :class="labelClass">KNX Objekt</label>
<select :class="inputClass" v-model.number="w.knxAddr">
<option :value="0">-- Waehlen --</option>
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">

View File

@ -72,11 +72,15 @@ export const textSources = {
7: 'KNX Dezimalfaktor (DPT 5.005)',
8: 'KNX Uhrzeit (DPT 10.001)',
9: 'KNX Datum (DPT 11.001)',
10: 'KNX Datum & Uhrzeit (DPT 19.001)'
10: 'KNX Datum & Uhrzeit (DPT 19.001)',
11: 'System Uhrzeit',
12: 'System Datum',
13: 'System Datum & Uhrzeit'
};
export const textSourceGroups = [
{ label: 'Statisch', values: [0] },
{ label: 'System', values: [11, 12, 13] },
{ label: 'DPT 1.x', values: [2] },
{ label: 'DPT 5.x', values: [3, 7] },
{ label: 'DPT 9.x', values: [1] },
@ -89,7 +93,7 @@ export const textSourceGroups = [
];
export const sourceOptions = {
label: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
label: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
button: [0],
led: [0, 2],
icon: [0, 2],
@ -126,7 +130,10 @@ export const defaultFormats = {
7: '%d',
8: '%02d:%02d:%02d',
9: '%02d.%02d.%04d',
10: '%02d.%02d.%04d %02d:%02d:%02d'
10: '%02d.%02d.%04d %02d:%02d:%02d',
11: '%02d:%02d:%02d',
12: '%02d.%02d.%04d',
13: '%02d.%02d.%04d %02d:%02d:%02d'
};
export const WIDGET_DEFAULTS = {