"""Saves a picture of every dynamic page in Pictures/pages/, drawn by the ESP32 itself. python tools/make_page_screenshots.py [ESP32 address, default 10.1.1.198] The ESP32 draws each page (/api/pages/preview) with the current date and Home Assistant values; pages whose background changes with the time of day get a picture per time slot. The pictures look like the live view in the web UI: yellow and dark dots. Only the Python standard library is used (the PNG files are written by hand). """ import json import os import re import struct import sys import urllib.request import zlib 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__))), 'Pictures', 'pages') W, H = 84, 19 CELL = 10 # pixels per dot BORDER = 10 DOT = 0.42 # dot radius, part of a cell ON, OFF, BG = (0xFF, 0xD2, 0x1F), (0x2C, 0x2F, 0x36), (0, 0, 0) TIME = '12:34' # the time pages without time slots are drawn for def call(path, body=None): data = None if body is None else json.dumps(body).encode() req = urllib.request.Request(ESP + path, data=data, headers={'Content-Type': 'application/json'}) with urllib.request.urlopen(req, timeout=20) as r: return json.loads(r.read()) def dots(hexdata): return [[int(hexdata[(y * 11 + x // 8) * 2:(y * 11 + x // 8) * 2 + 2], 16) >> (7 - x % 8) & 1 for x in range(W)] for y in range(H)] def png(path, image): width, height = W * CELL + 2 * BORDER, H * CELL + 2 * BORDER # One dot, 4x4 supersampled for smooth edges: coverage per pixel of a cell sub = 4 cover = [] for py in range(CELL): row = [] for px in range(CELL): inside = 0 for sy in range(sub): for sx in range(sub): dx = px + (sx + 0.5) / sub - CELL / 2 dy = py + (sy + 0.5) / sub - CELL / 2 inside += dx * dx + dy * dy <= (DOT * CELL) ** 2 row.append(inside / (sub * sub)) cover.append(row) def mix(color, a): return bytes(round(BG[i] + (color[i] - BG[i]) * a) for i in range(3)) rows = [] blank = mix(BG, 0) * width for y in range(height): gy = y - BORDER if gy < 0 or gy >= H * CELL: rows.append(b'\0' + blank) continue line = bytearray(mix(BG, 0) * BORDER) for gx in range(W * CELL): line += mix(ON if image[gy // CELL][gx // CELL] else OFF, cover[gy % CELL][gx % CELL]) line += mix(BG, 0) * BORDER rows.append(b'\0' + bytes(line)) def chunk(kind, data): return struct.pack('>I', len(data)) + kind + data + struct.pack('>I', zlib.crc32(kind + data) & 0xFFFFFFFF) with open(path, 'wb') as f: f.write(b'\x89PNG\r\n\x1a\n') f.write(chunk(b'IHDR', struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0))) f.write(chunk(b'IDAT', zlib.compress(b''.join(rows), 9))) f.write(chunk(b'IEND', b'')) def file_name(name, time=None): base = re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-') return base + ('-' + time.replace(':', '') if time else '') + '.png' def main(): os.makedirs(OUT, exist_ok=True) for p in call('/api/pages')['pages']: page = p['page'] # The entities the page shows: read them now, so the picture has real values for e in page['elements']: if e.get('entity'): query = urllib.parse.urlencode({'entity': e['entity'], 'attribute': e.get('attribute', '')}) try: call('/api/ha/value?' + query) except Exception: pass starts = [s['start'] for s in page['slots']] times = [None] if len(starts) < 2 else starts for t in times: # A slot from 00:00 is drawn at TIME; other slots a little after they start when = TIME if t is None else (TIME if t == '00:00' else '%s:%02d' % (t[:2], int(t[3:]) + 30)) r = call('/api/pages/preview', {'page': page, 'time': when}) name = file_name(p['name'], None if t is None else t) png(os.path.join(OUT, name), dots(r['data'])) print(name) if __name__ == '__main__': import urllib.parse main()