Dashboard 2 - Multi-state switch

Hey @hotNipi,
These gauges look awesome good. Pure art... Hand made stuff, no AI involved I assume :wink:

If you should find some time (or energy...)... My D1 dashboard is 'very' primitive, so should be doable to convert it to D2. The only problem is that I almost only have multistate-swich nodes on my dashboard, and there is no real D2 alternative available. So if you could ever create a simple prototype version for D2, that would be a very big help.

P.S. I don't expect you to build a full blown version, just some basic template stuff to get noobs like me started ...

And if you don't have time, then I completely understand it!! In that case you can perhaps join our support circle for code wizards who’ve magically run out of time for open source development :zipper_mouth_face:

Thanks!!!
Bart

Hi Bart, is it worth creating a separate thread? You could describe what you need from the multistate-switch. Might even start some competition for people to build interesting versions?

Might also prompt people to create separate D2 and web component versions.

Although maybe a little overweight, but could a package like this - Toggle switch for vue.js be implemented in DB2?

multi

2 Likes

No need, Vuetify already comes with one packaged:

2 Likes

Are tou referring to button toggle groups here? Something like: Button toggle component — Vuetify?

Can use that in a ui-template, if popular, we could also look at making it a core node

1 Like

Yes I've just found it, looks promising!

button

Hi @TotallyInformation , @joepavitt, @TotallyInformation,
It is fine for me if somebody can put this multistate-switch replies all to a separte topic!
Well I was talking about the node-red-contrib-ui-multistate-switch that @hotNipi and I created together some time ago. But now just something simple just to get started.

I had already last week a (very quick) at vuetify, to see whether something related was available. But I didn't find anything, which is why I asked it tonight.. Seems I have been searching with the wrong keywords, because something like you guys find would indeed do the job. Like e.g. 3 options for temperature control: ON, OFF, AUTOMATIC.

I have no idea at all whether it would be popular, but I "think" that for a lot of home automation dashboards it would be very useful for lots of use cases. So if it ever would be promoted to become a core ui node, that would be great. Of course, Rome was also not build in 1 day...

2 Likes

This one...

1 Like

Yes indeed. Good idea to share the animation, so people know what we are discussing.
But of course it doesn't need to have that look and feel.
If everything in the dashboard has the same kind of style, that is good enough for me.

I think the button toggle should do the trick. Examples didn't show option to have different colors for states. That's only thing I see what is actually missing if to compare with multistate switch.

Animation of course is another thing but hey...

Ah yes indeed, good that you point that out!!
The colors is a very strong visual indication of the state. Like red and green, for OFF and ON.
Don't have much styling in my D1 dashboard, but all my multi-state switches have colors.
So if you could find some time to share an example of a colored button toggle, that would really get my started to migrate do D2...

1 Like

Will look at it during next couple of days.

3 Likes

Something to play with

<template>
    <div class="mss-wrapper">
        <v-chip variant="text">
            {{label}}
        </v-chip>
        <v-btn-toggle  mandatory divided rounded="xl" :variant="variant" :color="selectedColor" v-model="selection">
            <v-btn v-for="option in options">
                <template v-if="option.icon" v-slot:prepend>
                    <v-icon> {{option.icon}} </v-icon>
                </template>
                {{option.label}}
            </v-btn>        
        </v-btn-toggle>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                //define me here
                label:"Multistate Prototype",                
                options:[
                    {label:"On",icon:"mdi-power-cycle",color:"green"}, 
                    {label:"Off",icon:"mdi-power-off",color:"red"},
                    {label:"Auto",color:"blue"},// you can omit the icon
                    {label:"Manual"}// can omit color - defaults to theme color
                ],
                // "outlined" (if the site bg is white or very light)"
                // "default" (if the site bg is dark)"
                // ("text" or "plain" also available but requires some styling)
                look:"outlined",              
                
                // no need to change those
                selection: null,
                changeByInput:false, // in case of input - render but don't send the msg out
               
            }
        },
      
        watch: {
            msg: function(){        
                if(this.msg.payload != undefined){
                    if(typeof this.msg.payload === "number"){
                    // validation should be ore strict based on defined options
                        this.changeByInput = true
                        this.selection = this.msg.payload
                    }
                    
                }
            },
            selection:function(){                
                if(this.changeByInput == true){
                    this.changeByInput = false
                }
                else{
                    this.send({payload:this.selection,topic:'multistate'})
                }
            }
        },
        
        computed: {            
            selectedColor:function(){
                if(this.selection == null){
                    return ""
                }
                return this.options[this.selection].color ?? "rgb(var(--v-theme-primary))"
            },
            variant:function(){
                return this.look == "default"  ? null : this.look
            }
        },
        
       
    }
</script>

<style>
/*styles should be defined in separate ui_template targeted to the site*/

.mss-wrapper {
    display:grid;
    grid-template-columns:1fr 1fr;
    align-items:center;
}
.mss-wrapper .v-chip__content{
    white-space:normal;
    border-color:rgba(var(--v-border-color),.3);
}
.mss-wrapper .v-btn-group{
    width:max-content;
}
.mss-wrapper .v-btn--variant-elevated, .mss-wrapper .v-btn--variant-outlined{
    color:#cccccc;
}
</style>


5 Likes

Definitely...
Kudos CSS master :pray:
This is really a very good starting point.
Really appreciated!!!

I have done a small change to work with values instead of indexes:

  1. Each option has now a value:
    options:[
       {label:"On", value: "on", icon:"mdi-power-cycle", color:"green"}, 
       {label:"Off", value: "off", icon:"mdi-power-off", color:"red"},
       {label:"Auto", value: "auto", color:"blue"},
       {label:"Manual", value: "manual"}
    ],
    
  2. The input message payload now needs to contain a value (to be selected), instead of an index.
  3. The output message payload now contains a (selected) value, instead of an index.

A short demo, which also shows your nice color updates:

values

[{"id":"dd0cb728ad173407","type":"ui-template","z":"0bff6d05f60fe1e6","group":"9fa39c19a26ce99e","page":"","ui":"","name":"","order":0,"width":0,"height":0,"head":"","format":"<template>\n    <div class=\"mss-wrapper\">\n        <v-chip variant=\"text\">\n            {{label}}\n        </v-chip>\n        <v-btn-toggle  mandatory divided rounded=\"xl\" :variant=\"variant\" :color=\"selectedColor\" v-model=\"selectedValue\">\n            <v-btn v-for=\"option in options\" :value=\"option.value\">\n                <template v-if=\"option.icon\" v-slot:prepend>\n                    <v-icon> {{option.icon}} </v-icon>\n                </template>\n                {{option.label}}\n            </v-btn>        \n        </v-btn-toggle>\n    </div>\n</template>\n\n<script>\n    export default {\n        data() {\n            return {\n                //define me here\n                label:\"Multistate Prototype\",                \n                options:[\n                    {label:\"On\", value: \"on\", icon:\"mdi-power-cycle\", color:\"green\"}, \n                    {label:\"Off\", value: \"off\", icon:\"mdi-power-off\", color:\"red\"},\n                    {label:\"Auto\", value: \"auto\", color:\"blue\"},// you can omit the icon\n                    {label:\"Manual\", value: \"manual\"}// can omit color - defaults to theme color\n                ],\n                // \"outlined\" (if the site bg is white or very light)\"\n                // \"default\" (if the site bg is dark)\"\n                // (\"text\" or \"plain\" also available but requires some styling)\n                look:\"outlined\",              \n                \n                // no need to change those\n                selection: null,\n                selectedValue: 'on', // Default selected value\n                changeByInput:false, // in case of input - render but don't send the msg out\n               \n            }\n        },\n      \n        watch: {\n            msg: function(){        \n                if(this.msg.payload != undefined){\n                    if(typeof this.msg.payload === \"string\"){\n                        // validation should be more strict based on defined options\n                        let option = this.options.find(option => option.value === this.msg.payload)\n                        if (option) {\n                            this.changeByInput = true\n                            this.selectedValue = option.value\n                        }\n                    }\n                    \n                }\n            },\n            selectedValue:function(){                \n                if(this.changeByInput == true){\n                    this.changeByInput = false\n                }\n                else{\n                    let selectedOption = this.options.find(option => option.value === this.selectedValue)\n                    if (selectedOption) {\n                        this.send({payload:selectedOption.value,topic:'multistate'})\n                    }\n                }\n            }\n        },\n        \n        computed: {            \n            selectedColor:function(){\n                if(this.selectedValue == null){\n                    return \"\"\n                }\n                return this.options.find(option => option.value === this.selectedValue).color ?? \"rgb(var(--v-theme-primary))\"\n            },\n            variant:function(){\n                return this.look == \"default\"  ? null : this.look\n            }\n        },\n        \n       \n    }\n</script>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":1200,"y":80,"wires":[["4a14410bba1d4a82"]]},{"id":"4a14410bba1d4a82","type":"debug","z":"0bff6d05f60fe1e6","name":"Selected option","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1380,"y":80,"wires":[]},{"id":"2693f680c134392c","type":"inject","z":"0bff6d05f60fe1e6","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"on","payloadType":"str","x":1030,"y":80,"wires":[["dd0cb728ad173407"]]},{"id":"e215342d9239358f","type":"inject","z":"0bff6d05f60fe1e6","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"off","payloadType":"str","x":1030,"y":120,"wires":[["dd0cb728ad173407"]]},{"id":"b0cc83d57f02f08b","type":"inject","z":"0bff6d05f60fe1e6","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"auto","payloadType":"str","x":1030,"y":160,"wires":[["dd0cb728ad173407"]]},{"id":"cbe9ae5a47a6d80d","type":"inject","z":"0bff6d05f60fe1e6","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"manual","payloadType":"str","x":1030,"y":200,"wires":[["dd0cb728ad173407"]]},{"id":"9fa39c19a26ce99e","type":"ui-group","name":"Alarm systeem","page":"8604ea49340f9d41","width":"6","height":"1","order":-1,"showTitle":true,"className":"","visible":"true","disabled":"false"},{"id":"8604ea49340f9d41","type":"ui-page","name":"Home","ui":"be29745a6e568f30","path":"/page1","icon":"home","layout":"grid","theme":"092547d34959327c","order":-1,"className":"","visible":"true","disabled":"false"},{"id":"be29745a6e568f30","type":"ui-base","name":"UI Name","path":"/dashboard","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false},{"id":"092547d34959327c","type":"ui-theme","name":"Theme Name","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"}}]
3 Likes

Excellent work guys!

Using the Vuetify framework, it's styling will coordinate nicely with the other widgets, for a consistent appearance (unless of course they are overridden by CSS)

2 Likes

@BartButenaers Don't mess with strings just like that :slight_smile:
Also don't take away option to deal with indexes. Some of us do like math and numbers :stuck_out_tongue:

TEMPLATE

<template>
    <div class="mss-wrapper">
        <v-chip variant="text">
            {{label}}
        </v-chip>
        <v-btn-toggle  mandatory divided rounded="xl" :variant="variant" :color="selectedColor" v-model="selection">
            <v-btn v-for="(option, index) in options" :key='index' :class="option.label ? '' : 'icon-only'">
                <template v-if="option.icon" v-slot:prepend >
                    <v-icon size="x-large"> {{option.icon}} </v-icon>
                </template>
                {{option.label}}
            </v-btn>        
        </v-btn-toggle>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                //define me here
                label:"Multistate Prototype",                
                options:[
                    {label:"",value:"on",icon:"mdi-antenna",color:"green"},// label can be empty 
                    {value:"off", icon:"mdi-basketball",color:"red"},//can omit the label
                    {label:"Auto",value:"auto", color:"blue",icon:"mdi-car-wrench"},// can omit the icon
                    {label:"Manual", value:"manual", color:"orange",icon:"mdi-car-shift-pattern"}// can omit color - defaults to theme color
                ],
                // "outlined" (if the site bg is white or very light)"
                // "default" (if the site bg is dark)"
                // ("text" or "plain" also available but requires some styling)
                look:"outlined",              
                
                // no need to change those
                selection: null,
                changeByInput:false, // in case of input - render but don't send the msg out
               
            }
        },
      
        watch: {
            msg: function(){        
                if(this.msg.payload != undefined){
                    if(typeof this.msg.payload === "number"){                   
                        if(this.isValidIndex(this.msg.payload)){
                            this.changeByInput = true
                            this.selection = this.msg.payload
                        }                        
                    }
                    else if(typeof this.msg.payload === "string"){
                        if(this.isValidValue(this.msg.payload)){
                            this.changeByInput = true
                            this.selection = this.findOptionIndexByValue(this.msg.payload) // may be null but quarded
                        }
                    }
                    
                }
            },
            selection:function(){                
                if(this.changeByInput == true){
                    this.changeByInput = false
                }
                else{
                    this.send(this.getOutputMessage())
                }
            }
        },

        methods: {
            isValidIndex: function (idx){
                if(idx < 0){
                    return false
                }
                if(idx >= this.options.length){
                    return false
                }
                return true                 
            },
            isValidValue:function (val){
                console.log('validate value',val)
                if(val == null){
                    return false
                }
                if(val == undefined){
                    return false
                }
                if(val == ""){
                    return false
                }
                if(this.options.findIndex(option => option.value === val ) == -1){
                    return false
                }
                return true

            },
            findOptionByValue:function(val){
                let opt = this.options.find(option => option.value === val)
                if(opt){
                    return opt
                }
                return null
            },
            findOptionIndexByValue:function(val){
                let idx = this.options.findIndex(option => option.value === val)                
                if(idx != -1){
                    return idx
                }
                return null
            },
            getOutputMessage:function(){                
                return {payload:this.options[this.selection].value,topic:'multistate'}                
            }
        },
        
        computed: {            
            selectedColor:function(){
                if(this.selection == null){
                    return ""
                }
                return this.options[this.selection].color ?? "rgb(var(--v-theme-primary))"
            },
            variant:function(){
                return this.look == "default"  ? null : this.look
            }
        }
    }
</script>

CSS

.mss-wrapper {
    display:grid;
    grid-template-columns:1fr 1fr;
    align-items:center;
}
.mss-wrapper .v-chip__content{
    white-space:normal;
}
.mss-wrapper .v-btn-group{        
    width:max-content; 
    border-color:rgba(var(--v-border-color),.3); 
}
.mss-wrapper .v-btn--variant-elevated, .mss-wrapper .v-btn--variant-outlined{    
   color:#cccccc;
}
.mss-wrapper .icon-only .v-btn__prepend{
    margin-inline:0;
}

EDIT - made label also optional.

image

3 Likes

Morning @hotNipi,
After a short night of sleep, I am not really convinced whether that is a good option.
Because in our multistate-switch ui node for D1, we support these value types:

image

So if we now consider numeric input als indices, we cannot support numeric values anymore.
Therefore my proposal is to support these options again...
Thoughts?

Aha. Didn't remember that. But anyway -you have at least example of validation strategy to follow.

1 Like

Very open to this being made into a core widget - issue opened to document

2 Likes

As a short term - also happy for this to be added as a nice ui-template example into the documentation too

1 Like