Advertisement
philmottin

ha-floorplan.html

Jun 16th, 2018
298
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 34.53 KB | None | 0 0
  1. <!--
  2.  Floorplan for Home Assistant
  3.  Version: 1.0.13
  4.  https://github.com/pkozul/ha-floorplan
  5.  
  6. *added multilayer support
  7. *fix for mutilayer floor conflit with mdi icon named SVG
  8. -->
  9.  
  10. <script src="lib/jquery-3.2.1.min.js"></script>
  11. <script src="lib/moment.min.js"></script>
  12. <script src="lib/svg-pan-zoom.min.js"></script>
  13. <script src="lib/custom_js.js"></script>
  14.  
  15. <!-- As documented here for chrome, removes the need for touchstart -->
  16. <meta name="viewport" content="width=device-width">
  17.  
  18. <dom-module id="ha-floorplan">
  19.  
  20.   <template>
  21.     <style>
  22.       .loading-container {
  23.         text-align: center;
  24.         padding: 8px;
  25.       }
  26.  
  27.       .loading {
  28.         height: 0px;
  29.         overflow: hidden;
  30.       }
  31.  
  32.       #errors {
  33.         color: #FF0000;
  34.         display: none;
  35.       }
  36.  
  37.       #warnings {
  38.         color: #FF851B;
  39.         display: none;
  40.       }
  41.  
  42.       #debug {
  43.         color: #000000;
  44.         display: none;
  45.       }
  46.     </style>
  47.  
  48.     <template is='dom-if' if='[[isLoading]]'>
  49.       <div class='loading-container'>
  50.         <paper-spinner active alt='Loading'></paper-spinner>
  51.       </div>
  52.     </template>
  53.  
  54.     <div id="errors">
  55.       <ul></ul>
  56.     </div>
  57.  
  58.     <div id="warnings">
  59.       <ul></ul>
  60.     </div>
  61.  
  62.     <div id="debug">
  63.       <ul></ul>
  64.     </div>
  65.  
  66.     <div id="floorplan" on-tap="stopPropagation"></div>
  67.  
  68.   </template>
  69.  
  70. </dom-module>
  71.  
  72. <script>
  73. //fix for mutilayer floor conflit with mdi icon named SVG
  74. var g_svg= ‘’;
  75.  
  76.   class HaFloorplan extends Polymer.Element {
  77.     static get is() { return 'ha-floorplan'; }
  78.  
  79.     static get properties() {
  80.       return {
  81.         hass: {
  82.           type: Object,
  83.           observer: 'hassChanged'
  84.         },
  85.         inDialog: {
  86.           type: Boolean,
  87.           value: false,
  88.         },
  89.         isPanel: {
  90.           type: Boolean,
  91.           value: false,
  92.         },
  93.         config: {
  94.           type: Object,
  95.         },
  96.         isLoading: {
  97.           type: Boolean,
  98.           value: true,
  99.         },
  100.         timeDifference: {
  101.           type: Number,
  102.           value: undefined,
  103.         },
  104.         entityConfigs: {
  105.           type: Array,
  106.           value: () => { return []; },
  107.         },
  108.         elementConfigs: {
  109.           type: Array,
  110.           value: () => { return []; },
  111.         },
  112.         cssRules: {
  113.           type: Array,
  114.           value: () => { return []; },
  115.         },
  116.         isInitialized: {
  117.           type: Boolean,
  118.           value: false,
  119.         },
  120.       };
  121.     }
  122.  
  123.     connectedCallback() {
  124.       super.connectedCallback();
  125.  
  126.       if (!this.isInitialized) {
  127.         this.initFloorplan();
  128.       }
  129.     }
  130.  
  131.     stopPropagation(e) {
  132.       e.stopPropagation();
  133.     }
  134.  
  135.     hassChanged(newHass, oldHass) {
  136.       this.handleEntities(newHass.states);
  137.     }
  138.  
  139.     initFloorplan() {
  140.       this.isInitialized = true;
  141.  
  142.       window.onerror = this.handleWindowError.bind(this);
  143.  
  144.       if (!this.config.groups) {
  145.         this.isLoading = false;
  146.         this.warn(`Cannot find 'groups' in floorplan configuration`);
  147.         return;
  148.       }
  149.  
  150.       let invalidGroups = this.config.groups.filter(x => x.entities && x.elements);
  151.       if (invalidGroups.length) {
  152.         this.isLoading = false;
  153.         this.warn(`A group cannot contain both 'entities' and 'elements' in floorplan configuration`);
  154.         return;
  155.       }
  156.  
  157.       invalidGroups = this.config.groups.filter(x => !x.entities && !x.elements);
  158.       if (invalidGroups.length) {
  159.         this.isLoading = false;
  160.         this.warn(`A group must contain either 'entities' or 'elements' in floorplan configuration`);
  161.         return;
  162.       }
  163.  
  164.       this.hass.connection.socket.addEventListener('message', event => {
  165.         let data = JSON.parse(event.data);
  166.  
  167.         // Store the time difference between the local web browser and the Home Assistant server
  168.         if (data.event && data.event.time_fired) {
  169.          let lastEventFiredTime = moment(data.event.time_fired).toDate();
  170.           this.timeDifference = moment().diff(moment(lastEventFiredTime), 'milliseconds');
  171.         }
  172.       });
  173.  
  174.       this.addExternalCss(() => {
  175.         this.loadFloorPlan(() => {
  176.           this.isLoading = false;
  177.           this.handleEntities(this.hass.states);
  178.           //original mutilayer addon
  179.           //$(document).trigger( “floorplan:loaded”, [this, svg]);
  180.          
  181.           //fix for mutilayer floor conflit with mdi icon named SVG
  182.           $(document).trigger( “floorplan:loaded”, [this, g_svg]);
  183.         });
  184.       });
  185.  
  186.       if (this.config.groups.find(entityGroup => entityGroup.state_transitions)) {
  187.         setInterval(this.updateStateTransitions.bind(this), 100);
  188.       }
  189.     }
  190.  
  191.     handleWindowError(msg, url, lineNo, columnNo, error) {
  192.       if (msg.toLowerCase().indexOf("script error") >= 0) {
  193.         this.error('Script error: See browser console for detail');
  194.       }
  195.       else {
  196.         let message = [
  197.           msg,
  198.           'URL: ' + url,
  199.           'Line: ' + lineNo + ', column: ' + columnNo,
  200.           'Error: ' + JSON.stringify(error)
  201.         ].join('<br>');
  202.  
  203.         this.error(message);
  204.       }
  205.  
  206.       return false;
  207.     }
  208.  
  209.     addExternalCss(callback) {
  210.       if (!this.config.stylesheet) {
  211.         callback();
  212.       }
  213.  
  214.       this.loadStyleSheet(this.config.stylesheet + '?cacheBuster=' + (new Date().getTime()), function (success, link) {
  215.         if (success) {
  216.           this.instance.root.appendChild(link);
  217.           let styleSheet = link['sheet'];
  218.           setTimeout(() => {
  219.             this.instance.cssRules = this.instance.getArray(styleSheet.cssRules);
  220.             callback();
  221.           }, 1000);
  222.         }
  223.         else {
  224.           this.instance.error("Error loading stylesheet");
  225.         }
  226.       }.bind({ instance: this, callback: callback }));
  227.     }
  228.  
  229.     loadFloorPlan(callback) {
  230.       jQuery.ajax({
  231.         url: this.config.image + '?cacheBuster=' + (new Date().getTime()),
  232.         success: function (result) {
  233.           let svg = $(result).find('svg')[0];
  234.          
  235.           //fix for mutilayer floor conflit with mdi icon named SVG
  236.           g_svg = svg;
  237.          
  238.           $(svg).height('100%');
  239.           $(svg).width('100%');
  240.           $(svg).css('position', this.instance.isPanel ? 'absolute' : 'relative');
  241.           $(svg).css('cursor', 'default');
  242.  
  243.           this.instance.$.floorplan.appendChild(svg);
  244.  
  245.           let uniqueId = (new Date()).getTime();
  246.  
  247.           let svgElements = $(svg).find('*').toArray();
  248.  
  249.           let elementGroups = this.instance.config.groups.filter(x => x.elements);
  250.           for (let elementGroup of elementGroups) {
  251.             for (let elementId of elementGroup.elements) {
  252.               let svgElement = $(svg).find(`[id="${elementId}"]`);
  253.  
  254.               if (svgElement.length) {
  255.                 $(svgElement).on('click', this.instance.onElementClick.bind({ instance: this.instance, elementId: elementId }));
  256.                 $(svgElement).css('cursor', 'pointer');
  257.  
  258.                 let elementConfig = {
  259.                   group: elementGroup,
  260.                 };
  261.  
  262.                 this.instance.elementConfigs[elementId] = elementConfig;
  263.  
  264.                 if (elementGroup.action.data.elements) {
  265.                   for (let otherElementId of elementGroup.action.data.elements) {
  266.                     let otherSvgElement = $(svg).find(`[id="${otherElementId}"]`);
  267.                     $(otherSvgElement).addClass(elementGroup.action.data.default_class);
  268.                   }
  269.                 }
  270.               }
  271.               else {
  272.                 this.instance.warn(`Cannot find '${elementId}' in SVG file`);
  273.               }
  274.             }
  275.           }
  276.  
  277.           let entityGroups = this.instance.config.groups.filter(x => x.entities);
  278.           for (let entityGroup of entityGroups) {
  279.             let targetEntityIds = [];
  280.  
  281.             // Split out HA entity groups into separate entities
  282.             if (entityGroup.groups) {
  283.               for (let entityId of entityGroup.groups) {
  284.                 let group = this.instance.hass.states[entityId];
  285.                 if (group) {
  286.                   for (let targetEntityId of group.attributes.entity_id) {
  287.                     targetEntityIds.push(targetEntityId);
  288.                   }
  289.                 }
  290.                 else {
  291.                   this.instance.warn(`Cannot find '${entityId}' in HA group configuration`);
  292.                 }
  293.               }
  294.             }
  295.  
  296.             // HA entities treated as is
  297.             if (entityGroup.entities) {
  298.               for (let entityId of entityGroup.entities) {
  299.                 let entity = this.instance.hass.states[entityId];
  300.                 if (entity) {
  301.                   targetEntityIds.push(entityId);
  302.                 }
  303.                 else {
  304.                   this.instance.warn(`Cannot find '${entityId}' in HA group configuration`);
  305.                 }
  306.               }
  307.             }
  308.  
  309.             for (let entityId of targetEntityIds) {
  310.               let entityConfig = {
  311.                 group: entityGroup,
  312.                 lastState: undefined,
  313.                 lastChangedTime: undefined,
  314.                 svgElementConfigs: {},
  315.                 imageUrl: undefined
  316.               };
  317.  
  318.               this.instance.entityConfigs[entityId] = entityConfig;
  319.  
  320.               let svgElement = svgElements.find(svgElement => svgElement.id === entityId);
  321.               if (!svgElement) {
  322.                 this.instance.warn(`Cannot find element '${entityId}' in SVG file`);
  323.                 continue;
  324.               }
  325.  
  326.               entityConfig.svgElementConfigs[svgElement.id] = {
  327.                 svgElementId: svgElement.id,
  328.                 svgElement: svgElement,
  329.                 clonedsvgElement: svgElement.cloneNode(true),
  330.                 entityId: entityId
  331.               };
  332.  
  333.               $(svgElement).find('*').each((i, svgNestedElement) => {
  334.                 // Ensure that all child elements have an Id.
  335.                 if (!svgNestedElement.id) {
  336.                   svgNestedElement.id = uniqueId++;
  337.                 }
  338.  
  339.                 entityConfig.svgElementConfigs[svgNestedElement.id] = {
  340.                   svgElementId: svgNestedElement.id,
  341.                   svgElement: svgNestedElement,
  342.                   clonedsvgElement: svgNestedElement.cloneNode(true),
  343.                   entityId: entityId
  344.                 };
  345.               });
  346.  
  347.               for (let svgElementId in entityConfig.svgElementConfigs) {
  348.                 let svgElementConfig = entityConfig.svgElementConfigs[svgElementId];
  349.  
  350.                 let svgElement = $(svgElementConfig.svgElement);
  351.  
  352.                 // Create a title element (to support hover over text)
  353.                 svgElement.append(document.createElementNS('http://www.w3.org/2000/svg', 'title'));
  354.  
  355.                 if (svgElement.length) {
  356.                   svgElementConfig.svgElement = svgElement[0];
  357.  
  358.                   $(svgElement).on('click', this.instance.onEntityClick.bind({ instance: this.instance, entityId: entityId }));
  359.                   $(svgElement).css('cursor', 'pointer');
  360.                   $(svgElement).addClass('ha-entity');
  361.  
  362.                   if ((svgElement[0].nodeName === 'text') && (svgElement[0].id === entityId)) {
  363.                    let boundingBox = svgElement[0].getBBox();
  364.                     let rect = $(document.createElementNS("http://www.w3.org/2000/svg", 'rect'))
  365.                       .attr('id', entityId + '.background')
  366.                       .attr('height', boundingBox.height + 1)
  367.                       .attr('width', boundingBox.width + 2)
  368.                       .height(boundingBox.height + 1)
  369.                       .width(boundingBox.width + 2)
  370.                       .attr('x', boundingBox.x - 1)
  371.                       .attr('y', boundingBox.y - 0.5)
  372.                       .css('fill-opacity', 0);
  373.  
  374.                     $(rect).insertBefore($(svgElement));
  375.                   }
  376.                 }
  377.               }
  378.             }
  379.           }
  380.  
  381.           // Enable pan / zoom if enabled in config
  382.           if ((this.instance.config.pan_zoom === null) || (this.instance.config.pan_zoom !== undefined)) {
  383.             svgPanZoom($(svg)[0], {
  384.               zoomEnabled: true,
  385.               controlIconsEnabled: true,
  386.               fit: true,
  387.               center: true,
  388.             });
  389.           }
  390.  
  391.           this.callback();
  392.  
  393.         }.bind({ instance: this, callback: callback })
  394.       });
  395.     }
  396.  
  397.     handleEntities(entities) {
  398.       let svg = this.$.floorplan.querySelector('svg');
  399.  
  400.       for (let entityId in entities) {
  401.         let entityState = entities[entityId];
  402.  
  403.         let entityConfig = this.entityConfigs[entityId];
  404.         if (!entityConfig)
  405.           continue;
  406.  
  407.         entityConfig.lastState = entityState.state;
  408.  
  409.         for (let svgElementId in entityConfig.svgElementConfigs) {
  410.           let svgElementConfig = entityConfig.svgElementConfigs[svgElementId];
  411.           let svgElement = svgElementConfig.svgElement;
  412.  
  413.           if (!svgElement)
  414.             continue;
  415.  
  416.           this.setHoverOverText(svgElement, entityState);
  417.  
  418.           if (svgElement.nodeName === 'text') {
  419.             let text = entityConfig.group.text_template ?
  420.               this.assemble(entityConfig.group.text_template, entityState, entities) : entityState.state;
  421.  
  422.             let tspan = $(svgElement).find('tspan');
  423.             if (tspan.length) {
  424.               $(tspan).text(text);
  425.             }
  426.             else {
  427.               let title = $(svgElement).find('title');
  428.               $(svgElement).text(text);
  429.               if (title.length) {
  430.                 $(svgElement).append(title);
  431.               }
  432.             }
  433.  
  434.             let rect = $(svgElement).parent().find(`[id="${entityId}.background"]`);
  435.             if (rect.length) {
  436.               let boundingBox = svgElement.getBBox();
  437.               $(rect)
  438.                 .attr("x", boundingBox.x - 1)
  439.                 .attr("y", boundingBox.y - 0.5)
  440.                 .attr('height', boundingBox.height + 1)
  441.                 .attr('width', boundingBox.width + 2)
  442.                 .height(boundingBox.height + 1)
  443.                 .width(boundingBox.width + 2);
  444.             }
  445.           }
  446.  
  447.           if (!this.cssRules || !this.cssRules.length)
  448.             return;
  449.  
  450.           let wasTransitionHandled = false;
  451.  
  452.           if (entityConfig.group.states && entityConfig.group.state_transitions) {
  453.            let transitionConfig = entityConfig.group.state_transitions.find(transitionConfig => (transitionConfig.to_state === entityState.state));
  454.             if (transitionConfig && transitionConfig.from_state && transitionConfig.to_state && transitionConfig.duration) {
  455.              // Determine the current time on the server (based on the local vs. server time difference)
  456.              let serverMoment = this.getServerMoment();
  457.               let lastChangedMoment = moment(entityState.last_changed);
  458.               let elapsed = Math.max(serverMoment.diff(lastChangedMoment, 'milliseconds'), 0);
  459.               let remaining = (transitionConfig.duration * 1000) - elapsed;
  460.  
  461.               if (remaining > 0) {
  462.                 entityConfig.lastChangedTime = lastChangedMoment.toDate();
  463.               }
  464.               else {
  465.                 this.setEntityStyle(svgElementConfig, svgElement, entityConfig);
  466.               }
  467.               wasTransitionHandled = true;
  468.             }
  469.           }
  470.  
  471.           if (entityConfig.group.image_template) {
  472.             let imageUrl = this.assemble(entityConfig.group.image_template, entityState, entities);
  473.             if (entityConfig.imageUrl !== imageUrl) {
  474.               entityConfig.imageUrl = imageUrl;
  475.               this.loadImage(imageUrl, entityId, entityState, (embeddedSvg, entityState) => {
  476.                 this.setHoverOverText(embeddedSvg, entityState);
  477.               });
  478.             }
  479.  
  480.             let embeddedSvg = $(svg).find(`[id="image.${entityId}"]`)[0];
  481.             this.setHoverOverText(embeddedSvg, entityState);
  482.           }
  483.  
  484.           let targetClass = undefined;
  485.           let obsoleteClasses = [];
  486.  
  487.           if (entityConfig.group.class_template) {
  488.             targetClass = this.assemble(entityConfig.group.class_template, entityState, entities);
  489.           }
  490.  
  491.           let originalClasses = this.getArray(svgElementConfig.clonedsvgElement.classList);
  492.  
  493.           // Get the config for the current state
  494.           if (entityConfig.group.states) {
  495.             let stateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === entityState.state));
  496.             if (stateConfig && stateConfig.class && !wasTransitionHandled) {
  497.              targetClass = stateConfig.class;
  498.             }
  499.  
  500.             // Remove any other previously-added state classes
  501.             for (let otherStateConfig of entityConfig.group.states) {
  502.               if (!stateConfig || (otherStateConfig.state != stateConfig.state)) {
  503.                 if (otherStateConfig.class && (otherStateConfig.class != 'ha-entity') && $(svgElement).hasClass(otherStateConfig.class)) {
  504.                  if (originalClasses.indexOf(otherStateConfig.class) < 0) {
  505.                    obsoleteClasses.push(otherStateConfig.class);
  506.                   }
  507.                 }
  508.               }
  509.             }
  510.           }
  511.           else {
  512.             for (let otherClassName of this.getArray(svgElement.classList)) {
  513.               if ((otherClassName != targetClass) && (otherClassName != 'ha-entity')) {
  514.                if (originalClasses.indexOf(otherClassName) < 0) {
  515.                  obsoleteClasses.push(otherClassName);
  516.                 }
  517.               }
  518.             }
  519.           }
  520.  
  521.           // Remove any obsolete classes from the entity
  522.           this.removeClasses(entityId, svgElement, obsoleteClasses);
  523.  
  524.           // Add the target class to the entity
  525.           if (targetClass) {
  526.             this.addClass(entityId, svgElement, targetClass);
  527.           }
  528.  
  529.           if (this.config.last_motion_entity && this.config.last_motion_class && entities[this.config.last_motion_entity] &&
  530.            (entityState.attributes.friendly_name === entities[this.config.last_motion_entity].state)) {
  531.            if (!$(svgElement).hasClass(this.config.last_motion_class)) {
  532.              $(svgElement).addClass(this.config.last_motion_class);
  533.             }
  534.           }
  535.           else {
  536.             if ($(svgElement).hasClass(this.config.last_motion_class)) {
  537.               $(svgElement).removeClass(this.config.last_motion_class);
  538.             }
  539.           }
  540.         }
  541.       }
  542.     }
  543.  
  544.     setHoverOverText(element, entityState) {
  545.       let title = $(element).find('title');
  546.       if (title.length) {
  547.         let dateFormat = this.config.date_format ? this.config.date_format : 'DD-MMM-YYYY';
  548.         let titleText = entityState.attributes.friendly_name + '\n' +
  549.           'State: ' + entityState.state + '\n' +
  550.           'Last changed date: ' + moment(entityState.last_changed).format(dateFormat) + '\n' +
  551.           'Last changed time: ' + moment(entityState.last_changed).format('HH:mm:ss');
  552.  
  553.         $(title).html(titleText);
  554.       }
  555.     }
  556.  
  557.     loadImage(imageUrl, entityId, entityState, callback) {
  558.       let svg = this.$.floorplan.querySelector('svg');
  559.       jQuery.ajax({
  560.         url: imageUrl, // allow the browser cache to be used
  561.         success: function (result) {
  562.           let svgElement = $(svg).find(`[id="${entityId}"]`);
  563.           let bbox = svgElement[0].getBBox();
  564.           let clientRect = svgElement[0].getBoundingClientRect();
  565.  
  566.           let embeddedSvg = $(result).find('svg');
  567.  
  568.           embeddedSvg.attr('id', `image.${entityId}`);
  569.           embeddedSvg.attr('preserveAspectRatio', 'xMinYMin meet')
  570.           embeddedSvg
  571.             .attr('height', bbox.height)
  572.             .attr('width', bbox.width)
  573.             .attr('x', bbox.x)
  574.             .attr('y', bbox.y);
  575.  
  576.           $(embeddedSvg).find('*').append(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
  577.             .on('click', this.onEntityClick.bind({ instance: this, entityId: entityId }))
  578.             .css('cursor', 'pointer')
  579.             .addClass('ha-entity');
  580.  
  581.           // Remove previous SVG
  582.           let previousEmbeddedSvg = $(svg).find(`[id="${embeddedSvg.attr('id')}"]`);
  583.           $(previousEmbeddedSvg).find('*')
  584.             .off('click')
  585.             .remove();
  586.  
  587.           $(svg).append(embeddedSvg);
  588.  
  589.           callback(embeddedSvg, entityState);
  590.  
  591.         }.bind(this)
  592.       });
  593.     }
  594.  
  595.     addClass(entityId, svgElement, className) {
  596.       if ($(svgElement).hasClass('ha-leave-me-alone')) {
  597.         return;
  598.       }
  599.  
  600.       if (!$(svgElement).hasClass(className)) {
  601.         //console.log(`${entityId}: adding class "${className}" for current state "${entityState.state}" (${svgElement.id})`);
  602.         $(svgElement).addClass(className);
  603.  
  604.         if ((svgElement.nodeName === 'text')) {
  605.           let rect = $(svgElement).parent().find(`[id="${entityId}.background"]`);
  606.           if (rect.length) {
  607.             if (!$(rect).hasClass(className + '-background')) {
  608.               $(rect).addClass(className + '-background');
  609.             }
  610.           }
  611.         }
  612.       }
  613.     }
  614.  
  615.     removeClasses(entityId, svgElement, classes) {
  616.       for (let className of classes) {
  617.         //console.log(`${entityId}: removing class "${className}" (${svgElement.id})`);
  618.         if ($(svgElement).hasClass(className)) {
  619.           $(svgElement).removeClass(className);
  620.  
  621.           if ((svgElement.nodeName === 'text')) {
  622.             let rect = $(svgElement).parent().find(`[id="${entityId}.background"]`);
  623.             if (rect.length) {
  624.               if ($(rect).hasClass(className + '-background')) {
  625.                 $(rect).removeClass(className + '-background');
  626.               }
  627.             }
  628.           }
  629.         }
  630.       }
  631.     }
  632.  
  633.     updateStateTransitions() {
  634.       if (!this.cssRules || !this.cssRules.length)
  635.         return;
  636.  
  637.       let svg = this.$.floorplan.querySelector('svg');
  638.  
  639.       for (let entityId in this.entityConfigs) {
  640.         let entityConfig = this.entityConfigs[entityId];
  641.  
  642.         if (!entityConfig || !entityConfig.group.states || !entityConfig.group.state_transitions || (entityConfig.lastChangedTime === undefined))
  643.           continue;
  644.  
  645.         for (let svgElementId in entityConfig.svgElementConfigs) {
  646.           let svgElementConfig = entityConfig.svgElementConfigs[svgElementId];
  647.           let svgElement = svgElementConfig.svgElement;
  648.  
  649.           if (!svgElement)
  650.             continue;
  651.  
  652.           let wasTransitionHandled = false;
  653.  
  654.           let transitionConfig = entityConfig.group.state_transitions.find(transitionConfig => (transitionConfig.to_state === entityConfig.lastState));
  655.           if (transitionConfig && transitionConfig.from_state && transitionConfig.to_state && transitionConfig.duration) {
  656.            let serverMoment = this.getServerMoment();
  657.             let fromStateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === transitionConfig.from_state));
  658.             let toStateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === transitionConfig.to_state));
  659.  
  660.             if (fromStateConfig && toStateConfig) {
  661.              let fromFill = this.getFill(fromStateConfig);
  662.               let toFill = this.getFill(toStateConfig);
  663.  
  664.               if (fromFill && toFill) {
  665.                let elapsed = serverMoment.diff(moment(entityConfig.lastChangedTime), 'milliseconds');
  666.                 if (elapsed < 0) {
  667.                  this.setTransitionFill(svgElement, fromFill, toFill, 1);
  668.                }
  669.                else {
  670.                  if (elapsed < (transitionConfig.duration * 1000)) {
  671.                    this.setTransitionFill(svgElement, fromFill, toFill, elapsed / (transitionConfig.duration * 1000));
  672.                  }
  673.                  else {
  674.                    this.setTransitionFill(svgElement, fromFill, toFill, 0);
  675.                    entityConfig.lastChangedTime = undefined;
  676.                  }
  677.                }
  678.  
  679.                wasTransitionHandled = true;
  680.              }
  681.            }
  682.          }
  683.  
  684.          if (!wasTransitionHandled) {
  685.            this.setEntityStyle(svgElementConfig, svgElement, entityConfig);
  686.          }
  687.        }
  688.      }
  689.    }
  690.  
  691.    setEntityStyle(svgElementConfig, svgElement, entityConfig, state) {
  692.      let stateConfig = entityConfig.group.states.find(stateConfig => (stateConfig.state === entityConfig.lastState));
  693.       if (stateConfig) {
  694.         let stroke = this.getStroke(stateConfig);
  695.         if (stroke) {
  696.           svgElement.style.stroke = stroke;
  697.         }
  698.         else {
  699.           if (svgElementConfig.clonedsvgElement) {
  700.             svgElement.style.stroke = svgElementConfig.clonedsvgElement.style.stroke;
  701.           }
  702.           else {
  703.             // ???
  704.           }
  705.         }
  706.  
  707.         let fill = this.getFill(stateConfig);
  708.         if (fill) {
  709.           svgElement.style.fill = fill;
  710.         }
  711.         else {
  712.           if (svgElementConfig.clonedsvgElement) {
  713.             svgElement.style.fill = svgElementConfig.clonedsvgElement.style.fill;
  714.           }
  715.           else {
  716.             // ???
  717.           }
  718.         }
  719.       }
  720.     }
  721.  
  722.     onElementClick(e) {
  723.       e.stopPropagation();
  724.  
  725.       let svgElement = e.target;
  726.  
  727.       let elementConfig = this.instance.elementConfigs[this.elementId];
  728.       if (elementConfig.group.action) {
  729.         let action = elementConfig.group.action;
  730.         if (action.service) {
  731.           switch (action.domain) {
  732.             case 'class':
  733.  
  734.               switch (action.service) {
  735.                 case 'toggle':
  736.                   let svg = this.instance.$.floorplan.querySelector('svg');
  737.                   let classes = action.data.classes;
  738.  
  739.                   for (let otherElementId of action.data.elements) {
  740.                     let otherSvgElement = $(svg).find(`[id="${otherElementId}"]`);
  741.  
  742.                     if ($(otherSvgElement).hasClass(classes[0])) {
  743.                       $(otherSvgElement).removeClass(classes[0]);
  744.                       $(otherSvgElement).addClass(classes[1]);
  745.                     }
  746.                     else if ($(otherSvgElement).hasClass(classes[1])) {
  747.                       $(otherSvgElement).removeClass(classes[1]);
  748.                       $(otherSvgElement).addClass(classes[0]);
  749.                     }
  750.                     else {
  751.                       $(otherSvgElement).addClass(action.data.default_class);
  752.                     }
  753.                   }
  754.                   break;
  755.               }
  756.               break;
  757.  
  758.             default:
  759.               domain = action.domain
  760.               let data = action.data ? action.data : {};
  761.               if (action.data_template) {
  762.                 let entities = this.instance.hass.states;
  763.                 let entityState = entities[entityId];
  764.                 let result = this.instance.assemble(action.data_template, entityState, entities);
  765.                 data = JSON.parse(result);
  766.               }
  767.               this.instance.hass.callService(domain, action.service, data);
  768.               break;
  769.           }
  770.         }
  771.       }
  772.     }
  773.  
  774.     onEntityClick(e) {
  775.       e.stopPropagation();
  776.  
  777.       let entityId = this.entityId;
  778.  
  779.       let entityConfig = this.instance.entityConfigs[entityId];
  780.       if (entityConfig.group.action) {
  781.         let action = entityConfig.group.action;
  782.         if (action.service) {
  783.           let domain = action.domain ? action.domain : entityId.substr(0, entityId.indexOf('.'));
  784.           domain = (domain == 'group') ? 'homeassistant' : domain;
  785.  
  786.           let data = {};
  787.           if (action.data) {
  788.             data = action.data;
  789.           }
  790.           if (action.data_template) {
  791.             let entities = this.instance.hass.states;
  792.             let entityState = entities[entityId];
  793.  
  794.             let result = this.instance.assemble(action.data_template, entityState, entities);
  795.             data = JSON.parse(result);
  796.           }
  797.  
  798.           if (!data.entity_id) {
  799.             data['entity_id'] = entityId;
  800.           }
  801.  
  802.           this.instance.hass.callService(domain, action.service, data);
  803.         }
  804.         else {
  805.           this.instance.fire('hass-more-info', { entityId: entityId });
  806.         }
  807.       }
  808.       else {
  809.         this.instance.fire('hass-more-info', { entityId: entityId });
  810.       }
  811.     }
  812.  
  813.     getFill(stateConfig) {
  814.       let fill = undefined;
  815.  
  816.       for (let cssRule of this.cssRules) {
  817.         if (cssRule.selectorText && cssRule.selectorText.indexOf(`.${stateConfig.class}`) >= 0) {
  818.          if (cssRule.style && cssRule.style.fill) {
  819.            if (cssRule.style.fill[0] === '#') {
  820.              fill = cssRule.style.fill;
  821.             }
  822.             else {
  823.               let rgb = cssRule.style.fill.substring(4).slice(0, -1).split(',').map(x => parseInt(x));
  824.               fill = `#${rgb[0].toString(16)[0]}${rgb[1].toString(16)[0]}${rgb[2].toString(16)[0]}`;
  825.             }
  826.           }
  827.         }
  828.       }
  829.  
  830.       return fill;
  831.     }
  832.  
  833.     getStroke(stateConfig) {
  834.       let stroke = undefined;
  835.  
  836.       for (let cssRule of this.cssRules) {
  837.         if (cssRule.selectorText && cssRule.selectorText.indexOf(`.${stateConfig.class}`) >= 0) {
  838.          if (cssRule.style && cssRule.style.stroke) {
  839.            if (cssRule.style.stroke[0] === '#') {
  840.              stroke = cssRule.style.stroke;
  841.             }
  842.             else {
  843.               let rgb = cssRule.style.stroke.substring(4).slice(0, -1).split(',').map(x => parseInt(x));
  844.               stroke = `#${rgb[0].toString(16)[0]}${rgb[1].toString(16)[0]}${rgb[2].toString(16)[0]}`;
  845.             }
  846.           }
  847.         }
  848.       }
  849.  
  850.       return stroke;
  851.     }
  852.  
  853.     setTransitionFill(svgElement, fromFill, toFill, value) {
  854.       if (value >= 1) {
  855.         svgElement.style.fill = fromFill;
  856.       }
  857.       else if (value <= 0) {
  858.        svgElement.style.fill = toFill;
  859.      }
  860.      else {
  861.        let color = this.rgbToHex(this.mix(this.hexToRgb(toFill), this.hexToRgb(fromFill), value));
  862.        svgElement.style.fill = color;
  863.      }
  864.    }
  865.  
  866.    getServerMoment() {
  867.      let serverMoment = moment();
  868.      if (this.timeDifference >= 0)
  869.         serverMoment.subtract(this.timeDifference, 'milliseconds');
  870.       else
  871.         serverMoment.add(Math.abs(this.timeDifference), 'milliseconds');
  872.       return serverMoment;
  873.     }
  874.  
  875.     getArray(list) {
  876.       return Array.isArray(list) ? list : Object.keys(list).map(key => list[key]);
  877.     }
  878.  
  879.     assemble(code, entity, entities) {
  880.       let functionBody = (code.indexOf('return') >= 0) ? code : `return \`${code}\`;`;
  881.       let func = new Function('entity', 'entities', 'hass', 'config', functionBody);
  882.       return func(entity, entities, this.hass, this.config);
  883.     }
  884.  
  885.     error(message) {
  886.       let errors = this.$.errors;
  887.       $(errors).find('ul').append(`<li>${message}</li>`)
  888.       $(errors).css('display', 'block');
  889.     }
  890.  
  891.     warn(message) {
  892.       if ((this.config.warnings === null) || (this.config.warnings !== undefined)) {
  893.         let warnings = this.$.warnings;
  894.         $(warnings).find('ul').append(`<li>${message}</li>`)
  895.         $(warnings).css('display', 'block');
  896.       }
  897.     }
  898.  
  899.     debug(message) {
  900.       let debug = this.$.debug;
  901.       $(debug).find('ul').append(`<li>${message}</li>`)
  902.       $(debug).css('display', 'block');
  903.     }
  904.  
  905.     loadStyleSheet(path, fn, scope) {
  906.       let head = document.getElementsByTagName('head')[0]; // reference to document.head for appending/ removing link nodes
  907.       let link = document.createElement('link');           // create the link node
  908.       link.setAttribute('href', path);
  909.       link.setAttribute('rel', 'stylesheet');
  910.       link.setAttribute('type', 'text/css');
  911.  
  912.       let sheet, cssRules;
  913.       // get the correct properties to check for depending on the browser
  914.       if ('sheet' in link) {
  915.         sheet = 'sheet'; cssRules = 'cssRules';
  916.       }
  917.       else {
  918.         sheet = 'styleSheet'; cssRules = 'rules';
  919.       }
  920.  
  921.       let interval_id = setInterval(function () {                     // start checking whether the style sheet has successfully loaded
  922.         try {
  923.           if (link[sheet] && link[sheet][cssRules].length) { // SUCCESS! our style sheet has loaded
  924.            clearInterval(interval_id);                      // clear the counters
  925.             clearTimeout(timeout_id);
  926.             fn.call(scope || window, true, link);           // fire the callback with success == true
  927.           }
  928.         } catch (e) { } finally { }
  929.       }, 10),                                                   // how often to check if the stylesheet is loaded
  930.         timeout_id = setTimeout(function () {       // start counting down till fail
  931.           clearInterval(interval_id);             // clear the counters
  932.           clearTimeout(timeout_id);
  933.           head.removeChild(link);                // since the style sheet didn't load, remove the link node from the DOM
  934.           fn.call(scope || window, false, link); // fire the callback with success == false
  935.         }, 15000);                                 // how long to wait before failing
  936.  
  937.       head.appendChild(link);  // insert the link node into the DOM and start loading the style sheet
  938.  
  939.       return link; // return the link node;
  940.     }
  941.  
  942.     rgbToHex(rgb) {
  943.       return "#" + ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1);
  944.    }
  945.  
  946.    hexToRgb(hex) {
  947.      // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
  948.      let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  949.      hex = hex.replace(shorthandRegex, (m, r, g, b) => {
  950.         return r + r + g + g + b + b;
  951.       });
  952.  
  953.       let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  954.       return result ? {
  955.         r: parseInt(result[1], 16),
  956.         g: parseInt(result[2], 16),
  957.         b: parseInt(result[3], 16)
  958.       } : null;
  959.     }
  960.  
  961.     mix(color1, color2, weight) {
  962.       let p = weight;
  963.       let w = p * 2 - 1;
  964.       let w1 = ((w / 1) + 1) / 2;
  965.       let w2 = 1 - w1;
  966.       let rgb = [
  967.         Math.round(color1.r * w1 + color2.r * w2),
  968.         Math.round(color1.g * w1 + color2.g * w2),
  969.         Math.round(color1.b * w1 + color2.b * w2)
  970.       ];
  971.       return rgb;
  972.     }
  973.  
  974.     fire(type, detail, options) {
  975.       options = options || {};
  976.       detail = (detail === null || detail === undefined) ? {} : detail;
  977.       const event = new Event(type, {
  978.         bubbles: options.bubbles === undefined ? true : options.bubbles,
  979.         cancelable: Boolean(options.cancelable),
  980.         composed: options.composed === undefined ? true : options.composed
  981.       });
  982.       event.detail = detail;
  983.       const node = options.node || this;
  984.       node.dispatchEvent(event);
  985.       return event;
  986.     }
  987.   }
  988.   customElements.define(HaFloorplan.is, HaFloorplan);
  989.  
  990. </script>
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement