Skip to content

Conversation

@OskarLinde
Copy link
Contributor

@OskarLinde OskarLinde commented May 2, 2025

This PR exposes the page-local <svg> element as a global variable svg inside rule scripts.

Why?

In many cases, it's necessary to access and manipulate SVG elements programmatically — especially when:

  • The elements can't be grouped cleanly (e.g., interleaved geometry)
  • They lack unique IDs
  • The rule doesn't target a specific item directly

Currently, this requires traversing the DOM manually to locate the correct shadow root and extract the right element, which is error-prone in multi-page setups and often results in dozens of lines of utility code. It's easy to accidentally target another view’s SVG, leading to subtle bugs.

This change injects the page-local <svg> element as a scoped svg variable into the script execution context, making it directly and safely accessible from within rules. This greatly simplifies common patterns like:

const zone = svg.querySelector('[data-mesh-id="zone.kitchen"]');

and removes the need for custom DOM discovery logic typically injected via startup_action. There may be other ways to achieve this that I’ve overlooked, but this approach has worked very well for me in practice.

@exetico
Copy link
Member

exetico commented May 2, 2025

Hi @OskarLinde

Thanks for the PR! I understand the underlaying though, but I actually don't agree that much. With that said: It's not always required to agree upon a thing, if it's just a question about exposing another context - and that's the case here!

Therefore I'll happily merge it 😄.

@exetico exetico merged commit 52e6b0f into ExperienceLovelace:master May 2, 2025
3 checks passed
@exetico
Copy link
Member

exetico commented May 2, 2025

Let me iterate:
Why would you ever end up in a situation where "They lack unique IDs"? It's your own play-ground, where you're able to manage both the entity in HA, and the SVG file in your SVG/code-editor.

Also, could I ask you to provide some additional context on why "The rule doesn't target a specific item directly" are a good argument? I'm asking, just to get more context about your usage of ha-floorplan. As we're not collecting any user matrix data, it's helpful to get some specific examples now and then 😄!

@OskarLinde
Copy link
Contributor Author

OskarLinde commented May 3, 2025

Thanks for merging!

I will elaborate. Maybe my usage is a bit unusual :-).

Why unique IDs aren't possible:
This is an SVG autogenerated from a 3D mesh (using a tool I made). The draw paths in the SVG file are individual sides/polygons of the 3D geometry, and each 3D object has a unique identifier – that I then assign as a mesh-id property for each element in the SVG file. Since the objects are three dimensional, parts of objects occlude other objects – so the draw order needs to be computed and represented in the SVG file by ordering the DOM elements (things further down in the SVG file are drawn later). This means that a single object is represented by multiple paths (up to several hundred, even thousand) and they cannot be grouped together since other geometry might need to be drawn in between them. Here's an example of what part of the SVG file looks like: see for example how parts of the walls.0.3 object is interleaved between other objects, because some parts of the walls need to be drawn after parts of the individual objects (because they are in front) – other parts need to be drawn before.

  <line class="zone stroke" data-mesh-id="zone.dining" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="245.0622" x2="245.0622" y1="480.2159" y2="440.4425"/>
  <path class="zone fill" d="M 174.354,334.380 L 174.354,374.153 L 245.062,480.216 Z" data-mesh-id="zone.dining" fill="#0f0f0f" fill-rule="evenodd" stroke="none"/>
  <line class="zone stroke" data-mesh-id="zone.dining" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="174.3539" x2="174.3539" y1="334.38" y2="374.1534"/>
  <line class="zone stroke" data-mesh-id="zone.dining" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="174.3539" x2="245.0622" y1="374.1534" y2="480.2159"/>
  <path class="walls fill" d="M 354.232,249.228 L 373.368,234.873 L 331.269,260.710 Z" data-mesh-id="walls.0.3" fill="#0f0f0f" fill-rule="evenodd" stroke="none"/>
  <line class="walls stroke" data-mesh-id="walls.0.3" stroke="#CCCCCC" stroke-linecap="round" stroke-width="0.5px" x1="331.2691" x2="354.2323" y1="260.7099" y2="249.2283"/>
  <path class="walls fill" d="M 373.368,234.873 L 327.442,257.836 L 331.269,260.710 Z" data-mesh-id="walls.0.3" fill="#0f0f0f" fill-rule="evenodd" stroke="none"/>
  <line class="walls stroke" data-mesh-id="walls.0.3" stroke="#CCCCCC" stroke-linecap="round" stroke-width="0.5px" x1="373.3683" x2="327.4419" y1="234.8728" y2="257.836"/>
  <path class="zone fill" d="M 369.425,320.811 L 327.442,257.836 L 327.442,297.609 Z" data-mesh-id="zone.pantry" fill="#0f0f0f" fill-rule="evenodd" stroke="none"/>
  <line class="zone stroke" data-mesh-id="zone.pantry" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="369.425" x2="327.4419" y1="320.8106" y2="257.836"/>
  <line class="zone stroke" data-mesh-id="zone.pantry" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="327.4419" x2="327.4419" y1="257.836" y2="297.6094"/>
  <path class="walls fill" d="M 315.960,268.364 L 323.615,259.750 L 292.997,279.846 Z" data-mesh-id="walls.0.3" fill="#0f0f0f" fill-rule="evenodd" stroke="none"/>
  <line class="walls stroke" data-mesh-id="walls.0.3" stroke="#CCCCCC" stroke-linecap="round" stroke-width="0.5px" x1="292.9971" x2="315.9603" y1="279.8459" y2="268.3643"/>
  <path class="walls fill" d="M 323.615,259.750 L 302.565,270.274 L 292.997,279.846 Z" data-mesh-id="walls.0.3" fill="#0f0f0f" fill-rule="evenodd" stroke="none"/>
  <line class="walls stroke" data-mesh-id="walls.0.3" stroke="#CCCCCC" stroke-linecap="round" stroke-width="0.5px" x1="323.6147" x2="302.5651" y1="259.7496" y2="270.2744"/>
  <path class="walls fill" d="M 373.368,234.873 L 373.368,274.646 L 415.351,337.621 Z" data-mesh-id="walls.0.3" fill="#191919" fill-rule="evenodd" stroke="none"/>
  <line class="walls stroke" data-mesh-id="walls.0.3" stroke="#CCCCCC" stroke-linecap="round" stroke-width="0.5px" x1="373.3683" x2="373.3683" y1="234.8728" y2="274.6462"/>
  <line class="walls stroke" data-mesh-id="walls.0.3" stroke="#CCCCCC" stroke-linecap="round" stroke-width="0.5px" x1="373.3683" x2="415.3514" y1="274.6462" y2="337.6208"/>
  <path class="zone fill" d="M 285.343,278.886 L 347.212,371.690 L 351.040,369.777 Z" data-mesh-id="zone.downstairs_hallway" fill="#191919" fill-rule="evenodd" stroke="none"/>
  <line class="zone stroke" data-mesh-id="zone.downstairs_hallway" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="285.3427" x2="347.2125" y1="278.8856" y2="371.6903"/>
  <line class="zone stroke" data-mesh-id="zone.downstairs_hallway" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="347.2125" x2="351.0397" y1="371.6903" y2="369.7766"/>
  <path class="zone fill" d="M 347.212,371.690 L 285.343,278.886 L 347.212,411.464 Z" data-mesh-id="zone.downstairs_hallway" fill="#191919" fill-rule="evenodd" stroke="none"/>
  <line class="zone stroke" data-mesh-id="zone.downstairs_hallway" stroke="#FF0000" stroke-linecap="round" stroke-opacity="0.5" stroke-width="0.5px" x1="347.2125" x2="285.3427" y1="371.6903" y2="278.8856"/>

