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>
No files matched your search
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
@@ -122,6 +122,27 @@ when the shown text changes, so round values with the decimals setting.
|
|||||||
Settings are stored in the ESP32's NVS. The defaults are the original behaviour. There is
|
Settings are stored in the ESP32's NVS. The defaults are the original behaviour. There is
|
||||||
no login: anyone on your WiFi can use the page, as with OTA.
|
no login: anyone on your WiFi can use the page, as with OTA.
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
The dynamic pages on the display, drawn by the ESP32 itself (`python
|
||||||
|
tools/make_page_screenshots.py` makes these pictures again). The Clock page is built in;
|
||||||
|
the others are the example pages in [examples/home-pages.json](examples/home-pages.json)
|
||||||
|
(import them in the web UI, System tab; they use this home's Home Assistant entities).
|
||||||
|
|
||||||
|
| Page | |
|
||||||
|
|---|---|
|
||||||
|
| **Clock** (built in), day and from 17:00 |   |
|
||||||
|
| **Weer**: clock, weather icon by state, humidity, temperature |  |
|
||||||
|
| **Dag en nacht**: sun from 07:00, moon and stars from 19:00, date |   |
|
||||||
|
| **Energie**: net power now, solar today, clock |  |
|
||||||
|
| **3D printer**: progress, time left, status message (scrolls), nozzle temperature |  |
|
||||||
|
| **Binnen**: room and outside temperature, CO2, air quality |  |
|
||||||
|
| **Verbruik**: water and energy used today, solar today |  |
|
||||||
|
| **Grote klok**: big clock, inverted from 22:00 |   |
|
||||||
|
| **Datum**: weekday and date |  |
|
||||||
|
| **Aftellen**: countdown to Christmas |  |
|
||||||
|
| **Stopwatch** |  |
|
||||||
|
|
||||||
## Flashing the Nano over WiFi
|
## Flashing the Nano over WiFi
|
||||||
|
|
||||||
The ESP32 can flash the Nano for you, so you don't need a USB cable at the display:
|
The ESP32 can flash the Nano for you, so you don't need a USB cable at the display:
|
||||||
|
|||||||
@@ -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()
|
||||||