Skip to content

Commit

Permalink
Merge pull request #10 from DisabledMonkey/develop
Browse files Browse the repository at this point in the history
Update to Hue v2 API
  • Loading branch information
DisabledMonkey authored Apr 6, 2024
2 parents fb5af8c + 8ab6d06 commit a967e19
Show file tree
Hide file tree
Showing 5 changed files with 762 additions and 1,610 deletions.
111 changes: 67 additions & 44 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,71 @@
const q = require('daskeyboard-applet');
const logger = q.logger;
const HueBridge = require('./lib/hue.js').HueBridge;
//const QHook = require('./lib/q-hook.js').QHook;

class HueQ extends q.DesktopApp {
constructor() {
super();
// run every 30 seconds
this.pollingInterval = 30 * 1000;
this._rooms = null;
this._room = null;
//QHook.on(this,this.toggle.bind(this));
this.pollingInterval = 1000 * 60 * 30; // only poll rarely, as we will only poll when we get events from hue
logger.info("Hue Q ready to go!");
}

async processConfig(config) {
logger.info("Process Config");
let success = await super.processConfig(config);
if (success) {
if (typeof this.config.room != 'undefined') {
logger.info("Listen for Hue Events!");
this.bridgeId = this.config.room.split('-')[0];
this.bridge = await HueBridge.find(this.bridgeId);
this.roomId = this.config.room.split('-')[1];
this.room = await this.bridge.group(this.roomId);
this.bridge.on(this.onEvent.bind(this), this.onError.bind(this)); // after we loaded config, register for hue bridge events
}
}
}

async onEvent(message) {
let events = JSON.parse(message.data);
let doRefresh = false;
if (this.room) {
for (let event of events) {
for (let item of event.data) {
if (item.owner.rtype == 'room') {
let roomId = item.id_v1.split('/').filter(i => i)[1];
if (roomId == this.room.id) {
doRefresh = true;
}
}
}
}
}
if (doRefresh) {
this.poll();
}
}

async onError(event) {
console.error(event);
}

async handleFlash() {
this.toggle(); // override flash functionality to toggle lights on/off
}

async run() {
logger.info("Hue running.");
try {
let room = await this.room();
room.refresh();
return new q.Signal({
await this.room.refresh();
let signal = {
points: this.points(),
name: 'Philips Hue',
message: this.message(),
link: {
url: '', // wish something like this could trigger applet code! possibly protocols hueq://togglelights/5 etc.
label: 'Toggle Light!'
},
isMuted: true,
});
};
logger.info(`${JSON.stringify(signal)}`);
return new q.Signal(signal);
} catch (e) {
logger.error(`Sending error signal: ${e}`);
setTimeout(this.poll.bind(this), 5000); // try again in 5 seconds if we fail!
return new q.Signal({
points: [[new q.Point('#ff0000', q.Effects.BLINK)]],
name: 'Philips Hue',
Expand All @@ -40,69 +76,56 @@ class HueQ extends q.DesktopApp {
}

message() {
return this._room.name + ' lights are ' + (this._room.state.any_on ? 'on' : 'off') + '!';
return this.room.name + ' lights are ' + (this.room.state.any_on ? 'on' : 'off') + '!';
}

points() {
return [[new q.Point(this.color())]];
}

color() {
return (this._room.state.any_on ?
(this.config.useLightColor ? this._room.color : this.config.onColor) :
this.config.offColor);
return (this.room.state.any_on ?
(this.config.useLightColor ? this.room.color : this.config.onColor) :
this.config.offColor);
}

async rooms() {
if (!this._rooms) {
logger.info("Connecting to Bridges and getting Rooms");
// get all rooms for all logged in bridges
this._rooms = [];
let bridges = await HueBridge.find();
for (let b in bridges) {
let bridge = bridges[b];
for (let bridge of bridges) {
if (bridge.loggedIn) {
let groups = await bridge.groups();
for (let g in groups) {
let group = groups[g];
for (let group of groups) {
if (group.type == 'Room') {
this._rooms.push(group);
}
}
} else {
logger.info("Bridge: " + bridge.id + ' not logged in!');
}
}
}
return this._rooms;
}

async room() {
if (!this._room) {
let rconf = this.config.room;
let r = rconf.split('-');
let bridges = await HueBridge.find();
for (let b in bridges) {
let bridge = bridges[b];
if (bridge.loggedIn && r[0]==bridge.id) {
this._room = await bridge.group(r[1]);
}
if (bridges.length == 0) {
logger.info("No Bridges Found!");
}
}
return this._room;
return this._rooms;
}

async toggle() {
let room = await this.room();
room.toggle();
this.room.toggle();
}

async options(question) {
switch (question) {
case 'room':
let options = [];
let rooms = await this.rooms();
if (rooms.length>0) {
for (let r in rooms) {
let room = rooms[r];
options.push({ key: room.bridge.id+'-'+room.id, value: room.name });
if (rooms.length > 0) {
for (let room of rooms) {
options.push({ key: room.bridge.id + '-' + room.id, value: room.name });
}
} else {
options.push({ key: 0, value: 'Please press the button on your hue bridge and start this configuration again.' });
Expand Down
134 changes: 89 additions & 45 deletions lib/hue.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,113 @@
const HueLib = require('node-hue-api');
const HueApi = HueLib.HueApi;
const v3 = require('node-hue-api').v3;
const discovery = v3.discovery;
const hueApi = v3.api;
const GroupLightState = v3.lightStates.GroupLightState;

var EventSource = require('eventsource')
var os = require("os");
const rgb = require('./rgb');
const settings = require("settings-store");
settings.init({
appName: "daskeyboard-applet--hue",
appName: "daskeyboard-applet--hue",
publisherName: "DisabledMonkey",
reverseDNS: "com.disabledmonkey.daskeyboard.hue"
});

const q = require('daskeyboard-applet');
const logger = q.logger;

class HueBridge {
constructor(obj) {
obj && Object.assign(this, obj);
this.loggedIn = false;
this.user = settings.value('bridge' + this.id,null); // if we already have registered this application with this bridge!
if (this.user) {
this.login();
} else {
// try for the next 60 seconds to register (give user time to run and hit button on their hue bridge)
this.registerLoop(60);
}
this.id = obj.config.name;
}

static async find(id) {
let bridges = [];
let hbridges = await HueLib.nupnpSearch();
for (let b in hbridges) {
let bridge = new HueBridge(hbridges[b]);
if (typeof id != 'undefined' && id==bridge.id) {
return bridge;
try {
let hbridges = await discovery.nupnpSearch();
for (let hbridge of hbridges) {
let bridge = new HueBridge(hbridge);
logger.info('Found Bridge: ' + bridge);
await bridge.init(); // try to login
if (typeof id != 'undefined' && id == bridge.id) {
return bridge;
}
bridges.push(bridge);
}
bridges.push(bridge);
} catch (e) {
logger.error(e);
}
return bridges;
}

async registerLoop(attempt,seconds) {
async init() {
this.loggedIn = false;
this.setupApi = await hueApi.createLocal(this.ipaddress).connect();
this.user = settings.value('bridge' + this.id, null); // if we already have registered this application with this bridge!
if (this.user) {
await this.login();
} else {
// try for the next 60 seconds to register (give user time to run and hit button on their hue bridge)
await this.registerLoop(60);
}
}

async registerLoop(attempt, seconds) {
if (attempt <= 0) { return false; }
attempt--;
seconds = seconds || 1000;
setTimeout(function () {
await setTimeout(function () {
if (!this.loggedIn) {
this.register();
this.registerLoop(attempt,seconds);
this.registerLoop(attempt, seconds);
}
}.bind(this), seconds);
}

async register() {
logger.info('Register Bridge: ' + this.id);
try {
let h = new HueApi();
this.user = await h.registerUser(this.ipaddress, 'Das Keyboard Q'); // need to make sure this happens shortly after the hue bridge button has been pushed.
this.login();
this.user = await this.setupApi.users.createUser('Das Keyboard Q', os.hostname()); // need to make sure this happens shortly after the hue bridge button has been pushed.
await this.login();
return true;
} catch (error) {
// just ignore errors
logger.error(error);
}
return false;
}

login() {
this.api = new HueApi(this.ipaddress, this.user);
async login() {
logger.info('Login Bridge: ' + this.id);
this.api = await hueApi.createLocal(this.ipaddress).connect(this.user.username); //new HueApi(this.ipaddress, this.user);
this.bridgeConfig = (await this.api.configuration.getConfiguration()).data;

settings.setValue('bridge' + this.id, this.user);
this.loggedIn = true;
return this;
}

async on(onmessage, onerror) {
// start loading
logger.info('Wait for server side events!');
let url = 'https://' + this.ipaddress + '/eventstream/clip/v2';
let streamConfig = {
headers: {
'hue-application-key': this.user.username,
'Accept': 'text/event-stream'
}
};
this.eventSource = new EventSource(url, streamConfig);
this.eventSource.onmessage = onmessage;
this.eventSource.onerror = onerror;
}

async groups() {
let groups = [];
try {
let allgroups = await this.api.groups();
for (let g in allgroups) {
groups.push(new HueGroup(this,allgroups[g]));
let allgroups = await this.api.groups.getAll();
for (let group of allgroups) {
groups.push(new HueGroup(this, group));
}
} catch (error) {
// just ignore errors
Expand All @@ -80,50 +117,57 @@ class HueBridge {

async group(id) {
let groups = await this.groups();
for (let g in groups) {
let group = groups[g];
if (group.id==id) {
return group;
}
}
return null;
return groups.find(group => group.id == id);
}
}

class HueGroup {
constructor(bridge,obj) {
obj && Object.assign(this, obj);
constructor(bridge, obj) {
Object.assign(this, obj);
Object.assign(this, obj.data);
this.bridge = bridge;
this.color = '#00ff00';
this.refreshRate = 5000;

}

get api() {
return this.bridge.api;
}

async light() {
return await this.api.lights.getLightAttributesAndState(this.lights[0]);
}

async scheduleRefresh() {
return await this.refresh();
}

async refresh() {
let g = await this.bridge.group(this.id);
Object.assign(this, g);
// get color from first light in group
let light = new HueLight(this.bridge,await this.api.getLightStatus(this.lights[0]));
let l = await this.light();
let light = new HueLight(this.bridge, l);
this.color = light.hex;
return this;
}

async toggle() {
this.refresh();
let state = HueLib.lightState.create();
await this.refresh();
let state = new GroupLightState();
switch (this.state.any_on) {
case true: state.off(); break;
case false: state.on(); break;
}
this.api.setGroupLightState(this.id, state);
await this.api.groups.setGroupState(this.id, state);
}
}

class HueLight {
constructor(bridge,obj) {
obj && Object.assign(this, obj);
constructor(bridge, obj) {
Object.assign(this, obj);
Object.assign(this, obj.data);
this.bridge = bridge;
}

Expand All @@ -139,7 +183,7 @@ class HueLight {
}

rgbToHex(rgb) {
return '#'+this.intToHex(rgb[0])+this.intToHex(rgb[1])+this.intToHex(rgb[2]);
return '#' + this.intToHex(rgb[0]) + this.intToHex(rgb[1]) + this.intToHex(rgb[2]);
}

intToHex(i) {
Expand Down
Loading

0 comments on commit a967e19

Please sign in to comment.