From c8a9b0ba3ed581a9fef7ee2b459b1de84d976ff0 Mon Sep 17 00:00:00 2001 From: Matthew <21284075+MattieX@users.noreply.github.com> Date: Thu, 5 Nov 2020 23:23:27 +0000 Subject: [PATCH] feat(notification): add philips hue (#681) Co-authored-by: Jef LeCompte Co-authored-by: Nathan Banks --- .env-example | 9 +++ README.md | 23 +++++++ package-lock.json | 20 ++++++ package.json | 1 + src/config.ts | 12 ++++ src/notification/notification.ts | 2 + src/notification/philips-hue.ts | 108 +++++++++++++++++++++++++++++++ 7 files changed, 175 insertions(+) create mode 100644 src/notification/philips-hue.ts diff --git a/.env-example b/.env-example index 7cd2d33988..c47b0cbb11 100644 --- a/.env-example +++ b/.env-example @@ -35,6 +35,15 @@ PAGE_SLEEP_MAX= PAGE_TIMEOUT= PAGERDUTY_INTEGRATION_KEY= PAGERDUTY_SEVERITY= +PHILIPS_HUE_API_KEY= +PHILIPS_HUE_CLOUD_ACCESS_TOKEN= +PHILIPS_HUE_CLOUD_CLIENT_ID= +PHILIPS_HUE_CLOUD_CLIENT_SECRET= +PHILIPS_HUE_CLOUD_REFRESH_TOKEN= +PHILIPS_HUE_LAN_BRIDGE_IP= +PHILIPS_HUE_LIGHT_COLOR= +PHILIPS_HUE_LIGHT_IDS= +PHILIPS_HUE_LIGHT_PATTERN= PHONE_CARRIER= PHONE_NUMBER= PLAY_SOUND= diff --git a/README.md b/README.md index 3503f71aad..07e3e88a6c 100644 --- a/README.md +++ b/README.md @@ -335,7 +335,23 @@ environment variables are **optional**._ | `PAGERDUTY_SEVERITY` | Severity of PagerDuty events | Default: `info` | +
+Philips Hue + +| Environment variable | Description | Notes | +|:---:|---|---| +| `PHILIPS_HUE_API_KEY` | Hue Bridge API Key | **Required**, generate key using instructions [here](https://developers.meethue.com/develop/get-started-2/). This will be used for both LAN and cloud access over the official Remote Hue API. | +| `PHILIPS_HUE_LAN_BRIDGE_IP` | LAN IP Address of your Hue Bridge | LAN only, e.g. `192.168.x.x`| +| `PHILIPS_HUE_LIGHT_IDS` | Light IDs | Optional (all if not supplied). Comma seperated, e.g.: `1`, `2` |See Hue App → About for IDs | +| `PHILIPS_HUE_LIGHT_COLOR` | Color in RGB Format | Optional (NVIDIA green if not supplied). Comma separated, e.g.: `255`, `255`, `255`| +| `PHILIPS_HUE_LIGHT_PATTERN` | `blink` or empty | Optional - lights will flash for 30 seconds if `blink` is supplied. | +| `PHILIPS_HUE_CLOUD_ACCESS_TOKEN` | Remote Access Token | Cloud only, the access token obtained from Philips's Remote Hue API. Instructions to generate [here](https://developers.meethue.com/develop/hue-api/remote-authentication/). | +| `PHILIPS_HUE_CLOUD_REFRESH_TOKEN` | Remote Refresh Token | Cloud only, the refresh token obtained from Philips's Remote Hue API. | +| `PHILIPS_HUE_CLOUD_CLIENT_ID` | Remote Client ID | Cloud only, the client ID to use when accessing the Remote Hue API. | +| `PHILIPS_HUE_CLOUD_CLIENT_SECRET` | Remote Client Secret | Cloud only, the client secret to use when accessing the Remote Hue API. | + +
Pushbullet @@ -414,8 +430,15 @@ environment variables are **optional**._
+ + + + + + + ## FAQ **Q: What's Node.js and how do I install it?** Visit [their website](https://nodejs.org/en/) and download and install diff --git a/package-lock.json b/package-lock.json index b8490e2d38..2ddad4e5ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1624,6 +1624,11 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -4148,6 +4153,11 @@ "integrity": "sha1-mYR1wXhEVobQsyJG2l3428++jqM=", "dev": true }, + "get-ssl-certificate": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/get-ssl-certificate/-/get-ssl-certificate-2.3.3.tgz", + "integrity": "sha512-aKYXS1S5+2IYw4W5+lKC/M+lvaNYPe0PhnQ144NWARcBg35H3ZvyVZ6y0LNGtiAxggFBHeO7LaVGO4bgHK4g1Q==" + }, "get-stdin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", @@ -5911,6 +5921,16 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, + "node-hue-api": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/node-hue-api/-/node-hue-api-4.0.9.tgz", + "integrity": "sha512-xsMUGKDSeMtYsKHSKNCn5XFq4eEArbEaFRAAccGBIlQ+ysrVKjlg1So44wY32gMgYfm3S6sJQOw2jLyPxu3Dkw==", + "requires": { + "axios": "^0.19.0", + "bottleneck": "^2.19.5", + "get-ssl-certificate": "^2.3.3" + } + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", diff --git a/package.json b/package.json index 6ac8eda0a5..b4fbb54ac0 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "messaging-api-telegram": "^1.0.1", "mqtt": "^4.2.4", "node-fetch": "^2.6.1", + "node-hue-api": "^4.0.9", "node-notifier": "^8.0.0", "node-pagerduty": "^1.3.5", "nodemailer": "^6.4.14", diff --git a/src/config.ts b/src/config.ts index c8e48555ac..e28f01d774 100644 --- a/src/config.ts +++ b/src/config.ts @@ -151,6 +151,18 @@ const notifications = { integrationKey: envOrString(process.env.PAGERDUTY_INTEGRATION_KEY), severity: envOrString(process.env.PAGERDUTY_SEVERITY, 'info') }, + philips_hue: { + accessToken: envOrString(process.env.PHILIPS_HUE_CLOUD_ACCESS_TOKEN), + apiKey: envOrString(process.env.PHILIPS_HUE_API_KEY), + bridgeIp: envOrString(process.env.PHILIPS_HUE_LAN_BRIDGE_IP), + clientId: envOrString(process.env.PHILIPS_HUE_CLOUD_CLIENT_ID), + clientSecret: envOrString(process.env.PHILIPS_HUE_CLOUD_CLIENT_SECRET), + lightColor: envOrString(process.env.PHILIPS_HUE_LIGHT_COLOR), + lightIds: envOrString(process.env.PHILIPS_HUE_LIGHT_IDS), + lightPattern: envOrString(process.env.PHILIPS_HUE_LIGHT_PATTERN), + refreshToken: envOrString(process.env.PHILIPS_HUE_CLOUD_REFRESH_TOKEN), + remoteApiUsername: envOrString(process.env.PHILIPS_HUE_API_KEY) + }, phone: { availableCarriers: new Map([ ['att', 'txt.att.net'], diff --git a/src/notification/notification.ts b/src/notification/notification.ts index 8cf9dd8534..ebb017a5ea 100644 --- a/src/notification/notification.ts +++ b/src/notification/notification.ts @@ -1,4 +1,5 @@ import {Link, Store} from '../store/model'; +import {adjustPhilipsHueLights} from './philips-hue'; import {playSound} from './sound'; import {sendDesktopNotification} from './desktop'; import {sendDiscordMessage} from './discord'; @@ -21,6 +22,7 @@ export function sendNotification(link: Link, store: Store) { sendSms(link, store); sendDesktopNotification(link, store); // Non-priority + adjustPhilipsHueLights(); sendDiscordMessage(link, store); sendMqttMessage(link, store); sendPagerDutyNotification(link, store); diff --git a/src/notification/philips-hue.ts b/src/notification/philips-hue.ts new file mode 100644 index 0000000000..b85d40fbbd --- /dev/null +++ b/src/notification/philips-hue.ts @@ -0,0 +1,108 @@ +import type Api from 'node-hue-api/lib/api/Api'; +import {config} from '../config'; +import {v3 as hueAPI} from 'node-hue-api'; +import {logger} from '../logger'; + +const hue = config.notifications.philips_hue; +const apiKey = hue.apiKey; +const bridgeIp = hue.bridgeIp; +const lightIds = hue.lightIds; +const lightColor = hue.lightColor; +const lightPattern = hue.lightPattern; +const LightState = hueAPI.lightStates.LightState; +const clientId = hue.clientId; +const clientSecret = hue.clientSecret; +const accessToken = hue.accessToken; +const refreshToken = hue.refreshToken; +const remoteApiUsername = hue.remoteApiUsername; + +// Default Light State +const lightState = new LightState() + .on(true) + .brightness(100) + .rgb(46.27, 72.55, 0); + +const adjustLightsWithAPI = (hueBridge: Api) => { + logger.debug('Connected to Philips Hue bridge.'); + // Set the custom light state (COLOR and METHOD here) + if (lightColor) { + const rgbArray = lightColor.split(','); + // If there's not three values, must not be RGB + if (rgbArray.length === 3) { + lightState.rgb(rgbArray[0], rgbArray[1], rgbArray[2]); + } else { + logger.debug('✖ Error assigning RGB Values'); + } + } + + // If blink is specified, then blink the lights + if (lightPattern === 'blink') { + lightState.alertLong(); + } + + // If we've been given light IDs, then only adjust those IDs + if (lightIds) { + const arrayOfIDs = lightIds.split(','); + arrayOfIDs.forEach(light => { + logger.debug('adjusting all hue lights'); + (hueBridge.lights.setLightState(light, lightState) as Promise).catch((error: Error) => { + logger.error('Failed to adjust all lights.'); + logger.error(error); + throw error; + }); + }); + } else { // Adjust all light IDs + hueBridge.lights.getAll().then((allLights: any[]) => { + allLights.forEach((light: any) => { + logger.debug('adjusting specified lights'); + (hueBridge.lights.setLightState(light, lightState) as Promise).catch((error: Error) => { + logger.error('Failed to adjust specified lights.'); + logger.error(error); + throw error; + }); + }); + }).catch((error: Error) => { + logger.error('Failed to get all lights.'); + logger.error(error); + throw error; + }); + } +}; + +export function adjustPhilipsHueLights() { + // Check if the required variables have been set + if (hue.apiKey && hue.bridgeIp) { + logger.info('↗ adjusting Philips Hue lights over LAN'); + (async () => { + logger.debug('Attempting to connect to Philips Hue bridge at ' + bridgeIp); + hueAPI.api.createLocal(bridgeIp).connect(apiKey).then( + hueBridge => { + adjustLightsWithAPI(hueBridge); + logger.info('✔ adjusted Philips Hue lights over LAN'); + }, + (error: Error) => { + logger.error('✖ couldn\'t adjust hue lights.', error); + }); + })(); + } else if (hue.apiKey && hue.clientId && hue.clientSecret) { + logger.info('↗ adjusting Philips Hue lights over cloud'); + (async () => { + logger.debug('Attempting to connect to Philips Hue bridge over cloud'); + const remoteBootstrap = hueAPI.api.createRemote(clientId, clientSecret); + if (hue.accessToken && hue.refreshToken) { + remoteBootstrap.connectWithTokens(accessToken, refreshToken, remoteApiUsername) + .then(hueBridge => { + adjustLightsWithAPI(hueBridge); + logger.info('✔ adjusted Philips Hue lights over cloud'); + }, + (error: Error) => { + logger.error('Failed to get a remote Philips Hue connection using supplied tokens.'); + logger.error(error); + throw error; + }); + } + })(); + } else { + logger.error('✖ couldn\'t adjust hue lights'); + } +}