diff --git a/CHANGELOG.md b/CHANGELOG.md index e75486e..577f2ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ See the [Readme file](https://github.com/jsiegenthaler/hueget/blob/master/README # Bug Fixes and Improvements +## 0.7.0 (2024-01-06) +* Added toggle capability for lights and groups +* Bumped dependencies: "axios": "^1.6.5" + ## 0.6.6 (2023-11-27) * Added some more README.md improvements diff --git a/README.md b/README.md index 48f3bec..2874b2a 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ Flash your lights in the entire house when the doorbell rings. I have a Shelly1 ## Control Hue Lights directly from Shelly Motion Sensors Anything that can call a url when triggered - such as a Shelly Motion Sensor - can be used to turn the lights on and off again. Make sure the motion sensor calls a url to turn lights on, and a url to turn lights off. The Shelly Motion Sensor is ideal for this, as you can activate call urls for different motion triggers. +## Toggle lights from a Shelly Button 1 +Toggle a light or group of lights from a button that sends a non-changing URL. The ```toggle``` command is perfect for any pushbutton controller that does not know (or can not know) the current light state, and only sends a non-changing static URL, such as a Shelly Button 1. + ## Be Home Soon Alert Flash lights in a room or in any group (zone, room) when someone comes home. The ```alert=lselect``` command is perfect to generate a 15 second long flash without any extra programming. Just call the URL from Apple HomeKit automations when a person arrives in your geofence. @@ -144,6 +147,7 @@ Examples: * Turn light 31 on at 50% brightness: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/lights/31/state?on=true&bri=50 * Turn light 31 on at 100% brightness: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/lights/31/state?on=true&bri=100 * Turn light 31 on at 100% brightness, 0.5,0.6 xy: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/lights/31/state?on=true&bri=100&xy=[0.5%2c0.6] +* Toggle light 31: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/lights/31/toggle * Identify light 31 with a single blink: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/lights/31/state?alert=select * Identify light 31 with 15 seconds of blinking: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/lights/31/state?alert=lselect @@ -151,12 +155,14 @@ Examples: ### Group 0 (a special group for all lights in your home) * Turn group 0 on: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/0/action?on=true * Turn group 0 off: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/0/action?on=false +* Toggle group 0: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/0/toggle * Identify group 0 with 15 seconds of blinking: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/0/action?alert=lselect ### Group 2 (example) * Turn group 2 on: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/2/action?on=true * Turn group 2 off: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/2/action?on=false +* Toggle group 2: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/2/toggle * Turn group 2 on at 50% brightness: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/2/action?on=true&bri=50 * Turn group 2 on at 100% brightness: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/2/action?on=true&bri=100 * Turn group 2 on at 100% brightness, 0.5,0.6 xy: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/2/state?on=true&bri=100&xy=[0.5%2c0.6] @@ -165,7 +171,15 @@ Examples: Groups are collections of lights, and are used for Rooms and Zones in the Hue app. -# Supported Keywords +## Special Commands +The hueget server supports a special toggle command, which does not exist natively in the Philips Hue bridge. This toggles (changes the state) of a specified light or a group, allowing you to toggle the light/group state with a single URL. + +Syntax: +* Toggle light 1: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/lights/1/toggle +* Toggle group 2: http://192.168.0.101:3000/api/yourPhilipsHueBridgeUsername/groups/2/toggle + + +## Supported Keywords The API is transparent to all Philips Hue keywords. It expects all name=value pairs to be separated by a comma. If any comma is required inside a value, eg: for the xy command which expects a value array, then you must url encode the comma to %2c. The full JSON response for a light looks like this: @@ -178,12 +192,12 @@ The full JSON response for a group looks like this: {"name":"Lounge","lights":["9","1","2"],"sensors":[],"type":"Room","state":{"all_on":false,"any_on":false},"recycle":false,"class":"Lounge","action":{"on":false,"bri":0,"hue":7800,"sat":138,"effect":"none","xy":[0.5302,0.392],"ct":153,"alert":"select","colormode":"xy"}} ``` The most common action keywords for state or group are: -on, bri, hue, sat, effect, xy, ct, alert, colormode, mode (lights only) +on, bri, hue, sat, effect, xy, ct, alert, colormode, mode (lights only). More keywords exist, see the [API documentation](#api-documentation). ## on (get and set) Turn a light on or off. On=true, Off=false. -Valid for light or group. +Valid for light or group. A group also supports all_on and any_on. ## bri (get and set) The brightness value to set the light to. Brightness is a scale from 1 (the minimum the light is capable of) to 254 (the maximum). @@ -247,7 +261,7 @@ See the [API documentation](#api-documentation). ## API Documentation For full details of the control capabilities, please see the [official Philips Hue API reference](https://developers.meethue.com/develop/hue-api/). -An [alternative unoffical reference](http://www.burgestrand.se/hue-api/), somewhat outdated also exists. +An [alternative unoffical reference](http://www.burgestrand.se/hue-api/), somewhat outdated, also exists. # Finding your Light or Group ids @@ -265,6 +279,6 @@ Go backwards in the text until you find the keyword **state**, this is at the st ... ,"31":{"state":{"on":true,"bri":100,"hue":65396 ... ``` -Use the same method for groups to find the group id of the room you wish to control. Note that group id 0 is a special group containing all lights in the system, and is not returned by the ‘get all groups’ command. Group 0 is not visible, and cannot be created, modified or deleted using the API. +Use the same method for groups to find the group id of the room you wish to control. Note that group id 0 is a special group containing all lights in the system, and is not returned by the ‘get all groups’ command. Group 0 is not visible, and cannot be created, modified or deleted using the API, but group 0 can be controlled by the API. diff --git a/hueget.js b/hueget.js index 31999b1..3848584 100644 --- a/hueget.js +++ b/hueget.js @@ -96,9 +96,12 @@ app.use('/api/' + options.username, (req, res) => { break; } //console.log('expectedCommand', expectedCommand ); - if (!command.startsWith(expectedCommand)) { throw errPrefix + 'unknown command "' + command + '", expecting "' + expectedCommand + '": "' + req.url + '"'; } - if (!command.includes(expectedCommand + '?')) { throw errPrefix + 'query character "?" missing in "' + command + '", expecting "' + expectedCommand + '?": "' + req.url + '"'; } - if (command.endsWith(expectedCommand + '?')) { throw errPrefix + 'query missing in "' + command + '", expecting "' + expectedCommand + '?": "' + req.url + '"'; } + // toggle is a special case, raise error for anything else that does not fit the syntax + if (command != 'toggle') { + if (!command.startsWith(expectedCommand)) { throw errPrefix + 'unknown command "' + command + '", expecting "' + expectedCommand + '": "' + req.url + '"'; } + if (!command.includes(expectedCommand + '?')) { throw errPrefix + 'query character "?" missing in "' + command + '", expecting "' + expectedCommand + '?": "' + req.url + '"'; } + if (command.endsWith(expectedCommand + '?')) { throw errPrefix + 'query missing in "' + command + '", expecting "' + expectedCommand + '?": "' + req.url + '"'; } + } } @@ -137,41 +140,91 @@ app.use('/api/' + options.username, (req, res) => { result = result + ',"' + pair[0] + '":' + pairValue; }); result = result.replace('{,','{') + '}'; // clean up, add brackets - //console.log('result', result ); + console.log('result', result ); dataObj = JSON.parse(result); } + + // if a dataObj exists, send PUT; otherwise, send a GET // GET http://192.168.0.101/api//lights/31 // PUT http://192.168.0.101/api//lights/31/state --data "{""on"":true}" var url = 'http://' + options.ip + '/api/' + options.username + '/' + resource; if (id) { url = url + '/' + id; } // add id if supplied - if (dataObj){ - console.log('sending PUT: %s %s', url + '/' + expectedCommand, dataObj || ''); - axios.put(url + '/' + expectedCommand, dataObj) + + + // special handling for toggle command, this toggles a light or group state + if (command == 'toggle') { + console.log('toggling current state') + // Get actual state + console.log('sending GET: %s', url); + axios.get(url) .then(response => { - console.log('PUT response:', response.status, response.statusText, JSON.stringify(response.data) ); - res.json(response.data); - }) + // for lights /lights/ state = on true/false + // for groups, /groups/ state = all_on true/false + switch(resource) { + case 'lights': + console.log('GET response:', response.status, response.statusText, "state:on="+response.data["state"]["on"] ); + state = !response.data["state"]["on"] // get the current on state , as a boolean, and invert it + expectedCommand = 'state' + break; + case 'groups': + console.log('GET response:', response.status, response.statusText, "state:all_on="+response.data["state"]["all_on"] ); + state = !response.data["state"]["all_on"] // get the current all_on state, as a boolean, and invert it + expectedCommand = 'action' + break; + } + // toggle light or group state + // lights: http://localhost:3000/api//lights/31/state?on=true + // groups: http://localhost:3000/api//groups/0/action?on=true + console.log('sending PUT: %s%s', url + '/' + "state?on=", state.toString() || ''); + axios.put(url + '/' + expectedCommand,'{"on":' + state.toString() + '}') + .then(response => { + console.log('PUT response:', response.status, response.statusText, JSON.stringify(response.data) ); + res.json(response.data); + }) + .catch(error => { + const errText = error.syscall + ' ' + error.code + ' ' + error.address + ':' + error.port; + console.log('PUT error:', errText); + res.json({ error: errText }); + }); + }) .catch(error => { const errText = error.syscall + ' ' + error.code + ' ' + error.address + ':' + error.port; - console.log('PUT error:', errText); + console.log('GET error:', errText); res.json({ error: errText }); }); - } else { - console.log('sending GET: %s', url); - axios.get(url) + + + // normal handling for non-toggle commands + } else { + if (dataObj){ + console.log('sending PUT: %s %s', url + '/' + expectedCommand, dataObj || ''); + axios.put(url + '/' + expectedCommand, dataObj) .then(response => { - console.log('GET response:', response.status, response.statusText, JSON.stringify(response.data) ); + console.log('PUT response:', response.status, response.statusText, JSON.stringify(response.data) ); res.json(response.data); }) .catch(error => { const errText = error.syscall + ' ' + error.code + ' ' + error.address + ':' + error.port; - console.log('GET error:', errText); + console.log('PUT error:', errText); res.json({ error: errText }); }); + } else { + console.log('sending GET: %s', url); + axios.get(url) + .then(response => { + console.log('GET response:', response.status, response.statusText, JSON.stringify(response.data) ); + res.json(response.data); + }) + .catch(error => { + const errText = error.syscall + ' ' + error.code + ' ' + error.address + ':' + error.port; + console.log('GET error:', errText); + res.json({ error: errText }); + }); } + } return; diff --git a/package-lock.json b/package-lock.json index 273c8ca..810ca7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "hueget", - "version": "0.6.6", + "version": "0.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "hueget", - "version": "0.6.6", + "version": "0.7.0", "license": "ISC", "dependencies": { - "axios": "^1.6.2", + "axios": "^1.6.5", "express": "^4.18.2", "stdio": "^2.1.1" }, @@ -40,11 +40,11 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -253,9 +253,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "funding": [ { "type": "individual", @@ -691,11 +691,11 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -860,9 +860,9 @@ } }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==" }, "form-data": { "version": "4.0.0", diff --git a/package.json b/package.json index 027bf23..9ed5654 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hueget", - "version": "0.6.6", + "version": "0.7.0", "description": "A simple API that converts the Philips Hue API PUT to GET requests, allowing control of Philips Hue lights via http GET requests", "main": "hueget.js", "scripts": { @@ -25,7 +25,7 @@ "author": "Jochen Siegenthaler (https://github.com/jsiegenthaler)", "license": "ISC", "dependencies": { - "axios": "^1.6.2", + "axios": "^1.6.5", "express": "^4.18.2", "stdio": "^2.1.1" },