How to set up layers for different floorplans

ha-floorplan.html (continued!):

    getServerMoment() {
      let serverMoment = moment();
      if (this.timeDifference >= 0)
        serverMoment.subtract(this.timeDifference, 'milliseconds');
      else
        serverMoment.add(Math.abs(this.timeDifference), 'milliseconds');
      return serverMoment;
    },

    getArray(list) {
      return Array.isArray(list) ? list : Object.keys(list).map(key => list[key]);
    },

    assemble(code, entity, entities) {
      let functionBody = (code.indexOf('return') >= 0) ? code : `return \`${code}\`;`;
      let func = new Function('entity', 'entities', 'hass', 'config', functionBody);
      return func(entity, entities, this.hass, this.config);
    },

    error(message) {
      let errors = Polymer.dom(this.$.errors).node;
      $(errors).find('ul').append(`<li>${message}</li>`)
      $(errors).css('display', 'block');
    },

    warn(message) {
      if ((this.config.warnings === null) || (this.config.warnings !== undefined)) {
        let warnings = Polymer.dom(this.$.warnings).node;
        $(warnings).find('ul').append(`<li>${message}</li>`)
        $(warnings).css('display', 'block');
      }
    },

    debug(message) {
      let debug = Polymer.dom(this.$.debug).node;
      $(debug).find('ul').append(`<li>${message}</li>`)
      $(debug).css('display', 'block');
    },

    loadStyleSheet(path, fn, scope) {
      let head = document.getElementsByTagName('head')[0]; // reference to document.head for appending/ removing link nodes
      let link = document.createElement('link');           // create the link node
      link.setAttribute('href', path);
      link.setAttribute('rel', 'stylesheet');
      link.setAttribute('type', 'text/css');

      let sheet, cssRules;
      // get the correct properties to check for depending on the browser
      if ('sheet' in link) {
        sheet = 'sheet'; cssRules = 'cssRules';
      }
      else {
        sheet = 'styleSheet'; cssRules = 'rules';
      }

      let interval_id = setInterval(function () {                     // start checking whether the style sheet has successfully loaded
        try {
          if (link[sheet] && link[sheet][cssRules].length) { // SUCCESS! our style sheet has loaded
            clearInterval(interval_id);                      // clear the counters
            clearTimeout(timeout_id);
            fn.call(scope || window, true, link);           // fire the callback with success == true
          }
        } catch (e) { } finally { }
      }, 10),                                                   // how often to check if the stylesheet is loaded
        timeout_id = setTimeout(function () {       // start counting down till fail
          clearInterval(interval_id);             // clear the counters
          clearTimeout(timeout_id);
          head.removeChild(link);                // since the style sheet didn't load, remove the link node from the DOM
          fn.call(scope || window, false, link); // fire the callback with success == false
        }, 15000);                                 // how long to wait before failing

      head.appendChild(link);  // insert the link node into the DOM and start loading the style sheet

      return link; // return the link node;
    },

    rgbToHex(rgb) {
      return "#" + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1);
    },

    hexToRgb(hex) {
      // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
      let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
      hex = hex.replace(shorthandRegex, (m, r, g, b) => {
        return r + r + g + g + b + b;
      });

      let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    },

    mix(color1, color2, weight) {
      let p = weight;
      let w = p * 2 - 1;
      let w1 = ((w / 1) + 1) / 2;
      let w2 = 1 - w1;
      let rgb = [
        Math.round(color1.r * w1 + color2.r * w2),
        Math.round(color1.g * w1 + color2.g * w2),
        Math.round(color1.b * w1 + color2.b * w2)
      ];
      return rgb;
    }
  });

</script>

Awesome! Will test this later! What was different from your first attemt?

The code for both the js and html was altered by a good willing ha user… Also, silly things like deleting the browser’s cache or getting rid of extra Inkscape labels and tags helped…

Ok, will use the ones you posted then :slight_smile: Thank you for your hard work and sharing! :smile:

…and it`s working! Thanks again!

When using different layers, you may find that you want to use the same entity in different layers. I found an easy workaround for this for most of my devices, ie MQTT devices. The workaround I found for these duplicate entities is to create a new entity per different layer as follows:

- name: "Stairs"
  command_topic: milight/EDIT/rgb_cct/1
  state_topic: milight/updates/EDIT/rgb_cct/1
  <<: &MILIGHT_PARAMS
      platform: mqtt_json
      computed_color: true
      color_temp: true
      brightness: true
- name: "Upstairs Stairs"
  command_topic: milight/EDIT/rgb_cct/1
  state_topic: milight/updates/EDIT/rgb_cct/1
  <<: *MILIGHT_PARAMS #copy of light.stairs created for floorplan multilayers

Not the prettiest solution, as it adds a lot of unneeded code, but it works. Since these duplicated entities use the same MQTT topics, their statuses are linked and update at the same time on all layers. Sweet!

For the only other type of entity that I have on several layers, ie. camera, I just created a group with only that entity. This kinda works, but looks a bit crappy… Snapshot displays, but unnecessary info is added and no correct status (‘Streaming’, ‘Recording’ or ‘Idle’) is associated. I might have to look into @pkozul’s latest published camera implementation of floorplan, but that would involve editing the changes made in the HA-floorplant.htm and that could take more time than I have.

Let me know, if you found another more elegant solution…

Hi!
I’m trying to get this working but after adding your files nothing happens. Are you sure that I don’t need to change anything else? Like adding the buttons in the floorpan.yaml or similar. The buttons are dead so I don’t know if the rest will work after fixing that.

First thing I’d check is the svg IDs for buttons and especially layers. If you use Inkscape, this can be checked in the XML editor.

yes, what @monkey-house said. And be sure to comment out the start of the Javascript and html document (the posted ones are not). between the posted html and html continued there should be a empty line (don’t know if that is necessary, but to be sure, just do that). and meaby most important, clean your browser cache. if it’s still not working, try the same on a different device, ex. from Google Chrome on tablet to Google Chrome on a computer (and if course clean the browser cache on the new device first)

1 Like

Thank you. Both checked with the editor to be as described. page0 and show.page0
Still no luck unfortunately. I’ll check the .js for any errors due to the split of the file.

It seems to be the buttons I have issues with. I have tried to search, but is there an instruction for how to add the “onclick” code to the buttons? I guess it should be done in Incscape with xml-editor?

You don`t need to do anything to the onclick function of the elements in the svg-file.
Since it’s not working out of the box, meaby just make a new simple svg-file to test.

