UNPKG

iobroker.hue

Version:

Connects Philips Hue LED Bulbs, Friends of Hue LED Lamps and Stripes and other SmartLink capable Devices (LivingWhites, some LivingColors) via Philips Hue Bridges

1,195 lines 116 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); /** * * ioBroker Philips Hue Bridge Adapter * * Copyright (c) 2017-2023 Bluefox <dogafox@gmail.com> * Copyright (c) 2014-2016 hobbyquaker * Apache License * */ const node_hue_api_1 = require("node-hue-api"); const utils = __importStar(require("@iobroker/adapter-core")); const hueHelper = __importStar(require("./lib/hueHelper")); const tools = __importStar(require("./lib/tools")); const GroupState_1 = __importDefault(require("node-hue-api/lib/model/lightstate/GroupState")); const hue_push_client_1 = __importDefault(require("hue-push-client")); const v2_client_1 = require("./lib/v2/v2-client"); const constants_1 = require("./lib/constants"); /** IDs currently blocked from polling */ const blockedIds = {}; /** Map ioBroker channel to light id */ const channelIds = {}; /** Map ioBroker group name to group id */ const groupIds = {}; /** Existing lights on API */ const pollLights = []; /** Existing sensors on API */ const pollSensors = []; /** Existing groups on API */ const pollGroups = []; let noDevices; const SUPPORTED_SENSORS = [ 'ZLLSwitch', 'ZGPSwitch', 'Daylight', 'ZLLTemperature', 'ZLLPresence', 'ZLLLightLevel', 'ZLLRelativeRotary' ]; const SOFTWARE_SENSORS = ['CLIPGenericStatus', 'CLIPGenericFlag']; class Hue extends utils.Adapter { constructor(options = {}) { super({ ...options, name: 'hue' }); /** Object which contains all UUIDs and the corresponding metadata */ this.UUIDs = {}; /** Time to wait before between setting and polling group state */ this.GROUP_UPDATE_DELAY_MS = 150; this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { this.subscribeStates('*'); this.config.port = this.config.port ? Math.round(this.config.port) : 80; if (this.config.syncSoftwareSensors) { for (const softwareSensor of SOFTWARE_SENSORS) { SUPPORTED_SENSORS.push(softwareSensor); } } // polling interval has to be greater equal 2 this.config.pollingInterval = Math.round(this.config.pollingInterval) < 2 ? 2 : Math.round(this.config.pollingInterval); if (!this.config.bridge) { this.log.warn(`No bridge configured yet - please configure the adapter first`); return; } await this.connect(); if (this.config.ssl) { this.clientV2 = new v2_client_1.HueV2Client({ user: this.config.user, address: this.config.bridge }); try { await this.syncSmartScenes(); } catch (e) { this.log.warn(`Could not create smart scenes: ${e.message}`); } try { await this.syncContactSensors(); } catch (e) { this.log.warn(`Could not create contact scenes: ${e.message}`); } } if (this.config.polling) { this.poll(); } } /** * Creates contact sensors and deletes no longer existing ones */ async syncContactSensors() { var _a, _b; const contactSensors = await this.clientV2.getContactSensors(); const res = await this.getObjectViewAsync('system', 'state', { startkey: this.namespace, endkey: `${this.namespace}\u9999` }); for (const row of res.rows) { if (((_b = (_a = row.value.native) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.type) !== 'contact') { continue; } const contactData = row.value.native.data; const contactSensorId = contactData.id; const sensorExistsInBridge = contactSensors.data.some(contactSensor => contactSensor.id === contactSensorId); if (!sensorExistsInBridge) { const deviceId = contactData.owner.rid; this.log.info(`Deleted contact sensor "${deviceId}"`); await this.delObjectAsync(deviceId, { recursive: true }); } } for (const contactSensor of contactSensors.data) { const deviceId = contactSensor.owner.rid; const device = await this.clientV2.getDevice(deviceId); const deviceData = device.data[0]; await this.extendObjectAsync(deviceId, { type: 'device', common: { name: deviceData.metadata.name }, native: { data: deviceData } }); await this.extendObjectAsync(`${deviceId}.${contactSensor.id}`, { type: 'state', common: { name: 'Contact State', type: 'boolean', role: 'sensor.contact', write: false, read: true }, native: { data: contactSensor } }); await this.setStateAsync(`${deviceId}.${contactSensor.id}`, this.contactToStateVal(contactSensor.contact_report.state), true); for (const service of deviceData.services) { await this.createService(deviceId, service); } } } /** * Create state for given service * * @param deviceId id of the device * @param resource the resource to create a state for */ async createService(deviceId, resource) { if (resource.rtype === 'device_power') { const devicePowerResponse = await this.clientV2.getDevicePower(resource.rid); const devicePowerData = devicePowerResponse.data[0]; await this.extendObjectAsync(`${deviceId}.${resource.rid}`, { type: 'state', common: { name: 'Battery Level', type: 'number', role: 'value.battery', write: false, read: true, unit: '%' }, native: { data: devicePowerData } }); await this.setStateAsync(`${deviceId}.${resource.rid}`, devicePowerData.power_state.battery_level, true); return; } if (resource.rtype === 'tamper') { const tamperStateResponse = await this.clientV2.getTamperState(resource.rid); const tamperData = tamperStateResponse.data[0]; await this.extendObjectAsync(`${deviceId}.${resource.rid}`, { type: 'state', common: { name: 'Tamper Alarm', type: 'boolean', role: 'sensor.alarm', write: false, read: true, def: false }, native: { data: tamperData } }); if (tamperData.tamper_reports.length > 0) { await this.setStateAsync(`${deviceId}.${resource.rid}`, this.tamperToStateVal(tamperData.tamper_reports[0].state), true); } return; } this.log.debug(`Do not create service for "${resource.rtype}"`); } /** * Convert contact sensor string to boolean (note, that open means true) * * @param contactState contact state from HUE API */ contactToStateVal(contactState) { return contactState === 'no_contact'; } /** * Convert tamper state to ioBroker state value, true means tampered * * @param tamperState tamper state from HUE API */ tamperToStateVal(tamperState) { return tamperState === 'tampered'; } /** * Creates smart scenes for existing groups and deletes no longer existing ones */ async syncSmartScenes() { var _a, _b; const scenesData = await this.clientV2.getSmartScenes(); const res = await this.getObjectViewAsync('system', 'state', { startkey: this.namespace, endkey: `${this.namespace}\u9999` }); for (const row of res.rows) { if (((_b = (_a = row.value.native) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.type) !== 'smart_scene') { continue; } const smartSceneId = row.value.native.data.id; const sceneExistsInBridge = scenesData.data.some(smartScene => smartScene.id === smartSceneId); if (!sceneExistsInBridge) { this.log.info(`Deleted smart scene "${smartSceneId}"`); const groupUuid = row.value.native.data.group.rid; await this.delObjectAsync(`${groupUuid}.${smartSceneId}`); // check if group is now empty const res = await this.getObjectViewAsync('system', 'state', { startkey: `${this.namespace}.${groupUuid}.`, endkey: `${this.namespace}.${groupUuid}.\u9999` }); if (res.rows.length === 0) { await this.delObjectAsync(groupUuid); } } } for (const sceneData of scenesData.data) { const groupUuid = sceneData.group.rid; const isGroup = sceneData.group.rtype === 'room'; let groupOrZoneData; if (isGroup) { groupOrZoneData = await this.clientV2.getRoom(groupUuid); } else { groupOrZoneData = await this.clientV2.getZone(groupUuid); } await this.extendObjectAsync(groupUuid, { type: 'channel', common: { name: groupOrZoneData.data[0].metadata.name }, native: { data: groupOrZoneData.data } }); await this.extendObjectAsync(`${groupUuid}.${sceneData.id}`, { type: 'state', common: { name: sceneData.metadata.name, type: 'boolean', role: 'switch', write: true, read: true }, native: { data: sceneData } }); } } /** * Is called when adapter shuts down - callback has to be called under any circumstances! * @param callback */ async onUnload(callback) { try { if (this.pollingInterval) { clearTimeout(this.pollingInterval); this.pollingInterval = undefined; } if (this.reconnectTimeout) { clearTimeout(this.reconnectTimeout); this.reconnectTimeout = undefined; } this.pushClient.close(); await this.setStateAsync('info.connection', false, true); this.log.info('cleaned everything up...'); callback(); } catch (_a) { callback(); } } /** * Handle messages from frontend * * @param obj the received message */ async onMessage(obj) { if (obj) { switch (obj.command) { case 'browse': { const timeout = obj.message.timeout; const res = await this.browse(timeout); if (obj.callback) { this.sendTo(obj.from, obj.command, res, obj.callback); } break; } case 'createUser': { const res = await this.createUser(obj.message.ip, obj.message.port); if (obj.callback) { if (res.error === 0) { this.sendTo(obj.from, obj.command, { user: res.message }, obj.callback); } else if (res.error === 403) { this.sendTo(obj.from, obj.command, { error: 'Not open' }, obj.callback); } else { this.sendTo(obj.from, obj.command, { error: 'Unknown error' }, obj.callback); } } break; } default: this.log.warn(`Unknown command: ${obj.command}`); if (obj.callback) { this.sendTo(obj.from, obj.command, obj.message, obj.callback); } break; } } } /** * Is called if a subscribed state changes * @param id * @param state */ async onStateChange(id, state) { var _a, _b, _c, _d; if (!id || !state || state.ack) { return; } this.log.debug(`stateChange ${id} ${JSON.stringify(state)}`); const tmp = id.split('.'); let dp = tmp.pop(); let stateObj; try { stateObj = await this.getForeignObjectAsync(id); } catch (e) { this.log.error(`Could not get object "${id}" on stateChange: ${e.message}`); return; } if (((_b = (_a = stateObj === null || stateObj === void 0 ? void 0 : stateObj.native) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.type) === 'smart_scene') { try { const uuid = stateObj.native.data.id; if (state.val) { this.log.info(`Start smart scene "${stateObj.common.name}"`); await this.clientV2.startSmartScene(uuid); } else { this.log.info(`Stop smart scene "${stateObj.common.name}"`); await this.clientV2.stopSmartScene(uuid); } } catch (e) { this.log.error(`Could not start smart scene "${stateObj.common.name}": ${e.message}`); } return; } if (dp.startsWith('scene_')) { try { // it's a scene -> get a scene id to start it const groupState = new node_hue_api_1.v3.lightStates.GroupLightState(); if (!stateObj) { throw new Error(`Object "${id}" is not existing`); } groupState.scene(stateObj.native.id); await this.api.groups.setGroupState(0, groupState); this.log.info(`Started scene: ${stateObj.common.name}`); } catch (e) { this.log.error(`Could not start scene: ${e.message || e}`); } return; } // check if it is a sensor const channelId = id.substring(0, id.lastIndexOf('.')); let channelObj; try { channelObj = await this.getForeignObjectAsync(channelId); } catch (e) { this.log.error(`Cannot get channelObj on stateChange for id "${id}" (${channelId}): ${e.message}`); return; } if (((_c = channelObj === null || channelObj === void 0 ? void 0 : channelObj.common) === null || _c === void 0 ? void 0 : _c.role) && SUPPORTED_SENSORS.includes(channelObj.common.role)) { // it's a sensor - we support turning it on and off try { if (dp === 'on') { const sensor = await this.api.sensors.get(channelObj.native.id); // @ts-expect-error is there are more official way? sensor._data.config = { on: state.val }; await this.api.sensors.updateSensorConfig(sensor); this.log.debug(`Changed ${dp} of sensor ${channelObj.native.id} to ${state.val}`); } else if (dp === 'status') { const sensor = await this.api.sensors.get(channelObj.native.id); // @ts-expect-error types are suboptimal sensor.status = parseInt(state.val); // @ts-expect-error types are suboptimal await this.api.sensors.updateSensorState(sensor); this.log.debug(`Changed ${dp} of sensor ${channelObj.native.id} to ${state.val}`); } else if (dp === 'flag') { const sensor = await this.api.sensors.get(channelObj.native.id); // @ts-expect-error types are suboptimal sensor.flag = state.val; // @ts-expect-error types are suboptimal await this.api.sensors.updateSensorState(sensor); this.log.debug(`Changed ${dp} of sensor ${channelObj.native.id} to ${state.val}`); } else { this.log.warn(`Changed ${dp} of sensor ${channelObj.native.id} to ${state.val} - currently not supported`); } } catch (e) { this.log.error(`Cannot update sensor ${channelObj.native.id}: ${e.message}`); } return; } id = tmp.slice(2).join('.'); // Enable/Disable streaming of Entertainment if (dp === 'activeStream') { if (state.val) { // turn streaming on this.log.debug(`Enable streaming of ${id} (${groupIds[id]})`); await this.api.groups.enableStreaming(groupIds[id]); } else { //turn streaming off this.log.debug(`Disable streaming of ${id} (${groupIds[id]})`); await this.api.groups.disableStreaming(groupIds[id]); } return; } // anyOn and allOn will just act like on dp if (dp === 'anyOn' || dp === 'allOn') { dp = 'on'; } const fullIdBase = `${tmp.join('.')}.`; // if .on changed instead change .bri to 254 or 0, except it is a switch that has no brightness let bri = 0; if (dp === 'on' && !this.config.nativeTurnOffBehaviour && !(channelObj && channelObj.common && channelObj.common.role === 'switch')) { bri = state.val ? 254 : 0; await this.setStateAsync([id, 'bri'].join('.'), { val: bri, ack: false }); return; } // if .level changed instead change .bri to level.val*254 if (dp === 'level' && typeof state.val === 'number') { bri = hueHelper.levelToBrightness(state.val); await this.setStateAsync([id, 'bri'].join('.'), { val: bri, ack: false }); return; } // get lamp states let idStates; try { idStates = await this.getStatesAsync(`${id}.*`); } catch (e) { this.log.error(e); return; } // gather states that need to be changed const ls = {}; const alls = {}; let finalLS = {}; let lampOn = false; let commandSupported = false; /** * Sets the light states and all light states according to the current state values * @param idState - state id * @param prefill - prefill requires ack of state to be true else it returns immediately */ const handleParam = (idState, prefill) => { if (!idStates[idState]) { return; } if (prefill && !idStates[idState].ack) { return; } const idtmp = idState.split('.'); const iddp = idtmp.pop(); switch (iddp) { case 'on': alls.bri = idStates[idState].val ? 254 : 0; ls.bri = idStates[idState].val ? 254 : 0; if (idStates[idState].ack && ls.bri > 0) { lampOn = true; } break; case 'bri': alls.bri = idStates[idState].val; ls.bri = idStates[idState].val; // @ts-expect-error check it if (idStates[idState].ack && idStates[idState].val > 0) { lampOn = true; } break; case 'alert': alls.alert = idStates[idState].val; if (dp === 'alert') { ls.alert = idStates[idState].val; } break; case 'effect': alls[iddp] = idStates[idState].val; if (dp === 'effect') { ls[iddp] = idStates[idState].val; } break; case 'r': case 'g': case 'b': alls[iddp] = idStates[idState].val; if (dp === 'r' || dp === 'g' || dp === 'b') { ls[iddp] = idStates[idState].val; } break; case 'ct': alls[iddp] = idStates[idState].val; if (dp === 'ct') { ls[iddp] = idStates[idState].val; } break; case 'hue': case 'sat': alls[iddp] = idStates[idState].val; if (dp === 'hue' || dp === 'sat') { ls[iddp] = idStates[idState].val; } break; case 'xy': alls[iddp] = idStates[idState].val; if (dp === 'xy') { ls[iddp] = idStates[idState].val; } break; case 'command': commandSupported = true; alls[iddp] = idStates[idState].val; break; default: alls[iddp] = idStates[idState].val; break; } idStates[idState].handled = true; }; // work through the relevant states in the correct order for the logic to work // but only if ack=true - so real values from device handleParam(`${fullIdBase}on`, true); handleParam(`${fullIdBase}bri`, true); handleParam(`${fullIdBase}ct`, true); handleParam(`${fullIdBase}alert`, true); handleParam(`${fullIdBase}effect`, true); handleParam(`${fullIdBase}colormode`, true); handleParam(`${fullIdBase}r`, true); handleParam(`${fullIdBase}g`, true); handleParam(`${fullIdBase}b`, true); handleParam(`${fullIdBase}hue`, true); handleParam(`${fullIdBase}sat`, true); handleParam(`${fullIdBase}xy`, true); handleParam(`${fullIdBase}command`, true); handleParam(`${fullIdBase}level`, true); // Walk through the rest or ack=false (=to be changed) values for (const idState in idStates) { if (!idStates[idState] || idStates[idState].val === null || idStates[idState].handled) { continue; } handleParam(idState, false); } let sceneId; // Handle commands at the end because they overwrite also anything if (commandSupported && dp === 'command') { try { const commands = JSON.parse(state.val); if (typeof commands.scene === 'string') { // we need to get the id of the scene - try the object scene-tree first let sceneObj = await this.getObjectAsync(`${channelId}.scene_${commands.scene.toLowerCase()}`); // if no id could be obtained, try the global scene-tree if (sceneObj === null) { sceneObj = await this.getObjectAsync(`${this.namespace}.lightScenes.scene_${commands.scene.toLowerCase()}`); } if (sceneObj === null || sceneObj === void 0 ? void 0 : sceneObj.native) { sceneId = sceneObj.native.id; } } for (const command of Object.keys(commands)) { if (command === 'on') { // if on is the only command and nativeTurnOn is activated if (Object.keys(commands).length === 1 && this.config.nativeTurnOffBehaviour) { finalLS.on = !!commands[command]; // we can set finalLs directly } else { // convert on to bri if (commands[command] && !Object.prototype.hasOwnProperty.call(commands, 'bri')) { ls.bri = 254; } else { ls.bri = 0; } } } else if (command === 'level') { //convert level to bri if (!Object.prototype.hasOwnProperty.call(commands, 'bri')) { ls.bri = hueHelper.levelToBrightness(parseInt(commands[command])); } else { ls.bri = 254; } } else { ls[command] = commands[command]; } } } catch (e) { this.log.error(e.message); return; } } // maybe someone emitted a state change for a non-existing device via script if (!((_d = channelObj === null || channelObj === void 0 ? void 0 : channelObj.common) === null || _d === void 0 ? void 0 : _d.role)) { this.log.error(`Object "${id}" on stateChange is null, undefined or corrupted`); return; } // apply rgb to xy with modelId if ('r' in ls || 'g' in ls || 'b' in ls) { if (!('r' in ls) || ls.r > 255 || ls.r < 0 || typeof ls.r !== 'number') { ls.r = 0; } if (!('g' in ls) || ls.g > 255 || ls.g < 0 || typeof ls.g !== 'number') { ls.g = 0; } if (!('b' in ls) || ls.b > 255 || ls.b < 0 || typeof ls.b !== 'number') { ls.b = 0; } const xyb = hueHelper.RgbToXYB(ls.r / 255, ls.g / 255, ls.b / 255, Object.prototype.hasOwnProperty.call(channelObj.native, 'modelid') ? channelObj.native.modelid.trim() : 'default'); ls.bri = xyb.b; ls.xy = `${xyb.x},${xyb.y}`; } // create lightState from ls and check values let lightState = /(LightGroup)|(Room)|(Zone)|(Entertainment)/g.test(channelObj.common.role) ? new node_hue_api_1.v3.lightStates.GroupLightState() : new node_hue_api_1.v3.lightStates.LightState(); if (parseInt(ls.bri) > 0) { const bri = Math.min(254, ls.bri); if (isNaN(bri)) { throw new Error(`Error on converting value for bri: ${bri} - ${ls.bri} (${typeof ls.bri})`); } lightState = lightState.bri(bri); finalLS.bri = bri; // if nativeTurnOnOffBehaviour -> only turn a group on if no lamp is on yet on brightness change if (!this.config.nativeTurnOffBehaviour || !alls['anyOn']) { finalLS.on = true; lightState = lightState.on(true); } } else { lightState = lightState.off(); finalLS.bri = 0; finalLS.on = false; } if ('xy' in ls) { if (typeof ls.xy !== 'string') { if (ls.xy) { ls.xy = ls.xy.toString(); } else { this.log.warn(`Invalid xy value: "${ls.xy}"`); ls.xy = '0,0'; } } let xy = ls.xy.toString().split(','); xy = { x: xy[0], y: xy[1] }; xy = hueHelper.GamutXYforModel(xy.x, xy.y, Object.prototype.hasOwnProperty.call(channelObj.native, 'modelid') ? channelObj.native.modelid.trim() : 'default'); if (!xy) { this.log.error(`Invalid "xy" value "${state.val}" for id "${id}"`); return; } finalLS.xy = `${xy.x},${xy.y}`; lightState = lightState.xy(parseFloat(xy.x), parseFloat(xy.y)); if (!lampOn && (!('bri' in ls) || ls.bri === 0)) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } const rgb = hueHelper.XYBtoRGB(xy.x, xy.y, finalLS.bri / 254); finalLS.r = Math.round(rgb.Red * 254); finalLS.g = Math.round(rgb.Green * 254); finalLS.b = Math.round(rgb.Blue * 254); } if ('ct' in ls) { if (typeof ls.ct !== 'number') { this.log.error(`Invalid "ct" value "${state.val}" (type: ${typeof ls.ct}) for id "${id}"`); return; } finalLS.ct = Math.max(constants_1.MIN_CT, Math.min(constants_1.MAX_CT, ls.ct)); finalLS.ct = hueHelper.miredToKelvin(finalLS.ct); lightState = lightState.ct(finalLS.ct); if (!lampOn && (!('bri' in ls) || ls.bri === 0) && this.config.turnOnWithOthers) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } } if ('hue' in ls) { if (typeof ls.hue !== 'number') { this.log.error(`Invalid "hue" value "${state.val}" (type: ${typeof ls.hue}) for id "${id}"`); return; } finalLS.hue = Math.min(ls.hue, 360); if (finalLS.hue < 0) { finalLS.hue = 360; } // Convert 360° into 0-65535 value finalLS.hue = Math.round((finalLS.hue / 360) * 65535); if (finalLS.hue > 65535) { // may be round error finalLS.hue = 65535; } lightState = lightState.hue(finalLS.hue); if (!lampOn && (!('bri' in ls) || ls.bri === 0) && this.config.turnOnWithOthers) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } } if ('sat' in ls) { finalLS.sat = Math.max(0, Math.min(254, ls.sat)) || 0; lightState = lightState.sat(finalLS.sat); if (!lampOn && (!('bri' in ls) || ls.bri === 0) && this.config.turnOnWithOthers) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } } if ('alert' in ls) { if (['select', 'lselect'].indexOf(ls.alert) === -1) { finalLS.alert = 'none'; } else { finalLS.alert = ls.alert; } lightState = lightState.alert(finalLS.alert); } if ('effect' in ls) { finalLS.effect = ls.effect ? 'colorloop' : 'none'; lightState = lightState.effect(finalLS.effect); if (!lampOn && ((finalLS.effect !== 'none' && !('bri' in ls)) || ls.bri === 0) && this.config.turnOnWithOthers) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } } // only available in command state if ('transitiontime' in ls) { const transitiontime = Math.max(0, Math.min(65535, parseInt(ls.transitiontime))); if (!isNaN(transitiontime)) { finalLS.transitiontime = transitiontime; lightState = lightState.transitiontime(transitiontime); } } if ('sat_inc' in ls && !('sat' in finalLS) && 'sat' in alls) { finalLS.sat = (((ls.sat_inc + alls.sat) % 255) + 255) % 255; if (!lampOn && (!('bri' in ls) || ls.bri === 0) && this.config.turnOnWithOthers) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } lightState = lightState.sat(finalLS.sat); } if ('hue_inc' in ls && !('hue' in finalLS) && 'hue' in alls) { alls.hue = alls.hue % 360; if (alls.hue < 0) { alls.hue += 360; } // Convert 360° into 0-65535 value alls.hue = (alls.hue / 360) * 65535; if (alls.hue > 65535) { // may be round error alls.hue = 65535; } finalLS.hue = (((ls.hue_inc + alls.hue) % 65536) + 65536) % 65536; if (!lampOn && (!('bri' in ls) || ls.bri === 0) && this.config.turnOnWithOthers) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } lightState = lightState.hue(finalLS.hue); } if ('ct_inc' in ls && !('ct' in finalLS) && 'ct' in alls) { alls.ct = 500 - 153 - ((alls.ct - constants_1.MIN_CT) / (constants_1.MAX_CT - constants_1.MIN_CT)) * (500 - 153) + 153; finalLS.ct = ((((alls.ct - 153 + ls.ct_inc) % 348) + 348) % 348) + 153; if (!lampOn && (!('bri' in ls) || ls.bri === 0) && this.config.turnOnWithOthers) { lightState = lightState.on(true); lightState = lightState.bri(254); finalLS.bri = 254; finalLS.on = true; } lightState = lightState.ct(finalLS.ct); } if ('bri_inc' in ls) { finalLS.bri = (((parseInt(alls.bri, 10) + parseInt(ls.bri_inc, 10)) % 255) + 255) % 255; if (finalLS.bri === 0) { if (lampOn) { lightState = lightState.on(false); finalLS.on = false; } else { this.setState([id, 'bri'].join('.'), { val: 0, ack: false }); return; } } else { finalLS.on = true; lightState = lightState.on(true); } lightState = lightState.bri(finalLS.bri); } // change colormode if ('xy' in finalLS) { finalLS.colormode = 'xy'; } else if ('ct' in finalLS) { finalLS.colormode = 'ct'; } else if ('hue' in finalLS || 'sat' in finalLS) { finalLS.colormode = 'hs'; } // set level to final bri / 2.54 if ('bri' in finalLS) { finalLS.level = Math.max(Math.min(Math.round(finalLS.bri / 2.54), 100), 0); } // if dp is on, and we use native turn-off behaviour only set the lightState if (dp === 'on' && this.config.nativeTurnOffBehaviour) { // todo: this is somehow dirty but the code above is messy -> integrate above in a more clever way later lightState = /(LightGroup)|(Room)|(Zone)|(Entertainment)/g.test(channelObj.common.role) ? new node_hue_api_1.v3.lightStates.GroupLightState() : new node_hue_api_1.v3.lightStates.LightState(); if (state.val) { lightState.on(true); } else { lightState.off(); } } // this can only happen for cmd - groups if (sceneId !== undefined && lightState instanceof GroupState_1.default) { lightState.scene(sceneId); } blockedIds[id] = true; if (!this.config.ignoreGroups && /(LightGroup)|(Room)|(Zone)|(Entertainment)/g.test(channelObj.common.role)) { // log final changes / states this.log.debug(`final lightState for ${channelObj.common.name}:${JSON.stringify(finalLS)}`); try { await this.api.groups.setGroupState(groupIds[id], lightState); await this.delay(this.GROUP_UPDATE_DELAY_MS); await this.updateGroupState({ id: groupIds[id], name: channelObj._id.substring(this.namespace.length + 1) }); this.log.debug(`updated group state (${groupIds[id]}) after change`); } catch (e) { this.log.error(`Could not set GroupState of ${channelObj.common.name}: ${e.message}`); } } else if (channelObj.common.role === 'switch') { if (Object.prototype.hasOwnProperty.call(finalLS, 'on')) { finalLS = { on: finalLS.on }; // log final changes / states this.log.debug(`final lightState for ${channelObj.common.name}:${JSON.stringify(finalLS)}`); lightState = new node_hue_api_1.v3.lightStates.LightState(); lightState.on(finalLS.on); try { await this.api.lights.setLightState(channelIds[id], lightState); await this.updateLightState({ id: channelIds[id], name: channelObj._id.substring(this.namespace.length + 1) }); this.log.debug(`updated LightState (${channelIds[id]}) after change`); } catch (e) { this.log.error(`Could not set LightState of ${channelObj.common.name}: ${e.message}`); } } else { this.log.warn('invalid switch operation'); } } else { // log final changes / states this.log.debug(`final lightState for ${channelObj.common.name}:${JSON.stringify(finalLS)}`); try { await this.api.lights.setLightState(channelIds[id], lightState); await this.updateLightState({ id: channelIds[id], name: channelObj._id.substring(this.namespace.length + 1) }); this.log.debug(`updated LightState (${channelIds[id]}) after change`); } catch (e) { this.log.error(`Could not set LightState of ${channelObj.common.name}: ${e.message}`); } } } /** * Search for bridges via upnp and nupnp * * @param timeout - timeout to abort the search */ async browse(timeout) { if (isNaN(timeout)) { timeout = 5000; } let res1 = []; let res2 = []; // methods can throw timeout error try { res1 = await node_hue_api_1.v3.discovery.upnpSearch(timeout); } catch (e) { this.log.error(`Error on browsing via UPNP: ${e.message}`); } try { res2 = await node_hue_api_1.v3.discovery.nupnpSearch(); } catch (e) { this.log.error(`Error on browsing via NUPNP: ${e.message}`); } const bridges = res1.concat(res2); const ips = []; // rm duplicates - reverse because splicing for (let i = bridges.length - 1; i >= 0; i--) { if (ips.includes(bridges[i].ipaddress)) { bridges.splice(i, 1); } else { ips.push(bridges[i].ipaddress); } } const ipsWithLabels = ips.map(ip => ({ value: ip, label: ip })); return ipsWithLabels; } /** * Create user on the bridge by given Ip * * @param ip - ip address of the bridge * @param port - port of the bridge */ async createUser(ip, port) { const deviceName = 'ioBroker.hue'; try { const api = this.config.ssl ? await node_hue_api_1.v3.api.createLocal(ip, port).connect() : // @ts-expect-error third party types are incorrect await node_hue_api_1.v3.api.createInsecureLocal(ip, port).connect(); const newUser = await api.users.createUser(ip, deviceName); this.log.info(`created new User: ${newUser.username}`); return { error: 0, message: newUser.username }; } catch (e) { // 101 is bridge button not pressed if (!e.getHueErrorType || e.getHueErrorType() !== 101) { this.log.error(e.message); } // we see error as an error code only to detect 101, we do not use whole e here, // because it seems to be a circular structure sometimes return { error: e.getHueErrorType ? e.getHueErrorType() : -1, message: e.getHueErrorMessage ? e.getHueErrorMessage() : e.message }; } } /** * polls the given group and sets states accordingly * * @param group group object containing id and name of the group */ async updateGroupState(group) { this.log.debug(`polling group ${group.name} (${group.id})`); const values = []; try { let result = await this.api.groups.getGroup(group.id); const states = {}; result = result['_data']; for (const stateA of Object.keys(result.action)) { states[stateA] = result.action[stateA]; } // add the anyOn State states.anyOn = result.state['any_on']; states.allOn = result.state['all_on']; if (states.reachable === false && states.bri !== undefined) { states.bri = 0; states.on = false; } if (states.on === false && states.bri !== undefined) { states.bri = 0; } if (states.xy !== undefined) { const xy = states.xy.toString().split(','); states.xy = states.xy.toString(); const rgb = hueHelper.XYBtoRGB(xy[0], xy[1], states.bri / 254); states.r = Math.round(rgb.Red * 254); states.g = Math.round(rgb.Green * 254); states.b = Math.round(rgb.Blue * 254); } if (states.bri !== undefined) { states.level = Math.max(Math.min(Math.round(states.bri / 2.54), 100), 0); } if (states.hue !== undefined) { states.hue = Math.round((states.hue / 65535) * 360); } if (states.ct !== undefined) { // convert color temperature from mired to kelvin states.ct = hueHelper.miredToKelvin(states.ct); if (!isFinite(states.ct)) { // issue #234 // invalid value we cannot determine the meant value, fallback to max states.ct = 6536; // 153 } } // Next two are entertainment states if (result.class) { states.class = result.class; } if (result.stream && result.stream.active !== undefined) { states.activeStream = result.stream.active; } for (const stateB of Object.keys(states)) { values.push({ id: `${this.namespace}.${group.name}.${stateB}`, val: states[stateB] }); } } catch (e) { this.log.error(`Cannot update group state of ${group.name} (${group.id}): ${e.message || e}`); } // poll guard to prevent too fast polling of recently changed id const blockableId = group.name.replace(/[\s.]/g, '_'); if (blockedIds[blockableId] === true) { this.log.debug(`Unblock ${blockableId}`); blockedIds[blockableId] = false; } await this.syncStates(values); } /** * poll the given light and sets states accordingly * * @param light object containing the light id and the name */ async updateLightState(light) { this.log.debug(`polling light ${light.name} (${light.id})`); const values = []; try { let result = await this.api.lights.getLight(parseInt(light.id)); const states = {}; result = result['_data']; if (result.swupdate && result.swupdate.state) { values.push({ id: `${this.namespace}.${light.name}.updateable`, val: result.swupdate.state }); } for (const stateA of Object.keys(result.state)) { states[stateA] = result.state[stateA]; } if (!this.config.ignoreOsram) { if (states.reachable === false && states.bri !== undefined) { states.bri = 0; states.on = false; } } if (states.on === false && states.bri !== undefined) { states.bri = 0; } if (states.xy !== undefined) { const xy = states.xy.toString().split(','); states.xy = states.xy.toString(); const rgb = hueHelper.XYBtoRGB(xy[0], xy[1], states.bri / 254); states.r = Math.round(rgb.Red * 254); states.g = Math.round(rgb.Green * 254); states.b = Math.round(rgb.Blue * 254); } if (states.bri !== undefined) { states.level = Math.max(Math.min(Math.round(states.bri / 2.54), 100), 0); } if (states.hue !== undefined) { states.hue = Math.round((states.hue / 65535) * 360); } if (states.ct !== undefined) { states.ct = hueHelper.miredToKelvin(states.ct); } for (const stateB of Object.keys(states)) { values.push({ id: `${this.namespace}.${light.name}.${stateB}`, val: states[stateB] }); } } catch (e) { this.log.error(`Cannot update light state ${light.name} (${light.id}): ${e.message}`); } // poll guard to prevent too fast polling of recently changed id const blockableId = light.name.replace(/[\s.]/g, '_'); if (blockedIds[blockableId] === true) { this.log.debug(`Unblock ${blockableId}`); blockedIds[blockableId] = false; } await this.syncStates(values); } /** * Create a push connection to the Hue bridge, to listen to updates in near real-time */ createPushConnection() { // @ts-expect-error lib export is wrong this.pushClient = new hue_push_client_1.default({ ip: this.config.bridge, user: this.config.user }); this.pushClient.addEventListener('open', async () => { this.log.info('Push connection established'); try { this.UUIDs = await this.pushClient.uuids(); } catch (e) { this.log.error(`Could not get UUIDs: ${e.message}`); } }); this.pushClient.addEventListener('close', () => { this.log.info('Push connection c