Has someone integrated a Shelly TRV?

whoops didnt upload it yet... its very very beta right now which is why I probably didnt upload it yet and wanted feedback on it.

/**
 *
 *  Shelly TRV Driver
 *
 *  Copyright © 2018-2019 Scott Grayban
 *  Copyright © 2020 Allterco Robotics US
 *
 *  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.
 *
 * Hubitat is the Trademark and intellectual Property of Hubitat Inc.
 * Shelly is the Trademark and Intellectual Property of Allterco Robotics Ltd
 *
 *-------------------------------------------------------------------------------------------------------------------
 *
 *  1.0.0 - Initial code
 *
 */

import groovy.json.*
import groovy.transform.Field
def setVersion(){
	state.Version = "1.0.0"
	state.InternalName = "ShellyTRV"
}

metadata {
	definition (
		name: "Shelly TRV",
		namespace: "ShellyUSA",
		author: "Scott Grayban",
                importUrl: "https://raw.githubusercontent.com/ShellyUSA/Hubitat-Drivers/master/ShellyTRV.groovy"
		)
	{
        capability "Actuator"
        capability "Sensor"
        capability "Refresh"
        capability "Polling"
        capability "SignalStrength"
        capability "TemperatureMeasurement"
        capability "Valve"
        capability "Battery"
        
        attribute "FW_Update_Needed", "string"
        attribute "LastRefresh", "string"
        attribute "internal_tempC", "number"
        attribute "internal_tempF", "number"
        attribute "DeviceOverTemp", "string"
        attribute "MAC", "string"
        attribute "RelayChannel", "number"
        attribute "Primary_IP", "string"
        attribute "Primary_SSID", "string"
        attribute "Secondary_IP", "string"
        attribute "Secondary_SSID", "string"
        attribute "WiFiSignal", "string"
        attribute "Cloud", "string"
        attribute "Cloud_Connected", "string"
        attribute "energy", "number"
        attribute "DeviceType", "string"
        attribute "DeviceName", "string"
        attribute "NTPServer", "string"
        attribute "Position", "number"
        attribute "voltage", "number"
        
        command "RebootDevice"
        command "UpdateDeviceFW" // ota?update=1
        //command "updatecheck" // Only used for development
        command "getSettings"
        command "TargetTemp" , ["temp"]
	}

	preferences {
	def refreshRate = [:]
		refreshRate << ["1 min" : "Refresh every minute"]
		refreshRate << ["5 min" : "Refresh every 5 minutes"]
		refreshRate << ["15 min" : "Refresh every 15 minutes"]
		refreshRate << ["30 min" : "Refresh every 30 minutes"]
		refreshRate << ["manual" : "Manually or Polling Only"]

	input("ip", "string", title:"IP", description:"Shelly IP Address", defaultValue:"" , required: true)
	input name: "username", type: "text", title: "Username:", description: "(blank if none)", required: false
	input name: "password", type: "password", title: "Password:", description: "(blank if none)", required: false

	if (channel < 1) input name: "ntp_server", type: "text", title: "NTP time server:", description: "E.G. time.google.com or 192.168.0.59", defaultValue: "time.google.com", required: true

    // Only show for channel 0 since the device name is for the entire device
	if (channel < 1) input name: "devicename", type: "text", title: "Give your device a name:", description: "EG; Location/Room<br>NO SPACES in name", required: false
        
    input("refresh_Rate", "enum", title: "Device Refresh Rate", description:"<font color=red>!!WARNING!!</font><br>DO NOT USE if you have over 50 Shelly devices.", options: refreshRate, defaultValue: "manual")
        input "locale", "enum", title: "Choose refresh date format", required: true, defaultValue: true, options: [US:"US MM/DD/YYYY",UK:"UK DD/MM/YYYY"]

    input name: "debugOutput", type: "bool", title: "Enable debug logging?", defaultValue: true
	input name: "debugParse", type: "bool", title: "Enable JSON parse logging?", defaultValue: true
	input name: "txtEnable", type: "bool", title: "Enable descriptionText logging", defaultValue: true
	input name: "Shellyinfo", type: "text", title: "<center><font color=blue>Info Box</font><br>Shelly API docs are located</center>", 
        description: "<center><br><a href='http://shelly-api-docs.shelly.cloud/' title='shelly-api-docs.shelly.cloud' target='_blank'>[here]</a></center>"
	}
}

