So nobody wants to test the driver now? It will open and close and also set the target temp IF scheduling is enabled.
I ordered one .
awesome... will have you do some testing and see what useful commands should be added except scheduling since doing it in the webui or the shelly app is better/faster.
I use a standard 2 wire actuator.
Controlled with konnected & nodemcu. Works ace.
Got the TRV installed. Can I get a copy of the driver?
Sorry but I’m not seeing the trv driver in that GitHub repo.
Edit: oh is it a gen2 device that uses the single Shelly-plus driver?
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)
}
Never used a Shelly device before (and my radiators are off since it’s summer), but it seems to be working fine.
The driver connected to the device no issue. I don’t have a password setup on it (yet). I can open/close the valve, and can set the temperature. There’s a “position” that runs 1-100 I think, it can be manually stepped in the device’s built-in webpage by 10% increments.
I saw that just haven't added that function yet... figured it was summer for most of us here and wasn't needed yet
Lol, not needed at all in my use case, so take your time.
But now at least I know these things should work and I can order a few more come winter.
Thanks for pushing a new driver out so quickly!
Have you seen the new shelly Plus H&T ?
I haven’t had much reason to look at Shelly products before, the TRV was my first.
The temp and humidity sensor looks nice. I appreciate the display to be able to check quickly the current conditions in a room.
It’s also nice to see there’s at least one device manufacturer that makes it pretty simple to locally connect to their devices on a LAN, even though they also have a cloud-based functionality.
And cloud is NOT required.... that's a good thing!
And the driver for the new shelly Plus H&T is already done.
Hi.
Just bought a couple of the Shelly TRVs - Slightly tortuous connection (compared to Z-Wave.....), but seems to be working nicely, even updated the firmware Thanks!
Will the TRV driver code make its way into the repository at some point?
Cheers.
And..... Whilst in the 'device edit' state, I can see all the nice thermostat related things, elsewhere, they just look like either valves or temperature sensors......
Quick prod in this topic - is there any ongoing support for the Shelly TRVs working properly as thermostats...?
Thanks!
OK, my next Spirit is gone out of life. Can I finally conclude that the Shelly TRV didn't make his way to Hubitat and that it's not a choice ?
My Shelly TRV has been working fine with Scott’s driver code posted above.
No, you can't.
My Shelly TRV works like a charm.