Public Access
Stopwatch counts in real time from a start date, kept over a restart
The stopwatch counted with millis(), so it started at 0 after every restart and would overflow after 49 days. It now keeps the real start time (UTC ms) and the time counted before a pause, stored in NVS (system/stopwatch_store) on every change, so it keeps counting while the display is off or restarting. Stop pauses, Start continues, and the Stopwatch tab can set the start to a date and time (local, summer time by mktime) to count from, e.g. days since an event. Start, stop and set need the synced time. New page formats: 12d 4:05:09 and 3w 2d. Version 2.19.0, tested on the display over OTA (set start, future and invalid dates refused, pause, restart). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f5e77c7eca
commit
0166d0ee46
20 files changed
+278
-67
No files matched your search
+2
-1
@@ -29,7 +29,7 @@ debug output. Wiring is described in the [documentation](../../docs/wiring.md).
|
||||
| `logic/life.*` | Conway's Game of Life: the board with its edges and cycle check (`LifeBoard`), timing and restarts (`LifeRunner`) |
|
||||
| `logic/page_schedule.*` | Schedule blocks and trigger rules: the structs, checks, rule conditions |
|
||||
| `logic/page_selector.*` | Which page is shown now: rules, a manual pick, the rotating schedule (`PageSelector`) |
|
||||
| `logic/stopwatch.h` | The stopwatch of the dynamic pages |
|
||||
| `logic/stopwatch.h` | The stopwatch of the dynamic pages (real time, stored by `system/stopwatch_store`) |
|
||||
| `logic/power_timer.*` | Countdown to switching the display off |
|
||||
| `logic/status_led.h` | Blink pattern of the built-in LED |
|
||||
| `logic/boot_policy.*` | How to start after a reset: normal, OTA-only window, safe mode |
|
||||
@@ -39,6 +39,7 @@ debug output. Wiring is described in the [documentation](../../docs/wiring.md).
|
||||
| `system/system_store.*` | Settings in NVS: WiFi, recovery reason, firmware slot and version, power-on count |
|
||||
| `system/settings_store.*` | The web UI settings in NVS |
|
||||
| `system/schedule_store.*` | The page schedule and rules in NVS |
|
||||
| `system/stopwatch_store.*` | The stopwatch in NVS (keeps counting over a restart) |
|
||||
| **`display/`** | **What is drawn** |
|
||||
| `display/frames.*` | The screens: dynamic page, "No WiFi", page bitmap, live image, off image (`FlipdotFrames`) |
|
||||
| `display/bitmap_store.*` | Built-in bitmaps and the uploaded ones on LittleFS |
|
||||
|
||||
+44
-3
@@ -15,6 +15,7 @@
|
||||
#include "net/ha_api.h"
|
||||
#include "system/schedule_store.h"
|
||||
#include "system/settings_store.h"
|
||||
#include "system/stopwatch_store.h"
|
||||
|
||||
static void copyName(char *out, const char *name)
|
||||
{
|
||||
@@ -119,6 +120,7 @@ void appLoadSettings()
|
||||
{
|
||||
loadSettings(settings);
|
||||
loadSchedule(page_schedule);
|
||||
loadStopwatch(stopwatch);
|
||||
selector.setSchedule(&page_schedule);
|
||||
selector.setChosen(settings.page);
|
||||
haApiWatchRules(&page_schedule);
|
||||
@@ -486,15 +488,54 @@ const char *appDeleteFont(const char *name)
|
||||
return deleteFont(name);
|
||||
}
|
||||
|
||||
void appStopwatch(StopwatchAction action)
|
||||
static const char *NO_TIME = "The time is not synced yet";
|
||||
|
||||
const char *appStopwatch(StopwatchAction action)
|
||||
{
|
||||
// It counts in real time: before the sync, the clock still says 1970
|
||||
if (!time_known && action != StopwatchAction::Reset)
|
||||
return NO_TIME;
|
||||
int64_t now = epochMs();
|
||||
if (action == StopwatchAction::Start)
|
||||
stopwatch.start(millis());
|
||||
stopwatch.start(now);
|
||||
else if (action == StopwatchAction::Stop)
|
||||
stopwatch.stop(millis());
|
||||
stopwatch.stop(now);
|
||||
else
|
||||
stopwatch.reset();
|
||||
saveStopwatch(stopwatch);
|
||||
controller.pageChanged();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *appStopwatchSetStart(const char *local)
|
||||
{
|
||||
uint32_t minutes;
|
||||
if (!parseTarget(local, minutes))
|
||||
return "Invalid start: use YYYY-MM-DDTHH:MM";
|
||||
if (!time_known)
|
||||
return NO_TIME;
|
||||
struct tm t = {};
|
||||
sscanf(local, "%4d-%2d-%2dT%2d:%2d", &t.tm_year, &t.tm_mon, &t.tm_mday, &t.tm_hour, &t.tm_min);
|
||||
t.tm_year -= 1900;
|
||||
t.tm_mon -= 1;
|
||||
t.tm_isdst = -1; // the time zone decides (summer time at that date)
|
||||
time_t start = mktime(&t);
|
||||
if (start == (time_t)-1 || !stopwatch.setStart((int64_t)start * 1000, epochMs()))
|
||||
return "The start is in the future";
|
||||
saveStopwatch(stopwatch);
|
||||
controller.pageChanged();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool appStopwatchStart(char *out)
|
||||
{
|
||||
if (!time_known || !stopwatch.running())
|
||||
return false;
|
||||
time_t start = (time_t)((epochMs() - (int64_t)stopwatch.elapsed(epochMs())) / 1000);
|
||||
struct tm t;
|
||||
localtime_r(&start, &t);
|
||||
strftime(out, 20, "%Y-%m-%dT%H:%M:%S", &t);
|
||||
return true;
|
||||
}
|
||||
|
||||
void appShowLive(const uint8_t *bitmap)
|
||||
|
||||
+7
-1
@@ -77,7 +77,13 @@ enum class StopwatchAction
|
||||
Stop,
|
||||
Reset,
|
||||
};
|
||||
void appStopwatch(StopwatchAction action);
|
||||
// nullptr or what went wrong (start and stop need the synced time)
|
||||
const char *appStopwatch(StopwatchAction action);
|
||||
// Counts from this local date and time on ("YYYY-MM-DDTHH:MM", in the past), running
|
||||
const char *appStopwatchSetStart(const char *local);
|
||||
// The local time it counts from when it is running ("YYYY-MM-DDTHH:MM:SS"; out holds 20
|
||||
// characters); false when it is stopped or the time is not synced
|
||||
bool appStopwatchStart(char *out);
|
||||
|
||||
// Live mode of the web UI
|
||||
void appShowLive(const uint8_t *bitmap);
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ const uint32_t SETUP_AP_DELAY = 20000; // open the AP when WiFi fails thi
|
||||
const uint32_t WIFI_CONNECT_TIMEOUT = 30000;
|
||||
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
||||
|
||||
#define FIRMWARE_VERSION "2.18.2"
|
||||
#define FIRMWARE_VERSION "2.19.0"
|
||||
|
||||
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
|
||||
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "display/page_resources.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/font_store.h"
|
||||
@@ -32,9 +33,16 @@ const char *StoredPageResources::haValue(const PageElement &element)
|
||||
return haApiValue(element.entity, element.attribute);
|
||||
}
|
||||
|
||||
uint32_t StoredPageResources::stopwatchMs()
|
||||
uint64_t StoredPageResources::stopwatchMs()
|
||||
{
|
||||
return _stopwatch.elapsed(millis());
|
||||
return _stopwatch.elapsed(epochMs());
|
||||
}
|
||||
|
||||
int64_t epochMs()
|
||||
{
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, nullptr);
|
||||
return (int64_t)tv.tv_sec * 1000 + tv.tv_usec / 1000;
|
||||
}
|
||||
|
||||
bool StoredPageResources::stopwatchRunning()
|
||||
|
||||
@@ -14,7 +14,7 @@ public:
|
||||
bool background(const char *name, uint8_t *bitmap) override;
|
||||
Font font(const char *name) override;
|
||||
const char *haValue(const PageElement &element) override;
|
||||
uint32_t stopwatchMs() override;
|
||||
uint64_t stopwatchMs() override;
|
||||
bool stopwatchRunning() override;
|
||||
const LifeRunner *life(const PageElement &element) override;
|
||||
|
||||
@@ -23,4 +23,6 @@ private:
|
||||
};
|
||||
|
||||
extern Stopwatch stopwatch;
|
||||
// The real time in ms since 1970 (UTC), for the stopwatch; only right once the time is synced
|
||||
int64_t epochMs();
|
||||
extern StoredPageResources pageResources;
|
||||
@@ -778,13 +778,23 @@ static void formatCountdown(const PageElement &e, const LocalTime &t, char *out,
|
||||
snprintf(out, size, "%s%s", e.text, value);
|
||||
}
|
||||
|
||||
static void formatStopwatch(const PageElement &e, uint32_t ms, char *out, size_t size)
|
||||
static void formatStopwatch(const PageElement &e, uint64_t ms, char *out, size_t size)
|
||||
{
|
||||
unsigned long s = ms / 1000, h = s / 3600, m = s % 3600 / 60, sec = s % 60;
|
||||
unsigned long s = (unsigned long)(ms / 1000), h = s / 3600, m = s % 3600 / 60, sec = s % 60;
|
||||
unsigned long days = h / 24;
|
||||
char value[24];
|
||||
if (e.format == STOPWATCH_H_MM)
|
||||
if (e.format == STOPWATCH_W_D)
|
||||
{
|
||||
if (days >= 7)
|
||||
snprintf(value, sizeof(value), "%luw %lud", days / 7, days % 7);
|
||||
else
|
||||
snprintf(value, sizeof(value), "%lud", days);
|
||||
}
|
||||
else if (e.format == STOPWATCH_D_H_MM_SS && days)
|
||||
snprintf(value, sizeof(value), "%lud %lu:%02lu:%02lu", days, h % 24, m, sec);
|
||||
else if (e.format == STOPWATCH_H_MM)
|
||||
snprintf(value, sizeof(value), "%lu:%02lu", h, m);
|
||||
else if (e.format == STOPWATCH_H_MM_SS || h)
|
||||
else if (e.format == STOPWATCH_H_MM_SS || e.format == STOPWATCH_D_H_MM_SS || h)
|
||||
snprintf(value, sizeof(value), "%lu:%02lu:%02lu", h, m, sec);
|
||||
else
|
||||
snprintf(value, sizeof(value), "%lu:%02lu", m, sec);
|
||||
@@ -860,7 +870,7 @@ bool elementShowsSeconds(const PageElement &e, const LocalTime &t, bool stopwatc
|
||||
case ElementType::Clock:
|
||||
return e.format == CLOCK_HH_MM_SS || e.format == CLOCK_H_MM_SS;
|
||||
case ElementType::Stopwatch:
|
||||
return stopwatch_running && e.format != STOPWATCH_H_MM;
|
||||
return stopwatch_running && e.format != STOPWATCH_H_MM && e.format != STOPWATCH_W_D;
|
||||
case ElementType::Countdown:
|
||||
{
|
||||
int64_t left = secondsLeft(e, t);
|
||||
|
||||
@@ -97,7 +97,9 @@ const uint8_t COUNTDOWN_FORMAT_COUNT = 4;
|
||||
const uint8_t STOPWATCH_M_SS = 0; // 4:05, from an hour on 1:04:05
|
||||
const uint8_t STOPWATCH_H_MM = 1; // 0:04 (hours and minutes)
|
||||
const uint8_t STOPWATCH_H_MM_SS = 2; // 0:04:05
|
||||
const uint8_t STOPWATCH_FORMAT_COUNT = 3;
|
||||
const uint8_t STOPWATCH_D_H_MM_SS = 3; // 12d 4:05:09, below a day 4:05:09
|
||||
const uint8_t STOPWATCH_W_D = 4; // 3w 2d, without the weeks below a week
|
||||
const uint8_t STOPWATCH_FORMAT_COUNT = 5;
|
||||
|
||||
// Progress bar formats
|
||||
const uint8_t BAR_PLAIN = 0; // only the bar
|
||||
@@ -238,7 +240,7 @@ const PageSlot *activeSlot(const DynamicPage &page, int minute_of_day);
|
||||
struct ElementValues
|
||||
{
|
||||
const char *ha_value = nullptr; // nullptr: not known yet, shown as "--"
|
||||
uint32_t stopwatch_ms = 0;
|
||||
uint64_t stopwatch_ms = 0;
|
||||
};
|
||||
|
||||
// The text of an element (not for images). out_size must be at least 2.
|
||||
|
||||
@@ -270,7 +270,7 @@ uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now
|
||||
|
||||
uint32_t next = 0;
|
||||
bool running = resources.stopwatchRunning();
|
||||
uint32_t stopwatch_ms = resources.stopwatchMs();
|
||||
uint64_t stopwatch_ms = resources.stopwatchMs();
|
||||
for (int i = 0; i < page.element_count && i < PAGE_ELEMENTS; i++)
|
||||
{
|
||||
const PageElement &e = page.elements[i];
|
||||
@@ -292,10 +292,10 @@ uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now
|
||||
next = sooner(next, drawTextElement(e, text, now_ms, resources, bitmap, box));
|
||||
|
||||
if (elementShowsSeconds(e, time, running)) // the stopwatch's seconds count from its start
|
||||
next = sooner(next, e.type == ElementType::Stopwatch ? 1000 - stopwatch_ms % 1000
|
||||
next = sooner(next, e.type == ElementType::Stopwatch ? 1000 - (uint32_t)(stopwatch_ms % 1000)
|
||||
: 1000 - (time.millisecond % 1000));
|
||||
else if (e.type == ElementType::Stopwatch && running) // H:MM: the next stopwatch minute
|
||||
next = sooner(next, 60000 - stopwatch_ms % 60000);
|
||||
else if (e.type == ElementType::Stopwatch && running) // H:MM, weeks/days: the next stopwatch minute
|
||||
next = sooner(next, 60000 - (uint32_t)(stopwatch_ms % 60000));
|
||||
}
|
||||
if (boxes)
|
||||
boxes[i] = box;
|
||||
|
||||
@@ -20,7 +20,7 @@ public:
|
||||
virtual Font font(const char *name) = 0;
|
||||
// The current value of a Home Assistant element; nullptr when not known
|
||||
virtual const char *haValue(const PageElement &element) = 0;
|
||||
virtual uint32_t stopwatchMs() { return 0; }
|
||||
virtual uint64_t stopwatchMs() { return 0; }
|
||||
virtual bool stopwatchRunning() { return false; }
|
||||
// The running Game of Life of the shown page when this element starts like it
|
||||
// (sameLifeStart); nullptr otherwise (the start is drawn then)
|
||||
|
||||
+42
-17
@@ -3,35 +3,60 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// The stopwatch of the dynamic pages, started and stopped in the web UI and Home Assistant.
|
||||
// Times are millis(). Pure logic, tested in test/test_esp32.
|
||||
// Times are the real time in ms since 1970 (UTC), so it counts from a date, keeps counting
|
||||
// while the display is off or restarting (system/stopwatch_store keeps it in NVS) and can
|
||||
// run for years. Pure logic, tested in test/test_esp32.
|
||||
class Stopwatch
|
||||
{
|
||||
public:
|
||||
void start(uint32_t now)
|
||||
// The state as it is stored
|
||||
struct State
|
||||
{
|
||||
if (_running)
|
||||
bool running = false;
|
||||
int64_t started = 0; // when it was started (again)
|
||||
int64_t before = 0; // ms counted before the last start
|
||||
};
|
||||
|
||||
void start(int64_t now)
|
||||
{
|
||||
if (_state.running)
|
||||
return;
|
||||
_running = true;
|
||||
_started = now;
|
||||
_state.running = true;
|
||||
_state.started = now;
|
||||
}
|
||||
void stop(uint32_t now)
|
||||
void stop(int64_t now)
|
||||
{
|
||||
if (!_running)
|
||||
if (!_state.running)
|
||||
return;
|
||||
_before += now - _started;
|
||||
_running = false;
|
||||
_state.before = elapsed(now);
|
||||
_state.running = false;
|
||||
}
|
||||
// Back to 0, stopped
|
||||
void reset()
|
||||
void reset() { _state = State(); }
|
||||
// Counts from this moment (in the past) on, running; false (nothing changed) when it is
|
||||
// in the future
|
||||
bool setStart(int64_t start, int64_t now)
|
||||
{
|
||||
_running = false;
|
||||
_before = 0;
|
||||
if (start > now)
|
||||
return false;
|
||||
_state.running = true;
|
||||
_state.started = start;
|
||||
_state.before = 0;
|
||||
return true;
|
||||
}
|
||||
bool running() const { return _running; }
|
||||
uint32_t elapsed(uint32_t now) const { return _before + (_running ? now - _started : 0); }
|
||||
bool running() const { return _state.running; }
|
||||
// Never negative (the clock set back)
|
||||
uint64_t elapsed(int64_t now) const
|
||||
{
|
||||
int64_t ms = _state.before;
|
||||
if (_state.running && now > _state.started)
|
||||
ms += now - _state.started;
|
||||
return ms > 0 ? (uint64_t)ms : 0;
|
||||
}
|
||||
|
||||
const State &state() const { return _state; }
|
||||
void restore(const State &state) { _state = state; }
|
||||
|
||||
private:
|
||||
bool _running = false;
|
||||
uint32_t _started = 0;
|
||||
uint32_t _before = 0; // ms counted before the last start
|
||||
State _state;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "system/stopwatch_store.h"
|
||||
|
||||
#include <Preferences.h>
|
||||
|
||||
static const char *NAMESPACE = "flipdot";
|
||||
static const char *KEY = "stopwatch";
|
||||
// Increase when Stopwatch::State changes; records of other versions are ignored
|
||||
static const uint16_t VERSION = 1;
|
||||
|
||||
struct StoredStopwatch
|
||||
{
|
||||
uint16_t version;
|
||||
uint16_t size;
|
||||
Stopwatch::State state;
|
||||
};
|
||||
|
||||
void loadStopwatch(Stopwatch &stopwatch)
|
||||
{
|
||||
stopwatch.reset();
|
||||
StoredStopwatch stored;
|
||||
Preferences prefs;
|
||||
prefs.begin(NAMESPACE, true);
|
||||
bool ok = prefs.isKey(KEY) && prefs.getBytesLength(KEY) == sizeof(stored) &&
|
||||
prefs.getBytes(KEY, &stored, sizeof(stored)) == sizeof(stored);
|
||||
prefs.end();
|
||||
if (ok && stored.version == VERSION && stored.size == sizeof(Stopwatch::State) && stored.state.before >= 0 &&
|
||||
stored.state.started >= 0)
|
||||
stopwatch.restore(stored.state);
|
||||
}
|
||||
|
||||
void saveStopwatch(const Stopwatch &stopwatch)
|
||||
{
|
||||
StoredStopwatch stored;
|
||||
stored.version = VERSION;
|
||||
stored.size = sizeof(Stopwatch::State);
|
||||
stored.state = stopwatch.state();
|
||||
Preferences prefs;
|
||||
prefs.begin(NAMESPACE, false);
|
||||
prefs.putBytes(KEY, &stored, sizeof(stored));
|
||||
prefs.end();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "logic/stopwatch.h"
|
||||
|
||||
// The stopwatch in NVS (namespace "flipdot", key "stopwatch"), so it keeps counting over a
|
||||
// restart. A missing, outdated or invalid record gives a stopwatch at 0.
|
||||
void loadStopwatch(Stopwatch &stopwatch);
|
||||
void saveStopwatch(const Stopwatch &stopwatch);
|
||||
@@ -108,9 +108,11 @@ details{margin-top:10px}summary{cursor:pointer;color:var(--dim);font-size:13px;m
|
||||
|
||||
<section id="t-stopwatch"><div class="card"><h2>Stopwatch</h2>
|
||||
<div class="row"><b id="swtime" style="font-size:32px;font-variant-numeric:tabular-nums">0:00</b></div>
|
||||
<div class="row"><span id="swsince" class="hint"></span></div>
|
||||
<div class="row"><button class="b" onclick="stopwatch('start')">Start</button><button class="s" onclick="stopwatch('stop')">Stop</button>
|
||||
<button class="s" onclick="stopwatch('reset')">Reset</button></div>
|
||||
<p class="hint">Shown by pages with a stopwatch element (e.g. the Stopwatch page). Also in Home Assistant: the Stopwatch switch and the Stopwatch reset button.</p></div></section>
|
||||
<div class="row"><label>Counting since <input type="datetime-local" id="swstart"></label><button class="s" onclick="swSetStart()">Set start</button></div>
|
||||
<p class="hint">Shown by pages with a stopwatch element (e.g. the Stopwatch page). It counts in real time from its start, also while the display is off or restarting. Stop pauses it, Start continues. Set start: counts from that date and time on (e.g. days since an event). Also in Home Assistant: the Stopwatch switch and the Stopwatch reset button.</p></div></section>
|
||||
|
||||
<section id="t-bitmaps"><div class="card"><h2>Bitmaps</h2><div class="grid" id="bitmaps"></div>
|
||||
<details id="bitmaps_used_box"><summary></summary><div class="grid" id="bitmaps_used"></div></details>
|
||||
@@ -315,9 +317,10 @@ async function toBitmap(n){await post('/api/icon/to_bitmap',{name:n});msg(n+' is
|
||||
async function resetPage(n){if(!confirm('Reset '+n+' to the original page? Your changes to it are lost.'))return;await post('/api/pages/delete',{name:n});msg(n+' reset');if(pe&&same(pe.orig,n))peClose();loadLists()}
|
||||
async function delPage(n){if(!confirm('Delete page '+n+'?'))return;await post('/api/pages/delete',{name:n});msg('Deleted');if(pe&&same(pe.orig,n))peClose();settings=await api('/api/settings');loadLists()}
|
||||
async function delFont(n){if(!confirm('Delete font '+n+'?'))return;await post('/api/fonts/delete',{name:n});msg('Deleted');loadLists()}
|
||||
function swText(ms){const s=Math.floor(ms/1000),h=Math.floor(s/3600),m=Math.floor(s%3600/60),p=n=>String(n).padStart(2,'0');return(h?h+':'+p(m):m)+':'+p(s%60)}
|
||||
function swShow(sw){if(sw)$('swtime').textContent=swText(sw.ms)+(sw.running?' (running)':'')}
|
||||
async function stopwatch(action){swShow(await post('/api/stopwatch',{action}));if(pe)pePreview()}
|
||||
function swText(ms){const s=Math.floor(ms/1000),d=Math.floor(s/86400),h=Math.floor(s%86400/3600),m=Math.floor(s%3600/60),p=n=>String(n).padStart(2,'0');return(d?d+'d '+h+':'+p(m):h?h+':'+p(m):m)+':'+p(s%60)}
|
||||
function swShow(sw){if(!sw)return;$('swtime').textContent=swText(sw.ms)+(sw.running?' (running)':'');$('swsince').textContent=sw.start?'Counting since '+sw.start.replace('T',' '):sw.ms?'Paused':''}
|
||||
async function stopwatch(action,extra){swShow(await post('/api/stopwatch',Object.assign({action},extra)));if(pe)pePreview()}
|
||||
async function swSetStart(){const v=$('swstart').value;if(!v){msg('Pick a date and time first',true);return}await stopwatch('set',{start:v.slice(0,16)});msg('Counting since '+v.replace('T',' '))}
|
||||
async function live(bits){await post('/api/live',{data:toHex(bits)});msg('Shown on the display')}
|
||||
async function stopLive(){await post('/api/live/stop');msg('Back to normal')}
|
||||
// schedule and rules
|
||||
@@ -428,7 +431,7 @@ function peElems(){const d=$('pe_elems');d.innerHTML='';d.append(mk('datalist',{
|
||||
if(e.type=='countdown'){row(field('Until',inp(e,'target','datetime-local')),field('Format',sel(e,'format',CDF.map((x,k)=>[k,x[e.language=='en'?1:0]]))),
|
||||
field('Language',sel(e,'language',[['nl','Nederlands'],['en','English']],peElems)));
|
||||
row(field('Before',inp(e,'text','text',{size:10,maxLength:32})),field('When passed',inp(e,'suffix','text',{size:8,maxLength:16,placeholder:'0'})))}
|
||||
if(e.type=='stopwatch'){row(field('Format',sel(e,'format',[[0,'4:05 (every second)'],[1,'0:04 (hours, minutes)'],[2,'0:04:05 (every second)']])),
|
||||
if(e.type=='stopwatch'){row(field('Format',sel(e,'format',[[0,'4:05 (every second)'],[1,'0:04 (hours, minutes)'],[2,'0:04:05 (every second)'],[3,'12d 4:05:09 (days, every second)'],[4,'3w 2d (weeks, days)']])),
|
||||
field('Before',inp(e,'text','text',{size:8,maxLength:32})),field('After',inp(e,'suffix','text',{size:6,maxLength:16})));
|
||||
row(btn('Start',()=>stopwatch('start')),btn('Stop',()=>stopwatch('stop')),btn('Reset',()=>stopwatch('reset')))}
|
||||
if(e.type=='image'){const name=inp(e,'text','text',{size:20,maxLength:24,placeholder:e.entity?'weather ':'name'});name.setAttribute('list','bmnames');
|
||||
|
||||
+22
-10
@@ -137,6 +137,16 @@ static void handleRoot()
|
||||
server.send_P(200, "text/html", WEB_PAGE);
|
||||
}
|
||||
|
||||
// {running, ms, start (local, while running)}
|
||||
static void stopwatchJson(JsonObject sw)
|
||||
{
|
||||
sw["running"] = stopwatch.running();
|
||||
sw["ms"] = stopwatch.elapsed(epochMs());
|
||||
char start[20];
|
||||
if (appStopwatchStart(start))
|
||||
sw["start"] = start;
|
||||
}
|
||||
|
||||
static void handleStatus()
|
||||
{
|
||||
JsonDocument doc;
|
||||
@@ -168,9 +178,7 @@ static void handleStatus()
|
||||
doc["off_page"] = appSettings().off_page;
|
||||
doc["power_enabled"] = controller.powerEnabled();
|
||||
doc["slow_mode"] = controller.slowModeActive();
|
||||
JsonObject sw = doc["stopwatch"].to<JsonObject>();
|
||||
sw["running"] = stopwatch.running();
|
||||
sw["ms"] = stopwatch.elapsed(millis());
|
||||
stopwatchJson(doc["stopwatch"].to<JsonObject>());
|
||||
|
||||
JsonObject load = doc["load"].to<JsonObject>();
|
||||
load["loops_per_s"] = loopStats.loopsPerSecond();
|
||||
@@ -632,27 +640,31 @@ static void handleLive()
|
||||
sendOk();
|
||||
}
|
||||
|
||||
// {action: "start" | "stop" | "reset"}
|
||||
// {action: "start" | "stop" | "reset" | "set", start: "YYYY-MM-DDTHH:MM" (set, local time)}
|
||||
static void handleStopwatch()
|
||||
{
|
||||
JsonDocument body;
|
||||
if (!readBody(body))
|
||||
return;
|
||||
const char *action = body["action"] | "";
|
||||
const char *error;
|
||||
if (strcmp(action, "start") == 0)
|
||||
appStopwatch(StopwatchAction::Start);
|
||||
error = appStopwatch(StopwatchAction::Start);
|
||||
else if (strcmp(action, "stop") == 0)
|
||||
appStopwatch(StopwatchAction::Stop);
|
||||
error = appStopwatch(StopwatchAction::Stop);
|
||||
else if (strcmp(action, "reset") == 0)
|
||||
appStopwatch(StopwatchAction::Reset);
|
||||
error = appStopwatch(StopwatchAction::Reset);
|
||||
else if (strcmp(action, "set") == 0)
|
||||
error = appStopwatchSetStart(body["start"] | "");
|
||||
else
|
||||
error = "Unknown stopwatch action";
|
||||
if (error)
|
||||
{
|
||||
sendError(400, "Unknown stopwatch action");
|
||||
sendError(400, error);
|
||||
return;
|
||||
}
|
||||
JsonDocument doc;
|
||||
doc["running"] = stopwatch.running();
|
||||
doc["ms"] = stopwatch.elapsed(millis());
|
||||
stopwatchJson(doc.to<JsonObject>());
|
||||
sendJson(200, doc);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
// POST /api/schedule/back end a manual pick
|
||||
// POST /api/live show now {data}
|
||||
// POST /api/live/stop
|
||||
// POST /api/stopwatch {action: "start" | "stop" | "reset"} -> {running, ms}
|
||||
// POST /api/stopwatch {action: "start" | "stop" | "reset" | "set", start:
|
||||
// "YYYY-MM-DDTHH:MM" (set, local)} -> {running, ms, start}
|
||||
// GET /api/settings (the MQTT password and HA token only with ?secrets=1, for
|
||||
// the export; otherwise only *_set)
|
||||
// POST /api/settings changed fields; {settings, restart}
|
||||
|
||||
Reference in new issue
Block a user