Heiman Smart Smoke Sensor HS1SA Zigbee

Well, testing with another detector the log was same kind than the first when paired far from the hub: a lot of identical rows. When paired near the hub the log is cleaner. Anyway, still "unknown cluster" seen

[dev:80]2021-09-25 23:59:59.600 [debug]Model is SMOK_V16

[dev:80]2021-09-25 23:59:57.274 [debug]Manufacturer is Heiman

[dev:80]2021-09-25 23:59:54.957 [debug]Unknown --> Cluster-> 0001 AttrInt-> 51 Value-> 00

[dev:80]2021-09-25 23:59:52.675 [debug]Battery level is 100% (3 Volts)

[dev:80]2021-09-25 23:59:51.365 [debug]Heiman Zigbee Smoke Detector is Battery Powered

[dev:80]2021-09-25 23:59:49.088 [debug]Max Alarm Duration is 240 seconds

[dev:80]2021-09-25 23:59:46.770 [info]Heiman Zigbee Smoke Detector reports status is all clear

[dev:80]2021-09-25 23:59:44.468 [debug]Zone State is enrolled

[dev:80]2021-09-25 23:59:43.200 [debug]Zone Type is Fire Sensor

[dev:80]2021-09-25 23:59:31.208 [debug]Refreshing...

[dev:80]2021-09-25 23:59:31.202 [debug]Heiman Zigbee Smoke Detector Setting Battery Last Replaced to Current date for newly paired sensor

Will be interesting to see if this one reports in every 6 hours.

Here's an update to the driver - at least this should clear the Unknown cluster.

This is available in HPM now also....

/**
 * Heiman (and maybe frient) Zigbee Smoke Detector
 *
 *  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.
 *
 *   Original DTH on SmartThings by cuboy29
 *       Converted to Hubitat and extensivly modified by Scruffy-SJB
 *
 *   Code snippets copied from veeceeoh, Daniel Terryn and Marcus Liljergren - with thanks.
 */

// Ver: 2.0 - Parsed another unhandled battery catch-all
// Ver: 1.9 - Added fingerprint for frient Intelligent Smoke Alarm
// Ver: 1.8 - Added fingerprint for model SmokeSensor-EF-3.0  Enhanced status message processing.  New Current State - Date/Time last status recorded.
// Ver: 1.7 - Added fingerprint for model HS1CA-M HEIMAN Smart Carbon Monoxide Sensor
// Ver: 1.6 - Added fingerprint for model HS1SA-M
// Ver: 1.5 - Added ability to detect a device test and to trigger a last tested event
// Ver: 1.4 - Parsed another unhandled catch-all
// Ver: 1.3 - Updated to support Zigbee 3.0 version HS1SA-E

import hubitat.zigbee.clusters.iaszone.ZoneStatus
 
metadata {
	definition (name: "Heiman Zigbee Smoke Detector", namespace: "scruffy-sjb", author: "scruffy-sjb and cuboy29") {
		
        capability "Configuration"
        capability "Smoke Detector"
        capability "SmokeDetector"
        capability "Sensor"
        capability "Refresh"
        capability "Battery"
        
		command "resetToClear"
        command "resetBatteryReplacedDate"
        
        attribute "smoke", "string"
        attribute "batteryLastReplaced", "string"
        attribute "sensorLastTested", "string"
        attribute "lastStatus", "string"
          
        fingerprint profileID: "0104", deviceID: "0402", inClusters: "0000,0001,0003,0500,0502", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-EM", deviceJoinName: "HEIMAN Smoke Detector" //HEIMAN Smoke Sensor (HS1SA-E)
        fingerprint profileID: "0104", deviceID: "0402", inClusters: "0000,0003,0500,0001,0009,0502", outClusters: "0019", manufacturer: "HEIMAN", model: "SMOK_V16", deviceJoinName: "HEIMAN Smoke Detector M" //HEIMAN Smoke Sensor (HS1SA-M)
        fingerprint profileID: "0104", deviceID: "0402", inClusters: "0000,0003,0001,0500,0502,0B05", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-N-3.0", deviceJoinName: "HEIMAN Smoke Detector 3.0" //HEIMAN Smoke Sensor (HS1SA-E)
        fingerprint profileID: "0104", deviceID: "0402", inClusters: "0000,0001,0003,0500", manufacturer: "HEIMAN", model: "COSensor-EM", deviceJoinName: "HEIMAN CO Sensor" //HEIMAN Smart Carbon Monoxide Sensor (HS1CA-E)
        fingerprint profileID: "0104", deviceID: "0402", inClusters: "0000,0001,0003,0500,0502,0B05", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-EF-3.0", deviceJoinName: "HEIMAN Smoke Detector" //HEIMAN Smoke Sensor 
        fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,000F,0020,0500,0502", outClusters: "000A,0019", manufacturer: "frient A/S", model :"SMSZB-120", deviceJoinName: "frient Smoke Detector" // frient Intelligent Smoke Alarm
    }
}

