dashboards.casa

Mobile

Code and instructions handoff

Everything an AI coding agent needs to rebuild Mobile 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 11.0 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 Mobile, a wall-tablet dashboard for Home Assistant, in someone else's home. Everything you need is in this package: the full app source, the made-up demo home it ships with, a go-live kit, picture tools and this guide. Work through it in order; you should not need to ask the dashboard's authors anything. The person who gave you this package is not necessarily a developer — explain what you do in plain words and ask them only about their home (which entities, which rooms, which pictures).

What you're building

The demo home at dusk

A full-screen, touch-first dashboard drawn like a Calder mobile: the house hangs inside a wire hoop, the sun (by night the moon) is a bead you can drag round the hoop to scrub through the day, and every card, tag and cat portrait hangs on a wire and sways — gently on a calm day, wildly in a storm. Eight views: Home (house + sky, clock and weather, air quality per area, the A/C list, "coming up" calendar, rain radar, energy tags, a cat that drops in to say which room it is in), Rooms (one card per room: light + dimmer, A/C, curtains, lock, temperature), Curtains & Sheers (a 3-D curtain in front of a painting of that room's window view, day/night/weather), Animals (where each pet has been today / this week, litter boxes, latest camera spottings), Garden (a painting of the garden with every garden light as a tappable pin), Energy (solar, battery, grid and the house as a balance, power today, totals, savings), Network (servers, switches, access points, cameras) and Vacuum (a floor plan and the robot). A top strip (air quality, lights on, humidity, power) and a media bar (speaker, robot, a door lock) frame every view. English ⇄ 日本語 built in.

  • They need: Home Assistant 2024.1 or newer (it uses only the standard REST and WebSocket APIs), a wall tablet or any screen (made for 1376 × 1032 CSS px, an 11-inch tablet in landscape; it scales down to fit smaller screens), and a computer with Node.js 18+ to build it. Nothing else — no HACS card, no cloud.
  • Time: about an hour to run the demo and go live with a few rooms; a few hours to map a whole house; pictures of their own home are an extra evening (or keep the made-up pictures to start).

What's in this package

HANDOFF.md            this guide            README.md   3-line summary            LICENSE   MIT
package.json          exact versions we build with (npm install)
index.html            the page shell            tsconfig.json   TypeScript settings
vite.config.ts        dev server + builds (demo / live), BASE = where a build is hosted
.env.example          copy to .env.live for live mode (address, token for dev only, BASE)
panel-loader.js       mode B: the tiny module Home Assistant loads for a sidebar panel
live/README.md        the go-live kit's own guide (modes A, B, C in detail, troubleshooting)
docs/still.jpg        the picture above
tools/                picture tools: fit.py (resize/crop to a slot), cutout.py (house cut-out),
                      nightify.py (no-AI night versions), measure.html (click anchors), prompts.md
static/               files served as-is: local/ (pets, room pictures, spottings, network gear, SVGs, icon),
                      demo/ (demo-only camera stills and music covers), fonts/ (+ OFL licence)
src/
  main.tsx            start-up switch: demo (default) · live · sidebar panel
  main.demo.tsx       the demo entry (made-up home, no network at all)
  main.live.tsx       live entry, modes A and C (WebSocket to Home Assistant)
  main.panel.tsx      live entry, mode B (inside Home Assistant's sidebar)
  App.tsx             language provider + the shell
  app/views.tsx       the list of views (remove one here)
  config/             ★ every entity id, room, light, cat, curtain, plan and picture anchor lives here
  ha/                 HaBackend interface (types.ts), HaProvider hooks, mockBackend (demo),
                      wsBackend / hassBackend / authFetch (live)
  views/              one file per view            components/   cards, popups, shell, charts, 3-D curtains
  kinetic/            the mobile's physics (engine.ts), sky / sun / moon maths (sky.ts), weather (forecast.ts)
  i18n/               t("English") → ja.ts dictionary, Japanese web-font loader
  lib/                formatting, colour bands, history/calendar hooks, network data, music, demo REST mock
  demo/               the made-up home: snapshot of states, synthetic day (solar, battery, cats), calendar,
                      music, the network sandbox, the time/weather bridge — delete when live
  styles/             theme tokens (theme.css), fonts, app-wide CSS
  assets/             the made-up home's pictures used by the code (house, garden, rooms, cats, curtains)

Run the demo first

sh
npm install
npm run dev            # prints http://localhost:5173/ — open it

The made-up home ("Sakura Home", a family house in Tokyo) runs with no Home Assistant at all: src/demo/ fakes the states, a whole synthetic day of energy and pet movements, calendars and music, and a sandbox blocks every network request to Home Assistant. Try the URL parameters: ?t=21:30 (a time of day), ?wx=rainy (sunny, partlycloudy, cloudy, rainy, pouring, lightning-rainy, fog, snowy, windy), ?lang=ja. Drag the sun round the hoop. npm run build makes the same demo as static files in dist/. If this works, the toolchain is fine — now connect their house.

How it works

  • Data flow. Every view talks to Home Assistant through one interface, HaBackend (src/ha/types.ts): getStates, subscribe(ids, cb), callService(domain, service, data, target), callServiceResponse(…) (services that return data, e.g. weather.get_forecasts) and browseMedia. src/ha/HaProvider.tsx turns it into hooks — useEntity(id), useEntities(ids), useCallService(), useCallServiceResponse(), useBrowseMedia() — and each view subscribes only to the ids it shows. A few things come over REST with the same login: history (/api/history/period/…, charts and the pet timeline), calendars (/api/calendars/<id>), /api/states (the "lights on" list) and /api/template.
  • Demo vs live. src/main.tsx picks the entry at build time: the demo uses mockBackend (+ src/lib/mockApi.ts for the REST calls); live uses wsBackend (Home Assistant's WebSocket API through home-assistant-js-websocket, with Home Assistant's own login) or hassBackend (the hass object a sidebar panel receives). Views never know which. A live build contains no demo code.
  • Config. All entity ids, names and positions are in src/config/*.ts (plus a few constants named in Step 2). Every made-up id starts with sk_, so grep -rn "sk_" src --include=*.ts --include=*.tsx | grep -v src/demo lists every place to map.
  • The mobile. src/kinetic/engine.ts is a small spring simulation: each hanging thing is a Body (spring, damping, wind share); useSwing attaches one to an element. Wind comes from the weather entity's wind_speed / wind_bearing (Ambient.tsx); storms add gusts; taps kick. prefers-reduced-motion turns sway and big transitions off.
  • The sky. src/kinetic/sky.ts computes sun and moon (position, phase, sunrise/sunset) for the home's latitude and longitude from the tablet's clock; Ambient.tsx turns the sun's height into the wall colour (day → golden → dusk → night), shadow direction and a sun patch. Dragging the bead scrubs the sky (and the forecast) and glides back to now.
  • Weather. The weather entity's condition drives the hanging weather glyph, rain/snow/fog/lightning over the house, the curtain scenes and the sway; the hourly forecast (weather.get_forecasts) is shown while scrubbing ahead.
  • Pictures, layered and anchored. The house is three registered transparent plates (house-day, house-night, house-lit): the night plate fades in as the sun sets, and house-lit shows only inside the window boxes of rooms whose light is on (WINDOWS in src/config/house.ts, boxes as fractions of the painting). ENERGY_AT pins the solar / battery / house tags and CAT_PERCH where a cat hangs for each room. The garden is a day and a night painting with each light's position in src/config/garden.ts; the app paints the glows itself. Curtains are a three.js scene (src/components/curtains3d/engine.ts) in front of a day or night painting of the window view (src/config/curtainPlates.ts), with rain, lightning and haze done in shaders. Room cards use one ink sketch per room.
  • i18n. useT() returns t("English text"); src/i18n/ja.ts is a plain { "English": "日本語" } object; the language toggle sets data-lang on <html>. Japanese web fonts load only when Japanese is on (src/i18n/ja-fonts.ts).
  • Theme. CSS custom properties in src/styles/theme.css (--wall, --ink, --wire, --red, --yellow, --blue …, a night set under [data-phase="night"]); the primary colours of the hanging shapes are PAINT in src/kinetic/shapes.tsx.
  • The demo bridge. src/demo/bridge.ts lets a page that embeds the demo (an <iframe>) set the time and weather with postMessage — that is how dashboards.casa shows "what happens at night / in a storm". Demo only.

Step 1 — Inventory their Home Assistant

  1. Ask the person for their Home Assistant address and, for this step only, a long-lived access token (their profile → Security → Long-lived access tokens). Keep it out of every file you commit or build.
  2. List the entities:
    sh
    curl -s -H "Authorization: Bearer $TOKEN" http://homeassistant.local:8123/api/states \
      | jq -r '.[] | [.entity_id, .state, (.attributes.friendly_name // "")] | @tsv' | sort > states.tsv
    (or Developer Tools → States in the Home Assistant UI). Also note their areas (Settings → Areas).
  3. Domains this dashboard uses: light, switch, climate, sensor, weather, sun, calendar, media_player, vacuum, select, lock, script, button, input_number, input_select, input_boolean, input_text, input_datetime, camera (network view only).
  4. Match by meaning, not by name: for every row in the Step 2 tables find their entity (same domain, same unit), or decide to drop that card. Ask the person when two candidates fit. Write the result down as a two-column list (made-up id → their id) before you edit anything.

Step 2 — Map entities

Edit the files named in each table; replace each made-up id with theirs. Keep the structure (arrays, keys); delete rows for things they don't have; null is allowed where a table says so. Names shown on cards come from the config (name) or the entity's friendly_name. After mapping, run npm run dev:live (Step 3, mode C) and check every view.

A. Shell, top strip, weather, sun — src/config/entities.ts, src/components/vacuum/vacuumModel.ts

Config keyMade-up idWhat it isRead (state / attributes)CallsIf you don't have it
SHELL.mediamedia_player.sk_living_speakermain speaker (media bar + Music)state, media_title, media_artist, entity_picture, media_duration, media_position(_updated_at), volume_levelmedia_player.media_play_pause / media_next_track / media_previous_track / volume_setpoint it at any media_player; no speaker → remove <MediaBar>'s music block in src/components/shell/MediaBar.tsx
SHELL.vacuum / VACvacuum.sk_marurobot vacuumstate, battery_level, supported_features, friendly_namevacuum.start / pause / stop / return_to_base / locateremove the vacuum entry from src/app/views.tsx and the robot chip in MediaBar.tsx
SHELL.vacuumState / OP_STATEsensor.sk_maru_statevacuum's detailed status (optional)state—leave it: the vacuum's own state is used instead
MODEselect.sk_maru_clean_modecleaning mode select (optional)state, optionsselect.select_optionwithout it the chips show a default list that does nothing: delete the mode row in src/views/VacuumView.tsx
OP_ERRORsensor.sk_maru_errorvacuum error text (optional)state (no_error = fine)—leave it
IDENTIFYbutton.sk_maru_identify"find me" button (optional)—button.press (only if the vacuum lacks locate)leave it
SHELL.locklock.sk_kitchen_doordoor lock shown in the media barstate locked/unlockedlock.lock / lock.unlockdelete the lock block in MediaBar.tsx
TOPSTRIP.aqiOutsidesensor.sk_outside_aqiOutside AQIstate—any sensor of the same kind; a missing one shows "—"
TOPSTRIP.aqiInsidesensor.sk_downstairs_aqiDownstairs AQIstate—any sensor of the same kind; a missing one shows "—"
TOPSTRIP.lightsOnsensor.sk_lights_onLights Onnot read (the count comes from all light.* states, see M)—any sensor of the same kind; a missing one shows "—"
TOPSTRIP.humOutsidesensor.sk_outside_humidityOutside Humiditystate—any sensor of the same kind; a missing one shows "—"
TOPSTRIP.humDownstairssensor.sk_downstairs_humidityDownstairs Humiditystate—any sensor of the same kind; a missing one shows "—"
TOPSTRIP.humUpstairssensor.sk_upstairs_humidityUpstairs Humiditystate—any sensor of the same kind; a missing one shows "—"
TOPSTRIP.powersensor.sk_house_powerHouse Powerstate (kW)—any sensor of the same kind; a missing one shows "—"
WEATHERweather.sk_tokyoweather (Home clock line, sky, sway, radar wind, curtain scenes)state = condition, temperature, humidity, wind_speed, wind_bearing, cloud_coverageweather.get_forecasts {type: hourly} (response)any weather entity (Met.no is built in)
—sun.sunthe sunstate above_horizon/below_horizon, elevation, azimuth—built into Home Assistant
POOL_PROBE (in ClimateList.tsx)sensor.sk_pool_temperaturewater temperature shown on the pool-heater rowstate (°C)—change the constant or delete the pool-heater row

B. Home — A/C list CLIMATE (src/config/entities.ts)

Read: state (off, cool, heat, dry, fan_only, auto), current_temperature, temperature (target). Calls: climate.set_hvac_mode {hvac_mode: off|cool} (tap), climate.set_temperature {temperature} (the setpoint sheet). pin: "top"|"bottom" keeps a unit at the top/bottom of its list. Not there → delete the row.

#Name on the cardMade-up idFloor
1Dining Roomclimate.sk_ac_dining1F
2Corridor 1FLclimate.sk_ac_corridor1F
3Studyclimate.sk_ac_study1F
4Living 1FLclimate.sk_ac_living1F
5Tatami Roomclimate.sk_ac_tatami1F
6Stairway 2Fclimate.sk_ac_stairway2F · pinned top
7Guest Room 2climate.sk_ac_guest22F
8Nurseryclimate.sk_ac_nursery2F
9Guest Room 4climate.sk_ac_guest42F
10Studioclimate.sk_ac_studio2F
11Family Roomclimate.sk_ac_family2F
12Master Bed 1climate.sk_ac_master12F
13Master Bed 2climate.sk_ac_master22F
14Closetclimate.sk_ac_closet2F
15Pool Heaterclimate.sk_pool_heater2F · pinned bottom

C. Home — Air panel AREAS (src/config/entities.ts)

Read: each sensor's state (°C, %, AQI number, ppm). co2 is optional (leave the key out). Not there → delete the row.

AreaTemperatureHumidityAQICO₂
Outsidesensor.sk_outside_temperaturesensor.sk_outside_humiditysensor.sk_outside_aqi—
Upstairssensor.sk_upstairs_temperaturesensor.sk_upstairs_humiditysensor.sk_upstairs_aqisensor.sk_upstairs_co2
Downstairssensor.sk_downstairs_temperaturesensor.sk_downstairs_humiditysensor.sk_downstairs_aqisensor.sk_downstairs_co2
Master Bedroomsensor.sk_master_temperaturesensor.sk_master_humiditysensor.sk_master_aqisensor.sk_master_co2
Nurserysensor.sk_nursery_temperaturesensor.sk_nursery_humiditysensor.sk_nursery_aqisensor.sk_nursery_co2
Guest Room 2sensor.sk_guest2_temperaturesensor.sk_guest2_humiditysensor.sk_guest2_aqisensor.sk_guest2_co2
Guest Room 4sensor.sk_guest4_temperaturesensor.sk_guest4_humiditysensor.sk_guest4_aqisensor.sk_guest4_co2
Tatami Roomsensor.sk_tatami_temperaturesensor.sk_tatami_humiditysensor.sk_tatami_aqisensor.sk_tatami_co2
Studiosensor.sk_studio_temperaturesensor.sk_studio_humiditysensor.sk_studio_aqisensor.sk_studio_co2
Studysensor.sk_study_temperaturesensor.sk_study_humiditysensor.sk_study_aqisensor.sk_study_co2

D. Home — "Coming up" CALENDARS (src/config/entities.ts)

Read over REST: GET /api/calendars/<id>?start=…&end=… (summary, start, end, location). Colour = the dot colour. Use any calendar entities (Local Calendar, Google, CalDAV …); keep 1–6.

Made-up idNameColour
calendar.sk_familyFamily#ffffff
calendar.sk_japan_holidaysHolidays in Japan#ffb4a8
calendar.sk_birthdaysBirthdays#dcae65
calendar.sk_schoolSchool#9ac1e8
calendar.sk_catsCats#a9d6a0

E. Home — the house on the hoop (src/config/house.ts, HouseStage.tsx)

WINDOWS[].light: when that light is on and the sun is down, its window boxes on the house plate glow (state only). ENERGY_AT pins the three energy tags (sensors in F). Cats come from G. Not there → delete the window row.

Window groupLight (made-up id)Boxes on the plate
TATAMI ROOMlight.sk_1f_tatami1
GENKANlight.sk_1f_corridor2
GUEST ROOM 2light.sk_2f_guest21
HALL 2Flight.sk_1f_stairs1
GUEST ROOM 4light.sk_2f_guest41
CAT ROOMlight.sk_2f_catroom1
PINK ROOMlight.sk_2f_pinkroom1

F. Energy (src/views/EnergyView.tsx E, HouseStage.tsx E)

Read: states only. Power sensors in kW (grid positive = importing, negative = exporting; battery power positive = discharging), battery level in %, energy totals in kWh, savings in your currency. History for the "Power today" chart comes from GET /api/history/period/… (the recorder). Missing totals → make them with Utility Meter helpers (YAML in Step 2 notes); no solar/battery at all → remove energy from src/app/views.tsx and the three tags in HouseStage.tsx.

KeyMade-up idWhat it is
gridPowersensor.sk_grid_powerGrid Power
solarPowersensor.sk_solar_powerSolar Power
battsensor.sk_battery_levelHome Battery
loadPowersensor.sk_house_powerHouse Power
gridKwh.todaysensor.sk_grid_import_dailyGrid Import Today
gridKwh.weeksensor.sk_grid_import_weeklyGrid Import This Week
gridKwh.monthsensor.sk_grid_import_monthlyGrid Import This Month
solarKwh.todaysensor.sk_solar_energy_dailySolar Today
solarKwh.weeksensor.sk_solar_energy_weeklySolar This Week
solarKwh.monthsensor.sk_solar_energy_monthlySolar This Month
battKwh.todaysensor.sk_battery_export_dailyBattery Out Today
battKwh.weeksensor.sk_battery_export_weeklyBattery Out This Week
battKwh.monthsensor.sk_battery_export_monthlyBattery Out This Month
savings.todaysensor.sk_savings_todayEnergy Savings Today (JPY)
savings.yesterdaysensor.sk_savings_yesterdayEnergy Savings Yesterday (JPY)
savings.weeksensor.sk_savings_last_weekEnergy Savings Last Week (JPY)
savings.monthsensor.sk_savings_last_monthEnergy Savings Last Month (JPY)
savings.ytdsensor.sk_savings_ytdEnergy Savings YTD (JPY)

G. Animals (src/config/animals.ts)

Cats: one room-tracking sensor per pet, sensor.<id>_room (state = a room key from ROOM_RATIO, e.g. kitchen_1; away = outside), plus optional share-of-time sensors sensor.<id>_room_ratio_<7d|today>_<room key> (state = percent) for the donut. History of sensor.<id>_room feeds the timeline. Any presence source works (BLE tags + room presence, a microchip cat flap, cameras with object detection); the ratio sensors can be History Stats helpers (YAML in the notes below).

Cat id (CATS)NameRoom sensorPortrait
mochiMochisensor.mochi_room/local/pets/mochi.webp
yukiYukisensor.yuki_room/local/pets/yuki.webp
soraSorasensor.sora_room/local/pets/sora.webp
kuroKurosensor.kuro_room/local/pets/kuro.webp
hanaHanasensor.hana_room/local/pets/hana.webp
renRensensor.ren_room/local/pets/ren.webp
Room key (ROOM_RATIO)LabelRoom keyLabel
kitchen_1Kitchen1fl_living_roomPantry
living_westLiving Room 1FLdining_2Dining Room
dining_roomSalongenkanGenkan
office_1Studytatami_1Tatami
master_bedroomMastercat_roomCat Room
2fl_living_roomFamily Roomstairway_2Stairway 2F
hall_east_2Hall East 2F
Config keyMade-up idWhat it isReadIf you don't have it
SPOTTINGS[0]input_text.sk_spotting_1_camera, input_datetime.sk_spotting_1_timelatest cat spotting #1: camera name + time; picture /local/snapshots/animals/spotting-1.jpgstatedelete the entry (the card hides)
SPOTTINGS[1]input_text.sk_spotting_2_camera, input_datetime.sk_spotting_2_timelatest cat spotting #2: camera name + time; picture /local/snapshots/animals/spotting-2.jpgstatedelete the entry (the card hides)
LITTER_BOXES Cat Roomsensor.sk_litter_cat_room_state, sensor.sk_litter_cat_room_occupiedlitter box status / occupiedstatedelete the entry
LITTER_BOXES Downstairssensor.sk_litter_downstairs_state, sensor.sk_litter_downstairs_occupiedlitter box status / occupiedstatedelete the entry
LITTER_BOXES Tatamisensor.sk_litter_tatami_state, sensor.sk_litter_tatami_occupiedlitter box status / occupiedstatedelete the entry
LITTER_BOXES Mastersensor.sk_litter_master_state, sensor.sk_litter_master_occupiedlitter box status / occupiedstatedelete the entry

H. Rooms (src/config/rooms.ts ROOMS, ROOM_AC)

Per room card. light = a light group (tap = light.toggle, the dimmer = light.turn_on {brightness_pct}), climate (shows current_temperature), sheer / blackout = on/off entities that say whether the sheers / curtains are open (text only), temp / humidity sensors, lock (icon), featureLight = a second light with its own on/off drawing (light.turn_on/turn_off). Any field may be null. ROOM_AC[name] = the A/C chip: script toggles the unit (script.turn_on), helper is an input_number setpoint (input_number.increment / decrement). Picture: photo.

CardFloorLightClimateSheers / curtains stateTemp / humidityLock / feature lightA/C chip (script · setpoint)
TATAMI ROOM1stlight.sk_1f_tatamiclimate.sk_ac_tatamiswitch.sk_tatami_sheers_open_led / switch.sk_tatami_curtains_open_led— / —— / —script.sk_tatami_ac_toggle · input_number.sk_tatami_ac_target
LIVING ROOM1stlight.sk_1f_livingclimate.sk_ac_livingswitch.sk_living_sheers_open_led / switch.sk_living_curtains_open_ledsensor.sk_living_temperature / sensor.sk_living_humidity— / —script.sk_living_ac_toggle · input_number.sk_living_ac_target
KITCHEN1stlight.sk_1f_kitchen—— / —— / —lock.sk_kitchen_door / ——
DINING ROOM1stlight.sk_1f_diningclimate.sk_ac_diningswitch.sk_dining_sheers_open_led / switch.sk_dining_curtains_open_led— / sensor.sk_hall_1f_humidity— / light.sk_1f_dining_haloscript.sk_salon_ac_toggle · input_number.sk_salon_ac_target
ATRIUM1stlight.sk_1f_atrium—switch.sk_atrium_sheers_open_led / switch.sk_atrium_curtains_open_ledsensor.sk_atrium_temperature / sensor.sk_atrium_humidity— / ——
CORRIDOR1stlight.sk_1f_corridor—— / —— / sensor.sk_hall_1f_humidity— / ——
SALON1stlight.sk_1f_salonclimate.sk_ac_diningswitch.sk_salon_sheers_open_led / switch.sk_salon_curtains_open_led— / sensor.sk_hall_1f_humidity— / light.sk_1f_salon_haloscript.sk_salon_ac_toggle · input_number.sk_salon_ac_target
STUDY1stlight.sk_1f_studyclimate.sk_ac_study— / —sensor.sk_study_temperature / sensor.sk_study_humidity_2lock.sk_study_door / —script.sk_study_ac_toggle · input_number.sk_study_ac_target
STAIRS1stlight.sk_1f_stairsclimate.sk_ac_stairway— / —— / sensor.sk_hall_2f_humidity— / light.sk_1f_stairs_haloscript.sk_stairs_ac_toggle · input_number.sk_stairs_ac_target
MASTER BEDROOM2ndlight.sk_2f_masterclimate.sk_ac_master1— / —sensor.sk_master_temperature / sensor.sk_bedroom_hall_humiditylock.sk_master_door / —script.sk_master_ac1_toggle · input_number.sk_master_ac1_target<br>script.sk_master_ac2_toggle · input_number.sk_master_ac2_target
MASTER BATHROOM2ndlight.sk_2f_bath—— / —— / —— / ——
WALK-IN CLOSET2ndlight.sk_2f_closetclimate.sk_ac_closet— / —— / sensor.sk_bedroom_hall_humidity— / —script.sk_closet_ac_toggle · input_number.sk_closet_ac_target
FAMILY ROOM2ndlight.sk_2f_familyclimate.sk_ac_family— / —sensor.sk_family_temperature / sensor.sk_hall_2f_humidity— / —script.sk_family_ac_toggle · input_number.sk_family_ac_target
CAT ROOM2ndlight.sk_2f_catroom—— / —sensor.sk_cat_room_temperature / sensor.sk_cat_room_humidity— / ——
PINK ROOM2ndlight.sk_2f_pinkroomclimate.sk_ac_studioswitch.sk_pinkroom_sheers_open_led / switch.sk_pinkroom_curtains_open_ledsensor.sk_studio_temperature / —lock.sk_pinkroom_door / —script.sk_pinkroom_ac_toggle · input_number.sk_pinkroom_ac_target
NURSERY2ndlight.sk_2f_nurseryclimate.sk_ac_nurseryswitch.sk_nursery_sheers_open_led / switch.sk_nursery_curtains_open_ledsensor.sk_nursery_temperature / —— / —script.sk_nursery_ac_toggle · input_number.sk_nursery_ac_target
GUEST ROOM 22ndlight.sk_2f_guest2climate.sk_ac_guest2switch.sk_guest2_sheers_open_led / switch.sk_guest2_curtains_open_led— / —— / —script.sk_guest2_ac_toggle · input_number.sk_guest2_ac_target
GUEST ROOM 42ndlight.sk_2f_guest4climate.sk_ac_guest4switch.sk_guest4_sheers_open_led / switch.sk_guest4_curtains_open_ledsensor.sk_guest4_temperature / —— / —script.sk_guest4_ac_toggle · input_number.sk_guest4_ac_target

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

Each room has one or two covers. scripted: open = an on/off entity that is on while that cover is open, dir = an input_select the script sets to opening / closing / idle while it runs (optional), script = the script that toggles it (script.turn_on). buttons: openBtn / closeBtn = button entities (button.press), openLed / closeLed = on/off entities that show the last direction. With plain cover entities, the simplest route is a toggle script per cover (YAML in the notes) — or edit pressCover in CurtainsSheersView.tsx to call cover.toggle. outdoor: true rooms also follow the weather. Picture: src/config/curtainPlates.ts by key.

Room (key)FloorCoverKindEntities
Dining (dining)1FCurtainsscriptedopen switch.sk_dining_curtains_open_led<br>dir input_select.sk_dining_curtains_direction<br>script script.sk_dining_curtains_toggle
Dining (dining)1FSheersscriptedopen switch.sk_dining_sheers_open_led<br>dir input_select.sk_dining_sheers_direction<br>script script.sk_dining_sheers_toggle
Salon (salon)1FCurtainsscriptedopen switch.sk_salon_curtains_open_led<br>dir input_select.sk_salon_curtains_direction<br>script script.sk_salon_curtains_toggle
Salon (salon)1FSheersscriptedopen switch.sk_salon_sheers_open_led<br>dir input_select.sk_salon_sheers_direction<br>script script.sk_salon_sheers_toggle
Living Room (living)1FCurtainsscriptedopen switch.sk_living_curtains_open_led<br>dir input_select.sk_living_curtains_direction<br>script script.sk_living_curtains_toggle
Living Room (living)1FSheersscriptedopen switch.sk_living_sheers_open_led<br>dir input_select.sk_living_sheers_direction<br>script script.sk_living_sheers_toggle
Atrium (atrium)1FCurtainsscriptedopen input_boolean.sk_atrium_curtains_open<br>dir input_select.sk_atrium_curtains_direction<br>script script.sk_atrium_curtains_toggle
Atrium (atrium)1FSheersscriptedopen input_boolean.sk_atrium_sheers_open<br>dir input_select.sk_atrium_sheers_direction<br>script script.sk_atrium_sheers_toggle
Atrium Back (atriumback)1FCurtainsbuttonsbuttons button.sk_atriumback_curtains_open / button.sk_atriumback_curtains_close<br>leds switch.sk_atriumback_curtains_open_led / switch.sk_atriumback_curtains_close_led
Atrium Back (atriumback)1FSheersbuttonsbuttons button.sk_atriumback_sheers_open / button.sk_atriumback_sheers_close<br>leds switch.sk_atriumback_sheers_open_led / switch.sk_atriumback_sheers_close_led
Study (study)1FBlindsscriptedopen switch.sk_study_blinds_open_led<br>dir input_select.sk_study_blinds_direction<br>script script.sk_study_blinds_toggle
Tatami (tatami)1FCurtainsscriptedopen input_boolean.sk_tatami_curtains_open<br>dir input_select.sk_tatami_curtains_direction<br>script script.sk_tatami_curtains_toggle
Tatami (tatami)1FSheersscriptedopen input_boolean.sk_tatami_sheers_open<br>dir input_select.sk_tatami_sheers_direction<br>script script.sk_tatami_sheers_toggle
Family Room (family)2FCurtainsscriptedopen input_boolean.sk_family_curtains_open<br>dir input_select.sk_family_curtains_direction<br>script script.sk_family_curtains_toggle
Family Room (family)2FSheersscriptedopen input_boolean.sk_family_sheers_open<br>dir input_select.sk_family_sheers_direction<br>script script.sk_family_sheers_toggle
Master (Ken) (masterken)2FCurtainsscriptedopen input_boolean.sk_masterken_curtains_open<br>dir input_select.sk_masterken_curtains_direction<br>script script.sk_masterken_curtains_toggle
Master (Ken) (masterken)2FSheersscriptedopen input_boolean.sk_masterken_sheers_open<br>dir input_select.sk_masterken_sheers_direction<br>script script.sk_masterken_sheers_toggle
Master Bath (masterbath)2FBlindsscriptedopen switch.sk_masterbath_blinds_open_led<br>dir input_select.sk_masterbath_blinds_direction<br>script script.sk_masterbath_blinds_toggle
Cat Room (catroom)2FBlindsscriptedopen input_boolean.sk_catroom_blinds_open<br>dir input_select.sk_catroom_blinds_direction<br>script script.sk_catroom_blinds_toggle
Nursery (nursery)2FCurtainsscriptedopen input_boolean.sk_nursery_curtains_open<br>dir input_select.sk_nursery_curtains_direction<br>script script.sk_nursery_curtains_toggle
Nursery (nursery)2FSheersscriptedopen input_boolean.sk_nursery_sheers_open<br>dir input_select.sk_nursery_sheers_direction<br>script script.sk_nursery_sheers_toggle
Guest Room 2 (guest2)2FCurtainsscriptedopen input_boolean.sk_guest2_curtains_open<br>dir input_select.sk_guest2_curtains_direction<br>script script.sk_guest2_curtains_toggle
Guest Room 2 (guest2)2FSheersscriptedopen input_boolean.sk_guest2_sheers_open<br>dir input_select.sk_guest2_sheers_direction<br>script script.sk_guest2_sheers_toggle
Guest Room 4 (guest4)2FCurtainsscriptedopen input_boolean.sk_guest4_curtains_open<br>dir input_select.sk_guest4_curtains_direction<br>script script.sk_guest4_curtains_toggle
Guest Room 4 (guest4)2FSheersscriptedopen input_boolean.sk_guest4_sheers_open<br>dir input_select.sk_guest4_sheers_direction<br>script script.sk_guest4_sheers_toggle
Pink Room (pinkroom)2FCurtainsscriptedopen input_boolean.sk_pinkroom_curtains_open<br>dir input_select.sk_pinkroom_curtains_direction<br>script script.sk_pinkroom_curtains_toggle
Pink Room (pinkroom)2FSheersscriptedopen input_boolean.sk_pinkroom_sheers_open<br>dir input_select.sk_pinkroom_sheers_direction<br>script script.sk_pinkroom_sheers_toggle
Family Room TV (familytv)2FCurtainsscriptedopen input_boolean.sk_familytv_curtains_open<br>dir input_select.sk_familytv_curtains_direction<br>script script.sk_familytv_curtains_toggle
Family Room TV (familytv)2FSheersscriptedopen input_boolean.sk_familytv_sheers_open<br>dir input_select.sk_familytv_sheers_direction<br>script script.sk_familytv_sheers_toggle
Master (Mika) (mastermika)2FCurtainsscriptedopen switch.sk_mastermika_curtains_open_led<br>dir input_select.sk_mastermika_curtains_direction<br>script script.sk_mastermika_curtains_toggle
Master (Mika) (mastermika)2FSheersscriptedopen switch.sk_mastermika_sheers_open_led<br>dir input_select.sk_mastermika_sheers_direction<br>script script.sk_mastermika_sheers_toggle

J. Garden (src/config/garden.ts GARDEN_LIGHTS, FRINGES)

Each light: id (a light or switch; tap = light|switch.turn_on/turn_off, "All on/off" = homeassistant.turn_on/turn_off), pic (which painting), at = position on that painting (fractions), kind = glow shape. FRINGES = the four hanging groups of discs at the top (same ids). Fewer lights → delete rows here and in FRINGES.

NameMade-up idPaintingat
Pavilion Lanternswitch.sk_garden_paviliongarden[0.835, 0.337]
Olive Treeswitch.sk_garden_olivegarden[0.692, 0.625]
Pool Wallswitch.sk_garden_pool_wallgarden[0.395, 0.542]
Pool Stepsswitch.sk_garden_pool_stepsgarden[0.269, 0.725]
Table Lanternswitch.sk_garden_tablegarden[0.338, 0.25]
Patiolight.sk_garden_patiogarden[0.115, 0.567]
Red Mapleswitch.sk_garden_red_maplegarden[0.9, 0.708]
Front Pineswitch.sk_garden_front_pinefront[0.345, 0.716]
Stepping Stonesswitch.sk_garden_stepping_stonesgarden[0.69, 0.8]
Green Mapleswitch.sk_garden_green_maplegarden[0.095, 0.46]
Water Spoutsswitch.sk_garden_spoutsgarden[0.555, 0.39]
House Sconcelight.sk_garden_house_sconcefront[0.537, 0.672]
Fence Washswitch.sk_garden_fencefront[0.215, 0.744]
Veranda Downlightslight.sk_garden_verandafront[0.64, 0.582]
Sunbedsswitch.sk_garden_sunbedsgarden[0.545, 0.59]
Pavilion Stepsswitch.sk_garden_pavilion_stepsgarden[0.8, 0.575]
Side Gateswitch.sk_garden_side_gatefront[0.835, 0.725]
Side Pathswitch.sk_garden_side_pathfront[0.155, 0.81]
Back Pineswitch.sk_garden_back_pinegarden[0.473, 0.236]
Fern Bedswitch.sk_garden_fernsgarden[0.818, 0.86]

K. Music (src/components/music/useMusicData.ts, src/lib/music.ts)

Speakers (SPEAKERS): media_player per room — state + the media attributes in A; calls media_player.play_media / media_play_pause / media_next_track / media_previous_track / volume_set / shuffle_set / join / unjoin. Radio = media_player/browse_media of the main speaker (Home Assistant's built-in Radio Browser source). The library (favourites, recent, search) needs a music-service integration that answers services with a response: the demo names it musicplus with get_track_favorites, get_player_recent_tracks, get_playlist_favorites, search_tracks, search_artists, search_albums, search_playlists (response) and save_track_favorites, remove_track_favorites, and its account player media_player.sk_music_account. Map these names to your integration's services in src/lib/music.ts; without one, the library tabs stay empty and the speakers, radio and media bar still work.

RoomMade-up id
Living Roommedia_player.sk_living_speaker
Family Roommedia_player.sk_family_speaker
Master Bedroommedia_player.sk_master_speaker
Studymedia_player.sk_study_speaker
Pink Roommedia_player.sk_pinkroom_speaker

L. Network (src/lib/networkData.ts)

A static description of the network (devices, names, models, pictures) plus one sensor per live number: per device ent: { cpu, mem, temp, power, clients } (%, %, °C, W, count), WAN latency (ms), recorder/NAS storage, AP state, and camera entities (state only: recording / idle / unavailable). Any monitoring integration that gives these numbers works (router/switch integrations, SNMP, System Monitor, Glances …); missing keys show the stored value. No interest → remove network from src/app/views.tsx.

Made-up ids (63)
camera.sk_cat_room, camera.sk_doorbell, camera.sk_driveway, camera.sk_front_door, camera.sk_garage, camera.sk_garden
camera.sk_nursery, camera.sk_pool, sensor.sk_net_ap1_clients, sensor.sk_net_ap1_cpu, sensor.sk_net_ap1_memory, sensor.sk_net_ap1_state
sensor.sk_net_ap2_clients, sensor.sk_net_ap2_cpu, sensor.sk_net_ap2_memory, sensor.sk_net_ap2_state, sensor.sk_net_ap3_clients, sensor.sk_net_ap3_cpu
sensor.sk_net_ap3_memory, sensor.sk_net_ap3_state, sensor.sk_net_ap4_clients, sensor.sk_net_ap4_cpu, sensor.sk_net_ap4_memory, sensor.sk_net_ap4_state
sensor.sk_net_core24_clients, sensor.sk_net_core24_cpu, sensor.sk_net_core24_memory, sensor.sk_net_core24_temperature, sensor.sk_net_gate16_clients, sensor.sk_net_gate16_cpu
sensor.sk_net_gate16_memory, sensor.sk_net_gate16_temperature, sensor.sk_net_gateway_cpu, sensor.sk_net_gateway_memory, sensor.sk_net_gateway_temperature, sensor.sk_net_gateway_uptime
sensor.sk_net_ha_disk, sensor.sk_net_hall16_cpu, sensor.sk_net_hall16_memory, sensor.sk_net_hall16_temperature, sensor.sk_net_lan_clients, sensor.sk_net_minipc_cpu
sensor.sk_net_minipc_disk, sensor.sk_net_minipc_memory, sensor.sk_net_nas_cpu, sensor.sk_net_nas_disk_temperature, sensor.sk_net_nas_memory, sensor.sk_net_nas_volume_used
sensor.sk_net_nvr_recording, sensor.sk_net_nvr_storage, sensor.sk_net_out_core24_power, sensor.sk_net_out_gateway_power, sensor.sk_net_out_minipc_power, sensor.sk_net_out_modem1_power
sensor.sk_net_out_nas_power, sensor.sk_net_out_nvr_power, sensor.sk_net_pdu_budget, sensor.sk_net_pdu_cpu, sensor.sk_net_pdu_memory, sensor.sk_net_pdu_power
sensor.sk_net_wan1_latency, sensor.sk_utility_temperature, sensor.sk_utility_temperature_2

M. Lights On (top strip) and the dynamic ids

  • Lights On counts every light.* that is on from GET /api/states (refreshed every 30 s), skipping light groups (they have an entity_id list), keypads, LEDs and night lights, plus switch.* entities of the integration named in DIMMERS_INTEGRATION (src/lib/useRealLights.ts; found with the template integration_entities('dimmers') through /api/template). Use that for switch-type dimmers or lamps on smart plugs: put your integration's name there, or leave it (switches are then ignored). The popup turns a light off with light|switch.turn_off.
  • Pets: sensor.<pet id>_room and sensor.<pet id>_room_ratio_<7d|today>_<room key> are built from the pet ids in CATS (see G).

Helpers they may need (create them in the UI first)

Prefer Home Assistant's built-in helpers (Settings → Devices & Services → Helpers → Create helper): they are validated, editable and need no restart. Use entity ids, never device ids. The YAML below is for reference or for pasting into the script/automation editor's "Edit in YAML" — adjust the ids. It uses the current syntax (triggers: / trigger: state / actions:, Home Assistant 2024.10+); older versions write trigger: / platform: state / action:.

  1. Energy totals (gridKwh / solarKwh / battKwh daily, weekly, monthly). If they have kWh counters (state_class: total_increasing), add a Utility Meter helper per counter and cycle (daily, weekly, monthly). If they only have power sensors, first add an Integral (Riemann sum) helper per power sensor (method Left, metric prefix k, time unit hours), then the Utility Meters on top.
  2. House power now (loadPower, TOPSTRIP.power): if there is no whole-house power sensor, add a Combine the state of several sensors (min/max) helper of type sum over grid + solar + battery power (same unit; grid positive = importing, battery positive = discharging).
  3. Savings (savings.*): a Template sensor helper per period, for example (today): `{{ ((states('sensor.solar_energy_today')|float(0) - states('sensor.grid_export_today')|float(0)) * 31
    • states('sensor.grid_export_today')|float(0) * 16) | round(0) }}— 31 = their price per kWh, 16 = their feed-in rate; or delete the savings block insrc/views/EnergyView.tsx`.
  4. Pet room shares (sensor.<pet>_room_ratio_today_<room> / _7d_): History stats helpers on sensor.<pet>_room, type ratio, state = the room key; today = start {{ today_at() }}, end {{ now() }}; 7 days = start {{ now() - timedelta(days=7) }}, end {{ now() }}. That is two per pet and room, so trim ROOM_RATIO to the rooms their pets really use. For many at once, a YAML package is quicker:
    yaml
    sensor:
      - platform: history_stats
        name: mochi_room_ratio_today_kitchen_1
        entity_id: sensor.mochi_room
        state: "kitchen_1"
        type: ratio
        start: "{{ today_at() }}"
        end: "{{ now() }}"
    The room sensor itself must report the dashboard's room keys; if their tracker reports area names, map them with a Template sensor helper ({{ {'Kitchen': 'kitchen_1', 'Study': 'office_1'}.get(states('sensor.mochi_area'), 'away') }}) or rename the keys in ROOM_RATIO and CAT_PERCH to theirs.
  5. A/C chip on room cards (ROOM_AC): a Number helper for the setpoint (min 16, max 30, step 0.5), a script that toggles the unit (Settings → Automations & scenes → Scripts → + → ⋮ → Edit in YAML):
    yaml
    alias: Tatami A/C toggle
    mode: single
    sequence:
      - action: climate.toggle
        target: { entity_id: climate.tatami_ac }
    and an automation that applies the setpoint to the unit:
    yaml
    alias: Tatami A/C setpoint
    mode: restart
    triggers:
      - trigger: state
        entity_id: input_number.tatami_ac_target
    actions:
      - action: climate.set_temperature
        target: { entity_id: climate.tatami_ac }
        data: { temperature: "{{ trigger.to_state.state | float }}" }
    No such helpers wanted → set the room's ROOM_AC entry to []; the A/C list on Home still controls every unit.
  6. Curtains: with ordinary cover.* entities, use the third cover kind this package supports — { kind: "cover", label: "Curtains", cover: "cover.living_curtains" } in ROOMS3D (tap = cover.toggle, the state shows opening/closing/open/closed) — and put the same cover.* id in the room card's sheer / blackout field (open counts as open). The scripted / buttons kinds are for curtain motors driven by scripts or keypad buttons.
  7. Cat spottings (SPOTTINGS): an automation per slot (one camera per slot is simplest) that saves a snapshot when the camera sees a pet, and stores where and when. The code asks for /local/snapshots/animals/spotting-1.jpg; a mode-A build (BASE=/local/dashboard/, Step 3) serves that from <config>/www/dashboard/local/snapshots/animals/, so the snapshot goes there. Home Assistant only writes files into allowed folders: add /config/www to allowlist_external_dirs in the homeassistant: block of configuration.yaml (there is no UI field for it). The two helpers are a Text helper and a Date and/or time helper (date and time).
    yaml
    alias: Pet spotting 1 (garden camera)
    mode: single
    max_exceeded: silent
    triggers:
      - trigger: state
        entity_id: binary_sensor.garden_camera_animal   # their camera's animal / pet detection
        to: "on"
    actions:
      - action: camera.snapshot
        target: { entity_id: camera.garden }
        data: { filename: /config/www/dashboard/local/snapshots/animals/spotting-1.jpg }
      - action: input_text.set_value
        target: { entity_id: input_text.spotting_1_camera }
        data: { value: Garden }
      - action: input_datetime.set_datetime
        target: { entity_id: input_datetime.spotting_1_time }
        data: { datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}" }
      - delay: "00:05:00"
  8. Litter boxes (LITTER_BOXES): any sensor whose state mentions clean / run / work / active shows "cleaning"; occupied is on when on / true. Most smart litter-box integrations have both.

Step 3 — Go live

The go-live kit is already wired into this package (its own guide: live/README.md). Pick a mode with the person:

ModeWhatHow
A — inside Home Assistant (recommended)static files in Home Assistant's www/ folder, Home Assistant's own logincp .env.example .env.live (keep VITE_HA_MODE=live, BASE=/local/dashboard/) → npm run build:live → copy everything in dist/ to <config>/www/dashboard/ (Samba share, File editor or Studio Code Server app, or scp) → open http(s)://<their-home-assistant>:8123/local/dashboard/index.html on the tablet and log in once
B — sidebar panelthe same build as a page in Home Assistant's sidebarmode A, plus copy panel-loader.js into <config>/www/dashboard/ and add the panel_custom: block from the top of panel-loader.js to configuration.yaml, restart
C — developmentlive data while you change the codein .env.live set VITE_HA_TARGET (their address) and VITE_HA_TOKEN (a long-lived token) → npm run dev:live; keep it on their own network
  • Files in www/ are served without a login — never put a token or anything private in a build (the config refuses to bake VITE_HA_TOKEN into one). The app's own pictures in www/dashboard/ are public to anyone who can reach Home Assistant; that is normal for www/.
  • /local/… paths in the code are rewritten to the build's BASE (vite.config.ts → publicPaths): with BASE=/local/dashboard/, the app's /local/pets/… becomes /local/dashboard/local/pets/…, i.e. <config>/www/dashboard/local/pets/… — that is why the spotting snapshots go there.
  • Once live, delete the demo: src/demo/ (sandbox, snapshot, synthetic day, calendar, music, the time/weather bridge), src/ha/mockBackend.ts, src/lib/mockApi.ts, src/main.demo.tsx, static/demo/, and the else { import("./main.demo") } branch in src/main.tsx. Keep src/lib/clockEpoch.ts (the app uses it). A live build already leaves all of it out, so this is tidying, not a requirement.

Step 4 — Pictures

Every picture the app uses. keep = generic (UI, icons, textures, gear, fonts): fine in any home. replace = it shows the made-up home, its rooms, garden or pets: keep them while you test, then redraw them for the person's home. Sizes are what the app displays; python3 tools/fit.py --slots prints them.

PathSizeRole
src/assets/house/house-day.webp, house-night.webp, house-lit.webp1190×482, transparent, in registerthe house in the hoop: day, night (fades in at dusk), lit windows (shown only inside WINDOWS boxes of lit rooms)replace
src/assets/pages/garden.webp, garden-night.webp1300×552, in registerGarden view painting, day / night (lamps dark — the app paints glows)replace
src/assets/pages/front-day.webp, front-night.webp721×348Garden view: the small "house from the street" painting with front lightsreplace
src/assets/rooms/<key>.webp (18)480×720ink sketch on each room card (SKETCH in src/components/rooms/sketches.ts)replace
static/local/rooms/<key>.webp (18)480×720not shown by this design (the photo field in ROOMS); skip—
src/assets/cats/<id>.webp (6)192×192pet medallion that drops into the Home stagereplace
static/local/pets/<id>.webp (6)512×512pet portraits in Animals + the pet popup (CATS[].photo)replace
src/assets/curtains/<key>-day.webp, -night.webp (17 + 17)1200×896 · 896×1200 · 1264×848the window view behind each 3-D curtain (src/config/curtainPlates.ts)replace
src/assets/curtains/t-<key>.webp (17)188×128room tags in Curtains & Sheersreplace
static/local/snapshots/animals/spotting-1.jpg, -2.jpg1273×720latest pet spottings (live: written by the Step-2 automation)replace (automatic)
static/demo/camera/*.jpg, static/demo/music/cover-*.jpg1273×720 · 320×320demo-only stand-ins for camera images and album coversdelete when live
static/local/network/assets/gear/*.png (11)≤332 px wide, transparentNetwork view: gateway, switches, recorder, PDU, APs, mini PC, NASkeep (or draw their own gear)
static/local/diningon.svg, diningoff.svg, lights-on2.svg, lights-off2.svgvectoron/off drawings of the "feature light" on room cardskeep
static/local/litter-*.svg, static/local/icon.pngvector · 512×512litter-box states, app iconkeep
src/assets/fonts/jost-latin.woff2, static/fonts/manrope.woff2—Jost (all text), Manrope (numbers in some cards) — OFL, licences next to themkeep
the robot, the sky, sun, moon, stars, clouds, rain, snow, fog, curtains, plancodedrawn in SVG / canvas / three.jskeep

How the pictures are made. All "replace" pictures are one hand: black ink outlines + transparent watercolour washes on white paper (tools/prompts.md has the exact style line and a prompt per group). Rules that keep it working:

  • House — straight-on from the street, a little above, the whole house with white paper around it, no sky painted. Make the day picture, then the night as an edit of the day, then lit as an edit of the night, so all three line up; cut them out together with tools/cutout.py (it applies the day mask to all three), size 1190×482 or any size you then set in src/config/house.ts (PAINT_W/PAINT_H = the plate, CROP = { x: 0, y: 0, w: PAINT_W, h: PAINT_H }). Re-measure with tools/measure.html (box mode) every lit window → WINDOWS[].boxes, (point mode) the solar panels, battery and front door → ENERGY_AT, and a perch per pet room → CAT_PERCH.
  • Garden — day + night as a registered pair; measure every light (point mode) into GARDEN_LIGHTS[].at. For the small front painting, crop the house pictures with tools/fit.py and set FRONT_CROP to [0, 0, 1, 1].
  • Window views — photograph the view from each curtained window, repaint it (edit/img2img: "keep exactly the same view, window frames and mullions"), then the night as an edit of the painting; fit to the room's slot.
  • Rooms — one finished room as the style reference for the other 17; vertical page, white paper ground.
  • Pets — their own pet photos as the reference ("repaint this photo as a watercolour portrait"): it's their home.
  • No AI at all? tools/nightify.py makes a usable night and lit-window version from any day picture; the day pictures can be their own photos through a watercolour filter.

Generators (free first). This look comes out best from image editing of their own photos with a style reference, rather than from text alone.

RouteGood forNotes
Google Gemini (free tier, image editing)everything; best at "repaint this photo, keep the composition" and at night-as-editupload the photo + the style line; then "the very same picture at night" on the result
ChatGPT free image tierhouse, rooms, petsstrong at following the "ink + watercolour on white paper" style; ask for one picture at a time
Local: FLUX.1 [schnell] or SDXL in ComfyUI / Forge (free, needs a GPU)big batches, exact repeatabilityimg2img from their photos at denoise ≈ 0.55; a watercolour/ink LoRA; fixed seed + the same style reference keep a series consistent
Draw Things (Mac / iPad, free, local)the same as above without a PC GPUimg2img with SDXL or FLUX models
Adobe Firefly (free credits), Microsoft Designer / Bing Image Creator, Ideogram, Leonardo, Krearooms and garden from textFirefly and Krea accept a style reference picture; text-only generators drift — pick one and stay with it
No AIphotos + a watercolour/ink filter (GIMP + G'MIC, Photoshop, Procreate), a Sweet Home 3D or Blender model of the house rendered straight-on, or an illustratorthen tools/nightify.py for night versions

Keep a series consistent: same generator and model for the whole set, the same style line, one finished picture as the style reference for the rest, fixed seeds where available, and always make night versions as edits of the day. Process every result with tools/fit.py (it also strips camera metadata such as GPS — do that before anything goes into a build).

Step 5 — Build & run

  • npm run build:live → dist/ (live) · npm run build → dist/ (demo). Check npm run typecheck (0 errors).
  • Host: mode A or B (Step 3). Any static web server works for the demo build.
  • Screen: designed at 1376 × 1032 CSS px (11-inch tablet, landscape). Smaller screens get the whole layout scaled down to fit (src/components/shell/AppShell.tsx, DESIGN_W/DESIGN_H); it never scales up. A 10.9–11-inch iPad or Android tablet in landscape shows it 1:1; on bigger screens the grid stretches to fill them (the stage and cards keep their sizes) — check the layout there before mounting the tablet.
  • Wall tablet: Android — Fully Kiosk Browser (start URL = the dashboard, keep screen on, motion/schedule dimming). iPad — open the dashboard in Safari, Share → Add to Home Screen (runs full-screen), then Settings → Accessibility → Guided Access to lock it to the app; Display → Auto-Lock "Never" while on the charger.
  • Keep the tablet's time zone = the home's time zone (the sky uses the tablet's clock; see Gotchas).

Step 6 — Verify

  • npm run typecheck → 0 errors; npm run build:live → no warnings about missing files.
  • Every view opens (Home, Rooms, Curtains & Sheers, Animals, Garden, Energy, Network, Vacuum) without blank cards or "—" where the person expects a value.
  • Browser console: no errors, no failed requests (except RainViewer hiccups); no request to anything but their Home Assistant, Google Fonts (Japanese only) and api.rainviewer.com / tilecache.rainviewer.com.
  • Tap tests: a room light toggles and dims; an A/C turns on/off and its setpoint moves; a curtain opens and the card shows opening → open; a garden light lights its pin; media bar play/pause/volume; the lock (ask first!); vacuum start → dock (ask first).
  • Home: clock = the tablet's time; sunrise/sunset times are right for their place; at night the lit windows match the rooms whose lights are on; the weather glyph and temperature match their weather entity.
  • Energy: the "Power today" chart has today's curve; kWh totals and savings look sane.
  • Animals: each pet's room and today's timeline; litter boxes; spotting pictures appear after a detection.
  • 日本語 toggle works (or their language, if you added one).
  • On the real tablet: smooth swaying (≈60 fps), the Curtains view loads within a few seconds, memory stays flat after an hour (Safari: Develop → the tablet → Timelines).

Make it theirs

  • Names & rooms: name fields in src/config/*.ts; room cards are ROOMS (floor: "1st" | "2nd"); the house's lit windows WINDOWS; curtain rooms ROOMS3D (+ curtainPlates.ts); the vacuum plan src/config/plan.ts.
  • Their own floor plan (Vacuum view): GROUND is a list of rooms as polygons in metres — x to the right, y up, with y = 0 at the back of the house and negative values toward the street (so the street is at the bottom), label = where the name goes, vac: false for areas the robot can't reach (shown hatched), and BOUNDS = the drawing's frame. Draw it from a floor-plan image: open it in tools/measure.html, type the real width in metres, click each room's corners in polygon mode and paste the "metres" line; or measure the rooms with a tape and write the rectangles by hand. area must match the names their vacuum integration uses for its rooms if they want them to agree.
  • Location & time zone: LAT, LON and TYO (the home's UTC offset in ms) in src/kinetic/sky.ts; the same offset in src/kinetic/forecast.ts (wallOf) and src/components/home/HouseStage.tsx (9 * 3600000); LAT, LON, AIRPORT, CITY in src/components/home/RadarMap.tsx; the radar's rivers and coast in src/lib/tokyoGeo.ts (set SEA, COAST, RIVERS to "" to show rain over plain paper, or redraw them for their city).
  • Colours: tokens in src/styles/theme.css (day + [data-phase="night"]), the mobile's primaries PAINT in src/kinetic/shapes.tsx, the wall colour through the day in src/kinetic/sun.ts (wallColors).
  • Fonts: src/styles/fonts.css (+ --font / --num in theme.css); ship the font's licence with it.
  • Language: English ⇄ Japanese. To add one, copy src/i18n/ja.ts to e.g. fr.ts ({ "English": "Français" }), extend Lang and t() in src/i18n/i18n.tsx, and add it to src/components/shell/LangToggle.tsx. Every visible string goes through t("…"); untranslated strings fall back to English.
  • Views: remove a view from src/app/views.tsx; reorder there too.
  • Demo data for their own demo (optional): src/demo/snapshot.ts (states), src/demo/synth.ts (the synthetic day).

Gotchas

  • The clock is the home's clock. Sun, moon and sky read the tablet's wall-clock time as the home's local time and convert it with a fixed UTC offset (TYO, +9 h in the made-up home). Use the home's offset; homes with daylight saving time need the offset to follow it (replace the constant with one computed by Intl.DateTimeFormat for their time zone) or the sky is an hour off for half the year.
  • iPad / WebKit: CSS masks need the -webkit- prefix (already there); keep prefers-reduced-motion support; three.js in the Curtains view uses WebGL — older iPads may reload the page if many large textures pile up, so keep window paintings at the slot sizes (≤ 1264 px) and let the engine dispose them (it does when you leave the view).
  • Big Home Assistant installs: views subscribe only to the entities they show — keep it that way when you add cards (useEntities([...ids]), never "all states"). The one exception is Lights On, which reads /api/states every 30 s: raise pollMs in useRealLights if their instance has thousands of entities.
  • Units: power sensors in kW (convert W with a helper or divide in EnergyView.tsx), temperatures in °C as shown (Fahrenheit works but the colour bands in src/lib/bands.ts assume °C).
  • Missing entities don't crash anything: values show "—" and cards hide; that also means a typo in an id fails silently — check every view against Step 6.
  • History: charts and the pet timeline need the recorder (on by default) to keep at least 8 days (purge_keep_days), and History stats helpers count only what the recorder still has.
  • Radar: rain tiles come from RainViewer's free public API (no key); it is the only third-party request besides Japanese fonts.
  • www/ is public inside their network (and outside, if Home Assistant is exposed): no tokens or private documents in www/dashboard/.
  • Demo vs live: never ship a live build with src/demo/sandbox.ts imported — it disables WebSockets. main.tsx already keeps them apart; don't import anything from src/demo/ in app code.

Licences

  • Code: MIT — LICENSE ("Copyright (c) 2026 dashboards.casa").
  • Fonts: SIL Open Font License 1.1 — Jost (src/assets/fonts/OFL.txt) and Manrope (static/fonts/OFL.txt). Japanese fonts are loaded from Google Fonts at runtime (OFL), not shipped.
  • The made-up home's pictures and data (Sakura Home, its rooms, garden, pets and people): made for this demo, free to use as examples and placeholders.
  • npm packages keep their own licences (React, three.js — MIT; @mdi/js — Apache 2.0; home-assistant-js-websocket — Apache 2.0; Vite, TypeScript — MIT / Apache 2.0). RainViewer's API has its own terms (free, attribution appreciated).