diff --git a/CLAUDE.md b/CLAUDE.md index bc51209..b4f02e9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -168,6 +168,14 @@ See README.md for the overview. old hardcoded clock (also the default and the HA "Clock" option). `pageStoreBegin()` stores a copy of each built-in page (`logic/builtin_pages`) at boot when none is stored, so it can be edited; the built-in one is the fallback, and deleting the copy resets it. +- Pages hold 16 elements (format 6). The old formats in `page_store.cpp` are frozen at + `OLD_PAGE_ELEMENTS` (8). A `DynamicPage` is about 4 KB: allocate it with `new`, and reset + it with `clearPage()`, not `page = DynamicPage()` (a temporary on the 8 KB loop stack). +- HA attributes may be paths into lists (`forecast.1.condition`, `net/ha_api`). The + forecast comes from the template sensor in `docs/home-assistant.md`. +- `examples/home-pages.json` is written by `tools/export_examples.py` from the display. + Its PRIVATE map replaces private parts of entity ids; extend it for new pages and check + the output. - `ha_api` attribute lookups: cast `char` arrays to `const char *` for ArduinoJson keys. ArduinoJson 7.4 takes a `char[N]` as a literal of length N-1, which never matches (that broke all attributes from 2.9.0 to 2.10.0). diff --git a/Pictures/pages/3d-printer-finished.png b/Pictures/pages/3d-printer-finished.png index 497344c..c2d3241 100644 Binary files a/Pictures/pages/3d-printer-finished.png and b/Pictures/pages/3d-printer-finished.png differ diff --git a/Pictures/pages/3d-printer.png b/Pictures/pages/3d-printer.png index 966e521..74edfc8 100644 Binary files a/Pictures/pages/3d-printer.png and b/Pictures/pages/3d-printer.png differ diff --git a/Pictures/pages/binnen.png b/Pictures/pages/binnen.png index 8986c0f..bb30eb7 100644 Binary files a/Pictures/pages/binnen.png and b/Pictures/pages/binnen.png differ diff --git a/Pictures/pages/energie.png b/Pictures/pages/energie.png index cbab0f7..da4a6f5 100644 Binary files a/Pictures/pages/energie.png and b/Pictures/pages/energie.png differ diff --git a/Pictures/pages/verbruik.png b/Pictures/pages/verbruik.png index e5b8f12..de042aa 100644 Binary files a/Pictures/pages/verbruik.png and b/Pictures/pages/verbruik.png differ diff --git a/Pictures/pages/verwachting.png b/Pictures/pages/verwachting.png new file mode 100644 index 0000000..357a1c4 Binary files /dev/null and b/Pictures/pages/verwachting.png differ diff --git a/Pictures/pages/weer.png b/Pictures/pages/weer.png index 68abfcc..a32708e 100644 Binary files a/Pictures/pages/weer.png and b/Pictures/pages/weer.png differ diff --git a/Pictures/web-ui/bitmaps.png b/Pictures/web-ui/bitmaps.png index 068b737..de324a8 100644 Binary files a/Pictures/web-ui/bitmaps.png and b/Pictures/web-ui/bitmaps.png differ diff --git a/Pictures/web-ui/draw.png b/Pictures/web-ui/draw.png index 5235028..593c0bf 100644 Binary files a/Pictures/web-ui/draw.png and b/Pictures/web-ui/draw.png differ diff --git a/Pictures/web-ui/home-assistant.png b/Pictures/web-ui/home-assistant.png index 770ba96..5fa59bd 100644 Binary files a/Pictures/web-ui/home-assistant.png and b/Pictures/web-ui/home-assistant.png differ diff --git a/Pictures/web-ui/icons.png b/Pictures/web-ui/icons.png index 05bb8aa..b2bce00 100644 Binary files a/Pictures/web-ui/icons.png and b/Pictures/web-ui/icons.png differ diff --git a/Pictures/web-ui/pages.png b/Pictures/web-ui/pages.png index 19570ba..b4d5084 100644 Binary files a/Pictures/web-ui/pages.png and b/Pictures/web-ui/pages.png differ diff --git a/Pictures/web-ui/schedule.png b/Pictures/web-ui/schedule.png index 03aea01..abc3294 100644 Binary files a/Pictures/web-ui/schedule.png and b/Pictures/web-ui/schedule.png differ diff --git a/Pictures/web-ui/settings.png b/Pictures/web-ui/settings.png index 9222e41..fb56546 100644 Binary files a/Pictures/web-ui/settings.png and b/Pictures/web-ui/settings.png differ diff --git a/Pictures/web-ui/show.png b/Pictures/web-ui/show.png index 4d945bf..c7ba454 100644 Binary files a/Pictures/web-ui/show.png and b/Pictures/web-ui/show.png differ diff --git a/Pictures/web-ui/stopwatch.png b/Pictures/web-ui/stopwatch.png index 3fa9eb8..4d41d07 100644 Binary files a/Pictures/web-ui/stopwatch.png and b/Pictures/web-ui/stopwatch.png differ diff --git a/Pictures/web-ui/system.png b/Pictures/web-ui/system.png index 71472d6..2c0285b 100644 Binary files a/Pictures/web-ui/system.png and b/Pictures/web-ui/system.png differ diff --git a/docs/home-assistant.md b/docs/home-assistant.md index 3cbfdbc..2fd8b3a 100644 --- a/docs/home-assistant.md +++ b/docs/home-assistant.md @@ -50,12 +50,45 @@ state, e.g. the 3D printer page while `sensor.mainsailos_current_print_state` is `printing`. Each rule is read at its own interval; when Home Assistant can't be reached it is read once a minute at most, so the display keeps running. +## Weather forecast + +Weather entities have no forecast attribute anymore (since Home Assistant 2024.3); the +forecast comes from the `weather.get_forecasts` action. The **Verwachting** page reads it +from a template sensor that keeps the next days as an attribute. Add to +`configuration.yaml` (and restart Home Assistant, or reload the template entities): + +```yaml +template: + - trigger: + - trigger: time_pattern + minutes: /30 + - trigger: homeassistant + event: start + action: + - action: weather.get_forecasts + data: { type: daily } + target: { entity_id: weather.knmi_home } + response_variable: f + sensor: + - name: Weer verwachting + unique_id: weer_verwachting + state: "{{ f['weather.knmi_home'].forecast[0].condition }}" + attributes: + forecast: "{{ f['weather.knmi_home'].forecast[:4] }}" +``` + +The page then reads `sensor.weer_verwachting` with attribute paths: `forecast.0` is today, +`forecast.1` tomorrow, `forecast.2` the day after, e.g. `forecast.1.condition` (chooses the icon `weer7 `, +7 x 7 dots), `forecast.1.temperature` (max) and `forecast.1.templow` (min). The weekdays +are Date elements with a day offset. Use another weather entity by replacing +`weather.knmi_home` in the YAML (and on the page, for today's icon and temperature). + ## Plex and Tautulli The **Plex** page (in [examples/home-pages.json](../examples/home-pages.json)) uses Home Assistant's Plex and Tautulli integrations, no extra firmware: -- `sensor.mangavalkserver` (Plex): how many are watching. +- `sensor.plex_server` (Plex, named after your server): how many are watching. - `sensor.{plex_user}_state` (Tautulli, per user): `playing`, `paused`, ... chooses the icon (icons `plex playing`, `plex paused`, `plex buffering`). - `sensor.{plex_user}_full_title` and `sensor.{plex_user}_progress` (Tautulli, per user): @@ -64,4 +97,4 @@ Assistant's Plex and Tautulli integrations, no extra firmware: `{plex_user}` is the Plex user set in the web UI (Home Assistant tab), written the way Home Assistant puts it in entity ids (`MangaValk` -> `mangavalk`). A rule (Schedule tab) adds -the page to the rotation while `sensor.mangavalkserver` is above 0. +the page to the rotation while `sensor.plex_server` is above 0. diff --git a/docs/pages.md b/docs/pages.md index cbc8300..29d747f 100644 --- a/docs/pages.md +++ b/docs/pages.md @@ -3,22 +3,28 @@ [← Back to the README](../README.md) 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; +tools/make_page_screenshots.py` makes these pictures again). The Clock page comes with the firmware (it can be changed, Reset brings back the original); 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). +with their icons (import them in the web UI, System tab). They use this home's Home +Assistant entities: change them to yours in the page editor (`climate.home`, +`sensor.envoy_000000000000_...` and `sensor.plex_server` are placeholders). +`python tools/export_examples.py` writes the file again from the pages on the display and +replaces private parts of entity ids by these placeholders. | Page | | |---|---| -| **Clock** (built in), day and from 17:00 | ![Clock](../Pictures/pages/clock-0000.png) ![Clock, evening](../Pictures/pages/clock-1700.png) | +| **Clock** (from the firmware), 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) | +| **Verwachting**: today (icon, temperature now, max/min) and the next 2 days (weekday, small icon, max/min); needs the [forecast sensor](home-assistant.md#weather-forecast) | ![Verwachting](../Pictures/pages/verwachting.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) | -| **3D printer finished** (not in the examples): shown for 10 minutes after a print completes (rule), with the status message | ![3D printer finished](../Pictures/pages/3d-printer-finished.png) | +| **3D printer finished**: shown for 10 minutes after a print completes (rule), with the status message | ![3D printer finished](../Pictures/pages/3d-printer-finished.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) | +| **Bad**: how full the bath is (a progress bar from a Home Assistant number) and the clock; a rule shows it while the bath fills | ![Bad](../Pictures/pages/bad.png) | | **Plex**: how many watch, play/pause, title (scrolls) and progress of the Plex user ([Tautulli](home-assistant.md#plex-and-tautulli)) | ![Plex](../Pictures/pages/plex.png) | diff --git a/docs/web-ui.md b/docs/web-ui.md index c26c9dc..0d53f33 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -23,7 +23,7 @@ Bitmaps that pages or rules use are in their own subsection. ## Pages Dynamic pages: a background per time of day, a bitmap or an [icon](#icons) (e.g. Pikachu -from 00:00, yawning Pikachu from 17:00) with up to 8 elements on top. The preview is drawn by the ESP32, so it +from 00:00, yawning Pikachu from 17:00) with up to 16 elements on top. The preview is drawn by the ESP32, so it is exactly what the display shows; drag the elements on it, or select one and use the arrow keys. The Clock page (Pikachu, the default) is a normal page that can be changed; its Reset button brings back the original. See the [pictures of the pages](pages.md). @@ -31,8 +31,8 @@ its Reset button brings back the original. See the [pictures of the pages](pages | Element | Options | |---------|---------| | Clock | `08:05`, `8:05`, optionally with seconds | -| Date | 7 formats, Dutch or English (`do 15 okt`, `15 oktober`, `donderdag`, ...) | -| Home Assistant value | An entity or attribute, text before and after. Shown as a number (multiplier, decimals, auto unit `1257 W` → `1.3 kW`, without minus sign), a duration (`2h 21m`, `2:21:00`, `2:21`, `2 uur`), a time from an HA timestamp (`23:07`, `do 23:07`, `15-10 23:07`, `over 34 min`) or with own texts per state (`printing=Bezig, standby=Klaar`) | +| Date | 7 formats, Dutch or English (`do 15 okt`, `15 oktober`, `donderdag`, ...), today or up to 7 days ahead (the weekdays of a forecast) | +| Home Assistant value | An entity or attribute (also a path into lists, e.g. `forecast.1.temperature`), text before and after. Shown as a number (multiplier, decimals, auto unit `1257 W` → `1.3 kW`, without minus sign), a duration (`2h 21m`, `2:21:00`, `2:21`, `2 uur`), a time from an HA timestamp (`23:07`, `do 23:07`, `15-10 23:07`, `over 34 min`) or with own texts per state (`printing=Bezig, standby=Klaar`) | | Text | Up to 32 characters | | Countdown | To a date and time: smart (`12 dagen`, `5 uur`, `34 min`, `0:45`), days, `12d 04:05`, with seconds; a text when it has passed | | Stopwatch | `4:05`, `0:04`, `0:04:05`; started in the [Stopwatch](#stopwatch) tab or Home Assistant | diff --git a/examples/home-pages.json b/examples/home-pages.json index beecccd..0efd07d 100644 --- a/examples/home-pages.json +++ b/examples/home-pages.json @@ -1,143 +1,611 @@ { "format": "flipdot-backup", "version": 1, - "firmware": "2.9.0", + "firmware": "2.13.0", "exported": "2026-10-16T00:00:00Z", - "bitmaps": [ + "bitmaps": [], + "icons": [ { - "name": "weer sunny", - "data": "0400000000000000000000444000000000000000000020800000000000000000000e000000000000000000001f00000000000000000000df600000000000000000001f000000000000000000000e0000000000000000000020800000000000000000004440000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer clear-night", - "data": "0e00000000000000000000380000000000000000000070000000000000000000006040000000000000000000e000000000000000000000e000000000000000000000e000000000000000000000700000000000000000000078400000000000000000003f800000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer partlycloudy", - "data": "24000000000000000000000e000000000000000000005f000000000000000000001e00000000000000000000b3800000000000000000003fe00000000000000000007fe0000000000000000000ffe0000000000000000000ffe00000000000000000007fc0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer cloudy", - "data": "0000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe00000000000000000007fc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer fog", - "data": "0000000000000000000000ffe000000000000000000000000000000000000000007fe00000000000000000000000000000000000000000ff8000000000000000000000000000000000000000003fe00000000000000000000000000000000000000000ff80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer rainy", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe00000000000000000007fc0000000000000000000000000000000000000000044400000000000000000008880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer pouring", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe00000000000000000007fc00000000000000000005540000000000000000000aa800000000000000000005540000000000000000000aa8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer lightning", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe000000000000000000006000000000000000000000c000000000000000000001f0000000000000000000006000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer lightning-rainy", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe000000000000000000046400000000000000000008c800000000000000000001f0000000000000000000046400000000000000000008c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer snowy", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe00000000000000000007fc0000000000000000000444000000000000000000011000000000000000000004440000000000000000000110000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer snowy-rainy", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe00000000000000000007fc0000000000000000000444000000000000000000088800000000000000000001100000000000000000000444000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer hail", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe00000000000000000007fc0000000000000000000666000000000000000000066600000000000000000000000000000000000000000330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer windy", - "data": "000000000000000000000003000000000000000000000080000000000000000000ff800000000000000000000000000000000000000000ffe00000000000000000000000000000000000000000fe0000000000000000000000800000000000000000000300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer windy-variant", - "data": "00000000000000000000000e0000000000000000000037800000000000000000007fc0000000000000000000ffe0000000000000000000ffe00000000000000000007fc00000000000000000000000000000000000000000ff8000000000000000000000000000000000000000003fe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer default", - "data": "04000000000000000000000a000000000000000000000a000000000000000000001500000000000000000000150000000000000000000024800000000000000000002480000000000000000000404000000000000000000044400000000000000000008020000000000000000000ffe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "weer exceptional", - "data": "04000000000000000000000a000000000000000000000a000000000000000000001500000000000000000000150000000000000000000024800000000000000000002480000000000000000000404000000000000000000044400000000000000000008020000000000000000000ffe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "bg energie", - "data": "06000000000000000000000c0000000000000000000018000000000000000000003e000000000000000000000c0000000000000000000018000000000000000000003000000000000000000000600000000000000000000000000000000000000000000000000000000000000000080000000000000000000041000000000000000000001c000000000000000000003e00000000000000000000be800000000000000000003e000000000000000000001c0000000000000000000041000000000000000000000800000000000000000000" - }, - { - "name": "bg printer", - "data": "ffe00000000000000000008020000000000000000000bfa00000000000000000008420000000000000000000842000000000000000000080200000000000000000008e200000000000000000009f20000000000000000000ffe00000000000000000008020000000000000000000ffe000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "name": "bg bad", + "width": 16, + "height": 19, + "data": "000000380044004000e0004000000040000000408001ffff8001aaa9400240023ffc20046006" }, { "name": "bg binnen", - "data": "04000000000000000000000e000000000000000000001b00000000000000000000318000000000000000000060c0000000000000000000c060000000000000000000404000000000000000000058400000000000000000005b400000000000000000005b400000000000000000007fc000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "bg verbruik", - "data": "080000000000000000000008000000000000000000001c000000000000000000001c000000000000000000003e000000000000000000007f000000000000000000007f000000000000000000007f000000000000000000003e000000000000000000001c00000000000000000000000000000000000000000006000000000000000000000c0000000000000000000018000000000000000000003e000000000000000000000c00000000000000000000180000000000000000000030000000000000000000006000000000000000000000" - }, - { - "name": "bg datum", - "data": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000004100000000000000000000ffe00000000000000000008020000000000000000000ffe0000000000000000000aaa00000000000000000008020000000000000000000aaa00000000000000000008020000000000000000000aa200000000000000000008020000000000000000000ffe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - }, - { - "name": "bg kerst", - "data": "00000000000000000000000000000000000000000000000000000000000000000004000000000000000000000e0000000000000000000004000000000000000000000e000000000000000000001f000000000000000000000e000000000000000000001f000000000000000000003f800000000000000000001f000000000000000000003f800000000000000000007fc000000000000000000004000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "width": 11, + "height": 11, + "data": "04000e001b00318060c0c060404058405b405b407fc0" }, { "name": "bg dag", - "data": "0002000000000000000000002220000000000000000000104000000000000000000007000000000000000000000f800000000000000000006fb00000000000000000000f800000000000000000000700000000000000000000104000000000000000000022200000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "width": 20, + "height": 11, + "data": "000200002220001040000700000f80006fb0000f80000700001040002220000200" }, { - "name": "bg nacht", - "data": "0007000000000000000000001c008000000000000000403800000000000000000000302000000000000000000070020000000000000000007000000000000000000000700000000000000000000038001000000000000000003c200000000000000000101fc00000000000000000000700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "name": "bg datum", + "width": 11, + "height": 15, + "data": "00000000000000004100ffe08020ffe0aaa08020aaa08020aa208020ffe0" + }, + { + "name": "bg energie", + "width": 9, + "height": 19, + "data": "06000c0018003e000c0018003000600000000000080041001c003e00be803e001c0041000800" }, { "name": "bg kader", + "width": 84, + "height": 19, "data": "f8000000000000000001f080000000000000000000108000000000000000000010800000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000001080000000000000000000108000000000000000000010f8000000000000000001f0" }, + { + "name": "bg kerst", + "width": 10, + "height": 16, + "data": "00000000000004000e0004000e001f000e001f003f801f003f807fc004000e00" + }, + { + "name": "bg nacht", + "width": 28, + "height": 11, + "data": "00070000001c0080403800000030200000700200007000000070000000380010003c2000101fc00000070000" + }, + { + "name": "bg printer", + "width": 11, + "height": 11, + "data": "ffe08020bfa08420842080208e209f20ffe08020ffe0" + }, + { + "name": "bg verbruik", + "width": 8, + "height": 19, + "data": "08081c1c3e7f7f7f3e1c00060c183e0c183060" + }, { "name": "bg vol", + "width": 84, + "height": 19, "data": "fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0fffffffffffffffffffff0" }, { - "name": "plex logo", - "data": "0000000000000000000000f800000000000000000000f8000000000000000000007c000000000000000000007c000000000000000000003e000000000000000000003e000000000000000000001f000000000000000000001f000000000000000000000f800000000000000000001f000000000000000000001f000000000000000000003e000000000000000000003e000000000000000000007c000000000000000000007c00000000000000000000f800000000000000000000f8000000000000000000000000000000000000000000" + "name": "plex buffering", + "width": 5, + "height": 5, + "data": "00000000a8" }, { - "name": "plex playing", - "data": "8000000000000000000000c000000000000000000000e000000000000000000000c000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "name": "plex logo", + "width": 9, + "height": 18, + "data": "0000f800f8007c007c003e003e001f001f000f801f001f003e003e007c007c00f800f800" }, { "name": "plex paused", - "data": "d800000000000000000000d800000000000000000000d800000000000000000000d800000000000000000000d80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "width": 5, + "height": 5, + "data": "d8d8d8d8d8" }, { - "name": "plex buffering", - "data": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "name": "plex playing", + "width": 3, + "height": 5, + "data": "80c0e0c080" + }, + { + "name": "weer clear-night", + "width": 10, + "height": 11, + "data": "0e00380070006040e000e000e000700078403f800e00" + }, + { + "name": "weer cloudy", + "width": 11, + "height": 9, + "data": "0000000000000e0037807fc0ffe0ffe07fc0" + }, + { + "name": "weer default", + "width": 11, + "height": 11, + "data": "04000a000a001500150024802480404044408020ffe0" + }, + { + "name": "weer exceptional", + "width": 11, + "height": 11, + "data": "04000a000a001500150024802480404044408020ffe0" + }, + { + "name": "weer fog", + "width": 11, + "height": 10, + "data": "0000ffe000007fe00000ff8000003fe00000ff80" + }, + { + "name": "weer hail", + "width": 11, + "height": 11, + "data": "00000e0037807fc0ffe0ffe07fc06660666000003300" + }, + { + "name": "weer lightning", + "width": 11, + "height": 11, + "data": "00000e0037807fc0ffe0ffe006000c001f0006000c00" + }, + { + "name": "weer lightning-rainy", + "width": 11, + "height": 11, + "data": "00000e0037807fc0ffe0ffe046408c801f0046408c80" + }, + { + "name": "weer partlycloudy", + "width": 11, + "height": 10, + "data": "24000e005f001e00b3803fe07fe0ffe0ffe07fc0" + }, + { + "name": "weer pouring", + "width": 11, + "height": 11, + "data": "00000e0037807fc0ffe0ffe07fc05540aa805540aa80" + }, + { + "name": "weer rainy", + "width": 11, + "height": 10, + "data": "00000e0037807fc0ffe0ffe07fc0000044408880" + }, + { + "name": "weer snowy", + "width": 11, + "height": 11, + "data": "00000e0037807fc0ffe0ffe07fc04440110044401100" + }, + { + "name": "weer snowy-rainy", + "width": 11, + "height": 11, + "data": "00000e0037807fc0ffe0ffe07fc04440888011004440" + }, + { + "name": "weer sunny", + "width": 11, + "height": 11, + "data": "0400444020800e001f00df601f000e00208044400400" + }, + { + "name": "weer windy", + "width": 11, + "height": 10, + "data": "000003000080ff800000ffe00000fe0000800300" + }, + { + "name": "weer windy-variant", + "width": 11, + "height": 11, + "data": "00000e0037807fc0ffe0ffe07fc00000ff8000003fe0" + }, + { + "name": "weer7 clear-night", + "width": 7, + "height": 7, + "data": "3860c0c0c06038" + }, + { + "name": "weer7 cloudy", + "width": 7, + "height": 7, + "data": "00187cfefe0000" + }, + { + "name": "weer7 default", + "width": 7, + "height": 7, + "data": "38440418100010" + }, + { + "name": "weer7 exceptional", + "width": 7, + "height": 7, + "data": "10383810100010" + }, + { + "name": "weer7 fog", + "width": 7, + "height": 7, + "data": "00fe007e00fc00" + }, + { + "name": "weer7 hail", + "width": 7, + "height": 7, + "data": "3078fe006c6c00" + }, + { + "name": "weer7 lightning", + "width": 7, + "height": 7, + "data": "1c38703c183020" + }, + { + "name": "weer7 lightning-rainy", + "width": 7, + "height": 7, + "data": "3078fe18b238a2" + }, + { + "name": "weer7 partlycloudy", + "width": 7, + "height": 7, + "data": "a040ac1e7efe00" + }, + { + "name": "weer7 pouring", + "width": 7, + "height": 7, + "data": "3078fe00aa54aa" + }, + { + "name": "weer7 rainy", + "width": 7, + "height": 7, + "data": "3078fe00489200" + }, + { + "name": "weer7 snowy", + "width": 7, + "height": 7, + "data": "3078fe00920048" + }, + { + "name": "weer7 snowy-rainy", + "width": 7, + "height": 7, + "data": "3078fe00922448" + }, + { + "name": "weer7 sunny", + "width": 7, + "height": 7, + "data": "924438ba384492" + }, + { + "name": "weer7 windy", + "width": 7, + "height": 7, + "data": "0cfc00f804e800" + }, + { + "name": "weer7 windy-variant", + "width": 7, + "height": 7, + "data": "3078fe00f8007e" } ], "pages": [ { - "name": "Weer", + "name": "3D printer finished", "page": { - "slots": [], + "slots": [ + { + "start": "00:00", + "bitmap": "bg printer" + } + ], + "elements": [ + { + "type": "ha", + "x": 1, + "y": 13, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "sensor.mainsailos_current_display_message", + "attribute": "", + "factor": 1, + "scroll_width": 66, + "scroll_speed": 1, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 83, + "y": 13, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "°", + "entity": "sensor.mainsailos_bed_core_temp", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "text", + "x": 12, + "y": 2, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "Print finished", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 71, + "scroll_speed": 0.25, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + } + ] + } + }, + { + "name": "3D printer", + "page": { + "slots": [ + { + "start": "00:00", + "bitmap": "bg printer" + } + ], + "elements": [ + { + "type": "ha", + "x": 14, + "y": 2, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "%", + "entity": "sensor.mainsailos_progress", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 82, + "y": 2, + "align": "right", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "en", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "sensor.mainsailos_print_time_left", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "duration", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 1, + "y": 13, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "sensor.mainsailos_current_display_message", + "attribute": "", + "factor": 1, + "scroll_width": 66, + "scroll_speed": 0.25, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 83, + "y": 13, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "°", + "entity": "sensor.mainsailos_extruder_temperature", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + } + ] + } + }, + { + "name": "Aftellen", + "page": { + "slots": [ + { + "start": "00:00", + "bitmap": "bg kerst" + } + ], + "elements": [ + { + "type": "text", + "x": 14, + "y": 1, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "Kerst over", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "countdown", + "x": 14, + "y": 10, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "Vrolijk kerst!", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 70, + "scroll_speed": 8, + "target": "2026-12-25T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + } + ] + } + }, + { + "name": "Bad", + "page": { + "slots": [ + { + "start": "00:00", + "bitmap": "bg bad" + } + ], "elements": [ { "type": "clock", - "x": 0, - "y": 0, - "align": "left", + "x": 51, + "y": 1, + "align": "center", "overlay": "top", - "font": "Full height", + "font": "Normal", "format": 0, "language": "nl", "decimals": -1, @@ -149,14 +617,91 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "bar", + "x": 19, + "y": 11, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "input_number.bath_sensor_nodered", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 65, + "bar_height": 8, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + } + ] + } + }, + { + "name": "Binnen", + "page": { + "slots": [ + { + "start": "00:00", + "bitmap": "bg binnen" + } + ], + "elements": [ + { + "type": "ha", + "x": 14, + "y": 2, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": 1, + "text": "", + "suffix": "°", + "entity": "climate.home", + "attribute": "current_temperature", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 }, { "type": "image", - "x": 55, + "x": 49, "y": 0, "align": "left", "overlay": "top", @@ -172,10 +717,16 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 }, { "type": "ha", @@ -186,19 +737,54 @@ "font": "Small", "format": 0, "language": "nl", - "decimals": 0, + "decimals": 1, "text": "", - "suffix": "%", + "suffix": "°", "entity": "weather.knmi_home", - "attribute": "humidity", + "attribute": "temperature", "factor": 1, "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 0, + "y": 13, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "CO2 ", + "suffix": "ppm", + "entity": "sensor.eco2", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 }, { "type": "ha", @@ -209,19 +795,25 @@ "font": "Small", "format": 0, "language": "nl", - "decimals": 1, - "text": "", - "suffix": "°C", - "entity": "weather.knmi_home", - "attribute": "temperature", + "decimals": -1, + "text": "lucht ", + "suffix": "", + "entity": "sensor.air_quality_index_aqi_2", + "attribute": "", "factor": 1, "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 } ] } @@ -258,10 +850,16 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 }, { "type": "date", @@ -281,10 +879,116 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 + } + ] + } + }, + { + "name": "Datum", + "page": { + "slots": [ + { + "start": "00:00", + "bitmap": "bg datum" + } + ], + "elements": [ + { + "type": "date", + "x": 14, + "y": 1, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 5, + "language": "en", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "date", + "x": 14, + "y": 12, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 2, + "language": "en", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "clock", + "x": 82, + "y": 12, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 } ] } @@ -317,10 +1021,16 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 }, { "type": "ha", @@ -340,10 +1050,16 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 }, { "type": "clock", @@ -363,348 +1079,16 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true - } - ] - } - }, - { - "name": "3D printer", - "page": { - "slots": [ - { - "start": "00:00", - "bitmap": "bg printer" - } - ], - "elements": [ - { - "type": "ha", - "x": 14, - "y": 0, - "align": "left", - "overlay": "top", - "font": "Normal", - "format": 0, - "language": "nl", - "decimals": 0, - "text": "", - "suffix": "%", - "entity": "sensor.mainsailos_progress", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 84, - "y": 0, - "align": "right", - "overlay": "top", - "font": "Normal", - "format": 0, - "language": "nl", - "decimals": 0, - "text": "", - "suffix": "m", - "entity": "sensor.mainsailos_print_time_left", - "attribute": "", - "factor": 0.016666666666666666, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 14, - "y": 13, - "align": "left", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": -1, - "text": "", - "suffix": "", - "entity": "sensor.mainsailos_current_display_message", - "attribute": "", - "factor": 1, - "scroll_width": 46, - "scroll_speed": 6, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 84, - "y": 13, - "align": "right", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": 0, - "text": "", - "suffix": "°", - "entity": "sensor.mainsailos_extruder_temperature", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - } - ] - } - }, - { - "name": "Binnen", - "page": { - "slots": [ - { - "start": "00:00", - "bitmap": "bg binnen" - } - ], - "elements": [ - { - "type": "ha", - "x": 14, - "y": 2, - "align": "left", - "overlay": "top", - "font": "Normal", - "format": 0, - "language": "nl", - "decimals": 1, - "text": "", - "suffix": "°", - "entity": "climate.home", - "attribute": "current_temperature", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "image", - "x": 49, - "y": 0, - "align": "left", - "overlay": "top", - "font": "Normal", - "format": 0, - "language": "nl", - "decimals": -1, - "text": "weer ", - "suffix": "", - "entity": "weather.knmi_home", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 84, - "y": 2, - "align": "right", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": 1, - "text": "", - "suffix": "°", - "entity": "weather.knmi_home", - "attribute": "temperature", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 0, - "y": 13, - "align": "left", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": 0, - "text": "CO2 ", - "suffix": "ppm", - "entity": "sensor.eco2", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 84, - "y": 13, - "align": "right", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": -1, - "text": "lucht ", - "suffix": "", - "entity": "sensor.air_quality_index_aqi_2", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - } - ] - } - }, - { - "name": "Verbruik", - "page": { - "slots": [ - { - "start": "00:00", - "bitmap": "bg verbruik" - } - ], - "elements": [ - { - "type": "ha", - "x": 10, - "y": 1, - "align": "left", - "overlay": "top", - "font": "Normal", - "format": 0, - "language": "nl", - "decimals": 0, - "text": "", - "suffix": " l", - "entity": "sensor.util_water_usage_daily", - "attribute": "", - "factor": 1000, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 10, - "y": 11, - "align": "left", - "overlay": "top", - "font": "Normal", - "format": 0, - "language": "nl", - "decimals": 1, - "text": "", - "suffix": " kWh", - "entity": "sensor.envoy_000000000000_today_s_energy_consumption", - "attribute": "", - "factor": 0.001, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "clock", - "x": 84, - "y": 0, - "align": "right", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": -1, - "text": "", - "suffix": "", - "entity": "", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "ha", - "x": 84, - "y": 13, - "align": "right", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": 1, - "text": "zon ", - "suffix": "kWh", - "entity": "sensor.envoy_000000000000_today_s_energy_production", - "attribute": "", - "factor": 0.001, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 } ] } @@ -741,133 +1125,34 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 } ] } }, { - "name": "Datum", + "name": "Plex", "page": { "slots": [ { "start": "00:00", - "bitmap": "bg datum" + "bitmap": "plex logo" } ], "elements": [ { - "type": "date", - "x": 14, - "y": 1, - "align": "left", - "overlay": "top", - "font": "Normal", - "format": 5, - "language": "nl", - "decimals": -1, - "text": "", - "suffix": "", - "entity": "", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "date", - "x": 14, - "y": 12, - "align": "left", - "overlay": "top", - "font": "Small", - "format": 2, - "language": "nl", - "decimals": -1, - "text": "", - "suffix": "", - "entity": "", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "clock", - "x": 84, - "y": 12, - "align": "right", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": -1, - "text": "", - "suffix": "", - "entity": "", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - } - ] - } - }, - { - "name": "Aftellen", - "page": { - "slots": [ - { - "start": "00:00", - "bitmap": "bg kerst" - } - ], - "elements": [ - { - "type": "text", - "x": 14, - "y": 1, - "align": "left", - "overlay": "top", - "font": "Small", - "format": 0, - "language": "nl", - "decimals": -1, - "text": "Kerst over", - "suffix": "", - "entity": "", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true - }, - { - "type": "countdown", - "x": 14, - "y": 10, + "type": "ha", + "x": 13, + "y": 0, "align": "left", "overlay": "top", "font": "Normal", @@ -875,17 +1160,110 @@ "language": "nl", "decimals": -1, "text": "", - "suffix": "Vrolijk kerst!", - "entity": "", + "suffix": " watching", + "entity": "sensor.plex_server", "attribute": "", "factor": 1, - "scroll_width": 70, + "scroll_width": 0, "scroll_speed": 8, - "target": "2026-12-25T00:00", + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "image", + "x": 13, + "y": 9, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "plex ", + "suffix": "", + "entity": "sensor.{plex_user}_state", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 19, + "y": 9, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "sensor.{plex_user}_full_title", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 0.1, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "bar", + "x": 13, + "y": 16, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "sensor.{plex_user}_progress", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 71, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 } ] } @@ -913,119 +1291,629 @@ "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 } ] } }, { - "name": "Plex", + "name": "Verbruik", "page": { "slots": [ { "start": "00:00", - "bitmap": "plex logo" + "bitmap": "bg verbruik" } ], "elements": [ { "type": "ha", - "x": 13, - "y": 0, + "x": 10, + "y": 1, "align": "left", "overlay": "top", "font": "Normal", "format": 0, "language": "nl", - "decimals": -1, + "decimals": 0, "text": "", - "suffix": " watching", - "entity": "sensor.mangavalkserver", + "suffix": " l", + "entity": "sensor.util_water_usage_daily", "attribute": "", - "factor": 1, + "factor": 1000, "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true - }, - { - "type": "image", - "x": 13, - "y": 9, - "align": "left", - "overlay": "top", - "font": "Normal", - "format": 0, - "language": "nl", - "decimals": -1, - "text": "plex ", - "suffix": "", - "entity": "sensor.{plex_user}_state", - "attribute": "", - "factor": 1, - "scroll_width": 0, - "scroll_speed": 8, - "target": "2000-01-01T00:00", - "bar_width": 40, - "bar_height": 3, - "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 }, { "type": "ha", - "x": 19, - "y": 9, + "x": 10, + "y": 11, "align": "left", "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": 1, + "text": "", + "suffix": " kWh", + "entity": "sensor.envoy_000000000000_today_s_energy_consumption", + "attribute": "", + "factor": 0.001, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "clock", + "x": 84, + "y": 0, + "align": "right", + "overlay": "top", "font": "Small", "format": 0, "language": "nl", "decimals": -1, "text": "", "suffix": "", - "entity": "sensor.{plex_user}_full_title", + "entity": "", "attribute": "", "factor": 1, - "scroll_width": 65, + "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 }, { - "type": "bar", + "type": "ha", + "x": 84, + "y": 13, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 1, + "text": "zon ", + "suffix": "kWh", + "entity": "sensor.envoy_000000000000_today_s_energy_production", + "attribute": "", + "factor": 0.001, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + } + ] + } + }, + { + "name": "Verwachting", + "page": { + "slots": [], + "elements": [ + { + "type": "image", + "x": 0, + "y": 0, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "weer ", + "suffix": "", + "entity": "weather.knmi_home", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", "x": 13, - "y": 16, + "y": 2, + "align": "left", + "overlay": "top", + "font": "Normal", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "°", + "entity": "weather.knmi_home", + "attribute": "temperature", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 14, + "y": 13, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "°", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.0.temperature", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 14, + "y": 13, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "/", + "suffix": "°", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.0.templow", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "date", + "x": 48, + "y": 0, + "align": "center", + "overlay": "top", + "font": "Small", + "format": 6, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 1 + }, + { + "type": "image", + "x": 44, + "y": 6, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "weer7 ", + "suffix": "", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.1.condition", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 47, + "y": 14, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.1.temperature", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 47, + "y": 14, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "/", + "suffix": "", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.1.templow", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "date", + "x": 72, + "y": 0, + "align": "center", + "overlay": "top", + "font": "Small", + "format": 6, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 2 + }, + { + "type": "image", + "x": 68, + "y": 6, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "weer7 ", + "suffix": "", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.2.condition", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 71, + "y": 14, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.2.temperature", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 71, + "y": 14, + "align": "left", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "/", + "suffix": "", + "entity": "sensor.weer_verwachting", + "attribute": "forecast.2.templow", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + } + ] + } + }, + { + "name": "Weer", + "page": { + "slots": [], + "elements": [ + { + "type": "clock", + "x": 0, + "y": 0, + "align": "left", + "overlay": "top", + "font": "Full height", + "format": 0, + "language": "nl", + "decimals": -1, + "text": "", + "suffix": "", + "entity": "", + "attribute": "", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "image", + "x": 55, + "y": 0, "align": "left", "overlay": "top", "font": "Normal", "format": 0, "language": "nl", "decimals": -1, - "text": "", + "text": "weer ", "suffix": "", - "entity": "sensor.{plex_user}_progress", + "entity": "weather.knmi_home", "attribute": "", "factor": 1, "scroll_width": 0, "scroll_speed": 8, "target": "2000-01-01T00:00", - "bar_width": 71, + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, "bar_height": 3, "bar_max": 100, - "bar_frame": true + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 84, + "y": 2, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 0, + "text": "", + "suffix": "%", + "entity": "weather.knmi_home", + "attribute": "humidity", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 + }, + { + "type": "ha", + "x": 84, + "y": 13, + "align": "right", + "overlay": "top", + "font": "Small", + "format": 0, + "language": "nl", + "decimals": 1, + "text": "", + "suffix": "°C", + "entity": "weather.knmi_home", + "attribute": "temperature", + "factor": 1, + "scroll_width": 0, + "scroll_speed": 8, + "target": "2000-01-01T00:00", + "show": "number", + "input_unit": "s", + "auto_unit": false, + "absolute": false, + "map": "", + "bar_width": 40, + "bar_height": 3, + "bar_max": 100, + "bar_frame": true, + "day_offset": 0 } ] } } ], "fonts": [] -} \ No newline at end of file +} diff --git a/src/esp32/config.h b/src/esp32/config.h index 3747325..db00a5d 100644 --- a/src/esp32/config.h +++ b/src/esp32/config.h @@ -118,7 +118,7 @@ const uint32_t SETUP_AP_DELAY = 20000; // open the AP when WiFi fails thi const uint32_t WIFI_CONNECT_TIMEOUT = 30000; const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000; -#define FIRMWARE_VERSION "2.12.0" +#define FIRMWARE_VERSION "2.13.0" // Home Assistant device id (do not change: Home Assistant's entities are linked to it) const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28}; diff --git a/src/esp32/display/page_store.cpp b/src/esp32/display/page_store.cpp index 0ebbce8..083dc14 100644 --- a/src/esp32/display/page_store.cpp +++ b/src/esp32/display/page_store.cpp @@ -8,7 +8,9 @@ static const char *DIR = "/pages"; static const char *EXTENSION = ".pg"; // Increase when DynamicPage changes; older files are ignored unless converted below -static const uint16_t VERSION = 5; +static const uint16_t VERSION = 6; +// Elements per page up to version 5 (the old formats below are frozen with it) +static const int OLD_PAGE_ELEMENTS = 8; struct StoredPage { @@ -42,7 +44,7 @@ struct Page uint8_t slot_count; PageSlot slots[PAGE_SLOTS]; uint8_t element_count; - Element elements[PAGE_ELEMENTS]; + Element elements[OLD_PAGE_ELEMENTS]; }; struct Stored @@ -54,11 +56,11 @@ struct Stored static void convert(const Page &old, DynamicPage &page) { - page = DynamicPage(); + clearPage(page); page.slot_count = old.slot_count; memcpy(page.slots, old.slots, sizeof(page.slots)); page.element_count = old.element_count; - for (int i = 0; i < PAGE_ELEMENTS; i++) + for (int i = 0; i < OLD_PAGE_ELEMENTS; i++) { const Element &o = old.elements[i]; PageElement &e = page.elements[i]; @@ -108,7 +110,7 @@ struct Page uint8_t slot_count; PageSlot slots[PAGE_SLOTS]; uint8_t element_count; - Element elements[PAGE_ELEMENTS]; + Element elements[OLD_PAGE_ELEMENTS]; }; struct Stored @@ -120,11 +122,11 @@ struct Stored static void convert(const Page &old, DynamicPage &page) { - page = DynamicPage(); + clearPage(page); page.slot_count = old.slot_count; memcpy(page.slots, old.slots, sizeof(page.slots)); page.element_count = old.element_count; - for (int i = 0; i < PAGE_ELEMENTS; i++) + for (int i = 0; i < OLD_PAGE_ELEMENTS; i++) { const Element &o = old.elements[i]; PageElement &e = page.elements[i]; @@ -187,7 +189,7 @@ struct Page uint8_t slot_count; PageSlot slots[PAGE_SLOTS]; uint8_t element_count; - Element elements[PAGE_ELEMENTS]; + Element elements[OLD_PAGE_ELEMENTS]; }; struct Stored @@ -205,11 +207,11 @@ static_assert(offsetof(PageElement, scroll_speed) >= BEFORE_SPEED && static void convert(const Page &old, DynamicPage &page) { - page = DynamicPage(); + clearPage(page); page.slot_count = old.slot_count; memcpy(page.slots, old.slots, sizeof(page.slots)); page.element_count = old.element_count; - for (int i = 0; i < PAGE_ELEMENTS; i++) + for (int i = 0; i < OLD_PAGE_ELEMENTS; i++) { const Element &o = old.elements[i]; PageElement &e = page.elements[i]; @@ -264,7 +266,7 @@ struct Page uint8_t slot_count; PageSlot slots[PAGE_SLOTS]; uint8_t element_count; - Element elements[PAGE_ELEMENTS]; + Element elements[OLD_PAGE_ELEMENTS]; }; struct Stored @@ -283,15 +285,80 @@ static void convert(const Page &old, DynamicPage &page) mid->slot_count = old.slot_count; memcpy(mid->slots, old.slots, sizeof(mid->slots)); mid->element_count = old.element_count; - for (int i = 0; i < PAGE_ELEMENTS; i++) + for (int i = 0; i < OLD_PAGE_ELEMENTS; i++) memcpy((void *)&mid->elements[i], &old.elements[i], COMMON); v4::convert(*mid, page); delete mid; } } // namespace v3 +// Version 5 (before 16 elements and the day offset of dates): 8 elements of the current +// layout without day_offset. Frozen copy, the elements are copied as a block. +namespace v5 +{ +struct Element +{ + ElementType type; + int16_t x, y; + Align align; + Overlay overlay; + char font[PAGE_NAME_SIZE]; + uint8_t format; + Language language; + int8_t decimals; + char text[ELEMENT_TEXT_SIZE]; + char suffix[ELEMENT_SUFFIX_SIZE]; + char entity[ELEMENT_ENTITY_SIZE]; + char attribute[ELEMENT_ATTRIBUTE_SIZE]; + float factor; + uint8_t scroll_width; + float scroll_speed; + uint32_t target; + HaShow show; + uint8_t input_unit; + bool auto_unit; + bool absolute; + char map[ELEMENT_MAP_SIZE]; + uint8_t bar_width; + uint8_t bar_height; + float bar_max; + bool bar_frame; +}; + +struct Page +{ + uint8_t slot_count; + PageSlot slots[PAGE_SLOTS]; + uint8_t element_count; + Element elements[OLD_PAGE_ELEMENTS]; +}; + +struct Stored +{ + uint16_t version; + uint16_t size; + Page page; +}; + +static const size_t COMMON = offsetof(Element, bar_frame) + sizeof(Element::bar_frame); +static_assert(offsetof(PageElement, bar_frame) == offsetof(Element, bar_frame) && + offsetof(PageElement, map) == offsetof(Element, map), + "same layout up to bar_frame"); + +static void convert(const Page &old, DynamicPage &page) +{ + clearPage(page); // day_offset 0 + page.slot_count = old.slot_count; + memcpy(page.slots, old.slots, sizeof(page.slots)); + page.element_count = old.element_count; + for (int i = 0; i < OLD_PAGE_ELEMENTS; i++) + memcpy((void *)&page.elements[i], &old.elements[i], COMMON); +} +} // namespace v5 + static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(v3::Stored) && - sizeof(v3::Stored) != sizeof(v4::Stored) && sizeof(v4::Stored) != sizeof(StoredPage), + sizeof(v3::Stored) != sizeof(v4::Stored) && sizeof(v4::Stored) != sizeof(v5::Stored) && + sizeof(v5::Stored) != sizeof(StoredPage), "the versions are told apart by their size"); static uint32_t revision = 0; @@ -362,6 +429,18 @@ bool loadPage(const char *name, DynamicPage &page) page = stored->page; delete stored; } + else if (size == sizeof(v5::Stored)) + { + v5::Stored *stored = new v5::Stored; + ok = file.read((uint8_t *)stored, sizeof(v5::Stored)) == sizeof(v5::Stored) && stored->version == 5 && + stored->size == sizeof(v5::Page); + if (ok) + { + v5::convert(stored->page, page); + ok = validatePage(page) == nullptr; + } + delete stored; + } else if (size == sizeof(v4::Stored)) { v4::Stored *stored = new v4::Stored; diff --git a/src/esp32/logic/builtin_pages.cpp b/src/esp32/logic/builtin_pages.cpp index 5bc0449..bdc757a 100644 --- a/src/esp32/logic/builtin_pages.cpp +++ b/src/esp32/logic/builtin_pages.cpp @@ -26,7 +26,7 @@ const char *builtinPageName(const char *name) static void clockPage(DynamicPage &page) { - page = DynamicPage(); + clearPage(page); page.slot_count = 2; page.slots[0].start = 0; strcpy(page.slots[0].bitmap, "Pikachu"); diff --git a/src/esp32/logic/page.cpp b/src/esp32/logic/page.cpp index 07e4deb..ba1e1b6 100644 --- a/src/esp32/logic/page.cpp +++ b/src/esp32/logic/page.cpp @@ -127,7 +127,7 @@ static bool strncasecmpAscii(const char *a, const char *b, size_t n) static bool validAttribute(const char *attribute) { for (const char *c = attribute; *c; c++) - if (!isalnum((unsigned char)*c) && *c != '_') + if (!isalnum((unsigned char)*c) && *c != '_' && *c != '.') // dots: a path, e.g. forecast.1.condition return false; return true; } @@ -196,6 +196,8 @@ const char *validatePage(const DynamicPage &page) return "The list of texts can only contain plain characters, up to 63"; if (e.format >= formatCount(e)) return "Unknown format"; + if (e.day_offset < 0 || e.day_offset > DAY_OFFSET_MAX) + return "Date: 0-7 days after today"; if (!isfinite(e.factor) || e.factor == 0) return "The multiplier must be a number other than 0"; if (e.scroll_width > 2 * BITMAP_WIDTH || @@ -368,8 +370,37 @@ static int language(const PageElement &e) return e.language < Language::Count ? (int)e.language : 0; } -static void formatDate(const PageElement &e, const LocalTime &t, char *out, size_t size) +static int daysInMonth(int year, int month) { + static const int DAYS[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + bool leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; + return month == 2 && leap ? 29 : DAYS[(month - 1 + 12) % 12]; +} + +LocalTime addDays(const LocalTime &time, int days) +{ + LocalTime t = time; + if (t.month < 1 || t.month > 12) + return t; + for (int i = 0; i < days; i++) + { + t.weekday = (t.weekday + 1) % 7; + if (++t.day > daysInMonth(t.year, t.month)) + { + t.day = 1; + if (++t.month > 12) + { + t.month = 1; + t.year++; + } + } + } + return t; +} + +static void formatDate(const PageElement &e, const LocalTime &today, char *out, size_t size) +{ + LocalTime t = addDays(today, e.day_offset); int lang = language(e); int wd = t.weekday >= 0 && t.weekday < 7 ? t.weekday : 0; int mo = t.month >= 1 && t.month <= 12 ? t.month - 1 : 0; diff --git a/src/esp32/logic/page.h b/src/esp32/logic/page.h index 9270869..c12edc0 100644 --- a/src/esp32/logic/page.h +++ b/src/esp32/logic/page.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -16,7 +17,7 @@ const int PAGE_NAME_SIZE = BITMAP_NAME_MAX + 1; const int PAGE_SLOTS = 8; -const int PAGE_ELEMENTS = 8; +const int PAGE_ELEMENTS = 16; const int ELEMENT_TEXT_SIZE = 33; const int ELEMENT_SUFFIX_SIZE = 17; const int ELEMENT_ENTITY_SIZE = 65; @@ -78,6 +79,8 @@ const uint8_t DATE_NUMERIC_YEAR = 4; // 15-10-2026 const uint8_t DATE_WEEKDAY = 5; // donderdag / Thursday const uint8_t DATE_WEEKDAY_SHORT = 6; // do / Thu const uint8_t DATE_FORMAT_COUNT = 7; +// Date elements can show a later day (a forecast's weekday) +const int8_t DAY_OFFSET_MAX = 7; // Countdown formats const uint8_t COUNTDOWN_SMART = 0; // 12 dagen, 5 uur, 34 min, then 0:45 @@ -171,6 +174,8 @@ struct PageElement uint8_t bar_height = 3; float bar_max = 100; // Bar: the value (after the multiplier) that fills it bool bar_frame = true; // Bar: a line around it, filled inside + // Added in page format 6 + int8_t day_offset = 0; // Date: days after today (0-DAY_OFFSET_MAX), e.g. 1: tomorrow }; struct DynamicPage @@ -181,6 +186,13 @@ struct DynamicPage PageElement elements[PAGE_ELEMENTS]; }; +// Back to an empty page with the defaults, without `page = DynamicPage()`: that can put a +// copy (about 4 KB) on the stack +inline void clearPage(DynamicPage &page) +{ + new (&page) DynamicPage(); +} + // Returns nullptr when the page is fine, otherwise a message for the user. Does not check // that the bitmaps and fonts exist. const char *validatePage(const DynamicPage &page); @@ -241,5 +253,8 @@ bool parseTimestamp(const char *text, int64_t &seconds); bool parseTarget(const char *text, uint32_t &minutes); // out must hold 17 characters void formatTarget(uint32_t minutes, char *out); +// The date `days` (0 or more) after this one, with the weekday; the time stays +LocalTime addDays(const LocalTime &time, int days); + // The local time in minutes since 2000-01-01 00:00 (seconds left out) uint32_t minutesSince2000(const LocalTime &time); diff --git a/src/esp32/net/ha_api.cpp b/src/esp32/net/ha_api.cpp index c3cfa47..a21db46 100644 --- a/src/esp32/net/ha_api.cpp +++ b/src/esp32/net/ha_api.cpp @@ -57,7 +57,7 @@ static String token; static char plex_user[PLEX_USER_SIZE] = ""; static std::vector entries; // Kept for the editor's previews, besides the watched ones -static const size_t MAX_ENTRIES = 24; +static const size_t MAX_ENTRIES = 32; static QueueHandle_t requests = nullptr; static QueueHandle_t responses = nullptr; @@ -74,6 +74,46 @@ static uint32_t preview_done_at = 0; // The task // --------------------------------------------------------------------------- +// Attributes can be a path into lists and objects: "forecast.1.condition" is the condition +// of the second forecast. One part of it (up to the next dot) goes into `part`; returns the +// rest, nullptr after the last part. +static const char *nextPart(const char *path, char *part, size_t size) +{ + size_t n = strcspn(path, "."); + strlcpy(part, path, n + 1 < size ? n + 1 : size); + return path[n] == '.' ? path + n + 1 : nullptr; +} + +static bool isIndex(const char *part) +{ + if (!part[0]) + return false; + for (const char *c = part; *c; c++) + if (!isdigit((unsigned char)*c)) + return false; + return true; +} + +// Only the path is kept from the answer. List indexes become [0]: ArduinoJson applies the +// first element of a filter list to all elements. +static void addFilter(JsonVariant f, const char *path) +{ + char part[ELEMENT_ATTRIBUTE_SIZE]; + const char *rest = nextPart(path, part, sizeof(part)); + if (isIndex(part)) + { + JsonVariant element = f.to().add(); + if (rest) + addFilter(element, rest); + else + element.set(true); + } + else if (rest) + addFilter(f[(const char *)part].to(), rest); // char arrays would be taken as literals + else + f[(const char *)part] = true; +} + // Blocks at most about 2 x HA_HTTP_TIMEOUT (connect, then the answer) static const char *fetch(const Request &r, char *value, size_t value_size) { @@ -99,7 +139,7 @@ static const char *fetch(const Request &r, char *value, size_t value_size) JsonDocument filter; if (r.attribute[0]) - filter["attributes"][(const char *)r.attribute] = true; // a char array would be taken as a literal of 32 characters + addFilter(filter["attributes"].to(), r.attribute); else filter["state"] = true; JsonDocument doc; @@ -110,7 +150,15 @@ static const char *fetch(const Request &r, char *value, size_t value_size) JsonVariantConst v = doc["state"]; if (r.attribute[0]) - v = doc["attributes"][(const char *)r.attribute]; + { + v = doc["attributes"]; + char part[ELEMENT_ATTRIBUTE_SIZE]; + for (const char *path = r.attribute; path && !v.isNull();) + { + path = nextPart(path, part, sizeof(part)); + v = isIndex(part) ? v[(size_t)atoi(part)] : v[(const char *)part]; + } + } if (v.isNull()) return "Unknown attribute"; String text; diff --git a/src/esp32/web/page_json.cpp b/src/esp32/web/page_json.cpp index b7b603f..e2e05df 100644 --- a/src/esp32/web/page_json.cpp +++ b/src/esp32/web/page_json.cpp @@ -73,6 +73,7 @@ void pageToJson(const DynamicPage &page, JsonObject out) o["bar_height"] = e.bar_height; o["bar_max"] = e.bar_max; o["bar_frame"] = e.bar_frame; + o["day_offset"] = e.day_offset; } } @@ -89,7 +90,7 @@ static bool copyText(JsonVariantConst value, char *out, size_t size) const char *pageFromJson(JsonVariantConst in, DynamicPage &page) { - page = DynamicPage(); + clearPage(page); JsonArrayConst slots = in["slots"]; JsonArrayConst elements = in["elements"]; if (slots.size() > (size_t)PAGE_SLOTS) @@ -136,6 +137,7 @@ const char *pageFromJson(JsonVariantConst in, DynamicPage &page) e.bar_height = constrain(o["bar_height"] | 3, 0, 255); e.bar_max = o["bar_max"] | 100.0f; e.bar_frame = o["bar_frame"] | true; + e.day_offset = constrain(o["day_offset"] | 0, -1, DAY_OFFSET_MAX + 1); // out of range: validatePage says so if (!copyText(o["map"], e.map, sizeof(e.map))) return "The list of texts is too long (63 characters)"; const char *target = o["target"] | ""; diff --git a/src/esp32/web/web_page.h b/src/esp32/web/web_page.h index 783e5cf..fa8f4d2 100644 --- a/src/esp32/web/web_page.h +++ b/src/esp32/web/web_page.h @@ -362,7 +362,7 @@ let pe=null,peTimer=null,peSeq=0,peData=null,peDrag=null; function tomorrow(){const d=new Date(Date.now()+864e5),p=n=>String(n).padStart(2,'0');return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())+'T12:00'} function newEl(t){return{type:t,x:t=='clock'?27:0,y:t=='clock'?5:0,align:'left',overlay:'top',font:'Normal',format:0,language:'nl',decimals:-1, text:t=='text'?'Hallo':'',suffix:'',entity:'',attribute:'',factor:1,scroll_width:0,scroll_speed:8,target:tomorrow(),show:'number',input_unit:'s',auto_unit:false,absolute:false,map:'', - bar_width:40,bar_height:3,bar_max:100,bar_frame:true}} + bar_width:40,bar_height:3,bar_max:100,bar_frame:true,day_offset:0}} function peShow(title,name){$('pe_title').textContent=title;$('pe_name').value=name;$('pe').style.display='block';peData=null;peBuild();pePreview();$('pe').scrollIntoView({behavior:'smooth'})} function peOpen(pg,copy){pe={orig:copy?'':pg.name,page:JSON.parse(JSON.stringify(pg.page)),sel:-1,boxes:[],pos:[]};peShow(copy?'Copy of '+pg.name:'Edit '+pg.name,copy?'':pg.name)} function peNew(){pe={orig:'',page:{slots:[{start:'00:00',bitmap:''}],elements:[newEl('clock')]},sel:-1,boxes:[],pos:[]};peShow('New page','')} @@ -388,9 +388,10 @@ function peElems(){const d=$('pe_elems');d.innerHTML='';d.append(mk('datalist',{ const row=(...k)=>rows.push(mk('div',{className:'row'},...k)); if(e.type=='clock')row(field('Format',sel(e,'format',[[0,'08:05'],[1,'8:05'],[2,'08:05:09 (every second)'],[3,'8:05:09 (every second)']]))); if(e.type=='date')row(field('Format',sel(e,'format',DATEF.map((x,k)=>[k,x[e.language=='en'?1:0]]))), - field('Language',sel(e,'language',[['nl','Nederlands'],['en','English']],peElems))); + field('Language',sel(e,'language',[['nl','Nederlands'],['en','English']],peElems)), + field('Day',sel(e,'day_offset',[[0,'today'],[1,'tomorrow'],[2,'+2 days'],[3,'+3 days'],[4,'+4 days'],[5,'+5 days'],[6,'+6 days'],[7,'+7 days']]))); if(e.type=='ha'){row(field('Entity',inp(e,'entity','text',{placeholder:'sensor.outside_temperature',size:28,maxLength:64})), - field('Attribute',inp(e,'attribute','text',{placeholder:'(the state)',size:12,maxLength:32})),btn('Test',()=>haTest(e))); + field('Attribute',inp(e,'attribute','text',{placeholder:'(the state) or forecast.1.temperature',size:12,maxLength:32})),btn('Test',()=>haTest(e))); const sh=e.show||'number',fmt=()=>{e.format=0;peElems()}; row(field('Show as',sel(e,'show',[['number','number'],['duration','duration'],['time','time / date'],['map','own texts']],fmt)), field('Before',inp(e,'text','text',{size:8,maxLength:32})),field('After',inp(e,'suffix','text',{size:6,maxLength:16}))); @@ -430,7 +431,7 @@ function peElems(){const d=$('pe_elems');d.innerHTML='';d.append(mk('datalist',{ else if(e.type=='bar')row(...pos,field('Align',sel(e,'align',[['left','left'],['center','center'],['right','right']])),ovl); else row(...pos,ovl); box.append(...rows);d.append(box)})} -function peAdd(t){if(pe.page.elements.length>=8){msg('8 elements at most',true);return}pe.page.elements.push(newEl(t));pe.sel=pe.page.elements.length-1;peElems();pePreview()} +function peAdd(t){if(pe.page.elements.length>=16){msg('16 elements at most',true);return}pe.page.elements.push(newEl(t));pe.sel=pe.page.elements.length-1;peElems();pePreview()} function peMark(){document.querySelectorAll('#pe_elems .el').forEach((d,i)=>d.classList.toggle('sel',i==pe.sel));peDraw()} function pePage(){const p=JSON.parse(JSON.stringify(pe.page));p.slots.sort(bySlot);return p} function pePreview(){clearTimeout(peTimer);peTimer=setTimeout(peRender,120)} diff --git a/test/test_esp32/test_main.cpp b/test/test_esp32/test_main.cpp index 783ef6a..4700ec6 100644 --- a/test/test_esp32/test_main.cpp +++ b/test/test_esp32/test_main.cpp @@ -2273,6 +2273,68 @@ void test_bar_render() TEST_ASSERT_TRUE(bitmapPixel(b, 61, 17)); } +void test_date_days_ahead() +{ + LocalTime t; // Thursday 31 December 2026 + t.year = 2026; + t.month = 12; + t.day = 31; + t.weekday = 4; + LocalTime n = addDays(t, 1); + TEST_ASSERT_EQUAL(2027, n.year); + TEST_ASSERT_EQUAL(1, n.month); + TEST_ASSERT_EQUAL(1, n.day); + TEST_ASSERT_EQUAL(5, n.weekday); + t.year = 2028; // leap year: 28 February + 1 = 29 February + t.month = 2; + t.day = 28; + TEST_ASSERT_EQUAL(29, addDays(t, 1).day); + TEST_ASSERT_EQUAL(3, addDays(t, 2).month); + t.year = 2027; + TEST_ASSERT_EQUAL(1, addDays(t, 1).day); + + DynamicPage page; + page.element_count = 1; + PageElement &e = page.elements[0]; + e.type = ElementType::Date; + e.format = DATE_WEEKDAY_SHORT; + LocalTime thu; + thu.year = 2026; + thu.month = 10; + thu.day = 15; + thu.weekday = 4; + char text[32]; + elementText(e, thu, ElementValues(), text, sizeof(text)); + TEST_ASSERT_EQUAL_STRING("do", text); + e.day_offset = 3; + elementText(e, thu, ElementValues(), text, sizeof(text)); + TEST_ASSERT_EQUAL_STRING("zo", text); + TEST_ASSERT_NULL(validatePage(page)); + e.day_offset = 8; + TEST_ASSERT_NOT_NULL(validatePage(page)); + e.day_offset = -1; + TEST_ASSERT_NOT_NULL(validatePage(page)); +} + +void test_attribute_paths_and_16_elements() +{ + DynamicPage page; + page.element_count = PAGE_ELEMENTS; + TEST_ASSERT_EQUAL(16, PAGE_ELEMENTS); + for (int i = 0; i < PAGE_ELEMENTS; i++) + { + page.elements[i].type = ElementType::HaValue; + strcpy(page.elements[i].entity, "sensor.weer_verwachting"); + strcpy(page.elements[i].attribute, "forecast.1.templow"); + } + TEST_ASSERT_NULL(validatePage(page)); + strcpy(page.elements[3].attribute, "forecast/1"); + TEST_ASSERT_NOT_NULL(validatePage(page)); + page.elements[3].attribute[0] = '\0'; + page.element_count = PAGE_ELEMENTS + 1; + TEST_ASSERT_NOT_NULL(validatePage(page)); +} + void test_bar_validation() { DynamicPage page = barPage(); @@ -2462,6 +2524,8 @@ int main() RUN_TEST(test_bar_fill); RUN_TEST(test_bar_render); RUN_TEST(test_bar_validation); + RUN_TEST(test_date_days_ahead); + RUN_TEST(test_attribute_paths_and_16_elements); RUN_TEST(test_plex_user_placeholder); RUN_TEST(test_slow_scrolling_below_one_dot_per_second); diff --git a/tools/export_examples.py b/tools/export_examples.py new file mode 100644 index 0000000..0d87195 --- /dev/null +++ b/tools/export_examples.py @@ -0,0 +1,90 @@ +"""Writes examples/home-pages.json from the pages on a display: the dynamic pages with the +icons, bitmaps and fonts they use, as a backup file that the web UI imports (System tab). + + python tools/export_examples.py [ESP32 address, default 10.1.1.198] + +Private parts of entity ids are replaced by placeholders (PRIVATE below); the export stops +when a page still contains one of them. The pages that come with the firmware (Clock) and +their icons (the Pikachus) are left out: every display has them. Only the Python standard +library is used. +""" + +import json +import os +import re +import sys +import urllib.parse +import urllib.request + +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__))), 'examples', 'home-pages.json') + +# Private text -> placeholder (in entity ids, attributes and texts) +PRIVATE = { + 'vigilantestraat': 'home', # the street (climate entity) + '122225067403': '000000000000', # the Envoy's serial number + 'mangavalkserver': 'plex_server', # the Plex server's name +} +FROM_FIRMWARE_PAGES = {'clock'} +FROM_FIRMWARE_ICONS = {'pikachu', 'pikachu yawning'} + + +def get(path): + with urllib.request.urlopen(ESP + path, timeout=60) as r: + return json.loads(r.read()) + + +def clean(value): + if isinstance(value, str): + for private, placeholder in PRIVATE.items(): + value = re.sub(re.escape(private), placeholder, value, flags=re.IGNORECASE) + return value + + +def main(): + pages = [p for p in get('/api/pages')['pages'] if p['name'].lower() not in FROM_FIRMWARE_PAGES] + icons = {i['name'].lower(): i for i in get('/api/icons')['icons']} + bitmaps = {b['name'].lower(): b for b in get('/api/bitmaps')['bitmaps'] if not b['builtin']} + fonts = {f['name'].lower(): f for f in get('/api/fonts')['fonts'] if not f['builtin']} + + # What the pages use: backgrounds, fixed images, the images by state (name start), fonts + images, used_fonts = set(), set() + for p in pages: + for s in p['page']['slots']: + images.add(s['bitmap'].lower()) + for e in p['page']['elements']: + used_fonts.add(e['font'].lower()) + if e['type'] != 'image': + continue + if not e['entity']: + images.add(e['text'].lower()) + else: + start = e['text'].lower() + images.update(n for n in list(icons) + list(bitmaps) if n.startswith(start)) + for e in p['page']['elements']: + for k, v in e.items(): + e[k] = clean(v) + + out = { + 'format': 'flipdot-backup', 'version': 1, 'firmware': get('/api/status')['version'], + 'exported': '2026-10-16T00:00:00Z', + 'bitmaps': [{'name': b['name'], 'data': b['data']} for n, b in sorted(bitmaps.items()) if n in images], + 'icons': [{k: i[k] for k in ('name', 'width', 'height', 'data')} for n, i in sorted(icons.items()) + if n in images and n not in FROM_FIRMWARE_ICONS], + 'pages': [{'name': p['name'], 'page': p['page']} for p in pages], + 'fonts': [], + } + for n in sorted(used_fonts & set(fonts)): + out['fonts'].append(get('/api/font?name=' + urllib.parse.quote(fonts[n]['name']))) + + text = json.dumps(out, ensure_ascii=False, indent=1) + left = [p for p in PRIVATE if p.lower() in text.lower()] + if left: + sys.exit('Private text still in the export: ' + ', '.join(left)) + with open(OUT, 'w', encoding='utf-8', newline='\n') as f: + f.write(text + '\n') + print(len(out['pages']), 'pages,', len(out['icons']), 'icons,', len(out['bitmaps']), 'bitmaps,', + len(out['fonts']), 'fonts ->', OUT) + + +main() diff --git a/tools/make_example_pages.py b/tools/make_example_pages.py deleted file mode 100644 index 09995fe..0000000 --- a/tools/make_example_pages.py +++ /dev/null @@ -1,397 +0,0 @@ -"""Builds examples/home-pages.json: icons, backgrounds and dynamic pages for this home (import it in the web UI, System tab). - - python tools/make_example_pages.py examples/home-pages.json -""" -import json -import sys - -W, H = 84, 19 - - -def blank(): - return [[0] * W for _ in range(H)] - - -def put(img, art, x0, y0): - for y, row in enumerate(art): - for x, c in enumerate(row): - if c == '#' and 0 <= x0 + x < W and 0 <= y0 + y < H: - img[y0 + y][x0 + x] = 1 - return img - - -def to_hex(img): - out = '' - for y in range(H): - for i in range(11): - v = 0 - for k in range(8): - x = i * 8 + k - if x < W and img[y][x]: - v |= 0x80 >> k - out += '%02x' % v - return out - - -CLOUD = [ - "...........", - "....###....", - "..##.####..", - ".#########.", - "###########", - "###########", - ".#########.", -] - -WEATHER = { - 'sunny': [ - ".....#.....", - ".#...#...#.", - "..#.....#..", - "....###....", - "...#####...", - "##.#####.##", - "...#####...", - "....###....", - "..#.....#..", - ".#...#...#.", - ".....#.....", - ], - 'clear-night': [ - "....###....", - "..###......", - ".###.......", - ".##......#.", - "###.......", - "###........", - "###........", - ".###.......", - ".####....#.", - "..#######..", - "....###....", - ], - 'partlycloudy': [ - "..#..#.....", - "....###....", - ".#.#####...", - "...####....", - "#.##..###..", - "..#########", - ".##########", - "###########", - "###########", - ".#########.", - "...........", - ], - 'cloudy': ["...........", "..........."] + CLOUD + ["..........."] * 2, - 'fog': [ - "...........", - "###########", - "...........", - ".##########", - "...........", - "#########..", - "...........", - "..#########", - "...........", - "#########..", - "...........", - ], - 'rainy': CLOUD + [ - "...........", - ".#...#...#.", - "#...#...#..", - "...........", - ], - 'pouring': CLOUD + [ - ".#.#.#.#.#.", - "#.#.#.#.#..", - ".#.#.#.#.#.", - "#.#.#.#.#..", - ], - 'lightning': CLOUD[:6] + [ - ".....##....", - "....##.....", - "...#####...", - ".....##....", - "....##.....", - ], - 'lightning-rainy': CLOUD[:6] + [ - ".#...##..#.", - "#...##..#..", - "...#####...", - ".#...##..#.", - "#...##..#..", - ], - 'snowy': CLOUD + [ - ".#...#...#.", - "...#...#...", - ".#...#...#.", - "...#...#...", - ], - 'snowy-rainy': CLOUD + [ - ".#...#...#.", - "#...#...#..", - "...#...#...", - ".#...#...#.", - ], - 'hail': CLOUD + [ - ".##..##..##", - ".##..##..##", - "...........", - "..##..##...", - ], - 'windy': [ - "...........", - "......##...", - "........#..", - "#########..", - "...........", - "###########", - "...........", - "#######....", - "........#..", - "......##...", - "...........", - ], - 'windy-variant': CLOUD + [ - "...........", - "#########..", - "...........", - "..#########", - ], - 'default': [ - ".....#.....", - "....#.#....", - "....#.#....", - "...#.#.#...", - "...#.#.#...", - "..#..#..#..", - "..#..#..#..", - ".#.......#.", - ".#...#...#.", - "#.........#", - "###########", - ], -} -WEATHER['exceptional'] = WEATHER['default'] - -BOLT = [ - "....##.", - "...##..", - "..##...", - ".#####.", - "...##..", - "..##...", - ".##....", - "##.....", -] -SUN_SMALL = [ - "....#....", - ".#.....#.", - "...###...", - "..#####..", - "#.#####.#", - "..#####..", - "...###...", - ".#.....#.", - "....#....", -] -DROP = [ - "...#...", - "...#...", - "..###..", - "..###..", - ".#####.", - "#######", - "#######", - "#######", - ".#####.", - "..###..", -] -HOUSE = [ - ".....#.....", - "....###....", - "...##.##...", - "..##...##..", - ".##.....##.", - "##.......##", - ".#.......#.", - ".#.##....#.", - ".#.##.##.#.", - ".#.##.##.#.", - ".#########.", -] -PRINTER = [ - "###########", - "#.........#", - "#.#######.#", - "#....#....#", - "#....#....#", - "#.........#", - "#...###...#", - "#..#####..#", - "###########", - "#.........#", - "###########", -] -CALENDAR = [ - ".#.....#...", - "###########", - "#.........#", - "###########", - "#.#.#.#.#.#", - "#.........#", - "#.#.#.#.#.#", - "#.........#", - "#.#.#.#...#", - "#.........#", - "###########", -] -TREE = [ - ".....#.....", - "....###....", - ".....#.....", - "....###....", - "...#####...", - "....###....", - "...#####...", - "..#######..", - "...#####...", - "..#######..", - ".#########.", - ".....#.....", - "....###....", -] -MOON = WEATHER['clear-night'] -STARS = [(1, 2), (24, 1), (27, 7), (3, 9), (22, 4)] -CORNER = ["#####", "#....", "#....", "#...."] - - -def corners(): - img = blank() - put(img, CORNER, 0, 0) - put(img, [r[::-1] for r in CORNER], W - 5, 0) - put(img, CORNER[::-1], 0, H - 4) - put(img, [r[::-1] for r in CORNER[::-1]], W - 5, H - 4) - return img - - -bitmaps = {} -for state, art in WEATHER.items(): - bitmaps['weer ' + state] = put(blank(), art, 0, 0) -bitmaps['bg energie'] = put(put(blank(), BOLT, 1, 0), SUN_SMALL, 0, 10) -bitmaps['bg printer'] = put(blank(), PRINTER, 0, 0) -bitmaps['bg binnen'] = put(blank(), HOUSE, 0, 0) -bitmaps['bg verbruik'] = put(put(blank(), DROP, 1, 0), BOLT, 1, 11) -bitmaps['bg datum'] = put(blank(), CALENDAR, 0, 4) -bitmaps['bg kerst'] = put(blank(), TREE, 0, 3) -bitmaps['bg dag'] = put(blank(), WEATHER['sunny'], 9, 0) -night = put(blank(), MOON, 9, 0) -for x, y in STARS: - night[y][x] = 1 -bitmaps['bg nacht'] = night -bitmaps['bg kader'] = corners() -bitmaps['bg vol'] = [[1] * W for _ in range(H)] - -# Plex: the chevron (a thick ">", tip on the middle row) and the Tautulli state icons -plex = blank() -for y in range(1, 18): - start = (8 - abs(y - 9)) // 2 - for x in range(start, start + 5): - plex[y][x] = 1 -bitmaps['plex logo'] = plex -bitmaps['plex playing'] = put(blank(), ["#..", "##.", "###", "##.", "#.."], 0, 0) -bitmaps['plex paused'] = put(blank(), ["##.##"] * 5, 0, 0) -bitmaps['plex buffering'] = put(blank(), [".....", ".....", ".....", ".....", "#.#.#"], 0, 0) - - -def el(type, x, y, font='Normal', **kw): - e = {'type': type, 'x': x, 'y': y, 'align': 'left', 'overlay': 'top', 'font': font, 'format': 0, - 'language': 'nl', 'decimals': -1, 'text': '', 'suffix': '', 'entity': '', 'attribute': '', - 'factor': 1, 'scroll_width': 0, 'scroll_speed': 8, 'target': '2000-01-01T00:00', - 'bar_width': 40, 'bar_height': 3, 'bar_max': 100, 'bar_frame': True} - e.update(kw) - return e - - -# Placeholders: put your own entity ids here (the Envoy ones contain its serial number) -ENVOY = 'sensor.envoy_000000000000_' -pages = { - # 1. Weather clock: large clock, weather icon, outside humidity and temperature - 'Weer': {'slots': [], 'elements': [ - el('clock', 0, 0, 'Full height'), - el('image', 55, 0, text='weer ', entity='weather.knmi_home'), - el('ha', 84, 2, 'Small', align='right', entity='weather.knmi_home', attribute='humidity', suffix='%', decimals=0), - el('ha', 84, 13, 'Small', align='right', entity='weather.knmi_home', attribute='temperature', suffix='°C', decimals=1), - ]}, - # 2. Sun by day, moon and stars at night, large clock and the date - 'Dag en nacht': {'slots': [{'start': '07:00', 'bitmap': 'bg dag'}, {'start': '19:00', 'bitmap': 'bg nacht'}], 'elements': [ - el('clock', 84, 0, 'Full height', align='right'), - el('date', 0, 13, 'Small', format=1), - ]}, - # 3. Energy: net power now (negative = export), solar today, small clock - 'Energie': {'slots': [{'start': '00:00', 'bitmap': 'bg energie'}], 'elements': [ - el('ha', 10, 0, entity=ENVOY + 'current_net_power_consumption', suffix=' W', decimals=0), - el('ha', 11, 11, entity=ENVOY + 'today_s_energy_production', suffix=' kWh', decimals=1, factor=0.001), - el('clock', 84, 0, 'Small', align='right'), - ]}, - # 4. 3D printer: progress, time left, status message (scrolls), nozzle temperature - '3D printer': {'slots': [{'start': '00:00', 'bitmap': 'bg printer'}], 'elements': [ - el('ha', 14, 0, entity='sensor.mainsailos_progress', suffix='%', decimals=0), - el('ha', 84, 0, align='right', entity='sensor.mainsailos_print_time_left', suffix='m', decimals=0, factor=1 / 60), - el('ha', 14, 13, 'Small', entity='sensor.mainsailos_current_display_message', scroll_width=46, scroll_speed=6), - el('ha', 84, 13, 'Small', align='right', entity='sensor.mainsailos_extruder_temperature', suffix='°', decimals=0), - ]}, - # 5. Indoor climate: room temperature, outside, CO2 and air quality - 'Binnen': {'slots': [{'start': '00:00', 'bitmap': 'bg binnen'}], 'elements': [ - el('ha', 14, 2, entity='climate.home', attribute='current_temperature', suffix='°', decimals=1), - el('image', 49, 0, text='weer ', entity='weather.knmi_home'), - el('ha', 84, 2, 'Small', align='right', entity='weather.knmi_home', attribute='temperature', suffix='°', decimals=1), - el('ha', 0, 13, 'Small', entity='sensor.eco2', text='CO2 ', suffix='ppm', decimals=0), - el('ha', 84, 13, 'Small', align='right', entity='sensor.air_quality_index_aqi_2', text='lucht '), - ]}, - # 6. Today: water and energy use, solar production - 'Verbruik': {'slots': [{'start': '00:00', 'bitmap': 'bg verbruik'}], 'elements': [ - el('ha', 10, 1, entity='sensor.util_water_usage_daily', suffix=' l', decimals=0, factor=1000), - el('ha', 10, 11, entity=ENVOY + 'today_s_energy_consumption', suffix=' kWh', decimals=1, factor=0.001), - el('clock', 84, 0, 'Small', align='right'), - el('ha', 84, 13, 'Small', align='right', entity=ENVOY + 'today_s_energy_production', text='zon ', suffix='kWh', decimals=1, factor=0.001), - ]}, - # 7. Big clock in a frame; from 22:00 black digits on yellow - 'Grote klok': {'slots': [{'start': '07:00', 'bitmap': 'bg kader'}, {'start': '22:00', 'bitmap': 'bg vol'}], 'elements': [ - el('clock', 42, 0, 'Full height', align='center', overlay='invert'), - ]}, - # 8. The date in Dutch with a calendar - 'Datum': {'slots': [{'start': '00:00', 'bitmap': 'bg datum'}], 'elements': [ - el('date', 14, 1, format=5), - el('date', 14, 12, 'Small', format=2), - el('clock', 84, 12, 'Small', align='right'), - ]}, - # Countdown to Christmas - 'Aftellen': {'slots': [{'start': '00:00', 'bitmap': 'bg kerst'}], 'elements': [ - el('text', 14, 1, 'Small', text='Kerst over'), - el('countdown', 14, 10, target='2026-12-25T00:00', suffix='Vrolijk kerst!', scroll_width=70, scroll_speed=8), - ]}, - # Stopwatch, large - 'Stopwatch': {'slots': [], 'elements': [ - el('stopwatch', 42, 0, 'Full height', align='center'), - ]}, - # Plex: how many watch (Plex integration), and for the Plex user of the settings - # (Tautulli's sensors): play/pause icon, title (scrolls) and a progress bar - 'Plex': {'slots': [{'start': '00:00', 'bitmap': 'plex logo'}], 'elements': [ - el('ha', 13, 0, entity='sensor.mangavalkserver', suffix=' watching'), - el('image', 13, 9, text='plex ', entity='sensor.{plex_user}_state'), - el('ha', 19, 9, 'Small', entity='sensor.{plex_user}_full_title', scroll_width=65, scroll_speed=8), - el('bar', 13, 16, entity='sensor.{plex_user}_progress', bar_width=71, bar_height=3), - ]}, -} - -backup = { - 'format': 'flipdot-backup', 'version': 1, 'firmware': '2.9.0', 'exported': '2026-10-16T00:00:00Z', - 'bitmaps': [{'name': n, 'data': to_hex(b)} for n, b in bitmaps.items()], - 'pages': [{'name': n, 'page': p} for n, p in pages.items()], - 'fonts': [], -} -json.dump(backup, open(sys.argv[1], 'w', encoding='utf-8'), ensure_ascii=False, indent=1) -print(len(bitmaps), 'bitmaps,', len(pages), 'pages')