Heiman Smart Smoke Sensor HS1SA Zigbee

The only unusual thing here is the "Unknown --> Cluster-> 0001 AttrInt-> 33 Value-> 7E"

Looking it up it seems to be a count of the number of battery cells in the device - 126. Which does not sound right at all.

How about trying a real smoke test to see what happens?

For the fun of it I ordered a Heiman z-wave smokie (I discovered that's what alarm installers call them) today. When it gets here maybe end of the week, I'll see if I can do something with a driver...

1 Like

That would be great, let me know when and I will test from my side.

Thanks

I received today a real alarm with my Heiman smoke alarm: my wife was making some cookies and she opened the oven (placed below the alarm sensor) to remove the cookies (hot vapours). So, not a real fire/smoke but the sensor was triggered as expected and not through the test button.

I received a warning (google home speakers and notification) and this is the log:

1 Like

Hi @simon thanks for your efforts to date on this driver. It appears I have the same device type as @kuzenkohome, with the same inclusters etc.
My device registers with the Hub OK, but does not register anything at all in the logs when I push the test button...just the loud beeps from the device itself is all i get. i do get these messages about an incomplete fingerprint not sure what it means?

2020-08-12 09:42:52.720 infoINCOMPLETE - DO NOT SUBMIT THIS - TRY AGAIN: fingerprint model:"SmokeSensor-N-3.0", manufacturer:"HEIMAN", profileId:"0104", endpointId:"01", inClusters:"0000,0003,0001,0500,0502,0B05", outClusters:"0019", application:"14"

dev:6432020-08-12 09:42:52.717 debugsendZigbeeCommands(cmd=[he raw F2B0 0 0 0x0004 {00 B0F2 01} {0x0000}])

dev:6432020-08-12 09:42:52.710 warnOne or multiple pieces of data needed for the fingerprint is missing, requesting it from the device. If it is a sleepy device you may have to wake it up and run this command again. Run this command again to get the new fingerprint.

dev:6432020-08-12 09:42:52.706 traceApplication: 14

dev:6432020-08-12 09:42:52.702 traceModel: SmokeSensor-N-3.0

dev:6432020-08-12 09:42:52.699 traceManufacturer: HEIMAN

dev:6432020-08-12 09:42:52.693 debugGetting info for Zigbee device...

dev:6432020-08-12 09:42:36.729 infoINCOMPLETE - DO NOT SUBMIT THIS - TRY AGAIN: fingerprint model:"SmokeSensor-N-3.0", manufacturer:"HEIMAN", profileId:"0104", endpointId:"01", inClusters:"0000,0003,0001,0500,0502,0B05", outClusters:"0019", application:"14"

dev:6432020-08-12 09:42:36.725 debugsendZigbeeCommands(cmd=[he raw F2B0 0 0 0x0004 {00 B0F2 01} {0x0000}])

dev:6432020-08-12 09:42:36.717 warnOne or multiple pieces of data needed for the fingerprint is missing, requesting it from the device. If it is a sleepy device you may have to wake it up and run this command again. Run this command again to get the new fingerprint.

dev:6432020-08-12 09:42:36.713 traceApplication: 14

dev:6432020-08-12 09:42:36.710 traceModel: SmokeSensor-N-3.0

dev:6432020-08-12 09:42:36.706 traceManufacturer: HEIMAN

dev:6432020-08-12 09:42:36.701 debugGetting info for Zigbee device...

dev:6432020-08-12 09:42:23.434 debugsendZigbeeCommands(cmd=[he raw 0xF2B0 1 0x01 0x0000 {10 00 00 04 00}, delay 2000, he raw 0xF2B0 1 0x01 0x0000 {10 00 00 05 00}, delay 2000])

dev:6432020-08-12 09:42:23.430 debugrefresh cmd: [he raw 0xF2B0 1 0x01 0x0000 {10 00 00 04 00}, delay 2000, he raw 0xF2B0 1 0x01 0x0000 {10 00 00 05 00}, delay 2000]

dev:6432020-08-12 09:42:23.402 debugrefresh() model='SmokeSensor-N-3.0'

dev:6432020-08-12 09:42:23.397 infoupdated()

dev:6432020-08-12 09:41:55.353 infoCurrent Preferences: [:]

dev:6432020-08-12 09:40:32.283 infoINCOMPLETE - DO NOT SUBMIT THIS - TRY AGAIN: fingerprint model:"SmokeSensor-N-3.0", manufacturer:"HEIMAN", profileId:"0104", endpointId:"01", inClusters:"0000,0003,0001,0500,0502,0B05", outClusters:"0019", application:"14"

dev:6432020-08-12 09:40:32.270 warnOne or multiple pieces of data needed for the fingerprint is missing, requesting it from the device. If it is a sleepy device you may have to wake it up and run this command again. Run this command again to get the new fingerprint.

dev:6432020-08-12 09:40:32.267 traceApplication: 14

dev:6432020-08-12 09:40:32.245 traceModel: SmokeSensor-N-3.0

dev:6432020-08-12 09:40:32.242 traceManufacturer: HEIMAN

dev:6432020-08-12 09:40:32.235 debugGetting info for Zigbee device...

dev:6432020-08-12 09:38:47.708 debugRefreshing...

dev:6432020-08-12 09:38:47.690 debugConfiguring...

dev:6432020-08-12 09:25:34.800 debugRefreshing...

dev:6432020-08-12 09:25:09.189 errorgroovy.lang.MissingMethodException: No signature of method: user_driver_hubitat_Heiman_Zigbee_Smoke_Detector_719.ping() is applicable for argument types: () values: [] Possible solutions: find(), print(java.lang.Object), print(java.io.PrintWriter), print(java.lang.Object), find(groovy.lang.Closure), run() (ping)

dev:6432020-08-12 09:24:12.427 debugRefreshing...

dev:6432020-08-12 09:24:12.416 debugConfiguring...

Can you double check the driver you are using? Are you sure it's the driver I posted here?

Thanks,
Simon

hi there @simon yep did that earlier today, i re-pasted into the Device driver page the current copy of the driver from this thread. i also removed the device and re-paired it (and of course selected your driver).
looking through your code, I don't see any procedure that refers to ping(), so i'm assuming thats the reason why the ping button on the device page doesn't work and those MissingMethod errors appear in the logs.
As for the base driver not appearing to work...and the incomplete fingerprint weirdness...i don't know. its a brand new device that has HS1SA-E marked on its side as its product code.
when i push the (test) button on the side of the HS1SA-E, i get the 3 loud beeps but absolutely nothing appears in the Hubitat logs, and the device page does not register any event or change in state.

quick question: is this supposed to be the last few lines of the driver? thats how it ends as seen on the thread...

Yes - that's correct. It's taking a hex value and turning it into a regular number.

Modified the code to include the fingerprint from above. Seems to be a Zigbee 3.0 version.
I put in an extra debug statement to display any Zone Status received in from the device, hope this will tell us what its sending when you hit the test button.
I removed the ping - I could never get the device to respond consistently (I'm guessing the device was asleep).
I added in a Battery Cell status message that @kuzenkohome was getting.

/**
 * 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 == 0x0033)) {  //Battery Cells
                def batteryCells = ConvertHexValue(descMap.value)
                handleCellsEvent(batteryCells)   
            }else if ((descMap?.cluster == "0001" && descMap.attrInt == 0x0021)) {  //Battery Percentage Remaining
                                                                                    //  -- 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)
	}
}

thanks @simon, much appreciated, i've updated the code in the driver to this latest version, and clicked 'refresh' and 'reset to clear'. its midnight now so will wait till morning before doing any tests which would wake the family (and probably the neighbours too :slight_smile:)

Great - I'm in Ireland right now - only a little after 3pm here.....

Success, The unit has a "tamper" switch on the bottom that is depressed when connected to the mounting base. With the base in place, self test button only reports "clear" which is logical. With the base removed it reports "smoke" when the self test button is pressed , then reports "clear" . Log below and the device event log is reporting properly also.

debugSmoke Sensor 1 all clear
debugZone Status Received--> zone status 0x0020 -- extended status 0x00
debugSmoke Sensor 1 has detected Smoke !!!
debugZone Status Received--> zone status 0x0021 -- extended status 0x00

My measured battery voltage is 3.06, driver reports 2.9

Your driver needs to have the device fingerprint changed to swap positions of the '0003' & '0001' clusters otherwise device discovery won't select your driver.

It needs to be exactly like this.
fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0001,0500,0502,0B05", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-N-3.0", deviceJoinName: "HEIMAN Smoke Sensor (HS3SA)"

Here's the log just for info purposes..

debugSmoke Sensor 1 all clear
debugZone Status Received--> zone status 0x0020 -- extended status 0x00
infoZigbee parsed:[raw:25940800060800002000, dni:2594, endpoint:08, cluster:0006, size:08, attrId:0000, encoding:20, command:0A, value:00, clusterInt:6, attrInt:0]
debugModel is SmokeSensor-N-3.0
debugSmoke Sensor 1 all clear
debugZone Status Received--> zone status 0x0020 -- extended status 0x00
debugSmartShield(text: null, manufacturerId: 0x0000, direction: 0x01, data: [], number: null, isManufacturerSpecific: false, messageType: 0x00, senderShortId: 0x2a85, isClusterSpecific: false, sourceEndpoint: 0x01, profileId: 0x0104, command: 0x01, clusterId: 0x0001, destinationEndpoint: 0x01, options: 0x0040)
debugBattery level is 80% (2.9 Volts)
infoZigbee Discovery Stopped
debugSmoke Sensor 1 reports status is all clear
debugZone State is enrolled
debugZone Type is Fire Sensor
infoMotion Dining Room temperature is 75.39°F
debugUnknown --> Cluster-> 0001 AttrInt-> 33 Value-> BA
infoCreated Zigbee Device Heiman Zigbee Smoke Detector
debugRefreshing...
debugSetting Battery Last Replaced to current date for newly paired sensor
debugConfiguring...

1 Like

Hi Guys,
I was able to make this sensor working using above driver. The only question I have - which option should I use in dashboard in order to have this sensor to be well visible and alerted when needed (detected information on dashboard)

I've updated the fingerprint.
I've now removed my debug statement
I saw in the log that battery cells was still not being detected - value reported in log is decimal, code tests for the value in hex - I've corrected that.

Many thanks @kuzenkohome

/**
 * Heiman 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: 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: "hubitat", author: "cuboy29") {
		
        capability "Configuration"
        capability "Smoke Detector"
        capability "SmokeDetector"
        capability "Sensor"
        capability "Refresh"
        capability "Battery"
        
		command "resetToClear"
        command "resetBatteryReplacedDate"
        
        attribute "smoke", "string"
        attribute "batteryLastReplaced", "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,0001,0500,0502,0B05", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-N-3.0", deviceJoinName: "HEIMAN Smoke Detector 3.0" //HEIMAN Smoke Sensor (HS1SA-E)
    }
}

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 == '0030') || (descMap.value == '0020'))){  //Zone Status Clear
                sendEvent(name:"smoke", value:"clear")
                log.debug "${device.displayName} reports status is all clear"
			}else if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0002) && ((descMap.value == '0031') || (descMap.value == '0021'))){  //Zone Status Smoke
                sendEvent(name:"smoke", value:"detected")
                log.debug "${device.displayName} reports status is smoke"                
			}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 == 0x0021)) {  //Battery Cells
                def batteryCells = ConvertHexValue(descMap.value)
                handleCellsEvent(batteryCells)                
            }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}"
	}
	return descMap   
}    


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

    def descriptionText = value ? "${device.displayName} has detected Smoke !!!" : "${device.displayName} all clear"
    log.debug descriptionText
	return [
			name			: 'smoke',
			value			: value ? 'detected' : 'clear',
            isStateChange: true,
			descriptionText : descriptionText
	]
}

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

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

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",
        ] +  zigbee.enrollResponse(1200) + zigbee.configureReporting(0x0500, 0x0002, 0x19, 60, 3600, 0x01) + "delay 200" + 
        zigbee.configureReporting(0x0001, 0x0020, 0x20, 600, 7200, 0x01) + refresh()
    
    return cmds 
}

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

def resetToClear() {
	sendEvent(name:"smoke", value:"clear")
    log.debug "Resetting to Clear..."
}

/**
 * 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))
	}
}

You can create a dashboard tile for the device. There is a template for Smoke. The tile will show if it's Clear or if Smoke is Detected.

The default tile does not change color when Smoke is Detected - but I'm sure that if you were to use something like Smartly to enhance your dashboard you would be able to do that.

I just bought a HS1SA model and use it with your driver. I don't see any events except last battery replace.

ID: 0FA1
Manufacturer: HEIMAN  
Product Name:
Model Number: SmokeSensor-N-3.0
deviceTypeId: 706
manufacturer : HEIMAN
idAsInt : 1
inClusters : 0000,0003,0001,0500,0502,0B05
endpointId : 01
profileId : 0104
application : 14
outClusters : 0019
initialized : true
model : SmokeSensor-N-3.0
stage : 4

after a reboot is seems that the test smoke event is executed. NVM :wink:

@simon

sensor is sending battery reports (shows up in log) but driver is not registering the event in the device event log. Here's two.

dev:18372020-08-26 08:15:53.011 pm debugCluster-> 0001 AttrInt-> 33 Value-> BA
dev:18372020-08-26 08:15:53.010 pm debugBattery reports that it has (186 Cells)
dev:18372020-08-26 06:15:34.573 pm debugCluster-> 0001 AttrInt-> 33 Value-> BA
dev:18372020-08-26 06:15:34.571 pm debugBattery reports that it has (186 Cells)

Here's an update............

/**
 * Heiman 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: 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: "hubitat", author: "cuboy29") {
		
        capability "Configuration"
        capability "Smoke Detector"
        capability "SmokeDetector"
        capability "Sensor"
        capability "Refresh"
        capability "Battery"
        
		command "resetToClear"
        command "resetBatteryReplacedDate"
        
        attribute "smoke", "string"
        attribute "batteryLastReplaced", "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,0001,0500,0502,0B05", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-N-3.0", deviceJoinName: "HEIMAN Smoke Detector 3.0" //HEIMAN Smoke Sensor (HS1SA-E)
    }
}

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'))){  //Zone Status Clear
                sendEvent(name:"smoke", value:"clear")                
			}else if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0002) && ((descMap.value == '0030') || (descMap.value == '0020'))){  //Zone Status Clear
                sendEvent(name:"smoke", value:"clear")
                log.debug "${device.displayName} reports status is all clear"
			}else if ((descMap?.cluster == "0500" && descMap.attrInt == 0x0002) && ((descMap.value == '0031') || (descMap.value == '0021'))){  //Zone Status Smoke
                sendEvent(name:"smoke", value:"detected")
                log.debug "${device.displayName} reports status is smoke"                
			}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 == 0x0021)) {  //Battery Cells
                def batteryCells = ConvertHexValue(descMap.value)
                handleCellsEvent(batteryCells)                
            }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}"
	}
	return descMap   
}    


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

    def descriptionText = value ? "${device.displayName} has detected Smoke !!!" : "${device.displayName} all clear"
    log.debug descriptionText
	return [
			name			: 'smoke',
			value			: value ? 'detected' : 'clear',
            isStateChange: true,
			descriptionText : descriptionText
	]
}

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

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

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",
        ] +  zigbee.enrollResponse(1200) + zigbee.configureReporting(0x0500, 0x0002, 0x19, 60, 3600, 0x01) + "delay 200" + 
        zigbee.configureReporting(0x0001, 0x0020, 0x20, 600, 7200, 0x01) + refresh()
    
    return cmds 
}

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

def resetToClear() {
	sendEvent(name:"smoke", value:"clear")
    log.debug "Resetting to Clear..."
}

/**
 * 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))
	}
}

Updated driver, will see what happens.