Sengled Smart Light Switch

That's indeed a curious design decision on their part. However, you can fill in that gap with a tiny bit of caulk. I'd imagine most people who mount these on the wall don't plan on moving them around very often anyway.

Any update on support for the Sengled Smart Light Switch?

Hue dimmer and the AduroSmart are both in the compatible devices list. Why not just use those?

AduroSmart ERIA Smart Wireless Dimming Starter Kit, 1 Wireless Controlled Soft White A19 Bulb and 1 Wireless Dimming Switch https://www.amazon.com/dp/B07HJJBL5X/ref=cm_sw_r_cp_api_i_Hx76Db1199BFM

I did order AduroSmart ERIA Smart Wireless Dimming switch yesterday.

I was at Best Buy yesterday and saw the Sengled switch and picked one up thinking it would work.

1 Like

Sengled never got back to me on the issues I was having with it, so not much hope currently.

I was able to connect a Sengled Smart Light Switch as part of my migration from ST to HE. All buttons and functions (double press and hold) work fine for me.

I used ST code of a "ZigBee Battery Accessory Dimmer" Device Type with a single modification. "physicalgraph" needs to be replaced with "hubitat" in line 14.

Code that works for me:

/**
 *	Copyright 2019 SmartThings
 *
 *	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.
 *
 */
import hubitat.zigbee.zcl.DataType

metadata {
	definition (name: "ZigBee Battery Accessory Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch") {
		capability "Actuator"
		capability "Battery"
		capability "Configuration"
		capability "Health Check"
		capability "Switch"
		capability "Switch Level"

		// Sengled Switch is moved to the CST because of issues with battery reports so our way to resolve this is to hide the battery in OneApp by using metadata without it.
		fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,FC11", outClusters: "0003,0004,0006,0008,FC10", manufacturer: "sengled", model: "E1E-G7F", deviceJoinName: "Sengled Smart Switch", mnmn:"SmartThings", vid: "generic-dimmer"
		fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI wireless dimmer", deviceJoinName: "IKEA TRÅDFRI Wireless dimmer" // 01 [0104 or C05E] 0810 02 06 0000 0001 0003 0009 0B05 1000 06 0003 0004 0006 0008 0019 1000
		fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0B05", outClusters: "0003,0006,0008,0019", manufacturer: "Centralite Systems", model: "3131-G", deviceJoinName: "Centralite Smart Switch"
	}

	tiles(scale: 2) {
		multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){
			tileAttribute ("device.switch", key: "PRIMARY_CONTROL") {
				attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
				attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"turningOff"
				attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn"
			}
			tileAttribute ("device.level", key: "SLIDER_CONTROL") {
				attributeState "level", action:"switch level.setLevel"
			}
			tileAttribute ("device.battery", key: "SECONDARY_CONTROL") {
				attributeState "battery", label: 'battery ${currentValue}%', unit: "%"
			}
		}
		main "switch"
		details(["switch"])
	}
}

def getDOUBLE_STEP() { 10 }
def getSTEP() { 5 }

def getONOFF_ON_COMMAND() { 0x0001 }
def getONOFF_OFF_COMMAND() { 0x0000 }
def getLEVEL_MOVE_LEVEL_COMMAND() { 0x0000 }
def getLEVEL_MOVE_COMMAND() { 0x0001 }
def getLEVEL_STEP_COMMAND() { 0x0002 }
def getLEVEL_STOP_COMMAND() { 0x0003 }
def getLEVEL_MOVE_LEVEL_ONOFF_COMMAND() { 0x0004 }
def getLEVEL_MOVE_ONOFF_COMMAND() { 0x0005 }
def getLEVEL_STEP_ONOFF_COMMAND() { 0x0006 }
def getLEVEL_STOP_ONOFF_COMMAND() { 0x0007 }
def getLEVEL_DIRECTION_UP() { "00" }
def getLEVEL_DIRECTION_DOWN() { "01" }

def getBATTERY_VOLTAGE_ATTR() { 0x0020 }
def getBATTERY_PERCENT_ATTR() { 0x0021 }

def getMFR_SPECIFIC_CLUSTER() { 0xFC10 }

def getUINT8_STR() { "20" }


private boolean isIkeaDimmer() {
	device.getDataValue("model") == "TRADFRI wireless dimmer"
}
private boolean isSengledSwitch() {
	device.getDataValue("model") == "E1E-G7F"
}
private boolean isCentraliteSwitch() {
	device.getDataValue("model") == "3131-G"
}

