Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@dotMorten
Last active September 25, 2015 05:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dotMorten/e3ffea21730f05b7e09b to your computer and use it in GitHub Desktop.
Save dotMorten/e3ffea21730f05b7e09b to your computer and use it in GitHub Desktop.
ZBHT-2
/**
* Smartenit ZHBT-2
*
* Copyright 2015 Morten Nielsen
*
* 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.
*
*/
// ZBHT-2 Smartenit Temperature/Humidity sensor
// http://smartenit.com/product/zbht-2/
// Product brief: http://smartenit.com/sandbox/downloads/ZBHT-2_Product%20Brief.pdf
metadata {
definition (name: "Smartenit ZBHT-2", namespace: "dotMorten", author: "Morten Nielsen") {
capability "Relative Humidity Measurement"
capability "Temperature Measurement"
capability "Sensor"
capability "Battery"
//Manufacturer ID: 0x1075
//Device ID: 0x0302
//profileId
//0104 = Zigbee HA
//Cluster IDs:
//0x0000 Basic
//0x0001 Power Configuration
//0x0003 Identify
//0x0009 Alarms
//0x0402 Temperature Measurement
//0x0405 Relative Humidity Measurement
fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,0402,0405", manufacturer: "Smartenit", model: "ZBHT-2"
//Question: Do I need outClusters? Most have ' outClusters: "0019" ', but no documentation what that is
}
// simulator metadata
simulator {
for (int i = 0; i <= 100; i += 10) {
status "${i}F": "temperature: $i F"
}
for (int i = 0; i <= 100; i += 10) {
status "${i}%": "humidity: ${i}%"
}
}
// UI tile definitions
tiles {
valueTile("temperature", "device.temperature", width: 2, height: 2) {
state("temperature", label:'${currentValue}°',
backgroundColors:[
[value: 31, color: "#153591"],
[value: 44, color: "#1e9cbb"],
[value: 59, color: "#90d2a7"],
[value: 74, color: "#44b621"],
[value: 84, color: "#f1d801"],
[value: 95, color: "#d04e00"],
[value: 96, color: "#bc2323"]
]
)
}
valueTile("humidity", "device.humidity") {
state "humidity", label:'${currentValue}%', unit:""
}
main(["temperature", "humidity"])
details(["temperature", "humidity"])
}
}
def refresh() {
log.debug "_____________refresh begin"
[
"st rattr 0x${device.deviceNetworkId} 1 0x0402 0x0000",
"st rattr 0x${device.deviceNetworkId} 1 0x0405 0x0000"
]
}
def configure() {
log.debug "_____________configure begin"
def configCmds = [
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}", //checkin time 6 hrs
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",
"zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x405 {${device.zigbeeId}} {}", "delay 500",
"zcl global send-me-a-report 0x405 0 0x29 30 3600 {6400}",
"send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
]
return configCmds + refresh()
}
private getEndpointId() {
new BigInteger(device.endpointId, 16).toString()
}
//Called when the device configuration has been updated
def updated() {
log.debug "___________DEBUG_____________: updated called"
configure()
}
// Parse incoming device messages to generate events
def parse(String description) {
log.debug "__________________Recieved msg:"
log.debug "${description}"
if (description?.startsWith('read attr -')) {
map = parseReportAttributeMessage(description)
def attrresult = map ? createEvent(map) : null
return attrresult;
}
null
}
private Map parseReportAttributeMessage(String description) {
Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param ->
def nameAndValue = param.split(":")
map += [(nameAndValue[0].trim()):nameAndValue[1].trim()]
}
Map resultMap = [:]
if (descMap.cluster == "0402" && descMap.attrId == "0000") {
def value = getTemperature(descMap.value)
return [
name: 'temperature',
value: value,
descriptionText: ""
]
}
else if (descMap.cluster == "0405" && descMap.attrId == "0000") {
def value = getHumidity(descMap.value)
return [
name: 'humidity',
value: value,
descriptionText: ""
]
}
else if (descMap.cluster == "0001" && descMap.attrId == "0020") {
return getBatteryResult(Integer.parseInt(descMap.value, 16))
}
return resultMap
}
def getTemperature(value) {
def celsius = Integer.parseInt(value, 16).shortValue() / 100
if(getTemperatureScale() == "C"){
return celsius
} else {
return celsiusToFahrenheit(celsius) as Integer
}
}
def getHumidity(value)
{
def humidity = Integer.parseInt(value, 16).shortValue() / 100
return humidity
}
private Map getBatteryResult(rawValue) {
log.debug "Battery"
log.debug rawValue
def linkText = getLinkText(device)
def result = [
name: 'battery',
value: '--'
]
def volts = rawValue / 10
def descriptionText
if (rawValue == 255) {}
else {
if (volts > 3.5) {
result.descriptionText = "${linkText} battery has too much power (${volts} volts)."
}
else {
def minVolts = 2.1
def maxVolts = 3.0
def pct = (volts - minVolts) / (maxVolts - minVolts)
result.value = Math.min(100, (int) pct * 100)
result.descriptionText = "${linkText} battery was ${result.value}%"
}
}
return result
}
@dotMorten
Copy link
Author

Sensor details:
image

@workingmonk
Copy link

in groovy, anything at the end of the method is returned. if you end with log messages rather than the command, the command never gets executed/returned. also, multiple commands need to be in array

@workingmonk
Copy link

def refresh() {
    log.debug "_____________refresh begin" 
    [
        "st rattr 0x${device.deviceNetworkId} 0x{device.endpointId} 0x0402 0x0000", "delay 500",
        "st rattr 0x${device.deviceNetworkId} 0x{device.endpointId} 0x0405 0x0000", "delay 500"
    ]
}

@workingmonk
Copy link

try this as your refresh method as hit the refresh method to see if the parse gets called

@workingmonk
Copy link

same goes for configure:

def configure() {
    log.debug "_____________configure begin" 
[
    "zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500",
    "zdo bind 0x${device.deviceNetworkId} 1 1 0x0405 {${device.zigbeeId}} {}", "delay 500"
]
}

This is obviously basic configuration and doesn't do any of the reporting.

@dotMorten
Copy link
Author

@workingmunk Thank you so much!

Dumb question: Where do I hit the "refresh" method? Also this device auto-reports each 3 minutes, and since it's battery powered any commands to it could be significantly delayed.

@workingmonk
Copy link

here you go, full config. since i don't have the device i am basing this on the zigbee spec

def configure() {
    def configCmds = [
    "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 1 {${device.zigbeeId}} {}", "delay 500",
    "zcl global send-me-a-report 1 0x20 0x20 30 21600 {01}",        //checkin time 6 hrs
    "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",

    "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x402 {${device.zigbeeId}} {}", "delay 500",
    "zcl global send-me-a-report 0x402 0 0x29 30 3600 {6400}",
    "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500",

    "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x405 {${device.zigbeeId}} {}", "delay 500",
    "zcl global send-me-a-report 0x405 0 0x29 30 3600 {6400}",
    "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500"
    ]
    return configCmds + refresh()
}

private getEndpointId() {
    new BigInteger(device.endpointId, 16).toString()
}

@workingmonk
Copy link

add this to the tiles

standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
        }
main(["temperature", "humidity"])
        details(["temperature", "humidity", "refresh"])

This should give you the refresh button

@dotMorten
Copy link
Author

@workingmonk Thanks! A few more questions:

    1. Do I actually need to do a configure?
    1. What does the first zcl do?
    1. I assume the two next zcl commands sets the reporting interval? What are the parameters? 3600 makes me think it'll only report once an hour - I'm happy with the 3min intervals it already has. Can't really make sense of how this relates to 2.4.7 in the cluster spec.
    1. getEndpointId: It doesn't look like this is used. Am I missing something?

@workingmonk
Copy link

  1. Yes, it is called when the device joins for the first time and sets up the device to keep checking back and what difference in value to send back the system
  2. By first zcl, I assume you mean the 1 0x20 command. That is the battery config
  3. 3600 is every hour IF there is no change. the minimum difference in reporting interval is 30 seconds.
  4. For the endpointId variable, Groovy converts these calls to include a camel-cased "get" on the front.

@dotMorten
Copy link
Author

@workingmonk
Regarding this:

1.Yes, it is called when the device joins for the first time and sets up the device

Does that mean I need to remove the device and bind it again? It already comes with a default reporting interval btw

@workingmonk
Copy link

yes, it does, but this sets to the expected reporting interval. You can either rejoin or use the IDE to send the configure.

I am not getting the notification for your messages because it is @workingmonk

@dotMorten
Copy link
Author

@workingmonk thanks! Tried forcing calling configure when updated is called. I see in the debug log that configure and refresh is called. Still parse is never hit. Code above updated to reflect current state

@workingmonk
Copy link

@dotMorten

def refresh() {
    log.debug "_____________refresh begin" 
    [
      "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0402 0x0000", "delay 500",
      "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0405 0x0000", "delay 500"
    ]
}

You missed the delay in the refresh code. the other issue is these are sleepy devices, so they may not get your messages unless they are awake. They check for messages close to every 7 seconds. Please add the refresh tile using the code:

standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
            state "default", action:"refresh.refresh", icon:"st.secondary.refresh"
        }
main(["temperature", "humidity"])
        details(["temperature", "humidity", "refresh"])

After that hit the refresh button couple of times and you should see the parse method being called.

@dotMorten
Copy link
Author

@workingmonk Thanks. When pressing the refresh button, I'm told "you are not authorized to perform the requested operation" ?
I'm adding a call to configure and refresh from updated() to trigger running this code - I also press the announce button on the device right before to wake it up so it should respond immediately (hint from a Smartenit guy)

Interestingly enough when I press the "Announce" button on the device, the hue bridge is reporting it:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment