Porting DTH for Fibaro Heat Contoller

I am still having trouble porting over this device to habitat.

@Patrick please can the support team help me understand the layout, capabilities and definitions that need to go into a Hubitat device handler. I can't find any documentation anywhere (other than the rather unclear post (App and driver porting to Hubitat) that @csteele pointed me to a few months ago.

I am trying to port this device:
https://github.com/timowen001gh/Fibaro-HeatController-Valve

and got this far:

  • Fibaro Heat Controller FGT-001
  • Branch from Tomáš Mrázek 2018 original file to port radiator valve to Hubitat.
  • 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:
  • Apache License, Version 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: "Fibaro Heat Controller", namespace: "Tomas-Mrazek", author: "Tomáš Mrázek") {
fingerprint mfr: "010F", prod: "1301", model: "1000"

capability "Battery"
capability "Thermostat"
capability "Valve"
capability "Thermostat Setpoint"
capability "Refresh"
capability "Polling"

attribute "externalSensorConnected", "string"
attribute "openWindowDetected", "string"
attribute "batterySensor", "number"

command "setThermostatSetpointUp"
command "setThermostatSetpointDown"

}

standardTile("off", "device.valve", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
  state "thermostatMode", action: "thermostatMode.off", icon: "st.vents.vent-closed"
}


standardTile("heat", "device.thermostatMode", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
  state "thermostatMode", action: "thermostatMode.heat", icon: "st.vents.vent-open-text"
}

valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
  state "battery", label: 'valve battery\n${currentValue}%', unit: "%"
}

valueTile("batterySensor", "device.batterySensor", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
  state "batterySensor", label: 'sensor battery\n${currentValue}%', unit: "%"
}

standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) {
  state "refresh", label: 'refresh', action: "refresh.refresh", icon: "st.secondary.refresh-icon"
}

}

preferences {
input name: "overrideScheduleDuration", title: "Override Schedule duration (minutes between 10 and 10000)", description: "", type: "number", range: "10..10000", defaultValue: "240", required: true
input name: "openWindowDetector", title: "Open Window Detector", description: "", type: "bool", defaultValue: true
input name: "fastOpenWindowDetector", title: "Fast Open Window Detector", description: "", type: "bool", defaultValue: false
input name: "increaseRecieverSensitivity", title: "Increase Receiver Sensitivity (shortens battery life)", description: "shortens battery life", type: "bool", defaultValue: false
input name: "ledWhenRemoteControll", title: "LED Indications When Controlling Remotely", description: "", type: "bool", defaultValue: false
input name: "protectManualOnOff", title: "Protect from setting Full ON and Full OFF mode by turning the knob manually", description: "", type: "bool", defaultValue: false
}

/**

  • SMARTTHINGS UX EVENTS

*/

def off() {
setThermostatMode("off")
}

def auto() {
setThermostatMode("auto")
}

def heat() {
setThermostatMode("heat")
}

def setThermostatMode(String mode) {
sendEvent(name: "thermostatMode", value: mode, isStateChange: true)
switch (mode) {
case "auto":
encapsulate(zwave.thermostatModeV2.thermostatModeSet(mode: 1), 1)
break
case "heat":
encapsulate(zwave.thermostatModeV2.thermostatModeSet(mode: 31), 1)
break
case "off":
encapsulate(zwave.thermostatModeV2.thermostatModeSet(mode: 0), 1)
break
}
}

def setThermostatSetpointUp() {
def setpoint = device.latestValue("thermostatSetpoint")
if (setpoint < 24) {
setpoint = setpoint + 1
}
setThermostatSetpoint(setpoint)
}

def setThermostatSetpointDown() {
def setpoint = device.latestValue("thermostatSetpoint")
if (setpoint > 16) {
setpoint = setpoint - 1
}
setThermostatSetpoint(setpoint)
}

def setThermostatSetpoint(setpoint) {
sendEvent(name: "thermostatSetpoint", unit: "C", value: setpoint.setScale(0, BigDecimal.ROUND_DOWN), isStateChange: true)
encapsulate(zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: 0, scaledValue: setpoint, setpointType: 1, size: 2]), 1)
}

def updated() {
if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return
def paramsString = [settings.openWindowDetector, settings.fastOpenWindowDetector, settings.increaseRecieverSensitivity, settings.ledWhenRemoteControll, settings.protectManualOnOff]
def params = paramsString.collect({(it == true) ? 1 : 0})
def cmds =
cmds << [cmd: zwave.configurationV1.configurationSet(parameterNumber: 1, scaledConfigurationValue: settings.overrideScheduleDuration)]
cmds << [cmd: zwave.configurationV1.configurationSet(parameterNumber: 2, scaledConfigurationValue: getIntegerFromParams(params))]
for (int i = 1; i <= 2; i++) {
cmds << [cmd: zwave.configurationV1.configurationGet(parameterNumber: i)]
}
state.lastUpdated = now()
response(encapsulateSequence(cmds, 2000))
}

def refresh() {
log.debug "refresh()"
def cmds =
cmds << [cmd: zwave.batteryV1.batteryGet(), endpoint: 1]
cmds << [cmd: zwave.batteryV1.batteryGet(), endpoint: 2]
cmds << [cmd: zwave.thermostatModeV2.thermostatModeGet(), endpoint: 1]
cmds << [cmd: zwave.sensorMultilevelV5.sensorMultilevelGet(), endpoint: 2]
cmds << [cmd: zwave.configurationV1.configurationGet(parameterNumber: 3)]
encapsulateSequence(cmds, 2000)
}

def poll() {
log.debug "poll()"
def cmds =
cmds << [cmd: zwave.batteryV1.batteryGet(), endpoint: 1]
cmds << [cmd: zwave.batteryV1.batteryGet(), endpoint: 2]
cmds << [cmd: zwave.sensorMultilevelV5.sensorMultilevelGet(), endpoint: 2]
encapsulateSequence(cmds, 2000)
}

private encapsulate(hubitat.zwave.Command cmd, endpoint = null) {
if (zwaveInfo.zw.contains("s")) {
if (endpoint) {
secureEncapsulate(multichannelEncapsulate(cmd, endpoint)).format()
} else {
secureEncapsulate(cmd).format()
}
} else {
log.warn "${device.displayName} - no encapsulation supported for command: ${cmd}"
cmd.format()
}
}

private encapsulateSequence(cmds, delay) {
def commands = cmds.collect{[it.get('cmd'), it.get('endpoint')]}
delayBetween(commands.collect{encapsulate(it)}, delay)
}

private secureEncapsulate(hubitat.zwave.Command cmd) {
//log.trace "${device.displayName} - encapsulating command using Secure Encapsulation, command: ${cmd}"
zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd)
}

private multichannelEncapsulate(hubitat.zwave.Command cmd, endpoint) {
//log.trace "${device.displayName} - encapsulating command using Multi Channel Encapsulation, command: ${cmd}"
zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd)
}

/**

  • Z-WAVE DEVICE EVENTS

*/

def parse(String description) {
//log.debug "PARSE – description – ${description}"
def result = null
def cmd = zwave.parse(description)
if (cmd) {
result = zwaveEvent(cmd)
} else {
log.warn "${device.displayName} - non-parsed event: ${description}"
}
return result
}

def zwaveEvent(hubitat.zwave.Command cmd) {
log.info "${device.displayName} - unhandled parsed event without result ${cmd}"
return result
}

def zwaveEvent(hubitat.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions())
if (encapsulatedCommand) {
//log.debug "${device.displayName} - parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}"
zwaveEvent(encapsulatedCommand)
} else {
//log.warn "${device.displayName} – unable to extract secure command from $cmd"
createEvent(descriptionText: cmd.toString())
}
}

def zwaveEvent(hubitat.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions())
if (encapsulatedCommand) {
//log.debug "${device.displayName} - parsed MultiChannelCmdEncap into: ${encapsulatedCommand}"
zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint)
} else {
//log.warn "${device.displayName} – unable to extract multi channel command from $cmd"
createEvent(descriptionText: cmd.toString())
}
}

def zwaveEvent(hubitat.zwave.commands.basicv1.BasicReport cmd, sourceEndPoint = null) {
def result = createEvent(descriptionText: "${device.displayName}: ${cmd}")
log.info "${device.displayName} - parsed event ${cmd} into: ${result}"
return result
}

def zwaveEvent(hubitat.zwave.commands.batteryv1.BatteryReport cmd, sourceEndPoint = null) {
def result
switch(sourceEndPoint) {
case 1:
result = createEvent([name: "battery", unit: "%", value: cmd.batteryLevel])
break
case 2:
result = createEvent([name: "batterySensor", unit: "%", value: cmd.batteryLevel])
break
default:
result = createEvent([name: "battery", unit: "%", value: cmd.batteryLevel])
break
}
log.info "${device.displayName} - parsed event ${cmd} into: ${result}"
return result
}

def zwaveEvent(hubitat.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd, sourceEndPoint = null) {
def result = createEvent([name: "thermostatSetpoint", unit: "C", value: cmd.scaledValue.setScale(0, BigDecimal.ROUND_DOWN)])
log.info "${device.displayName} - parsed event ${cmd} into: ${result}"
return result
}

def zwaveEvent(hubitat.zwave.commands.thermostatmodev2.ThermostatModeReport cmd, sourceEndPoint = null) {
def result
switch (cmd.mode) {
case 1:
result = createEvent([name: "thermostatMode", value: "auto"])
break
case 31:
result = createEvent([name: "thermostatMode", value: "heat"])
break
case 0:
result = createEvent([name: "thermostatMode", value: "off"])
break
}
log.info "${device.displayName} - parsed event ${cmd} into: ${result}"
return result
}

def zwaveEvent(hubitat.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd, sourceEndPoint = null) {
def result = createEvent([name: "temperature", unit: "C", value: cmd.scaledSensorValue])
log.info "${device.displayName} - parsed event ${cmd} into: ${result}"
return result
}

def zwaveEvent(hubitat.zwave.commands.configurationv1.ConfigurationReport cmd, sourceEndPoint = null) {
switch (cmd.parameterNumber) {
case 1:
settings.overrideScheduleDuration = cmd.scaledConfigurationValue
break
case 2:
def params = getParamsFromInteger(cmd.scaledConfigurationValue, 5)
settings.openWindowDetector = params.get(0)
settings.fastOpenWindowDetector = params.get(1)
settings.increaseRecieverSensitivity = params.get(2)
settings.ledWhenRemoteControll = params.get(3)
settings.protectManualOnOff = params.get(4)
break
case 3:
def params = getParamsFromInteger(cmd.scaledConfigurationValue, 2)
def result1 = createEvent([name: "externalSensorConnected", value: (params.get(0) == 1) ? "true" : "false"])
def result2 = createEvent([name: "openWindowDetected", value: (params.get(1) == 1) ? "true" : "false"])
log.info "${device.displayName} - parsed event ${cmd} into: ${result1} | ${result2}"
return [result1, result2]
}
log.info "${device.displayName} - parsed event without result ${cmd}"
}

/**

  • UTILS

*/

private Map cmdVersions() {
[0x80: 1, 0x40: 2, 0x43: 2, 0x31: 5, 0x70: 1]
}

private getParamsFromInteger(decimal, numberOfParams) {
def bit = Math.pow(new Double(2), new Double(numberOfParams - 1))
def params =
for(int i = 0; i < numberOfParams; i++) {
if (decimal >= bit) {
params << 1
decimal = decimal - bit
bit = bit / 2
} else {
params << 0
if (numberOfParams != i) {
bit = bit / 2
}
}
}
params = params.reverse()
return params
}

private getIntegerFromParams(params) {
def bit = 1
def decimal = 0
for(int i = 0; i < params.size(); i++) {
decimal = decimal + params.get(i) * bit
bit = bit * 2
}
return decimal
}

The driver displays correctly on the device page, however I cannot send command to the device.

Thank you in advance for your time.

Tim

1 Like