Progress bars can show the percentage on top

The percentage (rounded down, in the element's font) is centred inside the
bar and its dots invert the bar, so it reads on the filled and the empty
part. It uses the bar's unused format and font fields, so the page format
stays the same. The Bad page shows it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-18 16:27:56 +02:00
1 parent 9959c1aa74
commit 948087077d
9 files changed
+187 -9

No files matched your search

+1 -1
View File
@@ -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.15.1"
#define FIRMWARE_VERSION "2.16.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};
+16
View File
@@ -150,6 +150,8 @@ static uint8_t formatCount(const PageElement &e)
return COUNTDOWN_FORMAT_COUNT;
case ElementType::Stopwatch:
return STOPWATCH_FORMAT_COUNT;
case ElementType::Bar:
return BAR_FORMAT_COUNT;
default:
return 255; // not used
}
@@ -788,6 +790,20 @@ int barFill(const PageElement &e, const char *ha_value, int fill_width)
return n;
}
void barPercent(const PageElement &e, const char *ha_value, char *out, size_t size)
{
double v;
if (!ha_value || !parseNumber(ha_value, v))
{
snprintf(out, size, "--");
return;
}
double percent = v * e.factor / e.bar_max * 100;
// Rounded down, so 100% only shows when the bar is full; + 1e-9 keeps 0.29 x 100 at 29
int n = !(percent > 0) ? 0 : percent >= 100 ? 100 : (int)(percent + 1e-9);
snprintf(out, size, "%d%%", n);
}
bool elementShowsSeconds(const PageElement &e, const LocalTime &t, bool stopwatch_running)
{
switch (e.type)
+8 -1
View File
@@ -98,6 +98,11 @@ 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;
// Progress bar formats
const uint8_t BAR_PLAIN = 0; // only the bar
const uint8_t BAR_PERCENT = 1; // with the percentage on top (inverting the bar below it)
const uint8_t BAR_FORMAT_COUNT = 2;
// How a Home Assistant value is shown
enum class HaShow : uint8_t
{
@@ -152,7 +157,7 @@ struct PageElement
Align align = Align::Left;
Overlay overlay = Overlay::OnTop;
char font[PAGE_NAME_SIZE] = "Normal";
uint8_t format = 0; // CLOCK_*, DATE_*, COUNTDOWN_*, STOPWATCH_*, HaValue: DURATION_* or TIMESTAMP_*
uint8_t format = 0; // CLOCK_*, DATE_*, COUNTDOWN_*, STOPWATCH_*, BAR_*, HaValue: DURATION_* or TIMESTAMP_*
Language language = Language::Dutch;
int8_t decimals = DECIMALS_AS_IS;
// Text: the text; HaValue, Countdown, Stopwatch: shown before the value; Image: the
@@ -222,6 +227,8 @@ void elementText(const PageElement &element, const LocalTime &time, const Elemen
// Bar: how many dots of fill_width are filled for this Home Assistant value (value x
// factor / bar_max, 0 to fill_width); 0 when the value is unknown or not a number
int barFill(const PageElement &element, const char *ha_value, int fill_width);
// Bar with BAR_PERCENT: the text on it, e.g. "42%" (rounded down, 0-100), "--" when unknown
void barPercent(const PageElement &element, const char *ha_value, char *out, size_t out_size);
// Whether the element's text shows seconds now (it then changes every second)
bool elementShowsSeconds(const PageElement &element, const LocalTime &time, bool stopwatch_running);
+34
View File
@@ -90,6 +90,38 @@ static void drawImage(const PageElement &e, PageResources &resources, uint8_t *b
}
}
// Bar with BAR_PERCENT: the percentage in the middle of the area inside the frame, its
// dots flip what is below them (so it shows on the filled and the empty part). Centred on
// the dots the text really has (fonts have empty rows); left out when it doesn't fit.
static void drawBarPercent(const PageElement &e, PageResources &resources, uint8_t *bitmap, int x, int y, int w,
int h)
{
char text[8];
barPercent(e, resources.haValue(e), text, sizeof(text));
Font font = usableFont(resources, e.font);
uint8_t dots[BITMAP_BYTES];
memset(dots, 0, BITMAP_BYTES);
drawText(dots, font, 0, 0, text, DrawMode::Set);
int left = BITMAP_WIDTH, right = -1, top = BITMAP_HEIGHT, bottom = -1;
for (int yy = 0; yy < BITMAP_HEIGHT; yy++)
for (int xx = 0; xx < BITMAP_WIDTH; xx++)
if (bitmapPixel(dots, xx, yy))
{
left = xx < left ? xx : left;
right = xx > right ? xx : right;
top = yy < top ? yy : top;
bottom = yy > bottom ? yy : bottom;
}
int tw = right - left + 1, th = bottom - top + 1;
if (right < 0 || tw > w || th > h)
return;
int ox = x + (w - tw) / 2 - left, oy = y + (h - th) / 2 - top;
for (int yy = top; yy <= bottom; yy++)
for (int xx = left; xx <= right; xx++)
if (bitmapPixel(dots, xx, yy))
setBitmapPixel(bitmap, ox + xx, oy + yy, !bitmapPixel(bitmap, ox + xx, oy + yy));
}
// Bar: a frame (optional) filled from the left by the Home Assistant value
static void drawBar(const PageElement &e, PageResources &resources, uint8_t *bitmap, ElementBox &box)
{
@@ -120,6 +152,8 @@ static void drawBar(const PageElement &e, PageResources &resources, uint8_t *bit
int bx = left + x, by = e.y + y;
setBitmapPixel(bitmap, bx, by, e.overlay == Overlay::Invert ? !bitmapPixel(bitmap, bx, by) : true);
}
if (e.format == BAR_PERCENT)
drawBarPercent(e, resources, bitmap, left + inner, e.y + inner, w - 2 * inner, h - 2 * inner);
}
// A text element; returns the ms until it scrolls on (0: it doesn't scroll)
+3 -1
View File
@@ -429,6 +429,8 @@ function peElems(){const d=$('pe_elems');d.innerHTML='';d.append(mk('datalist',{
row(field('Full at',inpf(e,'bar_max',{style:'width:6em'})),field('Multiply by',inpf(e,'factor',{style:'width:6em'})),
field('Width',inp(e,'bar_width','number',{style:'width:4em',min:1,max:84})),field('Height',inp(e,'bar_height','number',{style:'width:4em',min:1,max:19})),
chk(e,'bar_frame','frame (from 3 x 3 dots)'));
const pc=mk('input',{type:'checkbox',checked:e.format==1});pc.onchange=()=>{e.format=pc.checked?1:0;if(pc.checked&&same(e.font,'Normal'))e.font='Small';peElems();pePreview()};
row(mk('label',{},pc,' percentage on top (inverts the bar below it; only shown when it fits inside, Small needs a height of 7 with frame)'));
rows.push(mk('p',{className:'hint',textContent:'Filled from the left: value x multiplier / full at (e.g. a percentage: full at 100; seconds of a duration: use an entity with the percentage). Unknown values show an empty bar.'}))}
const fonts=fontList.map(f=>[f.name,f.name+' ('+f.height+')']);if(!fontList.some(f=>same(f.name,e.font)))fonts.push([e.font,e.font+' (gone)']);
const pos=[field('X',inp(e,'x','number',{style:'width:5em'})),field('Y',inp(e,'y','number',{style:'width:5em'}))];
@@ -437,7 +439,7 @@ function peElems(){const d=$('pe_elems');d.innerHTML='';d.append(mk('datalist',{
row(...pos,field('Align',sel(e,'align',[['left','left'],['center','center'],['right','right']])),field('Font',sel(e,'font',fonts)),ovl);
row(field('Scroll in a window of',inp(e,'scroll_width','number',{style:'width:5em',min:0,max:168})),'dots (0: off), at',
inpf(e,'scroll_speed',{style:'width:5em',min:0.05,max:30}),'dots per second (0.25: one every 4 s)')}
else if(e.type=='bar')row(...pos,field('Align',sel(e,'align',[['left','left'],['center','center'],['right','right']])),ovl);
else if(e.type=='bar')row(...pos,field('Align',sel(e,'align',[['left','left'],['center','center'],['right','right']])),...(e.format==1?[field('Font',sel(e,'font',fonts))]:[]),ovl);
else row(...pos,ovl);
box.append(...rows);d.append(box)})}
function peAdd(t){if(pe.page.elements.length>=16){msg('16 elements at most',true);return}pe.page.elements.push(newEl(t));pe.sel=pe.page.elements.length-1;peElems();pePreview()}