Public Access
Add pictures of all dynamic pages to the README
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>
This commit is contained in:
1 parent
53e9057d02
commit
e991210ce6
16 files changed
+135
No files matched your search
@@ -0,0 +1,114 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user