One advantage of this is that in the 3D editor I can simply create an object and name it for example "binary_sensor.garage_door.closed", and then make another version of the same object – shift/rotate it to swing open – and name it "binary_sensor.garage_door.open".

Here's the rule that takes advantage of the svg variable. Without that variable, this would have required a ton more code:

            - entities:
                - binary_sensor.garage_door
                - binary_sensor.hallway_garage_door
              state_action:
                action: call-service
                service: floorplan.execute
                service_data:
                  code: |
                    >
                      const openEls = Array.from(svg.querySelectorAll('[data-mesh-id="' + entity.entity_id + '.open"]'));
                      const closedEls = Array.from(svg.querySelectorAll('[data-mesh-id="' + entity.entity_id + '.closed"]'));
                      
                      openEls.forEach(el => {
                        el.classList.toggle('open', entity.state === 'on');
                        el.classList.toggle('unavailable', entity.state === 'unavailable');
                      });
                      
                      closedEls.forEach(el => {
                        el.classList.toggle('closed', entity.state === 'off');
                        el.classList.toggle('unavailable', entity.state === 'unavailable');
                      });

with the corresponding css:

/* Unavailable: closed mesh = red + blinking */
[data-mesh-id$=".closed"].unavailable {
  fill: red;
  visibility: visible;
  opacity: 1;
  animation: blink 1s steps(2, start) infinite;
}

/* Unavailable: open mesh = hidden */
[data-mesh-id$=".open"].unavailable {
  visibility: hidden;
  opacity: 0;
  animation: none;
}

/* Normal closed state (non-blinking) */
[data-mesh-id$=".closed"].closed {
  fill: green;
  visibility: visible;
  opacity: 1;
  animation: none;
}

/* Normal open state */
[data-mesh-id$=".open"].open {
  fill: red;
  visibility: visible;
  opacity: 1;
  animation: none;
}

/* Hidden fallback for open/closed not matching state */
[data-mesh-id$=".closed"]:not(.closed):not(.unavailable) {
  visibility: hidden;
  opacity: 0;
}

[data-mesh-id$=".open"]:not(.open):not(.unavailable) {
  visibility: hidden;
  opacity: 0;
}

/* Blinking animation */
@keyframes blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

Note, this is all work in progress.

@rafalcieslak
Copy link

Would it be much to ask for a new minor release containing these changes? 🙏

I would like to access the svg root for loads of custom advanced logic (e.g. dynamically copying values from one area of the SVG to another, accessing SVG metadata and reusable gradients which my SVG editor fails to name consistently), and the convenience of installing via HACS is immesurable.

@exetico
Copy link
Member

exetico commented Jun 2, 2025

Sure, @rafalcieslak, thank you for the bump. I didn't meant to hold back the changes.

v1.1.3: https://github.com/ExperienceLovelace/ha-floorplan/releases/tag/v1.1.3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants