Tasmota Button and Timer in Dashboard 2

Hello guys,
I've started the migration from DB1 to DB2 in these days, and I want to share with you two simple templates I've created to interact with my Tasmotas.

[{"id":"64fe8684fcebe813","type":"mqtt in","z":"e6bc0b05cf1eec9b","name":"","topic":"stat/TASMOTA/RESULT","qos":"2","datatype":"auto-detect","broker":"a191294b6458a69f","nl":false,"rap":true,"rh":0,"inputs":0,"x":160,"y":160,"wires":[["ad63327ea5a9623c","3d68d24a2f298c61"]]},{"id":"ad63327ea5a9623c","type":"ui-template","z":"e6bc0b05cf1eec9b","group":"579ca770cee73954","page":"","ui":"","name":"Tasmota Toggle Button","order":1,"width":"4","height":"1","head":"","format":"<style>\n    .single_btn_container {\n        display: grid;\n        grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;\n        grid-template-rows: 1fr;\n        gap: 6px 6px;\n        grid-auto-flow: row;\n        grid-template-areas: \"btn_1 btn_1 btn_1 btn_1 btn_1 btn_1 btn_1 btn_1 btn_1 btn_1\"\n    }\n\n    .btn_1 {\n        grid-area: btn_1;\n\n    }\n\n</style>\n\n\n<template>\n<div class=\"single_btn_container\">\n    <div @click=\"btn_click()\" :class=\"[btn_base_class, btn_status_class]\">\n        <div class=\"hv_centered\">{{ btn_status }}</div>\n    </div>\n</div>\n</template>\n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n                this_tasmota: 'TASMOTA', // Tasmota topic\n                power_outlet: 'POWER', // The output to be triggered\n                btn_status: 'OFFLINE',\n                btn_base_class: 'btn_1 db2_button hv_centered',\n                btn_status_class: 'db2_button_offline',\n            }\n        },\n        watch: {\n            // watch for any changes of msg\n            msg: function () {\n                // Reload status\n                if(this.msg.payload == 'connect'){\n\n                }\n                // Update status\n                else if(this.power_outlet in this.msg.payload){\n                    this.btn_status = this.msg.payload[this.power_outlet]\n                }   \n            },\n            btn_status: function(){\n                if(this.btn_status == 'ON'){\n                    this.btn_status_class = 'db2_button_on'\n                }\n                else if(this.btn_status == 'OFF'){\n                    this.btn_status_class = 'db2_button_off'\n                }\n                else if(this.btn_status == 'OFFLINE'){\n                    this.btn_status_class = 'db2_button_offline'\n\n                }\n            }\n        },\n        computed: {},\n        methods: {\n            // expose a method to our <template> and Vue Application\n            refresh_tasmota: function(){\n                var msg2 = {topic: 'cmnd/' + this.this_tasmota + '/' + this.power_outlet,\n                payload: '' }\n                this.send(msg2)\n            },\n            btn_click: function () {\n                if(this.btn_status != 'OFFLINE'){\n                    this.btn_status_class = 'db2_button_trans'\n                    this.btn_status = '> > >'\n                    var msg2 = {topic: 'cmnd/' + this.this_tasmota + '/' + this.power_outlet, payload: 'toggle' }\n                    this.send(msg2)\n                }\n                else if(this.btn_status == 'OFFLINE')\n                {\n                    var msg2 = {topic: 'cmnd/' + this.this_tasmota + '/' + this.power_outlet, \n                    payload: '' }\n                    this.send(msg2)\n                }\n            }\n        },\n        mounted()\n            {\n                if(this.btn_status == 'OFFLINE'){\n                    this.refresh_tasmota()\n                }\n        }, \n        unmounted() {}\n    }\n</script>\n","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":430,"y":160,"wires":[["4704bd317e29f576"]]},{"id":"3d68d24a2f298c61","type":"ui-template","z":"e6bc0b05cf1eec9b","group":"579ca770cee73954","page":"","ui":"","name":"Tasmota Timer","order":2,"width":"4","height":"1","head":"","format":"<style>\n\n    .single_timer_container {\n        display: grid;\n        grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;\n        grid-template-rows: 1fr;\n        gap: 6px 6px;\n        grid-auto-flow: row;\n        grid-template-areas:\n            \"switch_1 switch_1 timer_on timer_on timer_on timer_on timer_off timer_off timer_off timer_off\";\n    }\n\n    .switch_1 {\n        grid-area: switch_1;\n        width: 100% !important;\n        border: 1px solid black;\n        border-radius: 8px;\n        background-color: #363636;\n    }\n\n    .timer_on {\n        grid-area: timer_on;\n        width: 100% !important;\n        border: 1px solid black;\n        border-radius: 8px;\n    }\n\n    .timer_off {\n        grid-area: timer_off;\n        width: 100% !important;\n        border: 1px solid black;\n        border-radius: 8px;\n    }\n\n.hv_centered{\n    display: flex; \n    justify-content: center; \n    align-items: center; \n    height:100%\n}\n\n.v-field__outline\n{\n    display: none !important; \n}\n\n.v-text-field .v-input__details{\n    display:none;\n}\n\n.v-text-field .v-field--no-label input, .v-text-field .v-field--active input{\n    padding-top:4px;\n    padding-bottom:4px;\n    font-size: 1.5rem;\n}\n\n.v-selection-control__wrapper{\n    width: 100% !important;\n    place-self: center;\n}\n\n.centered{\n    width: 100%;\n    place-self: center;\n}\n\n</style>\n\n<template>\n<div class=\"single_timer_container\">\n    <!-- Switch -->\n    <div class=\"switch_1\">\n       <v-switch color=\"error\" @change=\"switch_change()\" v-model=\"power_switch\" hide-details inset class=\"centered\"\n            hide-details inset color=\"primary\"></v-switch>\n    </div>\n\n    <!--Timer on -->\n    <div class=\"timer_on db2_rounded_clear\">\n        <v-text-field @change=\"time_on_update()\" class=\"centered\" v-model=\"vtime_on\" class=\"timer\" type=\"time\"></v-text-field>\n    </div>\n    <!--Timer off -->\n    <div class=\"timer_off db2_rounded_clear\">\n        <v-text-field @change=\"time_on_update()\" class=\"centered\" v-model=\"vtime_off\" class=\"timer\" type=\"time\"></v-text-field>\n    </div>\n</div>\n</template>\n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n                this_tasmota: 'TASMOTA', // Tasmota topic\n                out1: 'Timer1', // Timer used for on operation\n                out2: 'Timer2', // Timer used for off operation\n                output: 1, // The actual output triggered\n                power_switch: false,\n                vtime_on: null,\n                vtime_off: null\n            }\n        },\n        watch: {\n            // watch for any changes of msg\n            msg: function () {\n                // Reload status\n                if(this.msg.payload == 'connect'){\n//                    var msg2 = {topic: 'cmnd/' + this.this_tasmota + '/Timers', payload: '' }\n//                    this.send(msg2)\n                    }\n\n                // Update status\n                else if('Timers' in this.msg.payload){\n                    this.vtime_on = this.msg.payload[this.out1]['Time']\n                    this.vtime_off = this.msg.payload[this.out2]['Time']\n\n                    if(this.msg.payload[this.out1]['Enable'] == 1){\n                        this.power_switch = true\n                    }\n                    else if(this.msg.payload[this.out1]['Enable'] == 0){\n                        this.power_switch = false\n                    }\n                }\n            }\n        },\n        computed: {\n            // automatically compute this variable\n            // whenever VueJS deems appropriate\n        },\n        methods: {\n            // expose a method to our <template> and Vue Application\n            refresh_tasmota: function(){\n                var msg2 = {topic: 'cmnd/' + this.this_tasmota + '/Timers', payload: '' }\n                this.send(msg2)\n            },\n            btn_click: function () {\n                var msg2 = {topic:'power_status', payload: 'toggle' }\n                this.send(msg2)\n\n                this.btn_status_class = 'md-button_trans'\n                if((this.power_status) == 'ON'){\n                    this.btn_status = 'ON >> OFF'\n                }\n                else if ((this.power_status) == 'OFF'){\n                    this.btn_status = 'OFF >> ON'\n                }\n            },\n            time_on_update: function()\n            {\n                var msg2 = {topic:'cmnd/' + this.this_tasmota + \"/\" + this.out1, payload: {'Time':this.vtime_on}}\n                this.send(msg2)\n                this.refresh_tasmota()\n            },\n            time_off_update: function()\n            {\n                var msg2 = {topic:'cmnd/' + this.this_tasmota + \"/\" + this.out2, payload: {'Time':this.vtime_off}}\n                this.send(msg2)\n                this.refresh_tasmota()\n            },\n            switch_change: function()\n            {\n\n                var switch_val\n                if(this.power_switch){switch_val = 1}\n                else if(!this.power_switch){switch_val = 0}\n\n                var msg2 = {topic:'cmnd/' + this.this_tasmota + \"/\" + this.out1, \n                            payload: {'Enable': switch_val}}                            \n                \n                this.send(msg2)\n                \n                var msg2 = {topic:'cmnd/' + this.this_tasmota + \"/\" + this.out2, \n                            payload: {'Enable': switch_val}}\n                \n                this.send(msg2)\n                this.refresh_tasmota()\n            },\n        },\n        mounted() {\n            // code here when the component is first loaded\n            this.refresh_tasmota()\n\n        },\n        unmounted() {\n            // code here when the component is removed from the Dashboard\n            // i.e. when the user navigates away from the page\n        }\n    }\n</script>\n","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":420,"y":220,"wires":[["4704bd317e29f576"]]},{"id":"4704bd317e29f576","type":"mqtt out","z":"e6bc0b05cf1eec9b","name":"","topic":"","qos":"1","retain":"true","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"be1a44bb.690768","x":650,"y":160,"wires":[]},{"id":"6b704c821345e461","type":"ui-template","z":"e6bc0b05cf1eec9b","group":"","page":"","ui":"43e57a187b2ebc1f","name":"BTN CSS","order":0,"width":0,"height":0,"head":"","format":"<style>\n\n    .db2_button {\n        margin: 1px;\n        padding: 0px;\n        height: 100%;\n        font-size: 1.5rem;\n        font-weight: bold;\n        border: 1px solid black;\n\n        color: #202020;\n        border-radius: 12px;\n\n        -webkit-user-select: none;\n        /* Safari */\n        -moz-user-select: none;\n        /* Firefox */\n        -ms-user-select: none;\n        /* IE10+/Edge */\n        user-select: none;\n        /* Standard */\n    }\n\n    .db2_button_off {\n        background-image: linear-gradient(rgb(0, 200, 0), rgb(0, 100, 0));\n        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2), inset 0 -2px 4px rgba(0, 0, 0, 0.5);\n        color: #111111 !important;\n        transition: transform 0.2s ease, box-shadow 0.2s ease;\n    }\n\n    .db2_button_on {\n        background-image: linear-gradient(rgb(240, 0, 0), rgb(120, 0, 0));\n        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2), inset 0 -2px 4px rgba(0, 0, 0, 0.5);\n        color: #111111 !important;\n        transition: transform 0.2s ease, box-shadow 0.2s ease;\n    }\n\n    .db2_button_offline {\n        background-image: linear-gradient(rgb(140, 140, 140), rgb(40, 40, 40));\n        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2), inset 0 -2px 4px rgba(0, 0, 0, 0.5);\n        color: #111111 !important;\n        transition: transform 0.2s ease, box-shadow 0.2s ease;\n    }\n\n\n    .db2_button_trans {\n        background-image: linear-gradient(rgb(200, 200, 200), rgb(100, 100, 100));\n        color: #111111 !important;\n    }\n\n    .centered {\n        width: 100%;\n        place-self: center;\n    }\n</style>\n","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"widget:ui","className":"","x":120,"y":120,"wires":[[]]},{"id":"641ce0395de2921f","type":"ui-template","z":"e6bc0b05cf1eec9b","group":"","page":"","ui":"43e57a187b2ebc1f","name":"Dashboard CSS","order":0,"width":0,"height":0,"head":"","format":"<style>\n.v-card-title{\n    font-family: Roboto, \"Helvetica Neue\", sans-serif;\n    font-size: 1.5rem;\n    color: #5DADE2;\n}\n\n.{\n    font-family: Roboto, \"Helvetica Neue\", sans-serif;\n}\n\n.hv_centered {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    height: 100%\n}\n</style>\n","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"widget:ui","className":"","x":140,"y":80,"wires":[[]]},{"id":"a191294b6458a69f","type":"mqtt-broker","name":"","broker":"localhost","port":1883,"clientid":"","autoConnect":true,"usetls":false,"protocolVersion":4,"keepalive":60,"cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthRetain":"false","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closeRetain":"false","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willRetain":"false","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"579ca770cee73954","type":"ui-group","name":"Button and timer","page":"42d7a8585092a037","width":"4","height":1,"order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"be1a44bb.690768","type":"mqtt-broker","name":"","broker":"mqtt.maxpasseri.casa","port":"1883","clientid":"","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"autoUnsubscribe":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"userProps":"","sessionExpiry":""},{"id":"43e57a187b2ebc1f","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":true,"showPageTitle":false,"navigationStyle":"none","titleBarStyle":"hidden","showReconnectNotification":false,"notificationDisplayTime":"5","showDisconnectNotification":false},{"id":"42d7a8585092a037","type":"ui-page","name":"Page 2","ui":"43e57a187b2ebc1f","path":"/page2","icon":"home","layout":"grid","theme":"f019925b84983e76","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":2,"className":"","visible":"true","disabled":"false"},{"id":"f019925b84983e76","type":"ui-theme","name":"Dark","colors":{"surface":"#303030","primary":"#0094ce","bgPage":"#303030","groupBg":"#303030","groupOutline":"#545454"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"10px","widgetGap":"12px"}}]

I am also finishing a Thermostat template, using the same style, which I'll share once fully tested.
I bet lot of things are wrong in my code, but if you want to try I'll appreciate and if you have suggestion either.
The templates are splittable and stackable into groups as needed.

Screenshot 2025-01-03 alle 10.43.31

And when you click on the time input:

Important notes in order to make them functioning:

Button
It has 4 states, OFFLINE (when first loaded and before Tasmota updates the output), Transitioning (once clicked and before the output triggers), ON, OFF.
Please take care that the dashboard template doesn't modify its state, but always waits the actual output to be triggered, this ensure validity of states across multiple Dashboards.

  • Modify the topic "TASMOTA" in MQTT input node in stat/TASMOTA/RESULT
  • Modify the variables in the template to suit your case:
this_tasmota: 'TASMOTA', // Tasmota topic
power_outlet: 'POWER', // The output to be triggered

Pay attention, with multiple outlet Tasmotas you need to name them Power1, Power2, Power3 and so on, but with one output devices (such as Sonoff mini, as in this case) you need to name it "POWER", because POWER1 is not recognized as output

  • The MQTT command generated will be relative to these two parameters

Timer

  • Modify the variables in the template to suit your case:
this_tasmota: 'TASMOTA', // Tasmota topic
out1: 'Timer1', // Timer used for on operation
out2: 'Timer2', // Timer used for off operation
output: 1, // The actual output triggered

In Tasmota WEBUI modify all the other flags in Timer page to let them function properly (days, repeat, etc..)..or simply inject commands in MQTT to do so.
Be aware that all the datas are stored directly in Tasmota, so the timers will be triggered even with NodeRed shut down or disconnected.

The work in process, with either the Opentherm Thermostat template, looks like this on my iPhone:

Enjoy !!!

3 Likes

You can actually force Tasmota to use the power index

SetOption26 Use indexes even when only one relay is present
0 = messages use POWER (default)
1 = messages use POWER1

2 Likes