def SensorTestOptions = [:]
	SensorTestOptions << ["1" : "Yes"] // 0x01
	SensorTestOptions << ["0" : "No"]  // 0x00

preferences {
	input "SensorTest", "enum", title: "Enable Sensor Testing", options: SensorTestOptions, description: "Default: Yes", required: false, displayDuringSetup: true
}        

def parse(String description) {
    def descMap = [:]
    
	if (description?.startsWith('zone status')) {
			descMap = parseIasMessage(description)
    }else if (description?.startsWith('enroll request')) {
		    List cmds = zigbee.enrollResponse()
		    descMap = cmds?.collect { new hubitat.device.HubAction(it) }
	}else if (description?.startsWith('catchall')) {
            descMap = parseCatchAllMessage(description)
    }else if (description?.startsWith('read attr'))  {  
            descMap = zigbee.parseDescriptionAsMap(description)
            if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0001) && (descMap.value == '0028')){  //Zone Type
                log.debug "Zone Type is Fire Sensor"
			}else if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0000) && (descMap.value == '01')){  //Zone State
                log.debug "Zone State is enrolled"
			}else if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0002) && ((descMap.value == '20') || (descMap.value == '0020'))){  //Zone Status Clear
                SmokeOrClear("clear")    
			}else if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0002) && ((descMap.value == '30') || (descMap.value == '0030'))){  //Zone Status Clear
                SmokeOrClear("clear") 
			}else if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0002) && ((descMap.value == '0031') || (descMap.value == '0021'))){  //Zone Status Smoke
                SmokeOrClear("detected")              
			}else if ((descMap?.cluster == "0502" && descMap.attrInt == 0x0000)){  //Alarm Max Duration
                def int alarmMinutes = Integer.parseInt(descMap.value,16) 
                log.debug "Max Alarm Duration is ${alarmMinutes} seconds"              
			}else if ((descMap?.cluster == "0000" && descMap.attrInt == 0x0007) && ((descMap.value == '03') )){  //PowerSource
                log.debug "${device.displayName} is Battery Powered"    
			}else if ((descMap?.cluster == "0001" && descMap.attrInt == 0x0020)) {  //Battery Voltage
                def batteryVoltage = ConvertHexValue(descMap.value)
                handleBatteryEvent(batteryVoltage)
            }else if ((descMap?.cluster == "0001" && descMap.attrInt == 0x0051)) {  //Battery Cells
                log.debug "Battery Attribute 0x0051 Received"
                def batteryCells = ConvertHexValue(descMap.value)
                handleCellsEvent(batteryCells)   
			}else if ((descMap?.cluster == "0001" && descMap.attrInt == 0x0033)) {  //Battery Cells
                log.debug "Battery Attribute 0x0033 Received"
                def batteryCells = ConvertHexValue(descMap.value)
                handleCellsEvent(batteryCells)   
            }else if ((descMap?.cluster == "0001" && descMap.attrInt == 0x0021)) {  //Battery Percentage Remaining
                log.debug "Battery Attribute 0x0021 Received"                                                                 //  -- Ignore Attribute                                                             
            }else if (descMap?.cluster == "0000" && descMap.attrInt == 0x0004){  //Manufacture
                sendEvent(name: "manufacture", value: descMap.value)
                log.debug "Manufacturer is ${descMap.value}"
            }else if (descMap?.cluster == "0000" && descMap.attrInt == 0x0005){  //Model 
                sendEvent(name: "model", value: descMap.value)
                log.debug "Model is ${descMap.value}"
            }else {log.debug "Unknown --> Cluster-> ${descMap?.cluster}  AttrInt-> ${descMap.attrInt}  Value-> ${descMap.value}"
            }
       // log.debug "Cluster-> ${descMap?.cluster}  AttrInt-> ${descMap.attrInt}  Value-> ${descMap.value}"
    }else { 
        log.debug "Unparsed -> $description" 
        descMap = zigbee.parseDescriptionAsMap(description)
    }
    // log.debug "$descMap"
	return descMap   
}    

private parseCatchAllMessage(String description) {
    
    Map resultMap = [:]
    def descMap = zigbee.parse(description)  
    if (shouldProcessMessage(descMap)) {
        log.debug descMap.inspect()               
    }
    return resultMap
}

private boolean shouldProcessMessage(cluster) {
    // 0x0B is default response indicating message got through
    // 0x07 is bind message
    // 0x04 - No Idea !!!!!
    boolean ignoredMessage = cluster.profileId != 0x0104 || 
        cluster.command == 0x0B ||
        cluster.command == 0x07 ||
        cluster.command == 0x04 ||        
        (cluster.data.size() > 0 && cluster.data.first() == 0x3e)
    return !ignoredMessage
}

def refresh() {
	log.debug "Refreshing..."
	def refreshCmds = []
    
    refreshCmds +=
	zigbee.readAttribute(0x0500, 0x0001) +	   // IAS ZoneType
    zigbee.readAttribute(0x0500, 0x0000) +	   // IAS ZoneState
    zigbee.readAttribute(0x0500, 0x0002) +	   // IAS ZoneStatus
    zigbee.readAttribute(0x0502, 0x0000) +	   // Alarm Max Duration
    zigbee.readAttribute(0x0000, 0x0007) +	   // Power Source
    zigbee.readAttribute(0x0001, 0x0020) +     // Battery Voltage      
    zigbee.readAttribute(0x0001, 0x0033) +     // Battery Cells         
    zigbee.readAttribute(0x0000, 0x0004) +	   // Manufacturer Name
    zigbee.readAttribute(0x0000, 0x0005) +	   // Model Indentification
    zigbee.enrollResponse()
    
	return refreshCmds
}

def configure() {
    log.debug "Configuring..."
    
	if (!device.currentState('batteryLastReplaced')?.value)
		resetBatteryReplacedDate(true)
    
    def cmds = [
        // Bindings
        "zdo bind 0x${device.deviceNetworkId} 1 1 0x0500 {${device.zigbeeId}} {}", "delay 200"
        ] 
        // cmds += zigbee.enrollResponse(1200) 
        cmds += zigbee.enrollResponse() 
        cmds += zigbee.configureReporting(0x0500, 0x0002, 0x19, 0, 3600, 0x00)
        cmds += zigbee.configureReporting(0x0001, 0x0020, 0x20, 600, 7200, 0x01) 
        return cmds + refresh()
}

def resetBatteryReplacedDate(paired) {
	def newlyPaired = paired ? " for newly paired sensor" : ""
	sendEvent(name: "batteryLastReplaced", value: new Date())
	log.debug "${device.displayName} Setting Battery Last Replaced to Current date${newlyPaired}"
}

def resetSensorTestedDate() {
    def newlyTested=""
    sendEvent(name: "sensorLastTested", value: new Date())
    log.debug "${device.displayName} Setting Sensor Last Tested to Current date${newlyTested}"
}

def resetToClear() {
	sendEvent(name:"smoke", value:"clear")
    sendEvent(name: "lastStatus", value: new Date(), displayed: True)
    log.debug "Resetting to Clear..."
	didWeGetClear = 0
}

/**
 * Code borrowed (mixed and matched) from both Daniel Terryn and veeceeoh
 *
 * Create battery event from reported battery voltage.
 *
 */

private handleBatteryEvent(rawVolts) {
    rawVolts = rawVolts / 10.0
	def minVolts = voltsmin ? voltsmin : 2.5
	def maxVolts = voltsmax ? voltsmax : 3.0
	def pct = (rawVolts - minVolts) / (maxVolts - minVolts)
	def roundedPct = Math.min(100, Math.round(pct * 100))
	log.debug "Battery level is ${roundedPct}% (${rawVolts} Volts)"
	
    sendEvent(name:"battery", value: roundedPct, unit: "%", isStateChange: true)
	return 
}

private handleCellsEvent(noCells) {
	log.debug "Battery reports that it has (${noCells} Cells)"
	return 
}

def ConvertHexValue(value) {
	if (value != null)
	{
		return Math.round(Integer.parseInt(value, 16))
	}
}

def SmokeOrClear(value) {
    if (value == "clear") {
        sendEvent(name:"smoke", value:"clear", isStateChange: true)
        log.info "${device.displayName} reports status is all clear"
    } else {
        sendEvent(name:"smoke", value:"detected", isStateChange: true)
        log.info "${device.displayName} reports smoke is detected"
    }
    
    sendEvent(name: "lastStatus", value: new Date(), displayed: True)
}

private Map parseIasMessage(String description) {
    // log.debug "Zone Status Received--> ${description}"
    ZoneStatus zs = zigbee.parseZoneStatus(description)    
    translateZoneStatus(zs)    
}

private Map translateZoneStatus(ZoneStatus zs) {
	// Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion       
	return getSmokeResult(zs.isAlarm1Set() || zs.isAlarm2Set())
}

private Map getSmokeResult(value) {
	if (value) {
        if (SensorTest == "") {
            SensorTest = "1"    // Default is Yes
        }
        
        if (SensorTest == "1") {
    		def descriptionText = "${device.displayName} status is pending"
            sendEvent(name: "lastStatus", value: new Date(), displayed: True)
	    	log.debug descriptionText
		    runIn(3,EventOrTest)
		    return [
			    name			: 'smoke',
			    value			: 'pending',
                isStateChange: false,
			    descriptionText : descriptionText
            ]
        } else {
    		def descriptionText = "${device.displayName} testing disabled - smoke detected !!!"
	    	log.debug descriptionText
            sendEvent(name: "lastStatus", value: new Date(), displayed: True)
		    return [
			    name			: 'smoke',
			    value			: 'detected',
                isStateChange: true,
			    descriptionText : descriptionText
            ]
        }                 
   } else {
		def descriptionText = "${device.displayName} all clear"
		log.debug descriptionText
        sendEvent(name: "lastStatus", value: new Date(), displayed: True)
		return [
			name			: 'smoke',
			value			: 'clear',
            isStateChange: true,
			descriptionText : descriptionText
		]
	}
}		

def EventOrTest() {
    if (device.currentValue("smoke") == "clear") {
		log.debug "${device.displayName} was tested sucessfully"
        resetSensorTestedDate()
	} else {
		log.debug "${device.displayName} This was not a test - smoke detected !!!"
		sendEvent(name:"smoke", value:"detected")
        sendEvent(name: "lastStatus", value: new Date(), displayed: True)
	}
}
2 Likes

Thanks a lot! I'll keep on testing and let you know. I suppose the "unknown cluster" line is now "dev:852021-09-27 13:18:31.418 debugBattery reports that it has (0 Cells)"; what ever that means.

Yep - that's what was the unknown cluster - reporting that the battery has no cells is nonsensical.... but that's that was in the unknown cluster data as well...........

Hi. Well... removing and re-adding the detectors with the new driver did not make any success for status or battery reports: nothing still seen after the re-install few days ago. The alarms are passing through, which is - of course - the biggest thing :smiling_face_with_three_hearts:, but without the status reports it is important to perform the detector tests from the button regularly for controlling the devices are still online. Thank you, Simon, for your great efforts!

I just successfully tested my Heiman Smoke Detector HS1SA-E using this driver (update from Sept 2021 above).
Not sure if I had to, but I uninstalled the device using the previous driver; and also disarmed/re-armed HSM.
This is just to confirm for others buying that detector.

@simon Any thoughts about adding your driver to the Hubitat Package Manager?

Yes - something that's on my "to-do list"

S

Should be in HPM now.......

2 Likes

@simon, I'm among your first very satisfied HPM users. Thank you!

Just installed the first of my Heiman CO Sensors (zigbee) with a couple smokes to follow. One question: I just added the CO sensor after installing both the CO and Smoke drivers. But the CO sensor was paired as the "Heiman Zigbee Smoke Detector" even though under Current States, it says "COSensor-EF-3.0", as I'd expect.

Shouldn't the fingerprint have picked out the CO device handler, or would you recommend I switch DH's manually? TIA....

The CO and the Smokie drivers are very similar and so are the fingerprints.

I would just manually change the driver on the device .

Thanks,
Simon

Yeah that worked. Just strange cuz other sibling-ish devices don't have that problem.
Whatevs NBD -- love this, thanks again.

1 Like

Used HPM Match Up. Worked well. Thanks

1 Like

I'm from belgium and was looking for a driver that works with
' klikaanklikuit ' ZSDR-850
https://klikaanklikuit.nl/product/slimme-draadloze-rookmelder/

I found it verry much looking like Smart Smoke Sensor Heiman HS1SA

so I tried it and kinda works...
Don't know how to use it now in hubitat... beside the battery-% and the 'smoke: clear' nothing much happens... not even a test button from the hubitat.

I wanted to mention it for other dutch speaking ppl or when somebody is trying to find this topic or info.

1 Like

That sure looks like a Heiman.

In the logs battery %age and clear messages are normal.

If you press the test button on the device it should give you a Smoke event and it should make plenty of noise. If you have testing enabled in the driver it will tell you that the device was tested.

That's about all it does......

Thanks

Hi, I also have the same "klikaanklikuit" sensor. It looks very much similar to Heiman. However, according to the device data it is build by Trust.
Trust.com - Zigbee Smoke Detector ZSDR-850

Like @gertjan.deprez says, it kinda works... However, if I do a test, there is no mentioning of a test being done. Nothing appears in the logs... Which does not give me much confidence.
I've added a screenshot of the device data.

image

I'm happy to help doing some testing, if someone could point me out in the right direction....

Yes - it is being seen as the same as a Heiman HSISA-E

If it transmits something that the driver does not recognize there should be in the log a message that starts with "Unknown --> Cluster->" - if you hit the test button on the device can you check if anything like that appears in the logs?

Thanks,
Simon

Hi Simon,
[.. removed not relevant text ...]
Thanks for creating this driver btw :slight_smile:
Cheers, Henk

UPDATE: I've removed the device and reconnected it to the hub. It now works as a charm!

Great. The device gets a configuration command to tell it to send battery reports, I forget the default frequency.. It does work.