The widgets are all the same
<template>
<v-card class = "card">
<v-btn class = "btnBasic" :class = "btnClass" stacked @click="changeState()">
{{btnLabel}}
<v-icon class = "iconBasic" :class = "iconClass" >{{iconImage}}</v-icon>
</v-btn>
</v-card>
</template>
<script>
export default {
data() {
return {
source: 'Dashboard',
systemData: {button: {applianceName: 'Kitchen Air Fryer',
systemAttributeID: 'OnOff'},
},
// Note that when btnState = 0 the button will always be shown as 'Off'
btnState: 0,
btnData: {class: ['btnOff', 'btnOn'],
label: ['SO']
},
iconData: {class: ['iconOff', 'iconOn'],
image: ['mdi-power-socket-uk']
},
outputValue: ['Off', 'On'],
}
},
methods: {
changeState: function () {
this.toggleState(this.btnState)
let topic = `${this.source}/${this.systemData.button.applianceName}/${this.systemData.button.systemAttributeID}`
this.send( {topic, payload: this.outputValue[this.btnState]} )
},
toggleState: function () {
this.btnState = (++this.btnState) % this.outputValue.length
},
validateOption: function(option) {
const optionMax = option.length - 1
let optionSelect = this.btnState
if (optionSelect > optionMax) {
optionSelect = optionMax
}
return option[optionSelect]
},
/** Update Widget property values if required
*
* Input ui-update
* label Array of string, length 1 or more. Only number of outputValues
* will be shown. Note: The first entry in the Array will match the first
* entry in 'output'
* output Array of output values (any), length 1 or more. Each 'click' of
* button will output next value in Array. Button label will also
* update for each 'click' if there is more than 1 element in btnData.label
* Array
* icon New mdi icon
*
*/
setDynamicUpdates(uiUpdates) {
for (const [property, value] of Object.entries(uiUpdates)) {
if (Array.isArray(value)) {
if (property === 'label') {
this.btnData.label = value
}
if (property === 'output') {
this.outputValue = value
}
}
if (property === 'icon') {
this.iconData.image = value
}
}
},
},
watch: {
msg: function () {
const [protocol, applianceName, deviceName, attribute, action = ''] = this.msg.topic.split('/')
const btnApplianceName = this.systemData.button.applianceName
if (btnApplianceName === applianceName) {
const payloadValue = this.msg.payload?.value
if (attribute === 'OnOff') {
// Only change btnState if msg.payload is valid (i.e. is found in outputValue)
if (this.outputValue.indexOf(payloadValue) !== -1) {
this.btnState = this.outputValue.indexOf(payloadValue)
}
}
}
},
},
computed: {
// Automatically compute these variables whenever VueJS deems appropriate
btnLabel: function() {
return this.validateOption(this.btnData.label)
},
btnClass: function() {
return this.validateOption(this.btnData.class)
},
iconClass: function() {
return this.validateOption(this.iconData.class)
},
iconImage: function() {
return this.validateOption(this.iconData.image)
},
},
mounted () {
this.$socket.on("msg-input:" + this.id, (msg) => {
/**
* Comply with Dashboard 2 schema to set dynamic values
*
* msg: ui-update: {<property>: value}
*
*/
if ( Object.hasOwn(msg, 'ui_update') ) {
this.setDynamicUpdates(msg.ui_update)
}
})
},
unmounted() {
// code here when the component is removed from the Dashboard
// i.e. when the user navigates away from the page
this.$socket.off(`msg-input:${this.id}`)
},
}
</script>
<style scoped>
</style>
CSS ui-template node
.card {
background-color: #4F4F4F; /* Very dark gray */
border: 1px solid black;
border-radius: 8px;
width: 75px !important;
height: 105px;
}
.btnBasic {
background-color: #4F4F4F; /* Very dark gray */
border-radius: 8px;
width: 70px !important;
height: 75px !important;
}
.btnOn {
color: rgb(189, 150, 8); /* Dark washed rose */
}
.btnOff {
color: darkgrey;
}
.iconBasic {
font-size: 45px;
height: 45px;
width: 45px;
}
.iconOn {
color: rgb(189, 150, 8); /* Dark washed rose */
text-shadow: rgb(189, 150, 8) 0px 0px 10px; /* Dark washed rose */
}
.iconOff {
color: darkgrey;
text-shadow: 0px 0px 0px;
}
.v-slider-thumb__surface{
background-color: currentColor;
}