-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathMiniSplit.groovy
More file actions
496 lines (475 loc) · 21.2 KB
/
MiniSplit.groovy
File metadata and controls
496 lines (475 loc) · 21.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
/**
* Import Url: https://raw.githubusercontent.com/arnbme/hubitat/master/MiniSplit.groovy
*
* MiniSplit App
* Functions:
* Controls my dumb Fujitsu mini splits in conjunction with Broadlink IR blasters, and an HE Virtual Thermostat device
* Allows for using mini split Dry mode as part of cooling process, or stand alone.
*
* I wrote this after attempting this with RM, it's just to slow and tedious for coding complex logic, IMHO.
* Also I could not get the HVAC manager app functional with my Fujitsu devices
*
* Preinstallation Requirements
* 1. Install IR Blasters, this app is designed to work with Broadlink devices using the now withdrawn Broadlink Mini Driver app at
* https://community.hubitat.com/t/withdrawn-native-broadlink-rm-rm-pro-rm-mini-sp-driver-rc-hvac-manager/31344
*
* 2. Create working IR code sets for cooling (multiple temperatures), heat (multiple temperatures) dry, off, and more as necessary
*
* 3. Must have a working Virtual Thermostat (or perhaps a real device).
* I use a Virtual Thermostat averaging temperatures from 3 devices using the HE Average Temperature app at
* https://github.com/hubitat/HubitatPublic/tree/master/example-apps
* Changed the app created child from a virtual temperature sensor device to a virtual thermostat.
*
* 4. A dashboard displaying the Virtual Thermostat device or some other method for control
*
* How to store Mini Split IR codes for Fujitsu units. (Your remote key names and setup may vary)
* 1. Set mini-split device power ON, pressing the remote's Stop/Start key. Mini-Split device powers on with last setting stored in the REMOTE device.
* 2. Set device to exact temperature, mode, swing state, louver settings, and Fan (auto suggested) speed, and other settings wanted
* 3. Press remote's Start/Stop key setting power to OFF.
* 4. Set IR blaster into learning mode
* 5. Aim remote at the blaster then press remote's Start/Stop key, setting power ON
* (the Off IR code is sent anytime power is On)
* 6. Save and name the IR codes
* My IR code names and settings
* AC On2169 - Mode: Cool, Temperature 21C 69F, Fan: auto (swing, louvers and other settings: whatever you want)
* AC On2271 - Mode: Cool, Temperature 22C 71F, Fan: auto (swing, louvers and other settings: whatever you want)
* AC On2373 - Mode: Cool, Temperature 23C 73F, Fan: auto (swing, louvers and other settings: whatever you want)
* AC On2475 - Mode: Cool, Temperature 24C 75F, Fan: auto (swing, louvers and other settings: whatever you want)
* AC On2577 - Mode: Cool, Temperature 25C 77F, Fan: auto (swing, louvers and other settings: whatever you want) not currently used
* ACDry74Swing - Mode: Dry, Temperature: 74F 25C Swing: On (swing, louvers and other settings: whatever you want)
* Dry may or may not utilize temperature or fan speed setting depending upon the mini-split hardware,
8 check the manufacturer's operating manual
* ACFanSwing - Mode: Fan, Temperature: n/a Swing: On (swing, louvers and other settings: whatever you want)
* AC Off - Mode: off
* 7. Adjust IR code names in the code as necessary
*
* Notes:
* Most Mini-splits use Celcious for native temperature settings. When using Fahrenheit, temperature is appoximate.
* Most Mini-splits have a built in non accessable thermostat.
* IR communication is one way, from remote or IR blaster to mini-split
* When an Ir code is store with a blank in it's name, the Broadlink app replaces the space with a _ character.
* However when specifying the name in the sendStoredCode command, the _ character cannot be specified.
* Probably best not to use embedded blanks with IR command names. I learned the hard way.
*
*
* Copyright 2020 Arn Burkhoff
*
* Changes to Apache License
* 4. Redistribution. Add paragraph 4e.
* 4e. This software is free for Private Use. All derivatives and copies of this software must be free of any charges,
* and cannot be used for commercial purposes.
*
* 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.
*
* Jul 19, 2022 v0.1.6 Change cooling to use newly added non-swing codes
* Aug 20, 2021 v0.1.6 Add support for Dry codes with temperatures using mode Dry and Dry+
* added codes ACDry(72|76|78)Swing used along with existing ACDry74Swing
* Aug 09, 2021 v0.1.5 When cool: change to use colder settings making fan run faster in auto mode and perhaps blow colder
* Aug 09, 2021 v0.1.5 add support for dry+ mode same as cool but only uses dry. Must set cool temp prior to setting dry+ no temps are shown
* Jul 22, 2021 v0.1.4 delay on dashboard due to additional 2 second delay. Change order of IR command and dashboard display update
* Jul 20, 2021 v0.1.4 in qHandler routine double send command due to occasional missed IR Blaster command sends, or mini-split device failing to respond
* Oct 15, 2020 v0.1.3 Add flag that stops this app from processing HSM Away and Night Mode changes when using Thermostat Scheduler.
* Oct 14, 2020 v0.1.2 When outside temperature below setting value, do not use now inefficient MiniSplits for heating divert to baseboards, except on emergency heat.
* Fix issue where heat and cool would not occur.
* Oct 13, 2020 v0.1.2 Setup for individually controlling each miniSplit. Add Label to define app name.
* Aug 15, 2020 v0.1.1 Add optional command queueing to buffer and remove many unneeded commands that may conflict
* due to commands entered from dashboard such as temperature down or up tapping on icon
* This is in addition to the Temperature Handler delay
* add optional input setting for number of milliseconds to delay command runin from 0 to 2000 default 1000
* This slows things down a little but hopefully stops a deluge of commands and IR triggering
* Jul 19, 2020 v0.1.0 Add optional user coolplus dry and fan offsets overriding hysterisis
* Display MyCool set points or Dry and Fan
* Jul 14, 2020 v0.0.9T Occasional error on transmission. Use a 125ms delay between all device commands vs issue all 5 at once
* Jul 05, 2020 v0.0.9 confirm all ccalculation fields are BigDecimal, adjust AC ir cooling codes
* Jun 30, 2020 v0.0.8 Fix BigDecimal problem calculating dryPoint and fanPoint
* Jun 30, 2020 v0.0.7 Add bool for standard cooling or extended cooling logic
* standard Cool mode directly controlled by thermostat
* add logic for heat and emergency heat
* Attempted setting mode 'mycool' but dashboard hides temperature controls so used input seting.
* Jun 28, 2020 v0.0.6 Set the cooling cycle as follows applies when mode is cool and globalMyCool is true
* cooling >=cooling set point
* drying >=cooling set point - hysterisis
* fan >=cooling set point - hysterisis*2
* off < cooling set point - hysterisis*2
* Jun 28, 2020 v0.0.5 Process HSM arming Status
* Jun 28, 2020 v0.0.4 Change pauseExecution to a runIn
* Jun 27, 2020 v0.0.3 Support user defined thermostat mode: dry Turn on dry when idle and mode=dry regardless of temperature
* Used virtual thermostat command setSupportedThermostatModes adding dry
* with string [cool, off, dry, fan, heat, auto, emergency heat] also ajusting settings order on device
* Subscribe to thermostatMode changes
* set Fan mode to dry, cool, or off giving clear visual operational indication
* Jun 26, 2020 v0.0.2 Delay temperature and coolSetPoint changes or 2 seconds allowing any virtual thermostat operating mode changes to complete
* set fan operating mode to dry when using dry mode, otherwise set fan mode to on
* Jun 25, 2020 v0.0.1 Use thermostatModeHandler for all IR processing, dont set operating mode to dry
* Jun 25, 2020 v0.0.0 Create
*/
definition(
name: "MiniSplit",
namespace: "arnbme",
author: "Arn Burkhoff",
description: "(${version()}) Mini Split control app",
category: "Convenience",
iconUrl: "",
iconX2Url: "",
iconX3Url: "",
importUrl: "https://raw.githubusercontent.com/arnbme/hubitat/master/MiniSplit.groovy")
preferences {
page(name: "mainPage")
}
def version()
{
return "0.1.6";
}
def mainPage()
{
dynamicPage(name: "mainPage", title: "Mini Split Settings", install: true, uninstall: true)
{
section
{
if (settings.logDebugs)
input "buttonDebugOff", "button", title: "Stop Debug Logging"
else
input "buttonDebugOn", "button", title: "Debug For 30 minutes"
input "globalDisable", "bool", required: true, defaultValue: false,
title: "Disable All Functions. Default: Off/False"
input "thisName", "text", title: "Name of this MiniSplit controller zone. Eg: Zone1 Office", submitOnChange: true, required: true
if(thisName) app.updateLabel("$thisName")
input "globalThermostat", "capability.thermostat", required: true, multiple: false, submitOnChange: true,
title: "A Thermostat that controls the Mini splits"
input "globalMyCool", "bool", required: true, defaultValue: false,
title: "ON: Uses app's extended cooling logic with Fan, Dry and Cool points.<br />OFF: Follows Thermostat's cooling and idle state. Default: Off/False"
input name: "globalDryOffset", type: "decimal", required: false, range: "0.1..2.0", submitOnChange: true,
title: "MyCool Dry Offset from Cool Set Point, may be specified in tenths. Optional, when not defined thermostat hysteris is used"
input name: "globalFanOffset", type: "decimal", required: false, range: "0.1..2.0", submitOnChange: true,
title: "MyCool Fan Offset from Dry point, may be specified in tenths. Optional, when not defined thermostat hysteris is used"
if (settings.globalThermostat)
{
def coolSetPoint = globalThermostat.currentValue("coolingSetpoint")
def hysteresis = globalThermostat.currentValue("hysteresis") as BigDecimal
def dryPoint=coolSetPoint - hysteresis
if (settings.globalDryOffset)
dryPoint=coolSetPoint - settings.globalDryOffset
def fanPoint=dryPoint - hysteresis
if (settings.globalFanOffset)
fanPoint=dryPoint - settings.globalFanOffset
paragraph "MyCool settings Hysteresis:$hysteresis, coolSetpoint:$coolSetPoint, dryPoint:$dryPoint, fanPoint:$fanPoint"
}
input name: "globalCommandDelay", type: "number", required: false, range: "0..2000", submitOnChange: false, defaultValue: 1000,
title: "MilliSeconds of delay (Optional). Mitigates sending extraneos commands when changing temperature or mode settings. Default: 1000"
input "globalIrBlasters", "capability.actuator", required: true, multiple: true,
title: "One or More IR Blasters"
input "globalThermostatApp", "bool", required: false, defaultValue: false,
title: "Thermostat Scheduler app is controlling this device. When true: disables the app's Off function for Away and Night HSM status modes. Default: Off/False"
input name: "globalMinOutside", type: "number", required: false, range: "0..100", submitOnChange: true,
title: "When outside temperature at or below, do not use Minisplits for heating (Optional)"
if (globalMinOutside){
input name: "globalTempOutside", type: "capability.temperatureMeasurement", required: true,
title: "Use this device for outside Temperature"
}
}
}
}
def installed() {
log.info "Installed with settings: ${settings}"
initialize()
}
def updated() {
log.info "Updated with settings: ${settings}"
unsubscribe()
initialize()
}
def initialize()
{
if (globalDisable)
{}
else
{
subscribe(globalThermostat, "thermostatOperatingState", temperatureHandler)
subscribe(globalThermostat, "coolingSetpoint", temperatureHandler)
subscribe(globalThermostat, "heatingSetpoint", temperatureHandler)
subscribe(globalThermostat, "temperature", temperatureHandler)
subscribe(globalThermostat, "thermostatMode", temperatureHandler)
if (settings?.globalThermostatApp)
{}
else
subscribe(location, "hsmStatus", hsmStatusHandler)
}
}
// Process Debug buttons
void appButtonHandler(btn)
{
switch(btn)
{
case "buttonDebugOff":
debugOff()
break
case "buttonDebugOn":
app.updateSetting("logDebugs",[value:"true",type:"bool"])
runIn(1800,debugOff) //turns off debug logging after 30 Minutes
break
}
}
void debugOff(){
// stops debug logging
log.info "settings?.thisName: debug logging disabled"
unschedule(debugOff)
app.updateSetting("logDebugs",[value:"false",type:"bool"])
}
def temperatureHandler(evt)
{
// Temperature, Thermostat Mode, heatingSetPoint, or coolingSetPoint changed on Thermostat
if (settings.logDebugs) log.debug "temperatureHandler entered Value: ${evt.value} mode: ${globalThermostat.currentValue("thermostatMode")}"
runIn(2,thermostatModeHandler,[data: ["value":"${evt.value}"]]) //This overrwrites prior pending requests, eliminating them
}
void hsmStatusHandler(evt)
{
// HSM arming status changed
if (settings.logDebugs) log.debug "hsmStatusHandler entered Value: ${evt.value}"
if (evt.value.startsWith('arming'))
{}
else
if (evt.value=='disarmed')
{
def offMode=state?.offMode //get prior mode
switch (offMode)
{
case 'ignore':
break
case 'off':
globalThermostat.off()
break
case 'cool':
globalThermostat.cool()
break
case 'dry':
globalThermostat.setThermostatMode('dry')
break
case 'dry+':
globalThermostat.setThermostatMode('dry+')
break
case 'fan':
globalThermostat.setThermostatMode('fan')
break
case 'auto':
globalThermostat.auto()
break
case 'heat':
globalThermostat.heat()
break
case 'emergency heat':
globalThermostat.emergencyHeat()
break
/* default:
forget about this, execute is not allowed in Hubitat
def cmd = "globalThermostat.${offMode}()"
log.debug "cmd: $cmd"
cmd.execute()
break
*/ }
state.offMode='ignore'
}
else
if (evt.value=='armedAway' || evt.value == 'armedNight')
{
state.offMode=globalThermostat.currentValue("thermostatMode") //restore upon disarm
globalThermostat.off()
}
else
state.offMode='ignore'
}
def thermostatModeHandler(evt)
{
// Thermostat operating state changed, blast IR code to mini-splits
// Note this ignores the thermostat device operatingMode
def acMode = globalThermostat.currentValue("thermostatMode")
if (settings.logDebugs) log.debug "thermostatModeHandler entered Value: ${evt.value} acMode: $acMode"
def irCode='AC Off'
switch (acMode)
{
case 'cool':
def coolSetPoint = globalThermostat.currentValue("coolingSetpoint")
def hysteresis = globalThermostat.currentValue("hysteresis") as BigDecimal
def temperature=globalThermostat.currentValue("temperature") as BigDecimal
if (settings.globalMyCool)
{
// all fields should be BigDecimal
def dryPoint=coolSetPoint - hysteresis
if (settings.globalDryOffset)
dryPoint=coolSetPoint - settings.globalDryOffset
def fanPoint=dryPoint - hysteresis
if (settings.globalFanOffset)
fanPoint=dryPoint - settings.globalFanOffset
if (settings.logDebugs) log.debug "coolSetPoint: $coolSetPoint ${coolSetPoint.class.name} hysteresis: $hysteresis ${hysteresis.class.name} dryPoint: $dryPoint fanPoint: $fanPoint ${dryPoint.class.name} temperature: $temperature ${temperature.class.name}"
if (temperature>=coolSetPoint)
{
if (coolSetPoint < 72) irCode='AC On2169'
else
if (coolSetPoint < 74) irCode='AC On2271'
else
if (coolSetPoint < 76) irCode='AC On2373'
else
if (coolSetPoint < 78) irCode='AC On2475'
else
if (coolSetPoint < 80) irCode='AC On2577'
else
irCode='AC On2579'
}
else
if (temperature>=dryPoint)
irCode='ACDry74Swing'
else
if (temperature>=fanPoint)
irCode='ACFanSwing'
else
irCode='AC Off'
}
else
{
if (globalThermostat.currentValue("thermostatOperatingState") =='cooling' || coolSetPoint < temperature-hysteresis)
{
if (coolSetPoint < 72) irCode='AC2170NS'
// if (coolSetPoint < 72) irCode='AC On2169'
else
if (coolSetPoint < 74) irCode='AC2272NS'
// if (coolSetPoint < 74) irCode='AC On2271'
// if (coolSetPoint < 74) irCode='AC On2169'
else
if (coolSetPoint < 76) irCode='AC2374NS'
// if (coolSetPoint < 76) irCode='AC On2373'
// if (coolSetPoint < 76) irCode='AC On2271'
else
if (coolSetPoint < 78) irCode='AC2476NS'
// if (coolSetPoint < 78) irCode='AC On2475'
// if (coolSetPoint < 78) irCode='AC On2373'
else
if (coolSetPoint < 80) irCode='AC2578NS'
// if (coolSetPoint < 80) irCode='AC On2577'
// if (coolSetPoint < 80) irCode='AC On2475'
else
irCode='AC2680NS'
// irCode='AC On2579'
}
else
irCode='AC Off'
}
break
case 'heat':
// MiniSplis are not efficient below a certain temperature
if (settings?.globalMinOutside && settings.globalTempOutside.currentValue("temperature") <= settings.globalMinOutside)
{
irCode='AC Off'
globalThermostat.setThermostatOperatingState('idle')
break
}
case 'emergency heat':
def heatSetPoint = globalThermostat.currentValue("heatingSetpoint")
def hysteresis = globalThermostat.currentValue("hysteresis") as BigDecimal
def temperature=globalThermostat.currentValue("temperature") as BigDecimal
if (globalThermostat.currentValue("thermostatOperatingState") =='heating' || heatSetPoint > temperature+hysteresis)
{
if (heatSetPoint < 68) irCode='ACHeat2068'
else
if (heatSetPoint < 70) irCode='ACHeat2170'
else
if (heatSetPoint < 72) irCode='ACHeat2272'
else
if (heatSetPoint < 74) irCode='ACHeat2374'
else
if (heatSetPoint < 76) irCode='ACHeat2476'
else
irCode='ACHeat2578'
}
else
irCode='AC Off'
if (settings.logDebugs) log.debug "thermostatModeHandler mode:$acMode irCode:$irCode HeatSetPoint:$heatSetPoint Hysteresis:$hysteresis Temperature:$temperature ${globalThermostat.currentValue('thermostatOperatingState')}"
break
case 'off':
irCode='AC Off'
break
case 'dry':
def coolSetPoint = globalThermostat.currentValue("coolingSetpoint")
if (coolSetPoint <= 72) irCode='ACDry70Swing'
else
if (coolSetPoint <= 74) irCode='ACDry72Swing'
else
if (coolSetPoint <= 76) irCode='ACDry74Swing'
else
if (coolSetPoint <= 78) irCode='ACDry76Swing'
else
irCode='ACDry78Swing'
break
case 'dry+': //dry with thermostat control
def coolSetPoint = globalThermostat.currentValue("coolingSetpoint")
def hysteresis = globalThermostat.currentValue("hysteresis") as BigDecimal
def temperature=globalThermostat.currentValue("temperature") as BigDecimal
if (coolSetPoint < temperature-hysteresis)
{
if (coolSetPoint <= 72) irCode='ACDry70Swing'
else
if (coolSetPoint <= 74) irCode='ACDry72Swing'
else
if (coolSetPoint <= 76) irCode='ACDry74Swing'
else
if (coolSetPoint <= 78) irCode='ACDry76Swing'
else
irCode='ACDry78Swing'
}
else
irCode='AC Off'
break
case 'fan':
irCode='ACFanSwing'
break
}
if (settings.logDebugs) log.debug "thermostatModeHandler irCode: $irCode Prior irCode: ${state.priorIrCode}"
if (settings.globalCommandDelay && settings.globalCommandDelay > 0)
{
if (settings.logDebugs) log.debug "thermostatModeHandler queueing for ${settings.globalCommandDelay} milliseconds command: irCode: $irCode Prior irCode: ${state.priorIrCode}"
runInMillis(settings.globalCommandDelay,"qHandler",[data: irCode])
}
else
{
if (settings.logDebugs) log.debug "thermostatModeHandler not queueing command: irCode: $irCode Prior irCode: ${state.priorIrCode}"
qHandler(irCode)
}
}
void qHandler(irCode) //process commands
{
if (settings.logDebugs) log.debug "qHandler irCode: $irCode Prior irCode: ${state.priorIrCode}"
if (irCode != state?.priorIrCode)
{
state.priorIrCode=irCode
if (irCode.startsWith('ACDry'))
// globalThermostat.setThermostatFanMode('Dry')
globalThermostat.setThermostatFanMode(irCode)
else
if (irCode=='AC Off')
globalThermostat.setThermostatFanMode('Off')
else
if (irCode=='ACFanSwing')
globalThermostat.setThermostatFanMode('Fan Only')
else
if (irCode.startsWith('ACHeat'))
globalThermostat.setThermostatFanMode('Heat')
else
if (settings.globalMyCool)
globalThermostat.setThermostatFanMode('myCool')
else
globalThermostat.setThermostatFanMode('Cool')
globalIrBlasters.each
{
it.SendStoredCode(irCode)
pauseExecution(2000) //July 20, 2021 reissue command compensating for lost or ignored commands
it.SendStoredCode(irCode)
if (globalIrBlasters.size()> 1)
pauseExecution(125)
}
}
}