Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Home Assistant 3D Floorplan Ext

A standalone Lovelace custom card for placing Home Assistant entities on an interactive 3D house model, with real point-light illumination instead of zone overlays. Load a .glb, switch to Edit Mode, place markers on the model, then configure real lights, animated objects, and covers directly in the card's own editor.

This is a fork of Hollako/Home-Assistant-3D-Floorplan that has grown into a fully independent card: it no longer loads or extends the original at runtime. The custom element tags are custom:home-assistant-3d-floorplan-ext (card) and home-assistant-3d-floorplan-ext-editor (config editor) — both installable side by side with the original card, since neither the tag names nor the config schema collide.

Thanks to the original author

None of this would exist without Hollako's original Home-Assistant-3D-Floorplan card. Getting real-time entity markers, physically-based lighting, and a full 3D model editor working smoothly inside a Lovelace custom card — in plain JavaScript, no build step — is genuinely excellent, well-crafted engineering. The marker system, coordinate handling, camera controls, and editor UI this fork builds on are all their work, and all of it is a pleasure to read and extend. Sincere thanks for building it and sharing it.

Why this fork exists

The original card's room lighting is driven by "Areas" (brightness_zones): flat polygons on the floor that light up their footprint. That works well for a simple floor glow, but it has two structural limits that this fork exists to remove:

  1. Floor level is always 0. The overlay-to-model point conversion zeroes out the vertical axis unconditionally, so a zone can't sit at a real floor height — annoying on any model whose ground floor isn't at world-Y 0.
  2. One zone lights everything inside its polygon. A garden that wraps around a house needs one giant zone, but there's no way to stop that zone's light from leaking through walls into every room inside its own footprint.

Rather than patch around those limits, this fork replaces zone-based lighting entirely with real, individually placed point_lights — each one an actual THREE.PointLight / THREE.SpotLight / LED-strip approximation, positioned in true 3D space, with correct wall-shadowing (opt-in per light) and no polygon-vs-room ambiguity.

What's different from the original

Original This fork
Room/area lighting brightness_zones polygons, floor level fixed at 0 Removed entirely — see point_lights below
Point/spot/LED lights Not available point_lights: point, spot, and linear (LED-strip) kinds, each independently shadow-capable
Shadow rendering Not supported Dynamic shadow budget (max 4 concurrent shadow-casters, only counting lights that are currently on)
Marker tap/hold actions Configured per-marker in the Edit Mode popup Configured centrally in the point_lights editor section; the marker popup is positioning-only
Marker name label Global on/off only Global default plus a per-marker show_label override
Value markers (temperature, etc.) temperature/humidity device classes Also wind_speed, pressure, atmospheric_pressure, illuminance
Cover/blind position tracking Not available slide_animations (linear position → mesh offset) and lamella_groups (tilt position → one-of-three mesh swap)
Stepped visibility Not available segment_visibility (sensor state → mesh show/hide, with hidden_state or show_state logic)
Publishing marker/view changes Manual "Copy YAML" only One-click Publish to Dashboard button that writes straight into the live dashboard config, with a clipboard fallback if it can't
Live robot vacuum position Not available vacuum_trackers: a live-tracked marker (or a real 3D object) that follows a robot vacuum's actual position, calibrated against your model's coordinates
Config editor Original editor, wrapped Fully forked and merged into one editor with collapsible sections
Versioning Upstream version number Own semantic version (currently 1.3.0), shown in the editor and in exported settings files

Everything the original card does that isn't listed above — marker placement, the coordinate system, color thresholds, animations, interactive_objects, camera views, offline-device detection — works the same way here, because most of that code is carried over unchanged.

Install

HACS (custom repository)

  1. HACS → the three-dot menu → Custom repositories.
  2. Add this repository's URL, category Dashboard.
  3. Install Home Assistant 3D Floorplan Ext, then add the resource (HACS does this automatically).

Manual

Copy home-assistant-3d-floorplan-ext.js into <config>/www/, then add it as a Lovelace resource:

url: /local/home-assistant-3d-floorplan-ext.js
type: module

Basic card

type: custom:home-assistant-3d-floorplan-ext
title: My House
model: /local/floorplans/home.glb
view_mode: "3d"
markers: []
point_lights: []

All of the original card's top-level options (marker_size, show_labels, show_entity_state, coordinate_map, ambient_darkness, offline_states, model_performance_profile, and so on) work exactly as documented in the original README — they aren't repeated in full here.

Markers

Marker placement, coordinates, color thresholds, and live-state coloring are unchanged from the original. Two additions:

markers:
  - entity: sensor.garden_wind_speed
    name: Wind speed
    marker_display: value   # auto | icon | value — auto now also covers
                             # wind_speed / pressure / atmospheric_pressure / illuminance
    show_label: hide         # auto | show | hide — overrides the global
                              # "Names" toggle for just this marker (handy
                              # when two markers sit close together)

Tap/hold actions are not set on the marker anymore — see Point Lights below; the marker's Edit Mode popup only has Icon, Marker display, Marker name, Light intensity, and X/Y/Z position.

Point Lights

A real Three.js light, independent of the marker/zone system — no marker required, though a light can follow one automatically.

point_lights:
  - entity: light.kitchen_ceiling
    light_kind: point          # point | spot | linear, default point
    position: {}                # omit or leave a field blank to follow the
                                 # marker with the same entity on that axis
    color: "#ffd9aa"
    intensity: 1.5              # brightness when on; for light.* entities
                                 # this is additionally scaled by the actual
                                 # brightness attribute (0-255)
    distance: 300
    decay: 2
    on_state: "on"
    cast_shadows: false          # see shadow budget below
    tap_action: toggle           # toggle | more-info | navigate | none
    hold_action: more-info
    navigation_path: ""          # used when tap/hold action is "navigate"

light_kind: spot adds a directed cone, aimed down by default:

    angle: 0.5        # cone half-angle, radians
    penumbra: 0.3      # cone edge softness, 0-1
    tilt_x: 0          # degrees, sideways tilt
    tilt_y: 0

light_kind: linear approximates an LED strip as several point-light samples along a line through the position:

    strip_width: 200    # model units
    strip_angle: 0        # degrees, in the X/Z plane
    sample_count: 5        # 2-12

Shadow budget

cast_shadows: true makes a light shadow-casting (walls block it) at the cost of a real-time cube shadow map. Too many simultaneous shadow-casting lights overflow the GPU's shader variable limit and make the entire model disappear — not just the offending light — so this fork enforces a hard cap of 4 concurrent shadow-casters, counted only among lights that are currently on (a linear light's samples count individually). The budget is re-allocated every frame: turn one shadow-casting light off and another one configured for shadows will pick up the freed slot automatically. A 5th light beyond the budget still lights normally, just without its own shadow.

Animated 3D objects, interactive objects

Unchanged from the original — animations (rotate/oscillate/bob tied to an entity state) and interactive_objects (click a mesh to toggle/more-info/navigate, optional per-state color/opacity) work exactly as before.

Slide animations

For covers whose position (not just open/closed) should move a mesh — rollershutters, blinds sliding on a track:

slide_animations:
  - object_name: rollo_mitte
    entity: cover.living_room_blind
    attribute: current_position   # optional, default current_position
    axis: y                        # x | y | z, optional default y
    from_value: 0
    to_value: 100
    distance: 70.0                  # model units moved from open to closed
    invert: false
    hide_at_top: true                # hide the mesh once fully retracted

Lamella groups

For venetian-blind style covers where tilt position selects one of three fixed mesh states (closed/half/open) rather than a continuous slide:

lamella_groups:
  - entity: cover.east_blind
    attribute: current_tilt_position   # optional, default current_tilt_position
    objects:
      zu: blind_east_closed
      halb: blind_east_half
      auf: blind_east_open
    thresholds:                         # optional, default 33 / 66
      zu_max: 33
      halb_max: 66

Segment visibility

Simple 1:1 sensor-to-mesh visibility, e.g. for a pergola roof that reveals itself in steps:

segment_visibility:
  - object_name: entity_pergola_segment_01
    entity: binary_sensor.pergola_under_10_percent
    hidden_state: "on"    # optional, default "on" — hidden while the sensor
                           # has this state; ANY other state (including
                           # "unavailable") shows the object
    show_state: ""          # optional — if set, takes priority over
                             # hidden_state: the object is visible ONLY
                             # while the entity is exactly this state,
                             # everything else (including "unavailable")
                             # hides it

object_name matches both the exact mesh name and any <object_name>_m<N> material-split siblings that SweetHome3D/Blender exports produce for multi-material objects — all parts show/hide together.

Vacuum trackers

Shows a robot vacuum's live position inside the 3D model — either as a small pulsing marker, or as a real 3D object that moves across the floor. This isn't specific to any one vacuum brand: it works with any integration that exposes the vacuum's position as {x, y} (or {x, y, a} with a heading angle) in the vacuum's own fixed local coordinate system — for example the Dreame Vacuum integration's camera.<vacuum>_map entity, whose vacuum_position/charger_position attributes this was built and tested against, as well as most Xiaomi/Roborock/Valetudo-based integrations that expose similar map data.

vacuum_trackers:
  - entity: vacuum.my_robot                    # used only to read the current state, for hiding
    map_entity: camera.my_robot_map             # entity carrying the live position attribute
    position_attribute: vacuum_position          # optional, default "vacuum_position"
    hidden_states: docked                          # comma-separated (editor) or YAML list — states
                                                     # in which the marker/object is hidden entirely.
                                                     # Leave empty to never hide it.
    height: 78                                       # model Y (floor height) the marker/object sits at
    icon: mdi:robot-vacuum                            # icon-marker mode only (see below)
    calibration:
      a: 0.0
      b: 0.0
      tx: 0.0
      ty: 0.0
    invert_y: false                                    # see "Getting the calibration right" below