Make one layer with id: buttons. This layer will always be visible. Make one layer with id: page0
Make one layer with id: page1
On the buttons-layer, make a element with id: show.toggle
On page0, a rectangel
On page1, a circle

Then, in google chrome, delete the browser cache (make sure to check off all three boxes), then close the browser, relaunch the browser, and check if pressing the show.toggle button, it`s changing between the circle and the rectangle. If it does, all is good, and just copy the old file back, delete browser cache again (if necessary) close the browser, and relaunch the browser.

And just to make sure, when opening custom_js in your editor, it`s not called custom_js.js.js? If you have named it custom_js.js, it may got an extra “.js” at the end depending on how you edited it.

And you have comment out the start of the custom_js?

So the start of custom_js should look like this:

/**
  Pages start at 0 and go to 9
  Objects named 'show.pageN' where N is a number from 0-9 will show page N when clicked
  Objects named 'show.toggle' will cycle through all pages
  Layers in the SVG should be named page0-page9
  page0 is the default starting page 
*/ 

and not like this:

  Pages start at 0 and go to 9
  Objects named 'show.pageN' where N is a number from 0-9 will show page N when clicked
  Objects named 'show.toggle' will cycle through all pages
  Layers in the SVG should be named page0-page9
  page0 is the default starting page 
*/ 

Not sure if the above is necessary, but just to be sure do it.

And at last the space between the two html document is there like this:

setTransitionFill(svgElement, fromFill, toFill, value) {
  if (value >= 1) {
    svgElement.style.fill = fromFill;
  }
  else if (value <= 0) {
    svgElement.style.fill = toFill;
  }
  else {
    let color = this.rgbToHex(this.mix(this.hexToRgb(toFill), this.hexToRgb(fromFill), value));
    svgElement.style.fill = color;
  }
},

getServerMoment() {
  let serverMoment = moment();
  if (this.timeDifference >= 0)
    serverMoment.subtract(this.timeDifference, 'milliseconds');
  else
    serverMoment.add(Math.abs(this.timeDifference), 'milliseconds');
  return serverMoment;
},

getArray(list) {
  return Array.isArray(list) ? list : Object.keys(list).map(key => list[key]);
},

assemble(code, entity, entities) {
  let functionBody = (code.indexOf('return') >= 0) ? code : `return \`${code}\`;`;
  let func = new Function('entity', 'entities', 'hass', 'config', functionBody);
  return func(entity, entities, this.hass, this.config);
},

I am sorry if you have tried all this, but if all this is done, I don’t know what could be wrong.

Thank you so much for the time spent on that answer! I will go through the points and also test a simple svg if the others don’t help.
By onclick I was refferring to some other comments in this thread that shows this:
onclick="case ‘select’:
for (let otherElement of action.data.elements) {
let otherSvgElement = $(svg).find([id="${otherElement.id}"]);
let otherSvgElementclass = otherElement.class;

            $(otherSvgElement).removeClass().addClass(otherSvgElementclass);
          }
        break;">

Is that not needed to get this functionality going?

No problem :slight_smile: No, not at all, that was something we looked at before @monkey-house got the javascript to work.

Wow, then I’ve waisted some time :smiley:

Yeah, you and me both :stuck_out_tongue: Used some time on layer change with no luck before this too.

It works! So now I’m only waiting for a fix for Xiaomi multisensors changing names and the use of sliders in Floorplan, then I’m all set :slight_smile:
But now, with multilayer I can actually get going with floorplan.
Thank you!

2 Likes

I recently followed the instructions you laid out above and as far as I can tell the naming conventions in the svg are correct. The custom_js.js and ha-floorplan.html have the code above but Homeassistant returns:

Uncaught ReferenceError: svg is not defined
URL: http://192.168.1.57:8123/local/custom_ui/floorplan/ha-floorplan.html
Line: 166, column: 53
Error: {}

Does anyone know what may cause this? Line 166 is the added line $(document).trigger( “floorplan:loaded”, [this, svg]); if I open it in NotePad++

Thanks!

1 Like

Have you deleted the browser cache? then closed and opened your browser again?

Yeah I cleared the cache both in fully kiosk and cleared the browser on my pc that I was using to setup HA/Floorplan. They both prompt with the same ha-floorplan.html uncaught reference error.

Out of curiosity what version of FloorPlan are you running 1.0.6 or 1.0.7.4?