-
-
Notifications
You must be signed in to change notification settings - Fork 93
Examples Vue Complex Home Dashboard
I'm afraid that to actually use this example directly, you would need to have my complex flow that builds the data from a combination of my Drayton Wiser smart home heating system and various custom sensors and controls. So I've not bothered to post the flow that controls all of this but it may be of use if you are struggling with how to use VueJS with Node-RED and uibuilder.
This code is valid with uibuilder v1.2.2. It uses VueJS, and bootstrap-vue. Note the use of Vue components to break up the complexity of the page structure.
/*global document,$,window,uibuilder,Vue,_ */
/** Copyright (c) 2019 Julian Knight (Totally Information)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
**/
/** This is the default, template Front-End JavaScript for uibuilder
* It is usable as is though you will want to add your own code to
* process incoming and outgoing messages.
*
* uibuilderfe.js (or uibuilderfe.min.js) exposes the following global object:
* @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods
**/
'use strict'
/** Get a nested property from an object without returning any errors.
* If the property or property chain doesn't exist, undefined is returned.
* Property names with spaces may use either dot or bracket "[]" notation.
* Note that bracketed property names without surrounding quotes will fail the lookup.
* e.g. embedded variables are not supported.
* @param {Object} obj The object to check
* @param {string} prop The property or property chain to get (e.g. obj.prop1.prop1a or obj['prop1'].prop2)
* @returns {*|undefined} The value of the objects property or undefined if the property doesn't exist
*/
function getProp(obj, prop) {
if (typeof obj !== 'object') throw 'getProp: obj is not an object'
if (typeof prop !== 'string') throw 'getProp: prop is not a string'
// Replace [] notation with dot notation
prop = prop.replace(/\[["'`](.*)["'`]\]/g,".$1")
return prop.split('.').reduce(function(prev, curr) {
return prev ? prev[curr] : undefined
}, obj || self)
} // --- end of fn getProp() --- //
// Initialise Bootstrap-Vue: Not needed if loading via CDN
//Vue.use(BootstrapVue)
// Template Components
Vue.component('lights-tab', {
// NB: prop defined as 'home-data' because it is used as an HTML attribute. BUT use as variable 'homeData'
props: ['home-data', 'switches'],
template: '#lights-tab-template',
data: function() { return {
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
}},
computed: {
// orderedSwitches: function() {
// return _.orderBy(this.switches, 'room').filter(function (sw) {
// return sw.room === 'NA' ? false : true
// })
// },
},
methods: {
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
switchClick: function(clickData) {
let [switchId, switchStatus] = clickData
//console.log('switchClick', switchId, switchStatus)
uibuilder.send({
'topic': `COMMAND/${switchId}`,
'payload': switchStatus.toLowerCase() === 'on' ? 'Off' : 'On'
})
},
},
})
Vue.component('demand-card', {
props: [
'percentage-demand', 'demand-level', 'demand-max',
'demand-on-off-output', 'is-boosted',
],
template: '#demand-card-template',
computed: {
classDemandActive: function() {
return this.demandOnOffOutput === 'On' ? 'text-danger font-weight-bold': ''
},
isBoostedText: function() {
return this.isBoosted ? 'On' : 'Off'
},
classIsBoosted: function() {
return this.isBoosted ? 'text-danger font-weight-bold': ''
},
},
})
Vue.component('device-tab', {
props: ['home-data', 'devices'],
template: '#device-tab-template',
data: function() { return {
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
}},
computed: {
orderedDevices: function() {
// Use LoDash to reorder the object
return _.orderBy(this.devices, 'id')
},
},
methods: {
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
},
})
// Initialise Vue
new Vue({
el: "#app",
// We don't really need a function here but you do in components - keeping things consistent
data: function() { return {
// For formatting dates and times
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
// Which tab should be active?
tabIndex: 0,
// heating
lastUpdate : '[None]',
hTimer : null,
showNoUpdAlert : false,
demand : undefined,
percentageDemand : undefined,
demandOnOffOutput : 'N/A',
demandMax : 100,
isBoosted : false,
homeData : [],
// Field definitions - @see https://bootstrap-vue.js.org/docs/components/table#field-definition-reference
homeDataFields : [
{ key: 'floor',
label: 'Floor',
sortable: true,
class: 'border-right text-center',
thStyle: {width: '2em !important'},
},
{ key: 'Name',
label: 'Room',
sortable: true,
class: 'border-right',
// Variant applies to the whole column, including the header and footer
//variant: 'danger'
tdClass: (value, key, item) => {
const c = []
if ( item.ControlOutputState === 'On' ) c.push('bg-primary')
if ( item.outside === true ) c.push('font-italic')
return c.join(' ')
},
tdAttr: {'title':'Blue BG = Room requesting heat. Italic = room is outside'},
},
{ key: 'CalculatedTemperature',
label: '°c',
sortable: true,
class: 'text-right border-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
tdClass: (value, key, item) => {
const c = []
// Highlight if too cold or too hot
if ( item.outside === true ) {
// Outdoors
c.push('font-italic')
if ( value < 0 )
// Freezing
c.push('bg-danger')
else if ( value < 20 )
// <2
c.push('bg-warning')
else if ( value < 50 )
// <5
c.push(['text-white', 'bg-primary'])
else if ( value > 300 )
// >30
c.push('bg-warning')
} else {
// Indoors
if ( value < 100 )
// <10
c.push('bg-danger')
else if ( value > 230 )
// >23
c.push('bg-warning')
}
//if ( item.ControlOutputState === 'On' ) c.push('bg-primary')
return c.join(' ')
},
tdAttr: {'title':"Temperature. Highlighted if too high or too low."},
},
{ key: 'CalculatedHumidity',
label: 'H%',
sortable: true,
class: 'text-right',
formatter: (value, key, item) => {
let h = Math.round(value)
h = isNaN(h) ? value : h
return h === undefined ? '' : h + '%'
},
tdClass: (value, key, item) => {
const c = []
// Highlight if too high or too low
if ( item.outside === true ) {
// Outdoors
c.push('font-italic')
if ( value <40 ) c.push(['text-white', 'bg-primary'])
} else {
// Indoors
if ( value <40 ) c.push(['text-white', 'bg-primary'])
else if (value >60 ) c.push('bg-warning')
}
return c.join(' ')
},
tdAttr: {'title':"Humidity. Highlighted if too high or too low."},
},
// A virtual column with custom formatter
{ key: 'override',
label: 'O/ride',
class: 'border-left text-right',
thStyle: {width: '2em !important'},
formatter: (value, key, item) => {
if ( item.SetPointOrigin === undefined ) return false
else return item.SetPointOrigin!=='FromSchedule' ? true : false
},
},
{ key: 'details',
label: 'More',
class: 'text-right',
thStyle: {width: '2em !important'},
},
],
// For the details view of homeData table
hdDetailsFields: [
'ProductType','BatteryLevel','DisplayedSignalStrength',
{ key: 'SetPoint',
label: 'SetPoint °c',
class: 'text-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
},
{ key: 'MeasuredTemperature',
label: 'Measured °c',
class: 'text-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
},
{ key: 'MeasuredHumidity',
label: 'Measured Humidity',
class: 'text-right',
formatter: (value, key, item) => {
return value ? (value + '%') : ''
},
},
],
// Current Switch Settings
switches: {},
// Current Device statuses
devices: {},
}}, // --- End of data --- //
// computed: dynamic data, used as {{ cName }} - cached
computed: {
// Set the FG & BG of the demand card if demand is on
qDemandBg: function() {
return this.demandOnOffOutput === 'On' ? 'primary': ''
},
qDemandFg: function() {
return this.demandOnOffOutput === 'On' ? 'white': ''
},
classDemandActive: function() {
return this.demandOnOffOutput === 'On' ? 'text-danger': ''
},
// colour the demand bar depending on demand level
demandLevel: function() {
let a = null
switch (true) {
case ( typeof this.percentageDemand === 'string' ):
a = 'dark'
break
case this.percentageDemand <= 30:
a = 'success'
break
case this.percentageDemand <= 60:
a = 'warning'
break
default:
a = 'danger'
break
}
return a
},
// If a room has boost turned on
isBoostedFill: function() {
return this.isBoosted ? 'fill:#dc3545' : 'fill:#ffc107'
},
isBoostedText: function() {
return this.isBoosted ? 'On' : 'Off'
},
}, // --- End of computed --- //
// methods:
methods: {
/** Return a setInterval timer for the heating update warning
* @callback cb setInterval function
* @param {number} timeout The timeout to be passed to the setInterval fn. Optional, defaults to 2 minutes.
* @returns {cb} setInterval function
*/
heatingUpdTimer: function(timeout=120000) {
const viewApp = this
return setInterval(function(){
//console.log('Vue:methods:heatingUpdTimer heating update not received in 2 minutes')
viewApp.showNoUpdAlert = true
}, timeout)
},
/** Handle row-clicked event on rooms table
* @param {Object} item The Row data for the clicked row
* @param {number} index The row index for the clicked row
* @param {Object} event The click event data
**/
// TODO: need separate array to maintain display state
onRoomsRowClicked (item, index, event) {
item._showDetails = !item._showDetails
},
// Filter Fn for heating room table - @see https://bootstrap-vue.js.org/docs/components/table#filtering
currentRoomsTblFilter: function(item) {
if ( item.CalculatedTemperature ) return true
else return false
},
/** Invoked when user changes tab - saves current tab - @see mounted
* @param {number} i Selected tab index number
*/
changeTab: function(i) {
// Save to browser's session storage
sessionStorage.currentTab = i
},
// Format date/time
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
// return formatted HTML version of JSON object
syntaxHighlight: function(json) {
json = JSON.stringify(json, undefined, 4)
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number'
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key'
} else {
cls = 'string'
}
} else if (/true|false/.test(match)) {
cls = 'boolean'
} else if (/null/.test(match)) {
cls = 'null'
}
return '<span class="' + cls + '">' + match + '</span>'
})
} // --- End of syntaxHighlight --- //
}, // --- End of methods --- //
// Available hooks: init,mounted,updated,destroyed
mounted: function(){
uibuilder.debug(false) // output uibuilderfe debug messages
//console.debug('Vue:mounted - setting up uibuilder watchers')
// Save confusion by keeping a specific reference to this Vue app
const vueApp = this
// Start countdown. If lastUpdate not updated in 2 minutes, show a warning.
vueApp.hTimer = vueApp.heatingUpdTimer()
// On-load Reset the current tab to the one saved in session storage - strange, stored as number but retrieves as a string
vueApp.tabIndex = Number(sessionStorage.currentTab)
// If msg changes - msg is updated when a standard msg is received from Node-RED over Socket.IO
// Note that you can also listen for 'msgsReceived' as they are updated at the same time
// but newVal relates to the attribute being listened to.
uibuilder.onChange('msg', function(newVal){
//console.debug('Vue:mounted:UIBUILDER: property msg changed! ', newVal)
vueApp.msgRecvd = newVal
// What kind of message did we receive?
// Use getProp so we don't pollute the original input. Then tidy the topic
let topic = getProp(newVal, 'topic').replace(/\/SWITCH..$/,'')
if ( topic.substring(0,8) === 'DEVICES/' ) topic = 'DEVICES'
switch (topic) {
// Full homeDetails
case 'Home Details':
//console.debug('UIBUILDER:onChange:msg: homeDetails msg received ', newVal)
/** To update the home details, we are expecting a msg like:
* msg = {
* 'topic' : 'Home Details',
* 'payload' : {
* 'homeDetails': homeDetails, // ARRAY
* 'demand' : demand, // OBJECT
* 'lastUpdate': new Date(),
* },
* }
*/
// for convenience
const data = newVal.payload
// Formatted last update
vueApp.lastUpdate = vueApp.fmtTime(data.lastUpdate)
// clear and restart countdown. If lastUpdate not updated in 2 minutes, show a warning.
vueApp.showNoUpdAlert = false; clearInterval(vueApp.hTimer); vueApp.hTimer = null;
vueApp.hTimer = vueApp.heatingUpdTimer()
vueApp.demand = data.demand
// for convenience ...
vueApp.percentageDemand = data.demand.PercentageDemand
vueApp.demandOnOffOutput = data.demand.DemandOnOffOutput
vueApp.isBoosted = data.demand.isBoosted
//vueApp.qDemand = data.demand.DemandOnOffOutput === 'On' ? true : false
//vueApp.HeatingRelayState = data.demand.HeatingRelayState
//vueApp.IsSmartValvePreventingDemand = data.demand.IsSmartValvePreventingDemand
// Sorted array of home data
vueApp.homeData = data.homeDetails
// vvv NB: The below adds the _showDetails field TOO LATE for it to be
// correctly responsive - now added at source
// Add _showDetails:false to all members of the array for the table display
//vueApp.homeData.map(item => {item._showDetails = false; return item;})
// TODO: Should we null/delete the newVal var? Or would that kill vueApp.homeData as well?
break
// Individual switch update
case 'COMMAND':
//console.debug('UIBUILDER:onChange:msg: COMMAND/SWITCHnn msg received ', newVal)
let sw = newVal.topic.replace('COMMAND/','')
vueApp.switches[sw].status = newVal.payload
break
// Full switch update
case 'SWITCHES':
vueApp.switches = newVal.payload
break
// Individual device update
case 'DEVICES':
//console.debug('UIBUILDER:onChange:msg: DEVICES/+ msg received ', newVal)
let dev = newVal.topic.replace('DEVICES/','')
vueApp.devices[dev].status = newVal.payload
break
// Full devices update
case 'DEVICESFULL':
vueApp.devices = newVal.payload
break
// Don't process default
default:
//ignore
}
}) // ---- End of uibuilder.onChange() watcher function ---- //
} // --- End of mounted hook --- //
}) // --- End of app1 --- //
// EOF/*global document,$,window,uibuilder,Vue,_ */
/** Copyright (c) 2019 Julian Knight (Totally Information)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
**/
/** This is the default, template Front-End JavaScript for uibuilder
* It is usable as is though you will want to add your own code to
* process incoming and outgoing messages.
*
* uibuilderfe.js (or uibuilderfe.min.js) exposes the following global object:
* @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods
**/
'use strict'
/** Get a nested property from an object without returning any errors.
* If the property or property chain doesn't exist, undefined is returned.
* Property names with spaces may use either dot or bracket "[]" notation.
* Note that bracketed property names without surrounding quotes will fail the lookup.
* e.g. embedded variables are not supported.
* @param {Object} obj The object to check
* @param {string} prop The property or property chain to get (e.g. obj.prop1.prop1a or obj['prop1'].prop2)
* @returns {*|undefined} The value of the objects property or undefined if the property doesn't exist
*/
function getProp(obj, prop) {
if (typeof obj !== 'object') throw 'getProp: obj is not an object'
if (typeof prop !== 'string') throw 'getProp: prop is not a string'
// Replace [] notation with dot notation
prop = prop.replace(/\[["'`](.*)["'`]\]/g,".$1")
return prop.split('.').reduce(function(prev, curr) {
return prev ? prev[curr] : undefined
}, obj || self)
} // --- end of fn getProp() --- //
// Initialise Bootstrap-Vue: Not needed if loading via CDN
//Vue.use(BootstrapVue)
// Template Components
Vue.component('lights-tab', {
// NB: prop defined as 'home-data' because it is used as an HTML attribute. BUT use as variable 'homeData'
props: ['home-data', 'switches'],
template: '#lights-tab-template',
data: function() { return {
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
}},
computed: {
// orderedSwitches: function() {
// return _.orderBy(this.switches, 'room').filter(function (sw) {
// return sw.room === 'NA' ? false : true
// })
// },
},
methods: {
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
switchClick: function(clickData) {
let [switchId, switchStatus] = clickData
//console.log('switchClick', switchId, switchStatus)
uibuilder.send({
'topic': `COMMAND/${switchId}`,
'payload': switchStatus.toLowerCase() === 'on' ? 'Off' : 'On'
})
},
},
})
Vue.component('demand-card', {
props: [
'percentage-demand', 'demand-level', 'demand-max',
'demand-on-off-output', 'is-boosted',
],
template: '#demand-card-template',
computed: {
classDemandActive: function() {
return this.demandOnOffOutput === 'On' ? 'text-danger font-weight-bold': ''
},
isBoostedText: function() {
return this.isBoosted ? 'On' : 'Off'
},
classIsBoosted: function() {
return this.isBoosted ? 'text-danger font-weight-bold': ''
},
},
})
Vue.component('device-tab', {
props: ['home-data', 'devices'],
template: '#device-tab-template',
data: function() { return {
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
}},
computed: {
orderedDevices: function() {
// Use LoDash to reorder the object
return _.orderBy(this.devices, 'id')
},
},
methods: {
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
},
})
// Initialise Vue
new Vue({
el: "#app",
// We don't really need a function here but you do in components - keeping things consistent
data: function() { return {
// For formatting dates and times
dtOpts: {
timeZone: 'Europe/London',
weekday: 'short', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric',
},
dtFmt: 'en-GB',
// Which tab should be active?
tabIndex: 0,
// heating
lastUpdate : '[None]',
hTimer : null,
showNoUpdAlert : false,
demand : undefined,
percentageDemand : undefined,
demandOnOffOutput : 'N/A',
demandMax : 100,
isBoosted : false,
homeData : [],
// Field definitions - @see https://bootstrap-vue.js.org/docs/components/table#field-definition-reference
homeDataFields : [
{ key: 'floor',
label: 'Floor',
sortable: true,
class: 'border-right text-center',
thStyle: {width: '2em !important'},
},
{ key: 'Name',
label: 'Room',
sortable: true,
class: 'border-right',
// Variant applies to the whole column, including the header and footer
//variant: 'danger'
tdClass: (value, key, item) => {
const c = []
if ( item.ControlOutputState === 'On' ) c.push('bg-primary')
if ( item.outside === true ) c.push('font-italic')
return c.join(' ')
},
tdAttr: {'title':'Blue BG = Room requesting heat. Italic = room is outside'},
},
{ key: 'CalculatedTemperature',
label: '°c',
sortable: true,
class: 'text-right border-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
tdClass: (value, key, item) => {
const c = []
// Highlight if too cold or too hot
if ( item.outside === true ) {
// Outdoors
c.push('font-italic')
if ( value < 0 )
// Freezing
c.push('bg-danger')
else if ( value < 20 )
// <2
c.push('bg-warning')
else if ( value < 50 )
// <5
c.push(['text-white', 'bg-primary'])
else if ( value > 300 )
// >30
c.push('bg-warning')
} else {
// Indoors
if ( value < 100 )
// <10
c.push('bg-danger')
else if ( value > 230 )
// >23
c.push('bg-warning')
}
//if ( item.ControlOutputState === 'On' ) c.push('bg-primary')
return c.join(' ')
},
tdAttr: {'title':"Temperature. Highlighted if too high or too low."},
},
{ key: 'CalculatedHumidity',
label: 'H%',
sortable: true,
class: 'text-right',
formatter: (value, key, item) => {
let h = Math.round(value)
h = isNaN(h) ? value : h
return h === undefined ? '' : h + '%'
},
tdClass: (value, key, item) => {
const c = []
// Highlight if too high or too low
if ( item.outside === true ) {
// Outdoors
c.push('font-italic')
if ( value <40 ) c.push(['text-white', 'bg-primary'])
} else {
// Indoors
if ( value <40 ) c.push(['text-white', 'bg-primary'])
else if (value >60 ) c.push('bg-warning')
}
return c.join(' ')
},
tdAttr: {'title':"Humidity. Highlighted if too high or too low."},
},
// A virtual column with custom formatter
{ key: 'override',
label: 'O/ride',
class: 'border-left text-right',
thStyle: {width: '2em !important'},
formatter: (value, key, item) => {
if ( item.SetPointOrigin === undefined ) return false
else return item.SetPointOrigin!=='FromSchedule' ? true : false
},
},
{ key: 'details',
label: 'More',
class: 'text-right',
thStyle: {width: '2em !important'},
},
],
// For the details view of homeData table
hdDetailsFields: [
'ProductType','BatteryLevel','DisplayedSignalStrength',
{ key: 'SetPoint',
label: 'SetPoint °c',
class: 'text-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
},
{ key: 'MeasuredTemperature',
label: 'Measured °c',
class: 'text-right',
formatter: (value, key, item) => {
// -200 or -32768 are unset or invalid
if ( value < -99 ) return ''
const t = (value/10).toFixed(1)
return isNaN(t) ? value : t
},
},
{ key: 'MeasuredHumidity',
label: 'Measured Humidity',
class: 'text-right',
formatter: (value, key, item) => {
return value ? (value + '%') : ''
},
},
],
// Current Switch Settings
switches: {},
// Current Device statuses
devices: {},
}}, // --- End of data --- //
// computed: dynamic data, used as {{ cName }} - cached
computed: {
// Set the FG & BG of the demand card if demand is on
qDemandBg: function() {
return this.demandOnOffOutput === 'On' ? 'primary': ''
},
qDemandFg: function() {
return this.demandOnOffOutput === 'On' ? 'white': ''
},
classDemandActive: function() {
return this.demandOnOffOutput === 'On' ? 'text-danger': ''
},
// colour the demand bar depending on demand level
demandLevel: function() {
let a = null
switch (true) {
case ( typeof this.percentageDemand === 'string' ):
a = 'dark'
break
case this.percentageDemand <= 30:
a = 'success'
break
case this.percentageDemand <= 60:
a = 'warning'
break
default:
a = 'danger'
break
}
return a
},
// If a room has boost turned on
isBoostedFill: function() {
return this.isBoosted ? 'fill:#dc3545' : 'fill:#ffc107'
},
isBoostedText: function() {
return this.isBoosted ? 'On' : 'Off'
},
}, // --- End of computed --- //
// methods:
methods: {
/** Return a setInterval timer for the heating update warning
* @callback cb setInterval function
* @param {number} timeout The timeout to be passed to the setInterval fn. Optional, defaults to 2 minutes.
* @returns {cb} setInterval function
*/
heatingUpdTimer: function(timeout=120000) {
const viewApp = this
return setInterval(function(){
//console.log('Vue:methods:heatingUpdTimer heating update not received in 2 minutes')
viewApp.showNoUpdAlert = true
}, timeout)
},
/** Handle row-clicked event on rooms table
* @param {Object} item The Row data for the clicked row
* @param {number} index The row index for the clicked row
* @param {Object} event The click event data
**/
// TODO: need separate array to maintain display state
onRoomsRowClicked (item, index, event) {
item._showDetails = !item._showDetails
},
// Filter Fn for heating room table - @see https://bootstrap-vue.js.org/docs/components/table#filtering
currentRoomsTblFilter: function(item) {
if ( item.CalculatedTemperature ) return true
else return false
},
/** Invoked when user changes tab - saves current tab - @see mounted
* @param {number} i Selected tab index number
*/
changeTab: function(i) {
// Save to browser's session storage
sessionStorage.currentTab = i
},
// Format date/time
fmtTime: function(t) {
return new Intl.DateTimeFormat(this.dtFmt, this.dtOpts).format(new Date(t))
},
// return formatted HTML version of JSON object
syntaxHighlight: function(json) {
json = JSON.stringify(json, undefined, 4)
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'number'
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'key'
} else {
cls = 'string'
}
} else if (/true|false/.test(match)) {
cls = 'boolean'
} else if (/null/.test(match)) {
cls = 'null'
}
return '<span class="' + cls + '">' + match + '</span>'
})
} // --- End of syntaxHighlight --- //
}, // --- End of methods --- //
// Available hooks: init,mounted,updated,destroyed
mounted: function(){
uibuilder.debug(false) // output uibuilderfe debug messages
//console.debug('Vue:mounted - setting up uibuilder watchers')
// Save confusion by keeping a specific reference to this Vue app
const vueApp = this
// Start countdown. If lastUpdate not updated in 2 minutes, show a warning.
vueApp.hTimer = vueApp.heatingUpdTimer()
// On-load Reset the current tab to the one saved in session storage - strange, stored as number but retrieves as a string
vueApp.tabIndex = Number(sessionStorage.currentTab)
// If msg changes - msg is updated when a standard msg is received from Node-RED over Socket.IO
// Note that you can also listen for 'msgsReceived' as they are updated at the same time
// but newVal relates to the attribute being listened to.
uibuilder.onChange('msg', function(newVal){
//console.debug('Vue:mounted:UIBUILDER: property msg changed! ', newVal)
vueApp.msgRecvd = newVal
// What kind of message did we receive?
// Use getProp so we don't pollute the original input. Then tidy the topic
let topic = getProp(newVal, 'topic').replace(/\/SWITCH..$/,'')
if ( topic.substring(0,8) === 'DEVICES/' ) topic = 'DEVICES'
switch (topic) {
// Full homeDetails
case 'Home Details':
//console.debug('UIBUILDER:onChange:msg: homeDetails msg received ', newVal)
/** To update the home details, we are expecting a msg like:
* msg = {
* 'topic' : 'Home Details',
* 'payload' : {
* 'homeDetails': homeDetails, // ARRAY
* 'demand' : demand, // OBJECT
* 'lastUpdate': new Date(),
* },
* }
*/
// for convenience
const data = newVal.payload
// Formatted last update
vueApp.lastUpdate = vueApp.fmtTime(data.lastUpdate)
// clear and restart countdown. If lastUpdate not updated in 2 minutes, show a warning.
vueApp.showNoUpdAlert = false; clearInterval(vueApp.hTimer); vueApp.hTimer = null;
vueApp.hTimer = vueApp.heatingUpdTimer()
vueApp.demand = data.demand
// for convenience ...
vueApp.percentageDemand = data.demand.PercentageDemand
vueApp.demandOnOffOutput = data.demand.DemandOnOffOutput
vueApp.isBoosted = data.demand.isBoosted
//vueApp.qDemand = data.demand.DemandOnOffOutput === 'On' ? true : false
//vueApp.HeatingRelayState = data.demand.HeatingRelayState
//vueApp.IsSmartValvePreventingDemand = data.demand.IsSmartValvePreventingDemand
// Sorted array of home data
vueApp.homeData = data.homeDetails
// vvv NB: The below adds the _showDetails field TOO LATE for it to be
// correctly responsive - now added at source
// Add _showDetails:false to all members of the array for the table display
//vueApp.homeData.map(item => {item._showDetails = false; return item;})
// TODO: Should we null/delete the newVal var? Or would that kill vueApp.homeData as well?
break
// Individual switch update
case 'COMMAND':
//console.debug('UIBUILDER:onChange:msg: COMMAND/SWITCHnn msg received ', newVal)
let sw = newVal.topic.replace('COMMAND/','')
vueApp.switches[sw].status = newVal.payload
break
// Full switch update
case 'SWITCHES':
vueApp.switches = newVal.payload
break
// Individual device update
case 'DEVICES':
//console.debug('UIBUILDER:onChange:msg: DEVICES/+ msg received ', newVal)
let dev = newVal.topic.replace('DEVICES/','')
vueApp.devices[dev].status = newVal.payload
break
// Full devices update
case 'DEVICESFULL':
vueApp.devices = newVal.payload
break
// Don't process default
default:
//ignore
}
}) // ---- End of uibuilder.onChange() watcher function ---- //
} // --- End of mounted hook --- //
}) // --- End of app1 --- //
// EOFpre { background-color: #212121 !important; color: wheat;}
.tcentre { text-align: center; }
.uk-table th {
position: sticky; top: 0; z-index: 1;
background-color:black; color:#EEEEEE;
}
pre .string { color: orange; }
.number { color: white; }
.boolean { color: rgb(20, 99, 163); }
.null { color: magenta; }
.key { color: #069fb3;}
card-l-primary, l-primary, btn-l-primary {background-color: #73b7ff!important;}
.nodisplay { display: none; }Please feel free to add comments to the page (clearly mark with your initials & please add a commit msg so we know what has changed). You can contact me in the Discourse forum, or raise an issue here in GitHub! I will make sure all comments & suggestions are represented here.
-
Walkthrough 🔗 Getting started
-
In Progress and To Do 🔗 What's coming up for uibuilder?
-
Awesome uibuilder Examples, tutorials, templates and references.
-
How To
- How to send data when a client connects or reloads the page
- Send messages to a specific client
- Cache & Replay Messages
- Cache without a helper node
- Use webpack to optimise front-end libraries and code
- How to contribute & coding standards
- How to use NGINX as a proxy for Node-RED
- How to manage packages manually
- How to upload a file from the browser to Node-RED
-
Vanilla HTML/JavaScript examples
-
VueJS general hints, tips and examples
- Load Vue (v2 or v3) components without a build step (modern browsers only)
- How to use webpack with VueJS (or other frameworks)
- Awesome VueJS - Tips, info & libraries for working with Vue
- Components that work
-
VueJS v3 hints, tips and examples
-
VueJS v2 hints, tips and examples
- Dynamically load .vue files without a build step (Vue v2)
- Really Simple Example (Quote of the Day)
- Example charts using Chartkick, Chart.js, Google
- Example Gauge using vue-svg-gauge
- Example charts using ApexCharts
- Example chart using Vue-ECharts
- Example: debug messages using uibuilder & Vue
- Example: knob/gauge widget for uibuilder & Vue
- Example: Embedded video player using VideoJS
- Simple Button Acknowledgement Example Thanks to ringmybell
- Using Vue-Router without a build step Thanks to AFelix
- Vue Canvas Knob Component Thanks to Klaus Zerbe
-
Examples for other frameworks (check version before trying)
- Basic jQuery example - Updated for uibuilder v6.1
- ReactJS with no build - updated for uibuilder v5/6
-
Examples for other frameworks (may not work, out-of-date)
-
Outdated Pages (Historic only)
- v1 Examples (these need updating to uibuilder v2/v3/v4/v5)