Weather forecast page, 16 elements per page, attribute paths, examples from the display

- Home Assistant attributes can be paths into lists (forecast.1.condition).
- Date elements can show a day ahead (day offset 0-7).
- Pages hold 16 elements (page format 6; the old formats are frozen at 8).
  Pages are reset in place (clearPage), not through a 4 KB temporary.
- Verwachting page: today (icon, temperature, max/min) and the next 2
  days with 7 x 7 icons (weer7 <condition>), from a template sensor
  (docs/home-assistant.md).
- examples/home-pages.json is exported from the display with all pages
  and their icons, private entity id parts replaced by placeholders
  (tools/export_examples.py replaces tools/make_example_pages.py).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-16 22:36:29 +02:00
1 parent 839e86e8f1
commit a6b946e17c
33 files changed
+1918 -1050

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
"""Writes examples/home-pages.json from the pages on a display: the dynamic pages with the
icons, bitmaps and fonts they use, as a backup file that the web UI imports (System tab).
python tools/export_examples.py [ESP32 address, default 10.1.1.198]
Private parts of entity ids are replaced by placeholders (PRIVATE below); the export stops
when a page still contains one of them. The pages that come with the firmware (Clock) and
their icons (the Pikachus) are left out: every display has them. Only the Python standard
library is used.
"""
import json
import os
import re
import sys
import urllib.parse
import urllib.request
ESP = 'http://' + (sys.argv[1] if len(sys.argv) > 1 else '10.1.1.198')
OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'examples', 'home-pages.json')
# Private text -> placeholder (in entity ids, attributes and texts)
PRIVATE = {
'vigilantestraat': 'home', # the street (climate entity)
'122225067403': '000000000000', # the Envoy's serial number
'mangavalkserver': 'plex_server', # the Plex server's name
}
FROM_FIRMWARE_PAGES = {'clock'}
FROM_FIRMWARE_ICONS = {'pikachu', 'pikachu yawning'}
def get(path):
with urllib.request.urlopen(ESP + path, timeout=60) as r:
return json.loads(r.read())
def clean(value):
if isinstance(value, str):
for private, placeholder in PRIVATE.items():
value = re.sub(re.escape(private), placeholder, value, flags=re.IGNORECASE)
return value
def main():
pages = [p for p in get('/api/pages')['pages'] if p['name'].lower() not in FROM_FIRMWARE_PAGES]
icons = {i['name'].lower(): i for i in get('/api/icons')['icons']}
bitmaps = {b['name'].lower(): b for b in get('/api/bitmaps')['bitmaps'] if not b['builtin']}
fonts = {f['name'].lower(): f for f in get('/api/fonts')['fonts'] if not f['builtin']}
# What the pages use: backgrounds, fixed images, the images by state (name start), fonts
images, used_fonts = set(), set()
for p in pages:
for s in p['page']['slots']:
images.add(s['bitmap'].lower())
for e in p['page']['elements']:
used_fonts.add(e['font'].lower())
if e['type'] != 'image':
continue
if not e['entity']:
images.add(e['text'].lower())
else:
start = e['text'].lower()
images.update(n for n in list(icons) + list(bitmaps) if n.startswith(start))
for e in p['page']['elements']:
for k, v in e.items():
e[k] = clean(v)
out = {
'format': 'flipdot-backup', 'version': 1, 'firmware': get('/api/status')['version'],
'exported': '2026-10-16T00:00:00Z',
'bitmaps': [{'name': b['name'], 'data': b['data']} for n, b in sorted(bitmaps.items()) if n in images],
'icons': [{k: i[k] for k in ('name', 'width', 'height', 'data')} for n, i in sorted(icons.items())
if n in images and n not in FROM_FIRMWARE_ICONS],
'pages': [{'name': p['name'], 'page': p['page']} for p in pages],
'fonts': [],
}
for n in sorted(used_fonts & set(fonts)):
out['fonts'].append(get('/api/font?name=' + urllib.parse.quote(fonts[n]['name'])))
text = json.dumps(out, ensure_ascii=False, indent=1)
left = [p for p in PRIVATE if p.lower() in text.lower()]
if left:
sys.exit('Private text still in the export: ' + ', '.join(left))
with open(OUT, 'w', encoding='utf-8', newline='\n') as f:
f.write(text + '\n')
print(len(out['pages']), 'pages,', len(out['icons']), 'icons,', len(out['bitmaps']), 'bitmaps,',
len(out['fonts']), 'fonts ->', OUT)
main()
-397
View File
@@ -1,397 +0,0 @@
"""Builds examples/home-pages.json: icons, backgrounds and dynamic pages for this home (import it in the web UI, System tab).
python tools/make_example_pages.py examples/home-pages.json
"""
import json
import sys
W, H = 84, 19
def blank():
return [[0] * W for _ in range(H)]
def put(img, art, x0, y0):
for y, row in enumerate(art):
for x, c in enumerate(row):
if c == '#' and 0 <= x0 + x < W and 0 <= y0 + y < H:
img[y0 + y][x0 + x] = 1
return img
def to_hex(img):
out = ''
for y in range(H):
for i in range(11):
v = 0
for k in range(8):
x = i * 8 + k
if x < W and img[y][x]:
v |= 0x80 >> k
out += '%02x' % v
return out
CLOUD = [
"...........",
"....###....",
"..##.####..",
".#########.",
"###########",
"###########",
".#########.",
]
WEATHER = {
'sunny': [
".....#.....",
".#...#...#.",
"..#.....#..",
"....###....",
"...#####...",
"##.#####.##",
"...#####...",
"....###....",
"..#.....#..",
".#...#...#.",
".....#.....",
],
'clear-night': [
"....###....",
"..###......",
".###.......",
".##......#.",
"###.......",
"###........",
"###........",
".###.......",
".####....#.",
"..#######..",
"....###....",
],
'partlycloudy': [
"..#..#.....",
"....###....",
".#.#####...",
"...####....",
"#.##..###..",
"..#########",
".##########",
"###########",
"###########",
".#########.",
"...........",
],
'cloudy': ["...........", "..........."] + CLOUD + ["..........."] * 2,
'fog': [
"...........",
"###########",
"...........",
".##########",
"...........",
"#########..",
"...........",
"..#########",
"...........",
"#########..",
"...........",
],
'rainy': CLOUD + [
"...........",
".#...#...#.",
"#...#...#..",
"...........",
],
'pouring': CLOUD + [
".#.#.#.#.#.",
"#.#.#.#.#..",
".#.#.#.#.#.",
"#.#.#.#.#..",
],
'lightning': CLOUD[:6] + [
".....##....",
"....##.....",
"...#####...",
".....##....",
"....##.....",
],
'lightning-rainy': CLOUD[:6] + [
".#...##..#.",
"#...##..#..",
"...#####...",
".#...##..#.",
"#...##..#..",
],
'snowy': CLOUD + [
".#...#...#.",
"...#...#...",
".#...#...#.",
"...#...#...",
],
'snowy-rainy': CLOUD + [
".#...#...#.",
"#...#...#..",
"...#...#...",
".#...#...#.",
],
'hail': CLOUD + [
".##..##..##",
".##..##..##",
"...........",
"..##..##...",
],
'windy': [
"...........",
"......##...",
"........#..",
"#########..",
"...........",
"###########",
"...........",
"#######....",
"........#..",
"......##...",
"...........",
],
'windy-variant': CLOUD + [
"...........",
"#########..",
"...........",
"..#########",
],
'default': [
".....#.....",
"....#.#....",
"....#.#....",
"...#.#.#...",
"...#.#.#...",
"..#..#..#..",
"..#..#..#..",
".#.......#.",
".#...#...#.",
"#.........#",
"###########",
],
}
WEATHER['exceptional'] = WEATHER['default']
BOLT = [
"....##.",
"...##..",
"..##...",
".#####.",
"...##..",
"..##...",
".##....",
"##.....",
]
SUN_SMALL = [
"....#....",
".#.....#.",
"...###...",
"..#####..",
"#.#####.#",
"..#####..",
"...###...",
".#.....#.",
"....#....",
]
DROP = [
"...#...",
"...#...",
"..###..",
"..###..",
".#####.",
"#######",
"#######",
"#######",
".#####.",
"..###..",
]
HOUSE = [
".....#.....",
"....###....",
"...##.##...",
"..##...##..",
".##.....##.",
"##.......##",
".#.......#.",
".#.##....#.",
".#.##.##.#.",
".#.##.##.#.",
".#########.",
]
PRINTER = [
"###########",
"#.........#",
"#.#######.#",
"#....#....#",
"#....#....#",
"#.........#",
"#...###...#",
"#..#####..#",
"###########",
"#.........#",
"###########",
]
CALENDAR = [
".#.....#...",
"###########",
"#.........#",
"###########",
"#.#.#.#.#.#",
"#.........#",
"#.#.#.#.#.#",
"#.........#",
"#.#.#.#...#",
"#.........#",
"###########",
]
TREE = [
".....#.....",
"....###....",
".....#.....",
"....###....",
"...#####...",
"....###....",
"...#####...",
"..#######..",
"...#####...",
"..#######..",
".#########.",
".....#.....",
"....###....",
]
MOON = WEATHER['clear-night']
STARS = [(1, 2), (24, 1), (27, 7), (3, 9), (22, 4)]
CORNER = ["#####", "#....", "#....", "#...."]
def corners():
img = blank()
put(img, CORNER, 0, 0)
put(img, [r[::-1] for r in CORNER], W - 5, 0)
put(img, CORNER[::-1], 0, H - 4)
put(img, [r[::-1] for r in CORNER[::-1]], W - 5, H - 4)
return img
bitmaps = {}
for state, art in WEATHER.items():
bitmaps['weer ' + state] = put(blank(), art, 0, 0)
bitmaps['bg energie'] = put(put(blank(), BOLT, 1, 0), SUN_SMALL, 0, 10)
bitmaps['bg printer'] = put(blank(), PRINTER, 0, 0)
bitmaps['bg binnen'] = put(blank(), HOUSE, 0, 0)
bitmaps['bg verbruik'] = put(put(blank(), DROP, 1, 0), BOLT, 1, 11)
bitmaps['bg datum'] = put(blank(), CALENDAR, 0, 4)
bitmaps['bg kerst'] = put(blank(), TREE, 0, 3)
bitmaps['bg dag'] = put(blank(), WEATHER['sunny'], 9, 0)
night = put(blank(), MOON, 9, 0)
for x, y in STARS:
night[y][x] = 1
bitmaps['bg nacht'] = night
bitmaps['bg kader'] = corners()
bitmaps['bg vol'] = [[1] * W for _ in range(H)]
# Plex: the chevron (a thick ">", tip on the middle row) and the Tautulli state icons
plex = blank()
for y in range(1, 18):
start = (8 - abs(y - 9)) // 2
for x in range(start, start + 5):
plex[y][x] = 1
bitmaps['plex logo'] = plex
bitmaps['plex playing'] = put(blank(), ["#..", "##.", "###", "##.", "#.."], 0, 0)
bitmaps['plex paused'] = put(blank(), ["##.##"] * 5, 0, 0)
bitmaps['plex buffering'] = put(blank(), [".....", ".....", ".....", ".....", "#.#.#"], 0, 0)
def el(type, x, y, font='Normal', **kw):
e = {'type': type, 'x': x, 'y': y, 'align': 'left', 'overlay': 'top', 'font': font, 'format': 0,
'language': 'nl', 'decimals': -1, 'text': '', 'suffix': '', 'entity': '', 'attribute': '',
'factor': 1, 'scroll_width': 0, 'scroll_speed': 8, 'target': '2000-01-01T00:00',
'bar_width': 40, 'bar_height': 3, 'bar_max': 100, 'bar_frame': True}
e.update(kw)
return e
# Placeholders: put your own entity ids here (the Envoy ones contain its serial number)
ENVOY = 'sensor.envoy_000000000000_'
pages = {
# 1. Weather clock: large clock, weather icon, outside humidity and temperature
'Weer': {'slots': [], 'elements': [
el('clock', 0, 0, 'Full height'),
el('image', 55, 0, text='weer ', entity='weather.knmi_home'),
el('ha', 84, 2, 'Small', align='right', entity='weather.knmi_home', attribute='humidity', suffix='%', decimals=0),
el('ha', 84, 13, 'Small', align='right', entity='weather.knmi_home', attribute='temperature', suffix='°C', decimals=1),
]},
# 2. Sun by day, moon and stars at night, large clock and the date
'Dag en nacht': {'slots': [{'start': '07:00', 'bitmap': 'bg dag'}, {'start': '19:00', 'bitmap': 'bg nacht'}], 'elements': [
el('clock', 84, 0, 'Full height', align='right'),
el('date', 0, 13, 'Small', format=1),
]},
# 3. Energy: net power now (negative = export), solar today, small clock
'Energie': {'slots': [{'start': '00:00', 'bitmap': 'bg energie'}], 'elements': [
el('ha', 10, 0, entity=ENVOY + 'current_net_power_consumption', suffix=' W', decimals=0),
el('ha', 11, 11, entity=ENVOY + 'today_s_energy_production', suffix=' kWh', decimals=1, factor=0.001),
el('clock', 84, 0, 'Small', align='right'),
]},
# 4. 3D printer: progress, time left, status message (scrolls), nozzle temperature
'3D printer': {'slots': [{'start': '00:00', 'bitmap': 'bg printer'}], 'elements': [
el('ha', 14, 0, entity='sensor.mainsailos_progress', suffix='%', decimals=0),
el('ha', 84, 0, align='right', entity='sensor.mainsailos_print_time_left', suffix='m', decimals=0, factor=1 / 60),
el('ha', 14, 13, 'Small', entity='sensor.mainsailos_current_display_message', scroll_width=46, scroll_speed=6),
el('ha', 84, 13, 'Small', align='right', entity='sensor.mainsailos_extruder_temperature', suffix='°', decimals=0),
]},
# 5. Indoor climate: room temperature, outside, CO2 and air quality
'Binnen': {'slots': [{'start': '00:00', 'bitmap': 'bg binnen'}], 'elements': [
el('ha', 14, 2, entity='climate.home', attribute='current_temperature', suffix='°', decimals=1),
el('image', 49, 0, text='weer ', entity='weather.knmi_home'),
el('ha', 84, 2, 'Small', align='right', entity='weather.knmi_home', attribute='temperature', suffix='°', decimals=1),
el('ha', 0, 13, 'Small', entity='sensor.eco2', text='CO2 ', suffix='ppm', decimals=0),
el('ha', 84, 13, 'Small', align='right', entity='sensor.air_quality_index_aqi_2', text='lucht '),
]},
# 6. Today: water and energy use, solar production
'Verbruik': {'slots': [{'start': '00:00', 'bitmap': 'bg verbruik'}], 'elements': [
el('ha', 10, 1, entity='sensor.util_water_usage_daily', suffix=' l', decimals=0, factor=1000),
el('ha', 10, 11, entity=ENVOY + 'today_s_energy_consumption', suffix=' kWh', decimals=1, factor=0.001),
el('clock', 84, 0, 'Small', align='right'),
el('ha', 84, 13, 'Small', align='right', entity=ENVOY + 'today_s_energy_production', text='zon ', suffix='kWh', decimals=1, factor=0.001),
]},
# 7. Big clock in a frame; from 22:00 black digits on yellow
'Grote klok': {'slots': [{'start': '07:00', 'bitmap': 'bg kader'}, {'start': '22:00', 'bitmap': 'bg vol'}], 'elements': [
el('clock', 42, 0, 'Full height', align='center', overlay='invert'),
]},
# 8. The date in Dutch with a calendar
'Datum': {'slots': [{'start': '00:00', 'bitmap': 'bg datum'}], 'elements': [
el('date', 14, 1, format=5),
el('date', 14, 12, 'Small', format=2),
el('clock', 84, 12, 'Small', align='right'),
]},
# Countdown to Christmas
'Aftellen': {'slots': [{'start': '00:00', 'bitmap': 'bg kerst'}], 'elements': [
el('text', 14, 1, 'Small', text='Kerst over'),
el('countdown', 14, 10, target='2026-12-25T00:00', suffix='Vrolijk kerst!', scroll_width=70, scroll_speed=8),
]},
# Stopwatch, large
'Stopwatch': {'slots': [], 'elements': [
el('stopwatch', 42, 0, 'Full height', align='center'),
]},
# Plex: how many watch (Plex integration), and for the Plex user of the settings
# (Tautulli's sensors): play/pause icon, title (scrolls) and a progress bar
'Plex': {'slots': [{'start': '00:00', 'bitmap': 'plex logo'}], 'elements': [
el('ha', 13, 0, entity='sensor.mangavalkserver', suffix=' watching'),
el('image', 13, 9, text='plex ', entity='sensor.{plex_user}_state'),
el('ha', 19, 9, 'Small', entity='sensor.{plex_user}_full_title', scroll_width=65, scroll_speed=8),
el('bar', 13, 16, entity='sensor.{plex_user}_progress', bar_width=71, bar_height=3),
]},
}
backup = {
'format': 'flipdot-backup', 'version': 1, 'firmware': '2.9.0', 'exported': '2026-10-16T00:00:00Z',
'bitmaps': [{'name': n, 'data': to_hex(b)} for n, b in bitmaps.items()],
'pages': [{'name': n, 'page': p} for n, p in pages.items()],
'fonts': [],
}
json.dump(backup, open(sys.argv[1], 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
print(len(bitmaps), 'bitmaps,', len(pages), 'pages')