My house as a live 3D dashboard – Blender model + three.js, driven by Home Assistant

Hi everyone :waving_hand:

For the last two weeks I’ve been building something I always wanted: my actual house as a 3D model in the browser, where everything you see is live Home Assistant state – lights, blinds, windows, doors, bins, the wallbox, the weather, even the wall clock in the dining room. You can fly into every room, switch things back through HA, and at night the house lights up exactly the way it does in reality.

Update, 30 Sep 2026: the network is in the model now (UniFi access points and switches with live stats and a topology view), offline devices and low batteries show up right where they are in the house, the energy view got live cards on the house plus a light streak across the PV modules, and the room picker shows all floors side by side. Marked (new, 30 Sep) below.

Update, 29 Sep 2026: new video and a big round of UI work. Every popup now shows a picture of the device instead of a list of text, the house can be controlled without the HA UI (heat pump, climate, house modes, garage door), and the two robot vacuums got their own cards – one of them with the live map from HA. The new parts are marked (new) below.

It’s not a Lovelace card – it’s a standalone web page (three.js) served on the local network, talking to HA over the WebSocket API. Below is how I built it, what it can do, what it runs on, and some nerd statistics at the end.


How I built it

The short version: an AI coding agent (Claude Code) wrote and maintained one reproducible Blender Python script that builds the whole house from my real building documents, and a three.js front-end that binds the model to HA. I did the directing, measuring and correcting – a lot of correcting.

  1. Building plans as the ground truth. My architect’s PDFs (floor plans, sections, elevations, the electrical plan) were rendered and calibrated in pixels per metre. Every wall, window, door and ceiling height comes from there. The electrical plan gave me the positions of lights, sockets, switches and outdoor wall lamps.
  2. Terrain from real data. The site plan with its height points (the surveyor’s Koten) plus the public 1 m digital terrain model (DGM1) from the state’s open-data geoportal, and the parcel boundary from the online cadastral map. That’s how the sloped front garden, the street, the driveway and the garden terraces got their real heights – including the slope along the street.
  3. Photos as reference. Where plans stop, photos take over: a drone top view to place all 24 PV modules, and photos for things like the front door lamp, the wall clock, the wallbox and its mounting, the bins, furniture. The model is deliberately low-poly, but proportions matter.
  4. Tape measure. For anything not in the plans (mounting heights, furniture, the clock on the pillar between the terrace doors) I measured on site, or used a known width in a photo as the scale.
  5. One build script. Nothing is modelled by hand in the Blender UI. build_haus.py deletes and rebuilds the entire collection on every run – so a change like “move that wall 10 cm” is one line, and the result is always reproducible. Blender runs headless, exports a Draco-compressed glTF, done.
  6. Every object carries its meaning. Custom properties on the Blender objects (kind, room, ha_entity, hinge axis, opening angle, two positions for each bin…) travel into the glTF as extras. The browser reads them and knows what’s a light, a blind, a window, which HA entity belongs to it and how it moves.
  7. Design helpers. For one new garden area (behind the garage) I let an image model draft a concept first, picked a variant, and then had it built into the model.

Features

Live state, both directions

  • The page subscribes to HA via the WebSocket API (subscribe_events, plus weather/subscribe_forecast for the forecast). Rooms are mapped to HA areas, so most lights and sensors are found automatically.
  • Tap a lamp in the 3D model → it toggles in HA. Long-press it → a card with a dimmer and colours, and the lamp in the card glows as bright as you set it. Tap a blind → it moves; one more button sets whole blind scenes (open, sun protection per side, closed).
  • Room cards show temperature, humidity, VOC and illuminance with 24 h sparklines from the HA history, plus a Devices tab with the power of every measured device in that room (new: tiles with readable names instead of raw entity names).
  • Everything that switches goes through HA services, so automations and the real devices stay the single source of truth.

