mirror of
https://github.com/OpenEPaperLink/OpenEPaperLink.git
synced 2026-03-21 10:06:07 +01:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d09e89c9dd | ||
|
|
ce319bb499 | ||
|
|
4dbcc753fd | ||
|
|
6951cd79b7 | ||
|
|
eeb18f204d | ||
|
|
bfff2ef0b9 | ||
|
|
ef5ddac368 | ||
|
|
299b8f300e | ||
|
|
0b0802ad02 | ||
|
|
f8ce3a51d2 | ||
|
|
0e63e064fc | ||
|
|
8d6c763aba | ||
|
|
ab48cbe747 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -104,22 +104,22 @@ extern Arduino_RGB_Display *gfx;
|
||||
#define LCD_VSYNC 17
|
||||
#define LCD_HSYNC 16
|
||||
#define LCD_PCLK 21
|
||||
#define LCD_R0 11
|
||||
#define LCD_R1 12
|
||||
#define LCD_R2 13
|
||||
#define LCD_R3 14
|
||||
#define LCD_R4 0
|
||||
#define LCD_R0 0
|
||||
#define LCD_R1 11
|
||||
#define LCD_R2 12
|
||||
#define LCD_R3 13
|
||||
#define LCD_R4 14
|
||||
#define LCD_G0 8
|
||||
#define LCD_G1 20
|
||||
#define LCD_G2 3
|
||||
#define LCD_G3 46
|
||||
#define LCD_G4 9
|
||||
#define LCD_G5 10
|
||||
#define LCD_B0 4
|
||||
#define LCD_B1 5
|
||||
#define LCD_B2 6
|
||||
#define LCD_B3 7
|
||||
#define LCD_B4 15
|
||||
#define LCD_B0 15
|
||||
#define LCD_B1 4
|
||||
#define LCD_B2 5
|
||||
#define LCD_B3 6
|
||||
#define LCD_B4 7
|
||||
#define LCD_BL 38
|
||||
#define LCD_DE 18
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ struct imgParam {
|
||||
|
||||
uint8_t zlib;
|
||||
uint8_t g5;
|
||||
uint8_t ts_option;
|
||||
};
|
||||
|
||||
void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams);
|
||||
|
||||
@@ -259,6 +259,27 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
} else if (interval < 180)
|
||||
interval = 60 * 60;
|
||||
|
||||
imageParams.ts_option = config.showtimestamp;
|
||||
if(imageParams.ts_option) {
|
||||
JsonDocument loc;
|
||||
getTemplate(loc, taginfo->contentMode, taginfo->hwType);
|
||||
|
||||
if(loc["ts_option"].is<int>()) {
|
||||
// Overide ts_option if present in template
|
||||
imageParams.ts_option = loc["ts_option"];
|
||||
}
|
||||
else {
|
||||
const JsonArray jsonArray = loc.as<JsonArray>();
|
||||
for (const JsonVariant &elem : jsonArray) {
|
||||
if(elem["ts_option"].is<int>()) {
|
||||
// Overide ts_option if present in template
|
||||
imageParams.ts_option = elem["ts_option"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (taginfo->contentMode) {
|
||||
case 0: // Not configured
|
||||
case 22: // Static image
|
||||
@@ -560,10 +581,26 @@ void drawNew(const uint8_t mac[8], tagRecord *&taginfo) {
|
||||
case 27: // Day Ahead:
|
||||
|
||||
if (getDayAheadFeed(filename, cfgobj, taginfo, imageParams)) {
|
||||
taginfo->nextupdate = now + (3600 - now % 3600);
|
||||
// Get user-configured update frequency in minutes, default to 15 minutes (matching 15-min data intervals)
|
||||
int updateFreqMinutes = 15;
|
||||
if (cfgobj["updatefreq"].is<String>()) {
|
||||
String freqStr = cfgobj["updatefreq"].as<String>();
|
||||
freqStr.trim();
|
||||
if (freqStr.length() > 0) {
|
||||
updateFreqMinutes = freqStr.toInt();
|
||||
if (updateFreqMinutes < 1) {
|
||||
wsErr("Invalid update frequency, defaulting to 15 minutes");
|
||||
updateFreqMinutes = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
int updateInterval = updateFreqMinutes * 60; // Convert to seconds
|
||||
|
||||
// Schedule next update (align to interval boundary for consistent timing)
|
||||
taginfo->nextupdate = now + (updateInterval - now % updateInterval);
|
||||
updateTagImage(filename, mac, 0, taginfo, imageParams);
|
||||
} else {
|
||||
taginfo->nextupdate = now + 300;
|
||||
taginfo->nextupdate = now + 300; // Retry in 5 minutes on failure
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
@@ -1645,9 +1682,277 @@ YAxisScale calculateYAxisScale(double priceMin, double priceMax, int divisions)
|
||||
return {minY, maxY, roundedStepSize};
|
||||
}
|
||||
|
||||
bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo, imgParam &imageParams) {
|
||||
// Helper function: calculate data range and align to interval boundaries
|
||||
// Returns: DataRange struct
|
||||
struct DataRange {
|
||||
int startIndex;
|
||||
int endIndex;
|
||||
int numAveragedSamples;
|
||||
int availableSamples;
|
||||
};
|
||||
|
||||
DataRange calculateDataRangeAndAlignment(const JsonDocument& doc, int originalDataSize, time_t now,
|
||||
int avgFactor, int targetIntervalMinutes, int barAreaWidth) {
|
||||
DataRange result;
|
||||
|
||||
// Step 1: Find data range - start at now-1h, trim far future if needed
|
||||
time_t targetStart = now - 3600; // 1 hour ago
|
||||
|
||||
// Find first data point at/before now-1h
|
||||
result.startIndex = 0;
|
||||
for (int i = originalDataSize - 1; i >= 0; i--) {
|
||||
time_t dataTime = doc[i]["time"];
|
||||
if (dataTime <= targetStart) {
|
||||
result.startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate maximum bars that can fit on display
|
||||
int maxBars = barAreaWidth; // 1px per bar minimum
|
||||
|
||||
// Calculate how many samples we'll have after averaging
|
||||
result.availableSamples = originalDataSize - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
|
||||
// Step 2: Trim from far future if needed (prioritize now and near future)
|
||||
result.endIndex = originalDataSize;
|
||||
if (result.numAveragedSamples > maxBars) {
|
||||
// Too many samples - limit the range
|
||||
int maxSamplesNeeded = maxBars * avgFactor;
|
||||
result.endIndex = result.startIndex + maxSamplesNeeded;
|
||||
if (result.endIndex > originalDataSize) result.endIndex = originalDataSize;
|
||||
result.availableSamples = result.endIndex - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
}
|
||||
|
||||
// Step 3: Align to interval boundary if averaging
|
||||
if (avgFactor > 1) {
|
||||
// Find first data point that aligns with target interval
|
||||
for (int i = result.startIndex; i < result.endIndex; i++) {
|
||||
time_t dataTime = doc[i]["time"];
|
||||
struct tm dt;
|
||||
localtime_r(&dataTime, &dt);
|
||||
|
||||
// Check if this timestamp aligns with target interval
|
||||
if (targetIntervalMinutes == 30) {
|
||||
if (dt.tm_min == 0 || dt.tm_min == 30) {
|
||||
result.startIndex = i;
|
||||
break;
|
||||
}
|
||||
} else if (targetIntervalMinutes == 60) {
|
||||
if (dt.tm_min == 0) {
|
||||
result.startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate after alignment
|
||||
result.availableSamples = result.endIndex - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function: Perform historic data backfill to optimize bar width
|
||||
// Returns: BackfillResult struct
|
||||
struct BackfillResult {
|
||||
int startIndex;
|
||||
int numAveragedSamples;
|
||||
int availableSamples;
|
||||
int addedHistoricCount;
|
||||
double barwidthBefore;
|
||||
double barwidthAfter;
|
||||
};
|
||||
|
||||
BackfillResult performHistoricBackfill(int startIndex, int endIndex, int numAveragedSamples,
|
||||
int avgFactor, int barAreaWidth, double targetBarwidth) {
|
||||
BackfillResult result;
|
||||
result.startIndex = startIndex;
|
||||
result.numAveragedSamples = numAveragedSamples;
|
||||
result.availableSamples = endIndex - startIndex;
|
||||
result.addedHistoricCount = 0;
|
||||
|
||||
int futureRawCount = endIndex - startIndex; // Raw samples (before averaging)
|
||||
int historicRawCount = startIndex; // Raw samples available before startIndex
|
||||
|
||||
// Calculate barwidth BEFORE backfill
|
||||
result.barwidthBefore = (double)barAreaWidth / numAveragedSamples;
|
||||
|
||||
if (historicRawCount > 0) {
|
||||
// Calculate current bar width
|
||||
double pixelsPerBar = (double)barAreaWidth / numAveragedSamples;
|
||||
int currentSpacing = (pixelsPerBar >= 3.0) ? 1 : 0;
|
||||
double currentBarWidth = pixelsPerBar - currentSpacing;
|
||||
|
||||
// Scenario 2: Bars > 5px target - add data to reduce toward target
|
||||
if (currentBarWidth > targetBarwidth) {
|
||||
// Target: 5px bars with 1px spacing = 6px total per bar
|
||||
int targetBars = barAreaWidth / (targetBarwidth + 1.0);
|
||||
int barsToAdd = targetBars - numAveragedSamples;
|
||||
|
||||
if (barsToAdd > 0) {
|
||||
int samplesToAdd = barsToAdd * avgFactor;
|
||||
result.addedHistoricCount = std::min(samplesToAdd, historicRawCount);
|
||||
result.startIndex -= result.addedHistoricCount;
|
||||
|
||||
// Bounds safety: ensure startIndex never goes negative
|
||||
if (result.startIndex < 0) {
|
||||
result.addedHistoricCount += result.startIndex; // Reduce by overflow amount
|
||||
result.startIndex = 0;
|
||||
}
|
||||
|
||||
result.availableSamples = endIndex - result.startIndex;
|
||||
result.numAveragedSamples = result.availableSamples / avgFactor;
|
||||
}
|
||||
}
|
||||
// Scenario 1: Bars <= 5px - fill empty space with historic data
|
||||
else {
|
||||
double currentPixelsPerBar = pixelsPerBar; // Use pixelsPerBar (not barwidth)
|
||||
int currentPixelsInt = (int)currentPixelsPerBar; // e.g., 3 from 3.05px
|
||||
|
||||
// Keep adding bars as long as pixelsPerBar doesn't drop below next integer
|
||||
while (historicRawCount > 0) {
|
||||
// Try adding avgFactor more raw samples (= 1 more bar after averaging)
|
||||
int testAveragedBars = result.numAveragedSamples + 1;
|
||||
double testPixelsPerBar = (double)barAreaWidth / testAveragedBars;
|
||||
int testPixelsInt = (int)testPixelsPerBar;
|
||||
|
||||
// Check if pixelsPerBar stays at or above current integer value
|
||||
// This ensures bars don't shrink (e.g., stay at 3px, don't drop to 2px)
|
||||
if (testPixelsInt >= currentPixelsInt) {
|
||||
// Pixels per bar still >= current integer - add this bar
|
||||
result.numAveragedSamples = testAveragedBars;
|
||||
historicRawCount -= avgFactor; // Consume raw samples
|
||||
result.addedHistoricCount += avgFactor;
|
||||
result.startIndex -= avgFactor; // Move start backwards into historic data
|
||||
|
||||
// Bounds safety: ensure startIndex never goes negative
|
||||
if (result.startIndex < 0) {
|
||||
result.addedHistoricCount += result.startIndex; // Reduce by overflow amount
|
||||
result.numAveragedSamples = (endIndex - 0) / avgFactor; // Recalculate with startIndex=0
|
||||
result.startIndex = 0;
|
||||
break; // Stop backfill
|
||||
}
|
||||
} else {
|
||||
// Next bar would drop below current integer, done
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update availableSamples after backfill
|
||||
if (result.addedHistoricCount > 0) {
|
||||
result.availableSamples = endIndex - result.startIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate barwidth after backfill
|
||||
result.barwidthAfter = (double)barAreaWidth / result.numAveragedSamples;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Data structure for averaged price points
|
||||
struct AveragedDataPoint {
|
||||
time_t time;
|
||||
double price;
|
||||
int hour;
|
||||
int minute;
|
||||
};
|
||||
|
||||
// Helper function: Find cheapest consecutive blocks for appliance scheduling
|
||||
// Returns: CheapBlock struct
|
||||
struct CheapBlock {
|
||||
int start;
|
||||
int end;
|
||||
};
|
||||
|
||||
struct CheapBlocks {
|
||||
CheapBlock block1;
|
||||
CheapBlock block2;
|
||||
};
|
||||
|
||||
CheapBlocks findCheapestConsecutiveBlocks(const AveragedDataPoint* avgData, int numSamples,
|
||||
int blockSamples, time_t now) {
|
||||
CheapBlocks result;
|
||||
result.block1.start = -1;
|
||||
result.block1.end = -1;
|
||||
result.block2.start = -1;
|
||||
result.block2.end = -1;
|
||||
|
||||
if (blockSamples <= 0 || blockSamples > numSamples) {
|
||||
return result; // Invalid or disabled
|
||||
}
|
||||
|
||||
// Find all candidate blocks (simple: sum all prices in each block)
|
||||
struct BlockCandidate {
|
||||
int start;
|
||||
int end;
|
||||
double sum;
|
||||
};
|
||||
std::vector<BlockCandidate> candidates;
|
||||
|
||||
// Scan through visible data to find all valid consecutive blocks
|
||||
for (int i = 0; i <= numSamples - blockSamples; i++) {
|
||||
const time_t block_start_time = avgData[i].time;
|
||||
|
||||
// Only consider future blocks (skip past data)
|
||||
if (block_start_time < now) continue;
|
||||
|
||||
// Calculate sum for this block
|
||||
double blockSum = 0;
|
||||
for (int j = 0; j < blockSamples; j++) {
|
||||
blockSum += avgData[i + j].price;
|
||||
}
|
||||
|
||||
BlockCandidate candidate;
|
||||
candidate.start = i;
|
||||
candidate.end = i + blockSamples - 1;
|
||||
candidate.sum = blockSum;
|
||||
candidates.push_back(candidate);
|
||||
}
|
||||
|
||||
// Sort candidates by sum (cheapest first)
|
||||
std::sort(candidates.begin(), candidates.end(),
|
||||
[](const BlockCandidate& a, const BlockCandidate& b) { return a.sum < b.sum; });
|
||||
|
||||
// Pick up to 2 non-overlapping cheap blocks
|
||||
if (candidates.size() > 0) {
|
||||
result.block1.start = candidates[0].start;
|
||||
result.block1.end = candidates[0].end;
|
||||
|
||||
// Find second block that doesn't overlap with first
|
||||
for (size_t i = 1; i < candidates.size(); i++) {
|
||||
bool overlaps = (candidates[i].start <= result.block1.end && candidates[i].end >= result.block1.start);
|
||||
if (!overlaps) {
|
||||
result.block2.start = candidates[i].start;
|
||||
result.block2.end = candidates[i].end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool getDayAheadFeed(String& filename, JsonObject& cfgobj, tagRecord*& taginfo, imgParam& imageParams) {
|
||||
wsLog("get dayahead prices");
|
||||
|
||||
// Magic number constants for bar width calculations
|
||||
const double TARGET_BARWIDTH = 5.0; // Optimal bar width in pixels (good visibility)
|
||||
const double MIN_BARWIDTH = 2.0; // Minimum bar width (below this, bars hard to see)
|
||||
const int MIN_LABEL_SPACING = 30; // Minimum pixels between label centers
|
||||
const int DASH_LENGTH = 3; // Dashed line segment length
|
||||
const int GAP_LENGTH = 2; // Gap between dashed line segments
|
||||
const int STEM_WIDTH = 3; // Current time arrow stem width
|
||||
const int ARROW_WIDTH = 8; // Current time arrow head width
|
||||
const int ARROW_HEIGHT = 6; // Current time arrow head height
|
||||
const int STEM_HEIGHT = 10; // Current time arrow stem height
|
||||
const int GAP_AFTER_ARROW = 2; // Gap between arrow tip and vertical line
|
||||
|
||||
JsonDocument loc;
|
||||
getTemplate(loc, 27, taginfo->hwType);
|
||||
|
||||
@@ -1657,11 +1962,6 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm timeinfo;
|
||||
localtime_r(&now, &timeinfo);
|
||||
|
||||
char dateString[40];
|
||||
strftime(dateString, sizeof(dateString), languageDateFormat[0].c_str(), &timeinfo);
|
||||
|
||||
HTTPClient http;
|
||||
http.begin(URL);
|
||||
@@ -1684,20 +1984,80 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
|
||||
initSprite(spr, imageParams.width, imageParams.height, imageParams);
|
||||
|
||||
int n = doc.size();
|
||||
if (n == 0) {
|
||||
int originalDataSize = doc.size();
|
||||
if (originalDataSize == 0) {
|
||||
wsErr("No data in dayahead feed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Detect native data interval (15-min, hourly, etc.)
|
||||
int nativeIntervalMinutes = 15; // Default to 15 minutes (most common in Europe)
|
||||
if (originalDataSize >= 2) {
|
||||
time_t time0 = doc[0]["time"];
|
||||
time_t time1 = doc[1]["time"];
|
||||
nativeIntervalMinutes = (time1 - time0) / 60;
|
||||
}
|
||||
|
||||
// Get user's preferred display interval (0 = native, 30 = 30 minutes, 60 = 1 hour)
|
||||
int targetIntervalMinutes = cfgobj["interval"] ? cfgobj["interval"].as<int>() : 0;
|
||||
if (targetIntervalMinutes == 0) {
|
||||
targetIntervalMinutes = nativeIntervalMinutes; // Use native interval
|
||||
}
|
||||
|
||||
// Calculate averaging factor (how many native samples per target interval)
|
||||
int avgFactor = targetIntervalMinutes / nativeIntervalMinutes;
|
||||
if (avgFactor < 1) avgFactor = 1; // Safety: no upsampling
|
||||
|
||||
// Get bar area width for calculations
|
||||
int barAreaWidth = loc["bars"][1].as<int>();
|
||||
|
||||
// Store RAW current price BEFORE averaging (for accurate "now" display)
|
||||
// We'll calculate this after we have units/tariff variables below
|
||||
double rawCurrentPrice = std::numeric_limits<double>::quiet_NaN();
|
||||
int rawCurrentIndex = -1;
|
||||
int rawCurrentHour = 0; // Hour from the native interval containing "now"
|
||||
int rawCurrentMinute = 0; // Minute from the native interval containing "now"
|
||||
|
||||
// Calculate data range and align to interval boundaries (Steps 1-3)
|
||||
DataRange range = calculateDataRangeAndAlignment(doc, originalDataSize, now, avgFactor,
|
||||
targetIntervalMinutes, barAreaWidth);
|
||||
int startIndex = range.startIndex;
|
||||
int endIndex = range.endIndex;
|
||||
int numAveragedSamples = range.numAveragedSamples;
|
||||
int availableSamples = range.availableSamples;
|
||||
|
||||
// Safety check
|
||||
if (numAveragedSamples == 0) {
|
||||
wsErr("Not enough data for selected interval");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform historic data backfill to optimize bar width (Step 4)
|
||||
BackfillResult backfill = performHistoricBackfill(startIndex, endIndex, numAveragedSamples,
|
||||
avgFactor, barAreaWidth, TARGET_BARWIDTH);
|
||||
startIndex = backfill.startIndex;
|
||||
numAveragedSamples = backfill.numAveragedSamples;
|
||||
availableSamples = backfill.availableSamples;
|
||||
|
||||
// Debug logging (always visible in web console)
|
||||
int futureRawCount = endIndex - (startIndex + backfill.addedHistoricCount);
|
||||
int historicRawCount = startIndex + backfill.addedHistoricCount;
|
||||
wsLog("Day-ahead bar sizing: future_raw=" + String(futureRawCount) +
|
||||
" (incl now), historic_raw=" + String(historicRawCount) +
|
||||
", barwidth_before=" + String(backfill.barwidthBefore, 1) +
|
||||
"px, added_historic=" + String(backfill.addedHistoricCount) +
|
||||
", barwidth_after=" + String(backfill.barwidthAfter, 1) +
|
||||
"px (target=" + String(TARGET_BARWIDTH, 1) + "px)");
|
||||
|
||||
int units = cfgobj["units"].as<int>();
|
||||
if (units == 0) units = 1;
|
||||
double tarifkwh;
|
||||
double tariftax = cfgobj["tarifftax"].as<double>();
|
||||
double minPrice = std::numeric_limits<double>::max();
|
||||
double maxPrice = std::numeric_limits<double>::lowest();
|
||||
double prices[n];
|
||||
|
||||
// Create averaged data array
|
||||
AveragedDataPoint avgData[numAveragedSamples];
|
||||
|
||||
// Parse tariff array if provided
|
||||
JsonDocument doc2;
|
||||
JsonArray tariffArray;
|
||||
std::string tariffString = cfgobj["tariffkwh"].as<std::string>();
|
||||
@@ -1708,19 +2068,84 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
Serial.println("Error in tariffkwh array");
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
const JsonObject &obj = doc[i];
|
||||
|
||||
if (tariffArray.size() == 24) {
|
||||
// Average the data according to selected interval
|
||||
for (int i = 0; i < numAveragedSamples; i++) {
|
||||
double priceSum = 0;
|
||||
time_t blockTime = doc[startIndex + i * avgFactor]["time"];
|
||||
|
||||
// Average prices across the interval
|
||||
for (int j = 0; j < avgFactor; j++) {
|
||||
int idx = startIndex + i * avgFactor + j;
|
||||
if (idx >= endIndex) break; // Safety check - respect endIndex
|
||||
|
||||
const JsonObject& obj = doc[idx];
|
||||
const time_t item_time = obj["time"];
|
||||
struct tm item_timeinfo;
|
||||
localtime_r(&item_time, &item_timeinfo);
|
||||
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
if (tariffArray.size() == 24) {
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
}
|
||||
|
||||
double price = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
priceSum += price;
|
||||
}
|
||||
prices[i] = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
|
||||
// Store averaged data
|
||||
avgData[i].price = priceSum / avgFactor;
|
||||
avgData[i].time = blockTime;
|
||||
struct tm blockTimeInfo;
|
||||
localtime_r(&blockTime, &blockTimeInfo);
|
||||
avgData[i].hour = blockTimeInfo.tm_hour;
|
||||
avgData[i].minute = blockTimeInfo.tm_min;
|
||||
}
|
||||
|
||||
// Now work with averaged data (n becomes numAveragedSamples)
|
||||
int n = numAveragedSamples;
|
||||
|
||||
// Calculate RAW current price (find closest PAST/CURRENT data point, never future)
|
||||
if (std::isnan(rawCurrentPrice)) {
|
||||
time_t closestTimeDiff = std::numeric_limits<time_t>::max();
|
||||
|
||||
for (int i = 0; i < originalDataSize; i++) {
|
||||
time_t dataTime = doc[i]["time"];
|
||||
|
||||
// Skip future timestamps - only consider past and current
|
||||
if (dataTime > now) continue;
|
||||
|
||||
time_t diff = now - dataTime;
|
||||
|
||||
if (diff < closestTimeDiff) {
|
||||
closestTimeDiff = diff;
|
||||
rawCurrentIndex = i;
|
||||
|
||||
// Calculate raw price with tariff (same logic as averaging loop)
|
||||
struct tm item_timeinfo;
|
||||
localtime_r(&dataTime, &item_timeinfo);
|
||||
|
||||
if (tariffArray.size() == 24) {
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
}
|
||||
|
||||
rawCurrentPrice = (doc[i]["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
rawCurrentHour = item_timeinfo.tm_hour; // Store the native interval's hour
|
||||
rawCurrentMinute = item_timeinfo.tm_min; // Store the native interval's minute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate min/max and create sorted price array for percentiles
|
||||
double minPrice = std::numeric_limits<double>::max();
|
||||
double maxPrice = std::numeric_limits<double>::lowest();
|
||||
double prices[n];
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
prices[i] = avgData[i].price;
|
||||
minPrice = std::min(minPrice, prices[i]);
|
||||
maxPrice = std::max(maxPrice, prices[i]);
|
||||
}
|
||||
@@ -1742,7 +2167,25 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t barwidth = loc["bars"][1].as<int>() / n;
|
||||
// Step 4: Calculate optimal bar width and spacing
|
||||
// Goal: Fill entire width with bars, using spacing when room allows
|
||||
int availableWidth = loc["bars"][1].as<int>();
|
||||
int pixelsPerBar = availableWidth / n; // How many pixels each bar+spacing can use
|
||||
int barwidth;
|
||||
int barSpacing;
|
||||
|
||||
if (pixelsPerBar >= 3) {
|
||||
// Room for spacing - use 1px gap between bars
|
||||
barSpacing = 1;
|
||||
barwidth = pixelsPerBar - barSpacing; // Remaining pixels for bar itself
|
||||
} else {
|
||||
// Tight fit - no room for spacing
|
||||
barSpacing = 0;
|
||||
barwidth = pixelsPerBar; // Use all available pixels for bar
|
||||
}
|
||||
|
||||
// Result: n × (barwidth + barSpacing) ≈ availableWidth (within rounding)
|
||||
|
||||
uint16_t barheight = loc["bars"][2].as<int>() / (maxPrice - minPrice);
|
||||
uint16_t arrowY = 0;
|
||||
if (loc["bars"].size() >= 5) arrowY = loc["bars"][4].as<int>();
|
||||
@@ -1751,43 +2194,148 @@ bool getDayAheadFeed(String &filename, JsonObject &cfgobj, tagRecord *&taginfo,
|
||||
bool showcurrent = true;
|
||||
if (cfgobj["showcurr"] && cfgobj["showcurr"] == "0") showcurrent = false;
|
||||
|
||||
// Calculate label interval based on display width and number of bars
|
||||
// Goal: Space labels far enough apart to avoid overlap
|
||||
// Note: availableWidth already declared above, reuse it here
|
||||
int maxLabels = availableWidth / MIN_LABEL_SPACING;
|
||||
int labelInterval = (n + maxLabels - 1) / maxLabels; // Round up division
|
||||
|
||||
// Ensure labelInterval is at least 1
|
||||
if (labelInterval < 1) labelInterval = 1;
|
||||
|
||||
// For better aesthetics, round to nearest multiple based on target interval
|
||||
int labelsPerHour = 60 / targetIntervalMinutes;
|
||||
if (labelsPerHour > 0) {
|
||||
// Try to show labels at nice intervals (every hour, every 2 hours, etc.)
|
||||
int hoursPerLabel = (labelInterval + labelsPerHour - 1) / labelsPerHour; // Round up
|
||||
if (hoursPerLabel < 1) hoursPerLabel = 1;
|
||||
labelInterval = hoursPerLabel * labelsPerHour;
|
||||
|
||||
// But don't exceed our max labels constraint
|
||||
if (n / labelInterval > maxLabels) {
|
||||
// Fall back to spacing-based calculation
|
||||
labelInterval = (n + maxLabels - 1) / maxLabels;
|
||||
}
|
||||
}
|
||||
|
||||
// Find cheapest consecutive block(s) for appliance scheduling (Step 5)
|
||||
double cheapBlockHoursRaw = cfgobj["cheapblock"] ? cfgobj["cheapblock"].as<double>() : 3.0;
|
||||
int blockSamples = (int)(cheapBlockHoursRaw * labelsPerHour); // Convert hours to sample count
|
||||
|
||||
CheapBlocks cheapBlocks = findCheapestConsecutiveBlocks(avgData, n, blockSamples, now);
|
||||
int cheapBlockStart1 = cheapBlocks.block1.start;
|
||||
int cheapBlockEnd1 = cheapBlocks.block1.end;
|
||||
int cheapBlockStart2 = cheapBlocks.block2.start;
|
||||
int cheapBlockEnd2 = cheapBlocks.block2.end;
|
||||
|
||||
// Store pointer position for drawing after all bars
|
||||
int pointerX = -1; // -1 means no pointer to draw
|
||||
int currentHour = 0; // Hour of the current interval
|
||||
int currentMinute = 0; // Minute of the current interval
|
||||
int currentBarHeight = 0; // Height of the current bar (for narrow bar line skipping)
|
||||
|
||||
// Draw light grey background for cheapest blocks (BEFORE labels/ticks so they draw on top)
|
||||
int baselineY = spr.height() - barBottom;
|
||||
int xAxisHeight = 20; // Height of X-axis area (ticks + labels)
|
||||
|
||||
if (cheapBlockStart1 >= 0 && cheapBlockEnd1 >= 0) {
|
||||
int rectX = barX + cheapBlockStart1 * (barwidth + barSpacing);
|
||||
int rectWidth = (cheapBlockEnd1 - cheapBlockStart1 + 1) * (barwidth + barSpacing);
|
||||
spr.fillRect(rectX, baselineY + 1, rectWidth, xAxisHeight - 1, TFT_LIGHTGREY);
|
||||
}
|
||||
if (cheapBlockStart2 >= 0 && cheapBlockEnd2 >= 0) {
|
||||
int rectX = barX + cheapBlockStart2 * (barwidth + barSpacing);
|
||||
int rectWidth = (cheapBlockEnd2 - cheapBlockStart2 + 1) * (barwidth + barSpacing);
|
||||
spr.fillRect(rectX, baselineY + 1, rectWidth, xAxisHeight - 1, TFT_LIGHTGREY);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const JsonObject &obj = doc[i];
|
||||
const time_t item_time = obj["time"];
|
||||
struct tm item_timeinfo;
|
||||
localtime_r(&item_time, &item_timeinfo);
|
||||
// Get data from avgData array (already trimmed to visible range)
|
||||
const time_t item_time = avgData[i].time;
|
||||
const int item_hour = avgData[i].hour;
|
||||
const int item_minute = avgData[i].minute;
|
||||
const double price = avgData[i].price;
|
||||
|
||||
if (tariffArray.size() == 24) {
|
||||
tarifkwh = tariffArray[item_timeinfo.tm_hour].as<double>();
|
||||
} else {
|
||||
tarifkwh = cfgobj["tariffkwh"].as<double>();
|
||||
}
|
||||
|
||||
const double price = (obj["price"].as<double>() / 10 + tarifkwh) * (1 + tariftax / 100) / units;
|
||||
|
||||
uint16_t barcolor = getPercentileColor(prices, n, price, imageParams.hwdata);
|
||||
uint16_t barcolor = getPercentileColor(prices, numAveragedSamples, price, imageParams.hwdata);
|
||||
uint16_t thisbarh = mapDouble(price, minPrice, maxPrice, 0, loc["bars"][2].as<int>());
|
||||
spr.fillRect(barX + i * barwidth, spr.height() - barBottom - thisbarh, barwidth - 1, thisbarh, barcolor);
|
||||
if (i % 2 == 0 && loc["time"][0]) {
|
||||
drawString(spr, String(item_timeinfo.tm_hour), barX + i * barwidth + barwidth / 3 + 1, spr.height() - barBottom + 3, loc["time"][0], TC_DATUM, TFT_BLACK);
|
||||
spr.fillRect(barX + i * (barwidth + barSpacing), spr.height() - barBottom - thisbarh, barwidth, thisbarh, barcolor);
|
||||
|
||||
// Draw tickmarks and labels at appropriate intervals
|
||||
int labelX = barX + i * (barwidth + barSpacing) + barwidth / 2;
|
||||
int tickY = spr.height() - barBottom;
|
||||
|
||||
// Check if this bar represents the start of an hour (minute = 0)
|
||||
bool isHourStart = (item_minute == 0);
|
||||
// Check if this bar represents a half-hour (minute = 30)
|
||||
bool isHalfHour = (item_minute == 30);
|
||||
|
||||
// Skip tick marks on displays with limited vertical space (height < 150px)
|
||||
// to prevent labels from being pushed off the bottom edge
|
||||
bool skipTicks = (spr.height() < 150);
|
||||
|
||||
// Every 2 hours: Black tick (4px) + label
|
||||
if (i % labelInterval == 0 && loc["time"][0]) {
|
||||
if (!skipTicks) {
|
||||
spr.drawLine(labelX, tickY, labelX, tickY + 4, TFT_BLACK);
|
||||
}
|
||||
drawString(spr, String(item_hour), labelX, tickY + (skipTicks ? 3 : 6), loc["time"][0], TC_DATUM, TFT_BLACK);
|
||||
}
|
||||
// Every hour (not already labeled): Dark grey tick (3px)
|
||||
else if (isHourStart && !skipTicks) {
|
||||
spr.drawLine(labelX, tickY, labelX, tickY + 3, getColor("darkgray"));
|
||||
}
|
||||
// Every half hour: Very short dark grey tick (1px)
|
||||
else if (isHalfHour && !skipTicks) {
|
||||
spr.drawPixel(labelX, tickY + 1, getColor("darkgray"));
|
||||
}
|
||||
|
||||
if (now - item_time < 3600 && std::isnan(pricenow) && showcurrent) {
|
||||
spr.fillRect(barX + i * barwidth + (barwidth > 6 ? 3 : 1), 5 + arrowY, (barwidth > 6 ? barwidth - 6 : barwidth - 2), 10, imageParams.highlightColor);
|
||||
spr.fillTriangle(barX + i * barwidth, 15 + arrowY,
|
||||
barX + i * barwidth + barwidth - 1, 15 + arrowY,
|
||||
barX + i * barwidth + (barwidth - 1) / 2, 15 + barwidth + arrowY, imageParams.highlightColor);
|
||||
spr.drawLine(barX + i * barwidth + (barwidth - 1) / 2, 20 + barwidth + arrowY, barX + i * barwidth + (barwidth - 1) / 2, spr.height(), TFT_BLACK);
|
||||
pricenow = price;
|
||||
// Store pointer position for drawing after all bars
|
||||
// Use RAW current price (stored before averaging) for accurate display
|
||||
// Find the bar that contains "now" by checking if now falls within this bar's time interval
|
||||
// Important: Use targetIntervalMinutes (the actual bar interval after averaging/display)
|
||||
if (std::isnan(pricenow) && showcurrent) {
|
||||
time_t barEndTime = item_time + (targetIntervalMinutes * 60); // End of this bar's time range
|
||||
if (now >= item_time && now < barEndTime) {
|
||||
pointerX = barX + i * (barwidth + barSpacing) + barwidth / 2; // Center of the bar
|
||||
pricenow = rawCurrentPrice; // Use raw price, not averaged
|
||||
currentHour = rawCurrentHour; // Use native interval's hour (not averaged block)
|
||||
currentMinute = rawCurrentMinute; // Use native interval's minute (not averaged block)
|
||||
currentBarHeight = thisbarh; // Store bar height for line drawing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw current hour pointer AFTER all bars (so it won't be chopped off)
|
||||
// Use constant narrow stem with wider arrow head for a proper arrow shape
|
||||
if (pointerX >= 0 && showcurrent) {
|
||||
// Arrow stem (narrow rectangle)
|
||||
spr.fillRect(pointerX - STEM_WIDTH / 2, 5 + arrowY, STEM_WIDTH, STEM_HEIGHT, imageParams.highlightColor);
|
||||
// Arrow head (wider triangle)
|
||||
spr.fillTriangle(pointerX - ARROW_WIDTH / 2, 15 + arrowY,
|
||||
pointerX + ARROW_WIDTH / 2, 15 + arrowY,
|
||||
pointerX, 15 + arrowY + ARROW_HEIGHT, imageParams.highlightColor);
|
||||
|
||||
// Vertical line: dashed from arrow to bar, solid from baseline to bottom
|
||||
int lineStartY = 21 + arrowY + GAP_AFTER_ARROW;
|
||||
int baselineY = spr.height() - barBottom;
|
||||
int barTopY = baselineY - currentBarHeight;
|
||||
|
||||
// Draw DASHED line from arrow to top of bar (with gap) - for visual clarity
|
||||
for (int y = lineStartY; y < barTopY - 2; y += DASH_LENGTH + GAP_LENGTH) {
|
||||
int segmentEnd = min(y + DASH_LENGTH, barTopY - 2);
|
||||
spr.drawLine(pointerX, y, pointerX, segmentEnd, TFT_BLACK);
|
||||
}
|
||||
|
||||
// Draw solid line from baseline to bottom (for time indication in X-axis area)
|
||||
spr.drawLine(pointerX, baselineY, pointerX, spr.height(), TFT_BLACK);
|
||||
}
|
||||
|
||||
if (showcurrent) {
|
||||
if (barwidth < 5) {
|
||||
drawString(spr, String(timeinfo.tm_hour) + ":00", spr.width() / 2, 5, "calibrib16.vlw", TC_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(currentHour) + ":" + (currentMinute < 10 ? "0" : "") + String(currentMinute), spr.width() / 2, 5, "calibrib16.vlw", TC_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(pricenow) + "/kWh", spr.width() / 2, 25, loc["head"][0], TC_DATUM, TFT_BLACK, 30);
|
||||
} else {
|
||||
drawString(spr, String(timeinfo.tm_hour) + ":00", barX, 5, loc["head"][0], TL_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(currentHour) + ":" + (currentMinute < 10 ? "0" : "") + String(currentMinute), barX, 5, loc["head"][0], TL_DATUM, TFT_BLACK, 30);
|
||||
drawString(spr, String(pricenow) + "/kWh", spr.width() - barX, 5, loc["head"][0], TR_DATUM, TFT_BLACK, 30);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,23 +365,59 @@ uint8_t *g5Compress(uint16_t width, uint16_t height, uint8_t *buffer, uint16_t b
|
||||
}
|
||||
#endif
|
||||
|
||||
void doTimestamp(TFT_eSprite *spr) {
|
||||
// The "ts_option" is a bitmapped variable with a default value of 1
|
||||
// which is black on white, long format @ bottom right.
|
||||
//
|
||||
// b2, b1, b0:
|
||||
// 0 - no timestamp
|
||||
// 1 - bottom right
|
||||
// 2 - top right
|
||||
// 3 - bottom left
|
||||
// 4 - top left
|
||||
// 5 -> 7 reserved
|
||||
// b3:
|
||||
// 0 - long format (year-month-day hr:min)
|
||||
// 1 - short format (month-day hr:min)
|
||||
// b4:
|
||||
// 0 - black on white
|
||||
// 1 - white on black
|
||||
// b5 -> b7: reserved
|
||||
//
|
||||
void doTimestamp(TFT_eSprite *spr, uint8_t ts_option) {
|
||||
time_t now = time(nullptr);
|
||||
struct tm *timeinfo = localtime(&now);
|
||||
char buffer[20];
|
||||
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M", timeinfo);
|
||||
strftime(buffer, sizeof(buffer),
|
||||
(ts_option & 0x8) ? "%m-%d %H:%M" : "%Y-%m-%d %H:%M",timeinfo);
|
||||
int ts_chars = strlen(buffer);
|
||||
|
||||
// spr->drawRect(spr->width() - 16 * 6 - 4, spr->height() - 10 - 2, 16 * 6 + 3, 11, TFT_BLACK);
|
||||
spr->drawRect(spr->width() - 16 * 6 - 3, spr->height() - 10 - 1, 16 * 6 + 1, 9, TFT_WHITE);
|
||||
spr->setTextColor(TFT_BLACK, TFT_WHITE);
|
||||
spr->setCursor(spr->width() - 16 * 6 - 2, spr->height() - 10, 1);
|
||||
uint16_t char_color;
|
||||
uint16_t bg_color;
|
||||
|
||||
if(ts_option & 0x10) {
|
||||
char_color = TFT_WHITE;
|
||||
bg_color= TFT_BLACK;
|
||||
}
|
||||
else {
|
||||
char_color = TFT_BLACK;
|
||||
bg_color = TFT_WHITE;
|
||||
}
|
||||
|
||||
ts_option = (ts_option & 0x3) - 1;
|
||||
|
||||
int32_t ts_x = (ts_option & 2) ? 1 : spr->width() - ts_chars * 6 - 2;
|
||||
int32_t ts_y = (ts_option & 1) ? 1 : spr->height() - 10;
|
||||
|
||||
spr->drawRect(ts_x - 1, ts_y - 1, ts_chars * 6 + 1, 9, bg_color);
|
||||
spr->setTextColor(char_color, bg_color);
|
||||
spr->setCursor(ts_x,ts_y);
|
||||
spr->print(buffer);
|
||||
}
|
||||
|
||||
void spr2buffer(TFT_eSprite &spr, String &fileout, imgParam &imageParams) {
|
||||
long t = millis();
|
||||
|
||||
if (config.showtimestamp) doTimestamp(&spr);
|
||||
if (imageParams.ts_option) doTimestamp(&spr,imageParams.ts_option);
|
||||
#ifdef HAS_TFT
|
||||
extern uint8_t YellowSense;
|
||||
if (fileout == "direct") {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <Arduino.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <system.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "commstructs.h"
|
||||
#include "contentmanager.h"
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "tag_db.h"
|
||||
#include "udp.h"
|
||||
#include "wifimanager.h"
|
||||
#include <sys/time.h>
|
||||
|
||||
#ifdef HAS_EXT_FLASHER
|
||||
#include "webflasher.h"
|
||||
@@ -648,6 +649,28 @@ void init_web() {
|
||||
request->send(200, "text/plain", "Ok, saved");
|
||||
});
|
||||
|
||||
// Allow external time sync (e.g., from Home Assistant) without Internet
|
||||
// Usage: POST /set_time with form field 'epoch' (UNIX time seconds)
|
||||
server.on("/set_time", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
if (request->hasParam("epoch", true)) {
|
||||
time_t epoch = static_cast<time_t>(request->getParam("epoch", true)->value().toInt());
|
||||
if (epoch > 1600000000) { // basic sanity check (~2020-09-13)
|
||||
struct timeval tv;
|
||||
tv.tv_sec = epoch;
|
||||
tv.tv_usec = 0;
|
||||
settimeofday(&tv, nullptr);
|
||||
logLine("Time set via /set_time");
|
||||
wsSendSysteminfo();
|
||||
request->send(200, "text/plain", "ok");
|
||||
return;
|
||||
} else {
|
||||
request->send(400, "text/plain", "invalid epoch");
|
||||
return;
|
||||
}
|
||||
}
|
||||
request->send(400, "text/plain", "missing 'epoch'");
|
||||
});
|
||||
|
||||
server.on("/set_var", HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
if (request->hasParam("key", true) && request->hasParam("val", true)) {
|
||||
std::string key = request->getParam("key", true)->value().c_str();
|
||||
|
||||
@@ -451,6 +451,29 @@
|
||||
"0": "No",
|
||||
"1": "-Yes"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "interval",
|
||||
"name": "Display interval",
|
||||
"desc": "Data averaging interval for better readability on smaller displays",
|
||||
"type": "select",
|
||||
"options": {
|
||||
"0": "-Native (15-min or hourly)",
|
||||
"30": "30 minutes",
|
||||
"60": "1 hour"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "cheapblock",
|
||||
"name": "Cheap block hours",
|
||||
"desc": "Number of hours for cheapest consecutive block (e.g., 3 for dishwasher, 2.5 for 2h30m). Set to 0 to disable. Shows up to 2 non-overlapping blocks.",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"key": "updatefreq",
|
||||
"name": "Update frequency",
|
||||
"desc": "Tag refresh interval in minutes (e.g., 15, 30, 60). Default is 15 minutes to match 15-minute data intervals. Higher values save battery.",
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1084,7 +1084,7 @@ function contentselected() {
|
||||
fetch('edit?list=%2F&recursive=1')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let files = data.filter(item => item.type === "file" && item.name.endsWith(".jpg"));
|
||||
let files = data.filter(item => item.type === "file" && (item.name.toLowerCase().endsWith(".jpg") || item.name.toLowerCase().endsWith(".jpeg")));
|
||||
if (element.type == 'binfile') files = data.filter(item => item.type === "file" && item.name.endsWith(".bin"));
|
||||
if (element.type == 'jsonfile') files = data.filter(item => item.type === "file" && item.name.endsWith(".json"));
|
||||
const optionElement = document.createElement("option");
|
||||
|
||||
@@ -215,7 +215,7 @@ function startPainter(mac, width, height, tagtype) {
|
||||
canvas.addEventListener('touchend', handleTouchEnd);
|
||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: true });
|
||||
|
||||
var sizes = [10,11,12,13,14,16,18,20,24,28,32,36,40,48,56,64,72,84];
|
||||
var sizes = [10,11,12,13,14,16,18,20,24,28,32,36,40,48,56,64,72,84,96,108,120,144,168,192,256,320,384,480,512];
|
||||
|
||||
const fontSelect = document.createElement('select');
|
||||
fontSelect.id = 'font-select';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Google Calendar
|
||||
|
||||
To use the 'clandar' content type, you need a helper script, using Google Apps Script. To use Google Apps Script to get all events for the next day and return them via JSON in a web app, you can follow these steps:
|
||||
To use the 'calendar' content type, you need a helper script, using Google Apps Script. To use Google Apps Script to get all events for the next day and return them via JSON in a web app, you can follow these steps:
|
||||
|
||||
* Create a new Google Apps Script project by going to [https://script.google.com](https://script.google.com) and clicking on "New project".
|
||||
* Paste the content of the calendar.js file into the editor
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 4,
|
||||
"version": 5,
|
||||
"name": "M2 2.9\"",
|
||||
"width": 296,
|
||||
"height": 128,
|
||||
@@ -43,7 +43,8 @@
|
||||
"day": [ 30, 18, "fonts/twcondensed20", 41, 108 ],
|
||||
"icon": [ 30, 55, 30 ],
|
||||
"wind": [ 18, 26 ],
|
||||
"line": [ 20, 128 ]
|
||||
"line": [ 20, 128 ],
|
||||
"ts_option": 10
|
||||
},
|
||||
"9": {
|
||||
"title": [ 2, 0, "bahnschrift20.vlw", 25 ],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "M3 1.6\" BWRY",
|
||||
"width": 168,
|
||||
"height": 168,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 2,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 7,5\"",
|
||||
"width": 800,
|
||||
"height": 480,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 2,00\"",
|
||||
"width": 152,
|
||||
"height": 200,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 3,5\"",
|
||||
"width": 384,
|
||||
"height": 184,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 2,
|
||||
"version": 3,
|
||||
"name": "HS BWRY 2,9\"",
|
||||
"width": 296,
|
||||
"height": 128,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"zlib_compression": "27",
|
||||
"options": [ "button", "led" ],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "HS BWRY 2,60\"",
|
||||
"width": 296,
|
||||
"height": 152,
|
||||
@@ -14,7 +14,8 @@
|
||||
"perceptual": {
|
||||
"white": [ 255, 255, 255 ],
|
||||
"black": [ 0, 0, 0 ],
|
||||
"yellow": [ 200, 200, 0 ]
|
||||
"red": [ 255, 0, 0 ],
|
||||
"yellow": [ 255, 255, 0 ]
|
||||
},
|
||||
"shortlut": 0,
|
||||
"zlib_compression": "27",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "TFT 320x172",
|
||||
"width": 320,
|
||||
"height": 172,
|
||||
@@ -23,7 +23,8 @@
|
||||
{ "text": [ 10, 95, "Channel:", "bahnschrift20", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 120, 95, "{ap_ch}", "bahnschrift20", 0, 0, 0, "1" ] },
|
||||
{ "text": [ 10, 120, "Tag count:", "bahnschrift20", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 120, 120, "{ap_tagcount}", "bahnschrift20", 0, 0, 0, "1" ] }
|
||||
{ "text": [ 120, 120, "{ap_tagcount}", "bahnschrift20", 0, 0, 0, "1" ] },
|
||||
{ "ts_option": 17}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1,
|
||||
"version": 2,
|
||||
"name": "TFT 160x80",
|
||||
"width": 160,
|
||||
"height": 80,
|
||||
@@ -22,7 +22,8 @@
|
||||
{ "text": [ 1, 45, "Ch:", "REFSAN12.vlw", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 45, 45, "{ap_ch}", "REFSAN12.vlw", 0, 0, 0, "1" ] },
|
||||
{ "text": [ 1, 62, "Tags:", "REFSAN12.vlw", "#888888", 0, 0, 1 ] },
|
||||
{ "text": [ 45, 62, "{ap_tagcount}", "REFSAN12.vlw", 0, 0, 0, "1" ] }
|
||||
{ "text": [ 45, 62, "{ap_tagcount}", "REFSAN12.vlw", 0, 0, 0, "1" ] },
|
||||
{ "ts_option": 25}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user