Neato (connect) app to control Neato robot vacuum cleaner.
[smartapps.git] / third-party / circadian-daylight.groovy
1 /**
2 * Circadian Daylight 2.6
3 *
4 * This SmartApp synchronizes your color changing lights with local perceived color
5 * temperature of the sky throughout the day. This gives your environment a more
6 * natural feel, with cooler whites during the midday and warmer tints near twilight
7 * and dawn.
8 *
9 * In addition, the SmartApp sets your lights to a nice cool white at 1% in
10 * "Sleep" mode, which is far brighter than starlight but won't reset your
11 * circadian rhythm or break down too much rhodopsin in your eyes.
12 *
13 * Human circadian rhythms are heavily influenced by ambient light levels and
14 * hues. Hormone production, brainwave activity, mood and wakefulness are
15 * just some of the cognitive functions tied to cyclical natural light.
16 * http://en.wikipedia.org/wiki/Zeitgeber
17 *
18 * Here's some further reading:
19 *
20 * http://www.cambridgeincolour.com/tutorials/sunrise-sunset-calculator.htm
21 * http://en.wikipedia.org/wiki/Color_temperature
22 *
23 * Technical notes: I had to make a lot of assumptions when writing this app
24 * * The Hue bulbs are only capable of producing a true color spectrum from
25 * 2700K to 6000K. The Hue Pro application indicates the range is
26 * a little wider on each side, but I stuck with the Philips
27 *  documentation
28 * * I aligned the color space to CIE with white at D50. I suspect "true"
29 * white for this application might actually be D65, but I will have
30 * to recalculate the color temperature if I move it.
31 * * There are no considerations for weather or altitude, but does use your
32 * hub's zip code to calculate the sun position.
33 * * The app doesn't calculate a true "Blue Hour" -- it just sets the lights to
34 * 2700K (warm white) until your hub goes into Night mode
35 *
36 * Version 2.6: March 26, 2016 - Fixes issue with hex colors.  Move your color changing bulbs to Color Temperature instead
37 * Version 2.5: March 14, 2016 - Add "disabled" switch
38 * Version 2.4: February 18, 2016 - Mode changes
39 * Version 2.3: January 23, 2016 - UX Improvements for publication, makes Campfire default instead of Moonlight
40 * Version 2.2: January 2, 2016 - Add better handling for off() schedules
41 * Version 2.1: October 27, 2015 - Replace motion sensors with time
42 * Version 2.0: September 19, 2015 - Update for Hub 2.0
43 * Version 1.5: June 26, 2015 - Merged with SANdood's optimizations, breaks unofficial LIGHTIFY support
44 * Version 1.4: May 21, 2015 - Clean up mode handling
45 * Version 1.3: April 8, 2015 - Reduced Hue IO, increased robustness
46 * Version 1.2: April 7, 2015 - Add support for LIGHTIFY bulbs, dimmers and user selected "Sleep"
47 * Version 1.1: April 1, 2015 - Add support for contact sensors
48 * Version 1.0: March 30, 2015 - Initial release
49 *
50 * The latest version of this file can be found at
51 * https://github.com/KristopherKubicki/smartapp-circadian-daylight/
52 *
53 */
54
55 definition(
56 name: "Circadian Daylight",
57 namespace: "KristopherKubicki",
58 author: "kristopher@acm.org",
59 description: "Sync your color changing lights and dimmers with natural daylight hues to improve your cognitive functions and restfulness.",
60 category: "Green Living",
61 iconUrl: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol.png",
62 iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MiscHacking/mindcontrol@2x.png"
63 )
64
65 preferences {
66     section("Thank you for installing Circadian Daylight! This application dims and adjusts the color temperature of your lights to match the state of the sun, which has been proven to aid in cognitive functions and restfulness. The default options are well suited for most users, but feel free to tweak accordingly!") {
67     }
68     section("Control these bulbs; Select each bulb only once") {
69         input "ctbulbs", "capability.colorTemperature", title: "Which Temperature Changing Bulbs?", multiple:true, required: false
70         input "bulbs", "capability.colorControl", title: "Which Color Changing Bulbs?", multiple:true, required: false
71         input "dimmers", "capability.switchLevel", title: "Which Dimmers?", multiple:true, required: false
72     }
73     section("What are your 'Sleep' modes? The modes you pick here will dim your lights and filter light to a softer, yellower hue to help you fall asleep easier. Protip: You can pick 'Nap' modes as well!") {
74         input "smodes", "mode", title: "What are your Sleep modes?", multiple:true, required: false
75     }
76     section("Override Constant Brightness (default) with Dynamic Brightness? If you'd like your lights to dim as the sun goes down, override this option. Most people don't like it, but it can look good in some settings.") {
77         input "dbright","bool", title: "On or off?", required: false
78     }
79     section("Override night time Campfire (default) with Moonlight? Circadian Daylight by default is easier on your eyes with a yellower hue at night. However if you'd like a whiter light instead, override this option. Note: this will likely disrupt your circadian rhythm.") {
80         input "dcamp","bool", title: "On or off?", required: false
81     }
82     section("Override night time Dimming (default) with Rhodopsin Bleaching? Override this option if you would not like Circadian Daylight to dim your lights during your Sleep modes. This is definitely not recommended!") {
83         input "ddim","bool", title: "On or off?", required: false
84     }
85     section("Disable Circadian Daylight when the following switches are on:") {
86         input "dswitches","capability.switch", title: "Switches", multiple:true, required: false
87     }
88 }
89
90 def installed() {
91     unsubscribe()
92     unschedule()
93     initialize()
94 }
95
96 def updated() {
97     unsubscribe()
98     unschedule()
99     initialize()
100 }
101
102 private def initialize() {
103     log.debug("initialize() with settings: ${settings}")
104     if(ctbulbs) { subscribe(ctbulbs, "switch.on", modeHandler) }
105     if(bulbs) { subscribe(bulbs, "switch.on", modeHandler) }
106     if(dimmers) { subscribe(dimmers, "switch.on", modeHandler) }
107     if(dswitches) { subscribe(dswitches, "switch.off", modeHandler) }
108     subscribe(location, "mode", modeHandler)
109     
110     // revamped for sunset handling instead of motion events
111     subscribe(location, "sunset", modeHandler)
112     subscribe(location, "sunrise", modeHandler)
113     schedule("0 */15 * * * ?", modeHandler)
114     subscribe(app,modeHandler)
115     subscribe(location, "sunsetTime", scheduleTurnOn)
116     // rather than schedule a cron entry, fire a status update a little bit in the future recursively
117     scheduleTurnOn()
118 }
119
120 def scheduleTurnOn() {
121     def int iterRate = 20
122     
123     // get sunrise and sunset times
124     def sunRiseSet = getSunriseAndSunset()
125     def sunriseTime = sunRiseSet.sunrise
126     log.debug("sunrise time ${sunriseTime}")
127     def sunsetTime = sunRiseSet.sunset
128     log.debug("sunset time ${sunsetTime}")
129     
130     if(sunriseTime.time > sunsetTime.time) {
131         sunriseTime = new Date(sunriseTime.time - (24 * 60 * 60 * 1000))
132     }
133     
134     def runTime = new Date(now() + 60*15*1000)
135     for (def i = 0; i < iterRate; i++) {
136         def long uts = sunriseTime.time + (i * ((sunsetTime.time - sunriseTime.time) / iterRate))
137         def timeBeforeSunset = new Date(uts)
138         if(timeBeforeSunset.time > now()) {
139             runTime = timeBeforeSunset
140             last
141         }
142     }
143     
144     log.debug "checking... ${runTime.time} : $runTime"
145     if(state.nextTime != runTime.time) {
146         state.nextTimer = runTime.time
147         log.debug "Scheduling next step at: $runTime (sunset is $sunsetTime) :: ${state.nextTimer}"
148         runOnce(runTime, modeHandler)
149     }
150 }
151
152
153 // Poll all bulbs, and modify the ones that differ from the expected state
154 def modeHandler(evt) {
155     for (dswitch in dswitches) {
156         if(dswitch.currentSwitch == "on") {
157             return
158         }
159     }
160     
161     def ct = getCT()
162     def hex = getHex()
163     def hsv = getHSV()
164     def bright = getBright()
165     
166     for(ctbulb in ctbulbs) {
167         if(ctbulb.currentValue("switch") == "on") {
168             if((settings.dbright == true || location.mode in settings.smodes) && ctbulb.currentValue("level") != bright) {
169                 ctbulb.setLevel(bright)
170             }
171             if(ctbulb.currentValue("colorTemperature") != ct) {
172                 ctbulb.setColorTemperature(ct)
173             }
174         }
175     }
176     def color = [hex: hex, hue: hsv.h, saturation: hsv.s, level: bright]
177     for(bulb in bulbs) {
178         if(bulb.currentValue("switch") == "on") {
179                         def tmp = bulb.currentValue("color")
180             if(bulb.currentValue("color") != hex) {
181                 if(settings.dbright == true || location.mode in settings.smodes) { 
182                         color.value = bright
183                 } else {
184                                         color.value = bulb.currentValue("level")
185                                 }
186                 def ret = bulb.setColor(color)
187                         }
188         }
189     }
190     for(dimmer in dimmers) {
191         if(dimmer.currentValue("switch") == "on") {
192                 if(dimmer.currentValue("level") != bright) {
193                 dimmer.setLevel(bright)
194             }
195         }
196     }
197     
198     scheduleTurnOn()
199 }
200
201 def getCTBright() {
202     def after = getSunriseAndSunset()
203     def midDay = after.sunrise.time + ((after.sunset.time - after.sunrise.time) / 2)
204     
205     def currentTime = now()
206     def float brightness = 1
207     def int colorTemp = 2700
208     if(currentTime > after.sunrise.time && currentTime < after.sunset.time) {
209         if(currentTime < midDay) {
210             colorTemp = 2700 + ((currentTime - after.sunrise.time) / (midDay - after.sunrise.time) * 3800)
211             brightness = ((currentTime - after.sunrise.time) / (midDay - after.sunrise.time))
212         }
213         else {
214             colorTemp = 6500 - ((currentTime - midDay) / (after.sunset.time - midDay) * 3800)
215             brightness = 1 - ((currentTime - midDay) / (after.sunset.time - midDay))
216             
217         }
218     }
219     
220     if(settings.dbright == false) {
221         brightness = 1
222     }
223     
224         if(location.mode in settings.smodes) {
225                 if(currentTime > after.sunset.time) {
226                         if(settings.dcamp == true) {
227                                 colorTemp = 6500
228                         }
229                         else {
230                                 colorTemp = 2700
231                         }
232                 }
233                 if(settings.ddim == false) {
234                         brightness = 0.01
235                 }
236         }
237     
238     def ct = [:]
239     ct = [colorTemp: colorTemp, brightness: Math.round(brightness * 100)]
240     ct
241 }
242
243 def getCT() {
244         def ctb = getCTBright()
245     //log.debug "Color Temperature: " + ctb.colorTemp
246     return ctb.colorTemp
247 }
248
249 def getHex() {
250         def ct = getCT()
251     //log.debug "Hex: " + rgbToHex(ctToRGB(ct)).toUpperCase()
252     return rgbToHex(ctToRGB(ct)).toUpperCase()
253 }
254
255 def getHSV() {
256         def ct = getCT()
257     //log.debug "HSV: " + rgbToHSV(ctToRGB(ct))
258     return rgbToHSV(ctToRGB(ct))
259 }
260
261 def getBright() {
262         def ctb = getCTBright()
263     //log.debug "Brightness: " + ctb.brightness
264     return ctb.brightness
265 }
266
267
268 // Based on color temperature converter from
269 // http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
270 // This will not work for color temperatures below 1000 or above 40000
271 def ctToRGB(ct) {
272     
273     if(ct < 1000) { ct = 1000 }
274     if(ct > 40000) { ct = 40000 }
275     
276     ct = ct / 100
277     
278     //red
279     def r
280     if(ct <= 66) { r = 255 }
281     else { r = 329.698727446 * ((ct - 60) ** -0.1332047592) }
282     if(r < 0) { r = 0 }
283     if(r > 255) { r = 255 }
284     
285     //green
286     def g
287     if (ct <= 66) { g = 99.4708025861 * Math.log(ct) - 161.1195681661 }
288     else { g = 288.1221695283 * ((ct - 60) ** -0.0755148492) }
289     if(g < 0) { g = 0 }
290     if(g > 255) { g = 255 }
291     
292     //blue
293     def b
294     if(ct >= 66) { b = 255 }
295     else if(ct <= 19) { b = 0 }
296     else { b = 138.5177312231 * Math.log(ct - 10) - 305.0447927307 }
297     if(b < 0) { b = 0 }
298     if(b > 255) { b = 255 }
299     
300     def rgb = [:]
301     rgb = [r: r as Integer, g: g as Integer, b: b as Integer]
302     rgb
303 }
304
305 def rgbToHex(rgb) {
306         return "#" + Integer.toHexString(rgb.r).padLeft(2,'0') + Integer.toHexString(rgb.g).padLeft(2,'0') + Integer.toHexString(rgb.b).padLeft(2,'0')
307 }
308
309 //http://www.rapidtables.com/convert/color/rgb-to-hsv.htm
310 def rgbToHSV(rgb) {
311         def h, s, v
312     
313     def r = rgb.r / 255
314     def g = rgb.g / 255
315     def b = rgb.b / 255
316     
317     def max = [r, g, b].max()
318     def min = [r, g, b].min()
319     
320     def delta = max - min
321        
322     //hue
323     if(delta == 0) { h = 0}
324     else if(max == r) { 
325         double dub = (g - b) / delta
326         h = 60 * (dub % 6)
327         }
328     else if(max == g) { h = 60 * (((b - r) / delta) + 2) }
329     else if(max == b) { h = 60 * (((r - g) / delta) + 4) }
330     
331     //saturation
332     if(max == 0) { s = 0 }
333     else { s = (delta / max) * 100 }
334     
335     //value
336     v = max * 100
337     
338     def degreesRange = (360 - 0)
339     def percentRange = (100 - 0)
340     
341     return [h: ((h * percentRange) / degreesRange) as Integer, s: ((s * percentRange) / degreesRange) as Integer, v: v as Integer]
342 }