Two rendering modes

  • Icon marker (default) — nothing further to configure. A small round marker with the configured icon appears and moves across the floor.
  • Real 3D object — add object_name (and origin, see below) to move an actual mesh from your model instead of a flat icon:
    object_name: entity_vacuum_robot   # a mesh in your model, named/tagged the
                                         # same way as segment_visibility objects
    origin: { x: -80, y: 75, z: 364 }    # the exact position where you PLACED
                                          # that object in your 3D editor (see below)
    rotate_with_heading: true              # optional: rotate the object to face
                                            # its direction of travel
    heading_offset: 0                        # optional, degrees — corrects for
                                              # whichever direction "forward" faces
                                              # on the modeled object

If object_name is set but the mesh doesn't exist in the currently loaded model (e.g. you haven't re-exported it yet), this falls back to the icon marker automatically — nothing breaks.

Why origin is required for the 3D-object mode: SweetHome3D (and most other 3D editors) bake an object's absolute position directly into its exported vertex data — the loaded mesh's own position starts at (0, 0, 0) regardless of where it visually sits in the model. To move it to a live, calculated position, this fork needs to know where it started (i.e. exactly where you placed it while modeling), so it can apply the difference as an offset. The simplest way to get an exact, reliable origin: model the vacuum sitting exactly on its charging dock, then use the dock's known coordinates (see calibration below) as origin — since the vacuum returns to that exact spot every time it docks, it's a self-verifying reference point.

Getting the calibration right

calibration is a 2D similarity transform (rotation + uniform scale + translation) that converts the vacuum's own raw {x, y} coordinates into your model's {x, z} coordinates (floor plane; model y stays fixed at height):

modelX = a·vx − b·vy + tx
modelZ = b·vx + a·vy + ty

To find a, b, tx, ty, you need two known, exact correspondences between a raw vacuum coordinate and the matching 3D-model coordinate:

  1. The charging dock. Whenever the vacuum is docked, its position attribute reads exactly the dock's fixed coordinates — a free, always-precise reference point. Note that raw (vx, vy) down from your vacuum's map data.
  2. A second point, as far from the dock as practical. Pause the vacuum somewhere else in the house, note its current raw (vx, vy), and in Edit Mode place a temporary marker at that exact physical spot to read off the matching model (x, z). The farther apart the two points are, the more accurate the resulting rotation/scale — two points close together amplify any small measurement error into a large rotational error far away.

With two point pairs (v1 → p1) and (v2 → p2) (as complex numbers v = vx + i·vy, p = x + i·z):

s = (p1 − p2) / (v1 − v2)        # s = a + i·b
t = p1 − s·v1                     # t = tx + i·ty

Sanity-check the scale (|s| = sqrt(a² + b²)) against your model's real-world scale — e.g. if your model is authored in centimeters, |s| should be close to 100 (model units per real meter). A value wildly off (like 2× or 0.5×) usually means one of the two reference points wasn't actually exact.

invert_y: the formula above is a pure rotation — it can't represent a mirrored coordinate system. If, after calibrating, one axis comes out backwards (e.g. left/right is correct but front/back is reversed, or vice versa), set invert_y: true and recompute a/b/tx/ty using (vx, −vy) in place of (vx, vy) for both reference points — a straight rotation fit through the mirrored inputs. This isn't a rare edge case: whether the vacuum's own map coordinate system happens to be mirrored relative to your 3D model depends on details of how the vacuum built its map, and can't be predicted in advance — treat it as a normal part of calibrating, not a sign something's broken.

Two smaller pitfalls this fork's implementation already handles for you, in case you're porting the idea elsewhere:

  • The live position keeps updating even while the marker/object is hidden (e.g. docked), so when it reappears it's already at the correct spot instead of visibly animating in from a stale position.
  • hidden_states should generally include only the truly-parked state (e.g. docked) — a state like "returning to dock" still means the vacuum is visibly moving across the floor, so hiding it there makes it disappear well before it actually arrives.

Publish to Dashboard

Marker positions and the "Save Home" startup camera view are saved to the browser's localStorage when you place/adjust them in Edit Mode — convenient for iterating, but invisible on any other device until they're written into the actual dashboard config. Edit Mode's sidebar has a Publish to Dashboard button that does this itself, using the browser's already-authenticated Home Assistant connection:

  1. Fetches the current dashboard config.
  2. Finds the one card on it matching this card's type and model path.
  3. Overwrites only markers, default_view, ambient_darkness, light_presets, and a few performance settings on that card — point_lights, animations, interactive_objects, and everything else are left untouched.
  4. Asks for confirmation, then saves.

If it can't find exactly one matching card (or has no live connection), it copies the same data to the clipboard instead, so nothing is lost — paste it into Home Assistant's own "Edit in YAML" card editor.

Coordinate system, camera views, offline detection, color thresholds

All unchanged from the original — see its README for the coordinate convention, compass/Save Home behavior, and offline-marker alert.

License

ISC — see LICENSE. Same license as the upstream project, with an added copyright line for this fork's own contributions.

Credits

Built on top of Hollako/Home-Assistant-3D-Floorplan. All credit for the original card, marker system, and 3D rendering foundation goes to its author.

About

Standalone Lovelace 3D floorplan card for Home Assistant with real point/spot/LED lights, cover animations, and stepped visibility -- forked from Hollako/Home-Assistant-3D-Floorplan

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages