/* * * Hive Receiver Driver * */ @Field String driverVersion = "v0.70 (6th September 2023)" @Field boolean debugMode = false #include BirdsLikeWires.library import groovy.transform.Field @Field String deviceName = "Hive Receiver" @Field int reportIntervalMinutes = 1 @Field int checkEveryMinutes = 4 metadata { definition (name: "$deviceName", namespace: "BirdsLikeWires", author: "Andrew Davison", importUrl: "https://raw.githubusercontent.com/birdslikewires/hubitat/master/hive/drivers/hive_receiver.groovy") { capability "Configuration" attribute "healthStatus", "enum", ["offline", "online"] if (debugMode) { command "testCommand" } fingerprint profileId: "0104", inClusters: "0000,0003,0009,000A,0201,FD00", outClusters: "000A,0402,0019", manufacturer: "Computime", model: "SLR1", deviceJoinName: "Computime Boiler Controller SLR1" fingerprint profileId: "0104", inClusters: "0000,0003,0009,000A,0201,FD00", outClusters: "000A,0402,0019", manufacturer: "Computime", model: "SLR2", deviceJoinName: "Computime Boiler Controller SLR2" } } preferences { input name: "infoLogging", type: "bool", title: "Enable logging", defaultValue: true input name: "debugLogging", type: "bool", title: "Enable debug logging", defaultValue: false input name: "traceLogging", type: "bool", title: "Enable trace logging", defaultValue: false } void testCommand(int childEndpoint) { logging("${device} : Test Command", "info") } void configureSpecifics() { // Called by general configure() method String modelCheck = "${getDeviceDataByName('model')}" device.name = "$deviceName $modelCheck" setThermostatDateAndTime(5) // Reporting // These had to be constructed manually as configureReporting seems to ignore the [destEndpoint:0x06] additional parameter. // NOTE! Though the water (endpoint 0x06) bind is reported as successful no reports are ever sent. The endpoint needs to be polled. :( // Attributes 1C, 23, 24 and 29 all apply to endpoint 6. Temperature setpoint (12) does not apply (set on the boiler). sendZigbeeCommands([ "zdo bind 0x${device.deviceNetworkId} 0x05 0x01 0x0201 {${device.zigbeeId}} {}, delay 2000", "he cr 0x${device.deviceNetworkId} 0x05 0x0201 0x0012 0x29 1 43200 {} {}, delay 2000", // (0x0201, 0x0012) OccupiedHeatingSetpoint "zdo bind 0x${device.deviceNetworkId} 0x05 0x01 0x0201 {${device.zigbeeId}} {}, delay 2000", "he cr 0x${device.deviceNetworkId} 0x05 0x0201 0x001C 0x30 1 43200 {} {}, delay 2000", // (0x0201, 0x001C) SystemMode "zdo bind 0x${device.deviceNetworkId} 0x05 0x01 0x0201 {${device.zigbeeId}} {}, delay 2000", "he cr 0x${device.deviceNetworkId} 0x05 0x0201 0x0023 0x30 1 43200 {} {}, delay 2000", // (0x0201, 0x0023) TemperatureSetpointHold "zdo bind 0x${device.deviceNetworkId} 0x05 0x01 0x0201 {${device.zigbeeId}} {}, delay 2000", "he cr 0x${device.deviceNetworkId} 0x05 0x0201 0x0024 0x21 1 43200 {} {}, delay 2000", // (0x0201, 0x0024) TemperatureSetpointHoldDuration "zdo bind 0x${device.deviceNetworkId} 0x05 0x01 0x0201 {${device.zigbeeId}} {}, delay 2000", "he cr 0x${device.deviceNetworkId} 0x05 0x0201 0x0029 0x19 1 43200 {} {}, delay 2000" // (0x0201, 0x0029) ThermostatRunningState ]) sendZigbeeCommands(zigbee.configureReporting(0x0201, 0x0000, 0x29, 1, 60)) // (0x0201, 0x0000) Temperature Reporting } void updateSpecifics() { // Called by library updated() method return } void auto(int childEndpoint) { // Schedule mode. This will run any existing schedule set through the thermostat. ArrayList cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0x04, [destEndpoint: childEndpoint]) // SystemMode cmds += zigbee.writeAttribute(0x0201, 0x0023, 0x30, 0x00, [destEndpoint: childEndpoint]) // TemperatureSetpointHold sendZigbeeCommands(cmds) logging("${device} : System set to schedule mode.", "info") } void cool(int childEndpoint) { // Being a heat-only system we can only take this to be a request to actively reach a temperature by the only means we have. logging("${device} : Request to cool received, but this system does not support active cooling.", "warn") heat(childEndpoint) } void setCoolingSetpoint(int childEndpoint, BigDecimal temperature) { // Being a heat-only system the cooling setpoint can only ever be the same as the heating setpoint. logging("${device} : Cooling setpoint requested, but this system does not support active cooling.", "warn") setHeatingSetpoint(childEndpoint, temperature) } void emergencyHeat(int childEndpoint) { // Boost mode. int boostTime = 30 int boostTemp = 2400 ArrayList cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0x05, [destEndpoint: childEndpoint], 0) // SystemMode cmds += zigbee.writeAttribute(0x0201, 0x0023, 0x30, 0x01, [destEndpoint: childEndpoint], 0) // TemperatureSetpointHold cmds += zigbee.writeAttribute(0x0201, 0x0024, 0x21, boostTime, [destEndpoint: childEndpoint], 0) // TemperatureSetpointHoldDuration cmds += zigbee.writeAttribute(0x0201, 0x0012, 0x29, boostTemp, [destEndpoint: childEndpoint], 0) // OccupiedHeatingSetpoint sendZigbeeCommands(cmds) logging("${device} : System boosting to ${boostTemp} for ${boostTime} minutes.", "info") } void fanAuto(int childEndpoint) { // No controllable fans here. logging("${device} : No controllable fans.", "warn") return } void fanCirculate(int childEndpoint) { // No controllable fans here either. logging("${device} : No controllable fans.", "warn") return } void fanOn(int childEndpoint) { // Still no controllable fans, stop asking. logging("${device} : No controllable fans.", "warn") return } void heat(int childEndpoint) { // Manual mode. ArrayList cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0x04, [destEndpoint: childEndpoint]) // SystemMode cmds += zigbee.writeAttribute(0x0201, 0x0023, 0x30, 0x01, [destEndpoint: childEndpoint]) // TemperatureSetpointHold cmds += zigbee.writeAttribute(0x0201, 0x0024, 0x21, 0x00, [destEndpoint: childEndpoint]) // TemperatureSetpointHoldDuration sendZigbeeCommands(cmds) } void setHeatingSetpoint(int childEndpoint, BigDecimal temperature) { // Convert from degF. if ("${location.temperatureScale}" == "F") temperature = (temperature / 1.8) - 32 (temperature < 5) ? temperature = 1 : temperature // Anything lower than 5degC is frost protect mode. (temperature > 32) ? temperature = 32 : temperature // Anything higher than 32degC is not supported. // System works in 0.5degC steps. temperature = temperature * 2 temperature = temperature.setScale(0, BigDecimal.ROUND_HALF_UP) temperature = temperature / 2 temperature = temperature.setScale(2, BigDecimal.ROUND_UP) * 100 int temperatureInt = temperature.toInteger() logging("${device} : setHeatingSetpoint : sanitised temperature input to ${temperatureInt}", "debug") sendZigbeeCommands(zigbee.writeAttribute(0x0201, 0x0012, 0x29, temperatureInt, [destEndpoint: childEndpoint])) } void setThermostatMode(int childEndpoint, String thermostatMode) { logging("${device} : setThermostatMode : ${thermostatMode} ", "debug") switch(thermostatMode) { case "auto": auto(childEndpoint) break case "off": off(childEndpoint) break case "heat": setThermostatModeWithSafety(childEndpoint, thermostatMode) break case "emergency heat": emergencyHeat(childEndpoint) break case "cool": setThermostatModeWithSafety(childEndpoint, thermostatMode) break } } void setThermostatModeWithSafety(int childEndpoint, String thermostatMode) { def currentOverrideMinutes = fetchChildStates("overrideMinutes","${childEndpoint}") def currentThermostatMode = fetchChildStates("thermostatMode","${childEndpoint}") if (currentOverrideMinutes[0] != "0" || currentThermostatMode[0] == "auto") { logging("${device} : We're running a schedule. Command 'heat' or 'cool' directly to switch to manual.", "warn") } else { switch(thermostatMode) { case "heat": heat(childEndpoint) break case "cool": cool(childEndpoint) break } } } void setThermostatFanMode(int childEndpoint, String fanMode) { switch(fanMode) { case "auto": fanAuto(childEndpoint) break case "circulate": fanCirculate(childEndpoint) break case "on": fanOn(childEndpoint) break } } void setThermostatDateAndTime(int childEndpoint) { // Zigbee epoch is measured from 1st January 2000 so we need to subtract 30 years worth of seconds from UNIX time! int zigbeeEpochTime = now()/1000-946684800 String zigbeeHexTime = zigbee.convertToHexString(zigbeeEpochTime,8) String zigbeeHexTimeReversed = new StringBuilder(8) .append(zigbeeHexTime, 6, 8) .append(zigbeeHexTime, 4, 6) .append(zigbeeHexTime, 2, 4) .append(zigbeeHexTime, 0, 2) .toString(); logging("${device} : zigbeeEpochTime = $zigbeeEpochTime | zigbeeHexTime = $zigbeeHexTime | zigbeeHexTimeReversed = $zigbeeHexTimeReversed", "debug") sendZigbeeCommands(["he wattr 0x${device.deviceNetworkId} 0x000${childEndpoint} 0x000A 0x0000 0x00E2 {$zigbeeHexTimeReversed}"]) } void off(int childEndpoint) { // Turns everything off, but respects the frost protect setting. ArrayList cmds = [] cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0x0000, [destEndpoint: childEndpoint]) // SystemMode to off cmds += zigbee.writeAttribute(0x0201, 0x0023, 0x30, 0x0001, [destEndpoint: childEndpoint]) // TemperatureSetpointHold cmds += zigbee.writeAttribute(0x0201, 0x0024, 0x21, 0xFFFF, [destEndpoint: childEndpoint]) // TemperatureSetpointHoldDuration sendZigbeeCommands(cmds) //runIn(3,getSystemMode) } void getOccupiedHeatingSetpoint(int childEndpoint) { sendZigbeeCommands(zigbee.readAttribute(0x0201, 0x0012, [destEndpoint: childEndpoint])) } void getSystemMode(int childEndpoint) { sendZigbeeCommands(zigbee.readAttribute(0x0201, 0x001C, [destEndpoint: childEndpoint])) } void getTemperatureSetpointHold(int childEndpoint) { sendZigbeeCommands(zigbee.readAttribute(0x0201, 0x0023, [destEndpoint: childEndpoint])) } void getTemperatureSetpointHoldDuration(int childEndpoint) { sendZigbeeCommands(zigbee.readAttribute(0x0201, 0x0024, [destEndpoint: childEndpoint])) } void getThermostatRunningState(int childEndpoint) { sendZigbeeCommands(zigbee.readAttribute(0x0201, 0x0029, [destEndpoint: childEndpoint])) } void parse(String description) { updateHealthStatus() checkDriver() logging("${device} : parse() : $description", "trace") Map descriptionMap = zigbee.parseDescriptionAsMap(description) if (descriptionMap) { def child if ("${descriptionMap.endpoint}" == "06") { child = fetchChild("BirdsLikeWires","Hive Receiver Water","${descriptionMap.endpoint}") } else { child = fetchChild("BirdsLikeWires","Hive Receiver Heating","${descriptionMap.endpoint}") } try { child.processMap(descriptionMap) } catch (Exception e) { // Slice-and-dice the string we receive. descriptionMap = description.split(', ').collectEntries { entry -> def pair = entry.split(': ') [(pair.first()): pair.last()] } try { child.processMap(descriptionMap) } catch (Exception ee) { reportToDev(descriptionMap) } } } else { reportToDev(descriptionMap) } }