Request for Support for Matter compatible AiDot WELOV P200 Smart Air Purifier

Thanks so much. This looks to be working perfectly. The reality is that the Matter implementation should include auto as a fan speed and that should be the default when an on command is sent to the purifier. I have a contact at the manufacturer and I need to talk to them about that. For now, I think setting it to medium when turned on is fine given that auto isn't an option.

I think your sensor readings of Good, Fair, Moderate, Poor, Very Poor, or Extreamly Poor are also OK. I believe, though it is going to take a bit more research that those actually correspond to the standard air quality values as defined here: AQI Basics | AirNow.gov

Will you be posting this on your GitHub for others to use?

Thanks again for all your help

I have two thoughts on this.

The first is I would think the action of "On" should simply be to return to the previous state. So it wouldn't matter if it was low/Medium/high it would return to that. That could be easily implemented if they added cluster 0x0006 which is the standard on/off cluster for MATTER. That said I do see the logic in it going to Auto.

The second is that technically if they fully implemented the Fan Control Cluster (0x0202) in MATTER, "Auto" may be possible. I had a heck of a time with my Govee Fan Matter driver because they couldn't implement the fanmode Attribute and so it depends on the Percent current setting. That is easily visible in my first driver with the 12 different speed values. The Fan Mode Attribute doesn't support that many options in Matter. Let me see if i can add a Auto command to the driver and have it call that attribute the appropriate value and we can see what happens.

Well from what I can tell the device doesn't pass AQI values. If it did i would have implemented that as it follows the standard hubitat AirQuality device Capability. The big problem with AQI is that it is defined differently based on where you live in the world. The way i would calculate it here for the US may be different then if you are somewhere in Europe for example. Even worse is that AQI can be different based on the sensors available and the types of pollutants a country/region tracks for AQI.

What I implemented was directly from the "Matter Application Cluster" documentation. Now they could implement other clusters for sensors and use them to pass the raw data that is used to calculate AQI. That list is below. Several of those are used in AQI based on where someone lives.

Yea.. i will post it on git hub eventually for safe keeping once i get it to a fairly good final state and we get most everything figured out. I may create a new repo though as most of what i have created is Govee Focused.

Honestly it is kind of fun figuring this out. I also kind of figure that this gets me preped to support the Govee Matter Air Purifier if they release one. I also just like helping folks when it doesn't look to complicated to do so. :slight_smile:

Here is another driver. The changes are

  1. I added commands for Auto and Smart Mode. I wasn't sure which if either would work so they are added for now.
  2. I adjusting some logging to help seperate mode changes from speed changes
  3. I device attribute for Fan Mode
  4. I added a routine to update the new Fan Mode Attribute when the fan mode value comes from the device.
/*
	Air Purifier Driver

	Copyright 2023 Hubitat Inc.  All Rights Reserved

	2023-11-02 2.3.7 maxwell
		-initial pub

*/

import groovy.transform.Field

@Field static final String   DEVICE_TYPE = 'MATTER_PURIFIER'

@Field Map getFanLevel = [
    "off": 0
    ,"low": 33
	,"medium": 66
	,"high": 100   
]

import groovy.transform.Field
import hubitat.helper.HexUtils

metadata {
    definition (name: "Air Purifier Driver(Matter)", namespace: "Mavrrick", author: "Mavrrick") {
        capability "Actuator"
        capability "Switch"
        capability "Configuration"
        capability "FanControl"
        capability "Initialize"
        capability "Refresh"
//        capability "AirQuality"
//        capability "FilterStatus"
        attribute "filterLife", "integer"
        attribute "airQuality", "string"
        attribute "fanMode", "string"
        
        command "setSpeed", [[name: "Fan speed*",type:"ENUM", description:"Fan speed to set", constraints: getFanLevel.collect {k,v -> k}]]
        command "auto"
        command "smart"
        
        fingerprint endpointId:"01", inClusters:"0003,0202,0071,005B,001D", outClusters:"", model:"Air Purifier", manufacturer:"Leedarson", controllerType:"MAT"

    }
    preferences {
        input(name:"cycleInterval", type:"number", title:"Number of seconds between cycles", defaultValue:30)
        input(name:"logEnable", type:"bool", title:"Enable debug logging", defaultValue:false)
        input(name:"txtEnable", type:"bool", title:"Enable descriptionText logging", defaultValue:true)
    }
}

