Skip to content

Instantly share code, notes, and snippets.

@leoherzog
Last active February 27, 2023 21:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save leoherzog/5971698e3db9f034ef05cc279ce2aafc to your computer and use it in GitHub Desktop.
Save leoherzog/5971698e3db9f034ef05cc279ce2aafc to your computer and use it in GitHub Desktop.
Google Apps Script Philips Hue Sunrise-Sunset Color Temperature Changer
// 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));
}
// 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');
}
}
/*
(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