def initialize() {
	log.info "initialize"
	if (txtEnable) log.info "initialize"
}

def installed() {
    log.debug "Installed"
    state.DeviceName = "NotSet"
    state.RelayName = "NotSet"
}

def uninstalled() {
    unschedule()
    log.debug "Uninstalled"
}

def updated() {
    if (txtEnable) log.info "Preferences updated..."
    log.warn "Debug logging is: ${debugOutput == true}"
    log.warn "Switch protection is: ${settings?.protect}"
    unschedule()
    dbCleanUp()
    
    if (ip != null) { // Don't set until IP is saved
    if (!(getDataValue("model") in ["SHSW-1","SHEM"])) {
        sendSwitchCommand "/settings/relay/${channel}?max_power=${maxpower}"
    }
    if (getDataValue("model") in ["SHPLG-1","SHPLG-S","SHPLG-U1"]) {
        logDebug "LED setting ${led_status} and ${led_power}"
        sendSwitchCommand "/settings?led_status_disable=${led_status}"
        sendSwitchCommand "/settings?led_power_disable=${led_power}"
    }
    if (channel < 1) sendSwitchCommand "/settings?sntp_server=${ntp_server}"
// Set device and relay name
    if (channel < 1) sendSwitchCommand "/settings?name=${devicename}"
    if (!(getDataValue("model") in ["SHPLG-S","SHPLG-1","SHPLG-U1"])) sendSwitchCommand "/settings/relay/${channel}?name=${relayname}"

    if (getDataValue("model") == "SHEM") sendSwitchCommand "/settings/relay/${channel}?ctraf_type=${ctraf_type}"
    }
    
    switch(refresh_Rate) {
		case "1 min" :
			runEvery1Minute(autorefresh)
			break
		case "5 min" :
			runEvery5Minutes(autorefresh)
			break
		case "15 min" :
			runEvery15Minutes(autorefresh)
			break
		case "30 min" :
			runEvery30Minutes(autorefresh)
			break
		case "manual" :
			unschedule(autorefresh)
            log.info "Autorefresh disabled"
            break
	}
	if (txtEnable) log.info ("Auto Refresh set for every ${refresh_Rate} minute(s).")

    if (debugOutput) runIn(1800,logsOff) //Off in 30 minutes
    if (debugParse) runIn(300,logsOff) //Off in 5 minutes
    
    state.LastRefresh = new Date().format("YYYY/MM/dd \n HH:mm:ss", location.timeZone)

    version()
    refresh()
    getSettings()
}

private dbCleanUp() {
	state.remove("version")
	state.remove("Version")
	state.remove("ShellyfwUpdate")
	state.remove("power")
	state.remove("overpower")
	state.remove("dcpower")
	state.remove("max_power")
	state.remove("internal_tempC")
	state.remove("Status")
    state.remove("max_power")
    state.remove("RelayName")
    state.remove("RelayChannel")
    state.remove("powerSource")
}

