Hi there,
Not sure how useful it might be to others, but I have created a custom house plan panel that shows which zones are currently active or not (i.e. based on the binary sensors).
First of all, I created our house plan as an SVG file, and named each zone (i.e. shape) such that the name exactly matches the zone’s entity ID in Home Assistant (i.e. binary_sensor.back_hallway). I then saved this SVG file to the www folder within HA. I then created the HTML file for the custom panel, and saved it to the panels folder within HA.
To get the custom panel into the HA web interface, I added the following to configuration.yaml:
panel_custom:
- name: zones
sidebar_title: Zones
sidebar_icon: mdi:hand-pointing-right
url_path: zones
After restarting HA, the Zones link appeared in the left menu of the HA interface.
It would be good to have this functionality embedded into a widget so that it could appear within the HA views, but I haven’t gotten that far yet. Also, it would be good to make it more generic so that the SVG image could be used to represent different kinds of components, other than just binary sensors.
BTW, below are the HTML and SVG file I used.
panels/zones.html
<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
crossorigin="anonymous"></script>
<dom-module id="ha-panel-zones">
<template>
<style include="ha-style">
.content {
padding: 16px;
}
paper-input {
max-width: 200px;
}
[hidden] {
display: none !important;
}
.svg-container {
display: inline-block;
position: relative;
max-width: 800px;
width: 100%;
vertical-align: middle;
overflow: hidden;
margin: 10px;
}
.svg-content {
display: inline-block;
position: absolute;
top: 0;
left: 0;
}
#floorplan svg path,
#floorplan svg rect {
stroke: #646464;
stroke-width: 0.02px;
}
</style>
<app-header-layout has-scrolling-region>
<app-header fixed>
<app-toolbar>
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
<div main-title>Zones</div>
</app-toolbar>
</app-header>
<div class="flex content">
<ha-zones hass="[[hass]]" entries="[[entries]]" hidden$="[[isLoading]]">
</ha-zones>
<div id="floorplan" class="svg-container"></div>
</div>
</app-header-layout>
</template>
</dom-module>
<script>
Polymer({
is: 'ha-panel-zones',
properties: {
hass: {
type: Object,
},
narrow: {
type: Boolean,
value: false,
},
showMenu: {
type: Boolean,
value: false,
},
isLoading: {
type: Boolean,
},
entries: {
type: Array,
},
},
attached: function () {
},
ready: function () {
var wsUri = ((window.location.protocol === 'https:') ? 'wss:' : 'ws:') + '//' + window.location.host + '/api/websocket';
HAWS.createConnection(wsUri, { authToken: 'HA_PASSWORD_HERE' }).then(conn => {
HAWS.subscribeEntities(conn, entities => this.handleEntities(entities));
}, err => { console.log(err); });
this.loadFloorPlan();
},
detached: function () {
},
handleEntities(entities) {
this.displayZones(entities);
},
displayZones(entities) {
var groups = HAWS.splitByGroups(entities).groups;
var zoneGroup;
for (var group of groups) {
if (group.entity_id === 'group.zones') {
zoneGroup = group;
break;
}
}
var zones = [];
if (zoneGroup) {
for (var i = 0; i < zoneGroup.attributes.entity_id.length; i++) {
var zoneId = zoneGroup.attributes.entity_id[i];
zones.push(entities[zoneId]);
}
}
for (zone of zones) {
if (zone.state.toLowerCase() === 'on') {
console.log(zone.entity_id, '=', zone.state);
}
var svg = $('#floorplan svg');
if (!svg[0]) {
svg = $(this.shadowRoot.querySelector('#floorplan svg'));
}
var shape = svg.find('[id="' + zone.entity_id + '"]');
if (shape) {
shape.css('fill', (zone.state.toLowerCase() === 'on') ? '#F8B9BE' : '#C4EDB1');
}
else {
console.error('Could not find SVG shape element for', zone.entity_id);
}
}
},
loadFloorPlan() {
$.ajax({
url: '/local/floorplan.svg',
success: function (result) {
svg = $(result).find('svg');
var floorplan = $('#floorplan');
if (!floorplan[0]) {
floorplan = $(this.shadowRoot.querySelector('#floorplan'));
}
floorplan.hide();
floorplan.append(svg);
svg.height('100%');
svg.width('100%');
svg.addClass('svg-coantent');
svg.find('rect').css('fill', '#e0e0e0');
svg.find('path').css('fill', '#e0e0e0');
floorplan.show();
}.bind(this)
});
},
});
</script>
www/floorplan.svg
<?xml version="1.0" standalone="no"?>
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="119.000000pt" height="100.000000pt" viewBox="0 0 119.000000 100.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,100.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none">
<path id="background" d="M790 780 l0 -170 -160 0 -160 0 0 -70 0 -70 -110 0 -110 0 0 70 0 70
-105 0 -105 0 0 -285 0 -285 500 0 500 0 0 75 0 75 50 0 50 0 0 80 0 80 -39 0
c-31 0 -41 4 -46 20 -9 30 4 60 26 60 18 0 19 10 19 260 l0 260 -155 0 -155 0
0 -170z"/>
<path id="binary_sensor.back_hallway" d="M190 200 c0 -19 7 -20 155 -20 148 0 155 1 155 20 0 19 -7 20 -155
20 -148 0 -155 -1 -155 -20z"/>
<path id="binary_sensor.front_hallway" d="M650 396 c0 -30 4 -34 31 -40 17 -3 107 -6 200 -6 l169 0 0 40 0 40
-200 0 -200 0 0 -34z"/>
<path id="binary_sensor.kitchen" d="M250 383 c0 -130 -6 -123 126 -123 95 0 114 -3 114 -15 0 -12 15 -15
75 -15 l75 0 0 100 0 100 -80 0 c-64 0 -80 3 -80 15 0 12 -19 15 -115 15
l-115 0 0 -77z"/>
<path id="binary_sensor.master_bedroom" d="M826 248 c-3 -50 -6 -115 -6 -145 l0 -53 105 0 105 0 0 80 c0 79 0
80 -25 80 -24 0 -25 2 -25 65 l0 65 -74 0 -73 0 -7 -92z"/>
<path d="M792 823 l2 -128 3 123 4 122 144 0 145 0 0 -250 0 -250 -107 -2
c-106 -2 -107 -2 -23 -5 51 -3 87 -9 92 -16 6 -9 8 -9 8 1 0 6 9 12 20 12 19
0 20 7 20 260 l0 260 -155 0 -155 0 2 -127z"/>
<path id="binary_sensor.theatre_room" d="M50 490 l0 -110 95 0 95 0 0 110 0 110 -95 0 -95 0 0 -110z"/>
</g>
</svg>