dashboards.casa

Sketchbook

Code and instructions handoff

Everything an AI coding agent needs to rebuild Sketchbook on your own Home Assistant: the full code, the made-up home's pictures as examples, a go-live kit and the guide below.

Download the code 19.3 MB

Give the zip to your coding agent (Claude Code, Codex, Cursor …) with the prompt. It runs the demo first, then swaps in your entities and your own pictures. No account or API key from us needed.

You are setting up Sketchbook, a wall-tablet dashboard for Home Assistant, in another person's home. This file is written for you, the agent. Work through it top to bottom; every step says what to change, where, and how to check it. You will not need to ask the authors anything.

What you're building

Sketchbook draws the home like a page from an architect's sketchbook: graphite pencil drawings that colour in when something is on. The Home page is a three-quarter aerial drawing of the house under a sky that follows the real sun (drag the sun or moon along its loop to preview any hour), with lit windows, a heated-pool glow, garden lamps, solar / battery / power notes and the cats popping up in the room they're in. Rooms shows 18 pencil sketches that bloom into colour when the room's light is on (dimmer, A/C ±, curtain and lock status). Curtains & Sheers puts real 3D curtains in front of each window's view and reacts to the weather. Then Animals (where each cat is and has been), Garden (a plan with 20 light pins), Energy (the day's solar, grid and battery chart), Network and Vacuum. English ⇄ 日本語.

Out of the box it runs as a demo of a made-up home ("Sakura Home" in Tokyo, a fictional family and six fictional cats). Nothing in it is a real home. Your job: run the demo, connect it to the person's Home Assistant, map their entities, optionally redraw the pictures of their own home, and put it on their tablet.

Sketchbook — the Home page at golden hour

What the person needs

  • Home Assistant 2024.1 or newer (uses the WebSocket API, weather.get_forecasts, and the REST history, calendar and template endpoints). No HACS cards, no custom frontend.
  • A wall tablet or any modern browser. The design is made for 1376 × 1032 CSS px, landscape (a 13" tablet); it also works at 1180 × 820 (11" tablets). Phones are not a target.
  • A computer with Node.js 18+ to build it (the tablet only runs the result).

Time: demo 10 min · mapping entities 1–3 h (depends on how many rooms and devices) · going live 30 min · pictures of their own home 2–6 h (optional — the made-up pictures work as placeholders).

What's in this package

HANDOFF.md              this guide            README.md   3-line pointer            LICENSE   MIT (code) + notes
package.json            exact versions it was built and tested with
vite.config.ts          dev server (+ live-mode proxy) and builds; BASE = where a build will be hosted
tsconfig.json  index.html  .gitignore
.env.example            copy to .env.live for live mode (Home Assistant address, token for the dev server, BASE)
panel-loader.js         mode B: makes the dashboard a page in Home Assistant's sidebar
live/README.md          the go-live kit in detail (modes A, B, C, troubleshooting)
docs/still.jpg          what it looks like
tools/
  sketchbook_art.py     picture pipeline (Python + Pillow): rooms, window plates, house, garden, cats, thumbnails,
                        background cut-out, measuring grid
  measure.html          click-to-measure helper for window polygons and anchor points (open in a browser, offline)
  prompts.json          the exact prompts used to draw the made-up home, each with the command that processes it
src/
  main.tsx              start-up switch: demo (default) · live (VITE_HA_MODE=live) · sidebar panel (?panel=1)
  main.demo.tsx         demo entry: mock backend + demo time/weather bridge
  main.live.tsx         live entry for modes A and C          main.panel.tsx   live entry for mode B
  App.tsx, app/views.tsx   the menu: which pages exist, their order and labels
  ha/                   types.ts (the HaBackend interface) · HaProvider.tsx (hooks) · wsBackend.ts (live) ·
                        hassBackend.ts (sidebar) · authFetch.ts (token on REST calls) · mockBackend.ts (demo)
  config/               ← EVERY entity id and per-house setting lives here (Step 2)
  views/, components/   the pages and their parts
  lib/                  sun & sky maths (sky.ts), forecasts, history, music, network data, formatting
  i18n/                 i18n.tsx + ja.ts (English → Japanese dictionary) + ja-fonts.ts
  styles/               theme.css (colour and font tokens), app.css, fonts.css
  demo/                 the made-up home: states snapshot, synthetic day, calendar, music, bridge — never in a live build
  assets/               pictures bundled by Vite (house, garden, rooms, plates, cats, thumbs) + fonts (+ OFL licences)
static/                 served as-is at /local/… and /fonts/…: robot and lamp icons, app icon, network gear,
                        camera "spotting" photos, demo music covers and camera stills, Manrope font

Run the demo first

sh
npm install
npm run dev            # serves http://localhost:5173/ (and on the LAN: the dev server listens on all interfaces)

Open it in a desktop browser window of roughly 1376 × 1032 (or devtools' device mode). The made-up home runs entirely in the browser: no Home Assistant, and the demo layer (src/demo/sandbox.ts) blocks every network request except Google Fonts (only when 日本語 is switched on) and the RainViewer radar tiles on the Home page. If you see the drawn house with a sky, the toolchain works — do this before touching their house.

Things to try: drag the sun on Home into the night (it springs back when you let go); tap a room sketch on Rooms (it colours in); open a curtain; switch to 日本語; Garden → "All lights on".

The demo clock starts at 16:30 and keeps running. Start-up URL parameters: ?t=21:15 (time; also ?demoTime=), ?wx=rainy (weather — sunny, partlycloudy, cloudy, rainy, pouring, lightning-rainy, fog, snowy), ?lang=ja. A page that shows the demo in an iframe can change time and weather live with postMessage (src/demo/bridge.ts documents the messages). As time moves, the demo switches its lights the way an automation would (evening scene after sunset, night lights after 23:00), so the drawings colour in at dusk.

Other scripts: npm run build → the same demo as static files in dist/ (any web host; BASE=/sub/path/ npm run build for a sub-folder) · npm run typecheck (tsc --noEmit, 0 errors) · npm run preview (serve dist/).

How it works

Home Assistant ──► HaBackend  (src/ha/types.ts — one small interface)
                     ├─ wsBackend    live, over HA's WebSocket API (subscribe_entities, call_service)
                     ├─ hassBackend  inside HA's sidebar (HA hands the app its `hass` object)
                     └─ mockBackend  demo: an in-memory made-up home
                ──► HaProvider hooks (src/ha/HaProvider.tsx)
                     useEntity(id) · useEntities([ids])      → state + attributes, live
                     useCallService()(domain, service, data, { entity_id })
                     useCallServiceResponse()                → services that return data (weather.get_forecasts)
                ──► views/ and components/
   + REST  fetch("/api/…"): history for the charts (/api/history/period), calendars (/api/calendars/<id>),
           the "lights on" list (/api/states + /api/template). In live mode authFetch adds the user's token.
  • Views never know which backend is behind the hooks. Each view subscribes only to its own entities (it stays light on big installations). Keep that when you add cards: useEntities([...ids]), never "all states".
  • Demo vs live: src/main.tsx picks main.demo.tsx unless the build/dev mode is live (see Step 3). A live build contains no demo code at all (src/demo/, the mock backend and the demo bridge are tree-shaken out).
  • Config: src/config/*.ts holds every entity id and every per-house setting (rooms, curtains, garden lights, where the windows are on the drawing, cats …). Changing the house = changing these files + the pictures.
  • i18n: visible text goes through t("English text") (useT() from src/i18n/i18n.tsx); src/i18n/ja.ts maps English → Japanese, and room/cat names shown on screen are looked up the same way. The flag toggle switches en/ja; Japanese fonts are loaded only then. To add their language: copy ja.ts to e.g. de.ts and translate the values; add the code to Lang and the lookup in i18n.tsx; add a button to components/shell/LangToggle.tsx; change the "ja-JP" locale picks (search ja-JP) to their locale; or replace Japanese entirely by editing ja.ts in place. Strings missing from the dictionary simply show in English.
  • Theming: CSS custom properties in src/styles/theme.css — paper --paper*, graphite --lead*, lamp yellow --lamp, coloured-pencil inks --cerulean --sap --vermilion --violet, energy --solar --grid --batt, the dark "cloth" sidebar --cloth*, fonts --f-hand (Shantell Sans) and --f-print. The sky tints the paper live: src/lib/sky.ts sets --paper, --paper-shade and --cloth from the sun's height.
  • Pictures and how they are layered and anchored (specs in Step 4):
    • Home (components/home/HouseStage.tsx): the sky is CSS, coloured from the real sun position (lib/sky.ts), with stars, clouds, rain, fog, snow and lightning from the weather entity. Over it the house: house-day-cut.webp (transparent background) cross-fades to house-night-body.webp (indigo body) as the sun sets, with house-night-lines.webp (white-pencil lines) on top at night. Between those layers an SVG in the drawing's own pixels (1800 × 791) draws the lit windows — WINDOWS in config/houseArt.ts: a polygon per window and the light(s) that light it, hatched yellow + glow, brighter after sunset — the heated pool (POOL_PTS), lamp halos (PAVILION, GARAGE, ENTRANCE_LAMP), the solar/battery/house energy notes (ENERGY_AT = the point on the house, ENERGY_LABEL = where the note sits) and cat perches (CAT_PERCH, one per cat-room key, where a cat pops up).
    • Rooms (components/rooms/RoomCard.tsx): two pixel-aligned layers per room, <key>-sk.webp (pencil) under <key>-col.webp (colour). Turning the light on reveals the colour with a round "bloom" from where you tapped; the colour layer's opacity follows brightness (42 % + 58 % × brightness).
    • Curtains & Sheers (views/CurtainsSheersView.tsx, components/curtains3d/engine.ts, three.js): a flat plate — the view out of that window, plates/<key>-day.webp / -night.webp — behind 3D curtains and sheers that open and close at motor speed. The weather entity picks the lighting (day / overcast / wet / storm and their night variants, rain and lightning on the glass). Per room in config/curtains3d.ts: the plate key, the entities, fabric colour, and the glass rectangle (glass: [0, 0, 1, 1] = the plate fills the window).
    • Garden (views/GardenView.tsx): garden-day.webp cross-fades to garden-night.webp; 20 light pins sit at fractions of the picture (config/garden.ts, x and y from 0 to 1); glows are SVG.
    • Animals: transparent cat sketches (assets/cats/<cat>.webp) in the donuts and pop-ups; two "spotted by the cameras" photos in static/local/snapshots/animals/.
    • Menu thumbnails: assets/thumbs/*.webp (168 × 112), cut from the pictures above.

Step 1 — Inventory their Home Assistant

Get the Home Assistant address (e.g. http://homeassistant.local:8123) and, for development only, a long-lived access token (their profile → Security → Long-lived access tokens). List every entity:

sh
export HA=http://homeassistant.local:8123 TOKEN=…            # keep the token out of git; .env.live is ignored
curl -s -H "Authorization: Bearer $TOKEN" "$HA/api/states" \
 | python3 -c 'import json,sys; [print(e["entity_id"], "|", e["state"], "|", e["attributes"].get("friendly_name", "")) for e in json.load(sys.stdin)]' \
 | sort > ha-entities.txt
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" "$HA/api/template" \
 -d '{"template":"{% for a in areas() %}{{ area_name(a) }}: {{ area_entities(a) | join(\", \") }}\n{% endfor %}"}' > ha-areas.txt

(Or Developer Tools → States in Home Assistant.) Domains this dashboard reads or controls: light, switch, climate, sensor, weather, calendar, media_player, vacuum, lock, script, input_number, input_select, input_boolean, button, select, input_text, input_datetime. Use ha-areas.txt to match their rooms to the dashboard's rooms. Ask the person which rooms, floors and features they want: the dashboard never needs every entity. Anything they don't have, you hide — each row below says how. Never put the token in a build or a committed file.

Step 2 — Map entities

Replace the made-up ids (*.sk_*) with theirs. "State/attributes used" is what the code reads; "services" is what it calls. Names you can change freely (room titles, cat names, labels) are in the same files. Rows marked (demo only) need nothing. Example ids are the made-up home's.

Rooms page and room cards — src/config/rooms.ts

KeyWhatDomainReadsCallsExample idsIf they don't have it
ROOMS[].name / floorroom card title (upper case is fine) and floor tab "1st"/"2nd"———TATAMI ROOM, 1stdelete the room's object to drop the card; the sketch key comes from KEY in components/rooms/RoomCard.tsx (add theirs there)
ROOMS[].lightthe room's light (a light group is ideal)lightstate, brightnesslight.toggle, light.turn_on {brightness_pct}light.sk_1f_tatami, light.sk_1f_living, light.sk_1f_kitchen, light.sk_1f_dining, light.sk_1f_atrium, light.sk_1f_corridor, light.sk_1f_salon, light.sk_1f_study, light.sk_1f_stairs, light.sk_2f_master, light.sk_2f_bath, light.sk_2f_closet, light.sk_2f_family, light.sk_2f_catroom, light.sk_2f_pinkroom, light.sk_2f_nursery, light.sk_2f_guest2, light.sk_2f_guest4light: null — the card shows the sketch without a switch
ROOMS[].climatethe room's A/C: its on/off look on the card, and the temperature if there is no sensorclimatestate, current_temperature—climate.sk_ac_tatami … (see CLIMATE below)null
ROOMS[].temp, humidityroom temperature / humiditysensorstate (°C, %)—sensor.sk_living_temperature, sensor.sk_atrium_temperature, sensor.sk_study_temperature, sensor.sk_master_temperature, sensor.sk_family_temperature, sensor.sk_cat_room_temperature, sensor.sk_studio_temperature, sensor.sk_nursery_temperature, sensor.sk_guest4_temperature · sensor.sk_living_humidity, sensor.sk_hall_1f_humidity, sensor.sk_atrium_humidity, sensor.sk_study_humidity_2, sensor.sk_hall_2f_humidity, sensor.sk_bedroom_hall_humiditynull / leave out — hidden
ROOMS[].sheer, blackout"sheers open" / "curtains open" text on the cardany entity whose state is on when open (a template switch or binary sensor over their cover — see helpers)state—switch.sk_tatami_sheers_open_led, switch.sk_tatami_curtains_open_led (same pattern for living, dining, atrium, salon, pinkroom, nursery, guest2, guest4)null — the card says "no curtains"
ROOMS[].locklock status on the cardlocklocked/unlocked—lock.sk_kitchen_door, lock.sk_study_door, lock.sk_master_door, lock.sk_pinkroom_doorleave out
ROOMS[].featureLighta feature lamp with its own on/off icon (onSvg/offSvg in static/local/)lightstatelight.turn_on / light.turn_offlight.sk_1f_dining_halo, light.sk_1f_salon_halo, light.sk_1f_stairs_haloleave out
ROOM_AC[room][]A/C control on a card: helper = target temperature, script = on/off, optional climate + color for rooms with two unitsinput_number, script, climatehelper state, min/max/step; climate stateinput_number.increment/decrement, script.turn_oninput_number.sk_tatami_ac_target + script.sk_tatami_ac_toggle (same pattern for living, salon, study, stairs, closet, family, pinkroom, nursery, guest2, guest4; master has master_ac1/master_ac2 with climate.sk_ac_master1/2)delete the room's entry (no A/C row on the card), or create the helper + script below

FLOOR_VIEW in this file is unused.

Home page — src/config/entities.ts, src/config/houseArt.ts, src/components/home/HouseStage.tsx

KeyWhatDomainReadsCallsExample idsIf they don't have it
WEATHERweather for the sky, label, temperature, radar windweatherstate (HA condition), temperature, wind_speed, wind_bearing; hourly forecast for dragging the sun aheadweather.get_forecasts {type: hourly} (response)weather.sk_tokyouse the weather entity of HA's default Met.no integration — every install has one
CLIMATE[]A/C list on the left ("Active / Inactive climate"); pin keeps a unit at the top/bottomclimatestate, current_temperature, temperatureclimate.set_hvac_mode (off / cool), climate.set_temperatureclimate.sk_ac_dining, climate.sk_ac_corridor, climate.sk_ac_study, climate.sk_ac_living, climate.sk_ac_tatami, climate.sk_ac_stairway, climate.sk_ac_guest2, climate.sk_ac_nursery, climate.sk_ac_guest4, climate.sk_ac_studio, climate.sk_ac_family, climate.sk_ac_master1, climate.sk_ac_master2, climate.sk_ac_closet, climate.sk_pool_heaterdelete rows; an empty list leaves the column empty
(ClimateList.tsx)pool water temperature shown on the pool-heater rowsensorstate °C—sensor.sk_pool_temperatureedit or remove in components/home/ClimateList.tsx
AREAS[]"Air in each room" tablesensorstate of temp (°C), hum (%), aqi, co2 (ppm, optional)—sensor.sk_<area>_temperature / _humidity / _aqi / _co2 for outside, upstairs, downstairs, master, nursery, guest2, guest4, tatami, studio, studydelete rows; omit co2
CALENDARS[]"Coming up" list, with a colour per calendarcalendarREST /api/calendars/<id>?start&end—calendar.sk_family, calendar.sk_japan_holidays, calendar.sk_birthdays, calendar.sk_school, calendar.sk_catsdelete rows; none → "No upcoming events"
WINDOWS[].ids (houseArt.ts)which light lights which drawn window (re-measure if you redraw the house)lightstate—light.sk_1f_tatami, light.sk_1f_corridor, light.sk_1f_salon, light.sk_2f_guest2, light.sk_2f_guest4, light.sk_2f_catroom, light.sk_2f_pinkroomdelete a window entry
LIVE_IDS (HouseStage.tsx)pool glow when the pool heater is not off; halos for pool steps, pavilion, garage-side lamp, entrance sconce; the three energy notesclimate, switch, light, sensorstate—climate.sk_pool_heater, switch.sk_garden_pool_steps, switch.sk_garden_pavilion, switch.sk_garden_side_gate, light.sk_garden_house_sconce, sensor.sk_solar_power (kW), sensor.sk_battery_level (%), sensor.sk_house_power (kW)replace with theirs or any id they lack: missing entities simply draw nothing ("—" on a note)
TOPSTRIPtop strip: air quality out/in, humidity out/down/up, power usesensorstate—sensor.sk_outside_aqi, sensor.sk_downstairs_aqi, sensor.sk_outside_humidity, sensor.sk_downstairs_humidity, sensor.sk_upstairs_humidity, sensor.sk_house_power (lightsOn: sensor.sk_lights_on is subscribed but not shown)edit the labels in components/shell/TopStrip.tsx
(TopStrip "Lights on")count + list of lights that are on—REST /api/states; switches of the integration named in DIMMERS_INTEGRATION (lib/useRealLights.ts, via /api/template integration_entities(...)) also count—dimmersset DIMMERS_INTEGRATION to an integration whose switches are lights, or leave it (no extra switches)
SHELLnow-playing bar, vacuum status and the door-lock button in the sidebarmedia_player, sensor, lockstate, media attributeslock.lock/unlock, media servicesmedia_player.sk_living_speaker, sensor.sk_maru_state, lock.sk_kitchen_door (vacuum = vacuum.sk_maru)point at theirs; remove the lock button in components/shell/Sidebar.tsx if none

The radar map (components/home/RadarMap.tsx) is not an entity: see "Make it theirs" for its location.

Curtains & Sheers — src/config/curtains3d.ts (ROOMS3D)

Each room has types[] — one per curtain, sheer or blind — of two kinds:

KindFieldsReadsCallsExample ids
scriptedopen (state on = open), dir (state opening / closing / anything else = idle), script (state on while moving)state of all threescript.turn_on on scriptswitch.sk_dining_curtains_open_led, input_select.sk_dining_curtains_direction, script.sk_dining_curtains_toggle — likewise for each of: dining, salon, living, atrium, study (blind), tatami, family, masterken, masterbath (venetian), catroom (venetian), nursery, guest2, guest4, pinkroom, familytv, mastermika; atrium uses input_boolean.sk_atrium_curtains_open / _sheers_open as open
buttonsopenBtn / closeBtn (buttons), openLed / closeLed (state on shows which way it is)state of the two LEDsbutton.pressbutton.sk_atriumback_curtains_open / _close, switch.sk_atriumback_curtains_open_led / _close_led (and the same for _sheers)

Most homes have cover.* entities instead: use the cover helpers below (no code change), or change the code — add a kind: "cover" with one cover id, and in views/CurtainsSheersView.tsx read open = state === "open", moving = state === "opening" || state === "closing" and call cover.open_cover / cover.close_cover. Each room also has curtainS / sheerS (motor seconds, used for the animation), baseHex (fabric colour), outdoor (true = the weather shows through), floor, name and the plate key. Delete a room object to drop it; an empty floor hides its tab content. src/config/curtains.ts is (demo only) — the mock backend reads motor times from it.

Garden — src/config/garden.ts

KeyWhatDomainReadsCallsExample idsIf they don't have it
GARDEN_LIGHTS[] (id, name, x, y)the numbered pins and the list; x/y = position as a fraction of the garden picturelight or switchstate<domain>.turn_on/turn_off; "All lights on/off" = homeassistant.turn_on/turn_off with all idsswitch.sk_garden_pavilion, switch.sk_garden_olive, switch.sk_garden_pool_wall, switch.sk_garden_pool_steps, switch.sk_garden_table, light.sk_garden_patio, switch.sk_garden_red_maple, switch.sk_garden_front_pine, switch.sk_garden_stepping_stones, switch.sk_garden_green_maple, switch.sk_garden_spouts, light.sk_garden_house_sconce, switch.sk_garden_fence, light.sk_garden_veranda, switch.sk_garden_sunbeds, switch.sk_garden_pavilion_steps, switch.sk_garden_side_gate, switch.sk_garden_side_path, switch.sk_garden_back_pine, switch.sk_garden_fernsdelete pins; no garden → remove the garden entry from src/app/views.tsx

Energy — src/views/EnergyView.tsx (E)

WhatDomainReadsExample idsIf they don't have it
power now (kW): grid (+ import, − export), solar, house; battery level (%)sensorstate; the day's history of grid, solar and battery for the chart (REST /api/history/period)sensor.sk_grid_power, sensor.sk_solar_power, sensor.sk_house_power, sensor.sk_battery_levelvalues must be kW: if theirs are W, make a Template helper {{ states('sensor.x') | float(0) / 1000 }}
energy totals (kWh) today / week / monthsensorstatesensor.sk_grid_import_daily / _weekly / _monthly, sensor.sk_solar_energy_daily / _weekly / _monthly, sensor.sk_battery_export_daily / _weekly / _monthlyUtility Meter helpers (below)
savings (in their currency) today, yesterday, last week, last month, year to datesensorstatesensor.sk_savings_today, sensor.sk_savings_yesterday, sensor.sk_savings_last_week, sensor.sk_savings_last_month, sensor.sk_savings_ytdTemplate helpers (below), or delete the savings block in EnergyView.tsx

No solar or battery at all → remove the energy entry from src/app/views.tsx and the three energy notes on Home (EnergyNote lines in HouseStage.tsx). The ¥ sign is in EnergyView.tsx.

Animals — src/config/animals.ts

KeyWhatDomainReadsExample idsIf they don't have it
CATS[] (id, name)the cats; id also names the picture assets/cats/<id>.webp——mochi, yuki, sora, kuro, hana, renno pets → remove animals from src/app/views.tsx and the cat pop-ups (pop block in HouseStage.tsx)
roomSensor(id) = sensor.<id>_roomthe room each cat is in now; state = a room key from ROOM_RATIO (or away)sensorstate, last_changed; history for the timelinesensor.mochi_roomfrom a BLE room-presence integration (e.g. Bermuda) with a collar tag, or a camera person/animal detector + a Template helper that maps area → room key
ratioSensor(id, period, room) = sensor.<id>_room_ratio_<today|7d>_<room>% of time per room, for the donutssensorstate (%)sensor.mochi_room_ratio_today_kitchen_1History stats helpers, type ratio (below) — 2 per cat per room; rooms they never use can be left out
ROOM_RATIO[]room keys, labels and donut colours——kitchen_1, 1fl_living_room, living_west, dining_2, dining_room, genkan, office_1, tatami_1, master_bedroom, cat_room, 2fl_living_room, stairway_2, hall_east_2rename to their room keys (keep them identical to the presence sensor's states)
ROOM_SAY / CAT_PERCH (houseArt.ts)what a cat "says" and where it pops up on the house drawing, per room key——genkan: at(1090, 520)keep keys in sync with ROOM_RATIO
SPOTTINGS[]"Spotted by the cameras": a picture file + cam (input_text: camera name) + time (input_datetime)input_text, input_datetimestateinput_text.sk_spotting_1_camera, input_datetime.sk_spotting_1_time (and _2)an automation that saves camera.snapshot to www/snapshots/animals/spotting-1.jpg and sets the two helpers when an animal is detected; or delete the block in views/AnimalsView.tsx
LITTER_BOXES[]litter box status (state: clear/occupied/cleaning)sensorstatesensor.sk_litter_cat_room_state / _occupied, sensor.sk_litter_downstairs_state / _occupied, sensor.sk_litter_tatami_state / _occupied, sensor.sk_litter_master_state / _occupieddelete rows / the LitterBoxes component

CAT_ACTIVITY_VIEW is unused.

Vacuum — src/components/vacuum/vacuumModel.ts, plan in src/config/groundPlan.ts

KeyDomainReadsCallsExample idsIf they don't have it
VACvacuumstate, battery_level, supported_featuresvacuum.start, pause, stop, return_to_base, locatevacuum.sk_maruno robot → remove vacuum from src/app/views.tsx and the vacuum status from Sidebar.tsx
MODEselectstate, optionsselect.select_optionselect.sk_maru_clean_modewithout it the picker falls back to Vacuum / Mop / Vacuum+Mop buttons that do nothing — remove the mode row in views/VacuumView.tsx
OP_STATE, OP_ERROR, IDENTIFYsensor, sensor, buttonstate(IDENTIFY: button.press)sensor.sk_maru_state, sensor.sk_maru_error, button.sk_maru_identifyoptional
AREA_SHAPES (groundPlan.ts)the floor plan drawn on the Vacuum page, in metres——"Tatami Room": [[0,-12],[5,-12],[5,-8.5],[0,-8.5]]redraw from their floor plan (rectangles are fine)

Music — src/components/music/useMusicData.ts, src/lib/music.ts, src/components/shell/MediaBar.tsx

WhatDomainReadsCallsExample ids
speakers per room (SPEAKERS: name + spk) and the now-playing barmedia_playerstate, media_title, media_artist, entity_picture, media_position(_updated_at), media_duration, volume_level, group_members, shuffle, sourcemedia_player.media_play_pause, media_next_track, media_previous_track, media_seek, volume_set, shuffle_set, play_media, join, unjoin, select_sourcemedia_player.sk_living_speaker, media_player.sk_family_speaker, media_player.sk_master_speaker, media_player.sk_study_speaker, media_player.sk_pinkroom_speaker
music-service account player (SP_PLAYER) + search and favouritesmedia_player + a custom musicplus integrationservice responsesmusicplus.search_tracks/_albums/_artists/_playlists, get_track_favorites, get_playlist_favorites, get_player_recent_tracks, save_track_favorites, remove_track_favoritesmedia_player.sk_music_account

Playback works with any media_player. The search/favourites tabs need a music integration that offers those response services; most homes don't have musicplus — then those tabs stay empty: hide them in components/music/MusicPlayer.tsx, or rewire them to media_player.browse_media / media_player.search_media. Internet radio uses media_player.browse_media with the Radio Browser integration.

Network — src/lib/networkData.ts (makeNet())

A drawn rack of network gear (gateway, switches, recorder, power strip, access points, servers, the internet line, cameras) where each device's ent map names sensors for cpu, mem, temp, clients, uptime, latency and power (63 made-up ids such as sensor.sk_net_wan1_latency, sensor.sk_net_lan_clients, sensor.sk_net_pdu_power). Match them to their network integration's sensors (router/switch/AP integrations expose most of these), drop devices they don't have (the pictures are generic, in static/local/network/assets/gear/), or remove network from src/app/views.tsx.

Helpers they may need to create

Create helpers in Home Assistant's UI (Settings → Devices & services → Helpers → Create helper), and scripts and automations in Settings → Automations & scenes (switch the editor to Edit in YAML and paste). With a token you can also create them through the config API (POST /api/config/script/config/<id>, POST /api/config/automation/config/<id>) — don't hand-edit configuration.yaml. The YAML below is the definition to paste; it uses the 2024.10+ syntax (triggers: / actions:; older versions: trigger: with platform: and service:). Replace living / ids with theirs.

A/C on a room card — a Number helper input_number.living_ac_target (16–30, step 1, °C), then:

yaml
# Script "Living A/C on/off" (script id: living_ac_toggle) — the card's fan button calls script.turn_on
sequence:
  - if:
      - condition: state
        entity_id: climate.living_ac
        state: "off"
    then:
      - action: climate.set_temperature
        target:
          entity_id: climate.living_ac
        data:
          hvac_mode: cool
          temperature: "{{ states('input_number.living_ac_target') | float(24) }}"
    else:
      - action: climate.turn_off
        target:
          entity_id: climate.living_ac
mode: single
yaml
# Automation "Living A/C follows its target" — the card's − / + change the helper
triggers:
  - trigger: state
    entity_id: input_number.living_ac_target
conditions:
  - condition: not
    conditions:
      - condition: state
        entity_id: climate.living_ac
        state: "off"
actions:
  - action: climate.set_temperature
    target:
      entity_id: climate.living_ac
    data:
      temperature: "{{ trigger.to_state.state | float }}"
mode: restart

Curtains from a cover (no code change) — for each curtain, two Template helpers and a script:

  • Template helper → Switch "Living curtains open": state template {{ is_state('cover.living_curtains', 'open') }}, turn on: cover.open_cover, turn off: cover.close_cover (target cover.living_curtains) → use as open / ROOMS[].blackout.
  • Template helper → Sensor "Living curtains direction": state template {{ states('cover.living_curtains') if states('cover.living_curtains') in ['opening', 'closing'] else 'idle' }} → use as dir.
  • Script living_curtains_toggle: sequence: [{action: cover.toggle, target: {entity_id: cover.living_curtains}}], mode: single → use as script.

Energy totals — if they only have power sensors, first an Integral helper per power sensor (method Left, metric prefix k, time unit hours) to get kWh. Then Utility Meter helpers on each kWh sensor with cycles daily, weekly, monthly (sensor.grid_import_daily …). Savings = Template helper (Sensor), e.g. today: {{ ((states('sensor.solar_energy_daily') | float(0) - states('sensor.grid_export_daily') | float(0)) * 0.31 + states('sensor.grid_export_daily') | float(0) * 0.16) | round(2) }} with their tariff and feed-in rate; yesterday/week/month/year come from Utility Meters on that savings sensor (Utility Meter tracks the last period in its last_period attribute — a Template helper can expose it).

Cat room time — History stats helper per cat per room (Settings → Helpers on recent versions; on older ones the history_stats sensor platform): entity sensor.mochi_room, state kitchen_1, type ratio, start {{ today_at() }}, end {{ now() }} (for _today_) or start {{ now() - timedelta(days=7) }} (for _7d_). Name it so the entity id becomes sensor.mochi_room_ratio_today_kitchen_1.

Step 3 — Go live

The kit is already wired into this package (src/main.tsx switch, src/main.live.tsx, src/main.panel.tsx, src/ha/wsBackend.ts, hassBackend.ts, authFetch.ts, vite.config.ts, .env.example, panel-loader.js). Read live/README.md — it has the full steps and troubleshooting. In short:

  1. cp .env.example .env.live.
  2. Mode C first (development against their Home Assistant): set VITE_HA_TARGET and VITE_HA_TOKEN, run npm run dev:live, open the printed address. The dev server proxies /api (REST + WebSocket) to Home Assistant and adds the token. Walk every page while you map entities (Step 2). Keep it on their network.
  3. Mode A (recommended for the wall): keep BASE=/local/dashboard/, npm run build:live, copy everything in dist/ to <config>/www/dashboard/ on the Home Assistant machine (Samba / File editor / Studio Code Server app, or scp), open http(s)://<their-ha>:8123/local/dashboard/index.html on the tablet and log in once with Home Assistant's own login page. No token is ever baked into a build (the config refuses).
  4. Mode B (a page in the sidebar): mode A's build + panel-loader.js in the same folder + the panel_custom: block from the top of panel-loader.js in configuration.yaml (the one YAML edit; restart Home Assistant).

Before the first live build, also do these app-specific changes:

  • Location and clock — src/lib/sky.ts: set LAT, LON to their home; the sun and sky read the tablet's wall clock as the home's local time through TYO (a fixed +9 h for the demo's Tokyo). Replace const TYO = 9 * 3600000; with their offset — best, make it follow the tablet (correct through daylight saving): const TYO = -new Date().getTimezoneOffset() * 60000; and move it into wallMs(), tyoParts(), skyNowMs() and crossings() as a call (tzMs()) so it is re-read after a DST change.
  • Radar — src/components/home/RadarMap.tsx: LAT, LON, and the three map labels (points AIRPORT, SUMIDA, BAY and their <text> lines: move/rename or delete), plus the outline file (see "Make it theirs").
  • Delete the demo when you are done (optional — a live build already leaves it out): src/demo/, src/ha/mockBackend.ts, src/lib/mockApi.ts, src/main.demo.tsx, and the else { import("./main.demo") } branch in src/main.tsx. src/config/curtains.ts is then unused too.

Step 4 — Pictures

Every picture the app uses. keep = generic, fine in any home; replace = shows the made-up home (it still works as a placeholder, but it isn't their house).

PathSizeRoleKeep / replace
src/assets/house/house-day-cut.webp1800 × 791, transparentHome: the house and plot, three-quarter aerial, dayreplace
src/assets/house/house-night-body.webp, house-night-lines.webp1800 × 791, transparentnight body + white-pencil linesreplace (made by house)
src/assets/garden/garden-day.webp, garden-night.webp2000 × 886Garden: bird's-eye of the garden, house along the topreplace (night made by garden)
src/assets/rooms/<key>-col.webp, <key>-sk.webp — 18 rooms: tatami, living, kitchen, dining, atrium, corridor, salon, office, stairs, master, bath, closet, family, catroom, pink, nursery, bed2, bed4800 × 447Rooms: colour drawing + its pencil versionreplace (sk made by room)
src/assets/plates/<key>-day.webp, -night.webp — 17 windows: dining, salon, living, atrium, atriumback, study, family, masterken, masterbath (1200 × 896, 4:3); tatami, catroom, nursery, guest2, guest4, familytv (896 × 1200, 3:4); pinkroom, mastermika (1264 × 848, 3:2)as listedCurtains: the view out of each windowreplace (night made by plate)
src/assets/plates/thumb/<key>.webp280 × 210the window chips under the curtainreplace (made by plate)
src/assets/cats/<cat>.webp — mochi, yuki, sora, kuro, hana, ren≈ 200 × 420, transparentAnimals + pop-ups: each cat sitting, facing youreplace (their pets)
src/assets/thumbs/*.webp — home, rooms, curtains, animals, garden, energy, network, translate168 × 112sidebar menu picturesreplace (made by thumbs)
src/assets/translate/two-people.webp1100 × 504only for the optional Translate pagekeep
static/local/snapshots/animals/spotting-1.jpg, -2.jpg1273 × 720"Spotted by the cameras" photosreplace (their automation writes them)
static/local/network/assets/gear/*.png (11)≈ 330 px widedrawn network gearkeep
static/local/robot/*.svg, lights-on2/off2.svg, diningon/off.svg, litter-*.svg, icon.pngiconsvacuum states, feature lamps, litter, app iconkeep (swap icon.png for their own if they like)
static/demo/music/cover-01…12.jpg, static/demo/camera/*.jpg320 × 320, 1273 × 720(demo only)delete with the demo
fonts: src/assets/fonts/*.woff2, static/fonts/manrope.woff2—Shantell Sans, Trirong, Manrope (OFL)keep

The style (every picture of a series must share it): a page from an architect's sketchbook on white drawing paper — confident graphite pencil line work with light hatching and faint construction lines, coloured in with soft coloured pencils, loose strokes, some paper left white; lamps glowing honey-gold; no people, no animals, no text. The exact prompts used for the made-up home are in tools/prompts.json — keep each "Style" tail, change only the subject.

Prompt templates (adapt the subject; keep the tail word for word):

  • Room (16:9, ~2k): "A wide landscape illustration of {their room: the main furniture, the lamps, the window treatment, materials and colours}. Style: a page from an architect's sketchbook on white drawing paper - confident graphite pencil line work with light hatching and faint construction lines, then coloured in with coloured pencils in soft natural colours, every lamp switched on and glowing honey-gold with warm light pooling around it. Loose pencil strokes, some paper left white. The main furniture sits across the middle band of the picture. No people, no animals, no text or lettering anywhere, no border, no frame." (Draw rooms lit: the colour layer is the "on" look.)
  • Window plate (4:3, 3:4 or 3:2 to match the window): "The view out through a floor-to-ceiling window of {their house type} seen from inside the room: slim dark charcoal window frames divide the glass into {N} tall panes, and the frames run edge to edge so the window fills the whole picture; beyond the glass, {what that window looks out on}, on a bright clear day. Style: an architect's sketchbook drawing on white paper - confident graphite pencil line work with light hatching, coloured in with soft coloured pencils (fresh greens, clear sky blue, warm timber, pale stone), loose strokes, some paper left white. No curtains, no blinds, no people, no animals, no text, no border."
  • House (21:9, ~4k, transparent background): "An elevated three-quarter bird's-eye drawing of one whole plot, drawn as a single isolated vignette on a transparent background (no sky, nothing outside the plot). {Their house: storeys, cladding, roof (solar panels?), windows per floor, front door, garage, front garden; the back garden wrapping behind it}. Style: …same tail… Daylight, every window unlit. No people, no animals, no cars, no text." Windows must be drawn unlit — the app lights them.
  • Garden (21:9, ~4k): "A high bird's-eye three-quarter view looking down into the back garden of {their house}, drawn edge to edge in a wide landscape format. Along the top of the picture runs the back of the house … {every feature that has a light: pool, trees, paths, lanterns}. Style: …"
  • Cat (2:3, ~1k, transparent): "{Their cat's coat, eye colour and build}, sitting upright and facing the viewer, the whole body from the tips of the ears to the paws, the tail curled round the paws, drawn as a single isolated sketch on a transparent background: confident graphite pencil outlines and light hatching, coloured in with soft coloured pencils, loose strokes, a faint pencil shadow under the paws. A gentle, sweet expression. Nothing else in the picture: no background, no text, no border."

Processing — tools/sketchbook_art.py (Python 3.8+ and Pillow: pip install Pillow; run from the package root; writes into src/assets/…; --out DIR to write elsewhere first). Usage is printed with no arguments.

sh
python3 tools/sketchbook_art.py room   living.png living          # → rooms/living-col.webp + rooms/living-sk.webp
python3 tools/sketchbook_art.py plate  dining-view.png dining 4:3 # → plates/dining-day/-night.webp + plates/thumb/dining.webp
python3 tools/sketchbook_art.py cutout house.png house-cut.png    # only if the generator can't do transparency
python3 tools/sketchbook_art.py house  house-cut.png              # → house/house-day-cut + night-body + night-lines
python3 tools/sketchbook_art.py garden garden.png                 # → garden/garden-day + garden-night
python3 tools/sketchbook_art.py cat    cat-mochi.png mochi        # → cats/mochi.webp
python3 tools/sketchbook_art.py thumbs network-corner.png         # → thumbs/*.webp (after the others exist)
python3 tools/sketchbook_art.py grid   src/assets/house/house-day-cut.webp grid.png 50   # a measuring grid

The pencil (sk) and night layers are derived from the same drawing, so they always line up with it. Pictures are cropped to fill (cover), centred — frame the subject in the middle band.

Anchors to re-measure after a new house or garden drawing (open tools/measure.html in a browser, load the picture, click): config/houseArt.ts — WINDOWS (4+ points per window, polygon mode → pts: P([...]), plus the light ids), POOL_PTS, GARAGE, PAVILION, ENTRANCE_LAMP, ENERGY_AT (solar panels, battery, middle of the house), ENERGY_LABEL (free space for each note), CAT_PERCH (one per cat-room key: a window sill or roof edge in that room). config/garden.ts — each light's x/y (fraction mode). Room cards, plates, cats and thumbnails need no anchors. Unused anchors can stay; the house art size is fixed by ART_W/ART_H (1800 × 791, set by the tool).

Generators (the made-up home was drawn with a paid AI image service — GPT Image, text prompts only; most people don't have one, and any of these work):

  • Best match for this look, free: ChatGPT's free image tier or Microsoft Designer / Bing Image Creator (both follow long style prompts well and keep pencil hatching); Google Gemini (good at "keep the same style as this picture"); Ideogram, Leonardo, Adobe Firefly, Krea (free daily credits).
  • Local, free, unlimited: FLUX.1 [schnell] or SDXL in ComfyUI or Forge (add a pencil-sketch LoRA, fix the seed per series), Draw Things on a Mac/iPad. Transparent backgrounds: render on white, then cutout.
  • Without AI: their own photos of each room through a "pencil sketch + watercolour" filter (the room command then makes the graphite layer — keep camera height and angle similar across rooms); a free 3D model of the house in Sweet Home 3D or Blender rendered from the three-quarter angle and run through the same filter (also great for the garden); or an illustrator.
  • Keep a series consistent: one style tail for everything; generate the house first and use it as the style reference for the rest ("same style as this drawing"); same seed / same model / same quality for a series; similar eye height and lens for rooms; let the subject sit in the middle band. Their own photos of the house, rooms and pets are fine as image-to-image references — it's their home.

Step 5 — Build & run

  • npm run build:live (mode A/B) → dist/; npm run build = the demo. Host: Home Assistant's www/ (mode A/B, recommended — same origin, HA's login), or any static host for the demo.
  • Wall tablet: Android → Fully Kiosk Browser (start URL = the dashboard, keep screen on, motion detection to wake, auto-reload on error); iPad → Safari → Share → Add to Home Screen (runs full-screen, no browser bars), then Settings → Accessibility → Guided Access to lock it; Display → Auto-Lock: Never while charging. Keep the tablet on power.
  • Screen size: designed at 1376 × 1032 CSS px landscape (13" class); fine at 1180 × 820 (11"). Layout is fluid, not scaled; on smaller screens trim columns rather than shrink. Portrait is not supported.

Step 6 — Verify

Tick every line on the real tablet (or mode C on a computer):

  • Every page opens: Home, Rooms (both floors), Curtains & Sheers (every window chip), Animals, Garden, Energy, Network, Vacuum (minus the ones you removed) — no blank areas, no "—" where a value should be.
  • Home: the sky matches the real time of day at their home; sunrise/sunset times on the loop match their weather app; dragging the sun previews another hour and springs back.
  • Turning a light on in Home Assistant colours in its room card within a second; the right window glows on the house drawing after dusk; tapping a card toggles the right light, the slider dims it.
  • A/C − / + change the right target; the fan button switches the right unit; the climate list shows real states.
  • Each curtain button moves the right curtain; open/closed text on the room card matches.
  • Garden pins toggle the right lights; "All lights on/off" works.
  • Energy: the chart shows today's real curve; totals and savings are plausible numbers in their units/currency.
  • Weather label, temperature and rain/fog/snow effects match weather.*; calendar shows their events.
  • Music: play/pause/next/volume on the right speakers.
  • Browser console: no errors (devtools, or Safari's Web Inspector over USB for an iPad).
  • Performance on the tablet: page turns smooth; leave it running an hour — memory steady, clock still ticking.

Make it theirs

  • Names: room titles (config/rooms.ts name, TITLE/KEY maps in components/rooms/RoomCard.tsx), cat names (config/animals.ts), labels in config/entities.ts, the house name in index.html (<title>, app title meta) and the sidebar logo (components/shell/Sidebar.tsx); add Japanese/their language for new names in i18n/ja.ts.
  • Rooms: add or remove room objects; each needs a KEY (picture name) and its two pictures. Floors are the "1st"/"2nd" tabs (views/RoomsView.tsx).
  • Pages: src/app/views.tsx is the menu — delete entries they don't want, reorder, rename. Optional extra: a Translate page exists (views/TranslateView.tsx, not in the menu; it uses a speech-to-text endpoint at /whisper and the free MyMemory translation API).
  • Colours and fonts: src/styles/theme.css tokens; fonts in src/styles/fonts.css (--f-hand, --f-print).
  • Radar map: RadarMap.tsx LAT/LON + labels; src/lib/tokyoGeo.ts holds the sea, coast and river outlines as SVG paths in zoom-9 Web-Mercator pixels relative to the centre. For their area, fetch natural=coastline and waterway=river ways in a ~150 km box from OpenStreetMap (Overpass API), project each point with the file's lonToX/latToY at zoom 9 minus the centre's, and write the paths — or set the three exports to ""/[] to show the radar alone.
  • Currency and tariff: the ¥ sign in EnergyView.tsx (savings), and the tariff in their savings helpers.
  • Language: see "How it works → i18n".

Gotchas

  • Clock and timezone: the demo deliberately reads the tablet's wall clock as Tokyo time so every visitor sees a sky that matches the clock. Live, set LAT/LON and make TYO follow the tablet (Step 3) — otherwise the sky is hours off. The tablet must be set to the home's timezone.
  • iPad / WebKit: Safari throttles timers and three.js in background tabs and when the screen sleeps — after waking, the sky and clock catch up on the next minute (a visibilitychange handler refreshes the sky). Add to Home Screen for full-screen; old iPads (3 GB RAM) can struggle with the 3D curtain page — it is the heaviest view; open it on demand rather than as the start page.
  • Memory on big installations: keep per-view subscriptions (never subscribe to all states); the "Lights on" list polls /api/states every 30 s — on thousands of entities raise pollMs in useRealLights.
  • Mixed content: a dashboard on https:// cannot talk to http:// Home Assistant — serve it from HA itself (mode A) and the address matches.
  • Pictures: WebP needs iOS 14+. Keep the house drawing's background truly transparent (the sky must show through); large pictures (over ~2000 px) cost memory on tablets for no visible gain.
  • prefers-reduced-motion is honoured: animations and transitions collapse to instant, rain/snow draw one still frame. Turn it off on the tablet if they want the full effect.
  • Entities that don't exist draw nothing rather than crash, so a forgotten id looks like "nothing happens" — check the browser console and ha-entities.txt when a control seems dead.
  • Services with responses (weather.get_forecasts) in mode C go over REST because the dev proxy can mangle large WebSocket frames (already set up in main.live.tsx).

Licences

  • Code: MIT — see LICENSE ("Copyright (c) 2026 dashboards.casa").
  • Fonts: each under the SIL Open Font License 1.1, licence file next to it — Shantell Sans (src/assets/fonts/OFL-ShantellSans.txt), Trirong (src/assets/fonts/OFL-Trirong.txt), Manrope (static/fonts/OFL-Manrope.txt). Japanese fonts are loaded from Google Fonts at runtime (OFL as well).
  • The made-up home's pictures: made for this demo (generated from text prompts, no reference photos) — free to use as examples and placeholders.
  • npm packages (React, three.js, Vite, @mdi/js icons, home-assistant-js-websocket …): their own licences, in node_modules/<package>/LICENSE after npm install.
  • Radar tiles: RainViewer's free public API — check its current terms (attribution) before a public deployment.