Thanks for the warm welcome! I hope I can be of benefit to the community and likewise also learn from it.
In that spirit, as there is interest in how to use a modern UI for home automation front ends I'll use this post to explain how to write a widget based HTML5 UI. As an example, lets look at one of my favorite widgets from my system, the dial widget (you can see it prominent in the screenshots in earlier posts and below). Firstly to talk about HTML5 as a front end platform, HTML is by far the predominant platform for user interfaces, by order of magnitudes, as the web is based on it and there are so many toolsets, libraries and tutorials it is an obvious choice for a HA UI. Before HTML5 and pre 2009 (roughly) it was quite difficult to do rich UI applications as HTML wasn't functional enough, there were other popular UI frameworks like flash and silverlight, JavaScript was slow and differing browser compatibility was a serious problem. Those days are gone now, you can implement almost anything with HTML5 function and browsers (even IE) take standards compatibility much more seriously (although not perfect) and JavaScript speed is vastly improved although not as fast as native code but for 99% of applications there is no practical difference.
To use HTML as a rich UI requires a number of different toolsets - the HTML provides the layout description for the page, CSS provides the style (eg. color, animation), SVG provides vector graphics support if needed and javascript programs the CSS/HTML entities and applies logic to make the page smart. So it isn't the easiest to learn especially when using as a UI for applications as it roots are in page layout & markup (displaying web pages) however its ubiquity helps as there is so much support for HTML (eg. helper tools like jQuery) that make it easier to build web pages. The web community is developing UI frameworks like AngularJS which make it easier to build applications by using design patterns like MVVM to connect a dynamic HTML5 front end with the back end. The details of MVC/MVVM design is out of scope for this post, however if you are interested in building your own HTML5 UI I do suggest you take a closer look at angular or similar UI web framework like jQuery mobile as it will help you get started (even though I chose not to use a web framework as I wanted access to the full power of the browser without any of the constraints a framework will bring).
OK, back to the widget. The dashboard and graphical designer I have implemented will grab all the HTML widgets in the widgets directory and install them as objects dynamically on loading inline into the dashboard HTML. Here is the appropriate code from the dashboard (it adds the widget as an object to the widgets toolbox so the user can drag the widget to the design surface when selecting a new widget when laying out a dashboard screen).
var objWidget = document.createElement("object");
objWidget.type = "text/html";
objWidget.data = "widgets/" + widgetTemplates[widgetNum] // location of widget
var title = document.createElement("p")
title.innerHTML = "<span>" + widgetNameType[0] + "</span><br /><br />"
TBContainer.appendChild(objWidget); // Add to toolbox Div
Here is the HTML code for the widget. Although the widget is fully formed HTML, CSS and JavaScript it isn't meant to be used standalone as a web page.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Dial Widget</title>
</head>
<body id="body">
<style>
body {
overflow: hidden;
}
</style>
<span id="TBtooltip" data-default="Displays current and average channel values" />
<span id="attrib0" data-type="channel" data-name="Source" data-default="" />
<span id="attrib1" data-type="channel" data-name="Average" data-default="" />
<span id="attrib2" data-type="input" data-name="Range" data-default="100" />
<span id="ontop" data-default="true" />
<div id="group">
<svg id="widget" width="100" height="80" style="position: absolute; left: 0px; top: 0px; z-index:4">
<style type="text/css">
text {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-weight: normal;
font-style: normal;
font-size: 20px;
text-align: center;
pointer-events: none;
}
</style>
<g id="svgGroup" style="position: absolute; left: 0px; top: 0px;">
<g id=" noScale">
</g>
<g id="scale">
<path id="seg1" fill="none" stroke="rgb(0, 134, 0)" d="M13.46,66.27 A40,40,0,0,1,10.1,52.79" stroke-width="20" />
<path id="seg2" fill="none" stroke="rgb(50, 134, 0)" d="M10.01,50.7 A40,40,0,0,1,12.18,36.98" stroke-width="20" />
<path id="seg3" fill="none" stroke="rgb(100, 134, 0)" d="M12.91,35.02 A40,40,0,0,1,20.27,23.23" stroke-width="20" />
<path id="seg4" fill="none" stroke="rgb(150, 134, 0)" d="M21.72,21.72 A40,40,0,0,1,33.1,13.75" stroke-width="20" />
<path id="seg5" fill="none" stroke="rgb(200, 134, 0)" d="M35.02,12.91 A40,40,0,0,1,48.6,10.02" stroke-width="20" />
<path id="seg6" fill="none" stroke="rgb(255, 134, 0)" d="M50.7,10.01 A40,40,0,0,1,64.33,12.66" stroke-width="20" />
<path id="seg7" fill="none" stroke="rgb(255, 100, 0)" d="M66.27,13.46 A40,40,0,0,1,77.79,21.23" stroke-width="20" />
<path id="seg8" fill="none" stroke="rgb(255, 70, 0)" d="M79.25,22.72 A40,40,0,0,1,86.82,34.37" stroke-width="20" />
<path id="seg9" fill="none" stroke="rgb(255, 35, 0)" d="M87.59,36.32 A40,40,0,0,1,90,50" stroke-width="20" />
<path id="seg10" fill="none" stroke="rgb(255, 0, 0)" d="M89.95,52.09 A40,40,0,0,1,86.82,65.63" stroke-width="20" />
<polyline id="avg" points="44,0 56,0 50,8 44,0" fill="rgb(20, 20, 230)" stroke-width="0" style="display: none" transform="rotate(-112, 50, 50)"><title id="avgtool">Average: 0</title></polyline>
</g>
</g>
</svg>
<svg id="needle" style="position: absolute; left: 0px; top: 0px; transform-Origin: 50% 62.5%; z-index:2">
<path id="svgNeedle" fill="rgb(100, 100, 100)" stroke="rgb(255, 255, 255)" stroke-width="1" d="M24.39,54.51 A26,26,0,1,1,27.6,63 l-20,4 Z" />
</svg>
<svg id="text" style="position: absolute; left: 0px; top: 0px; z-index: 3">
<text id="numVal" x="36" y="58" fill="rgb(255, 255, 255)">0.0</text>
</svg>
</div>
<script src="../widgetFramework.js"></script>
<script> ......... WIDGET JAVASCRIPT GOES HERE</script>
</body>
</html>
What you see here is:
- <span> entries are not exposed in the UI and are used to describe the widget semantics to the UI framework, in a similar way to the ini files describe the semantics of a device driver that I posted about earlier. In this case the first span holds the text for the tooltip popup when a mouse hovers over the widget (managed by the dashboard using a bootstrap function). The second and the third spans describe the two automation channels that this widget subscribes to (the framework uses a publish subscribe model for events), here the channel that the dial needle responds to (eg. the instantaneous value), and a secondary indicator that spins around the edge of the dial (eg. to represent an average value). The last two span descriptors save the settings for the dial range (eg. 0 - 100 for scaling the dial value) and if the widget should be in the foreground or background when rendered (eg. a container widget should be in the background). The UI framework will expose these settings when you right click the widget when in design mode, and the behavior is defined by the 'data-type' (eg. a channel list so that the user can select a channel as a feed from the server, or an input to prompt the user to enter a value). These settings are stored in a database and the UI framework will customise the widget based on the settings when loading at startup. This is a very powerful approach as you can easily describe a rich set of widget specific semantics to the UI framework as well as having the HTML/CSS/Javascript bring the widget alive.
- The rest of the HTML is embedded SVG which is a vector graphics language used by modern browsers to draw complicated shapes. The first section describes the font for the number display in the middle of the dial. The second bit with the 'path' commands draws the outer segments of the dial, the inner needle, outer secondary indicator and center. Any vector graphics program can be used to do the drawing and you just save the drawing in SVG format and cut/paste the relevant SVG commands into the widget template. I use the open source inkscape but you can also use sophisticated tools like adobe Illustrator for complex drawings or SVG-edit for simple drawings. I like SVG as you can create visually pleasing and sophisticated widgets, but you could also use the HTML5 canvas commands for simpler graphics without the added complexity of SVG.
- The final script tag brings in some of the common UI framework functionality and makes it easy for the widget to communicate with the framework through a separate javascript file that all widgets share.
Here is the JavaScript that makes the widget come alive and interact with the user and the automation framework:
var needleID = document.getElementById("needle"); // id of the path to rotate
var svgNeedle = document.getElementById("svgNeedle"); // id of the path to rotate
var svgText = document.getElementById("text"); // id of text SVG
var avgID = document.getElementById("avg"); // id average marker
var oldVal = 0;
// Called from framework when widget starts
function widgetStart(param) { // widget specific startup
range = parseInt(_attribs[2].value);
if (_attribs[1].value !== "") avgID.style.setProperty("display", "inline");
// Hide the svg used to display the widget in the toolbox for proper drag/drop (can only drag id=widget SVG element) & put dial face in background
return true;
}
function startDesign() { // called when switching to design mode
}
function endDesign() { // called when switching to design mode
}
function startEdit() { // called when editing started
}
function endEdit(param0) { // called when editing finishes
if (_attribs[1].value !== "") avgID.style.setProperty("display", "inline")
else avgID.style.setProperty("display", "none") // Only display average marker if channel is set
}
function scale(scaleX, scaleY) { // manage scaling
svgText.setAttribute("transform", "scale(" + scaleX + "," + scaleY + ")");
svgNeedle.setAttribute("transform", "scale(" + scaleX + "," + scaleY + ")"); // scale needle
}
// Called from framework for incoming channel events
function feed(channel, scope, data) {
var numeric = parseFloat(data);
if (isNaN(numeric)) return;
if (channel === _attribs[0].value.split("/")[2]) return rotateDial(numeric);
if (channel === _attribs[1].value.split("/")[2]) return setAvg(numeric);
}
// Called from framework for initial channel status
function ini(channel, scope, data) {
return feed(channel, scope, data);
}
// Set the average indicator
function setAvg(avgVal) {
document.getElementById("avgtool").textContent = "Average: " + avgVal;
if (avgVal > range * 1.05) avgVal = range * 1.05; // allow a little overrun
if (avgVal < range * -0.05) avgVal = range * -0.05; // allow a little underrun
var angle = parseInt(avgVal * 227 / range - 114);
avgID.setAttribute('transform', 'rotate(' + angle.toString() + ' 50 50)');
}
// Rotate dial between old and new
function rotateDial(newVal) {
var textVal = newVal;
var newVal = Math.abs(newVal);
if (newVal > range * 1.05) newVal = range * 1.05; // allow a little overrun
if (newVal < range * -0.05) newVal = range * -0.05; // allow a little underrun
if (range > 10) { // format displayed range
numID.textContent = Math.round(textVal);
} else {
numID.textContent = Math.round(textVal * 10) / 10;
}
numID.setAttribute("x", (document.getElementById("widget").clientWidth / 2 - numID.getBBox().width / 2)); // Adjust number to be center
needleID.style.setProperty('transition', 'transform ' + Math.abs(newVal - oldVal) * 2 / range + 's cubic-bezier(0.680, -0.550, 0.265, 1.550)');
needleID.style.setProperty('transform', 'rotate(' + newVal * 223 / range + 'deg)');
oldVal = newVal;
}
The JavaScript for this particular widget is a little easier to read than the HTML/SVG.
- The variable initialization at the top of the code gets the 'links' to the HTML objects we will be interacting with (eg. to spin the needle).
- The widgetStart function is called by the framework when the widget is first loaded into the dashboard and all the initialization code goes here. The _attribs[] array is provided by the UI framework and stores the settings for this particular widget instance (eg. if there is no secondary channel set when the widget was setup during design mode, don't display the secondary value indicator).
- The empty functions below the widgetStart function are optional and will be called when the dashboard enters or exits design mode, and when starting or finishing editing the widget properties in the designer. This allows the widget to do different things during design time and can be very useful for more sophisticated widgets (eg. add more design time functionality in addition to the standard design features all widgets inherit from the framework). In this example, if the secondary channel isn't set, the secondary indicator marker on the dial is hidden, a small but nice touch possible with only needing a line of code.
- The scale function handles how you scale the widget HTML/SVG entities when the user edits a widget and uses the mouse on one of the scale 'handles' to expand or contract the widget size. The framework will automatically scale simple widgets but sometimes you need the scaling logic to be different which you can implement in this function. In this function, a standard CSS transform function is used on the needle and text.
- The feed function is called by the framework when there is an event that the widget has subscribed to. Here we direct the incoming message depending if it is for the primary channel (the dial) or the secondary channel (the secondary indicator).
- The ini function is called when the widget is first instantiated. The automation framework maintains state of all events so when the browser first launches the framework sends the current channel state to the widget via this function. So it is similar to the feed function (that receives all live events) but is separate as you may want to do some initial work on the state data. Here we don't care, so we call the feed function to display the initial value.
- The next two functions are specific to the dial widget, and automate the needles. Here we take advantage of the power of CSS by using a nice bezier curve to vary the acceleration rate of the needle so it feels like a real dial needle - it accelerates quickly from 0, and if the dial has to swing a long way on the dial it will overshoot the mark and smoothly return to the true setting. It is small touches like this that bring the dashboard to life, for example I have a power dashboard screen where there are 6 of these dials (showing solar power, $$ saved per day, power per power phase etc.) and it looks great when you select the page and all the dials come to life! And only 1 line of code needed. That is the power of using these modern computing platforms like HTML5.
Below is a screenshot of the actual widget (from the power page). The widget is from the power dashboard screen and shows current power use on the dial, and the blue marker on the outside shows the average power for the day (ie. I'm using more power than average at that moment).
So I hope you like my widget and that the stuff above is informative enough to show how flexible and powerful technologies like HTML are for home automation and to get you excited about trying something similar yourself! The dial widget is a simple example, not so difficult to build (most of the code above came from a template) - there really is so much possible with a UI like this framework for desktops, tablets and phones, for future posts I'd like to describe the UI designer as well as a more sophisticated widget, the time series graph that uses the excellent D3 javascript libraries (eg. zoom and pan with finger gestures).
I'm happy to answer any questions about HTML development in general or my solution specifically and I'm considering making a demo site where you can play with the dashboard, WYSIWYG designer and widgets.