def refresh(){
    if (ip != null) { // Don't set until IP is saved
    logDebug "Shelly Status called"
    getSettings()
    def params = [uri: "http://${username}:${password}@${ip}/status"]

try {
    httpGet(params) {
        resp -> resp.headers.each {
        logJSON "Response: ${it.name} : ${it.value}"
    }
        obs = resp.data
        logJSON "params: ${params}"
        logJSON "response contentType: ${resp.contentType}"
	    logJSON "response data: ${resp.data}"

        if (obs.temperature != null) sendEvent(name: "internal_tempC", value: obs.temperature)
        if (obs.tmp != null) {
            sendEvent(name: "internal_tempC", unit: "C", value: obs.tmp.tC)
            sendEvent(name: "internal_tempF", unit: "F", value: obs.tmp.tF)
        }
        if (obs.overtemperature != null) sendEvent(name: "DeviceOverTemp", value: obs.overtemperature)
        
        if (obs.wifi_sta != null) {
        state.rssi = obs.wifi_sta.rssi
        state.ssid = obs.wifi_sta.ssid
        state.ip = obs.wifi_sta.ip
        sendEvent(name: "Primary_SSID", value: state.ssid)
        sendEvent(name: "Primary_IP", value: state.ip)
        }
        
        sendEvent(name: "Charging", value: obs.charger)
        sendEvent(name: "calibrated", value: obs.calibrated)
        sendEvent(name: "voltage", value: obs.bat.voltage)
        sendEvent(name: "battery", unit: "%", value: obs.bat.value)
        
/*
-30 dBm	Excellent | -67 dBm	Good | -70 dBm	Poor | -80 dBm	Weak | -90 dBm	Dead
*/
        signal = state.rssi
        if (signal <= 0 && signal >= -70) {
            sendEvent(name:  "WiFiSignal", value: "<font color='green'>Excellent</font>", isStateChange: true);
        } else
        if (signal < -70 && signal >= -80) {
            sendEvent(name:  "WiFiSignal", value: "<font color='green'>Good</font>", isStateChange: true);
        } else
        if (signal < -80 && signal >= -90) {
            sendEvent(name: "WiFiSignal", value: "<font color='yellow'>Poor</font>", isStateChange: true);
        } else 
        if (signal < -90 && signal >= -100) {
            sendEvent(name: "WiFiSignal", value: "<font color='red'>Weak</font>", isStateChange: true);
        }

        state.mac = obs.mac
        sendEvent(name: "MAC", value: state.mac)
        sendEvent(name: "rssi", value: state.rssi)
        
// Device FW Updates
        state.has_update = obs.has_update
        if (state.has_update == true) {
            if (txtEnable) log.info "sendEvent NEW SHELLY FIRMWARE"
            sendEvent(name: "FW_Update_Needed", value: "<font color='red'>FIRMWARE Update Required</font>")
        }
        
        if (state.has_update == false) {
            if (txtEnable) log.info "sendEvent Device FW is current"
            sendEvent(name: "FW_Update_Needed", value: "<font color='green'>Device FW is current</font>")
        }

// Cloud
        state.cloud = obs.cloud.enabled
        if (state.cloud == true) {
            sendEvent(name: "Cloud", value: "<font color='green'>Enabled</font>")
        } else {
            sendEvent(name: "Cloud", value: "<font color='red'>Disabled</font>")
        }
        
        state.cloudConnected = obs.cloud.connected
        if (state.cloudConnected == true) {
            sendEvent(name: "Cloud_Connected", value: "<font color='green'>Connected</font>")
        } else {
            sendEvent(name: "Cloud_Connected", value: "<font color='red'>Not Connected</font>")
        }
        
// Relays
        if (obs.relays != null) {
        if (channel ==0) ison = obs.relays.ison[0]
        if (channel ==1) ison = obs.relays.ison[1]
        if (channel ==2) ison = obs.relays.ison[2]
        if (channel ==3) ison = obs.relays.ison[3]
        if (ison == true) {
            sendEvent(name: "switch", value: "on")
        } else {
            sendEvent(name: "switch", value: "off")
        }
    }
} // End try
       } catch (e) {
           log.error "something went wrong: $e"
       }
    } // End if !==ip      
} // End Refresh Status


// Get shelly device type
def getSettings(){
    if (ip != null) { // Don't set until IP is saved
    getThermostatsSettings()
    logDebug "Get Shelly Settings"
    def paramsSettings = [uri: "http://${username}:${password}@${ip}/settings"]

try {
    httpGet(paramsSettings) {
        respSettings -> respSettings.headers.each {
        logJSON "ResponseSettings: ${it.name} : ${it.value}"
    }
        obsSettings = respSettings.data

        logJSON "params: ${paramsSettings}"
        logJSON "response contentType: ${respSettings.contentType}"
	    logJSON "response data: ${respSettings.data}"

        state.DeviceType = obsSettings.device.type
        if (state.DeviceType == "SHSW-1") sendEvent(name: "DeviceType", value: "Shelly 1")
        if (state.DeviceType == "SHSW-PM") sendEvent(name: "DeviceType", value: "Shelly 1PM")
        if (state.DeviceType == "SHSW-21") sendEvent(name: "DeviceType", value: "Shelly 2")
        if (state.DeviceType == "SHSW-25") sendEvent(name: "DeviceType", value: "Shelly 2.5")
        if (state.DeviceType == "SHSW-44") sendEvent(name: "DeviceType", value: "Shelly 4Pro")
        if (state.DeviceType == "SHEM") sendEvent(name: "DeviceType", value: "Shelly EM")
        if (state.DeviceType == "SHEM-3") sendEvent(name: "DeviceType", value: "Shelly EM3")
        if (state.DeviceType == "SHPLG-1") sendEvent(name: "DeviceType", value: "Shelly Plug")
        if (state.DeviceType == "SHPLG-S") sendEvent(name: "DeviceType", value: "Shelly PlugS")
        if (state.DeviceType == "SHPLG-U1") sendEvent(name: "DeviceType", value: "Shelly Plug US")
        if (state.DeviceType == "SHTRV-01") sendEvent(name: "DeviceType", value: "Shelly TRV")


        state.ShellyHostname = obsSettings.device.hostname
        state.sntp_server = obsSettings.sntp.server
        sendEvent(name: "NTPServer", value: state.sntp_server)

        if (obsSettings.led_status_disable != null) {
            if (obsSettings.led_status_disable == false) {
                sendEvent(name: "LED_NetworkStatus", value: "<font color='green'>Enabled</font>")
            } else {
                sendEvent(name: "LED_NetworkStatus", value: "<font color='red'>Disabled</font>")
            }
            if (obsSettings.led_power_disable == false) {
                sendEvent(name: "LED_Output", value: "<font color='green'>Enabled</font>")
            } else {
                sendEvent(name: "LED_Output", value: "<font color='red'>Disabled</font>")
            }
        }

//Get Device name
       if (obsSettings.name != "NotSet") {
           state.DeviceName = obsSettings.name
           sendEvent(name: "DeviceName", value: state.DeviceName)
           updateDataValue("DeviceName", state.DeviceName)
           if (txtEnable) log.info "DeviceName is ${obsSettings.name}"
       } else if (obsSettings.name != null) {
           state.DeviceName = "NotSet"
           sendEvent(name: "DeviceName", value: state.DeviceName)
           if (txtEnable) log.info "DeviceName is ${obsSettings.name}"
       }
//Get Relay name
        if (getDataValue("model") != "SHPLG-S" && getDataValue("model") != "SHPLG-1" && getDataValue("model") != "SHPLG-U1" && getDataValue("model") != "SHEM") {
            if (obsSettings.relays != null) {
                if (channel == 0) relay_name = obsSettings.relays.name[0]
                if (channel == 1) relay_name = obsSettings.relays.name[1]
                if (channel == 2) relay_name = obsSettings.relays.name[2]
                if (channel == 3) relay_name = obsSettings.relays.name[3]
                if (relay_name != null) {
                    state.RelayName = relay_name
                    sendEvent(name: "RelayName", value: state.RelayName)
                    if (txtEnable) log.info "RelayName is ${relay_name}"
                } else {
                    state.RelayName = "NotSet"
                    sendEvent(name: "RelayName", value: state.RelayName)
                    if (txtEnable) log.info "RelayName is ${relay_name}"
                }
                updateDataValue("RelayName", state.RelayName)
            }
        } // The Plug devices do not offer a relay name

        if (getDataValue("model") == "SHEM") {
            state.RelayName = obsSettings.relays.name[0]
            sendEvent(name: "RelayName", value: state.RelayName)
            updateDataValue("RelayName", state.RelayName)
        }
        
        if (obsSettings.wifi_sta1 != null) {
        state.rssi = obsSettings.wifi_sta1.rssi
        state.Secondary_ssid = obsSettings.wifi_sta1.ssid
        state.Secondary_IP = obsSettings.wifi_sta1.ip
            if (obsSettings.wifi_sta1.enabled == true) sendEvent(name: "Secondary_SSID", value: state.Secondary_ssid)
            if (state.Secondary_IP != null) sendEvent(name: "Secondary_IP", value: state.Secondary_IP)
        }
       
        logDebug "updating data values"
        updateDataValue("model", state.DeviceType)
        updateDataValue("ShellyHostname", state.ShellyHostname)
        updateDataValue("ShellyIP", state.ip)
        updateDataValue("ShellySSID", obsSettings.wifi_sta.ssid)
        updateDataValue("manufacturer", "Allterco Robotics")
        updateDataValue("MAC", state.mac)
        updateDataValue("DeviceName", state.DeviceName)

} // End try
       } catch (e) {
           log.error "something went wrong: $e"
       }
    } // End if !==ip      
} // End getSettings Status


def getThermostatsSettings(){
    if (ip != null) { // Don't set until IP is saved
    logDebug "Shelly Thermostats Settings called"
    def params = [uri: "http://${username}:${password}@${ip}/thermostats/0"]

try {
    httpGet(params) {
        resp -> resp.headers.each {
        logJSON "Response: ${it.name} : ${it.value}"
    }
        obs = resp.data
        logJSON "params: ${params}"
        logJSON "response contentType: ${resp.contentType}"
	    logJSON "response data: ${resp.data}"

        sendEvent(name: "Position", value: obs.pos)
        if (obs.pos >= 1) {
            sendEvent(name: "valve", value: "open", isStateChange: true)
        } else
            sendEvent(name: "valve", value: "closed", isStateChange: true)
        sendEvent(name: "temperature", value: obs.tmp.value)

} // End try
       } catch (e) {
           log.error "something went wrong: $e"
       }
    } // End if !==ip      
} // End thermostat Status

def open() {
        logDebug "Executing Open"
        sendSwitchCommand "/thermostats/0?pos=100"
        sendEvent(name: "valve", value: "open")
}

def close() {
        logDebug "Executing Close"
        sendSwitchCommand "/thermostats/0?pos=0"
        sendEvent(name: "valve", value: "closed")
}

def TargetTemp(temp) {
        logDebug "Executing Target Temp"
        sendSwitchCommand "/settings/thermostats/0?target_t=${temp}"
}

def ping() {
	logDebug "ping"
	poll()
}

def logsOff(){
	log.warn "debug logging auto disabled..."
	device.updateSetting("debugOutput",[value:"false",type:"bool"])
	device.updateSetting("debugParse",[value:"false",type:"bool"])
}

def autorefresh() {
    if (locale == "UK") {
	logDebug "Get last UK Date DD/MM/YYYY"
	state.LastRefresh = new Date().format("d/MM/YYYY \n HH:mm:ss", location.timeZone)
	sendEvent(name: "LastRefresh", value: state.LastRefresh, descriptionText: "Last refresh performed")
	} 
	if (locale == "US") {
	logDebug "Get last US Date MM/DD/YYYY"
	state.LastRefresh = new Date().format("MM/d/YYYY \n HH:mm:ss", location.timeZone)
	sendEvent(name: "LastRefresh", value: state.LastRefresh, descriptionText: "Last refresh performed")
	}
	if (txtEnable) log.info "Executing 'auto refresh'" //RK

    refresh()
}

private logJSON(msg) {
	if (settings?.debugParse || settings?.debugParse == null) {
	log.info "$msg"
	}
}

private logDebug(msg) {
	if (settings?.debugOutput || settings?.debugOutput == null) {
	log.debug "$msg"
	}
}

// handle commands
//RK Updated to include last refreshed
def poll() {
	if (locale == "UK") {
	logDebug "Get last UK Date DD/MM/YYYY"
	state.LastRefresh = new Date().format("d/MM/YYYY \n HH:mm:ss", location.timeZone)
	sendEvent(name: "LastRefresh", value: state.LastRefresh, descriptionText: "Last refresh performed")
	} 
	if (locale == "US") {
	logDebug "Get last US Date MM/DD/YYYY"
	state.LastRefresh = new Date().format("MM/d/YYYY \n HH:mm:ss", location.timeZone)
	sendEvent(name: "LastRefresh", value: state.LastRefresh, descriptionText: "Last refresh performed")
	}
	if (txtEnable) log.info "Executing 'poll'" //RK
	refresh()
}

def sendSwitchCommand(action) {
    if (txtEnable) log.info "Calling ${action}"
    def params = [uri: "http://${username}:${password}@${ip}/${action}"]
try {
    httpPost(params) {
        resp -> resp.headers.each {
        logDebug "Response: ${it.name} : ${it.value}"
    }
} // End try
        
} catch (e) {
        log.error "something went wrong: $e"
    }
    runIn(2, refresh)
}

def RebootDevice() {
    if (txtEnable) log.info "Rebooting Device"
    def params = [uri: "http://${username}:${password}@${ip}/reboot"]
try {
    httpPost(params) {
        resp -> resp.headers.each {
        logDebug "Response: ${it.name} : ${it.value}"
    }
} // End try
        
} catch (e) {
        log.error "something went wrong: $e"
    }
    runIn(15,refresh)
}

def UpdateDeviceFW() {
    if (txtEnable) log.info "Updating Device FW"
    def params = [uri: "http://${username}:${password}@${ip}/ota?update=1"]
try {
    httpPost(params) {
        resp -> resp.headers.each {
        logDebug "Response: ${it.name} : ${it.value}"
    }
} // End try
        
} catch (e) {
        log.error "something went wrong: $e"
    }
    runIn(30,refresh)
}

// Check Version   ***** with great thanks and acknowlegment to Cobra (github CobraVmax) for his original code **************
def version(){
	updatecheck()
	schedule("0 0 18 1/1 * ? *", updatecheck) // Cron schedule
//	schedule("0 0/1 * 1/1 * ? *", updatecheck) // Test Cron schedule
}

def updatecheck(){
	setVersion()
    def paramsUD = [uri: "https://raw.githubusercontent.com/ShellyUSA/Hubitat-Drivers/master/resources/version.json", contentType: "application/json; charset=utf-8"]
    try {
        httpGet(paramsUD) { respUD ->
            if (debugParse) log.debug " Version Checking - Response Data: ${respUD.data}"
            def copyrightRead = (respUD.data.copyright)
            state.Copyright = copyrightRead
            def newVerRaw = (respUD.data.versions.Driver.(state.InternalName))
            def newVer = (respUD.data.versions.Driver.(state.InternalName).replace(".", ""))
            def currentVer = state.Version.replace(".", "")
            state.UpdateInfo = (respUD.data.versions.UpdateInfo.Driver.(state.InternalName))
            state.author = (respUD.data.author)
            state.icon = (respUD.data.icon)
            if(newVer == "NLS"){
                state.DriverStatus = "<b>** This driver is no longer supported by $state.author  **</b>"
                log.warn "** This driver is no longer supported by $state.author **"
            } else
                if(newVer == "BETA"){
                state.Status = "<b>** THIS IS BETA CODE  **</b>"
                log.warn "** BETA CODE **"
            } else
                if(currentVer < newVer){
                state.DriverStatus = "<b>New Version Available (Version: $newVerRaw)</b>"
                log.warn "** There is a newer version of this driver available  (Version: $newVerRaw) **"
                log.warn "** $state.UpdateInfo **"
            } else
                if(currentVer > newVer){
                state.DriverStatus = "<b>You are using a Test version of this Driver (Version: $state.Version)</b>"
            } else {
                state.DriverStatus = "Current"
                log.info "You are using the current version of this driver"
            }
        } // httpGet
    } // try

    catch (e) {
        log.error "Something went wrong: CHECK THE JSON FILE AND IT'S URI -  $e"
    }
    if(state.DriverStatus == "Current"){
        state.UpdateInfo = "Up to date"
        sendEvent(name: "DriverUpdate", value: state.UpdateInfo)
        sendEvent(name: "DriverStatus", value: state.DriverStatus)
    } else {
        sendEvent(name: "DriverUpdate", value: state.UpdateInfo)
        sendEvent(name: "DriverStatus", value: state.DriverStatus)
    }

    sendEvent(name: "DriverAuthor", value: "sgrayban")
    sendEvent(name: "DriverVersion", value: state.Version)
}
3 Likes