/*
Govee RGBW driver
Copyright 2023 Hubitat Inc. All Rights Reserved
2023-11-02 2.3.7 mavrrick
-initial pub
*/
@Field static final String DEVICE_TYPE = 'MATTER_BULB'
//transitionTime options
@Field static Map ttOpts = [
defaultValue: '0',
defaultText: 'ASAP',
options:['0':'ASAP', '1':'1s', '2':'2s', '5':'5s']
]
import groovy.transform.Field
import hubitat.helper.HexUtils
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import groovy.json.JsonBuilder
def commandPort() { "4003" }
metadata {
definition (name: "Govee RGBW Matter Advanced", namespace: "Mavrrick", author: "Mavrrick") {
capability 'Actuator'
capability 'Switch'
capability 'SwitchLevel'
capability 'Configuration'
capability 'Color Control'
capability "ColorMode"
capability "ColorTemperature"
capability 'Light'
capability 'Initialize'
capability "LightEffects"
capability 'Refresh'
attribute "effectNum", "integer"
fingerprint endpointId:"01", inClusters:"0003,0004,0005,0006,0008,001D,0050,0300", outClusters:"", manufacturer:"Shenzhen Qianyan Technology", controllerType:"MAT"
}
preferences {
input(name:'transitionTime', type:'enum', title:"Level transition time (default:${ttOpts.defaultText})", options:ttOpts.options, defaultValue:ttOpts.defaultValue)
input(name: "lanControl", type: "bool", title: "Enable Local LAN control", description: "This is a advanced feature that only worked with some devices. Do not enable unless you are sure your device supports it", defaultValue: false)
if (lanControl) {
input("ip", "text", title: "IP Address", description: "IP address of your Govee light", required: false)
input(name: "lanScenes", type: "bool", title: "Enable Local LAN Scene Control", description: "If this is active your device will use Local Scenes control. Leave off to use Scenes/DIY's/Snapshots from the cloud API", defaultValue: false)
if (lanScenes) {
input(name: "lanScenesFile", type: "string", title: "LAN Scene File", description: "Please enter the file name with the Scenes for this device", defaultValue: "GoveeLanScenes_"+getDataValue("model")+".json")
}
}
input(name:"logEnable", type:"bool", title:"Enable debug logging", defaultValue:false)
input(name:"txtEnable", type:"bool", title:"Enable descriptionText logging", defaultValue:true)
}
}
void setEffect(effectNo) {
lanSetEffect (effectNo)
}
//parsers
void parse(String description) {
Map descMap
try {
descMap = matter.parseDescriptionAsMap(description)
} catch (e) {
logWarn "parse: exception ${e}
Failed to parse description: ${description}"
return
}
logDebug "parse: descMap:${descMap} description:${description}"
if (descMap == null) {
logWarn "parse: descMap is null description:${description}"
return
}
if (descMap.attrId == 'FFFB') { // parse the AttributeList first!
pareseAttributeList(descMap)
return
}
switch (descMap.cluster) {
case '0000' :
if (descMap.attrId == '4000') { //software build ?
updateDataValue('softwareBuild', descMap.value ?: 'unknown')
}
else {
logWarn "skipped softwareBuild, attribute:${descMap.attrId}, value:${descMap.value}"
}
break
case '0003' : // Identify
// gatherAttributesValuesInfo(descMap, IdentifyClusterAttributes)
break
case '0004' : // Groups
// gatherAttributesValuesInfo(descMap, GroupsClusterAttributes)
break
case '0005' : // Scenes
// gatherAttributesValuesInfo(descMap, ScenesClusterAttributes)
case '0006' : // On/Off Cluster
// gatherAttributesValuesInfo(descMap, OnOffClusterAttributes)
parseOnOffCluster(descMap)
break
case '0202' : // Fan Control Cluster
if (descMap.attrId == "0000") { //fan speed
sendSpeedEvent(descMap.value)
}
break
case '0008' : // LevelControl
if (descMap.attrId == '0000') { //current level
sendLevelEvent(descMap.value)
}
else {
logWarn "skipped level, attribute:${descMap.attrId}, value:${descMap.value}"
}
// gatherAttributesValuesInfo(descMap, LevelControlClusterAttributes)
break
case '001D' : // Descriptor, ep:00
// gatherAttributesValuesInfo(descMap, DescriptorClusterAttributes)
break
case '002F' : // PowerSource, ep:02 // parse: descMap:[endpoint:02, cluster:002F, attrId:000C, value:C8, clusterInt:47, attrInt:12] description:read attr - endpoint: 02, cluster: 002F, attrId: 000C, value: 04C8
parseBatteryEvent(descMap)
// gatherAttributesValuesInfo(descMap, PowerSourceClusterAttributes)
break
case '0028' : // BasicInformation, ep:00
// gatherAttributesValuesInfo(descMap, BasicInformationClusterAttributes)
break
case '0045' : // BooleanState
// gatherAttributesValuesInfo(descMap, BoleanStateClusterAttributes)
parseContactEvent(descMap)
break
case '0300' : // ColorControl
if (descMap.attrId == '0000') { //hue
sendHueEvent(descMap.value)
} else if (descMap.attrId == '0001') { //saturation
sendSaturationEvent(descMap.value)
}
else if (descMap.attrId == '0007') { //color temperature
sendCTEvent(descMap.value)
}
else if (descMap.attrId == '0008') { //color mode
logDebug "parse: skipped color mode:${descMap}"
}
else {
logWarn "parse: skipped color, attribute:${descMap.attrId}, value:${descMap.value}"
}
// gatherAttributesValuesInfo(descMap, ColorControlClusterAttributes)
break
default :
logWarn "parse: skipped:${descMap}"
}
}
void parseOnOffCluster(Map descMap) {
logDebug "parseOnOffCluster: descMap:${descMap}"
if (descMap.cluster != '0006') {
logWarn "parseOnOffCluster: unexpected cluster:${descMap.cluster} (attrId:${descMap.attrId})"
return
}
Integer attrInt = descMap.attrInt as Integer
Integer value
//String descriptionText = ''
//Map eventMap = [:]
String attrName = OnOffClusterAttributes[attrInt] ?: GlobalElementsAttributes[attrInt] ?: UNKNOWN
switch (descMap.attrId) {
case '0000' : // Switch
sendSwitchEvent(descMap.value)
break
case '4000' : // GlobalSceneControl
if (logEnable) { logInfo "parse: Switch: GlobalSceneControl = ${descMap.value}" }
if (state.onOff == null) { state.onOff = [:] } ; state.onOff['GlobalSceneControl'] = descMap.value
break
case '4001' : // OnTime
if (logEnable) { logInfo "parse: Switch: OnTime = ${descMap.value}" }
if (state.onOff == null) { state.onOff = [:] } ; state.onOff['OnTime'] = descMap.value
break
case '4002' : // OffWaitTime
if (logEnable) { logInfo "parse: Switch: OffWaitTime = ${descMap.value}" }
if (state.onOff == null) { state.onOff = [:] } ; state.onOff['OffWaitTime'] = descMap.value
break
case '4003' : // StartUpOnOff
value = descMap.value as int
String startUpOnOffText = "parse: Switch: StartUpOnOff = ${descMap.value} (${StartUpOnOffEnumOpts[value] ?: UNKNOWN})"
if (logEnable) { logInfo "${startUpOnOffText}" }
if (state.onOff == null) { state.onOff = [:] } ; state.onOff['StartUpOnOff'] = descMap.value
break
case ['FFF8', 'FFF9', 'FFFA', 'FFFB', 'FFFC', 'FFFD', '00FE'] :
if (logEnable) {
logInfo "parse: Switch: ${attrName} = ${descMap.value}"
}
break
default :
logWarn "parseOnOffCluster: unexpected attrId:${descMap.attrId} (raw:${descMap.value})"
}
}
/// Event Processing
private void sendSpeedEvent(String rawValue) {
Integer intValue = hexStrToUnsignedInt(rawValue)
switch(intValue) {
case 0 :
value = "off";
break;
case 8:
value = "speed 1";
break;
case 16:
value = "speed 2";
break;
case 24:
value = "speed3 (low)";
break;
case 32:
value = "speed 4";
break;
case 40:
value = "speed 5";
break;
case 48:
value = "speed 6 (medium)";
break;
case 56:
value = "speed 7";
break;
case 64:
value = "speed 8";
break;
case 72:
value = "speed 9";
break;
case 81:
value = "speed 10";
break;
case 90:
value = "speed 11";
break;
case 100:
value = "speed12 (high)";
break;
}
// if (device.currentValue("switch") == value) return
String descriptionText = "${device.displayName} was set to speed ${value}"
if (txtEnable) log.info descriptionText
sendEvent(name:"speed", value:value, descriptionText:descriptionText)
}
//events
private void sendSwitchEvent(String rawValue) {
String value = rawValue == "01" ? "on" : "off"
if (device.currentValue("switch") == value) return
String descriptionText = "${device.displayName} was turned ${value}"
if (txtEnable) log.info descriptionText
sendEvent(name:"switch", value:value, descriptionText:descriptionText)
}
private void sendLevelEvent(String rawValue) {
Integer value = Math.round(hexStrToUnsignedInt(rawValue) / 2.55)
if (value == 0 || value == device.currentValue("level")) return
String descriptionText = "${device.displayName} level was set to ${value}%"
if (txtEnable) log.info descriptionText
sendEvent(name:"level", value:value, descriptionText:descriptionText, unit: "%")
}
private void sendHueEvent(String rawValue, Boolean presetColor = false) {
Integer value = hex254ToInt100(rawValue)
if (device.currentValue("hue") != value ) {
sendRGBNameEvent(value)
String descriptionText = "${device.displayName} hue was set to ${value}%"
if (txtEnable) log.info descriptionText
sendEvent(name: "colorMode", value: "RGB")
sendEvent(name:"hue", value:value, descriptionText:descriptionText, unit: "%")
}
}
private void sendSaturationEvent(String rawValue, Boolean presetColor = false) {
Integer value = hex254ToInt100(rawValue)
if (device.currentValue("saturation") != value ) {
sendRGBNameEvent(null,value)
String descriptionText = "${device.displayName} saturation was set to ${value}%"
if (txtEnable) log.info descriptionText
sendEvent(name: "colorMode", value: "RGB")
sendEvent(name:"saturation", value:value, descriptionText:descriptionText, unit: "%")
}
}
private void sendRGBNameEvent(hue, sat = null){
String genericName
if (device.currentValue("saturation") == 0) {
genericName = "White"
} else if (hue == null) {
return
} else {
genericName = colorRGBName.find{k , v -> hue < k}.value
}
if (genericName == device.currentValue("colorName")) return
String descriptionText = "${device.displayName} color is ${genericName}"
if (txtEnable) log.info descriptionText
sendEvent(name: "colorName", value: genericName ,descriptionText: descriptionText)
}
private void sendCTEvent(String rawValue, Boolean presetColor = false) {
value = (Math.round(10000/(hexStrToUnsignedInt(rawValue))))*100
if (value != device.currentValue("colorTemperature")) {
String descriptionText = "${device.displayName} ColorTemp was set to ${value}K"
if (txtEnable) log.info descriptionText
sendEvent(name:"colorTemperature", value:value, descriptionText:descriptionText, unit: "K")
sendEvent(name: "colorMode", value: "CT")
}
}
/* private void sendCTEvent(String rawValue, Boolean presetColor = false) {
value = (Math.round(10000/(hexStrToUnsignedInt(rawValue))))*100
String descriptionText = "${device.displayName} ColorTemp was set to ${value}K"
if (txtEnable) log.info descriptionText
sendEvent(name:"colorTemperature", value:value, descriptionText:descriptionText, unit: "K")
} */
// Capability Commands
//// On/off Switch commands
void on() {
logDebug 'switching on()'
// setDigitalRequest() // 3 seconds
sendToDevice(matter.on())
}
void off() {
logDebug 'switching off()'
// setDigitalRequest()
sendToDevice(matter.off())
}
void toggle() {
logDebug 'toggling...'
setDigitalRequest()
String cmd = matter.invoke(device.endpointId, 0x0006, 0x0002)
sendToDevice(cmd)
}
//// Level control commands related to Light Devices
void setLevel(Object value, Object rate=0) { //new set level routine to enable immediate change
logDebug "setLevel(${value}, ${rate})"
Integer newLevel = value
Integer transitiontime2 = rate
newLevel2 = int100ToHex254(newLevel)
transition = HexUtils.integerToHexString(transitiontime2,2)
String cmds
if (device.currentValue("switch") == "on"){
List