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
JavaScript
"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