[Release] Fakro FTP-V Z-Wave Roof Window

Here's a driver I developed for my Fakro roof windows.

/**
 *  Fakro Roof Window driver for Hubitat
 * 
 *  Copyright 2021 Simon J Burke - ScruffySJB
 *
 *  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.
 *  
 * 
 ***************************Version 1.0******************************************
 *  Version History
 *
 *  2021-09-19: Version 1.0 Initial release
 *
*/

import groovy.transform.Field

metadata {
   definition (name: "Fakro Window Driver", namespace: "Scruffy-sjb", author: "Scruffy-SJB") {
      capability "Actuator"
      capability "WindowShade" 
      capability "Switch"  
      capability "Refresh"
      capability "Configuration"
	  capability "Notification"
	  capability "Sensor"
	  capability "Health Check"
	  
	  command "open"
      command "close"
      command "off"
      command "on"

      attribute "windowOpen", "String"
	  attribute "refreshRate", "String"
		
      fingerprint  mfr: "0085", deviceType: "0002", deviceId: "0017", inClusters: "0x5E,0x86,0x72,0x5A,0x85,0x59,0x73,0x25,0x26,0x27,0x70,0x71,0x77", deviceJoinName: "Fakro FTP-V, FTU-V Roof Window" 
   }

// Chain Execution Speeds
def chain =[:]
    chain << ["1" : "Slowest"]
    chain << ["2" : "Medium Slow (Default)"]
    chain << ["3" : "Medium Fast"]
    chain << ["4" : "Fastest"]
    
// Chain Execution Speeds (Rain Sensor)
def rain =[:]
    rain << ["1" : "Slowest"]
    rain << ["2" : "Medium Slow (Default)"]
    rain << ["3" : "Medium Fast"]
    rain << ["4" : "Fastest"]
    
// Calibration
def calib =[:]
    calib << ["1" : "Calibration (Default)"]
    calib << ["2" : "Decalibration"]

// Goto Position
def gotop =[:]
    gotop << ["1" : "Open to Maximum (Default)"]
    gotop << ["2" : "Goto Previous Position"]

//Rates for Poll
def rates = [:]
    rates << ["0" : "Disabled"]
    rates << ["1" : "Refresh every minute"]
    rates << ["5" : "Refresh every 5 minutes (Default)"]
    rates << ["10" : "Refresh every 10 minutes"]
    rates << ["15" : "Refresh every 15 minutes"]
    
@Field static Map<String, Map<Short, String>> supervisedPackets = [:]
@Field static Map<String, Short> sessionIDs = [:]   
@Field static Map commandClassVersions = 
[
         0x01: 2    // z-wave+ info
        ,0x20: 1    // basic
        ,0x25: 1    // switch binary
        ,0x26: 3    // switchMultiLevel    
        ,0x27: 1    // switch all
        ,0x59: 1    // association group info
        ,0x5A: 1    // device reset locally
        ,0x70: 1    // configuration    
        ,0x71: 3    // notification
        ,0x72: 2    // manufacturer specific
        ,0x73: 1    // powerlevel    
        ,0x77: 1    // node naming
        ,0x85: 2    // association
     	,0x86: 2    // version
        ,0x98: 1    // security
]   
    
   preferences {
      //parameter 7
      input name: "chainExecutionSpeed", type: "enum", options: chain, description: "Select chain execution speed", title: "Window Execution Speed", range: 1..4, defaultValue: 2, required: true
      //parameter 8
      input name: "rainExecutionSpeed", type: "enum", options: rain, description: "Select chain execution speed (rain)", title: "Window Execution Speed (Rain)", range: 1..4, defaultValue: 2, required: true       
	  //parameter 12
      input name: "calibration", type: "enum", options: calib, description: "Select calibration", title: "Calibration", range: 1..2, defaultValue: 1, required: true
      //parameter 13	  
      input name: "gotoPosition", type: "enum", options: gotop, description: "Select position", title: "Go to Position", range: 1..2, defaultValue: 1, required: true
      //parameter 15	  
      input name: "ventilation", type: "number", description: "", title: "Close after set time (Minutes)", range: "0..120", defaultValue: 0, required: false
      input name: "refreshRate", type: "enum", title: "Refresh rate", options: rates, description: "Select refresh rate", defaultValue: "5", required: false
   }
}

