Refactor code to use plejd outputs rather than devices as main entity

This commit is contained in:
Victor Hagelbäck 2021-03-31 20:04:45 +02:00
parent ef718cf1db
commit 9a76a3ba50
10 changed files with 185 additions and 158 deletions

View file

@ -45,8 +45,8 @@ class DeviceRegistry {
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId] = []; this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId] = [];
} }
if ( if (
outputDevice.roomId !== outputDevice.uniqueId && outputDevice.roomId !== outputDevice.uniqueId
!this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId) && !this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].includes(outputDevice.uniqueId)
) { ) {
this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].push(outputDevice.uniqueId); this.outputDeviceUniqueIdsByRoomId[outputDevice.roomId].push(outputDevice.uniqueId);
logger.verbose( logger.verbose(

View file

@ -65,23 +65,26 @@ class PlejdAddon extends EventEmitter {
}); });
// subscribe to changes from HA // subscribe to changes from HA
this.mqttClient.on(MqttClient.EVENTS.stateChanged, (device, command) => { this.mqttClient.on(
MqttClient.EVENTS.stateChanged,
/** @param device {import('./types/DeviceRegistry').OutputDevice} */
(device, command) => {
try { try {
const deviceId = device.id; const { uniqueId } = device;
if (device.typeName === 'Scene') { if (device.typeName === 'Scene') {
// we're triggering a scene, lets do that and jump out. // we're triggering a scene, lets do that and jump out.
// since scenes aren't "real" devices. // since scenes aren't "real" devices.
this.sceneManager.executeScene(device.id); this.sceneManager.executeScene(uniqueId);
return; return;
} }
let state = 'OFF'; let state = false;
let commandObj = {}; let commandObj = {};
if (typeof command === 'string') { if (typeof command === 'string') {
// switch command // switch command
state = command; state = command === 'ON';
commandObj = { commandObj = {
state, state,
}; };
@ -89,33 +92,34 @@ class PlejdAddon extends EventEmitter {
// since the switch doesn't get any updates on whether it's on or not, // since the switch doesn't get any updates on whether it's on or not,
// we fake this by directly send the updateState back to HA in order for // we fake this by directly send the updateState back to HA in order for
// it to change state. // it to change state.
this.mqttClient.updateState(deviceId, { this.mqttClient.updateOutputState(uniqueId, {
state: state === 'ON' ? 1 : 0, state,
}); });
} else { } else {
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring
state = command.state; state = command.state === 'ON';
commandObj = command; commandObj = command;
} }
if (state === 'ON') { if (state) {
this.plejdDeviceCommunication.turnOn(deviceId, commandObj); this.plejdDeviceCommunication.turnOn(uniqueId, commandObj);
} else { } else {
this.plejdDeviceCommunication.turnOff(deviceId, commandObj); this.plejdDeviceCommunication.turnOff(uniqueId, commandObj);
} }
} catch (err) { } catch (err) {
logger.error('Error in MqttClient.stateChanged callback', err); logger.error('Error in MqttClient.stateChanged callback', err);
} }
}); },
);
this.mqttClient.init(); this.mqttClient.init();
// subscribe to changes from Plejd // subscribe to changes from Plejd
this.plejdDeviceCommunication.on( this.plejdDeviceCommunication.on(
PlejdDeviceCommunication.EVENTS.stateChanged, PlejdDeviceCommunication.EVENTS.stateChanged,
(deviceId, command) => { (uniqueOutputId, command) => {
try { try {
this.mqttClient.updateState(deviceId, command); this.mqttClient.updateOutputState(uniqueOutputId, command);
} catch (err) { } catch (err) {
logger.error('Error in PlejdService.stateChanged callback', err); logger.error('Error in PlejdService.stateChanged callback', err);
} }

View file

@ -76,8 +76,8 @@ class PlejdApi {
} }
} }
} }
this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey;
this.deviceRegistry.setApiSite(this.siteDetails);
this.getDevices(); this.getDevices();
} }
@ -300,20 +300,22 @@ class PlejdApi {
* * `devices` - physical Plejd devices, duplicated for devices with multiple outputs * * `devices` - physical Plejd devices, duplicated for devices with multiple outputs
* devices: [{deviceId, title, objectId, ...}, {...}] * devices: [{deviceId, title, objectId, ...}, {...}]
* * `deviceAddress` - BLE address of each physical device * * `deviceAddress` - BLE address of each physical device
* deviceAddress: {[deviceId]: bleDeviceId} * deviceAddress: {[deviceId]: bleDeviceAddress}
* * `outputSettings` - lots of info about load settings, also links devices to output index * * `outputSettings` - lots of info about load settings, also links devices to output index
* outputSettings: [{deviceId, output, deviceParseId, ...}] //deviceParseId === objectId above * outputSettings: [{deviceId, output, deviceParseId, ...}] //deviceParseId === objectId above
* * `outputAddress`: BLE address of [0] main output and [n] other output (loads) * * `outputAddress`: BLE address of [0] main output and [n] other output (loads)
* outputAddress: {[deviceId]: {[output]: bleDeviceId}} * outputAddress: {[deviceId]: {[output]: bleDeviceAddress}}
* * `inputSettings` - detailed settings for inputs (buttons, RTR-01, ...), scenes triggered, ... * * `inputSettings` - detailed settings for inputs (buttons, RTR-01, ...), scenes triggered, ...
* inputSettings: [{deviceId, input, ...}] //deviceParseId === objectId above * inputSettings: [{deviceId, input, ...}] //deviceParseId === objectId above
* * `inputAddress` - Links inputs to what BLE device they control, or 255 for unassigned/scene * * `inputAddress` - Links inputs to what BLE device they control, or 255 for unassigned/scene
* inputAddress: {[deviceId]: {[input]: bleDeviceId}} * inputAddress: {[deviceId]: {[input]: bleDeviceAddress}}
*/ */
_getPlejdDevices() { _getPlejdDevices() {
this.deviceRegistry.clearPlejdDevices(); this.deviceRegistry.clearPlejdDevices();
this.siteDetails.devices.forEach((device) => { this.siteDetails.devices.forEach((device) => {
this.deviceRegistry.addPhysicalDevice(device);
const outputSettings = this.siteDetails.outputSettings.find( const outputSettings = this.siteDetails.outputSettings.find(
(x) => x.deviceParseId === device.objectId, (x) => x.deviceParseId === device.objectId,
); );
@ -328,7 +330,7 @@ class PlejdApi {
outputSettings.output, outputSettings.output,
); );
const bleDeviceIndex = this.siteDetails.outputAddress[device.deviceId][ const bleOutputAddress = this.siteDetails.outputAddress[device.deviceId][
outputSettings.output outputSettings.output
]; ];
@ -343,7 +345,7 @@ class PlejdApi {
/** @type {import('types/DeviceRegistry').OutputDevice} */ /** @type {import('types/DeviceRegistry').OutputDevice} */
const outputDevice = { const outputDevice = {
bleDeviceIndex, bleOutputAddress,
deviceId: device.deviceId, deviceId: device.deviceId,
dimmable, dimmable,
hiddenFromRoomList: device.hiddenFromRoomList, hiddenFromRoomList: device.hiddenFromRoomList,
@ -409,7 +411,7 @@ class PlejdApi {
/** @type {import('types/DeviceRegistry').OutputDevice} */ /** @type {import('types/DeviceRegistry').OutputDevice} */
const newDevice = { const newDevice = {
bleDeviceIndex: roomAddress, bleOutputAddress: roomAddress,
deviceId: null, deviceId: null,
dimmable, dimmable,
hiddenFromRoomList: false, hiddenFromRoomList: false,
@ -431,6 +433,7 @@ class PlejdApi {
} }
_getSceneDevices() { _getSceneDevices() {
this.deviceRegistry.clearSceneDevices();
// add scenes as switches // add scenes as switches
const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false); const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false);
@ -438,7 +441,7 @@ class PlejdApi {
const sceneNum = this.siteDetails.sceneIndex[scene.sceneId]; const sceneNum = this.siteDetails.sceneIndex[scene.sceneId];
/** @type {import('types/DeviceRegistry').OutputDevice} */ /** @type {import('types/DeviceRegistry').OutputDevice} */
const newScene = { const newScene = {
bleDeviceIndex: sceneNum, bleOutputAddress: sceneNum,
deviceId: undefined, deviceId: undefined,
dimmable: false, dimmable: false,
hiddenFromSceneList: scene.hiddenFromSceneList, hiddenFromSceneList: scene.hiddenFromSceneList,

View file

@ -49,6 +49,8 @@ class PlejBLEHandler extends EventEmitter {
connectedDevice = null; connectedDevice = null;
consecutiveWriteFails; consecutiveWriteFails;
consecutiveReconnectAttempts = 0; consecutiveReconnectAttempts = 0;
/** @type {import('./DeviceRegistry')} */
deviceRegistry;
discoveryTimeout = null; discoveryTimeout = null;
plejdService = null; plejdService = null;
pingRef = null; pingRef = null;
@ -152,21 +154,21 @@ class PlejBLEHandler extends EventEmitter {
logger.info('BLE init done, waiting for devices.'); logger.info('BLE init done, waiting for devices.');
} }
async sendCommand(command, deviceId, data) { async sendCommand(command, uniqueOutputId, data) {
let payload; let payload;
let brightnessVal; let brightnessVal;
switch (command) { switch (command) {
case COMMANDS.TURN_ON: case COMMANDS.TURN_ON:
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '01'); payload = this._createHexPayload(uniqueOutputId, BLE_CMD_STATE_CHANGE, '01');
break; break;
case COMMANDS.TURN_OFF: case COMMANDS.TURN_OFF:
payload = this._createHexPayload(deviceId, BLE_CMD_STATE_CHANGE, '00'); payload = this._createHexPayload(uniqueOutputId, BLE_CMD_STATE_CHANGE, '00');
break; break;
case COMMANDS.DIM: case COMMANDS.DIM:
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
brightnessVal = (data << 8) | data; brightnessVal = (data << 8) | data;
payload = this._createHexPayload( payload = this._createHexPayload(
deviceId, uniqueOutputId,
BLE_CMD_DIM2_CHANGE, BLE_CMD_DIM2_CHANGE,
`01${brightnessVal.toString(16).padStart(4, '0')}`, `01${brightnessVal.toString(16).padStart(4, '0')}`,
); );
@ -194,9 +196,9 @@ class PlejBLEHandler extends EventEmitter {
plejd.instance = device; plejd.instance = device;
const segments = plejd.path.split('/'); const segments = plejd.path.split('/');
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', ''); let plejdSerialNumber = segments[segments.length - 1].replace('dev_', '');
fixedPlejdPath = fixedPlejdPath.replace(/_/g, ''); plejdSerialNumber = plejdSerialNumber.replace(/_/g, '');
plejd.device = this.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath); plejd.device = this.deviceRegistry.getPhysicalDevice(plejdSerialNumber);
if (plejd.device) { if (plejd.device) {
logger.debug( logger.debug(
@ -204,7 +206,7 @@ class PlejBLEHandler extends EventEmitter {
); );
this.bleDevices.push(plejd); this.bleDevices.push(plejd);
} else { } else {
logger.warn(`Device registry does not contain device with serial ${fixedPlejdPath}`); logger.warn(`Device registry does not contain device with serial ${plejdSerialNumber}`);
} }
} catch (err) { } catch (err) {
logger.error(`Failed inspecting ${path}. `, err); logger.error(`Failed inspecting ${path}. `, err);
@ -796,7 +798,7 @@ class PlejBLEHandler extends EventEmitter {
return; return;
} }
const deviceId = decoded.readUInt8(0); const bleOutputAddress = decoded.readUInt8(0);
// Bytes 2-3 is Command/Request // Bytes 2-3 is Command/Request
const cmd = decoded.readUInt16BE(3); const cmd = decoded.readUInt16BE(3);
@ -810,38 +812,41 @@ class PlejBLEHandler extends EventEmitter {
logger.silly(`Dim: ${dim.toString(16)}, full precision: ${dimFull.toString(16)}`); logger.silly(`Dim: ${dim.toString(16)}, full precision: ${dimFull.toString(16)}`);
} }
const deviceName = this.deviceRegistry.getDeviceName(deviceId); const device = this.deviceRegistry.getOutputDeviceByBleOutputAddress(bleOutputAddress);
const deviceName = device ? device.name : 'Unknown';
const outputUniqueId = device ? device.uniqueId : null;
if (Logger.shouldLog('verbose')) { if (Logger.shouldLog('verbose')) {
// decoded.toString() could potentially be expensive // decoded.toString() could potentially be expensive
logger.verbose(`Raw event received: ${decoded.toString('hex')}`); logger.verbose(`Raw event received: ${decoded.toString('hex')}`);
logger.verbose( logger.verbose(
`Decoded: Device ${deviceId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`, `Decoded: Device ${outputUniqueId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`,
); );
} }
let command; let command;
let data = {}; let data = {};
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) { if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
logger.debug(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`); logger.debug(
`${deviceName} (${outputUniqueId}) got state+dim update. S: ${state}, D: ${dim}`,
);
command = COMMANDS.DIM; command = COMMANDS.DIM;
data = { state, dim }; data = { state, dim };
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data); this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
} else if (cmd === BLE_CMD_STATE_CHANGE) { } else if (cmd === BLE_CMD_STATE_CHANGE) {
logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`); logger.debug(`${deviceName} (${outputUniqueId}) got state update. S: ${state}`);
command = state ? COMMANDS.TURN_ON : COMMANDS.TURN_OFF; command = state ? COMMANDS.TURN_ON : COMMANDS.TURN_OFF;
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data); this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
} else if (cmd === BLE_CMD_SCENE_TRIG) { } else if (cmd === BLE_CMD_SCENE_TRIG) {
const sceneId = state; const sceneId = state;
const sceneName = this.deviceRegistry.getSceneName(sceneId); const sceneName = this.deviceRegistry.getSceneName(sceneId);
logger.debug( logger.debug(`${sceneName} (${sceneId}) scene triggered (device id ${outputUniqueId}).`);
`${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`,
);
command = COMMANDS.TRIGGER_SCENE; command = COMMANDS.TRIGGER_SCENE;
data = { sceneId }; data = { sceneId };
this.emit(PlejBLEHandler.EVENTS.commandReceived, deviceId, command, data); this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
} else if (cmd === BLE_CMD_TIME_UPDATE) { } else if (cmd === BLE_CMD_TIME_UPDATE) {
const now = new Date(); const now = new Date();
// Guess Plejd timezone based on HA time zone // Guess Plejd timezone based on HA time zone
@ -851,7 +856,7 @@ class PlejBLEHandler extends EventEmitter {
const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000; const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000;
const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000); const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000);
if ( if (
deviceId !== BLE_BROADCAST_DEVICE_ID bleOutputAddress !== BLE_BROADCAST_DEVICE_ID
|| Logger.shouldLog('verbose') || Logger.shouldLog('verbose')
|| Math.abs(diffSeconds) > 60 || Math.abs(diffSeconds) > 60
) { ) {
@ -863,7 +868,7 @@ class PlejBLEHandler extends EventEmitter {
logger.warn( logger.warn(
`Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`, `Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`,
); );
if (this.connectedDevice && deviceId === this.connectedDevice.id) { if (this.connectedDevice && bleOutputAddress === this.connectedDevice.id) {
// Requested time sync by us // Requested time sync by us
const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess; const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess;
logger.info(`Setting time to ${now.toString()}`); logger.info(`Setting time to ${now.toString()}`);
@ -881,7 +886,7 @@ class PlejBLEHandler extends EventEmitter {
); );
} }
} }
} else if (deviceId !== BLE_BROADCAST_DEVICE_ID) { } else if (bleOutputAddress !== BLE_BROADCAST_DEVICE_ID) {
logger.info('Got time response. Plejd clock time in sync with Home Assistant time'); logger.info('Got time response. Plejd clock time in sync with Home Assistant time');
} }
} }
@ -889,19 +894,19 @@ class PlejBLEHandler extends EventEmitter {
logger.verbose( logger.verbose(
`Command ${cmd.toString(16)} unknown. ${decoded.toString( `Command ${cmd.toString(16)} unknown. ${decoded.toString(
'hex', 'hex',
)}. Device ${deviceName} (${deviceId})`, )}. Device ${deviceName} (${bleOutputAddress}: ${outputUniqueId})`,
); );
} }
} }
_createHexPayload( _createHexPayload(
deviceId, bleOutputAddress,
command, command,
hexDataString, hexDataString,
requestResponseCommand = BLE_REQUEST_NO_RESPONSE, requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
) { ) {
return this._createPayload( return this._createPayload(
deviceId, bleOutputAddress,
command, command,
5 + Math.ceil(hexDataString.length / 2), 5 + Math.ceil(hexDataString.length / 2),
(payload) => payload.write(hexDataString, 5, 'hex'), (payload) => payload.write(hexDataString, 5, 'hex'),
@ -911,14 +916,14 @@ class PlejBLEHandler extends EventEmitter {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_createPayload( _createPayload(
deviceId, bleOutputAddress,
command, command,
bufferLength, bufferLength,
payloadBufferAddDataFunc, payloadBufferAddDataFunc,
requestResponseCommand = BLE_REQUEST_NO_RESPONSE, requestResponseCommand = BLE_REQUEST_NO_RESPONSE,
) { ) {
const payload = Buffer.alloc(bufferLength); const payload = Buffer.alloc(bufferLength);
payload.writeUInt8(deviceId); payload.writeUInt8(bleOutputAddress);
payload.writeUInt16BE(requestResponseCommand, 1); payload.writeUInt16BE(requestResponseCommand, 1);
payload.writeUInt16BE(command, 3); payload.writeUInt16BE(command, 3);
payloadBufferAddDataFunc(payload); payloadBufferAddDataFunc(payload);
@ -945,12 +950,12 @@ class PlejBLEHandler extends EventEmitter {
let ct = cipher.update(buf).toString('hex'); let ct = cipher.update(buf).toString('hex');
ct += cipher.final().toString('hex'); ct += cipher.final().toString('hex');
ct = Buffer.from(ct, 'hex'); const ctBuf = Buffer.from(ct, 'hex');
let output = ''; let output = '';
for (let i = 0, { length } = data; i < length; i++) { for (let i = 0, { length } = data; i < length; i++) {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
output += String.fromCharCode(data[i] ^ ct[i % 16]); output += String.fromCharCode(data[i] ^ ctBuf[i % 16]);
} }
return Buffer.from(output, 'ascii'); return Buffer.from(output, 'ascii');

View file

@ -12,11 +12,13 @@ const MAX_RETRY_COUNT = 10; // Could be made a setting
class PlejdDeviceCommunication extends EventEmitter { class PlejdDeviceCommunication extends EventEmitter {
bleConnected; bleConnected;
bleDeviceTransitionTimers = {}; bleOutputTransitionTimers = {};
plejdBleHandler; plejdBleHandler;
config; config;
/** @type {import('./DeviceRegistry')} */ /** @type {import('./DeviceRegistry')} */
deviceRegistry; deviceRegistry;
// eslint-disable-next-line max-len
/** @type {{uniqueOutputId: string, command: string, data: any, shouldRetry: boolean, retryCount?: number}[]} */
writeQueue = []; writeQueue = [];
writeQueueRef = null; writeQueueRef = null;
@ -35,7 +37,7 @@ class PlejdDeviceCommunication extends EventEmitter {
} }
cleanup() { cleanup() {
Object.values(this.bleDeviceTransitionTimers).forEach((t) => clearTimeout(t)); Object.values(this.bleOutputTransitionTimers).forEach((t) => clearTimeout(t));
this.plejdBleHandler.cleanup(); this.plejdBleHandler.cleanup();
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived); this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.commandReceived);
this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected); this.plejdBleHandler.removeAllListeners(PlejBLEHandler.EVENTS.connected);
@ -47,7 +49,10 @@ class PlejdDeviceCommunication extends EventEmitter {
this.cleanup(); this.cleanup();
this.bleConnected = false; this.bleConnected = false;
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
this.plejdBleHandler.on(PlejBLEHandler.EVENTS.commandReceived, (deviceId, command, data) => this._bleCommandReceived(deviceId, command, data)); this.plejdBleHandler.on(
PlejBLEHandler.EVENTS.commandReceived,
(uniqueOutputId, command, data) => this._bleCommandReceived(uniqueOutputId, command, data),
);
this.plejdBleHandler.on(PlejBLEHandler.EVENTS.connected, () => { this.plejdBleHandler.on(PlejBLEHandler.EVENTS.connected, () => {
logger.info('Bluetooth connected. Plejd BLE up and running!'); logger.info('Bluetooth connected. Plejd BLE up and running!');
@ -71,42 +76,42 @@ class PlejdDeviceCommunication extends EventEmitter {
} }
} }
turnOn(deviceId, command) { turnOn(uniqueOutputId, command) {
const deviceName = this.deviceRegistry.getOutputDeviceName(deviceId); const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
logger.info( logger.info(
`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${ `Plejd got turn on command for ${deviceName} (${uniqueOutputId}), brightness ${
command.brightness
}${command.transition ? `, transition: ${command.transition}` : ''}`,
);
this._transitionTo(uniqueOutputId, command.brightness, command.transition, deviceName);
}
turnOff(uniqueOutputId, command) {
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
logger.info(
`Plejd got turn off command for ${deviceName} (${uniqueOutputId})${
command.transition ? `, transition: ${command.transition}` : '' command.transition ? `, transition: ${command.transition}` : ''
}`, }`,
); );
this._transitionTo(deviceId, command.brightness, command.transition, deviceName); this._transitionTo(uniqueOutputId, 0, command.transition, deviceName);
} }
turnOff(deviceId, command) { _bleCommandReceived(uniqueOutputId, command, data) {
const deviceName = this.deviceRegistry.getOutputDeviceName(deviceId);
logger.info(
`Plejd got turn off command for ${deviceName} (${deviceId})${
command.transition ? `, transition: ${command.transition}` : ''
}`,
);
this._transitionTo(deviceId, 0, command.transition, deviceName);
}
_bleCommandReceived(deviceId, command, data) {
try { try {
if (command === COMMANDS.DIM) { if (command === COMMANDS.DIM) {
this.deviceRegistry.setOutputState(deviceId, data.state, data.dim); this.deviceRegistry.setOutputState(uniqueOutputId, data.state, data.dim);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: !!data.state, state: !!data.state,
brightness: data.dim, brightness: data.dim,
}); });
} else if (command === COMMANDS.TURN_ON) { } else if (command === COMMANDS.TURN_ON) {
this.deviceRegistry.setOutputState(deviceId, true); this.deviceRegistry.setOutputState(uniqueOutputId, true);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: 1, state: 1,
}); });
} else if (command === COMMANDS.TURN_OFF) { } else if (command === COMMANDS.TURN_OFF) {
this.deviceRegistry.setOutputState(deviceId, false); this.deviceRegistry.setOutputState(uniqueOutputId, false);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, deviceId, { this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: 0, state: 0,
}); });
} else if (command === COMMANDS.TRIGGER_SCENE) { } else if (command === COMMANDS.TRIGGER_SCENE) {
@ -119,18 +124,18 @@ class PlejdDeviceCommunication extends EventEmitter {
} }
} }
_clearDeviceTransitionTimer(deviceId) { _clearDeviceTransitionTimer(uniqueOutputId) {
if (this.bleDeviceTransitionTimers[deviceId]) { if (this.bleOutputTransitionTimers[uniqueOutputId]) {
clearInterval(this.bleDeviceTransitionTimers[deviceId]); clearInterval(this.bleOutputTransitionTimers[uniqueOutputId]);
} }
} }
_transitionTo(deviceId, targetBrightness, transition, deviceName) { _transitionTo(uniqueOutputId, targetBrightness, transition, deviceName) {
const device = this.deviceRegistry.getOutputDevice(deviceId); const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
const initialBrightness = device ? device.state && device.dim : null; const initialBrightness = device ? device.state && device.dim : null;
this._clearDeviceTransitionTimer(deviceId); this._clearDeviceTransitionTimer(uniqueOutputId);
const isDimmable = this.deviceRegistry.getOutputDevice(deviceId).dimmable; const isDimmable = this.deviceRegistry.getOutputDevice(uniqueOutputId).dimmable;
if ( if (
transition > 1 transition > 1
@ -165,7 +170,7 @@ class PlejdDeviceCommunication extends EventEmitter {
let nSteps = 0; let nSteps = 0;
this.bleDeviceTransitionTimers[deviceId] = setInterval(() => { this.bleOutputTransitionTimers[uniqueOutputId] = setInterval(() => {
const tElapsedMs = new Date().getTime() - dtStart.getTime(); const tElapsedMs = new Date().getTime() - dtStart.getTime();
let tElapsed = tElapsedMs / 1000; let tElapsed = tElapsedMs / 1000;
@ -179,20 +184,20 @@ class PlejdDeviceCommunication extends EventEmitter {
if (tElapsed === transition) { if (tElapsed === transition) {
nSteps++; nSteps++;
this._clearDeviceTransitionTimer(deviceId); this._clearDeviceTransitionTimer(uniqueOutputId);
newBrightness = targetBrightness; newBrightness = targetBrightness;
logger.debug( logger.debug(
`Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${ `Queueing finalize ${deviceName} (${uniqueOutputId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
tElapsedMs / (nSteps || 1) tElapsedMs / (nSteps || 1)
} ms.`, } ms.`,
); );
this._setBrightness(deviceId, newBrightness, true, deviceName); this._setBrightness(uniqueOutputId, newBrightness, true, deviceName);
} else { } else {
nSteps++; nSteps++;
logger.verbose( logger.verbose(
`Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`, `Queueing dim transition for ${deviceName} (${uniqueOutputId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
); );
this._setBrightness(deviceId, newBrightness, false, deviceName); this._setBrightness(uniqueOutputId, newBrightness, false, deviceName);
} }
}, transitionInterval); }, transitionInterval);
} else { } else {
@ -201,34 +206,34 @@ class PlejdDeviceCommunication extends EventEmitter {
`Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`, `Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`,
); );
} }
this._setBrightness(deviceId, targetBrightness, true, deviceName); this._setBrightness(uniqueOutputId, targetBrightness, true, deviceName);
} }
} }
_setBrightness(deviceId, brightness, shouldRetry, deviceName) { _setBrightness(unqiueOutputId, brightness, shouldRetry, deviceName) {
if (!brightness && brightness !== 0) { if (!brightness && brightness !== 0) {
logger.debug( logger.debug(
`Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`, `Queueing turn on ${deviceName} (${unqiueOutputId}). No brightness specified, setting DIM to previous.`,
); );
this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_ON, null, shouldRetry); this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_ON, null, shouldRetry);
} else if (brightness <= 0) { } else if (brightness <= 0) {
logger.debug(`Queueing turn off ${deviceId}`); logger.debug(`Queueing turn off ${unqiueOutputId}`);
this._appendCommandToWriteQueue(deviceId, COMMANDS.TURN_OFF, null, shouldRetry); this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_OFF, null, shouldRetry);
} else { } else {
if (brightness > 255) { if (brightness > 255) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
brightness = 255; brightness = 255;
} }
logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`); logger.debug(`Queueing ${unqiueOutputId} set brightness to ${brightness}`);
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
this._appendCommandToWriteQueue(deviceId, COMMANDS.DIM, brightness, shouldRetry); this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.DIM, brightness, shouldRetry);
} }
} }
_appendCommandToWriteQueue(deviceId, command, data, shouldRetry) { _appendCommandToWriteQueue(uniqueOutputId, command, data, shouldRetry) {
this.writeQueue.unshift({ this.writeQueue.unshift({
deviceId, uniqueOutputId,
command, command,
data, data,
shouldRetry, shouldRetry,
@ -250,28 +255,28 @@ class PlejdDeviceCommunication extends EventEmitter {
return; return;
} }
const queueItem = this.writeQueue.pop(); const queueItem = this.writeQueue.pop();
const deviceName = this.deviceRegistry.getOutputDeviceName(queueItem.deviceId); const deviceName = this.deviceRegistry.getOutputDeviceName(queueItem.uniqueOutputId);
logger.debug( logger.debug(
`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${ `Write queue: Processing ${deviceName} (${queueItem.uniqueOutputId}). Command ${
queueItem.command queueItem.command
}${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${ }${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${
this.writeQueue.length this.writeQueue.length
}`, }`,
); );
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) { if (this.writeQueue.some((item) => item.uniqueOutputId === queueItem.uniqueOutputId)) {
logger.verbose( logger.verbose(
`Skipping ${deviceName} (${queueItem.deviceId}) ` `Skipping ${deviceName} (${queueItem.uniqueOutputId}) `
+ `${queueItem.command} due to more recent command in queue.`, + `${queueItem.command} due to more recent command in queue.`,
); );
// Skip commands if new ones exist for the same deviceId // Skip commands if new ones exist for the same uniqueOutputId
// still process all messages in order // still process all messages in order
} else { } else {
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
try { try {
await this.plejdBleHandler.sendCommand( await this.plejdBleHandler.sendCommand(
queueItem.command, queueItem.command,
queueItem.deviceId, queueItem.uniqueOutputId,
queueItem.data, queueItem.data,
); );
} catch (err) { } catch (err) {
@ -282,7 +287,7 @@ class PlejdDeviceCommunication extends EventEmitter {
this.writeQueue.push(queueItem); // Add back to top of queue to be processed next; this.writeQueue.push(queueItem); // Add back to top of queue to be processed next;
} else { } else {
logger.error( logger.error(
`Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.deviceId}). Command ${queueItem.command} failed.`, `Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.uniqueOutputId}). Command ${queueItem.command} failed.`,
); );
break; break;
} }

View file

@ -1,18 +1,20 @@
const SceneStep = require('./SceneStep'); const SceneStep = require('./SceneStep');
class Scene { class Scene {
constructor(idx, scene, steps) { /**
* @param {import('./DeviceRegistry')} deviceRegistry
* @param {number} idx
* @param {import("./types/ApiSite").Scene} scene
*/
constructor(deviceRegistry, idx, scene) {
this.id = idx; this.id = idx;
this.title = scene.title; this.title = scene.title;
this.sceneId = scene.sceneId; this.sceneId = scene.sceneId;
const sceneSteps = steps.filter((x) => x.sceneId === scene.sceneId); this.steps = deviceRegistry
this.steps = []; .getApiSite()
.sceneSteps.filter((step) => step.sceneId === scene.sceneId)
// eslint-disable-next-line no-restricted-syntax .map((step) => new SceneStep(step));
for (const step of sceneSteps) {
this.steps.push(new SceneStep(step));
}
} }
} }

View file

@ -3,25 +3,28 @@ const Scene = require('./Scene');
const logger = Logger.getLogger('scene-manager'); const logger = Logger.getLogger('scene-manager');
class SceneManager { class SceneManager {
/** @private @type {import('./DeviceRegistry')} */
deviceRegistry; deviceRegistry;
plejdBle; /** @private @type {import('./PlejdDeviceCommunication')} */
plejdDeviceCommunication;
/** @private @type {Object.<number,Scene>} */
scenes; scenes;
constructor(deviceRegistry, plejdBle) { constructor(deviceRegistry, plejdDeviceCommunication) {
this.deviceRegistry = deviceRegistry; this.deviceRegistry = deviceRegistry;
this.plejdBle = plejdBle; this.plejdDeviceCommunication = plejdDeviceCommunication;
this.scenes = {}; this.scenes = {};
} }
init() { init() {
const scenes = this.deviceRegistry.apiSite.scenes.filter( const scenes = this.deviceRegistry
(x) => x.hiddenFromSceneList === false, .getApiSite()
); .scenes.filter((x) => x.hiddenFromSceneList === false);
this.scenes = {}; this.scenes = {};
scenes.forEach((scene) => { scenes.forEach((scene) => {
const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId]; const idx = this.deviceRegistry.getApiSite().sceneIndex[scene.sceneId];
this.scenes[idx] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps); this.scenes[idx] = new Scene(this.deviceRegistry, idx, scene);
}); });
} }
@ -34,14 +37,15 @@ class SceneManager {
} }
scene.steps.forEach((step) => { scene.steps.forEach((step) => {
const device = this.deviceRegistry.getDeviceBySerialNumber(step.deviceId); const uniqueId = this.deviceRegistry.getUniqueOutputId(step.deviceId, step.output);
const device = this.deviceRegistry.getOutputDevice(uniqueId);
if (device) { if (device) {
if (device.dimmable && step.state) { if (device.dimmable && step.state) {
this.plejdBle.turnOn(device.id, { brightness: step.brightness }); this.plejdDeviceCommunication.turnOn(uniqueId, { brightness: step.brightness });
} else if (!device.dimmable && step.state) { } else if (!device.dimmable && step.state) {
this.plejdBle.turnOn(device.id, {}); this.plejdDeviceCommunication.turnOn(uniqueId, {});
} else if (!step.state) { } else if (!step.state) {
this.plejdBle.turnOff(device.id, {}); this.plejdDeviceCommunication.turnOff(uniqueId, {});
} }
} }
}); });

View file

@ -1,7 +1,11 @@
class SceneStep { class SceneStep {
/**
* @param {import("./types/ApiSite").SceneStep} step
*/
constructor(step) { constructor(step) {
this.sceneId = step.sceneId; this.sceneId = step.sceneId;
this.deviceId = step.deviceId; this.deviceId = step.deviceId;
this.output = step.output;
this.state = step.state === 'On' ? 1 : 0; this.state = step.state === 'On' ? 1 : 0;
this.brightness = step.value; this.brightness = step.value;
} }

View file

@ -3,7 +3,7 @@
export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice }; export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice };
export interface OutputDevice { export interface OutputDevice {
bleDeviceIndex: number; bleOutputAddress: number;
deviceId: string; deviceId: string;
dim?: number; dim?: number;
dimmable: boolean; dimmable: boolean;
@ -13,7 +13,7 @@ export interface OutputDevice {
name: string; name: string;
output: number; output: number;
roomId: string; roomId: string;
state: number | undefined; state: boolean | undefined;
type: string; type: string;
typeName: string; typeName: string;
version: string; version: string;

View file

@ -1,6 +1,6 @@
/* eslint-disable no-use-before-define */ /* eslint-disable no-use-before-define */
import { ApiSite } from './ApiSite'; import { ApiSite } from './ApiSite.d.ts';
export type PlejdApi = { export type PlejdApi = {
config: any; config: any;