Public Access
tools/make_page_screenshots.py asks the ESP32 to draw every page (with the current Home Assistant values) and writes them as PNG in the style of the web UI's live view to Pictures/pages/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
4.2 KiB
Python
115 lines
4.2 KiB
Python
"""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()
|