// parse events into attributes
def parse(String description) {
    // log.debug "Parsing '${description}'"
    def result = []
    if (description.startsWith("Err 106")) {
        state.sec = 0
        result = createEvent(descriptionText: description, isStateChange: true)
    }
    else {
        def cmd = zwave.parse(description,[0x75:1])
        if (cmd) {
            result += zwaveEvent(cmd)
            //				log.debug "Parsed ${cmd} to ${result.inspect()}"
        } else {
            log.debug "Non-parsed event: ${description}"
        }
    }
    return result
}

def zwaveEvent(hubitat.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd){
    log.info "Report Received : Window open ${cmd.value}%"
    sendEvent(name: "lastCheckin", value: new Date())
	windowEvents(cmd)
}

def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd) {
//    log.debug "Basic report received - cmd.value = $cmd.value"
    sendEvent(name: "lastCheckin", value: new Date())
    windowEvents(cmd)    
}

private void windowEvents(hubitat.zwave.Command cmd) {
   def position = cmd.value

   if (position > 0)  {
      sendEvent(name: "windowOpen", value: "yes")
      switchValue = "on" 
      log.info "$device.displayName window is open"       
   } else {
      sendEvent(name: "windowOpen", value: "no")
      switchValue = "off"  
      log.info "$device.displayName window is closed"       
   }
    
    sendEvent(name: "position", value: position, unit: "%")  
    sendEvent(name: "switch", value: switchValue)
    log.info "$device.displayName position is $position"
    log.info "$device.displayName switch is $switchValue"
}

def zwaveEvent(hubitat.zwave.commands.associationv2.AssociationReport cmd) {
    def result = []
    if (cmd.nodeId.any { it == zwaveHubNodeId }) {
        result << sendEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}")
    } else if (cmd.groupingIdentifier == 1) {
        // We're not associated properly to group 1, set association
        result << sendEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}")
        result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId))
    }
    log.info "Report Received : $cmd"
    result
}

def zwaveEvent(hubitat.zwave.commands.notificationv3.NotificationReport cmd) {
    def events = []
    switch (cmd.notificationType) {
//       case 8:
//        processPowerMgtNot(cmd)
//        break

//        case 9:
//        processSystemNot(cmd)
//        break
   }

    log.info "Notification Report : $cmd"
    sendEvent(name: "lastCheckin", value: new Date())
}

def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation	 cmd) { // Devices that support the Security command class can send messages in an encrypted form; they arrive wrapped in a SecurityMessageEncapsulation command and must be unencapsulated
    log.debug "raw secEncap $cmd"
    state.sec = 1
    def encapsulatedCommand = cmd.encapsulatedCommand ([0x20: 1, 0x80: 1, 0x70: 1, 0x72: 1, 0x31: 5, 0x26: 3, 0x75: 1, 0x40: 2, 0x43: 2, 0x86: 1, 0x71: 3, 0x98: 2, 0x7A: 1 ]) 

    if (encapsulatedCommand) {
        log.warn "Encapsulated command extracted sucessfully"
        return zwaveEvent(encapsulatedCommand)
    } else {
        log.warn "Unable to extract encapsulated cmd from $cmd"
        createEvent(descriptionText: cmd.toString())
    }
}

def zwaveEvent(hubitat.zwave.Command cmd) {
    def map = [ descriptionText: "${device.displayName}: ${cmd}" ]
    log.warn "mics zwave.Command - ${device.displayName} - $cmd"
    sendEvent(map)
}

def zwaveEvent(hubitat.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) {
    if (cmd.manufacturerName) { updateDataValue("manufacturer", cmd.manufacturerName) }
    if (cmd.productTypeId) { updateDataValue("productTypeId", cmd.productTypeId.toString()) }
    if (cmd.productId) { updateDataValue("productId", cmd.productId.toString()) }
    if (cmd.manufacturerId){ updateDataValue("manufacturerId", cmd.manufacturerId.toString()) }
    log.info "Report Received : $cmd"
}

