/** * Tasmota - Virtual Air Conditioner * * Copyright 2020 AwfullySmart.com - HongTat Tan * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import groovy.json.JsonOutput String driverVersion() { return "20201204" } metadata { definition (name: "Tasmota Virtual Air Conditioner", namespace: "hongtat", author: "HongTat Tan", ocfDeviceType: "oic.d.airconditioner", vid: "ff0a15cd-13dd-32c2-b51c-780bb3aad494", mnmn: "SmartThingsCommunity") { capability "Actuator" capability "Temperature Measurement" capability "Thermostat Cooling Setpoint" capability "Thermostat Mode" capability "voicehouse43588.airConditionerFanSpeed" capability "voicehouse43588.airConditionerVerticalSwing" capability "voicehouse43588.airConditionerHorizontalSwing" capability "Switch" capability "Health Check" capability "Configuration" capability "Refresh" capability "Sensor" attribute "lastSeen", "string" } preferences { section { input(title: "Device Settings", description: "To view/update this settings, go to the Tasmota (Connect) SmartApp and select this device.", displayDuringSetup: false, type: "paragraph", element: "paragraph") input(title: "", description: "Tasmota Virtual Air Conditioner v${driverVersion()}", displayDuringSetup: false, type: "paragraph", element: "paragraph") } } tiles(scale: 2) { multiAttributeTile(name:"thermostat", type:"general", width:6, height:4, canChangeIcon: false) { tileAttribute("device.switch", key: "PRIMARY_CONTROL") { attributeState("off", label: "Off", action: "switch.on", icon: "st.thermostat.heating-cooling-off", backgroundColor: "#ffffff") attributeState("on", label: "On", action: "switch.off", backgroundColor: "#00a0dc") } tileAttribute("device.temperature", key: "SECONDARY_CONTROL") { attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal") } tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { attributeState("default", label: '${currentValue}', unit: "°", defaultState: true) } } controlTile("coolingSetpoint", "device.coolingSetpoint", "slider", sliderType: "COOLING", debouncePeriod: 750, range: "device.coolingSetpointRange", width: 2, height: 2) { state "default", action:"setCoolingSetpoint", label:'${currentValue}', backgroundColor: "#55D4ED" } controlTile("thermostatMode", "device.thermostatMode", "enum", width: 2 , height: 2, supportedStates: "device.supportedThermostatModes") { state("off", action: "setThermostatMode", label: 'Off', icon: "st.thermostat.heating-cooling-off") state("cool", action: "setThermostatMode", label: 'Cool', icon: "st.thermostat.cool") state("heat", action: "setThermostatMode", label: 'Heat', icon: "st.thermostat.heat") state("auto", action: "setThermostatMode", label: 'Auto', icon: "st.tesla.tesla-hvac") } controlTile("fanSpeed", "device.fanSpeed", "enum", width: 2 , height: 2, supportedStates: "device.supportedFanSpeed") { state("auto", action: "setFanSpeed", label: 'Auto', icon: "st.thermostat.fan-on") state("1", action: "setFanSpeed", label: 'Min', icon: "st.thermostat.fan-on") state("2", action: "setFanSpeed", label: 'Low', icon: "st.thermostat.fan-on") state("3", action: "setFanSpeed", label: 'Medium', icon: "st.thermostat.fan-on") state("4", action: "setFanSpeed", label: 'High', icon: "st.thermostat.fan-on") state("5", action: "setFanSpeed", label: 'Max', icon: "st.thermostat.fan-on") } controlTile("swingV", "device.swingV", "enum", width: 3 , height: 1, supportedStates: "device.supportedAirConditionerSwingV") { state("auto", action: "setSwingV", label: 'Auto') state("off", action: "setSwingV", label: 'Off') state("min", action: "setSwingV", label: 'Min') state("low", action: "setSwingV", label: 'Low') state("mid", action: "setSwingV", label: 'Mid') state("high", action: "setSwingV", label: 'High') state("max", action: "setSwingV", label: 'Max') } valueTile("spacerV", "spacer", decoration: "flat", inactiveLabel: false, width: 3, height: 1) { state "default", label:'Swing-V' } controlTile("swingH", "device.swingH", "enum", width: 3 , height: 1, supportedStates: "device.supportedAirConditionerSwingH") { state("auto", action: "setSwingH", label: 'Auto') state("off", action: "setSwingH", label: 'Off') state("left max", action: "setSwingH", label: 'Left Max') state("left", action: "setSwingH", label: 'Left') state("mid", action: "setSwingH", label: 'Mid') state("right", action: "setSwingH", label: 'Right') state("right max", action: "setSwingH", label: 'Right Max') state("wide", action: "setSwingH", label: 'Wide') } valueTile("spacerH", "spacer", decoration: "flat", inactiveLabel: false, width: 3, height: 1) { state "default", label: 'Swing-H' } main "thermostat" details(["thermostat", "thermostatMode", "fanSpeed", "coolingSetpoint", "swingV", "swingH", "spacerV", "spacerH"]) } } def parse(description) { } def parseEvents(status, json) { def events = [] if (status as Integer == 200) { // Contact sensor that senses if the AC is On or Off if (json?.contactSensor) { String cs = json?.contactSensor Map modeMap = [name: "thermostatMode", data:[supportedThermostatModes: supportedThermostatModes.encodeAsJson()]] Map switchMap = [name: "switch"] if (cs in ["open", "closed"]) { if (cs == "open") { switchMap.value = "on" if (state?.lastMode) { modeMap.value = state.lastMode } else { modeMap.value = "cool" } modeMap.displayed = true } else { switchMap.value = "off" modeMap.value = "off" modeMap.displayed = true } events << sendEvent(switchMap) events << sendEvent(modeMap) } } // Bridge's Last seen if (json?.lastSeen) { events << sendEvent(name: "switch", value: device.currentValue("switch")) events << sendEvent(name: "lastSeen", value: json?.lastSeen, displayed: false) } } return events } def ping() { // Intentionally left blank as parent should handle this } def installed() { state?.lastMode = "cool" sendEvent(name: "checkInterval", value: 30 * 60 + 2 * 60, displayed: false, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID]) sendEvent(name: "supportedThermostatModes", value: JsonOutput.toJson(supportedThermostatModes), displayed: false) sendEvent(name: "supportedFanSpeed", value: JsonOutput.toJson(supportedFanSpeed), displayed: false) sendEvent(name: "supportedAirConditionerSwingV", value: JsonOutput.toJson(supportedAirConditionerSwingV), displayed: false) sendEvent(name: "supportedAirConditionerSwingH", value: JsonOutput.toJson(supportedAirConditionerSwingH), displayed: false) sendEvent(name: "coolingSetpointRange", value: thermostatSetpointRange, displayed: false) sendEvent(name: "switch", value: "off", displayed: false) sendEvent(name: "fanSpeed", value: supportedFanSpeed[0], displayed: false) sendEvent(name: "coolingSetpoint", value: thermostatSetpointRange[0], unit: getTemperatureScale(), displayed: false) sendEvent(name: "temperature", value: thermostatSetpointRange[0], unit: getTemperatureScale(), displayed: false) sendEvent(name: "thermostatMode", value: "off", data:[supportedThermostatModes: JsonOutput.toJson(supportedThermostatModes)], displayed: false) sendEvent(name: "swingV", value: "auto", displayed: false) sendEvent(name: "swingH", value: "auto", displayed: false) } def updated() { initialize() } def initialize() { } def setFanSpeed(String speed) { def fanSpeed = [name: "fanSpeed"] if (supportedFanSpeed.contains(speed as String)) { fanSpeed.value = speed as String sendEvent(fanSpeed) done() } else { log.debug "Unsupported fan speed: '${speed}'" } } def on() { if (state?.lastMode) { setThermostatMode(state?.lastMode) } else { setThermostatMode("cool") } } def off() { setThermostatMode("off") } def cool() { setThermostatMode("cool") } def heat() { setThermostatMode("heat") } def auto() { setThermostatMode("auto") } def setThermostatMode(String mode) { def modeMap = [name: "thermostatMode", data:[supportedThermostatModes: supportedThermostatModes.encodeAsJson()]] def switchMap = [name: "switch"] if (supportedThermostatModes.contains(mode)) { switch (mode) { case "off": modeMap.value = "off" modeMap.displayed = true switchMap.value = "off" break default: modeMap.value = mode modeMap.displayed = true switchMap.value = "on" state?.lastMode = mode break } sendEvent(modeMap) sendEvent(switchMap) done() } else { log.debug "Unsupported AC mode: '${mode}'" } } def setCoolingSetpoint(setpoint) { if (setpoint < thermostatSetpointRange[0]) { setpoint = thermostatSetpointRange[0] } else if (setpoint > thermostatSetpointRange[1]) { setpoint = thermostatSetpointRange[1] } sendEvent(name: "coolingSetpoint", value: setpoint, unit: getTemperatureScale(), displayed: false) sendEvent(name: "temperature", value: setpoint, unit: getTemperatureScale()) done() } def setSwingV(verticalSwing) { if (supportedAirConditionerSwingV.contains(verticalSwing)) { sendEvent(name: "swingV", value: verticalSwing) done() } else { log.debug "Unsupported vertical swing mode: '${verticalSwing}'" } } def setSwingH(horizontalSwing) { if (supportedAirConditionerSwingH.contains(horizontalSwing)) { sendEvent(name: "swingH", value: horizontalSwing) done() } else { log.debug "Unsupported horizontal swing mode: '${horizontalSwing}'" } } def done() { def bridge = parent.childSetting(device.id, "bridge") ?: null def vendor = parent.childSetting(device.id, "hvac") ?: null if (bridge && vendor) { def command = [:] def power = device.currentState("switch").value command.Vendor = vendor command.Power = power command.Mode = (power == "off") ? state.lastMode : device.currentState("thermostatMode").value command.FanSpeed = device.currentState("fanSpeed").value command.Temp = device.currentState("coolingSetpoint").value command.SwingV = device.currentState("swingV").value command.SwingH = device.currentState("swingH").value if (getTemperatureScale() == "F") { command.Celsius = 'Off' } if (power == "off") { log.debug "Command 1: " + JsonOutput.toJson(command) parent.callTasmota(bridge, 'IRhvac ' + JsonOutput.toJson(command)) command.Mode = "off" log.debug "Command 2: " + JsonOutput.toJson(command) parent.callTasmota(bridge, 'IRhvac ' + JsonOutput.toJson(command)) } else { log.debug "Command: " + JsonOutput.toJson(command) parent.callTasmota(bridge, 'IRhvac ' + JsonOutput.toJson(command)) } } else { log.debug "Error: Please specify an IR bridge and air conditioner brand" } } def getThermostatSetpointRange() { (getTemperatureScale() == "C") ? [16, 31] : [60, 87] } def getSupportedThermostatModes() { ["off", "cool", "heat", "auto"] } def getSupportedFanSpeed() { ["auto", "1", "2", "3", "4", "5"] } def getSupportedAirConditionerSwingV() { ["auto", "off", "min", "low", "mid", "high", "max"] } def getSupportedAirConditionerSwingH() { ["auto", "off", "left max", "left", "mid", "right", "right max", "wide"] }