diff --git a/Pictures/pages/3d-printer.png b/Pictures/pages/3d-printer.png new file mode 100644 index 0000000..c08d44a Binary files /dev/null and b/Pictures/pages/3d-printer.png differ diff --git a/Pictures/pages/aftellen.png b/Pictures/pages/aftellen.png new file mode 100644 index 0000000..4105f25 Binary files /dev/null and b/Pictures/pages/aftellen.png differ diff --git a/Pictures/pages/binnen.png b/Pictures/pages/binnen.png new file mode 100644 index 0000000..dd7cbca Binary files /dev/null and b/Pictures/pages/binnen.png differ diff --git a/Pictures/pages/clock-0000.png b/Pictures/pages/clock-0000.png new file mode 100644 index 0000000..28ad2fc Binary files /dev/null and b/Pictures/pages/clock-0000.png differ diff --git a/Pictures/pages/clock-1700.png b/Pictures/pages/clock-1700.png new file mode 100644 index 0000000..5957bd0 Binary files /dev/null and b/Pictures/pages/clock-1700.png differ diff --git a/Pictures/pages/dag-en-nacht-0700.png b/Pictures/pages/dag-en-nacht-0700.png new file mode 100644 index 0000000..f270f8b Binary files /dev/null and b/Pictures/pages/dag-en-nacht-0700.png differ diff --git a/Pictures/pages/dag-en-nacht-1900.png b/Pictures/pages/dag-en-nacht-1900.png new file mode 100644 index 0000000..12f7157 Binary files /dev/null and b/Pictures/pages/dag-en-nacht-1900.png differ diff --git a/Pictures/pages/datum.png b/Pictures/pages/datum.png new file mode 100644 index 0000000..27c76e4 Binary files /dev/null and b/Pictures/pages/datum.png differ diff --git a/Pictures/pages/energie.png b/Pictures/pages/energie.png new file mode 100644 index 0000000..fa3f94c Binary files /dev/null and b/Pictures/pages/energie.png differ diff --git a/Pictures/pages/grote-klok-0700.png b/Pictures/pages/grote-klok-0700.png new file mode 100644 index 0000000..5771498 Binary files /dev/null and b/Pictures/pages/grote-klok-0700.png differ diff --git a/Pictures/pages/grote-klok-2200.png b/Pictures/pages/grote-klok-2200.png new file mode 100644 index 0000000..946d9c8 Binary files /dev/null and b/Pictures/pages/grote-klok-2200.png differ diff --git a/Pictures/pages/stopwatch.png b/Pictures/pages/stopwatch.png new file mode 100644 index 0000000..fb72e9b Binary files /dev/null and b/Pictures/pages/stopwatch.png differ diff --git a/Pictures/pages/verbruik.png b/Pictures/pages/verbruik.png new file mode 100644 index 0000000..d0508e4 Binary files /dev/null and b/Pictures/pages/verbruik.png differ diff --git a/Pictures/pages/weer.png b/Pictures/pages/weer.png new file mode 100644 index 0000000..f9cc07d Binary files /dev/null and b/Pictures/pages/weer.png differ diff --git a/README.md b/README.md index 48651ca..6ab54be 100644 --- a/README.md +++ b/README.md @@ -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 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 | ![Clock](Pictures/pages/clock-0000.png) ![Clock, evening](Pictures/pages/clock-1700.png) | +| **Weer**: clock, weather icon by state, humidity, temperature | ![Weer](Pictures/pages/weer.png) | +| **Dag en nacht**: sun from 07:00, moon and stars from 19:00, date | ![Dag en nacht, day](Pictures/pages/dag-en-nacht-0700.png) ![Dag en nacht, night](Pictures/pages/dag-en-nacht-1900.png) | +| **Energie**: net power now, solar today, clock | ![Energie](Pictures/pages/energie.png) | +| **3D printer**: progress, time left, status message (scrolls), nozzle temperature | ![3D printer](Pictures/pages/3d-printer.png) | +| **Binnen**: room and outside temperature, CO2, air quality | ![Binnen](Pictures/pages/binnen.png) | +| **Verbruik**: water and energy used today, solar today | ![Verbruik](Pictures/pages/verbruik.png) | +| **Grote klok**: big clock, inverted from 22:00 | ![Grote klok](Pictures/pages/grote-klok-0700.png) ![Grote klok, night](Pictures/pages/grote-klok-2200.png) | +| **Datum**: weekday and date | ![Datum](Pictures/pages/datum.png) | +| **Aftellen**: countdown to Christmas | ![Aftellen](Pictures/pages/aftellen.png) | +| **Stopwatch** | ![Stopwatch](Pictures/pages/stopwatch.png) | + ## Flashing the Nano over WiFi The ESP32 can flash the Nano for you, so you don't need a USB cable at the display: diff --git a/tools/make_page_screenshots.py b/tools/make_page_screenshots.py new file mode 100644 index 0000000..d188f59 --- /dev/null +++ b/tools/make_page_screenshots.py @@ -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()