//parsers
void parse(String description) {
    Map descMap = matter.parseDescriptionAsMap(description)
    if (logEnable) log.debug "descMap:${descMap}"
    switch (descMap.cluster) {        
        case "0006" :
            if (descMap.attrId == "0000") { //switch
                sendSwitchEvent(descMap.value)
            }
            break
        case "0000" :
            if (descMap.attrId == "4000") { //software build
                updateDataValue("softwareBuild",descMap.value ?: "unknown")
            }
            break
        case "005B" :
//            sendEvent(name:"airQualityIndex ", value:value, descriptionText:descriptionText)
            if (logEnable) log.debug  "parse: Air Quality, attribute:${descMap.attrId}, value:${descMap.value}"
            sendAirQualityEvent(descMap.value)
            break
        case "0071" :
            if (descMap.attrId == "0000") { //Filter State
//                sendEvent(name:"filterStatus ", value:value, descriptionText:descriptionText) 
                if (logEnable) log.debug  "parse: ilter State, attribute:${descMap.attrId}, value:${descMap.value}"
                sendFilterEvent(descMap.value)
            } else if (descMap.attrId == "0004") { //Last Change time
                if (logEnable) log.debug  "parse: Filter last change time, attribute:${descMap.attrId}, value:${descMap.value}"
            }
            break
        case "0202" :
            if (descMap.attrId == "0000") { //fan mode
                if (logEnable) log.debug "parse(): Fan Event - Fan Mode ${descMap.value}"
                sendModeEvent(descMap.value)
            } else if (descMap.attrId == "0001") { //fan speed mode
                if (logEnable) log.debug "parse(): Fan Event - Fan speed mode ${descMap.value}"
            } else if (descMap.attrId == "0002") { //fan speed Percent Setting
                sendSpeedEvent(descMap.value) 
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed  Percent ${descMap.value}"
            } else  if (descMap.attrId == "0003") { //fan speed Percent current
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed Percent Curent ${descMap.value}"
            } else if (descMap.attrId == "0004") { //fan speed max (Don't expect to actually ever return in parse
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed Max speed ${descMap.value}"
            } else if (descMap.attrId == "0005") { //fan speed setting
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed setting ${descMap.value}"
            } else if (descMap.attrId == "0006") { //fan speed setting current
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed current ${descMap.value}"
            } else if (descMap.attrId == "000A") { //WindSetting current
                if (logEnable) log.debug "parse(): Fan Event - Wind Speed setting ${descMap.value}"
            } else  if (descMap.attrId == "000B") { //Airflow Direction
                if (logEnable) log.debug "parse(): Fan Event - Airflow Direction setting ${descMap.value}"
            } else {
                if (logEnable) log.debug  "parse: skipped fan, attribute:${descMap.attrId}, value:${descMap.value}"
            }
//            gatherAttributesValuesInfo(descMap, FanClusterAttributes)
            break
        default :
            if (logEnable) {
                log.debug "skipped:${descMap}"
            }
    }
}

//events
private void sendSpeedEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue)
    
    switch(intValue) {
        case 0 :
            value = "off";
        break;
        case 33:
            value = "low";
        break;
        case 66:
            value = "medium";
        break;
        case 100:
            value = "high";
        break; 
    }
    
    String descriptionText = "${device.displayName} was set to speed ${value} RawValue: ${intValue}"
    if (txtEnable) log.info descriptionText
    sendEvent(name:"speed", value:value, descriptionText:descriptionText) 
}

private void sendModeEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue)
    
    switch(intValue) {
        case 0 :
            value = "Off";
        break;
        case 1:
            value = "Low";
        break;
        case 2:
            value = "medium";
        break;
        case 3:
            value = "High";
        break;
        case 4:
            value = "On";
        break;
        case 5:
            value = "Auto";
        break;
        case 6:
            value = "Smart";
        break;
    }
    
    String descriptionText = "${device.displayName} was set to FanMode ${value} RawValue: ${intValue}"
    if (txtEnable) log.info descriptionText
    sendEvent(name:"fanMode", value:value, descriptionText:descriptionText) 
}

private void sendFilterEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue) 

    String descriptionText = "${device.displayName} Filter % Life left ${intValue} RawValue: ${rawValue}"    
    if (txtEnable) log.info descriptionText
   sendEvent(name:"filterLife", value:intValue, descriptionText:descriptionText)
}

private void sendAirQualityEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue) 
    
    switch(intValue) {
        case 0 :
            value = "Unknown";
        break;
        case 1 :
            value = "Good";
        break;
        case 2:
            value = "Fair";
        break;
        case 3 :
            value = "Moderate";
        break;
        case 4 :
            value = "Poor";
        break;
        case 5 :
            value = "VeryPoor";
        break;
        case 6 :
            value = "ExtremelyPoor";
        break;
    }

    String descriptionText = "${device.displayName} Air Quality Value left ${value} RawValue: ${intValue}"    
    if (txtEnable) log.info descriptionText
   sendEvent(name:"airQuality", value:value, descriptionText:descriptionText)
}    

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

//capability commands
void on() {
    unschedule()
    if (logEnable) log.debug "on()"
    sendToDevice(matter.on())
    fanspeed = "medium"
    setSpeed(fanspeed)    
   
}

void off() {
    unschedule()
    if (logEnable) log.debug "off()"
//    sendToDevice(matter.off())
    fanspeed = "off"
    setSpeed(fanspeed)
}

void setSpeed(fanspeed) {
    unschedule()
    if (logEnable) log.debug "Setting Fan Speed to ${fanspeed}"
    switch(fanspeed) {
        case "off":
            value = 0;
        break;
        case "low":
            value = 33;
        break;
        case "medium":
            value = 66;
        break;
        case "high":
            value = 100;
        break; 
    }
    if (value > 101) {
        if (logEnable) {log.debug ("setSpeed(): Unknown value}")};
        on()
    } else {
        speedValue = intToHexStr(value)  
        if (logEnable) log.debug "Setting Fan Speed percent ${fanspeed}  % ${value} value to ${speedValue}"
        List<Map<String, String>> attributeWriteRequests = []
        attributeWriteRequests.add(matter.attributeWriteRequest(device.endpointId, 0x0202, 0x0002, 0x04, speedValue ))
        String cmd = matter.writeAttributes(attributeWriteRequests)            
        sendToDevice(cmd)
    }
}

void auto() {

    if (logEnable) log.debug "Setting Fan Mode to Auto"
    value = 5 // 5 is Auto 6 is smart
    modeValue = intToHexStr(value)  
    List<Map<String, String>> attributeWriteRequests = []
    attributeWriteRequests.add(matter.attributeWriteRequest(device.endpointId, 0x0202, 0x0000, 0x04, modeValue ))
    String cmd = matter.writeAttributes(attributeWriteRequests)            
    sendToDevice(cmd)
}

void smart() {

    if (logEnable) log.debug "Setting Fan Mode to Auto"
    value = 6 // 5 is Auto 6 is smart
    modeValue = intToHexStr(value)  
    List<Map<String, String>> attributeWriteRequests = []
    attributeWriteRequests.add(matter.attributeWriteRequest(device.endpointId, 0x0202, 0x0000, 0x04, modeValue ))
    String cmd = matter.writeAttributes(attributeWriteRequests)            
    sendToDevice(cmd)
}

void cycleSpeed() {
    cycleChange()
}

void cycleChange() {
    Integer randomSpeed = Math.abs(new Random().nextInt() % 12) + 1
    String newSpeed = "speed "+randomSpeed
    setSpeed(newSpeed)
    runIn(cycleInterval, cycleChange)
    
}


void configure() {
    log.warn "configure..."
    sendToDevice(subscribeCmd())
    unschedule()
}

//lifecycle commands
void updated(){
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    log.warn "description logging is: ${txtEnable == true}"
    if (logEnable) runIn(1800,logsOff)
}

void initialize() {
    log.info "initialize..."
//    initializeVars(fullInit = true)
    sendToDevice(subscribeCmd())
}

void refresh() {
    if (logEnable) log.debug "refresh()"
    sendToDevice(refreshCmd())
}

String refreshCmd() {
    List<Map<String, String>> attributePaths = []
    
        attributePaths.add(matter.attributePath(device.endpointId, 0x0006, 0x0000))         // on/off
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0000))         // FanMode
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0002))         // PercentSetting
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0003))         // PercentCurrent
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x000A))         // WindSetting
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x000B))         // AirflowDirectionEnum
        attributePaths.add(matter.attributePath(device.endpointId, 0x0003, 0x0000))         
        attributePaths.add(matter.attributePath(device.endpointId, 0x0003, 0x0001))         // Power Configuration Cluster : Status
        attributePaths.add(matter.attributePath(device.endpointId, 0x005B, 0x0000)) 
        attributePaths.add(matter.attributePath(device.endpointId, 0x0071, 0x0000))
    
    String cmd = matter.readAttributes(attributePaths)
    return cmd
}

String subscribeCmd() {
    List<Map<String, String>> attributePaths = []
    String cmd = ''
    
        attributePaths.add(matter.attributePath(0x01, 0x0006, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x02))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x03))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x0A))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x0B))
        attributePaths.add(matter.attributePath(0x01, 0x005B, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0071, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0071, 0x04))
        cmd = matter.subscribe(0, 0xFFFF, attributePaths)

    return cmd
}

void logsOff(){
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

Integer hex254ToInt100(String value) {
    return Math.round(hexStrToUnsignedInt(value) / 2.54)
}

String int100ToHex254(value) {
    return intToHexStr(Math.round(value * 2.54))
}


void sendToDevice(List<String> cmds, Integer delay = 300) {
    sendHubCommand(new hubitat.device.HubMultiAction(commands(cmds, delay), hubitat.device.Protocol.MATTER))
}

void sendToDevice(String cmd, Integer delay = 300) {
    sendHubCommand(new hubitat.device.HubAction(cmd, hubitat.device.Protocol.MATTER))
}

List<String> commands(List<String> cmds, Integer delay = 300) {
    return delayBetween(cmds.collect { it }, delay)
}
    
    

Thanks! I'll give this a try

Auto works perfectly. I'm not sure what "smart" is supposed to do. It isn't in the app and I can't find it in the manual either. When I issues the "smart" command from the Hubitat UI it doesn't seem to have any affect on the purifier.

Thanks again for your help

Ok i have tweaked it a little more based on your last response.

  1. Removed the command for Smart since it isn't needed for this device. I left it in the driver in certain spots as it may be used later for other MATTER Fan based devices.
  2. I added a preference to allow you to select your default "on" speed/mode. This will allow you to select what speed option is used when you click the "on" command.
  3. I also Added auto to the fan speed control options so now that will be available when you use the set speed option.

I think we have our basis for functionality covered now for your device with this driver assuming my last changes work. Is there any additional functionality that would be good for the device we don't have. Now would be the time to build functions in into the driver if we had any ideas. If not i will go ahead and post this last version to Github for others if they want it.

Here is the newest code for the driver

/*
	Air Purifier Driver

	Copyright 2023 Hubitat Inc.  All Rights Reserved

	2023-11-02 2.3.7 maxwell
		-initial pub

*/

import groovy.transform.Field

@Field static final String   DEVICE_TYPE = 'MATTER_PURIFIER'

@Field Map getFanLevel = [
    "off": 0
    ,"low": 33
	,"medium": 66
	,"high": 100
    ,"auto": 101
]

import groovy.transform.Field
import hubitat.helper.HexUtils

metadata {
    definition (name: "Air Purifier Driver(Matter)", namespace: "Mavrrick", author: "Mavrrick") {
        capability "Actuator"
        capability "Switch"
        capability "Configuration"
        capability "FanControl"
        capability "Initialize"
        capability "Refresh"
        attribute "filterLife", "integer"
        attribute "airQuality", "string"
        attribute "fanMode", "string"
        
        command "setSpeed", [[name: "Fan speed*",type:"ENUM", description:"Fan speed to set", constraints: getFanLevel.collect {k,v -> k}]]
        command "auto"
        
        fingerprint endpointId:"01", inClusters:"0003,0202,0071,005B,001D", outClusters:"", model:"Air Purifier", manufacturer:"Leedarson", controllerType:"MAT"

    }
    preferences {
        input(name:"cycleInterval", type:"number", title:"Number of seconds between cycles", defaultValue:30)
        input(name:"onDefault", type:"enum", title:"Default fan mode when turned on", options: getFanLevel.collect {k,v -> k},defaultValue:"auto")
        input(name:"logEnable", type:"bool", title:"Enable debug logging", defaultValue:false)
        input(name:"txtEnable", type:"bool", title:"Enable descriptionText logging", defaultValue:true)
    }
}

//parsers
void parse(String description) {
    Map descMap = matter.parseDescriptionAsMap(description)
    if (logEnable) log.debug "descMap:${descMap}"
    switch (descMap.cluster) {        
        case "0006" :
            if (descMap.attrId == "0000") { //switch
                sendSwitchEvent(descMap.value)
            }
            break
        case "0000" :
            if (descMap.attrId == "4000") { //software build
                updateDataValue("softwareBuild",descMap.value ?: "unknown")
            }
            break
        case "005B" :
//            sendEvent(name:"airQualityIndex ", value:value, descriptionText:descriptionText)
            if (logEnable) log.debug  "parse: Air Quality, attribute:${descMap.attrId}, value:${descMap.value}"
            sendAirQualityEvent(descMap.value)
            break
        case "0071" :
            if (descMap.attrId == "0000") { //Filter State
//                sendEvent(name:"filterStatus ", value:value, descriptionText:descriptionText) 
                if (logEnable) log.debug  "parse: ilter State, attribute:${descMap.attrId}, value:${descMap.value}"
                sendFilterEvent(descMap.value)
            } else if (descMap.attrId == "0004") { //Last Change time
                if (logEnable) log.debug  "parse: Filter last change time, attribute:${descMap.attrId}, value:${descMap.value}"
            }
            break
        case "0202" :
            if (descMap.attrId == "0000") { //fan mode
                if (logEnable) log.debug "parse(): Fan Event - Fan Mode ${descMap.value}"
                sendModeEvent(descMap.value)
            } else if (descMap.attrId == "0001") { //fan speed mode
                if (logEnable) log.debug "parse(): Fan Event - Fan speed mode ${descMap.value}"
            } else if (descMap.attrId == "0002") { //fan speed Percent Setting
                sendSpeedEvent(descMap.value) 
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed  Percent ${descMap.value}"
            } else  if (descMap.attrId == "0003") { //fan speed Percent current
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed Percent Curent ${descMap.value}"
            } else if (descMap.attrId == "0004") { //fan speed max (Don't expect to actually ever return in parse
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed Max speed ${descMap.value}"
            } else if (descMap.attrId == "0005") { //fan speed setting
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed setting ${descMap.value}"
            } else if (descMap.attrId == "0006") { //fan speed setting current
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed current ${descMap.value}"
            } else if (descMap.attrId == "000A") { //WindSetting current
                if (logEnable) log.debug "parse(): Fan Event - Wind Speed setting ${descMap.value}"
            } else  if (descMap.attrId == "000B") { //Airflow Direction
                if (logEnable) log.debug "parse(): Fan Event - Airflow Direction setting ${descMap.value}"
            } else {
                if (logEnable) log.debug  "parse: skipped fan, attribute:${descMap.attrId}, value:${descMap.value}"
            }
//            gatherAttributesValuesInfo(descMap, FanClusterAttributes)
            break
        default :
            if (logEnable) {
                log.debug "skipped:${descMap}"
            }
    }
}

//events
private void sendSpeedEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue)
    
    switch(intValue) {
        case 0 :
            value = "off";
        break;
        case 33:
            value = "low";
        break;
        case 66:
            value = "medium";
        break;
        case 100:
            value = "high";
        break; 
    }
    
    String descriptionText = "${device.displayName} was set to speed ${value} RawValue: ${intValue}"
    if (txtEnable) log.info descriptionText
    sendEvent(name:"speed", value:value, descriptionText:descriptionText) 
}

private void sendModeEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue)
    
    switch(intValue) {
        case 0 :
            value = "Off";
        break;
        case 1:
            value = "Low";
        break;
        case 2:
            value = "medium";
        break;
        case 3:
            value = "High";
        break;
        case 4:
            value = "On";
        break;
        case 5:
            value = "Auto";
        break;
        case 6:
            value = "Smart";
        break;
    }
    
    String descriptionText = "${device.displayName} was set to FanMode ${value} RawValue: ${intValue}"
    if (txtEnable) log.info descriptionText
    sendEvent(name:"fanMode", value:value, descriptionText:descriptionText) 
}

private void sendFilterEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue) 

    String descriptionText = "${device.displayName} Filter % Life left ${intValue} RawValue: ${rawValue}"    
    if (txtEnable) log.info descriptionText
   sendEvent(name:"filterLife", value:intValue, descriptionText:descriptionText)
}

private void sendAirQualityEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue) 
    
    switch(intValue) {
        case 0 :
            value = "Unknown";
        break;
        case 1 :
            value = "Good";
        break;
        case 2:
            value = "Fair";
        break;
        case 3 :
            value = "Moderate";
        break;
        case 4 :
            value = "Poor";
        break;
        case 5 :
            value = "VeryPoor";
        break;
        case 6 :
            value = "ExtremelyPoor";
        break;
    }

    String descriptionText = "${device.displayName} Air Quality Value left ${value} RawValue: ${intValue}"    
    if (txtEnable) log.info descriptionText
   sendEvent(name:"airQuality", value:value, descriptionText:descriptionText)
}    

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

//capability commands
void on() {
    unschedule()
    if (logEnable) log.debug "on()"
    sendToDevice(matter.on())
    fanspeed = onDefault
    setSpeed(fanspeed)    
   
}

void off() {
    unschedule()
    if (logEnable) log.debug "off()"
//    sendToDevice(matter.off())
    fanspeed = "off"
    setSpeed(fanspeed)
}

void setSpeed(fanspeed) {
    unschedule()
    if (logEnable) log.debug "Setting Fan Speed to ${fanspeed}"
    switch(fanspeed) {
        case "off":
            value = 0;
        break;
        case "low":
            value = 33;
        break;
        case "medium":
            value = 66;
        break;
        case "high":
            value = 100;
        break; 
        case "auto":
            value = 101;
        break; 
    }
    if (value == 101) {
        if (logEnable) {log.debug ("setSpeed(): Auto mode has been selected}")};
        auto()
    } else {
        speedValue = intToHexStr(value)  
        if (logEnable) log.debug "Setting Fan Speed percent ${fanspeed}  % ${value} value to ${speedValue}"
        List<Map<String, String>> attributeWriteRequests = []
        attributeWriteRequests.add(matter.attributeWriteRequest(device.endpointId, 0x0202, 0x0002, 0x04, speedValue ))
        String cmd = matter.writeAttributes(attributeWriteRequests)            
        sendToDevice(cmd)
    }
}

void auto() {

    if (logEnable) log.debug "Setting Fan Mode to Auto"
    value = 5 // 5 is Auto 6 is smart
    modeValue = intToHexStr(value)  
    List<Map<String, String>> attributeWriteRequests = []
    attributeWriteRequests.add(matter.attributeWriteRequest(device.endpointId, 0x0202, 0x0000, 0x04, modeValue ))
    String cmd = matter.writeAttributes(attributeWriteRequests)            
    sendToDevice(cmd)
}

void cycleSpeed() {
    cycleChange()
}

void cycleChange() {
    Integer randomSpeed = Math.abs(new Random().nextInt() % 12) + 1
    String newSpeed = "speed "+randomSpeed
    setSpeed(newSpeed)
    runIn(cycleInterval, cycleChange)
    
}


void configure() {
    log.warn "configure..."
    sendToDevice(subscribeCmd())
    unschedule()
}

//lifecycle commands
void updated(){
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    log.warn "description logging is: ${txtEnable == true}"
    if (logEnable) runIn(1800,logsOff)
}

void initialize() {
    log.info "initialize..."
//    initializeVars(fullInit = true)
    sendToDevice(subscribeCmd())
}

void refresh() {
    if (logEnable) log.debug "refresh()"
    sendToDevice(refreshCmd())
}

String refreshCmd() {
    List<Map<String, String>> attributePaths = []
    
        attributePaths.add(matter.attributePath(device.endpointId, 0x0006, 0x0000))         // on/off
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0000))         // FanMode
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0002))         // PercentSetting
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0003))         // PercentCurrent
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x000A))         // WindSetting
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x000B))         // AirflowDirectionEnum
        attributePaths.add(matter.attributePath(device.endpointId, 0x0003, 0x0000))         
        attributePaths.add(matter.attributePath(device.endpointId, 0x0003, 0x0001))         // Power Configuration Cluster : Status
        attributePaths.add(matter.attributePath(device.endpointId, 0x005B, 0x0000)) 
        attributePaths.add(matter.attributePath(device.endpointId, 0x0071, 0x0000))
    
    String cmd = matter.readAttributes(attributePaths)
    return cmd
}

String subscribeCmd() {
    List<Map<String, String>> attributePaths = []
    String cmd = ''
    
        attributePaths.add(matter.attributePath(0x01, 0x0006, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x02))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x03))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x0A))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x0B))
        attributePaths.add(matter.attributePath(0x01, 0x005B, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0071, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0071, 0x04))
        cmd = matter.subscribe(0, 0xFFFF, attributePaths)

    return cmd
}

// Helper methods

void logsOff(){
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

Integer hex254ToInt100(String value) {
    return Math.round(hexStrToUnsignedInt(value) / 2.54)
}

String int100ToHex254(value) {
    return intToHexStr(Math.round(value * 2.54))
}


void sendToDevice(List<String> cmds, Integer delay = 300) {
    sendHubCommand(new hubitat.device.HubMultiAction(commands(cmds, delay), hubitat.device.Protocol.MATTER))
}

void sendToDevice(String cmd, Integer delay = 300) {
    sendHubCommand(new hubitat.device.HubAction(cmd, hubitat.device.Protocol.MATTER))
}

List<String> commands(List<String> cmds, Integer delay = 300) {
    return delayBetween(cmds.collect { it }, delay)
}

Sigh. The enemy of good is perfect. Something you did in this version broke the driver. It isn't working at all to control the purifier. I went back the previous version you sent just to make sure it wasn't some other issue but the previous version works fine.
Thanks

Where there any errors in the logs?

I made a small tweak because I was able to generate a error when I just tested it for the "On" action. After you upgraded the last time did you go click on the "Save Preferences" button for the preferences. I added a new preference and that probably will require you to click save to set it.

/*
	Air Purifier Driver

	Copyright 2023 Hubitat Inc.  All Rights Reserved

	2023-11-02 2.3.7 maxwell
		-initial pub

*/

import groovy.transform.Field

@Field static final String   DEVICE_TYPE = 'MATTER_PURIFIER'

@Field Map getFanLevel = [
    "off": 0
    ,"low": 33
	,"medium": 66
	,"high": 100
    ,"auto": 101
]

import groovy.transform.Field
import hubitat.helper.HexUtils

metadata {
    definition (name: "Air Purifier Driver(Matter)", namespace: "Mavrrick", author: "Mavrrick") {
        capability "Actuator"
        capability "Switch"
        capability "Configuration"
        capability "FanControl"
        capability "Initialize"
        capability "Refresh"
        attribute "filterLife", "integer"
        attribute "airQuality", "string"
        attribute "fanMode", "string"
        
        command "setSpeed", [[name: "Fan speed*",type:"ENUM", description:"Fan speed to set", constraints: getFanLevel.collect {k,v -> k}]]
        command "auto"
        
        fingerprint endpointId:"01", inClusters:"0003,0202,0071,005B,001D", outClusters:"", model:"Air Purifier", manufacturer:"Leedarson", controllerType:"MAT"

    }
    preferences {
        input(name:"cycleInterval", type:"number", title:"Number of seconds between cycles", defaultValue:30)
        input(name:"onDefault", type:"enum", title:"Default fan mode when turned on", options: getFanLevel.collect {k,v -> k},defaultValue:"auto")
        input(name:"logEnable", type:"bool", title:"Enable debug logging", defaultValue:false)
        input(name:"txtEnable", type:"bool", title:"Enable descriptionText logging", defaultValue:true)
    }
}

//parsers
void parse(String description) {
    Map descMap = matter.parseDescriptionAsMap(description)
    if (logEnable) log.debug "descMap:${descMap}"
    switch (descMap.cluster) {        
        case "0006" :
            if (descMap.attrId == "0000") { //switch
                sendSwitchEvent(descMap.value)
            }
            break
        case "0000" :
            if (descMap.attrId == "4000") { //software build
                updateDataValue("softwareBuild",descMap.value ?: "unknown")
            }
            break
        case "005B" :
//            sendEvent(name:"airQualityIndex ", value:value, descriptionText:descriptionText)
            if (logEnable) log.debug  "parse: Air Quality, attribute:${descMap.attrId}, value:${descMap.value}"
            sendAirQualityEvent(descMap.value)
            break
        case "0071" :
            if (descMap.attrId == "0000") { //Filter State
//                sendEvent(name:"filterStatus ", value:value, descriptionText:descriptionText) 
                if (logEnable) log.debug  "parse: ilter State, attribute:${descMap.attrId}, value:${descMap.value}"
                sendFilterEvent(descMap.value)
            } else if (descMap.attrId == "0004") { //Last Change time
                if (logEnable) log.debug  "parse: Filter last change time, attribute:${descMap.attrId}, value:${descMap.value}"
            }
            break
        case "0202" :
            if (descMap.attrId == "0000") { //fan mode
                if (logEnable) log.debug "parse(): Fan Event - Fan Mode ${descMap.value}"
                sendModeEvent(descMap.value)
            } else if (descMap.attrId == "0001") { //fan speed mode
                if (logEnable) log.debug "parse(): Fan Event - Fan speed mode ${descMap.value}"
            } else if (descMap.attrId == "0002") { //fan speed Percent Setting
                sendSpeedEvent(descMap.value) 
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed  Percent ${descMap.value}"
            } else  if (descMap.attrId == "0003") { //fan speed Percent current
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed Percent Curent ${descMap.value}"
            } else if (descMap.attrId == "0004") { //fan speed max (Don't expect to actually ever return in parse
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed Max speed ${descMap.value}"
            } else if (descMap.attrId == "0005") { //fan speed setting
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed setting ${descMap.value}"
            } else if (descMap.attrId == "0006") { //fan speed setting current
                if (logEnable) log.debug "parse(): Fan Event - Fan Speed current ${descMap.value}"
            } else if (descMap.attrId == "000A") { //WindSetting current
                if (logEnable) log.debug "parse(): Fan Event - Wind Speed setting ${descMap.value}"
            } else  if (descMap.attrId == "000B") { //Airflow Direction
                if (logEnable) log.debug "parse(): Fan Event - Airflow Direction setting ${descMap.value}"
            } else {
                if (logEnable) log.debug  "parse: skipped fan, attribute:${descMap.attrId}, value:${descMap.value}"
            }
//            gatherAttributesValuesInfo(descMap, FanClusterAttributes)
            break
        default :
            if (logEnable) {
                log.debug "skipped:${descMap}"
            }
    }
}

//events
private void sendSpeedEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue)
    
    switch(intValue) {
        case 0 :
            value = "off";
        break;
        case 33:
            value = "low";
        break;
        case 66:
            value = "medium";
        break;
        case 100:
            value = "high";
        break; 
    }
    
    String descriptionText = "${device.displayName} was set to speed ${value} RawValue: ${intValue}"
    if (txtEnable) log.info descriptionText
    sendEvent(name:"speed", value:value, descriptionText:descriptionText) 
}

private void sendModeEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue)
    
    switch(intValue) {
        case 0 :
            value = "Off";
        break;
        case 1:
            value = "Low";
        break;
        case 2:
            value = "medium";
        break;
        case 3:
            value = "High";
        break;
        case 4:
            value = "On";
        break;
        case 5:
            value = "Auto";
        break;
        case 6:
            value = "Smart";
        break;
    }
    
    String descriptionText = "${device.displayName} was set to FanMode ${value} RawValue: ${intValue}"
    if (txtEnable) log.info descriptionText
    sendEvent(name:"fanMode", value:value, descriptionText:descriptionText) 
}

private void sendFilterEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue) 

    String descriptionText = "${device.displayName} Filter % Life left ${intValue} RawValue: ${rawValue}"    
    if (txtEnable) log.info descriptionText
   sendEvent(name:"filterLife", value:intValue, descriptionText:descriptionText)
}

private void sendAirQualityEvent(String rawValue) {      
    Integer intValue = hexStrToUnsignedInt(rawValue) 
    
    switch(intValue) {
        case 0 :
            value = "Unknown";
        break;
        case 1 :
            value = "Good";
        break;
        case 2:
            value = "Fair";
        break;
        case 3 :
            value = "Moderate";
        break;
        case 4 :
            value = "Poor";
        break;
        case 5 :
            value = "VeryPoor";
        break;
        case 6 :
            value = "ExtremelyPoor";
        break;
    }

    String descriptionText = "${device.displayName} Air Quality Value left ${value} RawValue: ${intValue}"    
    if (txtEnable) log.info descriptionText
   sendEvent(name:"airQuality", value:value, descriptionText:descriptionText)
}    

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

//capability commands
void on() {
    unschedule()
    if (logEnable) log.debug "on()"
    fanspeed = onDefault
    setSpeed(fanspeed)    
   
}

void off() {
    unschedule()
    if (logEnable) log.debug "off()"
    fanspeed = "off"
    setSpeed(fanspeed)
}

void setSpeed(fanspeed) {
    unschedule()
    if (logEnable) log.debug "Setting Fan Speed to ${fanspeed}"
    switch(fanspeed) {
        case "off":
            value = 0;
        break;
        case "low":
            value = 33;
        break;
        case "medium":
            value = 66;
        break;
        case "high":
            value = 100;
        break; 
        case "auto":
            value = 101;
        break; 
    }
    if (value == 101) {
        if (logEnable) {log.debug ("setSpeed(): Auto mode has been selected")};
        auto()
    } else {
        speedValue = intToHexStr(value)  
        if (logEnable) log.debug "Setting Fan Speed percent ${fanspeed}  % ${value} value to ${speedValue}"
        List<Map<String, String>> attributeWriteRequests = []
        attributeWriteRequests.add(matter.attributeWriteRequest(device.endpointId, 0x0202, 0x0002, 0x04, speedValue ))
        String cmd = matter.writeAttributes(attributeWriteRequests)            
        sendToDevice(cmd)
    }
}

void auto() {

    if (logEnable) log.debug "Setting Fan Mode to Auto"
    value = 5 // 5 is Auto 6 is smart
    modeValue = intToHexStr(value)  
    List<Map<String, String>> attributeWriteRequests = []
    attributeWriteRequests.add(matter.attributeWriteRequest(device.endpointId, 0x0202, 0x0000, 0x04, modeValue ))
    String cmd = matter.writeAttributes(attributeWriteRequests)            
    sendToDevice(cmd)
}

void cycleSpeed() {
    cycleChange()
}

void cycleChange() {
    Integer randomSpeed = Math.abs(new Random().nextInt() % 12) + 1
    String newSpeed = "speed "+randomSpeed
    setSpeed(newSpeed)
    runIn(cycleInterval, cycleChange)
    
}


void configure() {
    log.warn "configure..."
    sendToDevice(subscribeCmd())
    unschedule()
}

//lifecycle commands
void updated(){
    log.info "updated..."
    log.warn "debug logging is: ${logEnable == true}"
    log.warn "description logging is: ${txtEnable == true}"
    if (logEnable) runIn(1800,logsOff)
}

void initialize() {
    log.info "initialize..."
//    initializeVars(fullInit = true)
    sendToDevice(subscribeCmd())
}

void refresh() {
    if (logEnable) log.debug "refresh()"
    sendToDevice(refreshCmd())
}

String refreshCmd() {
    List<Map<String, String>> attributePaths = []
    
        attributePaths.add(matter.attributePath(device.endpointId, 0x0006, 0x0000))         // on/off
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0000))         // FanMode
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0002))         // PercentSetting
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x0003))         // PercentCurrent
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x000A))         // WindSetting
        attributePaths.add(matter.attributePath(device.endpointId, 0x0202, 0x000B))         // AirflowDirectionEnum
        attributePaths.add(matter.attributePath(device.endpointId, 0x0003, 0x0000))         
        attributePaths.add(matter.attributePath(device.endpointId, 0x0003, 0x0001))         // Power Configuration Cluster : Status
        attributePaths.add(matter.attributePath(device.endpointId, 0x005B, 0x0000)) 
        attributePaths.add(matter.attributePath(device.endpointId, 0x0071, 0x0000))
    
    String cmd = matter.readAttributes(attributePaths)
    return cmd
}

String subscribeCmd() {
    List<Map<String, String>> attributePaths = []
    String cmd = ''
    
        attributePaths.add(matter.attributePath(0x01, 0x0006, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x02))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x03))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x0A))
        attributePaths.add(matter.attributePath(0x01, 0x0202, 0x0B))
        attributePaths.add(matter.attributePath(0x01, 0x005B, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0071, 0x00))
        attributePaths.add(matter.attributePath(0x01, 0x0071, 0x04))
        cmd = matter.subscribe(0, 0xFFFF, attributePaths)

    return cmd
}

// Helper methods

void logsOff(){
    log.warn "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

Integer hex254ToInt100(String value) {
    return Math.round(hexStrToUnsignedInt(value) / 2.54)
}

String int100ToHex254(value) {
    return intToHexStr(Math.round(value * 2.54))
}


void sendToDevice(List<String> cmds, Integer delay = 300) {
    sendHubCommand(new hubitat.device.HubMultiAction(commands(cmds, delay), hubitat.device.Protocol.MATTER))
}

void sendToDevice(String cmd, Integer delay = 300) {
    sendHubCommand(new hubitat.device.HubAction(cmd, hubitat.device.Protocol.MATTER))
}

List<String> commands(List<String> cmds, Integer delay = 300) {
    return delayBetween(cmds.collect { it }, delay)
}

I think not having clicked on "Save Preferences" was the problem. To get you logs I turned on enable debug logging and hit save preferences. Suddenly everything was working. I just tried your latest code and that is working perfectly. I can't think of any more functionality to add unless I can convince the manufacturer to add sleep mode as a fan speed option. No clue whether I will have success there or not.
Thanks so much for your help. Can you please include the url to the code on your GitHub once you get around to posting it here in this thread.
Thanks again

It probably can't be added as a Fan Mode in Matter because the spec doesn't have anything for that. It is just a limiation of MATTER as it is now

The spec does allow for setting speed by a % value. That is actually how my driver does it. What you may want to ask for a speed % value that specifies that mode. Clearly the purifier was able to translate the 12 different % speeds in the govee driver I first sent you to a mode value. If they have a speed % that represents sleep then I can translate that in the driver.

I created a new repo for it

raw.githubusercontent.com/Mavrrick/Matter_By_Mavrrick/refs/heads/main/Air_Purifier_Leedarson.groovy

1 Like

Thanks so much. I need to email them again to let them know that you were able to implement auto mode. I'll mention the fan % for sleep mode as a possibility. I don't think that will fly for them and we'll just have to wait for an alternative for sleep mode as a discrete on/off.
Thanks again.

1 Like

I just posted a update to the Code in Github. It looks like I missed something in most of my fan drivers that is causing issues in the latest Hubitat firmare with Google Home. The update should fix it

1 Like

Hi @mavrrick58 . Thanks for doing this. I updated the driver code and it worked fine (old hub firmware). Then I updated the hub firmware and tested again. Something is still not quite right. When I set the fan speed, sometimes it works and sometimes the purifier turns off. I haven't found a pattern of when it works properly vs. when it turns off. Thanks again for all your help.

DId sometimes works sometimes turn off just start with this last update. The update really only did one thing. It populated a device attribute that was blank previously. This didn't change any code to related to how MATTER works..

I believe yes. I never noticed that behavior before. Thanks

I really don't see how that change could have any impact on the devices. It was one line of code that all it does is update a unused attribute to support Google Home. Please click on initialize and give it some time to see if this continues.

Well, Initialize fixed it.
Thanks!

1 Like

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.