iobroker.zigbee2mqtt
Version:
Zigbee2MQTT adapter for ioBroker
418 lines (387 loc) • 17.1 kB
JavaScript
const statesDefs = require('./states').states;
const createDeviceFromExposes = require('./exposes').createDeviceFromExposes;
const utils = require('./utils');
const colors = require('./colors.js');
const rgb = require('./rgb.js');
const ImageController = require('./imageController').ImageController;
class DeviceController {
constructor(adapter, deviceCache, groupCache, config, logCustomizations, createCache) {
this.adapter = adapter;
this.groupCache = groupCache;
this.deviceCache = deviceCache;
this.config = config;
this.logCustomizations = logCustomizations;
this.createCache = createCache;
this.imageController = new ImageController(adapter);
}
async createDeviceDefinitions(devicesMessage) {
utils.clearArray(this.deviceCache);
for (const devicesMessag of devicesMessage) {
if (this.logCustomizations.debugDevices.includes(devicesMessag.ieee_address)) {
this.adapter.log.warn(
`--->>> fromZ2M -> ${devicesMessag.ieee_address} exposes: ${JSON.stringify(devicesMessag)}`
);
}
if (devicesMessag.definition != null) {
// if the device is already present in the cache, remove it
this.removeDeviceByIeee(this.deviceCache, devicesMessag.ieee_address);
if (devicesMessag.definition.exposes) {
try {
const newDevice = await createDeviceFromExposes(devicesMessag, this.adapter);
newDevice.icon = await this.imageController.getDeviceIcon(devicesMessag);
this.deviceCache.push(newDevice);
} catch (err) {
this.adapter.log.warn(`Cannot ${devicesMessag.friendly_name} create Device from Exposes!`);
this.adapter.log.debug(JSON.stringify(devicesMessag));
this.adapter.log.debug(err);
}
}
}
}
}
async defineGroupDevice(groupID, ieee_address, scenes) {
const brmPropName =
this.adapter.config.brightnessMoveOnOff == true ? 'brightness_move_onoff' : 'brightness_move';
const brsPropName =
this.adapter.config.brightnessStepOnOff == true ? 'brightness_step_onoff' : 'brightness_step';
const newDevice = {
id: groupID,
ieee_address: ieee_address,
icon: undefined, // await imageController.getDeviceIcon(devicesMessag), device.definition.model
optionsValues: {},
states: [
statesDefs.available,
statesDefs.brightness,
statesDefs.colortemp_move,
statesDefs.transition,
{
id: 'state',
prob: 'state',
name: 'Switch state',
options: ['transition'],
icon: undefined,
role: 'switch',
write: true,
read: true,
type: 'boolean',
def: false,
getter: (payload) => payload.state === 'ON',
setter: (value) => (value ? 'ON' : 'OFF'),
},
{
id: 'brightness_move',
prop: brmPropName,
name: 'Increases or decreases the brightness by X units per second',
icon: undefined,
role: 'state',
write: true,
read: false,
type: 'number',
min: -50,
max: 50,
def: 0,
},
{
id: 'brightness_step',
prop: brsPropName,
name: 'Increases or decreases brightness by X steps',
icon: undefined,
role: 'state',
write: true,
read: false,
type: 'number',
min: -255,
max: 255,
def: 0,
},
{
id: 'color',
prop: 'color',
name: 'Color',
options: ['transition'],
icon: undefined,
role: 'level.color.rgb',
write: true,
read: true,
type: 'string',
def: '#ff00ff',
setter: (value) => {
let xy = [0, 0];
const rgbcolor = colors.ParseColor(value);
xy = rgb.rgb_to_cie(rgbcolor.r, rgbcolor.g, rgbcolor.b);
return {
x: xy[0],
y: xy[1],
};
},
getter: (payload) => {
if (payload.color_mode != 'xy' && this.config.colorTempSyncColor == false) {
return undefined;
}
if (payload.color && payload.color.x && payload.color.y) {
const colorval = rgb.cie_to_rgb(payload.color.x, payload.color.y);
return (
'#' +
utils.decimalToHex(colorval[0]) +
utils.decimalToHex(colorval[1]) +
utils.decimalToHex(colorval[2])
);
} else {
return undefined;
}
},
},
{
id: 'colortemp',
prop: 'color_temp',
name: 'Color temperature',
options: ['transition'],
icon: undefined,
role: 'level.color.temperature',
write: true,
read: true,
type: 'number',
min: this.config.useKelvin == true ? utils.miredKelvinConversion(550) : 150,
max: this.config.useKelvin == true ? utils.miredKelvinConversion(153) : 500,
def: this.config.useKelvin == true ? utils.miredKelvinConversion(153) : 500,
unit: this.config.useKelvin == true ? 'K' : 'mired',
setter: (value) => {
return utils.toMired(value);
},
getter: (payload) => {
if (payload.color_mode != 'color_temp') {
return undefined;
}
if (this.config.useKelvin == true) {
return utils.miredKelvinConversion(payload.color_temp);
} else {
return payload.color_temp;
}
},
},
{
id: 'state_toggle',
name: 'Toggle the state',
options: ['transition'],
icon: undefined,
role: 'button',
write: true,
read: true,
type: 'boolean',
setattr: 'state',
def: true,
setter: (value) => (value ? 'TOGGLE' : undefined),
},
{
id: 'effect',
name: 'Triggers an effect on the light (e.g. make light blink for a few seconds)',
prop: 'effect',
icon: undefined,
role: 'state',
write: true,
read: true,
type: 'string',
def: '',
states: {
blink: 'blink',
breathe: 'breathe',
okay: 'okay',
channel_change: 'channel_change',
finish_effect: 'finish_effect',
stop_effect: 'stop_effect',
},
},
],
};
// Create buttons for scenes
for (const scene of scenes) {
const sceneSate = {
id: `scene_${scene.id}`,
prop: `scene_recall`,
name: scene.name,
icon: undefined,
role: 'button',
write: true,
read: true,
def: true,
type: 'boolean',
setter: (value) => (value ? scene.id : undefined),
};
// @ts-ignore
newDevice.states.push(sceneSate);
}
// if the device is already present in the cache, remove it
this.removeDeviceByIeee(this.groupCache, ieee_address);
this.groupCache.push(newDevice);
}
async createGroupDefinitions(groupsMessage) {
utils.clearArray(this.groupCache);
for (const groupMessage of groupsMessage) {
if (this.logCustomizations.debugDevices.includes(groupMessage.id)) {
this.adapter.log.warn(`--->>> fromZ2M -> ${groupMessage.id} exposes: ${JSON.stringify(groupMessage)}`);
}
await this.defineGroupDevice(groupMessage.friendly_name, `group_${groupMessage.id}`, groupMessage.scenes);
}
}
async createOrUpdateDevices() {
for (const device of this.groupCache.concat(this.deviceCache)) {
let deviceName = await this.getDeviceName(device);
let description = await this.getDeviceDescription(device);
if (deviceName == '' && device.description) {
deviceName = device.description;
description = '';
}
// Manipulate deviceName if the device is disabled, so the update of the device is triggered as well
if (device.disabled && device.disabled == true) {
if (this.config.useEventInDesc == true) {
description = 'Device is disabled!';
} else {
deviceName = `[Disabled] ${deviceName}`;
}
}
if (!this.createCache[device.ieee_address] || this.createCache[device.ieee_address].name != deviceName || this.createCache[device.ieee_address].description != description) {
const deviceObj = {
type: 'device',
common: {
icon: device.icon,
name: deviceName,
desc: description,
statusStates: { onlineId: '' },
},
native: {
deviceRemoved: false,
groupDevice: false,
},
};
// Group Device
if (device.ieee_address.includes('group_')) {
deviceObj.native.groupDevice = true;
deviceObj.common.statusStates.onlineId = `${this.adapter.name}.${this.adapter.instance}.${device.ieee_address}.available`;
}
// Disabled Device
else if (device.disabled || device.disabled == true) {
// Placeholder for possible later logic
}
// Only the onlineId is set if the device is not disabled
else {
deviceObj.common.statusStates.onlineId = `${this.adapter.name}.${this.adapter.instance}.${device.ieee_address}.available`;
}
//@ts-ignore
await this.adapter.extendObjectAsync(device.ieee_address, deviceObj);
this.createCache[device.ieee_address] = { name: deviceName, description: description };
}
// Here it is checked whether the scenes match the current data from z2m.
// If necessary, scenes are automatically deleted from ioBroker.
const sceneStates = await this.adapter.getStatesAsync(`${device.ieee_address}.scene_*`);
const sceneIDs = Object.keys(sceneStates);
for (const sceneID of sceneIDs) {
const stateID = sceneID.split('.')[3];
if (device.states.find((x) => x.id == stateID) == null) {
this.adapter.delObject(sceneID);
}
}
if (device != undefined && device.states != undefined) {
for (const state of device.states) {
if (state && (!this.createCache[device.ieee_address][state.id] || this.createCache[device.ieee_address][state.id].name != state.name)) {
const iobState = {
type: 'state',
common: await this.copyAndCleanStateObj(state),
native: {},
};
await this.adapter.extendObjectAsync(`${device.ieee_address}.${state.id}`, iobState);
this.createCache[device.ieee_address][state.id] = {name: state.name, created: true};
}
}
}
}
}
async renameDeviceInCache(messageObj) {
const renamedDevice = this.groupCache
.concat(this.deviceCache)
.find((x) => x.id == messageObj.payload.data.from);
if (renamedDevice) {
renamedDevice.id = messageObj.payload.data.to;
}
}
async checkAndProgressDeviceRemove() {
let description = '';
let deviceName = '';
let iobDevices = await this.adapter.getDevicesAsync();
// Do not consider devices already marked as "deviceRemoved"
iobDevices = iobDevices.filter((x) => x.native.deviceRemoved == false);
// Do not consider groups
iobDevices = iobDevices.filter((x) => x.native.groupDevice == false);
for (const iobDevice of iobDevices) {
const ieee_address = iobDevice._id.split('.')[2];
//Check whether the devices found from the object tree are also available in the DeviceCache
if (!this.deviceCache.find((x) => x.ieee_address == ieee_address)) {
deviceName = iobDevice.common.name;
if (this.config.useEventInDesc == true) {
description = 'Device was removed!';
} else {
deviceName = `[Removed] ${deviceName}`;
}
this.adapter.extendObject(`${ieee_address}`, {
common: {
name: deviceName,
desc: description,
},
native: {
deviceRemoved: true,
},
});
this.adapter.setStateChangedAsync(`${ieee_address}.available`, false, true);
delete this.createCache[ieee_address];
}
}
}
removeDeviceByIeee(devices, ieee_address) {
const idx = devices.findIndex((x) => x.ieee_address == ieee_address);
if (idx > -1) {
devices.splice(idx, 1);
}
}
async copyAndCleanStateObj(state) {
const iobState = { ...state };
const blacklistedKeys = [
'prop',
'setter',
'setterOpt',
'getter',
'setattr',
'readable',
'writable',
'isOption',
'inOptions',
'isEvent',
'options',
];
for (const blacklistedKey of blacklistedKeys) {
delete iobState[blacklistedKey];
}
return iobState;
}
getDeviceName(device) {
return device.id == device.ieee_address ? '' : device.id;
}
getDeviceDescription(device) {
return device.description ? device.description : '';
}
processCoordinatorCheck(payload) {
if (payload && payload.data && payload.data.missing_routers) {
const missingRoutersCount = payload.data.missing_routers.length;
this.adapter.setState('info.missing_routers', JSON.stringify(payload.data.missing_routers), true);
this.adapter.setState('info.missing_routers_count', missingRoutersCount, true);
if (missingRoutersCount > 0) {
this.adapter.log[this.config.coordinatorCheckLogLvl](
`Coordinator check: ${missingRoutersCount} missing routers were found, please check the data point 'zigbee2mqtt.x.info.missing_routers'!`
);
} else {
this.adapter.log.info('Coordinator check: No missing router was found.');
}
}
}
}
module.exports = {
DeviceController,
};