[Release] Tasmota Sonoff Hubitat Driver & Device Support

As you used the new firmware you maybe able to try the fast power recovery.

1 Like

Had this happen to me, how did you flash? OTA or using the pins on the board? What I did was reflash the device using the pins again, then connect to Sonoff wifi and setup my wifi details. Also once I have setup my wifi details, I power off the device and then power on again.

Did you solve this? I'm running this on 12+ devices now on the firmware I released, some of them Sonoff, though none Sonoff TH. If you need any further assistance, please just ask

I tried the suggestion of power cycling it four times but that didn’t work.
I’m not sure what else to try.

If the Power Cycle reset truly doesn't work, the other option would be to flash it again through TTL, a guide can be found here:

It can also be done without soldering, but that requires some pogopins to be reliable.

Hello, I'm using several basics and minis without any issue (thanks Ericm!), but got tons of lines from my devices in my log. I have tried to disable them in Hubitat and in the webui, but I still get lots of them. Is there a way to completely disable logs?

@ericm Hi does anybody have a full feature driver for the Sonoff POW R2. I am currently using the S31 which gives Voltage, Current, Power. If you look in the Tasmota software console you have Power Today, Power Yesterday and Power Total as well as Power Factor and Reactive and Apparent Power. Not too worried about the Power factor etc but would love to have the Power today and yesterday as well as the power Totals.

The strange thing is all the info is in the logs plus some strange headers and body code, so why cant this info (Energy Today, Yesterday etc) be seen under the device and bee shown on the dashboard. Log entry below

dev:1392019-12-04 21:55:20.738 debugresult: [Time:2019-12-04T21:55:20, ENERGY:[TotalStartTime:2019-12-04T19:42:27, Total:0.003, Yesterday:0.000, Today:0.003, Period:0, Power:10, ApparentPower:15, ReactivePower:12, Factor:0.62, Voltage:223, Current:0.069]]

dev:1392019-12-04 21:55:20.733 debugParsing: mac:********, ip:c0a8****, port:ce6f, headers:UE9TVCAvIEhUVFAvMS4xDQpDb25uZWN0aW9uOiBjbG9zZQ0KSG9zdDogMTkyLjE2OC4wLjEwOjM5NTAxDQpDb250ZW50LUxlbmd0aDogMjMwDQpTZXJ2ZXI6IFNvbm9mZg0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uO2NoYXJzZXQ9VVRGLTgNCg==, body:eyJUaW1lIjoiMjAxOS0xMi0wNFQyMTo1NToyMCIsIkVORVJHWSI6eyJUb3RhbFN0YXJ0VGltZSI6IjIwMTktMTItMDRUMTk6NDI6MjciLCJUb3RhbCI6MC4wMDMsIlllc3RlcmRheSI6MC4wMDAsIlRvZGF5IjowLjAwMywiUGVyaW9kIjowLCJQb3dlciI6MTAsIkFwcGFyZW50UG93ZXIiOjE1LCJSZWFjdGl2ZVBvd2VyIjoxMiwiRmFjdG9yIjowLjYyLCJWb2x0YWdlIjoyMjMsIkN1cnJlbnQiOjAuMDY5fX0=

dev:1392019-12-04 21:55:20.675 debugresult: [Time:2019-12-04T21:55:20, Uptime:0T00:26:03, Heap:23, SleepMode:Dynamic, Sleep:50, LoadAvg:19, POWER:ON, Wifi:[AP:1, SSId:*******, BSSId:FC:EC:DA:E6:F7:**, Channel:4, RSSI:92, LinkCount:1, Downtime:0T00:00:04]]

I have one of the Sonoff POW R2 as well, if no one has a driver already written, I'll try to find time soon.

That would be awesome, I'm using the S31 driver as well.

SirReal

@markus
Hi Markus , that would be fantastic if you could. Not trying to be pushy :slight_smile: But would be nice to have the Voltage, Current, Wattage (current power) as well as the total power for today, total for yesterday and then the total used. I thank you for the effort from this community.

I'm using this with some tuya flashed plugs which works but only refreshes every minute as it is for the non-forked firmware.
image

2 Likes

I've created a driver, not yet put it on GitHub since I want to put it there with an app to install it, but this should work to install as a Virtual Device, or you can first install with a different Sonoff driver and switch to this one, just make sure to put the MAC address as Device Network ID if you add this manually without the app. Do note I have only tested this with my version of Tasmota for Hubitat, but it should work with the older version as well:

/**
 *  Copyright 2019 Markus Liljergren
 *  Copyright 2019 Eric Maycock
 *
 *  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.
 *
 *  Sonoff PowR2 - Tasmota
 *
 *  Author: Markus Liljergren (markus-li)
 *  Date: 2019-12-06 
 *
 *  Original Author: Eric Maycock (erocm123)
 *  Date: 2019-05-10
 *
 *
 */
 
import groovy.json.JsonSlurper

metadata {
	definition (name: "Sonoff PowR2 - Tasmota", namespace: "tasmota", author: "Markus Liljergren", vid:"generic-switch-power-energy") {
        capability "Actuator"
		capability "Switch"
		capability "Refresh"
		capability "Sensor"
        capability "Configuration"
        capability "HealthCheck"
        capability "Voltage Measurement"
		capability "Power Meter"
        capability "Energy Meter"
        
        attribute   "current", "string"
        attribute   "apparentPower", "string"
        attribute   "reactivePower", "string"
        attribute   "powerFactor", "string"
        attribute   "energyToday", "string"
        attribute   "energyYesterday", "string"
        attribute   "energyTotal", "string"
        attribute   "needUpdate", "string"
        attribute   "uptime", "string"
        attribute   "ip", "string"
        
        command "reboot"
	}

	simulator {
	}
    
    preferences {
        input description: "Once you change values on this page, the corner of the \"configuration\" icon will change to orange until all configuration parameters are updated.", title: "Settings", displayDuringSetup: false, type: "paragraph", element: "paragraph"
        input(name: "override", type: "bool", title: "Override IP", description: "Override the automatically discovered IP address?", displayDuringSetup: true, required: false)
        input(name: "ipAddress", type: "string", title: "IP Address", description: "IP Address of Sonoff", displayDuringSetup: true, required: false)
        input(name: "telePeriod", type: "string", title: "Update Frequency", description: "Tasmota update sensor value update interval, set this to any value between 10 and 3600 seconds. (default = 300)", displayDuringSetup: true, required: false)
		input(name: "port", type: "number", title: "Port", description: "Port", displayDuringSetup: true, required: false, defaultValue: 80)
		generate_preferences(configuration_model())
	}

	main(["switch"])
	details(["switch", 
             "refresh","configure","reboot",
             "ip", "uptime"])
}

def installed() {
	log.debug "installed()"
	configure()
}

def configure() {
    logging("configure()", 1)
    def cmds = []
    cmds = update_needed_settings()
    if (cmds != []) cmds
}

def updated()
{
    logging("updated()", 1)
    def cmds = [] 
    cmds = update_needed_settings()
    sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID])
    sendEvent(name:"needUpdate", value: device.currentValue("needUpdate"), displayed:false, isStateChange: true)
    logging(cmds,1)
    if (cmds != [] && cmds != null) cmds
}

private def logging(message, level) {
    if (logLevel != "0"){
    switch (logLevel) {
       case "1":
          if (level > 1)
             log.debug "$message"
       break
       case "99":
          log.debug "$message"
       break
    }
    }
}

def parse(description) {
	//log.debug "Parsing: ${description}"
    def events = []
    def descMap = parseDescriptionAsMap(description)
    def body
    //log.debug "descMap: ${descMap}"

    if (!state.mac || state.mac != descMap["mac"]) {
		logging("Mac address of device found ${descMap["mac"]}",1)
		state.mac = descMap["mac"]
	}
    
    if (state.mac != null && state.dni != state.mac) state.dni = setDeviceNetworkId(state.mac)
    if (descMap["body"] && descMap["body"] != "T04=") body = new String(descMap["body"].decodeBase64())

    if (body && body != "") {
    
    if(body.startsWith("{") || body.startsWith("[")) {
    logging("========== Parsing Report ==========",99)
    def slurper = new JsonSlurper()
    def result = slurper.parseText(body)
    
    logging("result: ${result}",1)
    

    if (result.containsKey("POWER")) {
        logging("POWER: $result.POWER",99)
        events << createEvent(name: "switch", value: result.POWER.toLowerCase())
    }
    if (result.containsKey("LoadAvg")) {
        logging("LoadAvg: $result.LoadAvg",99)
    }
    if (result.containsKey("Sleep")) {
        logging("Sleep: $result.Sleep",99)
    }
    if (result.containsKey("SleepMode")) {
        logging("SleepMode: $result.SleepMode",99)
    }
    if (result.containsKey("Vcc")) {
        logging("Vcc: $result.Vcc",99)
    }
    if (result.containsKey("Wifi")) {
        if (result.Wifi.containsKey("AP")) {
            logging("AP: $result.Wifi.AP",99)
        }
        if (result.Wifi.containsKey("BSSId")) {
            logging("BSSId: $result.Wifi.BSSId",99)
        }
        if (result.Wifi.containsKey("Channel")) {
            logging("Channel: $result.Wifi.Channel",99)
        }
        if (result.Wifi.containsKey("RSSI")) {
            logging("RSSI: $result.Wifi.RSSI",99)
        }
        if (result.Wifi.containsKey("SSId")) {
            logging("SSId: $result.Wifi.SSId",99)
        }
    }
    if (result.containsKey("StatusSNS")) {
        if (result.StatusSNS.containsKey("ENERGY")) {
            if (result.StatusSNS.ENERGY.containsKey("Total")) {
                logging("Total: $result.StatusSNS.ENERGY.Total kWh",99)
                events << createEvent(name: "energyTotal", value: "$result.StatusSNS.ENERGY.Total kWh")
            }
            if (result.StatusSNS.ENERGY.containsKey("Today")) {
                logging("Today: $result.StatusSNS.ENERGY.Today kWh",99)
                events << createEvent(name: "energyToday", value: "$result.StatusSNS.ENERGY.Today kWh")
            }
            if (result.StatusSNS.ENERGY.containsKey("Yesterday")) {
                logging("Yesterday: $result.StatusSNS.ENERGY.Yesterday kWh",99)
                events << createEvent(name: "energyYesterday", value: "$result.StatusSNS.ENERGY.Yesterday kWh")
            }
            if (result.StatusSNS.ENERGY.containsKey("Current")) {
                logging("Current: $result.StatusSNS.ENERGY.Current A",99)
                events << createEvent(name: "current", value: "$result.StatusSNS.ENERGY.Current A")
            }
            if (result.StatusSNS.ENERGY.containsKey("ApparentPower")) {
                logging("apparentPower: $result.StatusSNS.ENERGY.ApparentPower VA",99)
                events << createEvent(name: "apparentPower", value: "$result.StatusSNS.ENERGY.ApparentPower VA")
            }
            if (result.StatusSNS.ENERGY.containsKey("ReactivePower")) {
                logging("reactivePower: $result.StatusSNS.ENERGY.ReactivePower VAr",99)
                events << createEvent(name: "reactivePower", value: "$result.StatusSNS.ENERGY.ReactivePower VAr")
            }
            if (result.StatusSNS.ENERGY.containsKey("Factor")) {
                logging("powerFactor: $result.StatusSNS.ENERGY.Factor",99)
                events << createEvent(name: "powerFactor", value: "$result.StatusSNS.ENERGY.Factor")
            }
            if (result.StatusSNS.ENERGY.containsKey("Voltage")) {
                logging("Voltage: $result.StatusSNS.ENERGY.Voltage V",99)
                events << createEvent(name: "voltage", value: "$result.StatusSNS.ENERGY.Voltage V")
            }
            if (result.StatusSNS.ENERGY.containsKey("Power")) {
                logging("Power: $result.StatusSNS.ENERGY.Power W",99)
                events << createEvent(name: "power", value: "$result.StatusSNS.ENERGY.Power W")
            }
        }
    }
    if (result.containsKey("ENERGY")) {
        logging("Has ENERGY...", 1)
        if (result.ENERGY.containsKey("Total")) {
            logging("Total: $result.ENERGY.Total kWh",99)
            events << createEvent(name: "energyTotal", value: "$result.ENERGY.Total kWh")
        }
        if (result.ENERGY.containsKey("Today")) {
            logging("Today: $result.ENERGY.Today kWh",99)
            events << createEvent(name: "energyToday", value: "$result.ENERGY.Today kWh")
        }
        if (result.ENERGY.containsKey("Yesterday")) {
            logging("Yesterday: $result.ENERGY.Yesterday kWh",99)
            events << createEvent(name: "energyYesterday", value: "$result.ENERGY.Yesterday kWh")
        }
        if (result.ENERGY.containsKey("Current")) {
            logging("Current: $result.ENERGY.Current A",99)
            events << createEvent(name: "current", value: "$result.ENERGY.Current A")
        }
        if (result.ENERGY.containsKey("ApparentPower")) {
            logging("apparentPower: $result.ENERGY.ApparentPower VA",99)
            events << createEvent(name: "apparentPower", value: "$result.ENERGY.ApparentPower VA")
        }
        if (result.ENERGY.containsKey("ReactivePower")) {
            logging("reactivePower: $result.ENERGY.ReactivePower VAr",99)
            events << createEvent(name: "reactivePower", value: "$result.ENERGY.ReactivePower VAr")
        }
        if (result.ENERGY.containsKey("Factor")) {
            logging("powerFactor: $result.ENERGY.Factor",99)
            events << createEvent(name: "powerFactor", value: "$result.ENERGY.Factor")
        }
        if (result.ENERGY.containsKey("Voltage")) {
            logging("Voltage: $result.ENERGY.Voltage V",99)
            events << createEvent(name: "voltage", value: "$result.ENERGY.Voltage V")
        }
        if (result.ENERGY.containsKey("Power")) {
            logging("Power: $result.ENERGY.Power W",99)
            events << createEvent(name: "power", value: "$result.ENERGY.Power W")
        }
    }
    // StatusPTH:[PowerDelta:0, PowerLow:0, PowerHigh:0, VoltageLow:0, VoltageHigh:0, CurrentLow:0, CurrentHigh:0]
    if (result.containsKey("Hostname")) {
        logging("Hostname: $result.Hostname",99)
    }
    if (result.containsKey("IPAddress")) {
        logging("IPAddress: $result.IPAddress",99)
    }
    if (result.containsKey("WebServerMode")) {
        logging("WebServerMode: $result.WebServerMode",99)
    }
    if (result.containsKey("Version")) {
        logging("Version: $result.Version",99)
    }
    if (result.containsKey("Module")) {
        logging("Module: $result.Module",99)
    }
    if (result.containsKey("RestartReason")) {
        logging("RestartReason: $result.RestartReason",99)
    }
    if (result.containsKey("SetOption81")) {
        logging("SetOption81: $result.SetOption81",99)
    }
    if (result.containsKey("Uptime")) {
        logging("Uptime: $result.Uptime",99)
        events << createEvent(name: 'uptime', value: result.Uptime, displayed: false)
    }
    } else {
        //log.debug "Response is not JSON: $body"
    }
    }
    
    if (!device.currentValue("ip") || (device.currentValue("ip") != getDataValue("ip"))) events << createEvent(name: 'ip', value: getDataValue("ip"))
    
    return events
}

def getCommandString(command, value) {
    def uri = "/cm?"
    if (password) {
        uri += "user=admin&password=${password}&"
    }
	if (value) {
		uri += "cmnd=${command}%20${value}"
	}
	else {
		uri += "cmnd=${command}"
	}
    return uri
}

def parseDescriptionAsMap(description) {
	description.split(",").inject([:]) { map, param ->
		def nameAndValue = param.split(":")
        
        if (nameAndValue.length == 2) map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
        else map += [(nameAndValue[0].trim()):""]
	}
}


def on() {
	log.debug "on()"
    def cmds = []
    cmds << getAction(getCommandString("Power", "On"))
    return cmds
}

def off() {
    log.debug "off()"
	def cmds = []
    cmds << getAction(getCommandString("Power", "Off"))
    return cmds
}

def refresh() {
	log.debug "refresh()"
    def cmds = []
    cmds << getAction(getCommandString("Status", "0"))
    return cmds
}

def ping() {
    log.debug "ping()"
    refresh()
}

private getAction(uri){ 
  updateDNI()
    
  def headers = getHeader()

  def hubAction = new hubitat.device.HubAction(
    method: "GET",
    path: uri,
    headers: headers
  )
  return hubAction    
}

private postAction(uri, data){ 
  updateDNI()
  
  def headers = getHeader()
  
  def hubAction = new hubitat.device.HubAction(
    method: "POST",
    path: uri,
    headers: headers,
    body: data
  )
  return hubAction    
}

private setDeviceNetworkId(ip, port = null){
    def myDNI
    if (port == null) {
        myDNI = ip
    } else {
  	    def iphex = convertIPtoHex(ip)
  	    def porthex = convertPortToHex(port)
        myDNI = "$iphex:$porthex"
    }
    log.debug "Device Network Id set to ${myDNI}"
    return myDNI
}

private updateDNI() { 
    if (state.dni != null && state.dni != "" && device.deviceNetworkId != state.dni) {
       device.deviceNetworkId = state.dni
    }
}

private getHostAddress() {
    if (override == true && ipAddress != null && port != null){
        return "${ipAddress}:${port}"
    }
    else if(getDeviceDataByName("ip") && getDeviceDataByName("port")){
        return "${getDeviceDataByName("ip")}:${getDeviceDataByName("port")}"
    }else{
	    return "${ip}:80"
    }
}

private String convertIPtoHex(ipAddress) { 
    String hex = ipAddress.tokenize( '.' ).collect {  String.format( '%02x', it.toInteger() ) }.join()
    return hex
}

private String convertPortToHex(port) {
	String hexport = port.toString().format( '%04x', port.toInteger() )
    return hexport
}

private encodeCredentials(username, password){
	def userpassascii = "${username}:${password}"
    def userpass = "Basic " + userpassascii.bytes.encodeBase64().toString()
    return userpass
}

private getHeader(userpass = null){
    def headers = [:]
    headers.put("Host", getHostAddress())
    headers.put("Content-Type", "application/x-www-form-urlencoded")
    if (userpass != null)
       headers.put("Authorization", userpass)
    return headers
}

def reboot() {
	log.debug "reboot()"
    getAction(getCommandString("Restart", "1"))
}

def sync(ip, port) {
    def existingIp = getDataValue("ip")
    def existingPort = getDataValue("port")
    if (ip && ip != existingIp) {
        updateDataValue("ip", ip)
        sendEvent(name: 'ip', value: ip)
    }
    if (port && port != existingPort) {
        updateDataValue("port", port)
    }
}


def generate_preferences(configuration_model)
{
    def configuration = new XmlSlurper().parseText(configuration_model)
   
    configuration.Value.each
    {
        if(it.@hidden != "true" && it.@disabled != "true"){
        switch(it.@type)
        {   
            case ["number"]:
                input "${it.@index}", "number",
                    title:"${it.@label}\n" + "${it.Help}",
                    range: "${it.@min}..${it.@max}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
            case "list":
                def items = []
                it.Item.each { items << ["${it.@value}":"${it.@label}"] }
                input "${it.@index}", "enum",
                    title:"${it.@label}\n" + "${it.Help}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}",
                    options: items
            break
            case ["password"]:
                input "${it.@index}", "password",
                    title:"${it.@label}\n" + "${it.Help}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
            case "decimal":
               input "${it.@index}", "decimal",
                    title:"${it.@label}\n" + "${it.Help}",
                    range: "${it.@min}..${it.@max}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
            case "boolean":
               input "${it.@index}", "boolean",
                    title:"${it.@label}\n" + "${it.Help}",
                    defaultValue: "${it.@value}",
                    displayDuringSetup: "${it.@displayDuringSetup}"
            break
        }
        }
    }
}

 /*  Code has elements from other community source @CyrilPeponnet (Z-Wave Parameter Sync). */

def update_current_properties(cmd)
{
    def currentProperties = state.currentProperties ?: [:]
    currentProperties."${cmd.name}" = cmd.value

    if (state.settings?."${cmd.name}" != null)
    {
        if (state.settings."${cmd.name}".toString() == cmd.value)
        {
            sendEvent(name:"needUpdate", value:"NO", displayed:false, isStateChange: true)
        }
        else
        {
            sendEvent(name:"needUpdate", value:"YES", displayed:false, isStateChange: true)
        }
    }
    state.currentProperties = currentProperties
}


def update_needed_settings()
{
    def cmds = []
    def currentProperties = state.currentProperties ?: [:]
    
    state.settings = settings
     
    def configuration = new XmlSlurper().parseText(configuration_model())
    def isUpdateNeeded = "NO"
    
    cmds << getAction(getCommandString("SetOption81", "1"))
    cmds << getAction(getCommandString("LedPower", "1"))
    cmds << getAction(getCommandString("LedState", "8"))
    
    def telePeriodCmd = getCommandString("TelePeriod", (telePeriod == '' || telePeriod == null ? "300" : telePeriod))
    //logging("Teleperiod: " +telePeriodCmd,1)
    cmds << getAction(telePeriodCmd)
    cmds << getAction(getCommandString("HubitatHost", device.hub.getDataValue("localIP")))
    cmds << getAction(getCommandString("HubitatPort", device.hub.getDataValue("localSrvPortTCP")))
    
    configuration.Value.each
    {     
        if ("${it.@setting_type}" == "lan" && it.@disabled != "true"){
            if (currentProperties."${it.@index}" == null)
            {
               if (it.@setonly == "true"){
                  logging("Setting ${it.@index} will be updated to ${it.@value}", 2)
                  cmds << getAction("/configSet?name=${it.@index}&value=${it.@value}")
               } else {
                  isUpdateNeeded = "YES"
                  logging("Current value of setting ${it.@index} is unknown", 2)
                  cmds << getAction("/configGet?name=${it.@index}")
               }
            }
            else if ((settings."${it.@index}" != null || it.@hidden == "true") && currentProperties."${it.@index}" != (settings."${it.@index}" != null? settings."${it.@index}".toString() : "${it.@value}"))
            { 
                isUpdateNeeded = "YES"
                logging("Setting ${it.@index} will be updated to ${settings."${it.@index}"}", 2)
                cmds << getAction("/configSet?name=${it.@index}&value=${settings."${it.@index}"}")
            } 
        }
        
    }
    //logging("Cmds: " +cmds,1)
    sendEvent(name:"needUpdate", value: isUpdateNeeded, displayed:false, isStateChange: true)
    return cmds
}

def configuration_model()
{
'''
<configuration>
<Value type="password" byteSize="1" index="password" label="Password" min="" max="" value="" setting_type="preference" fw="">
<Help>
</Help>
</Value>
<Value type="list" index="logLevel" label="Debug Logging Level?" value="0" setting_type="preference" fw="">
<Help>
</Help>
    <Item label="None" value="0" />
    <Item label="Reports" value="1" />
    <Item label="All" value="99" />
</Value>
</configuration>
'''
}
4 Likes

@markus
Thank you very much Sir. It seems to be working fantastically well. What I wanted to ask you is where does the dashboard read the unit value from. Reason I ask is that when I select other Sonoff devices and add them to the dashboard as an attribute it does not show the units (just as an example I will select current but it wont show the A, just a number. I was thinking i could just add this to the driver code if it is not to difficult?

Once again thank you very much for your efforts.

You're welcome, glad it works. The units are added as part of the value saved, I save them as a string to have the unit included. All you would have to do is change how it is saved in the driver.

1 Like

@markus
Would you have an example of what I would need to look for and what I would change? Sorry to be a pain. Is is possible to add text to a dashboard tile so for example, I have added your "EnergyToday" tile and the "Total Energy" tile, but on the dashboard they don't have a "label" they look eaxctly the same just 2 different numbers. I added them as an attribute tile.

Look for these two things, first in metadata, you need the attribute, it should be string to display the unit:
attribute "energyToday", "string"

Then look for where it is set in the code, most likely in parse() with a createEvent/sendEvent:
events << createEvent(name: "energyToday", value: "$result.StatusSNS.ENERGY.Today kWh")c

The above reads the value of $result.StatusSNS.ENERGY.Today and also add kWh to the end.

For the tile, you have probably enabled "Hide Tile Template Names" for the Dashboard. You can even customize the Title displayed with some CSS trickery:
#tile-26 .tile-secondary {
visibility: hidden;
}
#tile-26 .tile-secondary:after {
content: 'My Title in 26';
visibility: visible;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

For further discussion about these things we should probably start a new topic... If you create one I'll look for it tomorrow, now I'm going to sleep, it's 2:43 in the morning here...

1 Like

Thanks @markus, driver seems to work great so far. Really appreciate it.

Does this require you to poll the devices to get the data or does it send it? Does it also get changes if the power is toggled on the device (and reflect the proper status in HE?). Thanks, would love to have power accurately reflected!

This does not poll the devices, the status is pushed from the device. How often the the device pushes the data is controlled by the update frequency setting (this setting sends a command to the device to change update frequency, it is not a poll frequency, see the Tasmota TelePeriod command).
The power state is reflected immediately (as soon as hubitat reacts to the call from the device at least). This is not dependant on any update frequency. It is pushed.

1 Like

Ahh, I guess I need to find some plug-in outlets that support templates that do monitoring. None of mine have those enabled via templates.