Securifi Peanut Smart Plug

Sorry dude, maybe the hide details function introduced extra characters? I just saved the driver file again on my C4 and C7 without error. I'll hate to do it, but posting the entire thing without hide details engaged

/**

  • Peanut Plug
  • Copyright 2020 pakmanw@sbcglobal.net
  • 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.
  • Peanut Plug
  • Author: pakmanw@sbcglobal.net
  • Change Log
  • 2017-09-17 - v01.01 Created
  • 2018-03-01 - v01.02 fix power accuracy issue
  • 2018-12-23 - v01.03 merging jamesham change to get the calibrated attr from peanut plug,
  •                  add support for new smartthings app
    
  • 2019-01-17 - v01.04 merging jamesham retain state code
  • 2019-09-27 - v01.05 update fingerprint from transman
  • 2019-09-30 - v01.06 new interface, add active power settings
  • 2019-10-01 - v01.07 add cost per kwh
  • 2019-10-02 - v01.08 add reset time
  • 2019-10-03 - v01.09 two decimal points for values
  • 2020-02-23 - v01.10 fix energy value not update issues
  • 2020-03-31 - v01.11 fix dickmal place display problem in some devices
    */

import hubitat.zigbee.zcl.DataType

metadata {
definition (name: "Peanut Plug", namespace: "pakmanwg", author: "pakmanw@sbcglobal.net", ocfDeviceType: "oic.d.switch",
vid: "generic-switch-power-energy") {
capability "Energy Meter"
capability "Actuator"
capability "Switch"
capability "Power Meter"
capability "Polling"
capability "Refresh"
capability "Configuration"
capability "Sensor"
capability "Light"
capability "Health Check"
capability "Voltage Measurement"

attribute "current", "number"
attribute "switchStatus", "string"

command "reset"

fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0004, 0005, 0006, 0B04, 0B05",
  outClusters: "0000, 0001, 0003, 0004, 0005, 0006, 0019, 0B04, 0B05",
  manufacturer: "Securifi Ltd."

}

// tile definitions
tiles(scale: 2) {
multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true) {
tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc"
attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff"
}
tileAttribute ("device.switchStatus", key: "SECONDARY_CONTROL") {
attributeState "switchStatus", label:'${currentValue}'
}
}
standardTile("refresh", "device.refresh", width: 2, height: 2) {
state "refresh", label:'Refresh', action: "refresh", icon:"st.secondary.refresh-icon"
}
standardTile("reset", "device.reset", width: 2, height: 2) {
state "reset", label:'Reset', action: "reset", icon:"st.secondary.refresh-icon"
}
valueTile("power", "device.power", width: 2, height: 2) {
state "default", label:'${currentValue} W', backgroundColor: "#cccccc"
}
valueTile("energy", "device.energy", width: 2, height: 2) {
state "default", label:'${currentValue} kWh', backgroundColor: "#cccccc"
}
valueTile("voltage", "device.voltage", width: 2, height: 2) {
state "default", label:'${currentValue} V', backgroundColor: "#cccccc"
}
valueTile("current", "device.current", width: 2, height: 2) {
state "default", label:'${currentValue} A', backgroundColor: "#cccccc"
}
valueTile("history", "device.history", decoration:"flat", width: 6, height: 3) {
state "history", label:'${currentValue}'
}

main(["switch"])
details(["switch", "power", "energy", "refresh", "voltage", "current", "reset"])

}

preferences {
input name: "retainState", type: "bool", title: "Retain State?", description: "Retain state on power loss?", required: false, displayDuringSetup: false,
defaultValue: true
// configParams?.each {
// getOptionsInput(it)
// }
input "energyPrice", "number", title: "$/kWh Cost:", description: "Electric Cost per Kwh in cent", range: "0..", defaultValue: 15
input "inactivePower", "decimal", title: "Reporting inactive when power is less than or equal to:", range: "0..
", defaultValue: 0
// ["Power", "Energy", "Voltage", "Current"].each {
// getBoolInput("display${it}", "Display ${it} Activity", true)
// }
}
}

private getOptionsInput(param) {
if (param.prefName) {
input "${param.prefName}", "enum",
title: "${param.name}:",
defaultValue: "${param.val}",
required: false,
displayDuringSetup: true,
options: param.options?.collect { name, val -> name }
}
}

private getBoolInput(name, title, defaultVal) {
input "${name}", "bool",
title: "${title}?",
defaultValue: defaultVal,
required: false
}

def installed() {
reset()
configure()
refresh()
}

def off() {
zigbee.off()
}

def on() {
zigbee.on()
}

def refresh() {
Integer reportIntervalMinutes = 5
setRetainState() +
zigbee.onOffRefresh() +
zigbee.simpleMeteringPowerRefresh() +
zigbee.electricMeasurementPowerRefresh() +
zigbee.onOffConfig(1, 600) +
zigbee.simpleMeteringPowerConfig(1, 600) +
zigbee.electricMeasurementPowerConfig(1, 600) +
voltageMeasurementRefresh() +
voltageMeasurementConfig(reportIntervalMinutes * 60, 600) +
currentMeasurementRefresh() +
currentMeasurementConfig(reportIntervalMinutes * 60, 600) +
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0600) +
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0601) +
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0602) +
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0603) +
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0604) +
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0605)
}

def currentMeasurementConfig(minReportTime=1, maxReportTime=600, reportableChange=0x0030) {
zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0508, DataType.UINT16, minReportTime, maxReportTime, reportableChange)
}

def currentMeasurementRefresh() {
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0508);
}

def voltageMeasurementConfig(minReportTime=1, maxReportTime=600, reportableChange=0x0018) {
zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0505, DataType.UINT16, minReportTime, maxReportTime, reportableChange)
}

def voltageMeasurementRefresh() {
zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x0505);
}

def getCurrentMultiplier() {
if (state.currentMultiplier && state.currentDivisor) {
return (state.currentMultiplier / state.currentDivisor)
} else {
return 0.001831
}
}

def getVoltageMultiplier() {
if (state.voltageMultiplier && state.voltageDivisor) {
return (state.voltageMultiplier / state.voltageDivisor)
} else {
return 0.0045777
}
}

def getPowerMultiplier() {
if (state.powerMultiplier && state.powerDivisor) {
return (state.powerMultiplier / state.powerDivisor)
} else {
return 0.277
}
}

def configure() {
log.debug "in configure()"
return configureHealthCheck() + setRetainState()
}

def configureHealthCheck() {
Integer hcIntervalMinutes = 12
sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID])
return refresh()
}

def updated() {
log.debug "in updated()"
// updated() doesn't have it's return value processed as hub commands, so we have to send them explicitly
def cmds = configureHealthCheck() + setRetainState()
cmds.each{ sendHubCommand(new hubitat.device.HubAction(it)) }
}

def ping() {
return zigbee.onOffRefresh()
}

def setRetainState() {
log.debug "Setting Retain State: $retainState"
if (retainState == null || retainState) {
if (retainState == null) {
log.warn "retainState is null, defaulting to 'true' behavior"
}
return zigbee.writeAttribute(0x0003, 0x0000, DataType.UINT16, 0x0000)
} else {
return zigbee.writeAttribute(0x0003, 0x0000, DataType.UINT16, 0x1111)
}
}

def reset() {
state.energyValue = 0.0
state.powerValue = 0.0
state.voltage = 0.0
state.current = 0.0
state.costValue = 0.0
state.time = now()
sendEvent(name: "energy", value: (String.format("%.2f", state.energyValue)))
sendEvent(name: "cost", value: (String.format("%.2f", state.costValue)))
state.resetTime = new Date().format('MM/dd/yy hh:mm a', location.timeZone)
}

def refreshHistory() {
def history = ""
def items = [:]

items["energyDuration"] = "Energy - Duration"
items["energyCost"] = "Energy - Cost"
["power", "voltage", "current"].each {
items["${it}Low"] = "${it.capitalize()} - Low"
items["${it}High"] = "${it.capitalize()} - High"
}
items.each { attrName, caption ->
def attr = device.currentState("${attrName}")
def val = attr?.value ?: ""
def unit = attr?.unit ?: ""
history += "${caption}: ${val} ${unit}\n"
}
sendEvent(createEventMap("history", history, false))
}

// Configuration Parameters
private getConfigParams() {
return [
powerValueChangeParam,
powerPercentageChangeParam,
powerReportIntervalParam,
energyReportIntervalParam,
voltageReportIntervalParam,
electricityReportIntervalParam
]
}

private getPowerValueChangeParam() {
return createConfigParamMap(151, "Power Report Value Change", 2, getPowerValueOptions(), "powerValueChange")
}

private getPowerPercentageChangeParam() {
return createConfigParamMap(152, "Power Report Percentage Change", 1, getPercentageOptions(10, "No Reports"), "powerPercentageChange")
}

private getPowerReportIntervalParam() {
return createConfigParamMap(171, "Power Reporting Interval", 4, getIntervalOptions(30, "No Reports"), "powerReportingInterval")
}

private getEnergyReportIntervalParam() {
return createConfigParamMap(172, "Energy Reporting Interval", 4, getIntervalOptions(300, "No Reports"), "energyReportingInterval")
}

private getVoltageReportIntervalParam() {
return createConfigParamMap(173, "Voltage Reporting Interval", 4, getIntervalOptions(0, "No Reports"), "voltageReportingInterval")
}

private getElectricityReportIntervalParam() {
return createConfigParamMap(174, "Electrical Current Reporting Interval", 4, getIntervalOptions(0, "No Reports"), "electricityReportingInterval")
}

private createConfigParamMap(num, name, size, options, prefName, val=null) {
if (val == null) {
val = (settings?."${prefName}" ?: findDefaultOptionName(options))
}
return [
num: num,
name: name,
size: size,
options: options,
prefName: prefName,
val: val
]
}

// Settings
private getEnergyPriceSetting() {
return safeToDec(settings?.energyPrice, 0.12)
}

private getInactivePowerSetting() {
return safeToDec(settings?.inactivePower, 0)
}

private getDebugOutputSetting() {
return settings?.debugOutput != false
}

private getMinimumReportingInterval() {
def minVal = (60 * 60 * 24 * 7)
[powerReportIntervalParam, energyReportIntervalParam, voltageReportIntervalParam, electricityReportIntervalParam].each {
def val = convertOptionSettingToInt(it.options, it.val)
if (val && val < minVal) {
minVal = val
}
}
return minVal
}

private getIntervalOptions(defaultVal=null, zeroName=null) {
def options = [:]
if (zeroName) {
options["${zeroName}"] = 0
}
options << getIntervalOptionsRange("Second", 1, [5,10,15,30,45])
options << getIntervalOptionsRange("Minute", 60, [1,2,3,4,5,10,15,30,45])
options << getIntervalOptionsRange("Hour", (60 * 60), [1,2,3,6,9,12,18])
options << getIntervalOptionsRange("Day", (60 * 60 * 24), [1,3,5])
options << getIntervalOptionsRange("Week", (60 * 60 * 24 * 7), [1,2])
return setDefaultOption(options, defaultVal)
}

private getIntervalOptionsRange(name, multiplier, range) {
def options = [:]
range?.each {
options["${it} ${name}${it == 1 ? '' : 's'}"] = (it * multiplier)
}
return options
}

private getPowerValueOptions() {
def options = [:]
[0,1,2,3,4,5,10,25,50,75,100,150,200,250,300,400,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,6000,7000,8000,9000,10000,12500,15000].each {
if (it == 0) {
options["No Reports"] = it
}
else {
options["${it} Watts"] = it
}
}
return setDefaultOption(options, 50)
}

private getPercentageOptions(defaultVal=null, zeroName=null) {
def options = [:]
if (zeroName) {
options["${zeroName}"] = 0
}
for (int i = 1; i <= 5; i += 1) {
options["${i}%"] = i
}
for (int i = 10; i <= 100; i += 5) {
options["${i}%"] = i
}
return setDefaultOption(options, defaultVal)
}

private convertOptionSettingToInt(options, settingVal) {
return safeToInt(options?.find { name, val -> "${settingVal}" == name }?.value, 0)
}

private setDefaultOption(options, defaultVal) {
def name = options.find { key, val -> val == defaultVal }?.key
if (name != null) {
return changeOptionName(options, defaultVal, "${name}${defaultOptionSuffix}")
}
else {
return options
}
}

private changeOptionName(options, optionVal, newName) {
def result = [:]
options?.each { name, val ->
if (val == optionVal) {
name = "${newName}"
}
result["${name}"] = val
}
return result
}

private findDefaultOptionName(options) {
def option = options?.find { name, val ->
name?.contains("${defaultOptionSuffix}")
}
return option?.key ?: ""
}

private getDefaultOptionSuffix() {
return " (Default)"
}

private createEventMap(name, value, displayed=null, desc=null, unit=null) {
desc = desc ?: "${name} is ${value}"

def eventMap = [
name: name,
value: value,
displayed: (displayed == null ? ("${getAttrVal(name)}" != "${value}") : displayed)
]

if (unit) {
eventMap.unit = unit
desc = "${desc} ${unit}"
}

if (desc && eventMap.displayed) {
logDebug desc
eventMap.descriptionText = "${device.displayName} - ${desc}"
}
else {
logTrace "Creating Event: ${eventMap}"
}
return eventMap
}

private getAttrVal(attrName) {
try {
return device?.currentValue("${attrName}")
}
catch (ex) {
logTrace "$ex"
return null
}
}

private safeToInt(val, defaultVal=0) {
return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal
}

private safeToDec(val, defaultVal=0) {
return "${val}"?.isBigDecimal() ? "${val}".toBigDecimal() : defaultVal
}

private roundTwoPlaces(val) {
return Math.round(safeToDec(val) * 100) / 100
}

private convertToLocalTimeString(dt) {
def timeZoneId = location?.timeZone?.ID
if (timeZoneId) {
return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId))
}
else {
return "$dt"
}
}

private isDuplicateCommand(lastExecuted, allowedMil) {
!lastExecuted ? false : (lastExecuted + allowedMil > new Date().time)
}

def parse(String description) {

// log.debug "description is $description"
def event = zigbee.getEvent(description)
if (event) {
if (event.name == "power") {
def powerValue = (event.value as Integer) * getPowerMultiplier()
sendEvent(name: "power", value: (String.format("%.2f", powerValue)))
def time = (now() - state.time) / 3600000 / 1000
state.time = now()
// log.debug "powerValues is $state.powerValue"
state.energyValue = state.energyValue + (time * powerValue)
state.powerValue = powerValue
// log.debug "energyValue is $state.energyValue"
def localCostPerKwh = 15
if (energyPrice) {
localCostPerKwh = energyPrice as Integer
}
sendEvent(name: "energy", value: (String.format("%.2f", state.energyValue)))
state.costValue = roundTwoPlaces(state.energyValue * localCostPerKwh / 100)
sendEvent(name: "cost", value: (String.format("%.2f", state.costValue)))
if (inactivePowerSetting == null) {
inactivePowerSetting = 0
}
if (state.resetTime == null) {
state.resetTime = new Date().format('MM/dd/yy hh:mm a', location.timeZone)
}
def costStr = (String.format("%.2f", state.costValue))
def switchStatusS = (powerValue > inactivePowerSetting) ? "Active | Cost $$state.costValue since $state.resetTime" :
"Inactive | Cost $$costStr since $state.resetTime"
// log.debug "$switchStatusS"
sendEvent(name: "switchStatus", value: switchStatusS, displayed: false)
// refreshHistory
} else {
sendEvent(event)
}
} else if (description?.startsWith("read attr -")) {
def descMap = zigbee.parseDescriptionAsMap(description)
// log.debug "Desc Map: $descMap"
if (descMap.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER) {
def intVal = Integer.parseInt(descMap.value,16)
if (descMap.attrInt == 0x0600) {
// log.debug "ACVoltageMultiplier $intVal"
state.voltageMultiplier = intVal
} else if (descMap.attrInt == 0x0601) {
// log.debug "ACVoltageDivisor $intVal"
state.voltageDivisor = intVal
} else if (descMap.attrInt == 0x0602) {
// log.debug "ACCurrentMultiplier $intVal"
state.currentMultiplier = intVal
} else if (descMap.attrInt == 0x0603) {
// log.debug "ACCurrentDivisor $intVal"
state.currentDivisor = intVal
} else if (descMap.attrInt == 0x0604) {
// log.debug "ACPowerMultiplier $intVal"
state.powerMultiplier = intVal
} else if (descMap.attrInt == 0x0605) {
// log.debug "ACPowerDivisor $intVal"
state.powerDivisor = intVal
} else if (descMap.attrInt == 0x0505) {
def voltageValue = roundTwoPlaces(intVal * getVoltageMultiplier())
// log.debug "Voltage ${voltageValue}"
state.voltage = $voltageValue
sendEvent(name: "voltage", value: (String.format("%.2f", voltageValue)))
} else if (descMap.attrInt == 0x0508) {
def currentValue = roundTwoPlaces(intVal * getCurrentMultiplier())
// log.debug "Current ${currentValue}"
state.current = $currentValue
sendEvent(name: "current", value: (String.format("%.2f", currentValue)))
}
} else {
log.warn "Not an electrical measurement"
}
} else {
log.warn "DID NOT PARSE MESSAGE for description : $description"
log.debug zigbee.parseDescriptionAsMap(description)
}
}