Day, night and the real sun

  • Sun position is calculated from my real location and the house’s real orientation, so shadows fall where they fall in the garden.
  • A time slider previews any time of day – including the wall clock in the model, which follows it.
  • At night, windows glow where HA says the light is on, LED strips glow along their actual path, and 19 outdoor lights (wall lamps, bollards, ground spots, tree spots) switch on at dusk and off when the house goes into sleep mode.

Floors, rooms and the view

  • Show everything, only the ground floor, only the upper floor, or the attic – the roof and upper floors fade away.
  • Pick a room and the camera flies in; the side card shows everything HA knows about that room.
  • (new, 30 Sep) The room picker shows the floors side by side – ground floor, upper floor and “more” (terrace, garage, attic) – and opens right above its button. Room focus only works for rooms you can actually see: with the roof on, none; with the upper floor showing, nothing downstairs.

Blinds, windows and doors

  • 22 blinds move to their real position. If the sun-protection mode moved a blind, a small badge on it says so.
  • Contact sensors open windows (tilted vs. fully open) and doors in the model.

Bins – only when it matters

  • Each bin has two positions stored in the model: its parking spot and “at the street”.
  • An input_boolean per bin says where it is; when it changes, the bin rolls along a path stored in the model: out of the side passage, through the garden gate (which swings fully open for it), down the driveway and onto the pavement – and back.
  • Pickup today → the bin blinks (same 400 ms rhythm as my WLED “trash reminder” in the hallway). Pickup tomorrow → it glows steadily in its colour. Otherwise it’s just a black bin.
  • A due bin stays visible through walls and the garage as an x-ray silhouette. (Trick: the same mesh again, drawn only where something is in front of it – depthFunc: GreaterDepth.)
  • The status bar shows one chip per pickup with coloured bin icons, and says honestly which bin is out and which one is still missing.

Weather and a clock that matches the one on my wall

  • A tiny clock sits in the top bar. Tap it: a card opens with the same Roman-numeral face as the real clock in my dining room, today’s weather and a 5-day forecast.
  • The actual wall clock in the model has moving hands, driven by the current time.

Wallbox (E3/DC multi connect II)

  • Modelled from photos, including the blue status LED (lit while the box is registered at my E3/DC storage system).
  • Tap it for charging power and PV share (read from the E3/DC via Modbus), solar mode, phases, charging mode and PV surplus.
  • (new) The card draws the wallbox itself: blue LED, the cable when a car is plugged in, and current flowing along it while charging. The car’s state of charge is a ring with a mark at the target.
  • The charging cable and the open socket flap in the 3D model only appear when a car is plugged in.

Energy view

  • A separate mode for PV, battery, grid and house consumption, anchored on the roof and the home battery.

  • (new) The energy flow is a small diagram: four nodes around a hub, and the active paths flow in the direction the power actually goes. The battery node carries its state of charge as a ring. Only measured values, nothing calculated as “the rest”.

  • (new, 30 Sep) Energy mode now puts live cards right on the house: PV on the roof, the home battery at the utility room, the grid at the pavement where the cable comes in, house consumption above the roof and the wallbox at the box – each with its 24 h curve. The arrows move in the direction the energy actually flows.

  • (new, 30 Sep) A light streak runs diagonally across the PV modules towards the battery: the faster and more often it runs, the more the array produces (measured against its 10.25 kWp). No sun, no streak. A first version just tinted the modules by output – nobody could tell what the colour meant.

Control the house without the HA UI (new)

Day-to-day changes shouldn’t need the HA UI, so they now live in one “Control the house” card with four tabs:

  • Heating: the heat pump with two round gauges (hot water and indoor, with the setpoint as a mark), the operating mode as five buttons with a sentence explaining the selected one, and for rooms and hot water a scale with sun = day setpoint, moon = night setpoint and a dot for the current value. − / + right below.
  • Climate: one tile per unit with the room temperature, a switch and the setpoint when it’s on.
  • Modes: holiday mode, guests, guest workplace and a “fireplace” blind scene as big switch tiles.
  • Doors: the garage door as a drawing whose sections follow the real position, and the laundry chute flap.

