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>
'''
}