Thanks for trying again, I am getting the same error. This is the first time I have updated a custom driver. I am copying and pasting over the old driver code. Is there a different way I should be doing the update?

Ugg, sorry guy, you have to totally erase the driver content and replace with my content. You can't just add to existing. Replace, hit save

I wonder what the problem could be. I tried erasing and replacing, I also tried creating a new driver with your code and received the same error.

It doesn't appear to be just you, I get the same error when I try to install the same code

For my 1 Securifi device, I use the generic driver, however I don't use it for anything other than a repeater
image

Good luck

1 Like

Thanks for checking. I use the power meter so I need the custom code.

Unless I am mistaken, the built in driver does support pwr monitoring :slight_smile:

The driver does (and most of the community-made Peanut Plug drivers do). The problem is that most Peanut Plugs out there did not have the firmware to do that. They need an upgrade from what they came out of the factory with to support it.

Most people do not actually need it or use it, but you have to do the firmware upgrade if you do want it.

Thanks for letting me know. I have upgraded the firmware on the plug, but I thought I had read that the custom driver was need to for power monitoring. I did try the built in driver and it is displaying power.

1 Like

Keep in mind others have reported this zigbee device causes trouble in the zigbee mesh.
See;
The great peanut panic thread

3 Likes

Thank you! I kept seeing this thread and thinking; have people no regard for their Zigbee mesh? Good lord, 6 of the Peanut menace plugs would routinely crash my network and cause bulbs to drop off daily. Guess what; My bulbs never drop off anymore.

2 Likes

Not everyone has trouble with Peanuts though. I have nearly 20 of them scattered across the house. Along with all sorts of other ZigBee devices.

1 Like

I am of the firm belief that Peanuts are 100% a problem. Even for users who claim reliability with them. The users with the good luck simply have not run into the "problem" yet. The problem is they do not play well with certain other brands on the same mesh. The brands vary, but in my case it was anything Philips Hue brand. And to go into more detail, it was the Philips Hue 4 button magnetic switches. This conclusion was after hours of step by step troubleshooting, re-meshing, more re-meshing, wiping the Hubitat multiple times, and finally, systematically removing things ONE by ONE until reliability was achieved. I had my system down to one Peanut Plug and one Philips Hue switch. The system would easily eat itself after about 6 hours with just those two things. Since originally the Peanuts were the last to be added to my system, and since that's when the problems started to happen, the Philips products win and all 20 Peanut plugs were replaced with INNR. Now my system has been flawless ever since.

1 Like

I do not have any Hue devices nor intend to have any, but I do have quite an "eclectic" mix of other ZigBee devices.

Be nice if there was the level of detail for the ZigBee mesh that is available for the C7's Z-Wave devices.

1 Like

I also have quite a few of the Hue 4-button switches and motion sensors. My Zigbee bulbs also wouldn’t stay connected until I removed the Peanuts. I could open the zigbee getChildAndRouteInfo page and the routes would change every time I refreshed the page. The routes seem to never change now. I also have 3 Zigbee mesh setup. None have Peanuts.

1 Like

Centralite are a good replacement for the Peanuts...big plus is they don't block the access to the second plug on the outlet regardless of whether you put them on the top or bottom plug, and they also fit well on a "vertical" power strip. Unlike many others including Innr, you can fit two on an outlet if you need to. $17.99 on Amazon, 100% reliable for me.

3 Likes

Do these monitor power use?

No, the Centralite 4200 series doesn't report power.

Sir, get an xbee. For an S2C Xbee and the USB board, it's under 30 bucks. The amount of info you get is amazing
Currently we have built in;
http://192.168.X.XX/hub/zigbee/getChildAndRouteInfo (replace X.XX with hub's IP)

This is Xbee

3 Likes

Hi,

I'm trying to use this smart plug (Brazil Smart Tuya Zigbee Socket BR Plug Wireless Outlet Timing Plug 16A Energy Monitor Alexa Google Home Smart Home Control,Socket & Outlet) with HE. The energy monitoring feature is not working with the "Generic Zigbee Outlet" driver. With "Peanut Plug" driver is working but no energy (kWh) information:


Here is the fingerprint from the plug:

Anyone know how to make this plug work with HE ?

Thanks.