def parse(String description) {
	log.debug "description is $description"
	def results = []

	def event = zigbee.getEvent(description)
	if (event) {
		results << createEvent(event)
	} else {
		def descMap = zigbee.parseDescriptionAsMap(description)

		if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER) {
			results = handleBatteryEvents(descMap)
		} else if (isSengledSwitch()) {
			results = handleSengledSwitchEvents(descMap)
		} else if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) {
			results = handleSwitchEvent(descMap)
		} else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) {
			if (isCentraliteSwitch()) {
				results = handleCentraliteSmartSwitchLevelEvent(descMap)
			} else if (isIkeaDimmer()) {
				results = handleIkeaDimmerLevelEvent(descMap)
			}
		} else {
			log.warn "DID NOT PARSE MESSAGE for description : $description"
			log.debug "${descMap}"
		}
	}

	log.debug "parse returned $results"
	return results
}

def handleSengledSwitchEvents(descMap) {
	def results = []

	if (descMap?.clusterInt == MFR_SPECIFIC_CLUSTER && descMap.data) {
		def currentLevel = device.currentValue("level") as Integer ?: 0
		def value = currentLevel

		switch (descMap.data[0]) {
			case '01':
				//short press of 'ON' button
				results << createEvent(name: "switch", value: "on")
				break
			case '02':
				// move up
				if (descMap.data[2] == '02') {
					//long press of 'BRIGHTEN' button
					value = Math.min(currentLevel + DOUBLE_STEP, 100)
				} else if (descMap.data[2] == '01') {
					//short press of 'BRIGHTEN' button
					value = Math.min(currentLevel + STEP, 100)
				} else {
					log.info "Invalid value ${descMap.data[2]} received for descMap.data[2]"
				}

				results << createEvent(name: "switch", value: "on")
				results << createEvent(name: "level", value: value)
				break
			case '03':
				//move down
				if (descMap.data[2] == '02') {
					//long press of 'DIM' button
					value = Math.max(currentLevel - DOUBLE_STEP, 0)
				} else if (descMap.data[2] == '01') {
					//short press of 'DIM' button
					value = Math.max(currentLevel - STEP, 0)
				} else {
					log.info "Invalid value ${descMap.data[2]} received for descMap.data[2]"
				}

				if (value == 0) {
					results << createEvent(name: "switch", value: "off")
				} else {
					results << createEvent(name: "level", value: value)
				}
				break
			case '04':
				//short press of 'OFF' button
				results << createEvent(name: "switch", value: "off")
				break
			case '06':
				//long press of 'ON' button
				results << createEvent(name: "switch", value: "on")
				break
			case '08':
				//long press of 'OFF' button
				results << createEvent(name: "switch", value: "off")
				break
			default:
				break
		}
	}

	return results
}

def handleCentraliteSmartSwitchLevelEvent(descMap) {
	def results = []

	if (descMap.commandInt == LEVEL_MOVE_ONOFF_COMMAND) {
		// device is sending 0x05 command while long pressing the upper button
		results = handleStepEvent(LEVEL_DIRECTION_UP, descMap)
	} else if (descMap.commandInt == LEVEL_MOVE_COMMAND) {
		//device is sending 0x01 command while long pressing the bottom button
		results = handleStepEvent(LEVEL_DIRECTION_DOWN, descMap)
	}

	return results
}

def handleIkeaDimmerLevelEvent(descMap) {
	def results = []

	if (descMap.commandInt == LEVEL_STEP_COMMAND) {
		results = handleStepEvent(descMap.data[0], descMap)
	} else if (descMap.commandInt == LEVEL_MOVE_COMMAND || descMap.commandInt == LEVEL_MOVE_ONOFF_COMMAND) {
		// Treat Level Move and Level Move with On/Off as Level Step
		results = handleStepEvent(descMap.data[0], descMap)
	} else if (descMap.commandInt == LEVEL_STOP_COMMAND || descMap.commandInt == LEVEL_STOP_ONOFF_COMMAND) {
		// We are not going to handle this event because we are not implementing this the way that the Zigbee spec indicates
		log.debug "Received stop move - not handling"
	} else if (descMap.commandInt == LEVEL_MOVE_LEVEL_ONOFF_COMMAND) {
		// The spec defines this as "Move to level with on/off". The IKEA Dimmer sends us 0x00 or 0xFF only, so we will treat this more as a
		// on/off command for the dimmer. Otherwise, we will treat this as off or on and setLevel.
		if (descMap.data[0] == "00") {
			results << createEvent(name: "switch", value: "off", isStateChange: true)
		} else if (descMap.data[0] == "FF") {
			// The IKEA Dimmer sends 0xFF -- this is technically not to spec, but we will treat this as an "on"
			if (device.currentValue("level") == 0) {
				results << createEvent(name: "level", value: DOUBLE_STEP)
			}

			results << createEvent(name: "switch", value: "on", isStateChange: true)
		} else {
			results << createEvent(name: "switch", value: "on", isStateChange: true)
			// Handle the Zigbee level the same way as we would normally with the same code path -- commandInt doesn't matter right now
			// The first byte is the level, the second two bytes are the rate -- we only care about the level right now.
			results << createEvent(zigbee.getEventFromAttrData(descMap.clusterInt, descMap.commandInt, UINT8_STR, descMap.data[0]))
		}
	}

	return results
}

def handleSwitchEvent(descMap) {
	def results = []

	if (descMap.commandInt == ONOFF_ON_COMMAND) {
		if (device.currentValue("level") == 0) {
			results << createEvent(name: "level", value: DOUBLE_STEP)
		}
		results << createEvent(name: "switch", value: "on")
	} else if (descMap.commandInt == ONOFF_OFF_COMMAND) {
		results << createEvent(name: "switch", value: "off")
	}

	return results
}

def handleStepEvent(direction, descMap) {
	def results = []
	def currentLevel = device.currentValue("level") as Integer ?: 0
	def value = null

	if (direction == LEVEL_DIRECTION_UP) {
		value = Math.min(currentLevel + DOUBLE_STEP, 100)
	} else if (direction == LEVEL_DIRECTION_DOWN) {
		value = Math.max(currentLevel - DOUBLE_STEP, 0)
	}

	if (value != null) {
		log.debug "Step ${direction == LEVEL_DIRECTION_UP ? "up" : "down"} by $DOUBLE_STEP to $value"

		// don't change level if switch will be turning off
		if (value == 0) {
			results << createEvent(name: "switch", value: "off")
		} else {
			results << createEvent(name: "switch", value: "on")
			results << createEvent(name: "level", value: value)
		}
	} else {
		log.debug "Received invalid direction ${direction} - descMap.data = ${descMap.data}"
	}

	return results
}

def handleBatteryEvents(descMap) {
	def results = []

	if (descMap.value) {
		def rawValue = zigbee.convertHexToInt(descMap.value)
		def batteryValue = null

		if (rawValue == 0xFF) {
			// Log invalid readings to info for analytics and skip sending an event.
			// This would be a good thing to watch for and form some sort of device health alert if too many come in.
			log.info "Invalid battery reading returned"
		} else if (descMap.attrInt == BATTERY_VOLTAGE_ATTR && !isIkeaDimmer()) { // Ignore from IKEA Dimmer if it sends this since it is probably 0
			def minVolts = 2.3
			def maxVolts = 3.0
			def batteryValueVoltage = rawValue / 10

			batteryValue = Math.round(((batteryValueVoltage - minVolts) / (maxVolts - minVolts)) * 100)
		} else if (descMap.attrInt == BATTERY_PERCENT_ATTR) {
			// The IKEA dimmer is sending us full percents, but the spec tells us these are half percents, so account for this
			batteryValue = Math.round(rawValue / (isIkeaDimmer() ? 1 : 2))
		}

		if (batteryValue != null) {
			batteryValue = Math.min(100, Math.max(0, batteryValue))

			results << createEvent(name: "battery", value: batteryValue, unit: "%", descriptionText: "{{ device.displayName }} battery was {{ value }}%", translatable: true)
		}
	}

	return results
}

def off() {
	sendEvent(name: "switch", value: "off", isStateChange: true)
}

def on() {
	sendEvent(name: "switch", value: "on", isStateChange: true)
}

def setLevel(value, rate = null) {
	if (value == 0) {
		sendEvent(name: "switch", value: "off")
		// OneApp expects a level event when the dimmer value is changed
		value = device.currentValue("level")
	} else {
		sendEvent(name: "switch", value: "on")
	}
	runIn(1, delayedSend, [data: createEvent(name: "level", value: value), overwrite: true])
}

def delayedSend(data) {
	sendEvent(data)
}

def ping() {
	if (isCentraliteSwitch()) {
		zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR)
	} else {
		zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR)
	}
}

def installed() {
	sendEvent(name: "switch", value: "on")
	sendEvent(name: "level", value: 100)
}

def configure() {
	def offlinePingable = isIkeaDimmer() ? "0" : "1" // We can't ping the IKEA dimmer, so tell device health this
	int reportInterval = 3 * 60 * 60

	// The checkInterval is twice the reportInterval plus lag (1-2 mins allowable)
	sendEvent(name: "checkInterval", value: 2 * 60 + 2 * reportInterval, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: offlinePingable], displayed: false)

	if (isCentraliteSwitch()) {
		zigbee.addBinding(zigbee.ONOFF_CLUSTER) + zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER) +
			zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR) +
			zigbee.batteryConfig(0, reportInterval, null)
	} else {
		zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR) +
			// Report no more frequently than 30 seconds, no less frequently than 6 hours, and when there is a change of 10% (expressed as half percents)
			zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR, DataType.UINT8, 30, reportInterval, 20)
	}
}


2 Likes

@hidoyatov.v.i how exactly did you configure this? i paired the Sengled Smart Switch, I used your DH from above. But it only shows 1 button, and even that does not seem to work. thanks

Hi @sanewton72

What do you mean under "1 button"?

Please make sure that type of the device is "ZigBee Battery Accessory Dimmer".
If you open the device on your local HE website, you should see commands similar to other switches (On, Off, Set Level and etc.)

Please see screenshots of my device.

thanks. i grabbed the updated version of the DH you posted earlier which made all the difference, now all configured & working well. If anyone has one of these things, it works well once you figure it out, if you want more info how to configure it going just ping me.

.
I modified hidoyatov.v.i's code (download modified device handler here) to make it work better in Node-RED

Before:

  • Maker API wouldn't fire events on repeated "on" or "off" presses. So if I clicked "off", i'd see an event change in Node-RED, if i pressed "off" again, Node-RED wouldn't be notified of this new event, now it will. This was accomplished by adding "isStateChange" to the event code and this is more a change for Node-REDs sake than Hubitat/Rule Machine
  • Added "type" to the event output to identify "press" vs "hold" (not sure if this would make a difference in Rule Machine or not, but a big help in Node-RED)
  • I changed the stepping up and down to 20 (instead of 10 and 5), If you want more or less steps, adjust that value near the top of the driver (affects both Hubitat and Node-RED)

Could you please give me some more info on the config? I've downloaded the DH and paired Sengled smart switch. How do I config the switch to control a light(s)? Button Controller app? Simple Lighting app? Rule machine app? Please forgive my rookie questions. I'm brand new to the world of Hubitat!

of course - happy to help! there's a few ways to do it, but they way i did it to achieve the behaviour I wanted is below. If you don't want auto off timers and motion control, you can just use the Rule Machine and Simple Automation parts:
DEVICE PAGE: (note the bottom where it shows what apps are using the device)

RULEMACHINE: 'mirrors' the Sengled Smart Switch and bulb dimmer values, based on Dim_UP and Dim_DOWN button presses on the Sengled Smart Switch. Requires a local variable. (there is only 1 trigger: ignore the "OR" that appears in the trigger list, it appeared as i was testing different strategies and i assume it won't go away without deleting the rule, but it doesn't have any effect so I left it alone).

And if you want/need it:
MOTION LIGHTING: uses Samsung Motion sensor for auto lighting

SIMPLE AUTOMATION: makes the ON and OFF buttons work as well (quirk of how the Smart Switch operates)

hopefully that should get you going. welcome to Hubitat! Lots of very clever and most importantly helpful guys in this community, i always seem to be on the asking end so its nice to be on the answering end for a change :wink:

1 Like

You probably should just use the built in mirror app. It would make this a lot easier.

If anybody want this I modified the ST driver to added in buttons for double tapped and hold of the on and off buttons. I made them all pushable button events as I find these more useful in automatons.

I also added in an option to turn off the debug logs as well as the ability to change the dimming levels in the device page.

1 Like

i could have...but i don't use mirror for any other devices whereas i use RM anyway, and this way i expect it would be lighter hub footprint (i.e. no unneeded code/features).

nice one...i shall update my driver to what you just posted and have play with it. thanks for sharing @at9 i think its a neat little dimmer switch, just clunky (so far at least) to use in HE.

Driver updated...i like the "WIP" addendum :slight_smile: will let you know once I have a chance to play with it. not now though, now bedtime methinks.

1 Like

i appreciate the work on this. I had it working in St then moved platform, no joy. Glad to see the switch has support here.

@at9 thanks so much for the driver. Have you noticed that the remote will stop functioning after a few days? At first I thought that the battery was dead because the indicator LED was dim on button press. This has now happened on at least 3 different occasions on 2 different remotes. I have to pull the battery for about 20 minutes, replace it, and everything is back to normal. I have 2 zigbee repeaters (IRIS v2 wall plugs) and all Sengled bulbs (non repeating). Do you have any idea on why the remotes maybe acting this way? Thanks.

I had this happen once one of mine and swapped the battery with another remote fixed it straight away. I left the battery out for a while and put it back in and haven't had an issue since.

If I haven't used it for a while it and might take a couple of pushes to wake the remote up so it might be an issue with reconnecting to the mesh again that is doing it.