Skip to content

Commit

Permalink
rm queue
Browse files Browse the repository at this point in the history
  • Loading branch information
foxriver76 committed Oct 27, 2019
1 parent c251de5 commit effb20c
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 144 deletions.
186 changes: 44 additions & 142 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ const hue = require('node-hue-api');
const v3 = hue.v3;
const utils = require('@iobroker/adapter-core');
const hueHelper = require('./lib/hueHelper');
const Bottleneck = require('bottleneck');
const md5 = require('md5');

let adapter;
let pollingInterval;
Expand Down Expand Up @@ -53,8 +51,7 @@ function startAdapter(options) {

submitHueCmd('groups.setGroupState', {
id: 0,
data: groupState,
prio: 1
data: groupState
}, (err) => {
if (!err) {
adapter.log.info(`Started scene: ${obj.common.name}`);
Expand All @@ -78,7 +75,7 @@ function startAdapter(options) {
try {
const sensor = await api.sensors.get(channelObj.native.id);
sensor['_rawData'].config = {on: state.val};
submitHueCmd('sensors.updateSensorConfig', {prio: 5, id: sensor}, e => {
submitHueCmd('sensors.updateSensorConfig', {id: sensor}, e => {
if (!e) {
adapter.log.debug(`Changed ${dp} of sensor ${channelObj.native.id} to ${state.val}`);
} // endIf
Expand Down Expand Up @@ -476,8 +473,7 @@ function startAdapter(options) {

submitHueCmd('groups.setGroupState', {
id: groupIds[id],
data: lightState,
prio: 1
data: lightState
}, (err, result) => {
setTimeout(updateGroupState, 150, {
id: groupIds[id],
Expand All @@ -498,8 +494,7 @@ function startAdapter(options) {

submitHueCmd('lights.setLightState', {
id: channelIds[id],
data: lightState,
prio: 1
data: lightState
}, (err, result) => {
setTimeout(updateLightState, 150, {
id: channelIds[id],
Expand All @@ -517,8 +512,7 @@ function startAdapter(options) {

submitHueCmd('lights.setLightState', {
id: channelIds[id],
data: lightState,
prio: 1
data: lightState
}, (err, result) => {
setTimeout(updateLightState, 150, {
id: channelIds[id],
Expand Down Expand Up @@ -605,82 +599,53 @@ async function createUser(ip, callback) {

let api;

let groupQueue;
let lightQueue;

const channelIds = {};
const groupIds = {};
const pollLights = [];
const pollSensors = [];
const pollGroups = [];

function submitHueCmd(cmd, args, callback) {
// select the bottleneck queue to be used
let queue = lightQueue;
if (cmd === 'groups.get' || cmd === 'groups.setGroupState') {
queue = groupQueue;
}

// construct a unique id based on the command name
// and serialized arguments
const id = `${cmd}:${args.id}:${md5(JSON.stringify(args))}`;

// skip any job submit if a job with the same id already exists in the
// queue
if (queue.jobStatus(id) !== null) {
adapter.log.debug(`job ${id} already in queue, skipping..`);
return;
}

// submit the job to the bottleneck
// queue
queue.submit({priority: args.prio, expiration: 5000, id: id}, async (arg, cb) => {
if (cmd === 'getFullState') {
try {
const res = await api.configuration.getAll();
async function submitHueCmd(cmd, args, cb) {
if (cmd === 'getFullState') {
try {
const res = await api.configuration.getAll();
cb(null, res);
} catch (e) {
cb(e);
} // endCatch
} else if (args.data !== undefined) {
try {
if (cmd.split('.').length === 2) {
const cmdArr = cmd.split('.');
const res = await api[cmdArr[0]][cmdArr[1]](args.id, args.data);
cb(null, res);
} catch (e) {
cb(e);
} // endCatch
} else if (arg.data !== undefined) {
try {
if (cmd.split('.').length === 2) {
const cmdArr = cmd.split('.');
const res = await api[cmdArr[0]][cmdArr[1]](arg.id, arg.data);
cb(null, res);
} else {
const res = await api[cmd](arg.id, arg.data);
cb(null, res);
} // endElse
} catch (e) {
cb(e);
} // endCatch
} else {
try {
if (cmd.split('.').length === 2) {
const cmdArr = cmd.split('.');
const res = await api[cmdArr[0]][cmdArr[1]](arg.id);
cb(null, res);
} else {
const res = await api[cmd](arg.id);
cb(null, res);
} // endElse
} catch (e) {
cb(e);
} // endCatch
} // endElse
}, args, (err, result) => {
if (err === null && result !== false) {
adapter.log.debug(`${id} result: ${JSON.stringify(result)}`);
callback(err, result);
}
});
}
} else {
const res = await api[cmd](args.id, args.data);
cb(null, res);
} // endElse
} catch (e) {
cb(e);
} // endCatch
} else {
try {
if (cmd.split('.').length === 2) {
const cmdArr = cmd.split('.');
const res = await api[cmdArr[0]][cmdArr[1]](args.id);
cb(null, res);
} else {
const res = await api[cmd](args.id);
cb(null, res);
} // endElse
} catch (e) {
cb(e);
} // endCatch
} // endElse
} // endSubmitHueCmd

function updateGroupState(group, prio, callback) {
adapter.log.debug(`polling group ${group.name} (${group.id}) with prio ${prio}`);

submitHueCmd('groups.get', {id: group.id, prio: prio}, (err, result) => {
submitHueCmd('groups.get', {id: group.id}, (err, result) => {
const values = [];
const states = {};
result = result['_rawData'];
Expand Down Expand Up @@ -736,7 +701,7 @@ function updateGroupState(group, prio, callback) {
function updateLightState(light, prio, callback) {
adapter.log.debug(`polling light ${light.name} (${light.id}) with prio ${prio}`);

submitHueCmd('lights.getLightById', {id: parseInt(light.id), prio: prio}, (err, result) => {
submitHueCmd('lights.getLightById', {id: parseInt(light.id)}, (err, result) => {
const values = [];
const states = {};

Expand Down Expand Up @@ -1495,7 +1460,7 @@ function poll() {

adapter.log.debug('Poll all states');

submitHueCmd('getFullState', {prio: 5, id: 'getFullState'}, (err, config) => {
submitHueCmd('getFullState', {}, (err, config) => {
const values = [];
const lights = config.lights;
const sensors = config.sensors;
Expand Down Expand Up @@ -1664,72 +1629,9 @@ async function main() {
adapter.subscribeStates('*');
adapter.config.port = adapter.config.port ? parseInt(adapter.config.port, 10) : 80;

// polling interval has to be greater equal 1
// polling interval has to be greater equal 2
adapter.config.pollingInterval = parseInt(adapter.config.pollingInterval, 10) < 2 ? 2 : parseInt(adapter.config.pollingInterval, 10);

// create a bottleneck limiter to max 1 cmd per 1 sec
groupQueue = new Bottleneck({
reservoir: 1, // initial value
reservoirRefreshAmount: 1,
reservoirRefreshInterval: 250 * 4, // must be divisible by 250
minTime: 25, // wait a minimum of 25 ms between command executions
highWater: 100 // start to drop older commands if > 100 commands in the queue
});
groupQueue.on('depleted', () => {
adapter.log.debug('groupQueue full. Throttling down...');
});
groupQueue.on('error', err => {
adapter.log.error(`groupQueue error: ${err}`);
});
groupQueue.on('retry', (error, jobInfo) => {
adapter.log.warn(`groupQueue: retry [${jobInfo.retryCount + 1}/10] job ${jobInfo.options.id}`);
});
groupQueue.on('failed', async (error, jobInfo) => {
const id = jobInfo.options.id;
if (error instanceof hue.ApiError) {
adapter.log.error(`groupQueue: job ${id} failed: ${error}`);
} else if (jobInfo.retryCount >= 10) {
adapter.log.error(`groupQueue: job ${id} max retry reached: ${error}`);
if (/Api Error: resource, \/groups\/.+, not available,/.test(error)) {
// seems like a room has been deleted -> resync by restarting adapter
adapter.log.warn('Room deleted -> restarting adapter to resync');
const obj = await adapter.getForeignObjectAsync(`system.adapter.${adapter.namespace}`);
if (obj) adapter.setForeignObject(`system.adapter.${adapter.namespace}`, obj);
} // endIf
} else {
adapter.log.warn(`groupQueue: job ${id} failed: ${error}`);
return 25; // retry in 25 ms
}
});

// create a bottleneck limiter to max 10 cmd per 1 sec
lightQueue = new Bottleneck({
reservoir: 10, // initial value
reservoirRefreshAmount: 10,
reservoirRefreshInterval: 1000, // must be divisible by 250
minTime: 25, // wait a minimum of 25 ms between command executions
highWater: 1000 // start to drop older commands if > 1000 commands in the queue
});
lightQueue.on('depleted', () => {
adapter.log.debug('lightQueue full. Throttling down...');
});
lightQueue.on('error', (err) => {
adapter.log.error(`lightQueue error: ${err}`);
});
lightQueue.on('retry', (error, jobInfo) => {
adapter.log.warn(`lightQueue: retry [${jobInfo.retryCount + 1}/10] job ${jobInfo.options.id}`);
});
lightQueue.on('failed', (error, jobInfo) => {
const id = jobInfo.options.id;
if (error instanceof hue.ApiError) {
adapter.log.error(`lightQueue: job ${id} failed: ${error}`);
} else if (jobInfo.retryCount >= 10) {
adapter.log.error(`lightQueue: job ${id} max retry reached: ${error}`);
} else {
adapter.log.warn(`lightQueue: job ${id} failed: ${error}`);
return 25; // retry in 25 ms
}
});
api = await v3.api.create(adapter.config.bridge, adapter.config.user, null, null, adapter.config.port);

connect(() => {
Expand Down
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
"url": "https://github.com/iobroker-community-adapters/ioBroker.hue"
},
"dependencies": {
"bottleneck": "^2.19.5",
"node-hue-api": "^3.3.2",
"md5": "^2.2.1",
"@iobroker/adapter-core": "^1.0.3"
},
"devDependencies": {
Expand Down

4 comments on commit effb20c

@jens-maus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kann ich zu diesem commit für die 2.3.0 bitte mal nachfragen wieso das gesamte management via bottleneck wieder entfernt wurde? Hat sich irgendetwas im event management im node-hue-api paket geändert, das dieses nun die Auslieferung von events an die hue bridge anhand dessen event limits managed? Damals hatte ich ja das event management via bottleneck klasse extra in iobroker.hue eingeführt da es ansonsten recht schnell zur Überlastung der hue bridge kommen kann und somit events verloren gehen. Hat sich hier also irgendetwas geändert das wirklich das event management via bottleneck nicht mehr notwendig ist?

@foxriver76
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sehr gerne. Vorab: Erst mal ist es nur experimentell. Im Stable ist die Queue drin.

In der Vergangenheit wurde für jede Gruppe + jedes Licht im Polling Intervall eine Anfrage an die API geschossen, was anscheinend dazu geführt hat, dass die Bridge bei größeren Installationen überlastet wurde. Deshalb hat BF auch die Queue eingeführt (?) und du sie anständig implementiert.

Das Polling ist nun nicht mehr abhängig von der Anzahl an Lichtern, Gruppen. Stattdessen werden alle x (pollingInterval) Sekunden fix 2 Anfragen (das config Objekt wird gepollt, welches alle Lichter, Sensoren und Gruppen, außer die all Gruppe enthält + die all (0) Gruppe wird gepollt) abgesetzt.

Aus diesem Grund vermute ich, dass die Qeue nicht weiter benötigt wird und zudem laut mehreren Berichten zu einer im 100er ms Bereich, jedoch merklichen Verzögerung führt.

@jens-maus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bei den ursprünglich umgesetzten Queuelösungen (auch die via bottleneck) geht es doch darum, das damit die von Philips hart vorgegebenen Grenzen der Philips Hue API eingehalten werden und es nicht zu der Situation kommen soll, das die Philips Hue Bridge in ihre API Limits läuft und dann komplett für den angegebenen API Key dicht macht. Natürlich sollte das regelmäßige polling alle X sekunden submitHueCmd('getFullState') vielleicht nicht alleine dazu führen, dass die Hue bridge in die Limits läuft (man beachte allerdings die Kommentare in der node-hue-api das man getFullState eigentlich nicht regelmäßig aufrufen sollte. Siehe: https://github.com/peter-murray/node-hue-api/blob/master/hue-api/index.js#L111-L112).

Trotzdem gibt und gab es eben in der Vergangenheit sehr gute Gründe diese Queuelösungen umzusetzen, sodass ich es weiterhin für keine gute Idee halte diese Queue einfach komplett zu entfernen nur um etwaigen kleine Verzögerungen komplett aus dem weg zu gehen. Gerade bei aufwendigen Installationen mit vielen Lichtern und vielen via API getriggerten Lichtprogrammen kommt es recht schnell dazu, dass man die von Philips vorgegebenen Limits schnell erreicht und dann kommt es bei erreichen der Limits zu komischen Effekten da Events von der Bridge dann z.B. einfach ignoriert oder verworfen werden. So z.B. wenn man Lichter dann ein/ausschalten will, die Bridge aber eben das nicht mehr für einen gewissen Zeitraum zulässt und damit das an/ausschaltkommando komplett verworfen wird. Die via bottleneck umgesetzte Queue stellte aber eben sicher das die von Philips vorgegebenen Grenzen eingehalten werden und es zu solchen Effekten nicht mehr kommen sollte. Ich bleibe also dabei, dass mir das entfernen der Queue schon merkliche Bauchschmerzen verursacht, da ich weiterhin denke das man sicherstellen sollte das die von Philips vorgegebenen API Limits eingehalten werden.

@foxriver76
Copy link
Member Author

@foxriver76 foxriver76 commented on effb20c Oct 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Natürlich sollte das regelmäßige polling alle X sekunden submitHueCmd('getFullState') vielleicht nicht alleine dazu führen, dass die Hue bridge in die Limits läuft (man beachte allerdings die Kommentare in der node-hue-api das man getFullState eigentlich nicht regelmäßig aufrufen sollte. Siehe: https://github.com/peter-murray/node-hue-api/blob/master/hue-api/index.js#L111-L112).

Kann mir ehrlichgesagt nicht vorstellen, dass das annährend so ein großes Problem sein könnte, wie zu viele einzelne Anfragen zu stellen. Auch ist das Feedback von größeren Installationen nach der Umstellung sehr positiv (s. Forum). Letztendlich wird ein größeres JSON geparsed statt viele kleine. Auf unserer Seite ist das schon mal gar kein Problem und auf der Bridge Seite sollte es auch keines darstellen.

Trotzdem gibt und gab es eben in der Vergangenheit sehr gute Gründe diese Queuelösungen umzusetzen,

Ich habe mal auf die Queue Implementierung zurückgerudert. Hierzu finde ich auf die schnelle leider keine offizielle Angabe zu einer Begrenzung (hast du hierzu eine Quelle?).

Edit: Gefunden https://developers.meethue.com/support/

How many commands you can send per second?

You can send commands to the lights too fast. If you stay roughly around 10 commands per second to the /lights resource as maximum you should be fine. For /groups commands you should keep to a maximum of 1 per second.

Please sign in to comment.