Last active
February 27, 2023 21:58
-
-
Save leoherzog/5971698e3db9f034ef05cc279ce2aafc to your computer and use it in GitHub Desktop.
Google Apps Script Philips Hue Sunrise-Sunset Color Temperature Changer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// updates the color temp of a hue group throughout the day, from blue in the morning to orange in the evening. | |
// | |
// copy these three to three files in a new apps script project (https://script.google.com/), | |
// make a new hue remote app id (https://developers.meethue.com/my-apps/), | |
// copy the app id and secret onto lines 54 and 55 of hue-oauth-stuff.gs, | |
// publish as a web app, | |
// go to the web app and authorize via oauth, | |
// run createBridgeUser() on the hue-oauth-stuff page, | |
// change the groupname to the Hue group you want to change, | |
// change the lat and long to your location (to calculate sunrise and sunset times), | |
// run updateColor() once to ensure it works, and | |
// create a ☰▸ trigger to run updateColor() every 5 minutes. | |
const groupname = 'Office'; | |
const lat = '40.6892582'; | |
const long = '-74.0446753'; | |
function updateColor() { | |
const now = new Date(); | |
if (now.getHours() <= 5 || now.getHours() >= 22) { | |
return; | |
} | |
const times = new SunCalc().getTimes(now, lat, long); | |
// https://stackoverflow.com/a/29839360/2700296 | |
const percentThroughDay = Math.round(((now - times.sunrise) / (times.sunset - times.sunrise)) * 100) / 100; | |
// https://developers.meethue.com/develop/hue-api/groupds-api/#set-gr-state | |
// ct is between 153 (6500K) and 500 (2000K) | |
// {%}*{difference}+{min} = number that's at % point between two numbers | |
const newTemp = Math.round((percentThroughDay * (500 - 153)) + 153); | |
let hueService = getHueService(); | |
let groups = UrlFetchApp.fetch('https://api.meethue.com/route/clip/v2/resource/room', { | |
"headers": { | |
"Authorization": "Bearer " + hueService.getAccessToken(), | |
"Content-Type": "application/json", | |
"hue-application-key": "D1l0InpzRTVlAkrs0000000QXfnw6zBe6xnoD800" | |
}, | |
"method": "get" | |
}); | |
groups = JSON.parse(groups.getContentText()); | |
console.log(JSON.stringify(groups, null, 2)); | |
let officeGroupId = groups.data.find(x => x.metadata.name === groupname).services.find(x => x.rtype === 'grouped_light').rid; | |
let change = UrlFetchApp.fetch('https://api.meethue.com/route/clip/v2/resource/grouped_light/' + officeGroupId, { | |
"headers": { | |
"Authorization": "Bearer " + hueService.getAccessToken(), | |
"Content-Type": "application/json", | |
"hue-application-key": "D1l0InpzRTVlAkrs0000000QXfnw6zBe6xnoD800" | |
}, | |
"method": "put", | |
"payload": JSON.stringify({"color_temperature": {"mirek": newTemp} }), | |
"muteHttpExceptions": true | |
}); | |
change = JSON.parse(change.getContentText()); | |
console.log(JSON.stringify(change, null, 2)); | |
} | |
function listAllLights() { | |
let hueService = getHueService(); | |
let lights = UrlFetchApp.fetch('https://api.meethue.com/route/clip/v2/resource/light', { | |
"headers": { | |
"Authorization": "Bearer " + hueService.getAccessToken(), | |
"Content-Type": "application/json", | |
"hue-application-key": "D1l0InpzRTVlAkrs0000000QXfnw6zBe6xnoD800" | |
}, | |
"method": "get" | |
}); | |
lights = JSON.parse(lights.getContentText()); | |
console.log(JSON.stringify(lights, null, 2)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://developers.meethue.com/develop/hue-api/remote-api-quick-start-guide/ | |
function createBridgeUser() { | |
var hueService = getHueService(); | |
let response; | |
response = UrlFetchApp.fetch('https://api.meethue.com/bridge/0/config', { | |
"headers": { | |
"Authorization": "Bearer " + hueService.getAccessToken(), | |
"Content-Type": "application/json" | |
}, | |
"method": "put" | |
}); | |
console.log(response.getContentText()); | |
console.log("Step 1 done. Now press the link button on the bridge."); | |
Utilities.sleep(10000); | |
response = UrlFetchApp.fetch('https://api.meethue.com/bridge/', { | |
"headers": { | |
"Authorization": "Bearer " + hueService.getAccessToken(), | |
"Content-Type": "application/json" | |
}, | |
"method": "post", | |
"payload": '{ "devicetype": "sunrise-sunset" }' | |
}); | |
console.log(response.getContentText()); | |
} | |
function doGet() { | |
var hueService = getHueService(); | |
var authorizationUrl = hueService.getAuthorizationUrl(); | |
var template = HtmlService.createTemplate( | |
'<a href="<?= authorizationUrl ?>" target="_blank">Authorize</a>.'); | |
template.authorizationUrl = authorizationUrl; | |
var page = template.evaluate(); | |
return HtmlService.createHtmlOutput(page); | |
} | |
function getHueService() { | |
// Create a new service with the given name. The name will be used when | |
// persisting the authorized token, so ensure it is unique within the | |
// scope of the property store. | |
return OAuth2.createService('hue') | |
// Set the endpoint URLs, which are the same for all Google services. | |
.setAuthorizationBaseUrl('https://api.meethue.com/v2/oauth2/authorize') | |
.setTokenUrl('https://api.meethue.com/v2/oauth2/token') | |
// Set the client ID and secret, from the Hue Developers Console. | |
.setClientId('id') | |
.setClientSecret('secret') | |
// Set the name of the callback function in the script referenced | |
// above that should be invoked to complete the OAuth flow. | |
.setCallbackFunction('authCallback') | |
// Set the property store where authorized tokens should be persisted. | |
.setPropertyStore(PropertiesService.getUserProperties()) | |
} | |
function authCallback(request) { | |
var hueService = getHueService(); | |
var isAuthorized = hueService.handleCallback(request); | |
if (isAuthorized) { | |
return HtmlService.createHtmlOutput('Success! You can close this tab.'); | |
} else { | |
return HtmlService.createHtmlOutput('Denied. You can close this tab'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
(c) 2011-2014, Vladimir Agafonkin | |
SunCalc is a JavaScript library for calculating sun/mooon position and light phases. | |
https://github.com/mourner/suncalc | |
see license - https://github.com/mourner/suncalc/blob/master/LICENSE | |
https://ramblings.mcpher.com/gassnippets2/suncalc/ | |
*/ | |
var SunCalc = function () { "use strict"; | |
// shortcuts for easier to read formulas | |
var self = this; | |
var PI = Math.PI, | |
sin = Math.sin, | |
cos = Math.cos, | |
tan = Math.tan, | |
asin = Math.asin, | |
atan = Math.atan2, | |
acos = Math.acos, | |
rad = PI / 180; | |
// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas | |
// date/time constants and conversions | |
var dayMs = 1000 * 60 * 60 * 24, | |
J1970 = 2440588, | |
J2000 = 2451545; | |
function toJulian(date) { | |
return date.valueOf() / dayMs - 0.5 + J1970; | |
} | |
function fromJulian(j) { | |
return new Date((j + 0.5 - J1970) * dayMs); | |
} | |
function toDays(date) { | |
return toJulian(date) - J2000; | |
} | |
// general calculations for position | |
var e = rad * 23.4397; // obliquity of the Earth | |
function getRightAscension(l, b) { | |
return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); | |
} | |
function getDeclination(l, b) { | |
return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); | |
} | |
function getAzimuth(H, phi, dec) { | |
return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); | |
} | |
function getAltitude(H, phi, dec) { | |
return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); | |
} | |
function getSiderealTime(d, lw) { | |
return rad * (280.16 + 360.9856235 * d) - lw; | |
} | |
// general sun calculations | |
function getSolarMeanAnomaly(d) { | |
return rad * (357.5291 + 0.98560028 * d); | |
} | |
function getEquationOfCenter(M) { | |
return rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)); | |
} | |
function getEclipticLongitude(M, C) { | |
var P = rad * 102.9372; // perihelion of the Earth | |
return M + C + P + PI; | |
} | |
function getSunCoords(d) { | |
var M = getSolarMeanAnomaly(d), | |
C = getEquationOfCenter(M), | |
L = getEclipticLongitude(M, C); | |
return { | |
dec: getDeclination(L, 0), | |
ra: getRightAscension(L, 0) | |
}; | |
} | |
// calculates sun position for a given date and latitude/longitude | |
self.getPosition = function (date, lat, lng) { | |
var lw = rad * -lng, | |
phi = rad * lat, | |
d = toDays(date), | |
c = getSunCoords(d), | |
H = getSiderealTime(d, lw) - c.ra; | |
return { | |
azimuth: getAzimuth(H, phi, c.dec), | |
altitude: getAltitude(H, phi, c.dec) | |
}; | |
}; | |
// sun times configuration (angle, morning name, evening name) | |
var times = [ | |
[-0.83, 'sunrise', 'sunset' ], | |
[ -0.3, 'sunriseEnd', 'sunsetStart' ], | |
[ -6, 'dawn', 'dusk' ], | |
[ -12, 'nauticalDawn', 'nauticalDusk'], | |
[ -18, 'nightEnd', 'night' ], | |
[ 6, 'goldenHourEnd', 'goldenHour' ] | |
]; | |
// adds a custom time to the times config | |
self.addTime = function (angle, riseName, setName) { | |
times.push([angle, riseName, setName]); | |
}; | |
// calculations for sun times | |
var J0 = 0.0009; | |
function getJulianCycle(d, lw) { | |
return Math.round(d - J0 - lw / (2 * PI)); | |
} | |
function getApproxTransit(Ht, lw, n) { | |
return J0 + (Ht + lw) / (2 * PI) + n; | |
} | |
function getSolarTransitJ(ds, M, L) { | |
return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); | |
} | |
function getHourAngle(h, phi, d) { | |
return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); | |
} | |
// calculates sun times for a given date and latitude/longitude | |
self.getTimes = function (date, lat, lng) { | |
var lw = rad * -lng, | |
phi = rad * lat, | |
d = toDays(date), | |
n = getJulianCycle(d, lw), | |
ds = getApproxTransit(0, lw, n), | |
M = getSolarMeanAnomaly(ds), | |
C = getEquationOfCenter(M), | |
L = getEclipticLongitude(M, C), | |
dec = getDeclination(L, 0), | |
Jnoon = getSolarTransitJ(ds, M, L); | |
// returns set time for the given sun altitude | |
function getSetJ(h) { | |
var w = getHourAngle(h, phi, dec), | |
a = getApproxTransit(w, lw, n); | |
return getSolarTransitJ(a, M, L); | |
} | |
var result = { | |
solarNoon: fromJulian(Jnoon), | |
nadir: fromJulian(Jnoon - 0.5) | |
}; | |
var i, len, time, angle, morningName, eveningName, Jset, Jrise; | |
for (i = 0, len = times.length; i < len; i += 1) { | |
time = times[i]; | |
Jset = getSetJ(time[0] * rad); | |
Jrise = Jnoon - (Jset - Jnoon); | |
result[time[1]] = fromJulian(Jrise); | |
result[time[2]] = fromJulian(Jset); | |
} | |
return result; | |
}; | |
// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas | |
function getMoonCoords(d) { // geocentric ecliptic coordinates of the moon | |
var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude | |
M = rad * (134.963 + 13.064993 * d), // mean anomaly | |
F = rad * (93.272 + 13.229350 * d), // mean distance | |
l = L + rad * 6.289 * sin(M), // longitude | |
b = rad * 5.128 * sin(F), // latitude | |
dt = 385001 - 20905 * cos(M); // distance to the moon in km | |
return { | |
ra: getRightAscension(l, b), | |
dec: getDeclination(l, b), | |
dist: dt | |
}; | |
} | |
self.getMoonPosition = function (date, lat, lng) { | |
var lw = rad * -lng, | |
phi = rad * lat, | |
d = toDays(date), | |
c = getMoonCoords(d), | |
H = getSiderealTime(d, lw) - c.ra, | |
h = getAltitude(H, phi, c.dec); | |
// altitude correction for refraction | |
h = h + rad * 0.017 / tan(h + rad * 10.26 / (h + rad * 5.10)); | |
return { | |
azimuth: getAzimuth(H, phi, c.dec), | |
altitude: h, | |
distance: c.dist | |
}; | |
}; | |
// calculations for illumination parameters of the moon, | |
// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and | |
// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus | |
// (Willmann-Bell, Richmond) 1998. | |
self.getMoonIllumination = function (date) { | |
var d = toDays(date), | |
s = getSunCoords(d), | |
m = getMoonCoords(d), | |
sdist = 149598000, // distance from Earth to Sun in km | |
phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)), | |
inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)); | |
return { | |
fraction: (1 + cos(inc)) / 2, | |
angle: atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) | |
- cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra)) | |
}; | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment