Skip to content

Commit f59aaee

Browse files
authored
Merge pull request #55 from ShellyUSA/DanielWinks
Daniel winks
2 parents e0a4fc5 + 52e5c6e commit f59aaee

File tree

3 files changed

+180
-30
lines changed

3 files changed

+180
-30
lines changed

PackageManifests/ShellyWebhookDrivers/packageManifest.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"packageName": "Shelly Webhook/Websocket Drivers",
33
"author": "ShellyUSA, Daniel Winks",
4-
"version": "2.2.3",
5-
"releaseNotes": "2.2.3 Fixes for inputs on Gen 1 device not properly being created/updated\r\nAdd Shelly Pro Dual Cover PM\r\n Add Shelly Vintage Bulb, minor fixes for Dimmer\r\n Add Shelly Dimmer 1&2 support\r\nAdd Shelly 2.5 (Gen1). Fix missing cover functionality.\r\n2.0.5 Fix overflow issue on setting parent energy totals.\r\nFix null issue on setting parent energy totals\r\n2.0.3 Fix for cover child devices not getting coverId properly from device data\r\n2.0.3 Add drivers for Pro2PM, Pro3, Uni gen1 and gen2 to HPM. \r\n2.0.1 Change logging level on a few things to keep logs from warning about Shelly auth related messages. \r\n2.0.0 Added support for many more devices. Major rewrite of library code. 🥳\r\n1.2.0 Added Shelly Flood, Shelly PM Mini Gen3\r\n1.1.0 Added support for Bluetooth Gateway, Blu devices (button, motion, door/window), H&T (gen 1)\r\n1.0.0 Release of Shelly Plug US, Button 1, Motion 2, Shelly H&T (gen 2 and 3), and Shelly Gas",
4+
"version": "2.3.0",
5+
"releaseNotes": "2.3.0 Add Shelly TRV\r\nFixes for inputs on Gen 1 device not properly being created/updated\r\nAdd Shelly Pro Dual Cover PM\r\n Add Shelly Vintage Bulb, minor fixes for Dimmer\r\n Add Shelly Dimmer 1&2 support\r\nAdd Shelly 2.5 (Gen1). Fix missing cover functionality.\r\n2.0.5 Fix overflow issue on setting parent energy totals.\r\nFix null issue on setting parent energy totals\r\n2.0.3 Fix for cover child devices not getting coverId properly from device data\r\n2.0.3 Add drivers for Pro2PM, Pro3, Uni gen1 and gen2 to HPM. \r\n2.0.1 Change logging level on a few things to keep logs from warning about Shelly auth related messages. \r\n2.0.0 Added support for many more devices. Major rewrite of library code. 🥳\r\n1.2.0 Added Shelly Flood, Shelly PM Mini Gen3\r\n1.1.0 Added support for Bluetooth Gateway, Blu devices (button, motion, door/window), H&T (gen 1)\r\n1.0.0 Release of Shelly Plug US, Button 1, Motion 2, Shelly H&T (gen 2 and 3), and Shelly Gas",
66
"minimumHEVersion": "2.3.7.114",
77
"dateReleased": "2024-02-10",
88
"licenseFile": "https://raw.githubusercontent.com/ShellyUSA/Hubitat-Drivers/master/LICENSE",
@@ -261,6 +261,13 @@
261261
"location": "https://raw.githubusercontent.com/ShellyUSA/Hubitat-Drivers/master/WebhookWebsocket/ShellyProDualCoverPM.groovy",
262262
"required": false
263263
},
264+
{
265+
"id": "7de01dd9-9df2-4bc3-b91d-7b6b4a4bc0bd",
266+
"name": "Shelly TRV",
267+
"namespace": "ShellyUSA",
268+
"location": "https://raw.githubusercontent.com/ShellyUSA/Hubitat-Drivers/master/WebhookWebsocket/ShellyTRV.groovy",
269+
"required": false
270+
},
264271
{
265272
"id": "6e2d0794-b72e-483f-8c39-6d5c8fed6540",
266273
"name": "Shelly Uni Gen 1",

ShellyDriverLibrary/ShellyUSA.ShellyUSA_Driver_Library.groovy

Lines changed: 139 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ List<String> getActionsToCreate() {
5959
if(hasActionsToCreateList() == true) { return ACTIONS_TO_CREATE }
6060
else {return []}
6161
}
62+
Boolean hasActionsToCreateEnabledTimesList() { return ACTIONS_TO_CREATE_ENABLED_TIMES != null }
63+
List<String> getActionsToCreateEnabledTimes() {
64+
if(hasActionsToCreateEnabledTimesList() == true) { return ACTIONS_TO_CREATE_ENABLED_TIMES }
65+
else {return []}
66+
}
6267

6368
Boolean deviceIsComponent() {return COMP == true}
6469
Boolean deviceIsComponentInputSwitch() {return INPUTSWITCH == true}
@@ -70,6 +75,7 @@ Boolean hasCapabilitySwitch() { return device.hasCapability('Switch') == true }
7075
Boolean hasCapabilityPresence() { return device.hasCapability('PresenceSensor') == true }
7176
Boolean hasCapabilityValve() { return device.hasCapability('Valve') == true }
7277
Boolean hasCapabilityCover() { return device.hasCapability('WindowShade') == true }
78+
Boolean hasCapabilityThermostatHeatingSetpoint() { return device.hasCapability('ThermostatHeatingSetpoint') == true }
7379
Boolean hasCapabilityCoverOrCoverChild() { return device.hasCapability('WindowShade') == true || getCoverChildren()?.size() > 0 }
7480

7581
@CompileStatic
@@ -153,6 +159,13 @@ if (device != null) {
153159
if(hasCapabilityCover() == true) {
154160
input(name: 'cover_invert_status', type:'bool', title: 'Invert open/closed state. If enabled open=0, 100=closed.', required: true, defaultValue: false)
155161
}
162+
if(hasCapabilityThermostatHeatingSetpoint() == true) {
163+
input(name: 'trv_temperature_offset', type:'number', title: 'Temperature offset', required: true, defaultValue: 0)
164+
input(name: 'trv_target_t_auto', type:'bool', title: 'Automatic temperature control', required: true, defaultValue: true)
165+
input(name: 'trv_ext_t_enabled', type:'bool', title: 'If temperature correction from external temp sensor is enabled', required: true, defaultValue: true)
166+
input(name: 'trv_display_brightness', type:'enum', title: 'Display brightness', required: true, options: [1:'1', 2:'2', 3:'3', 4:'4', 5:'5', 6:'6', 7:'7'])
167+
input(name: 'trv_temp_units', type:'enum', title: 'Temperature Units', required: true, options: ['C':'Celcius', 'F':'Fahrenheit'])
168+
}
156169

157170
input(name: 'logEnable', type: 'bool', title: 'Enable Logging', required: false, defaultValue: true)
158171
input(name: 'debugLogEnable', type: 'bool', title: 'Enable debug logging', required: false, defaultValue: true)
@@ -412,8 +425,11 @@ void getPreferencesFromShellyDevice() {
412425
void getPreferencesFromShellyDeviceGen1() {
413426
LinkedHashMap gen1SettingsResponse = (LinkedHashMap)sendGen1Command('settings')
414427
logTrace("Gen 1 Settings ${prettyJson(gen1SettingsResponse)}")
428+
// Get preferences from Shelly
415429
LinkedHashMap prefs = [:]
416430
LinkedHashMap motion = (LinkedHashMap)gen1SettingsResponse?.motion
431+
LinkedHashMap display = (LinkedHashMap)gen1SettingsResponse?.display
432+
List<LinkedHashMap> thermostats = (List<LinkedHashMap>)gen1SettingsResponse?.thermostats
417433
if(motion != null) {
418434
prefs['gen1_motion_sensitivity'] = motion?.sensitivity as Integer
419435
prefs['gen1_motion_blind_time_minutes'] = motion?.blind_time_minutes as Integer
@@ -422,6 +438,25 @@ void getPreferencesFromShellyDeviceGen1() {
422438
prefs['gen1_tamper_sensitivity'] = gen1SettingsResponse?.tamper_sensitivity as Integer
423439
}
424440
if(gen1SettingsResponse?.set_volume != null) {prefs['gen1_set_volume'] = gen1SettingsResponse?.set_volume as Integer}
441+
if(display != null) {
442+
if(display?.brightness != null) {prefs['trv_display_brightness'] = display?.brightness as Integer}
443+
}
444+
if(thermostats != null) {
445+
thermostats.eachWithIndex{ tstat, index ->
446+
LinkedHashMap target_t = (LinkedHashMap)tstat?.target_t
447+
if(target_t?.enabled != null) {prefs['trv_target_t_auto'] = target_t?.enabled as Boolean}
448+
if(target_t?.units != null) {prefs['trv_temp_units'] = target_t?.units}
449+
450+
LinkedHashMap ext_t = (LinkedHashMap)tstat?.ext_t
451+
if(ext_t?.enabled != null) {prefs['trv_ext_t_enabled'] = ext_t?.enabled as Boolean}
452+
453+
if(tstat?.temperature_offset != null) {prefs['trv_temperature_offset'] = tstat?.temperature_offset as BigDecimal}
454+
}
455+
}
456+
457+
if(prefs.size() > 0) { setHubitatDevicePreferences(prefs) }
458+
459+
425460

426461
//Process ADC(s)
427462
List adcs = (List)gen1SettingsResponse?.adcs
@@ -524,7 +559,6 @@ void getPreferencesFromShellyDeviceGen1() {
524559
}
525560
}
526561

527-
if(prefs.size() > 0) { setHubitatDevicePreferences(prefs) }
528562
refresh()
529563
}
530564

@@ -695,6 +729,24 @@ void sendPreferencesToShellyDevice() {
695729
sendGen1Command('settings', queryString)
696730
}
697731

732+
if(getDeviceSettings().trv_temperature_offset != null) {
733+
String queryString = "temperature_offset=${getDeviceSettings().trv_temperature_offset}".toString()
734+
sendGen1Command('settings', queryString)
735+
}
736+
if(getDeviceSettings().trv_target_t_auto != null) {
737+
String queryString = "target_t_enabled=${getDeviceSettings().trv_target_t_auto == true ? 1 : 0}".toString()
738+
sendGen1Command('settings/thermostat/0', queryString)
739+
}
740+
if(getDeviceSettings().trv_ext_t_enabled != null) {
741+
String queryString = "ext_t_enabled=${getDeviceSettings().trv_ext_t_enabled == true ? 1 : 0}".toString()
742+
sendGen1Command('settings/thermostat/0', queryString)
743+
}
744+
if(getDeviceSettings().trv_temp_units != null) {
745+
String u = "${getDeviceSettings().trv_temp_units}".toString()[0]
746+
String queryString = "temperature_unit=${u}".toString()
747+
sendGen1Command('settings', queryString)
748+
}
749+
698750
runInSeconds('parentGetPreferencesFromShellyDevice', 6)
699751
}
700752
/* #endregion */
@@ -1018,14 +1070,6 @@ void setGasPPM(Integer ppm) {
10181070
sendDeviceEvent([name: 'ppm', value: ppm])
10191071
}
10201072

1021-
@CompileStatic
1022-
void setValvePosition(Boolean open, Integer valve = 0) {
1023-
if(open == true) {
1024-
sendDeviceEvent([name: 'valve', value: 'open'])
1025-
} else {
1026-
sendDeviceEvent([name: 'valve', value: 'closed'])
1027-
}
1028-
}
10291073
/* #endregion */
10301074
/* #region Generic Getters and Setters */
10311075

@@ -1073,6 +1117,11 @@ Integer getIntegerDeviceSetting(String settingName) {
10731117
return thisDeviceHasSetting(settingName) ? getDeviceSettings()[settingName] as Integer : null
10741118
}
10751119

1120+
@CompileStatic
1121+
String getEnumDeviceSetting(String settingName) {
1122+
return thisDeviceHasSetting(settingName) ? "${getDeviceSettings()[settingName]}".toString() : null
1123+
}
1124+
10761125

10771126
@CompileStatic
10781127
Boolean hasChildren() {
@@ -1386,6 +1435,10 @@ void setHubitatDevicePreferences(LinkedHashMap<String, Object> preferences, Devi
13861435
} else {
13871436
setDeviceSetting(i, [type: preferenceMap[i].type, value: v], dev)
13881437
}
1438+
} else if(dev == null && k.startsWith('trv_')) {
1439+
if(k == 'trv_temperature_offset') {setDeviceSetting(k, [type:'number', value: v as BigDecimal])}
1440+
if(k in ['trv_target_t_auto', 'trv_ext_t_enabled']) {setDeviceSetting(k, [type:'bool', value: v])}
1441+
if(k in ['trv_display_brightness', 'trv_temp_units']) {setDeviceSetting(k, [type:'enum', value: "${v}".toString()])}
13891442
} else if(k == 'id' || k == 'name') {
13901443
logTrace("Skipping settings as configuration of ${k} from Hubitat, please configure from Shelly device web UI if needed.")
13911444
} else if(isInput == true && (k in ['type', 'factory_reset'])) {
@@ -1451,6 +1504,11 @@ void setValveState(String position, Integer id = 0) {
14511504
}
14521505
}
14531506

1507+
@CompileStatic
1508+
void setValvePositionState(Integer level, Integer id = 0) {
1509+
if(level != null) {sendDeviceEvent([name: 'valvePosition', value: level, unit: '%'])}
1510+
}
1511+
14541512
@CompileStatic
14551513
void setSwitchLevelState(Integer level, Integer id = 0) {
14561514
if(level != null) {
@@ -1513,12 +1571,6 @@ Boolean getSwitchState() {
15131571
return thisDevice().currentValue('switch', true) == 'on'
15141572
}
15151573

1516-
@CompileStatic
1517-
void setHeatingSetpoint(BigDecimal temperature) {
1518-
Integer id = getDeviceDataValue('tstatId') as Integer
1519-
parentSendGen1CommandAsync("settings/thermostat/${id}/?target_t_enabled=1&target_t=${temperature}")
1520-
}
1521-
15221574
void on() {
15231575
if(deviceIsInputSwitch(thisDevice()) == true) {
15241576
logWarn('Cannot change state of an input on a Shelly device from Hubitat!')
@@ -1608,6 +1660,7 @@ void setLastUpdated() {
16081660
if(hasCapabilityBattery() == true) {sendDeviceEvent([name: 'lastUpdated', value: nowFormatted()])}
16091661
}
16101662

1663+
//Rollers
16111664
@CompileStatic
16121665
void open() {
16131666
if(isGen1Device() == true) {
@@ -1624,7 +1677,6 @@ void close() {
16241677
} else {
16251678
parentPostCommandSync(coverCloseCommand(getIntegerDeviceDataValue('coverId')))
16261679
}
1627-
16281680
}
16291681

16301682
@CompileStatic
@@ -1651,6 +1703,35 @@ void stopPositionChange() {
16511703
}
16521704
}
16531705

1706+
//TRV
1707+
@CompileStatic
1708+
void setValvePosition(BigDecimal position) {
1709+
parentSendGen1CommandAsync("thermostat/0/?pos=${position as Integer}")
1710+
}
1711+
1712+
@CompileStatic
1713+
void setExternalTemperature(BigDecimal temperature) {
1714+
String units = getEnumDeviceSetting('trv_temp_units')
1715+
if(isCelciusScale() == false && units == 'C') {
1716+
temperature = fToC(temperature)
1717+
} else if(isCelciusScale() == true && units == 'F') {
1718+
temperature = cToF(temperature)
1719+
}
1720+
temperature = temperature.setScale(1, BigDecimal.ROUND_HALF_UP)
1721+
parentSendGen1CommandAsync("ext_t?temp=${temperature}")
1722+
}
1723+
1724+
@CompileStatic
1725+
void setHeatingSetpoint(BigDecimal temperature) {
1726+
String units = getEnumDeviceSetting('trv_temp_units')
1727+
if(isCelciusScale() == false && units == 'C') {
1728+
temperature = fToC(temperature)
1729+
} else if(isCelciusScale() == true && units == 'F') {
1730+
temperature = cToF(temperature)
1731+
}
1732+
temperature = temperature.setScale(1, BigDecimal.ROUND_HALF_UP)
1733+
parentSendGen1CommandAsync("thermostat/0?target_t_enabled=1&target_t=${temperature}")
1734+
}
16541735

16551736
void sendEventToShellyBluetoothHelper(String loc, Object value, String dni) {
16561737
sendLocationEvent(name:loc, value:value, data:dni)
@@ -2532,28 +2613,28 @@ void getStatusGen1Callback(AsyncResponse response, Map data = null) {
25322613
if(hasCapabilityBatteryGen1() == true) {
25332614
LinkedHashMap battery = (LinkedHashMap)json?.bat
25342615
Integer percent = battery?.value as Integer
2535-
setBatteryPercent(percent)
2616+
if(percent != null) {setBatteryPercent(percent)}
25362617
}
25372618
if(hasCapabilityLuxGen1() == true) {
25382619
Integer lux = ((LinkedHashMap)json?.lux)?.value as Integer
2539-
setIlluminance(lux)
2620+
if(lux != null ) {setIlluminance(lux)}
25402621
}
25412622
if(hasCapabilityTempGen1() == true) {
25422623
BigDecimal temp = (BigDecimal)(((LinkedHashMap)json?.tmp)?.value)
25432624
String tempUnits = (((LinkedHashMap)json?.tmp)?.units).toString()
2544-
if(tempUnits == 'C') {
2625+
if(tempUnits == 'C' && temp != null) {
25452626
setTemperatureC(temp)
2546-
} else if(tempUnits == 'F') {
2627+
} else if(tempUnits == 'F' && temp != null) {
25472628
setTemperatureF(temp)
25482629
}
25492630
}
25502631
if(hasCapabilityHumGen1() == true) {
25512632
BigDecimal hum = (BigDecimal)(((LinkedHashMap)json?.hum)?.value)
2552-
if(hum != null){setHumidityPercent(hum)}
2633+
if(hum != null) {setHumidityPercent(hum)}
25532634
}
25542635
if(hasCapabilityFloodGen1() == true) {
25552636
Boolean flood = (Boolean)json?.flood
2556-
if(flood != null){setFloodOn(flood)}
2637+
if(flood != null) {setFloodOn(flood)}
25572638
}
25582639
if(hasCapabilitySwitch() == true || hasChildSwitches() == true) {
25592640
List<LinkedHashMap> relays = (List<LinkedHashMap>)json?.relays
@@ -2570,9 +2651,17 @@ void getStatusGen1Callback(AsyncResponse response, Map data = null) {
25702651
List<LinkedHashMap<String, String>> valves = (List<LinkedHashMap<String, String>>)json?.valves
25712652
if(valves?.size() > 0) {
25722653
valves.eachWithIndex{ valve, index ->
2573-
String position = valve?.state
2654+
String valveState = valve?.state
2655+
logTrace("Valve status: ${valveState}")
2656+
setValveState(valveState.startsWith('open') ? 'open' : 'closed')
2657+
}
2658+
}
2659+
List<LinkedHashMap<String, String>> thermostats = (List<LinkedHashMap<String, String>>)json?.thermostats
2660+
if(thermostats?.size() > 0) {
2661+
thermostats.eachWithIndex{ tstat, index ->
2662+
BigDecimal position = tstat?.pos as BigDecimal
25742663
logTrace("Valve status: ${position}")
2575-
setValveState(position.startsWith('open') ? 'open' : 'closed')
2664+
setValvePositionState(position as Integer)
25762665
}
25772666
}
25782667
}
@@ -2671,6 +2760,26 @@ void getStatusGen1Callback(AsyncResponse response, Map data = null) {
26712760
if(isOn != null) {setSwitchState(isOn)}
26722761
}
26732762
}
2763+
if(json?.thermostats != null) {
2764+
List<LinkedHashMap> thermostats = (List<LinkedHashMap>)json?.thermostats
2765+
thermostats.eachWithIndex{ tstat, index ->
2766+
if(tstat?.pos != null) {setValvePositionState(tstat.pos as Integer)}
2767+
if(tstat?.tmp != null) {
2768+
LinkedHashMap tmp = (LinkedHashMap)tstat.tmp
2769+
if(tmp?.units != null && tmp?.value != null) {
2770+
if(isCelciusScale() == true && tmp?.units == 'C') {
2771+
setTemperatureC(tmp?.value as BigDecimal)
2772+
} else if(isCelciusScale() == false && tmp?.units == 'C') {
2773+
setTemperatureF(cToF(tmp?.value as BigDecimal))
2774+
} else if(isCelciusScale() == true && tmp?.units == 'F') {
2775+
setTemperatureC(fToC(tmp?.value as BigDecimal))
2776+
} else if(isCelciusScale() == false && tmp?.units == 'F') {
2777+
setTemperatureF(tmp?.value as BigDecimal)
2778+
}
2779+
}
2780+
}
2781+
}
2782+
}
26742783
}
26752784
}
26762785

@@ -2946,7 +3055,7 @@ void setDeviceActionsGen1() {
29463055
v.each{ m ->
29473056
Integer index = ((Map)m)?.index as Integer
29483057
String name = "${k}".toString()
2949-
Boolean hasEnabledTimes = actionHasEnabledTimes(v)
3058+
Boolean hasEnabledTimes = actionHasEnabledTimes(k,v)
29503059

29513060
Boolean create = false
29523061
String additionalParams = ''
@@ -2987,8 +3096,10 @@ void setDeviceActionsGen1() {
29873096
}
29883097

29893098
@CompileStatic
2990-
Boolean actionHasEnabledTimes(List<LinkedHashMap> action) {
2991-
if(action != null && action?.size() > 0) {
3099+
Boolean actionHasEnabledTimes(String actionName, List<LinkedHashMap> action) {
3100+
if(hasActionsToCreateEnabledTimesList() == true && actionName in getActionsToCreateEnabledTimes()) {
3101+
return true
3102+
} else if(action != null && action?.size() > 0) {
29923103
Map a = action[0]
29933104
if(a?.urls != null) {
29943105
List urls = (List)a.urls
@@ -3274,7 +3385,7 @@ void deleteHubitatWebhooksGen1() {
32743385
Integer index = ((Map)m)?.index as Integer
32753386
String queryString = "index=${index}&enabled=false".toString()
32763387
queryString += "&name=${k}".toString()
3277-
Boolean hasTimes = actionHasEnabledTimes(v)
3388+
Boolean hasTimes = actionHasEnabledTimes(k,v)
32783389
if(hasTimes) {
32793390
queryString += "&urls[0][url]=".toString()
32803391
queryString += "&urls[0][int]=".toString()

WebhookWebsocket/ShellyTRV.groovy

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#include ShellyUSA.ShellyUSA_Driver_Library
2+
3+
metadata {
4+
definition (name: 'Shelly TRV (Webhook)', namespace: 'ShellyUSA', author: 'Daniel Winks', importUrl: '') {
5+
capability 'Initialize'
6+
capability 'Refresh'
7+
capability 'Configuration'
8+
capability 'Battery' //battery - NUMBER, unit:%
9+
capability 'TemperatureMeasurement' //temperature - NUMBER, unit:°F || °C
10+
capability 'Valve' //valve - ENUM ['open', 'closed']
11+
capability 'ThermostatHeatingSetpoint' //heatingSetpoint - NUMBER, unit:°F || °C
12+
13+
command 'setValvePosition', [[name: 'Position', type: 'NUMBER', description:"Position (0..100)", constraints:["NUMBER"]]]
14+
command 'setExternalTemperature', [[name: 'External Temperature Measurement', type: 'NUMBER']]
15+
command 'getPreferencesFromShellyDeviceGen1'
16+
attribute 'valvePosition', 'number'
17+
attribute 'lastUpdated', 'string'
18+
}
19+
}
20+
21+
@Field static Boolean GEN1 = true
22+
@Field static Boolean HAS_BATTERY_GEN1 = true
23+
@Field static Boolean HAS_TEMP_GEN1 = true
24+
@Field static Boolean HAS_HUM_GEN1 = true
25+
@Field static List<String> ACTIONS_TO_CREATE = [
26+
'valve_open',
27+
'valve_close'
28+
]
29+
@Field static List<String> ACTIONS_TO_CREATE_ENABLED_TIMES = [
30+
'valve_open',
31+
'valve_close'
32+
]

0 commit comments

Comments
 (0)