Anything that moves something physical or changes a lot (garage door, flap, holiday mode, heating off) asks once – the button itself turns into the question, no modal dialog. Heating curve and other plant parameters stay in HA on purpose: a mis-tap on the couch iPad shouldn’t be able to change them.

Two robot vacuums, two cards (new)

  • Roborock S6 MaxV (upstairs): the card shows the live map from HA’s image entity including the robot’s position. HA sends no CORS header and the map is ~70 % transparent border, so a tiny same-origin PHP proxy delivers it and the browser crops it to the content. Below: clean a single room (room IDs from roborock.get_maps), fan speed and mop intensity as stepped buttons, consumables as rings with a “replaced” button (asks first), statistics and do-not-disturb.
  • eufy X10 Pro Omni (downstairs, via Matter): Matter gives no map, so the card draws the robot and its station from above – docked, charging, or out cleaning with the side brush spinning. The eight English cleaning modes became two simple questions: vacuum only or vacuum + mop, and auto / deep / quick / quiet.
  • Both have a 14-day cleaning calendar from the HA history.

Appliances and lights (new)

  • Washing machine and dishwasher are drawn in the card: drum or basket, a progress ring around the door, the drum turns while it runs, time left next to it. Leak sensors are small pills. Turning the smart plug off while a programme runs asks first.
  • The light card shows the lamp glowing as bright and in the colour it currently is, with a big dimmer.

Network (new, 30 Sep)

  • The UniFi gear sits in the model where it really hangs: three access points on the ceilings, the rack with the Cloud Gateway Ultra and a 16-port PoE switch above the utility room door, and three Flex Mini switches behind the TV and hi-fi boards and in the garage.
  • Tap one: CPU and memory as rings, clients per band, what it’s connected to, uptime, firmware, the status LED as a switch and a restart button (asks first). Everything comes from the HA UniFi integration.
  • The new “···” menu in the top bar has a network overview: the topology is built from each device’s uplink MAC, plus the current internet traffic at the gateway (UPnP – so not a speedtest, and shown in kbit/Mbit/Gbit as it fits) and clients per WLAN.
  • The same menu holds the HA connection status and the room colouring (temperature, humidity, air quality, ventilation) that used to take up space in the bottom bar.

Offline devices and low batteries – shown where they are (new, 30 Sep)

  • A yellow chip in the top bar counts devices that HA reports as unavailable. Tap it: a list with “offline since”, and a marker appears at the device in the model. A TV behind a socket that’s switched off on purpose doesn’t count.
  • Door and window sensors with a low battery get a small tag right at the sensor: room, what it is (window, top window, door) and the level. Only from 10 %, and only on the floor you’re looking at.

Small things that make it feel right

  • Warnings that must never be missed (water leak, alarm, ventilation emergency) always show as text; everything else is a compact icon with a counter.
  • Battery warnings only from 10 % (orange) and 5 % (red) – at 20 % nobody changes a battery.
  • Micro animations: popups grow out of the button that opened them, lists stagger in, badges bounce only when the number actually changes. All of it is off with “Reduce Motion”.
  • On the phone the dashboard folds away: only the people at home, the bins, the clock and real warnings stay visible. Navigation opens on tap, so about 80 % of the screen is house.
  • On page load it always starts with the same overview – no “where was I last time”.

Hardware and what I had to do for the old iPad

  • Wall/couch device: an iPad Pro 12.9" (4th gen, 2020, A12Z) in Safari – about six years old now, and the real benchmark.
  • Desktop: MacBook in Safari/Chrome; phones work too.
  • Hosting: a small Apache VM on the home network serves static files (HTML, JS, one glTF). No build step, no framework, no cloud.
  • Home Assistant: HAOS on a VM; the E3/DC storage system is read via Modbus TCP.

