iobroker.lovelace
Version:
With this adapter you can build visualization for ioBroker with Home Assistant Lovelace UI
550 lines (499 loc) • 23.8 kB
JavaScript
const utils = require('./utils');
const adapterData = require('./../dataSingleton');
exports.processLight = function (id, control, name, room, func, _obj, objects, forcedEntityId) {
const entity = utils.processCommon(name, room, func, _obj, 'light', forcedEntityId);
let state = control.states.find(s => s.id && s.name === 'SET');
entity.context.STATE = {setId: null, getId: null};
if (state && state.id) {
entity.context.STATE.setId = state.id;
entity.context.STATE.getId = state.id;
utils.addID2entity(state.id, entity);
entity.attributes.color_mode = ONOFF;
entity.attributes.supported_color_modes = [ONOFF];
}
state = control.states.find(s => s.id && s.name === 'ACTUAL');
if (state && state.id) {
entity.context.STATE.getId = state.id;
utils.addID2entity(state.id, entity);
}
return [entity];
};
// Uses color mode!
// Possible modes:
//const UNKNOWN = "unknown";
const ONOFF = 'onoff';
const BRIGHTNESS = 'brightness';
const COLOR_TEMP = 'color_temp';
const HS = 'hs';
//const XY = "xy"; -> not yet supported by frontend, it seems?
const RGB = 'rgb';
const RGBW = 'rgbw';
//const RGBWW = "rgbww"; <- two white???
//
// -> fill attribute supported_color_modes
//
const SUPPORT_EFFECT = 4;
//const SUPPORT_FLASH = 8;
//const SUPPORT_TRANSITION = 32;
function _getLightAdvancedState(control) {
function getState(name) {
const state = control.states.find(s => s.id && s.name === name);
if (state && state.id) { // && state.type === 'boolean') { ignore type so dimmer without ON_SET will be allowed.
return state;
}
return undefined;
}
let state = getState('ON_LIGHT');
if (!state) {
state = getState('ON');
}
if (!state) { //for dimmer, needs to be before 'SET' -> because in dimmer set is for level.
state = getState('ON_SET');
}
if (!state) {
state = getState('SET');
}
return state ? state.id : undefined;
}
function _lightAdvancedAddState(states, objects, entity) {
const getState = states.stateRead;
//prevent zigbee 'available' to become getId:
if (getState && getState.indexOf('zigbee.') === 0 && getState.indexOf('.available') > 0) {
entity.context.STATE.getId = states.states;
}
if (states.state) {
entity.context.STATE.isBoolean = objects[states.state] && objects[states.state].common && objects[states.state].common.type === 'boolean';
entity.attributes.supported_color_modes.push(ONOFF);
return true;
} else {
return false;
}
}
function _lightAdvancedAddColorTemperature(states, objects, entity) {
const iobMaxValue = 153;
const iobMinValue = 450;
if (states.color_temp) {
const attribute = entity.context.ATTRIBUTES.find(a => a.attribute === 'color_temp');
const tempObj = objects[states.color_temp];
attribute.convert_to_kelvin = tempObj && tempObj.common ? (tempObj.common.unit === 'K' || tempObj.common.unit === '°K') : false;
attribute.getParser = (entity, attr, state) => {
if (!state || !state.val) {
entity.attributes.color_temp = iobMinValue;
return;
}
let targetCt = state.val;
if (targetCt > 1000 && !attr.convert_to_kelvin) {
attr.convert_to_kelvin = true;
adapterData.log.warn('Need kelvin conversion for ' + states.color_temp + ' and did not detect that in setup. Please set unit to "K" in object settings.');
}
if (attr.convert_to_kelvin) {
targetCt = 1e6 / targetCt;
}
entity.attributes.color_temp = targetCt;
entity.attributes.color_mode = COLOR_TEMP;
};
entity.attributes.max_mireds = tempObj && tempObj.common && tempObj.common.max || iobMaxValue;
entity.attributes.min_mireds = tempObj && tempObj.common && tempObj.common.min || iobMinValue;
if (attribute.convert_to_kelvin || entity.attributes.max_mireds > 1000) {
attribute.convert_to_kelvin = true;
entity.attributes.max_mireds = tempObj && tempObj.common ? 1e6 / tempObj.common.max : iobMaxValue;
entity.attributes.min_mireds = tempObj && tempObj.common ? 1e6 / tempObj.common.min : iobMinValue;
}
adapterData.log.debug(entity.entity_id + ' ct needs kelvin conversion: ' + attribute.convert_to_kelvin);
if (entity.attributes.min_mireds > entity.attributes.max_mireds) {
//for kelvin conversion min and max need to be swapped.
const max = entity.attributes.min_mireds;
entity.attributes.min_mireds = entity.attributes.max_mireds;
entity.attributes.max_mireds = max;
}
entity.attributes.supported_color_modes.push(COLOR_TEMP);
}
}
function _lightAdvancedAddBrightness(states, objects, entity) {
if (!states.brightness && states.state && objects[states.state].common.type === 'number') {
states.brightness = states.state;
}
if (states.brightness) {
const attribute = entity.context.ATTRIBUTES.find(a => a.attribute === 'brightness');
attribute.getParser = (entity, attr, state) => {
state = state || {val: 0};
entity.attributes.brightness = ((state.val - attr.min) / (attr.max - attr.min)) * 255;
if (states.state === states.brightness) {
entity.state = entity.attributes.brightness > 0 ? 'on' : 'off';
}
if (!entity.attributes.color_mode || entity.attributes.color_mode === ONOFF) {
entity.attributes.color_mode = BRIGHTNESS;
}
};
attribute.min = (objects[attribute.getId] && objects[attribute.getId].common && objects[attribute.getId].common.min) || 0;
attribute.max = (objects[attribute.getId] && objects[attribute.getId].common && objects[attribute.getId].common.max) || 100;
entity.attributes.supported_color_modes.push(BRIGHTNESS);
}
}
function _lightAdvancedAddHueAndSat(states, objects, entity) {
if (states.hue) {
const hue_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'hue');
hue_attr.max = objects[states.hue] && objects[states.hue].common && objects[states.hue].common.max || 360;
hue_attr.getParser = (entity, attr, state) => {
state = state || {val: 0};
entity.attributes.hs_color[0] = state.val / attr.max * 360;
entity.attributes.color_mode = HS;
};
entity.attributes.supported_color_modes.push(HS);
entity.attributes.hs_color = [0,100];
}
//add saturation as own attribute. Will update saturation values from ioBroker correctly.
if (states.saturation) {
const sat_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'saturation');
sat_attr.max = objects[states.saturation] && objects[states.saturation].common && objects[states.saturation].common.max || 100;
if (!states.rgb_color && !states.red) {
if (!states.hue) {
adapterData.log.warn('Saturation present but no hue id found for ' + states.saturation + '. Hue won\'t work.');
return;
}
sat_attr.getParser = (entity, attr, state) => {
state = state || {val: 0};
entity.attributes.hs_color[1] = state.val / attr.max * 100;
entity.attributes.color_mode = HS;
};
} else {
sat_attr.getParser = () => {}; //ignore saturation updates.
}
} else if (states.hue) {
adapterData.log.warn('Hue present but no saturation id found for ' + states.hue + '. Saturation won\'t work.');
}
}
async function _lightAdvancedAddRGBSingle(states, objects, entity) {
if (states.rgb_color) {
const attribute = entity.context.ATTRIBUTES.find(a => a.attribute === 'rgb_color');
attribute.is_rgb_array = false;
attribute.is_rgb_string = true;
attribute.getParser = (entity, attr, state) => {
let str = state ? (state.val || '#000000').toString() : '#000000';
if (str[0] === '#') {
str = str.substring(1);
}
let r,g,b;
if (/([0-9]){1,3},([0-9]){1,3},([0-9]){1,3}/.test(str)) {
adapterData.log.debug('Have RGB decimal array.');
[r, g, b] = str.split(',').map(v => parseInt(v, 10));
} else {
if (!/^[\da-fA-F]{6}/.test(str)) {
adapterData.log.error('Malformed rgb string ' + str + ' expecting six hex digits.');
return;
}
r = parseInt(str.substr(0, 2), 16);
g = parseInt(str.substr(2, 2), 16);
b = parseInt(str.substr(4, 2), 16);
}
entity.attributes.color_mode = RGB;
entity.attributes.rgb_color = [r, g, b];
if (states.white) {
entity.attributes.color_mode = RGBW;
entity.attributes.rgbw_color[0] = r;
entity.attributes.rgbw_color[1] = g;
entity.attributes.rgbw_color[2] = b;
}
};
//check if current state is rgb array.
const rgbState = await adapterData.adapter.getForeignStateAsync(states.rgb_color);
if (rgbState && rgbState.val) {
attribute.is_rgb_array = /([0-9]){1,3},([0-9]){1,3},([0-9]){1,3}/.test(rgbState.val.toString());
}
entity.attributes.rgb_color = [0,0,0];
entity.attributes.supported_color_modes.push(RGB);
}
}
function _lightAdvancedAddRGB(states, objects, entity) {
if (states.red && states.green && states.blue) {
const red_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'red');
const green_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'green');
const blue_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'blue');
const rgbGetParser = (index, entity, attr, state) => {
let val = state ? state.val || 0 : 0;
val = val / attr.max * 255;
entity.attributes.rgb_color[index] = val;
if (entity.attributes.rgbw_color) {
entity.attributes.rgbw_color[index] = val;
}
entity.attributes.color_mode = states.white ? RGBW : RGB;
};
red_attr.getParser = rgbGetParser.bind(this, 0);
green_attr.getParser = rgbGetParser.bind(this, 1);
blue_attr.getParser = rgbGetParser.bind(this, 2);
red_attr.max = objects[states.red] && objects[states.red].common && objects[states.red].common.max || 100;
green_attr.max = objects[states.green] && objects[states.green].common && objects[states.green].common.max || 100;
blue_attr.max = objects[states.blue] && objects[states.blue].common && objects[states.blue].common.max || 100;
entity.attributes.supported_color_modes.push(RGB);
entity.attributes.rgb_color = [0,0,0];
}
}
async function _setLightAdvancedAttributesToIOBStates(data, entity, user) {
function NumToHex(num) {
let hex = Number(num).toString(16).toUpperCase();
if (hex.length < 2) {
hex = '0' + hex;
}
return hex;
}
if (data.service_data.color_temp) { //will also be false for ct = 0 -> but ct = 0 is not a useful mired value and creates issues with the conversion.
let ct = data.service_data.color_temp;
entity.attributes.color_temp = ct;
const attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'color_temp');
if (attr.convert_to_kelvin) {
ct = 1e6 / ct;
}
entity.attributes.color_mode = COLOR_TEMP;
await adapterData.adapter.setForeignStateAsync(attr.getId, ct, false, {user});
}
if (data.service_data.brightness >= 0 && !data.service_data.brightness_pct) {
data.service_data.brightness_pct = data.service_data.brightness / 255 * 100;
}
if (data.service_data.brightness_pct >= 0) {
const attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'brightness');
entity.attributes.brightness = (data.service_data.brightness_pct / 100) * 255;
if (!entity.context.STATE.isBoolean) {
entity.state = data.service_data.brightness_pct > 0 ? 'on' : 'off';
}
if (!entity.attributes.color_mode || entity.attributes.color_mode === ONOFF) {
entity.attributes.color_mode = BRIGHTNESS;
}
await adapterData.adapter.setForeignState(attr.getId, data.service_data.brightness_pct / 100 * (attr.max - attr.min) + attr.min, false, {user});
}
if (data.service_data.hs_color) {
const attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'hue');
const attr_Sat = entity.context.ATTRIBUTES.find(a => a.attribute === 'saturation');
entity.attributes.hs_color = data.service_data.hs_color;
const [h, s] = data.service_data.hs_color;
if (attr) {
await adapterData.adapter.setForeignStateAsync(attr.getId, h / 360 * attr.max, false, {user});
} else {
adapterData.log.warn('No hue for ' + entity.entity_id + ', can only set saturation.');
}
if (attr_Sat) {
await adapterData.adapter.setForeignStateAsync(attr_Sat.getId, s / 100 * attr_Sat.max, false, {user});
} else {
adapterData.log.warn('No saturation for ' + entity.entity_id + ', can only set hue.');
}
entity.attributes.color_mode = HS;
}
if (data.service_data.rgbw_color) {
const attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'white');
await adapterData.adapter.setForeignStateAsync(attr.getId, data.service_data.rgbw_color[3] / 255 * attr.max, false, {user});
data.service_data.rgb_color = data.service_data.rgbw_color; //make sure we set color, too.
entity.attributes.color_mode = RGBW;
}
if (data.service_data.rgb_color) {
const rgb_color = entity.context.ATTRIBUTES.find(a => a.attribute === 'rgb_color');
const [r, g, b] = data.service_data.rgb_color;
if (rgb_color) {
if (!rgb_color.is_rgb_array) {
const rgbString = '#' + NumToHex(r) + NumToHex(g) + NumToHex(b);
await adapterData.adapter.setForeignStateAsync(rgb_color.getId, rgbString, false, {user});
} else {
const rgbString = r + ',' + g + ',' + b;
await adapterData.adapter.setForeignStateAsync(rgb_color.getId, rgbString, false, {user});
}
} else {
const red_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'red');
const green_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'green');
const blue_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'blue');
//set r,g,b to single states in ioBroker. rgb is always [0-255] here (from HASS), so scale here.
await Promise.all([
adapterData.adapter.setForeignStateAsync(red_attr.getId, r / 255 * red_attr.max, false, {user}),
adapterData.adapter.setForeignStateAsync(green_attr.getId, g / 255 * green_attr.max, false, {user}),
adapterData.adapter.setForeignStateAsync(blue_attr.getId, b / 255 * blue_attr.max, false, {user})
]);
}
entity.attributes.color_mode = !data.service_data.rgbw_color ? RGB : RGBW;
}
if (data.service_data.effect) {
const effect_attr = entity.context.ATTRIBUTES.find(a => a.attributes === 'effect');
let val = effect_attr.states[data.service_data.effect];
if (val === undefined) {
val = data.service_data.effect;
}
await adapterData.adapter.setForeignStateAsync(effect_attr.getId, val, false, {user});
}
}
async function _handleTurnOnCmd(entity, command, data, user) {
// if ON/OFF object exists
if (entity.context.STATE.setId && entity.context.STATE.getId) {
// read actual state
const state = await adapterData.adapter.getForeignStateAsync(entity.context.STATE.getId);
// if lamp is not ON
if (!state || !state.val) {
// turn ON:
await adapterData.adapter.setForeignStateAsync(entity.context.STATE.setId, command.on, false, {user});
}
}
if (!entity.attributes.color_mode) {
entity.attributes.color_mode = ONOFF;
}
await _setLightAdvancedAttributesToIOBStates(data, entity, user);
}
/**
* Extract relevant ids from type-detector control object.
* Result object has optional members:
* state
* brightness
* color_temp
*
* hue
* saturation
*
* rgb_color
* red
* green
* blue
* white
*
* @param control
* @returns {{}}
*/
function convertControlToStates(control) {
function findState(name) {
const state = control.states.find(s => s.id && s.name === name);
return state ? state.id : undefined;
}
const states = {};
states.state = _getLightAdvancedState(control);
states.stateRead = findState('ON_ACTUAL');
states.color_temp = findState('TEMPERATURE');
//brightness a bit more complex:
states.brightness = findState('DIMMER');
if (!states.brightness) {
states.brightness = findState('BRIGHTNESS');
}
if (!states.brightness) {
const state = control.states.find(s => s.id && s.name === 'SET');
if (state && state.type === 'number') { //in dimmer SET is for level for everything else it is on/off.
states.brightness = state.id;
}
}
//color stuff:
states.hue = findState('HUE');
states.saturation = findState('SATURATION');
states.rgb_color = findState('RGB');
states.red = findState('RED');
states.green = findState('GREEN');
states.blue = findState('BLUE');
states.white = findState('WHITE');
return states;
}
/**
* Fills bare entity from states and objects with all that light entity can do.
* @param states - state ids, either created manually or from convertControlToStates function
* @param objects - objects for state ids.
* @param entity - bare entity to fill.
* @returns {Promise<*[entity]>}
*/
async function fillLightEntityFromStates(states, objects, entity) {
utils.fillEntityFromStates(states, entity); //already prefills attributes.
entity.attributes.supported_color_modes = [];
entity.attributes.color_mode = ONOFF;
entity.supported_features = 0;
if (!entity.context.COMMANDS) {
entity.context.COMMANDS = [];
}
const white_attr = entity.context.ATTRIBUTES.find(a => a.attribute === 'white');
if (states.white) {
white_attr.max = objects[white_attr.getId] && objects[white_attr.getId].common && objects[white_attr.getId].common.max || 100;
entity.attributes.rgbw_color = [entity.attributes.red, entity.attributes.green, entity.attributes.blue, entity.attributes.white];
if (states.red || states.rgb_color) {
entity.attributes.supported_color_modes.push(RGBW);
white_attr.getParser = (entity, attr, state) => {
const val = state ? state.val || 0 : 0;
entity.attributes.rgbw_color[3] = val / attr.max * 255;
entity.attributes.color_mode = RGBW;
};
}
}
//fill in on/off state id.
await _lightAdvancedAddState(states, objects, entity);
//fill in color temperature stuff.
await _lightAdvancedAddColorTemperature(states, objects, entity);
//if there is a "BRIGHTNESS" control, use it to dim lamp.
await _lightAdvancedAddBrightness(states, objects, entity);
//add hue and sat:
await _lightAdvancedAddHueAndSat(states, objects, entity);
//add rgb. Will only happen, if no hue.
await _lightAdvancedAddRGBSingle(states, objects, entity);
//add rgb as single states. Will only happen if no hue and no rgbSingle:
await _lightAdvancedAddRGB(states, objects, entity);
if (states.effect) {
const effect_attr = entity.context.ATTRIBUTES.find(a => a.attributes === 'effect');
effect_attr.states = objects[effect_attr.getId] && objects[effect_attr.getId].common && objects[effect_attr.getId].common.states || {0: 'Please', 1: 'Fill', 2: 'States'};
entity.attributes.effect_list = Object.values(effect_attr.states);
effect_attr.getParser = (entity, attr, state) => {
state = state || {val: 0};
entity.attributes.effect = effect_attr.states[state.val];
};
entity.supported_features |= SUPPORT_EFFECT;
}
entity.context.COMMANDS.push({
service: 'turn_on',
on: true,
setId: entity.context.STATE.setId,
parseCommand: _handleTurnOnCmd.bind(this)
});
if (!entity.context.STATE.isBoolean) {
const stateObj = objects[states.state];
entity.context.COMMANDS[0].on = stateObj && stateObj.common && stateObj.common.max || 100;
entity.context.COMMANDS.push({
service: 'turn_off',
off: stateObj && stateObj.common && stateObj.common.min || 0,
setId: entity.context.STATE.setId,
parseCommand: (entity, command, data, user) => {
return adapterData.adapter.setForeignStateAsync(command.setId, command.off, false, {user});
}
});
entity.context.STATE.getParser = function (entity, attr, state) {
state = state || {val: null};
entity.state = state.val > (stateObj && stateObj.common && stateObj.common.min || 0) ? 'on' : 'off';
if (!entity.attributes.color_mode) {
entity.attributes.color_mode = ONOFF;
}
};
}
return [entity];
}
/**
* Create manual light entity.
* @param id - id of "main" object, i.e. state.
* @param obj - iobroker object of id param
* @param entity - already created entity
* @param objects - id object cache
* @param custom - custom part of object
* @returns {Promise<[entity]>}
*/
exports.processManualEntity = async function(id, obj, entity, objects, custom) {
const states = custom.states || {
state: id
};
if (obj && obj.common && obj.common.type === 'number') {
states.brightness = id;
}
return fillLightEntityFromStates(states, objects, entity);
};
/**
* Create Light entity form type-detector detection.
* @param id
* @param control
* @param name
* @param room
* @param func
* @param _obj
* @param objects
* @returns {Promise<null|entity[]>}
*/
exports.processLightAdvanced = async function (id, control, name, room, func, _obj, objects, forcedEntityId) {
const states = convertControlToStates(control);
if (states.state) {
const entity = utils.processCommon(name, func, room, _obj, 'light', forcedEntityId);
return fillLightEntityFromStates(states, objects, entity);
} else {
adapterData.log.debug('Could not add ' + id + ' of type ' + control.type + ' -> no on/off control found.');
return null;
}
};