Merge pull request #127 from SweVictor/feature/improved-transitions
Improve transitioning of brightness
This commit is contained in:
commit
b02e33369e
2 changed files with 120 additions and 76 deletions
13
README.md
13
README.md
|
|
@ -103,6 +103,19 @@ includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/o
|
||||||
connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds.
|
connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds.
|
||||||
writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50.
|
writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50.
|
||||||
|
|
||||||
|
## Transitions
|
||||||
|
Transitions from Home Assistant are supported (for dimmable devices) when transition is longer than 1 second. Plejd will do a bit of internal transitioning (default soft start is 0.1 seconds).
|
||||||
|
|
||||||
|
This implementation will transition each device independently, meaning that brightness change might be choppy if transitioning many devices at once or a changing brightness a lot in a limited time. Hassio-plejd's communication channel seems to handle a few updates per second, this is the combined value for all devices.
|
||||||
|
|
||||||
|
Transition points will be skipped if the queue of messages to be sent is over a certain threshold, by default equal to the number of devices in the system. Total transition time is prioritized rather than smoothness.
|
||||||
|
|
||||||
|
Recommendations
|
||||||
|
* Only transition a few devices at a time when possible
|
||||||
|
* Expect 5-10 brightness changes per second, meaning 5 devices => 1-2 updates per device per second
|
||||||
|
* ... meaning that SLOW transitions will work well (wake-up light, gradually fade over a minute, ...), but quick ones will only work well for few devices or small relative changes in brightness
|
||||||
|
* When experiencing choppy quick transitions, turn transitioning off and let the Plejd hardware do the work instead
|
||||||
|
|
||||||
## I want voice control!
|
## I want voice control!
|
||||||
With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information:
|
With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information:
|
||||||
https://www.home-assistant.io/integrations/google_assistant/
|
https://www.home-assistant.io/integrations/google_assistant/
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const EventEmitter = require('events');
|
||||||
let debug = '';
|
let debug = '';
|
||||||
|
|
||||||
const getLogger = () => {
|
const getLogger = () => {
|
||||||
const consoleLogger = msg => console.log('plejd-ble', msg);
|
const consoleLogger = (...msg) => console.log('plejd-ble', ...msg);
|
||||||
if (debug === 'console') {
|
if (debug === 'console') {
|
||||||
return consoleLogger;
|
return consoleLogger;
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +39,9 @@ const BLUEZ_DEVICE_ID = 'org.bluez.Device1';
|
||||||
const GATT_SERVICE_ID = 'org.bluez.GattService1';
|
const GATT_SERVICE_ID = 'org.bluez.GattService1';
|
||||||
const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1';
|
const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1';
|
||||||
|
|
||||||
|
const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting
|
||||||
|
const MAX_WRITEQUEUE_LENGTH_TARGET = 0; // Could be made a setting. 0 => queue length = numDevices => 1 command pending per device max
|
||||||
|
|
||||||
class PlejdService extends EventEmitter {
|
class PlejdService extends EventEmitter {
|
||||||
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime, keepAlive = false) {
|
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime, keepAlive = false) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -49,6 +52,7 @@ class PlejdService extends EventEmitter {
|
||||||
this.connectedDevice = null;
|
this.connectedDevice = null;
|
||||||
this.plejdService = null;
|
this.plejdService = null;
|
||||||
this.bleDevices = [];
|
this.bleDevices = [];
|
||||||
|
this.bleDeviceTransitionTimers = {};
|
||||||
this.plejdDevices = {};
|
this.plejdDevices = {};
|
||||||
this.devices = devices;
|
this.devices = devices;
|
||||||
this.connectEventHooked = false;
|
this.connectEventHooked = false;
|
||||||
|
|
@ -56,6 +60,9 @@ class PlejdService extends EventEmitter {
|
||||||
this.writeQueueWaitTime = writeQueueWaitTime;
|
this.writeQueueWaitTime = writeQueueWaitTime;
|
||||||
this.writeQueue = [];
|
this.writeQueue = [];
|
||||||
this.writeQueueRef = null;
|
this.writeQueueRef = null;
|
||||||
|
|
||||||
|
this.maxQueueLengthTarget = MAX_WRITEQUEUE_LENGTH_TARGET || this.devices.length || 5;
|
||||||
|
logger('Max global transition queue length target', this.maxQueueLengthTarget)
|
||||||
|
|
||||||
// Holds a reference to all characteristics
|
// Holds a reference to all characteristics
|
||||||
this.characteristics = {
|
this.characteristics = {
|
||||||
|
|
@ -79,7 +86,7 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectedDevice = null;
|
this.connectedDevice = null;
|
||||||
this.bleDevices = [];
|
|
||||||
this.characteristics = {
|
this.characteristics = {
|
||||||
data: null,
|
data: null,
|
||||||
lastData: null,
|
lastData: null,
|
||||||
|
|
@ -146,10 +153,10 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _internalInit() {
|
async _internalInit() {
|
||||||
logger('got ' + this.bleDevices.length + ' device(s).');
|
logger('got ', this.bleDevices.length, ' device(s).');
|
||||||
|
|
||||||
for (const plejd of this.bleDevices) {
|
for (const plejd of this.bleDevices) {
|
||||||
logger('inspecting ' + plejd['path']);
|
logger('inspecting ', plejd['path']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd['path']);
|
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd['path']);
|
||||||
|
|
@ -164,7 +171,7 @@ class PlejdService extends EventEmitter {
|
||||||
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
|
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
|
||||||
plejd['device'] = this.devices.find(x => x.serialNumber === fixedPlejdPath);
|
plejd['device'] = this.devices.find(x => x.serialNumber === fixedPlejdPath);
|
||||||
|
|
||||||
logger('discovered ' + plejd['path'] + ' with rssi ' + plejd['rssi']);
|
logger('discovered ', plejd['path'] + ' with rssi ' + plejd['rssi']);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('plejd-ble: failed inspecting ' + plejd['path'] + ' error: ' + err);
|
console.log('plejd-ble: failed inspecting ' + plejd['path'] + ' error: ' + err);
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +205,7 @@ class PlejdService extends EventEmitter {
|
||||||
for (let path of managedPaths) {
|
for (let path of managedPaths) {
|
||||||
const pathInterfaces = Object.keys(managedObjects[path]);
|
const pathInterfaces = Object.keys(managedObjects[path]);
|
||||||
if (pathInterfaces.indexOf(iface) > -1) {
|
if (pathInterfaces.indexOf(iface) > -1) {
|
||||||
logger('found ble interface \'' + iface + '\' at ' + path);
|
logger('found ble interface \'', iface, '\' at ', path);
|
||||||
try {
|
try {
|
||||||
const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
||||||
return [path, adapterObject.getInterface(iface), adapterObject];
|
return [path, adapterObject.getInterface(iface), adapterObject];
|
||||||
|
|
@ -212,12 +219,12 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInterfacesAdded(path, interfaces) {
|
async onInterfacesAdded(path, interfaces) {
|
||||||
const [adapter, dev, service, characteristic] = path.split('/').slice(3);
|
// const [adapter, dev, service, characteristic] = path.split('/').slice(3);
|
||||||
const interfaceKeys = Object.keys(interfaces);
|
const interfaceKeys = Object.keys(interfaces);
|
||||||
|
|
||||||
if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
||||||
if (interfaces[BLUEZ_DEVICE_ID]['UUIDs'].value.indexOf(PLEJD_SERVICE) > -1) {
|
if (interfaces[BLUEZ_DEVICE_ID]['UUIDs'].value.indexOf(PLEJD_SERVICE) > -1) {
|
||||||
logger('found Plejd service on ' + path);
|
logger('found Plejd service on ', path);
|
||||||
this.bleDevices.push({
|
this.bleDevices.push({
|
||||||
'path': path
|
'path': path
|
||||||
});
|
});
|
||||||
|
|
@ -236,89 +243,113 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
turnOn(id, command) {
|
turnOn(id, command) {
|
||||||
logger('turning on ' + id + ' at brightness ' + (!command.brightness ? 255 : command.brightness));
|
console.log('Plejd got turn on command for ', id, ', brightness ', command.brightness, ', transition ', command.transition);
|
||||||
const brightness = command.brightness ? command.brightness : 0;
|
this._transitionTo(id, command.brightness, command.transition);
|
||||||
|
|
||||||
if (command.transition) {
|
|
||||||
// we have a transition time, split the target brightness
|
|
||||||
// into pieces spread of the transition time
|
|
||||||
const steps = command.transition * 2;
|
|
||||||
const brightnessStep = brightness / steps;
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
const transitionRef = setInterval(() => {
|
|
||||||
let currentBrightness = parseInt((brightnessStep * i) + 1);
|
|
||||||
if (currentBrightness > 254) {
|
|
||||||
currentBrightness = 254;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._turnOn(id, currentBrightness);
|
|
||||||
|
|
||||||
if (i >= steps) {
|
|
||||||
clearInterval(transitionRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
i++;
|
|
||||||
}, 400);
|
|
||||||
} else {
|
|
||||||
this._turnOn(id, brightness);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_turnOn(id, brightness) {
|
|
||||||
var payload;
|
|
||||||
if (!brightness || brightness === 0) {
|
|
||||||
logger('no brightness specified, setting to previous known.');
|
|
||||||
payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex');
|
|
||||||
} else {
|
|
||||||
logger('brightness is ' + brightness);
|
|
||||||
brightness = brightness << 8 | brightness;
|
|
||||||
payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.writeQueue.unshift(payload);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
turnOff(id, command) {
|
turnOff(id, command) {
|
||||||
logger('turning off ' + id);
|
console.log('Plejd got turn off command for ', id, ', transition ', command.transition);
|
||||||
|
this._transitionTo(id, 0, command.transition);
|
||||||
|
}
|
||||||
|
|
||||||
if (command.transition) {
|
|
||||||
// we have a transition time, split the target brightness (which will be 0)
|
|
||||||
// into pieces spread of the transition time
|
|
||||||
const initialBrightness = this.plejdDevices[id] ? this.plejdDevices[id].dim : 250;
|
|
||||||
console.log('initial brightness for ' + id + ' is ' + initialBrightness);
|
|
||||||
|
|
||||||
const steps = command.transition * 2;
|
_clearDeviceTransitionTimer(id) {
|
||||||
const brightnessStep = initialBrightness / steps;
|
if (this.bleDeviceTransitionTimers[id]) {
|
||||||
let currentBrightness = initialBrightness;
|
clearInterval(this.bleDeviceTransitionTimers[id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let i = 0;
|
_transitionTo(id, targetBrightness, transition) {
|
||||||
const transitionRef = setInterval(() => {
|
const initialBrightness = this.plejdDevices[id] ? this.plejdDevices[id].dim : null;
|
||||||
currentBrightness = parseInt(initialBrightness - (brightnessStep * i));
|
this._clearDeviceTransitionTimer(id);
|
||||||
if (currentBrightness <= 0 || i >= steps) {
|
|
||||||
clearInterval(transitionRef);
|
|
||||||
|
|
||||||
// finally, we turn it off
|
const isDimmable = this.devices.find(d => d.id === id).dimmable;
|
||||||
this._turnOff(id);
|
|
||||||
return;
|
if (transition > 1 && isDimmable && (initialBrightness || initialBrightness === 0) && (targetBrightness || targetBrightness === 0) && targetBrightness !== initialBrightness) {
|
||||||
|
// Transition time set, known initial and target brightness
|
||||||
|
// Calculate transition interval time based on delta brightness and max steps per second
|
||||||
|
// During transition, measure actual transition interval time and adjust stepping continously
|
||||||
|
// If transition <= 1 second, Plejd will do a better job than we can in transitioning so transitioning will be skipped
|
||||||
|
|
||||||
|
const deltaBrightness = targetBrightness - initialBrightness;
|
||||||
|
const transitionSteps = Math.min(Math.abs(deltaBrightness), MAX_TRANSITION_STEPS_PER_SECOND * transition);
|
||||||
|
const transitionInterval = transition * 1000 / transitionSteps;
|
||||||
|
|
||||||
|
logger('transitioning from', initialBrightness, 'to', targetBrightness, 'in', transition, 'seconds.');
|
||||||
|
logger('delta brightness', deltaBrightness, ', steps ', transitionSteps, ', interval', transitionInterval, 'ms');
|
||||||
|
|
||||||
|
const dtStart = new Date();
|
||||||
|
|
||||||
|
let nSteps = 0;
|
||||||
|
let nSkippedSteps = 0;
|
||||||
|
|
||||||
|
this.bleDeviceTransitionTimers[id] = setInterval(() => {
|
||||||
|
let tElapsedMs = (new Date().getTime() - dtStart.getTime());
|
||||||
|
let tElapsed = tElapsedMs / 1000;
|
||||||
|
|
||||||
|
if (tElapsed > transition || tElapsed < 0) {
|
||||||
|
tElapsed = transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._turnOn(id, currentBrightness);
|
let newBrightness = parseInt(initialBrightness + deltaBrightness * tElapsed / transition);
|
||||||
|
|
||||||
i++;
|
if (tElapsed === transition) {
|
||||||
}, 500);
|
nSteps++;
|
||||||
} else {
|
this._clearDeviceTransitionTimer(id);
|
||||||
this._turnOff(id);
|
newBrightness = targetBrightness;
|
||||||
|
logger('Completing transition from', initialBrightness, 'to', targetBrightness, 'in ', tElapsedMs, 'ms. Done steps', nSteps, ', skipped ' + nSkippedSteps + '. Average interval', tElapsedMs/(nSteps||1), 'ms.');
|
||||||
|
this._setBrightness(id, newBrightness);
|
||||||
|
}
|
||||||
|
else if (this.writeQueue.length <= this.maxQueueLengthTarget) {
|
||||||
|
nSteps++;
|
||||||
|
this._setBrightness(id, newBrightness);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nSkippedSteps++;
|
||||||
|
logger('Skipping transition step due to write queue full as configured. Queue length', this.writeQueue.length, ', max', this.maxQueueLengthTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, transitionInterval);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (transition && isDimmable) {
|
||||||
|
logger('Could not transition light change. Either initial value is unknown or change is too small. Requested from', initialBrightness, 'to', targetBrightness)
|
||||||
|
}
|
||||||
|
this._setBrightness(id, targetBrightness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setBrightness(id, brightness) {
|
||||||
|
if (!brightness && brightness !== 0) {
|
||||||
|
logger('no brightness specified, setting ', id, ' to previous known.');
|
||||||
|
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex');
|
||||||
|
this.writeQueue.unshift(payload);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (brightness <= 0) {
|
||||||
|
this._turnOff(id);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (brightness > 255) {
|
||||||
|
brightness = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('Setting ', id, 'brightness to ' + brightness);
|
||||||
|
brightness = brightness << 8 | brightness;
|
||||||
|
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex');
|
||||||
|
}
|
||||||
|
this.writeQueue.unshift(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_turnOff(id) {
|
_turnOff(id) {
|
||||||
|
logger('Turning off ', id);
|
||||||
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex');
|
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex');
|
||||||
this.writeQueue.unshift(payload);
|
this.writeQueue.unshift(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerScene(sceneIndex) {
|
triggerScene(sceneIndex) {
|
||||||
console.log('triggering scene with ID ' + sceneIndex);
|
console.log('triggering scene with ID', sceneIndex);
|
||||||
this.sceneManager.executeScene(sceneIndex, this);
|
this.sceneManager.executeScene(sceneIndex, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,7 +385,7 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('plejd-ble: sending ' + data.length + ' byte(s) of data to Plejd');
|
logger('sending ', data.length, ' byte(s) of data to Plejd', data);
|
||||||
const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data);
|
const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data);
|
||||||
await this.characteristics.data.WriteValue([...encryptedData], {});
|
await this.characteristics.data.WriteValue([...encryptedData], {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -567,7 +598,7 @@ class PlejdService extends EventEmitter {
|
||||||
const cmd = decoded.toString('hex', 3, 5);
|
const cmd = decoded.toString('hex', 3, 5);
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
logger('raw event received: ' + decoded.toString('hex'));
|
logger('raw event received: ', decoded.toString('hex'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
|
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
|
||||||
|
|
@ -625,14 +656,14 @@ class PlejdService extends EventEmitter {
|
||||||
_encryptDecrypt(key, addr, data) {
|
_encryptDecrypt(key, addr, data) {
|
||||||
var buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]);
|
var buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]);
|
||||||
|
|
||||||
var cipher = crypto.createCipheriv("aes-128-ecb", key, '');
|
var cipher = crypto.createCipheriv('aes-128-ecb', key, '');
|
||||||
cipher.setAutoPadding(false);
|
cipher.setAutoPadding(false);
|
||||||
|
|
||||||
var ct = cipher.update(buf).toString('hex');
|
var ct = cipher.update(buf).toString('hex');
|
||||||
ct += cipher.final().toString('hex');
|
ct += cipher.final().toString('hex');
|
||||||
ct = Buffer.from(ct, 'hex');
|
ct = Buffer.from(ct, 'hex');
|
||||||
|
|
||||||
var output = "";
|
var output = '';
|
||||||
for (var i = 0, length = data.length; i < length; i++) {
|
for (var i = 0, length = data.length; i < length; i++) {
|
||||||
output += String.fromCharCode(data[i] ^ ct[i % 16]);
|
output += String.fromCharCode(data[i] ^ ct[i % 16]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue