[Release] Tasmota 7.x/8.x firmware for Hubitat + Tuya, Sonoff and other drivers

Based on the modifications made by @ericm of Tasmota 6.x and updated to 7.x of the Original Tasmota firmware.

Due to all the current updates to the original Tasmota firmware, I would not recommend to go with 6.x firmwares on new devices, if things work and are stable and you're happy with what you have, you can definitely stay on an older firmware, but my drivers MIGHT not be compatible.
For the 6.x version of the firmware as well as for Hubitat drivers by @ericm, see this thread.

WARNING: Flashing this firmware MAY cause your device to not boot again. To fix this you MAY have to flash through TTL. Read all instructions of the original Tasmota firmware CAREFULLY and don't be in a rush!

My plan is to maintain (irregularly) updated versions of Tasmota with Hubitat support. I run this firmware on all my ESP8266-based devices in my own home (30+ at this moment). Some are Tuya-based others are Sonoff.

Source
The source for the firmware can be found on my GitHub.

Compiled Firmware
Make sure to read the original Tasmota release notes and docs for issues updating from 6.x and earlier as well as which .bin to use for which scenario. The links to the firmwares to use with my drivers are here:
Tasmota Hubitat v7.1.2
Tasmota Hubitat v7.2.0
Tasmota Hubitat v8.1.0

For v7.2.0 and v8.1.0, please read this post for more information.

DO NOT UPGRADE FROM AN OLDER VERSION ÙSING MINIMAL. The Minimal build is only meant for going between different versions of the SAME version number. At least in simplified terms. For Tuya Convert and version upgrades, use Basic first. There's still no guarantee it will work, but it is much more likely. It all depends on from which version to which version you're upgrading. There's a lot of information about this in the Tasmota official docs.
For help sorting out a bad flash, check out the Official Tasmota Docs for Device Recovery.
There's also information about the Migration Path.

Hubitat Drivers Info
THESE DRIVERS ONLY WORK WITH MY MODIFIED VERSION OF TASMOTA, NOT THE OFFICIAL TASMOTA RELEASE.

For my HE drivers using this firmware, please check out my GitHub Repo - Release Branch. See the README and the list of drivers below for more information.

I would be happy to adapt the drivers for other Sonoff, Tuya or other Tasmota-based devices. Please just ask and I'll see what is needed.

All that is needed in the Generic drivers will be the Tasmota Template string or Module number and the rest is auto-configured. This would be the first thing to try if your device doesn't have a specific driver in the repo.

For devices that need more than just a change of Module or Template, it is best to create a new driver. Let me know how things are working and leave me comments on which devices you are using and with which driver. Knowing people are using this keeps me motivated.

NOTE: Any driver without Generic in the name, MAY also set additional settings in Tasmota, such as SetOption81 (see Tasmota Commands). Best is to try the Generic drivers when using your own Template or Module settings.

Installation of Devices/Step-by-step guide
Follow the excellent instructions in the GitHub wiki written by @jchurch.

To install the devices, use Tasmota Connect. NOT Sonoff Connect.

These are the currently available drivers (updated: 2020-01-16):

Generic Drivers
Device Comment Import URL Model Info
Generic Power Monitor Plug RAW
Generic Power Monitor Plug (Child) RAW
Generic Power Monitor Plug (Parent) Multi-relay support RAW
Generic RGB/RGBW Controller/Bulb/Dimmer RGB+WW+CW should all work properly, please report progress RAW
Generic Temperature/Humidity/Pressure Device RAW
Generic Wifi Dimmer RAW
Generic Wifi Switch/Light Works as Light with Alexa RAW
Generic Wifi Switch/Plug Works as Plug/Outlet with Alexa RAW
Specific Drivers
Device Comment Import URL Model Info
AWP02L-N Plug RAW Link
AWP04L Power Monitor Plug RAW Link
Aoycocr X10S Power Monitor Plug RAW Link
Brilliant 20699 800lm RGBW Bulb RAW Link
Brilliant BL20925 Power Monitor Plug RAW Link
CE Smart Home LA-2-W3 Wall Outlet RAW Link
CE Smart Home LQ-2-W3 Wall Outlet RAW Link
CYYLTF BIFANS J23 Plug RAW Link
Gosund WP3 Plug RAW Link
KMC 4 Power Monitor Plug RAW Link
KMC 4 Power Monitor Plug (Child) RAW
Prime CCRCWFII113PK Plug RAW Link
RF/IR Contact Sensor (Child) RAW
RF/IR Motion Sensor (Child) RAW
RF/IR Smoke Detector (Child) RAW
RF/IR Switch/Toggle/Push (Child) RAW
RF/IR Water Sensor (Child) RAW
RFLink (Parent) Functional - Need feedback RAW Link
S120 Plug RAW
SK03 Power Monitor Outdoor Plug RAW Link
Sensor (Distance) UNTESTED driver RAW Link
Sonoff 4CH (Child) RAW
Sonoff 4CH (Parent) UNTESTED driver RAW Link
Sonoff 4CH Pro (Child) RAW
Sonoff 4CH Pro (Parent) UNTESTED driver RAW Link
Sonoff Basic RAW Link
Sonoff Basic R3 RAW Link
Sonoff Mini RAW Link
Sonoff POW RAW Link
Sonoff POW R2 RAW Link
Sonoff RF Bridge (Parent) Functional - Need feedback RAW Link
Sonoff S2X Works with both Sonoff S20 and S26. RAW Link
Sonoff S31 RAW Link
Sonoff SV RAW Link
Sonoff TH RAW Link
TuyaMCU CE Smart Home WF500D Dimmer (EXPERIMENTAL) WORKING, but need feedback from users RAW Link
TuyaMCU Wifi Dimmer WORKING, but need feedback from users RAW
TuyaMCU Wifi Touch Switch RAW
TuyaMCU Wifi Touch Switch (Child) RAW
Unbranded RGB Controller with IR RAW
YKYC-001 Power Monitor Plug RAW
ZNSN TuyaMCU Wifi Curtain Wall Panel NOT GENERIC - read the instructions RAW

The latest list is found here.

To install the devices, use Tasmota Connect. NOT Sonoff Connect.

The ones from @ericm in this thread work with this firmware as well, though most of the devices supported by his drivers have a driver above. If you use one of these drivers for a device that I don't have a driver for, please mention it in the thread and I'll make a driver for it. Though this is true, it has made it confusing, so it is now stricken out.

Final notes
It is my hope this release will help the community and I will try to answer any questions and fix bugs as soon as possible, but my time is limited.

11 Likes

Many thanks @markus I have successfully installed your Tasmota /HE complied firmware on my EM plug and then installed in into HE using the Sonoff Connect app, once in HE I then manually changed the driver to your Sonoff Pow R2 one. Some things I have noticed though are:

  1. Unfortunately in HE the device isn't changing state when for example I select on/off in Tasmota it falls out of sync.

  2. I wanted to use this for EM but it's not refreshing the state for example power. This is probably tied to the same issue I mentioned above.

  3. I notice under state variables it says the following. Although under current states the IP is listed.

  • mac : null
  • dni : null

Anyways let me know what you need and maybe we can work through this together.

EDIT: I just noticed a ton of these warnings in the HE logs. Just so you are aware I run HE and this plug on separate VLAN's with firewalls rules between allowing the ports it requires. I have opened 39501 and 80 but is there something else that it requires here to operate too?

sys:12019-12-15 06:11:28.380 am warnReceived data from 192.168.30.18, no matching device found for 192.168.30.18, C0A81E12:D1C5, null or C0A81E12.

My EM plugs are working fine.
Sounds like the app didn't update the network ID to the MAC address so maybe check this and do it manually.

That was one of the first things I tried (sorry should have mentioned) but unfortunately it didn't help. I might try and join to my normal lan to see if that works.

It appears that the MAC address is not being allowed through your VLAN.

The NULL should be the properly formatted MAC address. That's how Hubitat knows which device to send the message to for it to be parsed.

OK so this driver seems to require the device is on my main LAN right next to HE otherwise it creates a null entry and starts dropping those warnings into the logs. I cannot hard-code the IP or MAC address as I just get the same result. I do have 8 other Tasmota devices in HE that work fine on that other VLAN but this driver with it's MAC/DNI registration is different and I don't use that for my other devices. Any ideas?

btw, I have opened ports on the other VLAN and it had no effect so something else is going on here like the driver cannot scan outside it's local network which is fine but then it won't allow the manual hard-code either.

Are your other Tasmota devices using the modified firmware? The modified firmware for hubitat uses the same method as Hubduino to get "unsolicited" messages passed of to the correct device so that local changes on the device are not dropped.

With regular tasmota firmware, you can control the device from Hubitat but if you turn the device on/off physically on the device, that status is not passed back to hubitat. So the state of the device match up in Hubitat unless Hubitat queries the device. This can be overcome by querying the device on a regular basis but it creates a LOOOOTTTT of unneeded messages. This of it, if you polled every 5 seconds for 8 switches, that's 192 unnecessary messages every minute. Instead, with the Hubitat modified firmware it sends an HTTP POST message to the hub on port 39501 whenever the state changes. So, if the change was made on the hub, it is able to confirm that the change happened and if the change was local, hubitat can change it's status to match the device. Hubitat uses the MAC address in the header of that message to passed the message off to the appropriate device so that it can be parsed.

So, unless the MAC address is received by Hubitat, the message won't be able to be parsed correctly. You could always use regular tasmota firmware and a driver that doesn't require the message coming back from he device. But then you would lose the local change aspect of the custom firmware.

1 Like

I also have my devices separated into a separate subnet cut off from the Internet and everything except the Hubitat hub, but they are not behind a NAT nor are the source MAC addresses altered in the ethernet frames. This is probably an unusual setup and yours do sound like a more normal one.
As long as the IP seen as source for each device is unique per device (multimapped 1-to-1 in NAT would work as well as not masquerading/NATing at all) Hubitat will be able to know which device is which. However, the way the driver was written it would revert back to HW address matching automatically. I did a quick rewrite to support matching against IP instead.
This code is neither clean nor well tested, but should work. I will make sure to implement this way of handling network IDs in all my LAN device drivers once this is confirmed to be working.
The IP in Device ID has to be all UPPERCASE hex, this new driver does this automatically.

/**
 *  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: "overrideIP", type: "bool", title: "Override IP", description: "Override the automatically discovered IP address?", displayDuringSetup: true, required: false)
    input(name: "useIPAsID", type: "bool", title: "IP as Network ID", description: "Force the use of IP as network ID? Only use this if MAC matching isn't working in your setup... Also set Override IP to true and input the correct Sonoff IP.", 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 "2":
      if (level < 99)
         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 (useIPAsID) {
    state.dni = setDeviceNetworkId(ipAddress, true)
}
else 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") && overrideIP == false) {
    logging("IPAddress: $result.IPAddress",99)
    events << createEvent(name: "ip", value: "$result.IPAddress")
}
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(macOrIP, isIP = false){
def myDNI
if (isIP == false) {
    myDNI = macOrIP
} else {
    myDNI = convertIPtoHex(macOrIP)
}
logging("Device Network Id should be set to ${myDNI} from ${macOrIP}", 1)
return myDNI
}

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

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

private String convertIPtoHex(ipAddress) {
if (ipAddress != null) { 
    hex = ipAddress.tokenize( '.' ).collect {  String.format( '%02X', it.toInteger() ) }.join()
} else {
    hex = null
}
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 = null) {
def existingIp = getDataValue("ip")
def existingPort = getDataValue("port")
logging("Running sync()", 1)
if (ip && ip != existingIp) {
    sendEvent(name: 'ip', value: ip)
    logging("IP set to ${ip}", 1)
}
if (port && port != existingPort) {
    updateDataValue("port", port)
    logging("Port set to ${port}", 1)
}

}


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")))

if(overrideIP == true) {
    cmds << sync(ipAddress)
}

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="No Reports" value="2" />
<Item label="All" value="99" />
</Value>
</configuration>
'''
}
1 Like

Thoughts and reasoning
Before updating and publishing the companion drivers for this firmware I'd like to ask what the best-practice approach is for the network ID of LAN devices on Hubitat? Is there such a thing as a best-practice yet? I'm relatively new here, I started about 1 month ago, so I probably haven't read everything. However, it does feel like it is still not very clear what the best approach is.
To be able to replace one device with another easily, using the IP seems like a great way. But this requires all devices to keep the same IP, so either static IPs or assigned IPs in the DHCP server would be needed.
MAC addresses will not change (normally) and even if the IP change, that can then be automatically updated. To replace one device with another, the Network ID would have to be updated manually.
When using MAC address matching, as I've understood it, it also requires the devices to not have their ethernet frames altered for the actual MAC to be seen by Hubitat. Is this correct, or have I misunderstood something? This would cause issues in many scenarios where NAT or IP routing is involved.

My suggested approach
I intend to do the following with the Network ID:

  • Let the user choose in the driver which matching to use: IP or MAC
  • For child devices I will use the parent DEVICE id, this way, switching between IP or MAC will not affect child devices. Event parsing is done in the parent device anyway. No matching is needed.

Any thoughts on this?

I think it's all your personal preference.

I would not do this. People will pick the wrong one every time. Pick what you feel will work best and go with it. If folks are smart enough to figure out that they want the other one, they're smart enough to modify the driver themselves. Make it simple because people are going to try and implement it who have no idea what a MAC address is.

That isn't possible. Each device has to have a unique ID. I would recommend the child be the parent's DNI plus dash switch number. So, if you had a sonoff dual, the parent would have a DNI like C0A80178 and switch 1 would be C0A80178-1 and switch 2 would be C0A80178-2. That way you can parse the message in the parent and send it to the correct child to sort out. The parent would just be the traffic cop for sending/receiving messages. All of the action of parsing meaning from the message or commands from apps would be handled in the child devices.

Thank you for the feedback. That is a valid point, how about making the setting to be something like "Is the device IP fixed and will never change, set this to true if you experience issues with getting updates". This way "power" users can use this setting to make it work, and the "normal" user won't set it unless they really have a fixed IP, in which case it doesn't matter if they set it or not unless they really have some filtering of MAC addresses done in their network, which only a "power" user would have anyway, most likely.

I think I didn't express myself very clear, I mean to use $device.id-1, $device.id-2 etc, where $device.id is the parent id (which is unique per device on the hub). This way, even if you ever switch between MAC or IP, it doesn't affect child devices.

Excellent that driver has resolved my issue. Thank you very much!! :slight_smile:

1 Like

Glad it helped, will have to see how to implement this in my drivers without confusing normal users and without requiring fixed IPs... Any thoughts on the above?

1 Like

I found your info in the driver page enough to get it working. But maybe I am not a novice user so I can understand where @M874585684875 is coming from.

Then yup...that's definitely the easiest way to do it.

1 Like

I've published some drivers as well as restructured how I create drivers, so it should be easy and fast to create more drivers without having a lot of redundant code to keep track of. This way, if I make a change to one of my drivers, it is automatically added to any other drivers using the same functions.

1 Like

That is how I ended up doing it, hope these new drivers work well for everyone.

1 Like

Thanks very much for your work. I have just installed and it's working great. Well done.

EDIT: On the weekend I might cut over my other devices that are running other random versions of community Tasmota drivers to have consistency.

Sounds great! Glad you like them! If you tell me which type of devices they are I can generate more drivers if needed.

Currently I am running Sonoff Basic R3, Sonoff Mini also have a Sonoff Basic R3 Zigbee but I havent installed that as yet.

EDIT: I almost forgot I also use this one for a Tasmota plug I built with an installed / upgraded temp sensor I added. If this could be added i'd cut that over too.