I believe I had to use the "Enhanced Virtual Fan Driver" - See:
I think I ended up tweaking my copy to support the 3 speeds the fan supports as well as on/off and I had to "adjust" the speed settings, that matched with L/M/H. Sounds like you already have working rules to mirror these actions to the "real" fan device (or I think you can use Switch Bindings as well} - See this thread: Mirroring Virtual Fan to Physical Fan Controller
But as if I understand what you wrote, you just need to replace the driver for your virtual fan device with this "enhanced" driver, and likely remap it into Alexa. I shared my edits below, as I was updated this to work with an Inovelli Matter Canopy module, and I had to use this driver (with my tweaks) to get it working in Alexa: - My specific use case for the physical fan is: White Series Smart Fan/Light Canopy Module | Inovelli And yes, similar to you, I wrote a series of rules to map the enhanced virtual fan status to the physical fan.
Summary
/**
* Enhanced Virtual Fan Controller
*
* Copyright 2019 Joel Wetzel
*
* 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.
*
*/
metadata {
definition (name: "Enhanced Virtual Fan Controller", namespace: "joelwetzel", author: "Joel Wetzel", description: "A virtual fan controller that also behaves as a switch.") {
capability "Refresh"
capability "Actuator"
capability "Sensor"
capability "Switch"
capability "Switch Level"
capability "Fan Control"
attribute "lastSpeed", "string"
}
preferences {
section {
input (
type: "bool",
name: "enableDebugLogging",
title: "Enable Debug Logging?",
required: true,
defaultValue: false
)
}
}
}
def log (msg) {
if (enableDebugLogging) {
log.debug msg
}
}
def installed () {
initialize()
log.info "${device.displayName}.installed()"
updated()
}
def updated () {
initialize()
log.info "${device.displayName}.updated()"
}
def initialize() {
log.info "${device.displayName}.initialize()"
// Default values
sendEvent(name: "switch", value: "off", isStateChange: true)
sendEvent(name: "level", value: "0", isStateChange: true)
sendEvent(name: "speed", value: "off", isStateChange: true)
sendEvent(name: "lastSpeed", value: "low", isStateChange: true)
}
def refresh() {
}
def on() {
log "${device.displayName}.on()"
def lastSpeed = device.currentValue("lastSpeed")
sendEvent(name: "switch", value: "on", isStateChange: true)
sendEvent(name: "speed", value: lastSpeed, isStateChange: true)
sendEvent(name: "level", value: convertSpeedToLevel(lastSpeed), isStateChange: true)
}
def off() {
log "${device.displayName}.off()"
sendEvent(name: "switch", value: "off", isStateChange: true)
sendEvent(name: "speed", value: "off", isStateChange: true)
sendEvent(name: "level", value: 0, isStateChange: true)
}
def setSpeed(speed) {
log "${device.displayName}.setSpeed($speed)"
def adjustedSpeed = restrictSpeedLevels(speed) // Only allow certain speed settings. For example, don't allow "medium-high".
def adjustedLevel = convertSpeedToLevel(adjustedSpeed) // Some fan controllers depend on speed, some depend on level. Convert the speed to a level.
def adjustedSwitch = (adjustedSpeed == "off") ? "off" : "on" // If speed is "off", then turn off the switch too.
// Keep track of the last speed while on. Then if the fan is off, and
// we turn it back on, we can go back to the last on speed.
if (adjustedSpeed != "off") {
sendEvent(name: "lastSpeed", value: adjustedSpeed, isStateChange: true)
}
sendEvent(name: "switch", value: adjustedSwitch, isStateChange: true)
sendEvent(name: "speed", value: adjustedSpeed, isStateChange: true)
sendEvent(name: "level", value: adjustedLevel, isStateChange: true)
}
// If our input is level, convert it to a speed input.
def setLevel(level) {
log "${device.displayName}.setLevel($level)"
def requestedSpeed = convertLevelToSpeed(level)
setSpeed(requestedSpeed)
}
// This converts speeds back into levels. These values correspond well to a GE
// Z-Wave Plus Fan Controller, but might need to change for other smart fan
// controllers.
def convertSpeedToLevel(speed) {
switch (speed) {
case "off":
return 0
case "low":
return 33
case "medium":
return 66
case "high":
return 99
default:
return 10
}
}
// This restricts allowed speed levels. The GE Z-Wave Plus Smart Fan
// Controller doesn't support medium-low, medium-high, or auto, so
// this converts them into something else.
def restrictSpeedLevels(speed) {
switch (speed) {
case "off":
return "off"
case "low":
return "low"
case "medium-low":
return "medium"
case "medium":
return "medium"
case "medium-high":
return "high"
case "high":
return "high"
case "on":
return "low"
case "auto":
return "off"
default:
return "medium"
}
}
// This maps ranges of levels into speed values. Right now it's set for just
// three speeds and off.
def convertLevelToSpeed(level) {
if (level == 0) {
return "off"
}
else if (level < 34) {
return "low"
}
else if (level < 67) {
return "medium"
}
else {
return "high"
}
}