"""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()