/** * * Shelly PLUS 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 * *------------------------------------------------------------------------------------------------------------------- * * See all the Shelly Products at https://shelly.cloud/ * * Changes: * 1.0.2 - Change driver name to Shelly Plus * - Added Eco Mode status * 1.0.1 - Added temperature scale set from local hub * - WHOOPS on/off code was completely wrong * 1.0.0 - Initial port * */ import groovy.json.* import groovy.transform.Field def setVersion(){ state.Version = "1.0.2" state.InternalName = "ShellyPlus" } metadata { definition ( name: "Shelly Plus", namespace: "ShellyUSA", author: "Scott Grayban", importUrl: "https://raw.githubusercontent.com/ShellyUSA/Hubitat-Drivers/master/PLUS/ShellyPlus.groovy" ) { capability "Actuator" capability "Sensor" capability "Refresh" capability "Switch" capability "RelaySwitch" capability "Polling" capability "PowerMeter" capability "EnergyMeter" capability "SignalStrength" capability "TemperatureMeasurement" capability "VoltageMeasurement" attribute "StableFW_Update", "string" attribute "BetaFW_Update", "string" attribute "LastRefresh", "string" attribute "power", "number" attribute "overpower", "string" 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 "eMeter", "number" attribute "reactive", "number" attribute "MaxPower", "number" attribute "CircuitAmp", "string" attribute "LED_Output", "string" attribute "LED_NetworkStatus", "string" attribute "DeviceName", "string" attribute "RelayName", "string" attribute "Eco_Mode", "string" command "RebootDevice" command "UpdateDeviceFW" //command "CheckForUpdate" // Only used for development //command "getSettings" } 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 input("channel", "number", title:"Switch", description:"0,1,2,or 3 :", defaultValue:"0" , required: true) input("refresh_Rate", "enum", title: "Device Refresh Rate", description:"!!WARNING!!
DO NOT USE if you have over 50 Shelly devices.", options: refreshRate, defaultValue: "manual") input "protect", "enum", title:"Prevent accidental off/on", defaultValue: true, options: [Yes:"Yes",No:"No"], required: true 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: "
Info Box
Shelly API docs are located
", description: "

[here]
" } } 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() 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() } private dbCleanUp() { state.remove("version") state.remove("Version") state.remove("ver") state.remove("id") 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("powerSource") state.remove("has_update") } def refresh(){ logDebug "Shelly Refresh called" getWiFi() getDeviceInfo() getGetConfig() CheckForUpdate() def params = [uri: "http://${username}:${password}@${ip}/rpc/Switch.GetStatus?id=${channel}"] 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}" state.RelayChannel = channel sendEvent(name: "RelayChannel", value: state.RelayChannel) sendEvent(name: "voltage", value: obs.voltage) if (obs.temperature.tC != null){ if (state.temp_scale == "C") sendEvent(name: "temperature", value: obs.temperature.tC) if (state.temp_scale == "F") sendEvent(name: "temperature", value: obs.temperature.tF) } ison = obs.output if (ison == true) { sendEvent(name: "switch", value: "on") } else { sendEvent(name: "switch", value: "off") } // Power Meters power = obs.apower sendEvent(name: "power", unit: "W", value: power) // Power Totals // powerTotal = obs.meters.total // sendEvent(name: "energy", unit: "W", value: powerTotal) // Over Power // overpower = obs.relays.overpower // sendEvent(name: "overpower", value: overpower) state.temp_scale = location.getTemperatureScale() updateDataValue("DeviceName", state.DeviceName) updateDataValue("RelayName", state.RelayName) updateDataValue("ShellyIP", state.ip) updateDataValue("ShellySSID", state.ssid) updateDataValue("manufacturer", "Allterco Robotics") updateDataValue("MAC", state.mac) // updateDataValue("DeviceName", state.DeviceName) } // End try } catch (e) { log.error "something went wrong: $e" } } // End Refresh Status def getWiFi(){ logDebug "WiFi Status called" def params = [uri: "http://${username}:${password}@${ip}/rpc/wifi.GetStatus"] 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}" state.rssi = obs.rssi state.ssid = obs.ssid state.ip = obs.sta_ip sendEvent(name: "Primary_SSID", value: state.ssid) sendEvent(name: "Primary_IP", value: state.ip) /* -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: "Excellent", isStateChange: true); } else if (signal < -70 && signal >= -80) { sendEvent(name: "WiFiSignal", value: "Good", isStateChange: true); } else if (signal < -80 && signal >= -90) { sendEvent(name: "WiFiSignal", value: "Poor", isStateChange: true); } else if (signal < -90 && signal >= -100) { sendEvent(name: "WiFiSignal", value: "Weak", isStateChange: true); } sendEvent(name: "rssi", value: state.rssi) } // End try } catch (e) { log.error "something went wrong: $e" } } // End Wifi Status def getDeviceInfo(){ logDebug "Sys Status called" //getSettings() def params = [uri: "http://${username}:${password}@${ip}/rpc/Shelly.GetDeviceInfo"] 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}" state.mac = obs.mac updateDataValue("FW Version", obs.ver) updateDataValue("model", obs.model) updateDataValue("ShellyHostname", obs.id) updateDataValue("Device Type", obs.app) } // End try } catch (e) { log.error "something went wrong: $e" } } // End Device Info def getGetConfig(){ logDebug "Sys Status called" //getSettings() def params = [uri: "http://${username}:${password}@${ip}/rpc/Sys.GetConfig"] 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: "Eco_Mode", value: obs.device.eco_mode) } // End try } catch (e) { log.error "something went wrong: $e" } } // End sys get config def CheckForUpdate() { if (txtEnable) log.info "Check Device FW" def params = [uri: "http://${username}:${password}@${ip}/rpc/Shelly.CheckForUpdate"] try { httpGet(params) { resp -> resp.headers.each { logDebug "Response: ${it.name} : ${it.value}" } obs = resp.data response = "${obs.toString()}" logJSON "params: ${params}" logJSON "response contentType: ${resp.contentType}" logJSON "response data: ${resp.data}" if(response.contains("stable")) { sendEvent(name: "StableFW_Update", value: "Available", isStateChange: true); }else if(!(response.contains("stable"))) { sendEvent(name: "StableFW_Update", value: "Current", isStateChange: true); } if(response.contains("beta")) { sendEvent(name: "BetaFW_Update", value: "Available", isStateChange: true); }else if(!(response.contains("beta"))) { sendEvent(name: "BetaFW_Update", value: "Current", isStateChange: true); } } // End try } catch (e) { log.error "something went wrong: $e" } } //switch.on def on() { if (protect == "No") { logDebug "Executing switch.on" sendSwitchCommand "/rpc/Switch.Set?id=${channel}&on=true" } if (protect == "Yes") { sendEvent(name: "switch", value: "LOCKED") runIn(1, refresh) } } //switch.off def off() { if (protect == "No") { logDebug "Executing switch.off" sendSwitchCommand "/rpc/Switch.Set?id=${channel}&on=false" } if (protect == "Yes") { sendEvent(name: "switch", value: "LOCKED") runIn(1, refresh) } } 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 { httpGet(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}/rpc/Shelly.Reboot"] try { httpGet(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}/rpc/Shelly.Update?stage=stable"] try { httpGet(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 = "** This driver is no longer supported by $state.author **" log.warn "** This driver is no longer supported by $state.author **" } else if(newVer == "BETA"){ state.Status = "** THIS IS BETA CODE **" log.warn "** BETA CODE **" } else if(currentVer < newVer){ state.DriverStatus = "New Version Available (Version: $newVerRaw)" log.warn "** There is a newer version of this driver available (Version: $newVerRaw) **" log.warn "** $state.UpdateInfo **" } else if(currentVer > newVer){ state.DriverStatus = "You are using a Test version of this Driver (Version: $state.Version)" } 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) }