/**
* **************** Office Fan Parent App ****************
*
* Usage:
* This was designed to run a converted tower fan using relays to control the fan with three speeds
* Each speed switch device is part of a 4-channel relay board.
* The last relay is used as Oscillate mode, powering the oscillate motor.
* Requirements:
* A four channel smart Ziogbee relay baord
* Three of the relays have been physically attached to supply voltage to the appropriate motor wires for low, medium and high, and Oscillate
* The board I am using needs 5v usb, or 7-48v). An external power supply may be needed.
*
* Version 1.0 - 12/15/25 - Forked from Fan Hood Parent app.
**/
definition (
name: "Office Fan Parent App",
namespace: "Hubitat",
author: "ChrisB",
description: "Controller for a Converted Tower Fan using Relays",
category: "My Apps",
iconUrl: "",
iconX2Url: ""
)
preferences {
page name: "mainPage", title: "", install: true, uninstall: true
}
def mainPage() {
dynamicPage(name: "mainPage") {
section("Office Fan Speed Low Device") {
input (
name: "fanLow",
type: "capability.switch",
title: "Select Fan Speed Low Switch Device",
required: false,
multiple: false,
submitOnChange: true
)
}
section("Office Fan Speed Medium Device") {
input (
name: "fanMedium",
type: "capability.switch",
title: "Select Fan Speed Medium Switch Device",
required: false,
multiple: false,
submitOnChange: true
)
}
section("Office Fan Speed High Device") {
input (
name: "fanHigh",
type: "capability.switch",
title: "Select Fan Speed High Switch Device",
required: false,
multiple: false,
submitOnChange: true
)
}
section("Office Fan Relay Device") {
input (
name: "fanRelay",
type: "capability.switch",
title: "Select Fan Relay Switch Device",
required: false,
multiple: false,
submitOnChange: true
)
}
section("Office Fan Occilate Device") {
input (
name: "fanOsc",
type: "capability.switch",
title: "Select Office Fan OSC Switch Device",
required: false,
multiple: false,
submitOnChange: true
)
}
section("Fan Control with 4-button Scene Switch") {
input (
name: "fanSceneSwitch",
type: "capability.actuator",
title: "Select Fan Scene Switch Device (optional)",
required: false,
multiple: false,
submitOnChange: true
)
}
section("Fan Speed Indicator Light Driver") {
input (
name: "speedLight",
type: "capability.actuator",
title: "Select Indicator Light Driver Device (optional)",
required: false,
multiple: false,
submitOnChange: true
)
}
section("") {
input (
name: "useAutoOff",
type: "bool",
title: "Enable Auto-Off",
required: true,
defaultValue: false
)
}
section("") {
input (
name: "autoOff",
type: "enum",
title: "Auto Off Minutes",
options: ["600":10, "1200":20,"1800":30,"2700":45,"3600":60,"5400":90,"7200":120],
multiple: false,
defaultValue: 60,
required: false
)
}
section("") {
input (
name: "debugMode",
type: "bool",
title: "Enable logging",
required: true,
defaultValue: false
)
}
}
}
def installed() {
updated()
}
def updated() {
setOfficeFanDevice()
unsubscribe()
initialize()
}
def initialize() {
if (fanSceneSwitch) {
subscribe(fanSceneSwitch, "pushed", fanSceneSwitchHandler) // control with a scene switch
subscribe(fanSceneSwitch, "doubleTapped", fanSceneSwitchTimerHandler) // control timer with a scene switch
}
// subscribe for changes made on the Zigbee relay baord or with the rf board remote
subscribe(fanOsc, "switch", fanOscHandler)
subscribe(fanLow, "switch", fanLowHandler)
subscribe(fanMedium, "switch", fanMediumHandler)
subscribe(fanHigh, "switch", fanHighHandler)
subscribe(fanRelay, "switch", fanOnHandler) // subscribe for sensing fan turned on any speed for autooff
}
def setOscillate(status) {
if (status == "on") {
fanOsc.on()
}
if (status == "off") {
fanOsc.off()
}
}
def setOfficeFanDevice() {
logDebug("settingOfficeFanDevice")
if (!fanDriver) {
def ID = createOfficeFanDevice()
app.updateSetting("fanDriver",[value:getChildDevice(ID),type:"capability.actuator"])
}
}
def createOfficeFanDevice() {
logDebug("createOfficeFanDevice() called")
def deviceNetworkId = "CD_${app.id}_${new Date().time}"
try {
def fanHoodDevice = addChildDevice(
"Hubitat",
"Office Fan Child Driver",
deviceNetworkId,
null,
[name: "Office Fan Child Device", label: "Office Fan", isComponent: false]
)
logDebug("Created Office Fan device in 'Hubitat' using driver 'Office Fan Child Driver' (DNI: ${deviceNetworkId})")
state.fanHoodDevice = deviceNetworkId
return deviceNetworkId
} catch (Exception e) {
log.error "Failed to create Office Fan device: ${e}"
}
}
// driver speed change event - Change fan speed
def fanSpeedHandler(speed) {
logDebug("fanSpeedHandler(${speed}) called")
def status = fanDriver.currentValue("switch")
if (speed == "off") {fanOff()}
else if (speed == "low") {fanOnLow()}
else if (speed == "medium") {fanOnMed()}
else if (speed == "high") {fanOnHigh()}
}
def fanSwitchHandler(status, speed) {
if (status == "off") {
fanRelay.off()
}
else if (status == "on") {
}
}
// set the fan to the driver speed from child device
def setSpeed(speed) {
def action = ""
if (speed == "low" && currentFanSpeed() != "low") {fanOnLow(); action = "low"}
if (speed == "medium" && currentFanSpeed() != "medium") {fanOnMed(); action = "medium"}
if (speed == "high" && currentFanSpeed() != "high") {fanOnHigh(); action = "high"}
if (speed == "off" && currentFanSpeed() != "off") {fanOff(); action = "off"}
// set light color with speed, if using indicator
if (speedLight) {
if (speed == "low") {
speedLight.setColor("Yellow") // yellow
}
if (speed == "medium") {
speedLight.setColor("Orange") // orange
}
if (speed == "high") {
speedLight.setColor("Red") // red
}
if (speed == "off") {
speedLight.setColor("Black") // none
}
}
}
// Turn on speed and turn other speeds off
def fanOnLow() {
turnOffOtherSpeeds(false, true, true)
pauseExecution(100)
fanLow.on()
}
def fanOnMed() {
turnOffOtherSpeeds(true, false, true)
pauseExecution(100)
fanMedium.on()
}
def fanOnHigh() {
turnOffOtherSpeeds(true, true, false)
pauseExecution(100)
fanHigh.on()
}
def fanOff() {
fanRelay.off() // turn off the whole board with parent relay
}
def turnOffOtherSpeeds(low, medium, high) {
if (low && fanLow.currentValue("switch") == "on") {fanLow.off()}
if (medium && fanMedium.currentValue("switch") == "on") {fanMedium.off()}
if (high && fanHigh.currentValue("switch") == "on") {fanHigh.off()}
}
// get current speed
String currentFanSpeed() {
if (fanLow.currentValue("switch") == "on") {return "low"}
if (fanMedium.currentValue("switch") == "on") {return "medium"}
if (fanHigh.currentValue("switch") == "on") {return "high"}
else {return "off"}
}
// Scene Switch Multi button Handler for fan speeds
def fanSceneSwitchHandler(evt) {
logDebug("fanSceneSwitchHandler(${evt.value}) called")
def pressed = evt.value.toInteger()
def speed = ""
if (pressed == 1) {speed = "off"}
if (pressed == 2) {speed = "low"}
if (pressed == 3) {speed = "medium"}
if (pressed == 4) {speed = "high"}
fanDriver.setSpeed(speed)
}
// activate fan power off switch (4th relay clears all speed relays on the board when on)
def turnFanOff() {
fanOff()
}
// start timer when fan turns on to any speed (board switch turns on)
def fanOnHandler(evt) {
logDebug("fanOnHandler called with ${evt.value}")
if (evt.value == "on") { // fan On
def timerMinutes = (settings?.autoOff).toInteger()
logDebug("timerMinutes is ${timerMinutes}")
if (settings?.useAutoOff) runIn(timerMinutes,turnFanOff)
}
if (evt.value == "off") { // Fan Off
unschedule()
}
}
// ********** From Board Buttons (and RF remote) ***********
def fanLowHandler(evt) {
logDebug("fanLowHandler called with ${evt.value}")
if (evt.value == "on") {
fanOnLow()
fanDriver.setSpeedAttribute("low")
}
if (evt.value == "off" && checkOtherSpeedsOff("low")) {
fanDriver.off()
}
}
def fanMediumHandler(evt) {
logDebug("fanHighHandler called with ${evt.value}")
if (evt.value == "on") {
fanOnMed()
fanDriver.setSpeedAttribute("medium")
}
if (evt.value == "off" && checkOtherSpeedsOff("medium")) {
fanDriver.off()
}
}
def fanHighHandler(evt) {
logDebug("fanHighHandler called with ${evt.value}")
if (evt.value == "on") {
fanOnHigh()
fanDriver.setSpeedAttribute("high")
}
if (evt.value == "off" && checkOtherSpeedsOff("high")) {
fanDriver.off()
}
}
def fanOscHandler(evt) {
logDebug("fanOscHandler called with ${evt.value}")
if (evt.value == "on") {
fanDriver.setOscillateAttribute("on")
}
if (evt.value == "off") {
fanDriver.setOscillateAttribute("off")
}
}
// For rf remote or board buttons, lets a speed button act as off when pressed while current speed matches button
// could be done by checking driver speed, but hardware check is safer
def checkOtherSpeedsOff(speed) {
logDebug("checkOtherSpeedsOff(${speed}) called")
def othersOff = false
lowOff = fanLow.currentValue("switch") == "off"
medOff = fanMedium.currentValue("switch") == "off"
highOff = fanHigh.currentValue("switch") == "off"
logDebug("lowOff=${lowOff} medOff=${medOff} highOff=${highOff}")
if (speed == "low") {if (medOff && highOff) {othersOff = true}}
if (speed == "medium") {if (lowOff && highOff) {othersOff = true}}
if (speed == "high") {if (lowOff && medOff) {othersOff = true}}
logDebug("returning ${othersOff}")
return othersOff
}
def logDebug(txt){
try {
if (settings.debugMode) { log.debug("${app.label} - ${txt}") }
} catch(ex) {
log.error("bad debug message")
}
}