def zwaveEvent(hubitat.zwave.commands.zwaveplusinfov2.ZwaveplusInfoReport cmd) {
    if (cmd.zWaveplusNodeType) { updateDataValue("Z-Wave+NodeType", cmd.zWaveplusNodeType.toString()) }
    if (cmd.zWaveplusRoleType) { updateDataValue("Z-Wave+RoleType", cmd.zWaveplusRoleType.toString()) }
    if (cmd.zWaveplusVersion){ updateDataValue("Z-Wave+Version", cmd.zWaveplusVersion.toString()) }
    log.info "Report Received : $cmd"
}

def zwaveEvent(hubitat.zwave.commands.configurationv2.ConfigurationReport cmd ) {
//    log.info "Configuration Report Received : $cmd"
    def events = []

    switch (cmd.parameterNumber) {
        case 7:
        events << processParam7(cmd)
        break

        case 8:
        events << processParam8(cmd)
        break
        
        case 12:
        events << processParam12(cmd)
        break

        case 13:
        events << processParam13(cmd)
        break

        case 15:
        events << processParam15(cmd)
        break
		
		default:
		log.debug "Parameter $cmd received"
		break
    }
}

def zwaveEvent(hubitat.zwave.commands.versionv1.VersionReport cmd) {
    log.info "Version Report: $cmd"
    def zWaveLibraryTypeDisp  = String.format("%02X",cmd.zWaveLibraryType)
    def zWaveLibraryTypeDesc  = ""
    switch(cmd.zWaveLibraryType) {
        case 1:
        zWaveLibraryTypeDesc = "Static Controller"
        break

        case 2:
        zWaveLibraryTypeDesc = "Controller"
        break

        case 3:
        zWaveLibraryTypeDesc = "Enhanced Slave"
        break

        case 4:
        zWaveLibraryTypeDesc = "Slave"
        break

        case 5:
        zWaveLibraryTypeDesc = "Installer"
        break

        case 6:
        zWaveLibraryTypeDesc = "Routing Slave"
        break

        case 7:
        zWaveLibraryTypeDesc = "Bridge Controller"
        break

        case 8:
        zWaveLibraryTypeDesc = "Device Under Test (DUT)"
        break

        case 0x0A:
        zWaveLibraryTypeDesc = "AV Remote"
        break

        case 0x0B:
        zWaveLibraryTypeDesc = "AV Device"
        break

        default:
            zWaveLibraryTypeDesc = "N/A"
    }

    def zWaveVersion = String.format("%d.%02d",cmd.zWaveProtocolVersion,cmd.zWaveProtocolSubVersion)
    def firmwareLevel = String.format("%d.%02d",cmd.firmware0Version,cmd.firmware0SubVersion)
    log.info "Z-Wave type is $zWaveLibraryTypeDesc - Z-Wave Version is $zWaveVersion - Device Firmware Level is $firmwareLevel"
}   