What it took to keep the iPad happy:

  • Textures capped at 512 px on touch devices, at load time. That took texture memory from ~89 MB to ~49 MB.
  • A fixed pool of point lights (14 on desktop, 6 on touch) that is reassigned to the lamps closest to the camera. The shader never recompiles when you switch a lamp.
  • A pixel budget instead of full Retina (~1.5 megapixels on touch devices: the iPad renders at ~1×, phones still get ~2.4×), no MSAA, render on demand. On iOS a canvas that stops redrawing loses its buffer, so touch devices use preserveDrawingBuffer plus a minimum redraw.
  • The nastiest bug: the iPad’s GPU crashed (“BIF0 page fault”) 5 s after loading – but only once the interiors were in. It turned out to be a shader reading array[-1] in a branch that “never runs”. Metal evaluates both branches. Mac and simulator didn’t care, the real device did. Lesson: always clamp dynamic array indices in shaders. I found it by pulling the GPU crash log off the iPad over USB (pymobiledevice3).
  • pagehide releases the WebGL context explicitly, because Safari otherwise keeps the memory of the dead context.


Nerd statistics

Objects in the scene 623 (621 meshes)
Triangles in Blender ~499,000 (~564,000 rendered with all floors visible)
Vertices ~272,000
Materials / textures 326 / 84
glTF size (Draco) 7.8 MB
Draw calls (everything visible) ~1,030
Geometries / textures on the GPU 983 / 108, 35 shader programs
Load time on the LAN ~2.5 s
Lights in the model 59 (39 inside, 20 outside)
Blinds / windows / doors 22 / 19 / 15
Moving parts 61
Sockets and switches from the electrical plan 47 / 15
Plants, trees, bushes yes. Too many to count sensibly.
HA entities bound directly in the model 91
HA entity IDs referenced in the front-end 202, plus everything found through areas
Entities in my HA instance ~6,000
Build script (Blender Python) ~5,000 lines
Front-end (JS + HTML/CSS) ~7,000 lines
Time ~13 days, evenings and a few very long days

Tools

Claude Code (Anthropic) AI coding agent: wrote and maintained the Blender build script, the three.js front-end and all the browser tests
Blender 5.2 LTS Runs headless via Python (bpy); builds the whole scene from build_haus.py and exports Draco-compressed glTF
three.js 0.170 Rendering in the browser, loaded from a CDN via import map – no build step, no framework
Home Assistant WebSocket API for states, services, history and the weather forecast; Modbus TCP for the E3/DC storage system and wallbox
Python + PyMuPDF / Pillow Rendering and calibrating the plan PDFs; generating textures like the clock face and the wallbox front panel
DGM1 (state open data) 1 m terrain model for the real slope of garden, front yard, driveway and street
Google Pixel 9 Pro Reference photos (clock, wallbox, lamps, furniture)
DJI Mavic 2 Pro Drone top view of the roof to place the PV modules
Tape measure Everything the plans don’t say
Image model (ChatGPT) Concept drafts for a new garden area before building it
Playwright (WebKit/Chromium) Automated checks on desktop, iPad and iPhone viewports – and all screenshots and the video in this post
ffmpeg Encoding the video (rendered frame by frame at 30 fps with a virtual clock, so nothing stutters)
pymobiledevice3 Pulling the GPU crash log off the iPad over USB
Apache + PHP on a small VM Serves the static files; a tiny PHP script adds proper cache headers for the versioned model and textures

Tip if you want to build something similar

Start with the plans and a script, not with Blender by hand. Put every “meaning” (entity, room, hinge, angle) on the object as a custom property, let the browser read it from the glTF – and test on the oldest device you own, early.

Effort – straight from the session logs

I built all of this with Claude Code on the Max subscription. Model-wise it was Opus 5 for most of the work and Opus 5.5 for the later part (plus a few responses from Fable 5.1). The numbers below come from Claude Code’s local session logs (usage reported per API response, as of 30 Sep 2026, midday):

Period 18–30 Sep 2026 (about 11.6 days)
Active time (gaps over 10 min removed) ~43.5 hours
My messages 367
Model responses 5,978
Tool calls (file edits, Blender runs, browser tests, …) 6,328
Output tokens 5.81 million
Input tokens (uncached) 12,008
Cache writes 37.0 million
Cache reads 3.18 billion
What it would have cost at API list prices $1,830.73

The cache reads look absurd, but that’s how prompt caching works: every single response reads the whole conversation so far back from the cache, and after a few hundred messages that adds up to billions – even though a single request never contains more than a few hundred thousand tokens.

The cost line uses Anthropic’s published API prices (Sep 2026, standard speed, 1-hour cache writes – which is what Claude Code used): Opus 5 $1,524.52, Opus 5.5 $260.88, Fable 5.1 $45.33. Almost three quarters of it ($1,323.72) is cache reads alone. On the Max subscription none of this was billed per token – it was the flat monthly fee.

Happy to answer questions! :house_with_garden:

P.S. By that calculation this dashboard is now officially worth $1,830.73 – easily the most expensive light switch in the house. :wink:

3 Likes

Update, 6 Oct 2026 – a week of work since the last post. The biggest part: the two robot vacuums now drive through the 3D house, you can start a cleaning by tapping rooms, and there is a proper card for my “air guard” that stops the ventilation when the outside air is bad.

Robot vacuums on the floor plan

Both robots draw their trail, the area they have cleaned and their own position straight onto the floors of the model.

  • eufy Omni E25 (ground floor, via the robovac_mqtt integration) and Roborock S6 MaxV (upper floor, core integration). Both integrations only hand out the dense trail as a map image – the x/y sensors of the eufy update every 40–90 s, far too slow. So the dashboard reads the map image and lays it onto the floor.
  • Calibration: eufy measured at four walls (5.42 cm per map pixel). For the Roborock I matched the colour area of each of the seven rooms against the room floors of the model – 28 edges, least squares, most of them off by less than 10 cm.
  • The integrations connect far-apart points with a straight line, so a trail would run through walls on the way home. Everything outside a room floor is cut away.
  • The robot glides from one map point to the next at the speed it actually drove and turns into its heading. The eufy map refreshes every ~3 s, the Roborock map every 30–45 s – one lane in the hallway is 6–7 m between two images, so it glides up to 8 m before it jumps.
  • Docked, the eufy sits half inside its station (it backs in), the Roborock under the TV board where it really lives. For that the TV board in the model got 15 cm of floor clearance.
  • 15 minutes after docking the trail fades out by itself. A small eye in the robot card brings it back.

Start a cleaning from the house

  • Every room card has a “Clean kitchen” button. Which robot cleans is decided by the floor.
  • Several rooms: the floor opens from above, you tap the rooms, a bar at the bottom says “3 rooms – clean”. All rooms go to the robot as one job (Roborock app_segment_clean, eufy room_clean with room_ids).
  • The robot cards have the same room buttons with multi-select.
  • The wording follows the cleaning mode: “clean and mop” only when mopping is set – and for the Roborock only if mop and water tank are actually fitted.
  • First real test: Roborock started from the dashboard for the upper hallway at 12:18, back on the dock at 12:30, and the trail followed it live.

Two robot cards that look the same

  • Both cards now start with a drawing of the robot and its station, followed by the same order: actions, map, mode, rooms, suction, water, station (eufy), wear parts, the last 14 days, figures.
  • The map is rendered from the 3D model: an off-screen top view of the floor, roof and upper floors removed. On top of it the trail and the robot are drawn live – you see it drive, with a little arrow for its heading.
  • The eufy shows what its station is doing (washing the mop, drying, emptying the dust bin) with a small animation – Matter only ever said “docked”.
  • The robot buttons in the top bar show a short status underneath (Cleaning, Returning, Washing, Charging). While cleaning the icon wobbles like a driving robot, while charging a green bolt pulses. When the robot is simply ready, nothing is shown.

Air guard card

My ventilation stops automatically as soon as one outside value (NOx, VOC, PM2.5, PM10) goes over its limit, and starts again when all of them have been below for 5 minutes. The red button in the top bar used to open one line of text. Now it is a card:

  • A picture of the house with the paused supply air, since when, and in words when it will start again.
  • The four outside values as bars. The limit is a slider on the bar – drag it and the HA helper (input_number) is updated; change it in HA and the mark moves.
  • Tap a row and its 24 h history opens below, with the limit as a dashed line. Only one at a time.

Sun and solar forecast

The energy view got the sun: its path across today’s sky, a compass ring around the house and a card with azimuth, altitude, today’s solar forecast per hour (Forecast.Solar) and what has been produced so far.

Lights

  • The light button in the top bar: all off (with a second tap to confirm), outside lights on/off, and a tile per room that has lights on.
  • Night lights now light the room with a real falloff in their colour instead of tinting the whole room. A room with all blinds closed gets no daylight any more, and the inner window frames take the brightness of the room behind them.

Small things

  • Long cards keep their title and close button on top while scrolling, and a tap into the house closes them (except the list of open windows – you want to see those while turning the house).
  • Cards are only rewritten when something actually changed. Before, every HA event (my storage system reports every 2 s) replaced the buttons under the mouse and hover flickered.
  • Contrast pass against WCAG AA for all cards.
  • Camera live streams via go2rtc (MSE over WebSocket, native HLS on Safari); a clip replaces the live picture with a clear “recording · not live”.
  • Bins and open windows as tiles with a drawn symbol; tapping a bin in the model opens its pickup dates.
  • Model: the two downpipes on the garden side are gone (they don’t exist), and downpipes and gutters turn see-through when you look into a floor.

Effort – updated

Same method as before (Claude Code session logs, usage per API response, duplicates removed), now for all sessions that worked on the dashboard up to 6 Oct:

Period 18 Sep – 6 Oct 2026
Active time (gaps over 10 min removed) ~70 hours
My messages 636
Model responses 10,094
Output tokens 9.48 million
Input tokens (uncached) 20,244
Cache writes 52.2 million
Cache reads 5.41 billion
What it would have cost at API list prices $2,472.30

By model: Opus 5 $1,524.52, Opus 5.5 $902.45, Fable 5.1 $45.33 – $1,769.78 of it is cache reads alone. Still the flat Max subscription, nothing billed per token.

P.S. The light switch got more expensive: $2,472.30. But now it also tells the vacuum where to go. :wink:

Wow, this is fantastic! The bins rolling out through the garden gate to the pavement (blinking in the same rhythm as your WLED reminder) made me smile, and the x-ray silhouettes with depthFunc: GreaterDepth are a great trick. I also loved the honest cost breakdown: “the most expensive light switch in the house” :grinning_face_with_smiling_eyes:

Funny thing: while you were building yours, I was building almost exactly the same thing for my house, so reading your post felt like déjà vu. Same “plans and a script, not Blender by hand” approach, Three.js, the WebSocket API and about two weeks with AI coding agents (GitHub Copilot in my case). Here’s mine:

A few places where we went different ways, in case they’re useful:

  • Inside Home Assistant: it runs as a sidebar panel using the open session (no extra login, in the app too), or standalone with the Home Assistant OAuth login
  • Dynamic views: when something happens, the camera flies to that room on its own

Dynamic views: the camera flies to wherever something happens

  • Different looks: the same house as a scale model, a drawing, paper and cardboard… or back to the architectural plan it came from

The ground floor as an architectural plan

  • No house logic in the web: two YAML files map objects to entities and say how each type of thing is interpreted
  • Simulated mode that starts from the real state, and a demo editor that renders 4K 60 fps MP4s frame by frame (with a virtual clock too!)

The full write-up, with a second video: My house has a live 3D digital twin · Juancho · notes

I’d love to hear what you think, and to compare notes on what worked (and what didn’t) with the AI agents!

1 Like