def configure() {
    state.clear()            // Clear all previous states
    unschedule()      
    def cmds = []
    cmds << zwave.configurationV1.configurationSet(configurationValue: chainExecutionSpeed == "1" ? [0x01] : chainExecutionSpeed == "2" ? [0x02] : chainExecutionSpeed == "3" ? [0x03] : chainExecutionSpeed == "4" ? [0x04] : [0x02], parameterNumber:7, size:1, scaledConfigurationValue:  chainExecutionSpeed == "1" ? 0x01 : chainExecutionSpeed == "2" ? 0x02 : chainExecutionSpeed == "3" ? 0x03 : chainExecutionSpeed == "4" ? 0x04 : 0x02)
    cmds << zwave.configurationV1.configurationSet(configurationValue: chainExecutionSpeed == "1" ? [0x01] : rainExecutionSpeed == "2" ? [0x02] : rainExecutionSpeed == "3" ? [0x03] : rainExecutionSpeed == "4" ? [0x04] : [0x02], parameterNumber:8, size:1, scaledConfigurationValue:  rainExecutionSpeed == "1" ? 0x01 : rainExecutionSpeed == "2" ? 0x02 : rainExecutionSpeed == "3" ? 0x03 : rainExecutionSpeed == "4" ? 0x04 : 0x02)
    cmds << zwave.configurationV1.configurationSet(configurationValue: calibration == "1" ? [0x01] : calibration == "2" ? [0x02] : [0x01], parameterNumber:12, size:1, scaledConfigurationValue:  calibration == "1" ? 0x01 : calibration == "2" ? 0x02 : 0x01)
    cmds << zwave.configurationV1.configurationSet(configurationValue: gotoPosition == "1" ? [0x01] : gotoPosition == "2" ? [0x02] : [0x01], parameterNumber:13, size:1, scaledConfigurationValue:  gotoPosition == "1" ? 0x01 : gotoPosition == "2" ? 0x02 : 0x01)
    cmds << zwave.configurationV1.configurationSet(parameterNumber:15, scaledConfigurationValue: ventilation.toInteger())
    cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) 
    cmds << zwave.switchMultilevelV3.switchMultilevelGet()
    cmds << zwave.configurationV1.configurationGet(parameterNumber:7)
    cmds << zwave.configurationV1.configurationGet(parameterNumber:8)    
    cmds << zwave.configurationV1.configurationGet(parameterNumber:12)
    cmds << zwave.configurationV1.configurationGet(parameterNumber:13)
    cmds << zwave.configurationV1.configurationGet(parameterNumber:15)
    cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet()
    cmds << zwave.versionV1.versionGet()
    cmds << zwave.zwaveplusInfoV2.zwaveplusInfoGet()
    
    sendEvent(name: "configuration", value: "sent", displayed: true)
    log.info "Configuration sent"
    secureSequence(cmds)    
}

def updated() {
    if (!state.updatedLastRanAt || new Date().time >= state.updatedLastRanAt + 2000) {
        state.updatedLastRanAt = new Date().time
        unschedule(refresh)
        unschedule(poll)
        runIn (01, configure)
        sendEvent(name: "refreshRate", value: refreshRate)
		
		switch(refreshRate) {
            case "1":
            runEvery1Minute(poll)
            log.info "Refresh Scheduled for every minute"
            break
            case "15":
            runEvery15Minutes(poll)
            log.info "Refresh Scheduled for every 15 minutes"
            break
            case "10":
            runEvery10Minutes(poll)
            log.info "Refresh Scheduled for every 10 minutes"
            break
            case "5":
            runEvery5Minutes(poll)
            log.info "Refresh Scheduled for every 5 minutes"
            break
            case "0":
            runIn(60*60*24, 'poll')    // Must poll at least once per day
            log.info "Refresh off"}
    }
    else {
        log.warn "update ran within the last 2 seconds"
    }
}

// PING is used by Device-Watch in attempt to reach the Device
def ping() {
    refresh()
}

//Refresh
def refresh() {
    def cmds = []
    log.trace "refresh"
    cmds <<	zwave.basicV1.basicGet()						// get mode (basic)	
    cmds <<	zwave.sensorMultilevelV1.sensorMultilevelGet()	// get temp
    cmds << zwave.switchMultilevelV3.switchMultilevelGet()	// position
    secureSequence (cmds)
}

def secure(hubitat.zwave.Command cmd) {
    // state.sec = 1
    
    if (state.sec) {
        // log.debug "Seq secure - $cmd"
        zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()
        // zwaveSecureEncap(supervisedEncap(cmd).format())
    } 
    else {
        // log.debug "Seq unsecure- $cmd"
        cmd.format()
    }
}

def secureSequence(cmds, delay=1500) {
    //log.debug "SeSeq $commands"
    delayBetween(cmds.collect{ secure(it) }, delay)
}	

private currentDouble(attributeName) {
    if(device.currentValue(attributeName)) {
        return device.currentValue(attributeName).doubleValue()
    }
    else {
        return 0d
    }
}

//chainExtensionSpeed	
private processParam7(cmd) {
    def setValue
    if (cmd.scaledConfigurationValue == 1) {
        setValue = "1 - Slowest"
    }
    if (cmd.scaledConfigurationValue == 2) {
        setValue = "2 - Medium Slow"
    }
    if (cmd.scaledConfigurationValue == 3) {
        setValue = "3 - Medium Fast"
    }
    if (cmd.scaledConfigurationValue == 4) {
        setValue = "4 - Fastest"
    }
    log.info "Configuration Report - Chain Extension Speed: ${setValue}"
}

//rainExtensionSpeed	
private processParam8(cmd) {
    def setValue
    if (cmd.scaledConfigurationValue == 1) {
        setValue = "1 - Slowest"
    }
    if (cmd.scaledConfigurationValue == 2) {
        setValue = "2 - Medium Slow"
    }
    if (cmd.scaledConfigurationValue == 3) {
        setValue = "3 - Medium Fast"
    }
    if (cmd.scaledConfigurationValue == 4) {
        setValue = "4 - Fastest"
    }
    log.info "Configuration Report - Chain Extension Speed (Rain): ${setValue}"
}

//calibration
private processParam12(cmd) {
    def setValue
    if (cmd.scaledConfigurationValue == 1) {
        setValue = "1 - Calibrated"
    }
    if (cmd.scaledConfigurationValue == 2) {
        setValue = "2 - Out of Calibration"
    }
    if (cmd.scaledConfigurationValue == 3) {
        setValue = "3 - Encoder Error"
    }
    log.info "Configuration Report - Calibration: ${setValue}"
}

//gotoPosition
private processParam13(cmd) {
    def setValue
    if (cmd.scaledConfigurationValue == 1) {
        setValue = "1 - Goto Maximum"
    }
    if (cmd.scaledConfigurationValue == 2) {
        setValue = "2 - Goto Previous Position"
    }
    log.info "Configuration Report - Goto Position: ${setValue}"
}

//ventilation
private processParam15(cmd) {
    def setValue 
//    setValue = cmd.scaledConfigurationValue.ToString
	log.info "Configuration Report - Ventilation - Close after ${cmd.scaledConfigurationValue} minutes"
}

def poll() { 
    log.info "Polling...."

    def cmds = []
    cmds <<	zwave.basicV1.basicGet()						// get basic	
    cmds << zwave.switchMultilevelV3.switchMultilevelGet()	// position
    secureSequence (cmds)
}

void debugOff() {
   log.warn("Disabling debug logging")
   device.updateSetting("enableDebug", [value:"false", type:"bool"])
}

void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionGet cmd) {
    hubitat.zwave.Command encapCmd = cmd.encapsulatedCommand(commandClassVersions)
    log.debug "Supervision cmd received"
    if (encapCmd) {
        zwaveEvent(encapCmd)
    }
    sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(zwave.supervisionV1.supervisionReport(sessionID: cmd.sessionID, reserved: 0, moreStatusUpdates: false, status: 0xFF, duration: 0).format()), hubitat.device.Protocol.ZWAVE))
}

void zwaveEvent(hubitat.zwave.commands.supervisionv1.SupervisionReport cmd) {
    log.debug "supervision report for session: ${cmd.sessionID}"
    if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
    if (supervisedPackets["${device.id}"][cmd.sessionID] != null) { supervisedPackets["${device.id}"].remove(cmd.sessionID) }
    unschedule(supervisionCheck)
}

void supervisionCheck() {
    // re-attempt once
/*    if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
    
    supervisedPackets["${device.id}"].each { k, v ->
        log.debug "re-sending supervised session: ${k}"
        sendHubCommand(new hubitat.device.HubAction(zwaveSecureEncap(v), hubitat.device.Protocol.ZWAVE))
        supervisedPackets["${device.id}"].remove(k)
    }
*/
}

Short getSessionId() {
    Short sessId = 1
    if (!sessionIDs["${device.id}"]) {
        sessionIDs["${device.id}"] = sessId
        return sessId
    } else {
        sessId = sessId + sessionIDs["${device.id}"]
        if (sessId > 63) sessId = 1
        sessionIDs["${device.id}"] = sessId
        return sessId
    }
}

hubitat.zwave.Command supervisedEncap(hubitat.zwave.Command cmd) {
    if (getDataValue("S2")?.toInteger() != null) {
        hubitat.zwave.commands.supervisionv1.SupervisionGet supervised = new hubitat.zwave.commands.supervisionv1.SupervisionGet()
        supervised.sessionID = getSessionId()
        log.debug "new supervised packet for session: ${supervised.sessionID}"
        supervised.encapsulate(cmd)
        if (!supervisedPackets."${device.id}") { supervisedPackets."${device.id}" = [:] }
        supervisedPackets["${device.id}"][supervised.sessionID] = supervised.format()
        runIn(5, supervisionCheck)
        return supervised
    } else {
        return cmd
    }
}

void zwaveEvent(hubitat.zwave.commands.switchmultilevelv2.SwitchMultilevelStopLevelChange cmd) {
   logDebug("SwitchMultilevelStopLevelChange: $cmd")
   sendHubCommand(
      new hubitat.device.HubAction(zwave.switchMultilevelV1.switchMultilevelGet().format(),
                                    hubitat.device.Protocol.ZWAVE)
   )
}

def on() {
	log.trace("on")
    open()
}

def off() {
	log.trace("off")
    close()
}

def open() {
	log.trace("Open")
	state.switchState = "open"
	setLevel(255)    
}

def close() {
	log.trace("Close")
	state.switchState = "closed"
	setLevel(0)  
}

def setLevel(level) {
	log.trace "SetPosition value:${level}"
    def cmds = []
	cmds << zwave.basicV1.basicSet(value: level) 
    cmds <<	zwave.switchMultilevelV3.switchMultilevelGet()    
    secureSequence (cmds)
}

String startLevelChange(direction){
    Integer upDown = direction == "down" ? 1 : 0
    return secure(zwave.switchMultilevelV1.switchMultilevelStartLevelChange(upDown: upDown, ignoreStartLevel: 1, startLevel: 0))
}

List<String> stopLevelChange(){
    return [
            secure(zwave.switchMultilevelV1.switchMultilevelStopLevelChange())
            ,"delay 200"
            ,secure(zwave.basicV1.basicGet())
    ]
}
5 Likes

I will try this out with our Fakro skylights and roof windows!

1 Like

Hi Simon
Thanks for sharing. I am using your driver with a Fakro ZWS12 chain actuator with some success. I am able to issue open or close commands successfully but any other operation fails with an error like this:

org.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_Scruffy_sjb_Fakro_Window_Driver_619.startPositionChange() is applicable for argument types: (java.lang.String) values: [open] (method startPositionChange)

Unfortunately I am not skilled enough to diagnose or fix this. Any suggestions?

Regards
Andrew
Australia

Hi Andrew,

Glad it's mostly working for you.

I'm in the US at the moment and my Fakros are in Ireland. So I can't test anything until I get back there in mid-December.

The commands the driver has implemented are Open, Close and On, Off.

What command are you trying that gives this error (it's looking for a startPositionChange routine, which is not in the code).

Thanks, Simon

Can anyone tell me how to pair Fakro to hubitat, i can't find any information about it, thank you

Have a look here:

Thank you very much, but i didn't get, do i have to reset widow first? And is the remote will still be working after i pair window with hub? Sorry I'm new to automatons, thank you for your help

If the window and remote are paired together then you will need to un-pair (called excluding).
Once the window is excluded (factory reset would do this as well) it can then be included with Hubitat.

i failed to get the remote to pair with Hubitat. i contacted Fakro support and they kept telling me how to pair the remote with the window and never answered my actual question. So I have the Fakro remote in a box. However, once your window is working with Hubitat you have loads of other options in how to control your windows.

I hope that helps.

Thank you, but i have to keep remote working for my wife :grinning:

You could use a different type of remote - one that is paired to Hubitat.

Or a different wife????

Just stumbled across this - thanks so much @simon for this - I have 2 Fakro rooflights installed and will be able to test this out in a month or so!!!

1 Like

Download the Hubitat app