0.17.0 release - improve startup and fix color temp (#324)

* Add working support for color temperature

* Lint fixes

* Fix to config json version to make it build

* Clean up and BLE constants and prepare for lightlevel UUID

* Eagerly send HA discovery, standardize colorTemp, clean up MQTT subscribe

* Fix typo in MqttClient

* Listen to HA birth messages to make devices available after HA restart

- Extract constants to common file

* Prepare for 0.17.0 release
This commit is contained in:
Victor 2025-09-12 08:36:02 +02:00 committed by GitHub
parent d801410200
commit 1766afb2e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 650 additions and 165 deletions

View file

@ -1,5 +1,21 @@
# Changelog hassio-plejd Home Assistant Plejd addon # Changelog hassio-plejd Home Assistant Plejd addon
## [0.17.0](https://github.com/icanos/hassio-plejd/tree/0.17.0) (2025-09-10)
[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.16.0...0.17.0)
**Implemented enhancements:**
- Add working support for color temperature
- Eagerly send HA discovery, standardize colorTemp, clean up MQTT subscribe
- Clean up and BLE constants and prepare for lightlevel UUID
**Fixed:**
- Fix typo in MqttClient
- Lint fixes
- Fix to config json version to make it build
## [0.16.0](https://github.com/icanos/hassio-plejd/tree/0.16.0) (2025-08-07) ## [0.16.0](https://github.com/icanos/hassio-plejd/tree/0.16.0) (2025-08-07)
[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.15.0...0.16.0) [Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.15.0...0.16.0)

View file

@ -257,8 +257,9 @@ class DeviceRegistry {
* @param {string} uniqueOutputId * @param {string} uniqueOutputId
* @param {boolean} state * @param {boolean} state
* @param {number?} [dim] * @param {number?} [dim]
* @param {number?} [color]
*/ */
setOutputState(uniqueOutputId, state, dim) { setOutputState(uniqueOutputId, state, dim, color) {
const device = this.getOutputDevice(uniqueOutputId); const device = this.getOutputDevice(uniqueOutputId);
if (!device) { if (!device) {
logger.warn( logger.warn(
@ -268,9 +269,12 @@ class DeviceRegistry {
} }
device.state = state; device.state = state;
if (dim && device.dimmable) { if (typeof dim === 'number' && device.dimmable) {
device.dim = dim; device.dim = dim;
} }
if (typeof color === 'number' && device.colorTemp) {
device.colorTemp = color;
}
if (Logger.shouldLog('silly')) { if (Logger.shouldLog('silly')) {
logger.silly(`Updated state: ${JSON.stringify(device)}`); logger.silly(`Updated state: ${JSON.stringify(device)}`);
} }

View file

@ -1,32 +1,22 @@
const EventEmitter = require('events'); const { EventEmitter } = require('events');
const mqtt = require('mqtt'); const mqtt = require('mqtt');
const Configuration = require('./Configuration'); const Configuration = require('./Configuration');
const Logger = require('./Logger'); const Logger = require('./Logger');
const {
// const startTopics = ['hass/status', 'homeassistant/status']; MQTT_TYPES,
TOPIC_TYPES,
MQTT_STATE,
DEVICE_TYPES,
AVAILABILITY,
MQTT_TOPICS,
} = require('./constants');
const logger = Logger.getLogger('plejd-mqtt'); const logger = Logger.getLogger('plejd-mqtt');
const discoveryPrefix = 'homeassistant'; const discoveryPrefix = 'homeassistant';
const nodeId = 'plejd'; const nodeId = 'plejd';
/** @type {import('./types/Mqtt').MQTT_TYPES} */
const MQTT_TYPES = {
LIGHT: 'light',
SCENE: 'scene',
SWITCH: 'switch',
DEVICE_AUTOMATION: 'device_automation',
};
/** @type {import('./types/Mqtt').TOPIC_TYPES} */
const TOPIC_TYPES = {
CONFIG: 'config',
STATE: 'state',
AVAILABILITY: 'availability',
SET: 'set',
};
const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) => const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) =>
`${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`; `${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`;
@ -79,8 +69,9 @@ const getOutputDeviceDiscoveryPayload = (
device.colorTempSettings && device.colorTempSettings &&
device.colorTempSettings.behavior === 'adjustable' device.colorTempSettings.behavior === 'adjustable'
? { ? {
min_mireds: 1000000 / device.colorTempSettings.minTemperatureLimit, color_temp_kelvin: true,
max_mireds: 1000000 / device.colorTempSettings.maxTemperatureLimit, min_kelvin: device.colorTempSettings.minTemperatureLimit,
max_kelvin: device.colorTempSettings.maxTemperatureLimit,
supported_color_modes: ['color_temp'], supported_color_modes: ['color_temp'],
} }
: {}), : {}),
@ -94,7 +85,7 @@ const getSceneDiscoveryPayload = (
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE), '~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
command_topic: `~/${TOPIC_TYPES.SET}`, command_topic: `~/${TOPIC_TYPES.SET}`,
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`, availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
payload_on: 'ON', payload_on: MQTT_STATE.ON,
qos: 1, qos: 1,
retain: true, // Discovery messages should be retained to account for HA restarts retain: true, // Discovery messages should be retained to account for HA restarts
}); });
@ -136,13 +127,16 @@ const getSceneDeviceTriggerhDiscoveryPayload = (
}, },
}); });
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF'); const getMqttStateString = (/** @type {boolean} */ state) =>
const AVAILABLITY = { ONLINE: 'online', OFFLINE: 'offline' }; state ? MQTT_STATE.ON : MQTT_STATE.OFF;
class MqttClient extends EventEmitter { class MqttClient extends EventEmitter {
/** @type {import('DeviceRegistry')} */ /** @type {import('DeviceRegistry')} */
deviceRegistry; deviceRegistry;
static STATE = MQTT_STATE;
static DEVICE_TYPES = DEVICE_TYPES;
static EVENTS = { static EVENTS = {
connected: 'connected', connected: 'connected',
stateChanged: 'stateChanged', stateChanged: 'stateChanged',
@ -180,25 +174,24 @@ class MqttClient extends EventEmitter {
this.client.on('connect', () => { this.client.on('connect', () => {
logger.info('Connected to MQTT.'); logger.info('Connected to MQTT.');
logger.verbose('Emitting internal MqttClient.EVENTS.connected event');
this.emit(MqttClient.EVENTS.connected); this.emit(MqttClient.EVENTS.connected);
// Testing to skip listening to HA birth messages all together // Listen for future Home Assistant birth messages if HA is not yet started
// this.client.subscribe( this.client.subscribe(
// startTopics, [MQTT_TOPICS.STATUS],
// { {
// qos: 1, qos: 1,
// nl: true, // don't echo back messages sent nl: true, // don't echo back messages sent
// rap: true, // retain as published - don't force retain = 0 rap: true, // retain as published - don't force retain = 0
// rh: 0, // Retain handling 0 presumably ignores retained messages rh: 0, // Retain handling 0 presumably ignores retained messages
// }, },
// (err) => { (err) => {
// if (err) { if (err) {
// logger.error('Unable to subscribe to status topics', err); logger.error('Unable to subscribe to status topic', err);
// } }
},
// this.emit(MqttClient.EVENTS.connected); );
// },
// );
}); });
this.client.on('close', () => { this.client.on('close', () => {
@ -256,6 +249,14 @@ class MqttClient extends EventEmitter {
default: default:
logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`); logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`);
} }
} else if (topic === MQTT_TOPICS.STATUS) {
const status = message.toString();
if (status === AVAILABILITY.ONLINE) {
logger.verbose(
'Home Assistant is online, emitting internal MqttClient.EVENTS.connected event',
);
this.emit(MqttClient.EVENTS.connected);
}
} else { } else {
logger.verbose( logger.verbose(
`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`, `Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`,
@ -282,7 +283,7 @@ class MqttClient extends EventEmitter {
const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT; const mqttType = outputDevice.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
this.client.publish( this.client.publish(
getTopicName(outputDevice.uniqueId, mqttType, 'availability'), getTopicName(outputDevice.uniqueId, mqttType, 'availability'),
AVAILABLITY.OFFLINE, AVAILABILITY.OFFLINE,
{ {
retain: false, // Availability messages should NOT be retained retain: false, // Availability messages should NOT be retained
qos: 1, qos: 1,
@ -294,7 +295,7 @@ class MqttClient extends EventEmitter {
allSceneDevices.forEach((sceneDevice) => { allSceneDevices.forEach((sceneDevice) => {
this.client.publish( this.client.publish(
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY), getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
AVAILABLITY.OFFLINE, AVAILABILITY.OFFLINE,
{ {
retain: false, // Availability messages should NOT be retained retain: false, // Availability messages should NOT be retained
qos: 1, qos: 1,
@ -342,7 +343,7 @@ class MqttClient extends EventEmitter {
// Forcefully remove retained (from us, v0.11 and before) AVAILABILITY messages // Forcefully remove retained (from us, v0.11 and before) AVAILABILITY messages
this.client.publish( this.client.publish(
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABLILITY), getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
null, null,
{ {
retain: true, // Retain true to remove previously retained message retain: true, // Retain true to remove previously retained message
@ -356,7 +357,7 @@ class MqttClient extends EventEmitter {
this.client.publish( this.client.publish(
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY), getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
AVAILABLITY.ONLINE, AVAILABILITY.ONLINE,
{ {
retain: false, // Availability messages should NOT be retained retain: false, // Availability messages should NOT be retained
qos: 1, qos: 1,
@ -430,7 +431,7 @@ class MqttClient extends EventEmitter {
this.client.publish( this.client.publish(
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY), getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
AVAILABLITY.ONLINE, AVAILABILITY.ONLINE,
{ {
retain: false, // Availability messages should NOT be retained retain: false, // Availability messages should NOT be retained
qos: 1, qos: 1,
@ -457,7 +458,7 @@ class MqttClient extends EventEmitter {
/** /**
* @param {string} uniqueOutputId * @param {string} uniqueOutputId
* @param {{ state: boolean; brightness?: number; }} data * @param {{ state: boolean; brightness?: number; color?: number}} data
*/ */
updateOutputState(uniqueOutputId, data) { updateOutputState(uniqueOutputId, data) {
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId); const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
@ -470,7 +471,7 @@ class MqttClient extends EventEmitter {
logger.verbose( logger.verbose(
`Updating state for ${device.name}: ${data.state}${ `Updating state for ${device.name}: ${data.state}${
data.brightness ? `, dim: ${data.brightness}` : '' data.brightness ? `, dim: ${data.brightness}` : ''
}`, }${data.color ? `, color: ${data.color}K` : ''}`,
); );
let payload = null; let payload = null;
@ -478,10 +479,19 @@ class MqttClient extends EventEmitter {
payload = getMqttStateString(data.state); payload = getMqttStateString(data.state);
} else { } else {
if (device.dimmable) { if (device.dimmable) {
if (data.color) {
payload = {
state: getMqttStateString(data.state),
brightness: data.brightness,
color_mode: 'color_temp',
color_temp: data.color,
};
} else {
payload = { payload = {
state: getMqttStateString(data.state), state: getMqttStateString(data.state),
brightness: data.brightness, brightness: data.brightness,
}; };
}
} else { } else {
payload = { payload = {
state: getMqttStateString(data.state), state: getMqttStateString(data.state),

View file

@ -1,4 +1,4 @@
const EventEmitter = require('events'); const { EventEmitter } = require('events');
const Configuration = require('./Configuration'); const Configuration = require('./Configuration');
const Logger = require('./Logger'); const Logger = require('./Logger');
@ -55,6 +55,15 @@ class PlejdAddon extends EventEmitter {
process.on(signal, this.processCleanupFunc); process.on(signal, this.processCleanupFunc);
}); });
// Eagerly send discovery as soon as possible
try {
logger.verbose('Eagerly sending discovery to Home Assistant.');
this.mqttClient.sendDiscoveryToHomeAssistant();
} catch (err) {
logger.error('Error in eager discovery send', err);
}
// Send discovery again on MQTT connect to ensure Home Assistant receives device info after reconnects or broker restarts.
this.mqttClient.on(MqttClient.EVENTS.connected, () => { this.mqttClient.on(MqttClient.EVENTS.connected, () => {
try { try {
logger.verbose('connected to mqtt.'); logger.verbose('connected to mqtt.');
@ -72,7 +81,7 @@ class PlejdAddon extends EventEmitter {
try { try {
const { uniqueId } = device; const { uniqueId } = device;
if (device.typeName === 'Scene') { if (device.typeName === MqttClient.DEVICE_TYPES.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(uniqueId); this.sceneManager.executeScene(uniqueId);
@ -93,7 +102,7 @@ class PlejdAddon extends EventEmitter {
if (typeof command === 'string') { if (typeof command === 'string') {
// switch command // switch command
state = command === 'ON'; state = command === MqttClient.STATE.ON;
commandObj = { commandObj = {
state, state,
}; };
@ -106,7 +115,7 @@ class PlejdAddon extends EventEmitter {
}); });
} else { } else {
// eslint-disable-next-line prefer-destructuring // eslint-disable-next-line prefer-destructuring
state = command.state === 'ON'; state = command.state === MqttClient.STATE.ON;
commandObj = command; commandObj = command;
} }

View file

@ -4,11 +4,16 @@ const fs = require('fs');
const Configuration = require('./Configuration'); const Configuration = require('./Configuration');
const Logger = require('./Logger'); const Logger = require('./Logger');
const API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak'; const {
const API_BASE_URL = 'https://cloud.plejd.com/parse/'; API: {
const API_LOGIN_URL = 'login'; APP_ID: API_APP_ID,
const API_SITE_LIST_URL = 'functions/getSiteList'; BASE_URL: API_BASE_URL,
const API_SITE_DETAILS_URL = 'functions/getSiteById'; LOGIN_URL: API_LOGIN_URL,
SITE_LIST_URL: API_SITE_LIST_URL,
SITE_DETAILS_URL: API_SITE_DETAILS_URL,
},
DEVICE_TYPES,
} = require('./constants');
const TRAITS = { const TRAITS = {
NO_LOAD: 0, // 0b0000 NO_LOAD: 0, // 0b0000
@ -305,7 +310,7 @@ class PlejdApi {
return { return {
name: 'REL-01', name: 'REL-01',
description: '1 channel relay, 3500 VA', description: '1 channel relay, 3500 VA',
type: 'switch', type: DEVICE_TYPES.SWITCH,
dimmable: false, dimmable: false,
broadcastClicks: false, broadcastClicks: false,
}; };
@ -313,7 +318,7 @@ class PlejdApi {
return { return {
name: 'SPR-01', name: 'SPR-01',
description: 'Smart plug on/off with relay, 3500 VA', description: 'Smart plug on/off with relay, 3500 VA',
type: 'switch', type: DEVICE_TYPES.SWITCH,
dimmable: false, dimmable: false,
broadcastClicks: false, broadcastClicks: false,
}; };
@ -363,7 +368,7 @@ class PlejdApi {
return { return {
name: 'REL-01-2P', name: 'REL-01-2P',
description: '1-channel relay with 2-pole 3500 VA', description: '1-channel relay with 2-pole 3500 VA',
type: 'switch', type: DEVICE_TYPES.SWITCH,
dimmable: false, dimmable: false,
broadcastClicks: false, broadcastClicks: false,
}; };
@ -371,7 +376,7 @@ class PlejdApi {
return { return {
name: 'REL-02', name: 'REL-02',
description: '2-channel relay with combined 3500 VA', description: '2-channel relay with combined 3500 VA',
type: 'switch', type: DEVICE_TYPES.SWITCH,
dimmable: false, dimmable: false,
broadcastClicks: false, broadcastClicks: false,
}; };
@ -512,11 +517,6 @@ class PlejdApi {
// 2. outputSettings.dimCurve NOT IN ["NonDimmable", "RelayNormal"]: Dimmable // 2. outputSettings.dimCurve NOT IN ["NonDimmable", "RelayNormal"]: Dimmable
// 3. outputSettings.predefinedLoad !== null && outputSettings.predefinedLoad.loadType === "DWN": Dimmable // 3. outputSettings.predefinedLoad !== null && outputSettings.predefinedLoad.loadType === "DWN": Dimmable
const colorTemp =
outputSettings &&
outputSettings.colorTemperature &&
outputSettings.colorTemperature.behavior === 'adjustable';
try { try {
const decodedDeviceType = this._getDeviceType(plejdDevice); const decodedDeviceType = this._getDeviceType(plejdDevice);
@ -533,7 +533,7 @@ class PlejdApi {
/** @type {import('types/DeviceRegistry').OutputDevice} */ /** @type {import('types/DeviceRegistry').OutputDevice} */
const outputDevice = { const outputDevice = {
bleOutputAddress, bleOutputAddress,
colorTemp, colorTemp: null,
colorTempSettings: outputSettings ? outputSettings.colorTemperature : null, colorTempSettings: outputSettings ? outputSettings.colorTemperature : null,
deviceId: device.deviceId, deviceId: device.deviceId,
dimmable, dimmable,
@ -640,7 +640,7 @@ class PlejdApi {
const newDevice = { const newDevice = {
bleOutputAddress: roomAddress, bleOutputAddress: roomAddress,
deviceId: null, deviceId: null,
colorTemp: false, colorTemp: null,
dimmable, dimmable,
name: room.title, name: room.title,
output: undefined, output: undefined,
@ -670,7 +670,7 @@ class PlejdApi {
/** @type {import('types/DeviceRegistry').OutputDevice} */ /** @type {import('types/DeviceRegistry').OutputDevice} */
const newScene = { const newScene = {
bleOutputAddress: sceneNum, bleOutputAddress: sceneNum,
colorTemp: false, colorTemp: null,
deviceId: undefined, deviceId: undefined,
dimmable: false, dimmable: false,
name: scene.title, name: scene.title,
@ -678,9 +678,9 @@ class PlejdApi {
roomId: undefined, roomId: undefined,
roomName: undefined, roomName: undefined,
state: false, state: false,
type: 'scene', type: DEVICE_TYPES.SCENE,
typeDescription: 'A Plejd scene', typeDescription: 'A Plejd scene',
typeName: 'Scene', typeName: DEVICE_TYPES.SCENE,
version: undefined, version: undefined,
uniqueId: scene.sceneId, uniqueId: scene.sceneId,
}; };

View file

@ -4,43 +4,44 @@ const xor = require('buffer-xor');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const Configuration = require('./Configuration'); const Configuration = require('./Configuration');
const constants = require('./constants'); const {
COMMANDS,
BLE,
PLEJD_UUIDS,
BLUEZ: {
SERVICE_NAME: BLUEZ_SERVICE_NAME,
ADAPTER_ID: BLUEZ_ADAPTER_ID,
DEVICE_ID: BLUEZ_DEVICE_ID,
GATT_SERVICE_ID,
GATT_CHAR_ID: GATT_CHRC_ID,
},
DBUS: { OM_INTERFACE: DBUS_OM_INTERFACE, PROP_INTERFACE: DBUS_PROP_INTERFACE },
} = require('./constants');
const Logger = require('./Logger'); const Logger = require('./Logger');
const { COMMANDS } = constants;
const logger = Logger.getLogger('plejd-ble'); const logger = Logger.getLogger('plejd-ble');
// UUIDs const { PLEJD_SERVICE, AUTH_UUID, DATA_UUID, LAST_DATA_UUID, PING_UUID } = PLEJD_UUIDS;
const BLE_UUID_SUFFIX = '6085-4726-be45-040c957391b5'; const { COMMANDS: BLE_COMMANDS, BROADCAST_DEVICE_ID: BLE_BROADCAST_DEVICE_ID } = BLE;
const PLEJD_SERVICE = `31ba0001-${BLE_UUID_SUFFIX}`;
const DATA_UUID = `31ba0004-${BLE_UUID_SUFFIX}`;
const LAST_DATA_UUID = `31ba0005-${BLE_UUID_SUFFIX}`;
const AUTH_UUID = `31ba0009-${BLE_UUID_SUFFIX}`;
const PING_UUID = `31ba000a-${BLE_UUID_SUFFIX}`;
const BLE_CMD_DIM_CHANGE = 0x00c8; // BLE commands for easier access
const BLE_CMD_DIM2_CHANGE = 0x0098; const {
const BLE_CMD_STATE_CHANGE = 0x0097; REMOTE_CLICK: BLE_CMD_REMOTE_CLICK,
const BLE_CMD_SCENE_TRIG = 0x0021; TIME_UPDATE: BLE_CMD_TIME_UPDATE,
const BLE_CMD_TIME_UPDATE = 0x001b; SCENE_TRIGGER: BLE_CMD_SCENE_TRIG,
const BLE_CMD_REMOTE_CLICK = 0x0016; STATE_CHANGE: BLE_CMD_STATE_CHANGE,
DIM_CHANGE: BLE_CMD_DIM_CHANGE,
COLOR_CHANGE: BLE_CMD_COLOR_CHANGE,
} = BLE_COMMANDS;
const BLE_BROADCAST_DEVICE_ID = 0x01; const BLE_CMD_DIM2_CHANGE = 0x0098; // Dim + state update
const BLE_REQUEST_NO_RESPONSE = 0x0110; const BLE_REQUEST_NO_RESPONSE = 0x0110; // Set value, no response.
const BLE_REQUEST_RESPONSE = 0x0102; const BLE_REQUEST_RESPONSE = 0x0102; // Request response, time for example
// const BLE_REQUEST_READ_VALUE = 0x0103; // const BLE_REQUEST_READ_VALUE = 0x0103; // Read value?
const BLUEZ_SERVICE_NAME = 'org.bluez';
const DBUS_OM_INTERFACE = 'org.freedesktop.DBus.ObjectManager';
const DBUS_PROP_INTERFACE = 'org.freedesktop.DBus.Properties';
const BLUEZ_ADAPTER_ID = 'org.bluez.Adapter1';
const BLUEZ_DEVICE_ID = 'org.bluez.Device1';
const GATT_SERVICE_ID = 'org.bluez.GattService1';
const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1';
const PAYLOAD_POSITION_OFFSET = 5; const PAYLOAD_POSITION_OFFSET = 5;
const DIM_LEVEL_POSITION_OFFSET = 7; const DIM_LEVEL_POSITION_OFFSET = 7;
const COLOR_TEMP_POSITION_OFFSET = 9;
const delay = (timeout) => const delay = (timeout) =>
new Promise((resolve) => { new Promise((resolve) => {
@ -168,11 +169,13 @@ class PlejBLEHandler extends EventEmitter {
/** /**
* @param {string} command * @param {string} command
* @param {number} bleOutputAddress * @param {number} bleOutputAddress
* @param {number} data * @param {number?} brightness
* @param {number?} colorTemp
*/ */
async sendCommand(command, bleOutputAddress, data) { async sendCommand(command, bleOutputAddress, brightness, colorTemp) {
let payload; let payload;
let brightnessVal; let brightnessVal;
switch (command) { switch (command) {
case COMMANDS.TURN_ON: case COMMANDS.TURN_ON:
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '01'); payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '01');
@ -182,18 +185,39 @@ class PlejBLEHandler extends EventEmitter {
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 = (brightness << 8) | brightness;
payload = this._createHexPayload( payload = this._createHexPayload(
bleOutputAddress, bleOutputAddress,
BLE_CMD_DIM2_CHANGE, BLE_CMD_DIM2_CHANGE,
`01${brightnessVal.toString(16).padStart(4, '0')}`, `01${brightnessVal.toString(16).padStart(4, '0')}`,
); );
break;
case COMMANDS.COLOR:
// eslint-disable-next-line no-bitwise
payload = this._createHexPayload(
bleOutputAddress,
BLE_CMD_COLOR_CHANGE,
// Not clear why 030111 is used. See https://github.com/icanos/hassio-plejd/issues/163
`030111${colorTemp.toString(16).padStart(4, '0')}`,
);
break; break;
default: default:
logger.error(`Unknown command ${command}`); logger.error(`Unknown command ${command}`);
throw new Error(`Unknown command ${command}`); throw new Error(`Unknown command ${command}`);
} }
await this._write(payload); await this._write(payload);
if (command === COMMANDS.COLOR) {
// Color BLE command is not echoed back, so we manually emit the event here
const device = this.deviceRegistry.getOutputDeviceByBleOutputAddress(bleOutputAddress);
if (device) {
this.emit(PlejBLEHandler.EVENTS.commandReceived, device.uniqueId, command, {
state: 1,
color: colorTemp,
});
}
}
} }
async _initDiscoveredPlejdDevice(path) { async _initDiscoveredPlejdDevice(path) {
@ -302,10 +326,12 @@ class PlejBLEHandler extends EventEmitter {
} else { } else {
logger.info('Plejd clock updates disabled in configuration.'); logger.info('Plejd clock updates disabled in configuration.');
} }
this._startPing(); this._startPing();
// After we've authenticated, we need to hook up the event listener // After we've authenticated:
// for changes to lastData.
// Hook up the event listener for changes to lastData.
this.characteristics.lastDataProperties.on( this.characteristics.lastDataProperties.on(
'PropertiesChanged', 'PropertiesChanged',
( (
@ -577,6 +603,7 @@ class PlejBLEHandler extends EventEmitter {
); );
const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, payload); const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, payload);
await this.characteristics.data.WriteValue([...encryptedData], {}); await this.characteristics.data.WriteValue([...encryptedData], {});
await this._onWriteSuccess(); await this._onWriteSuccess();
} catch (err) { } catch (err) {
await this._onWriteFailed(err); await this._onWriteFailed(err);
@ -850,7 +877,6 @@ class PlejBLEHandler extends EventEmitter {
const outputUniqueId = device ? device.uniqueId : null; const outputUniqueId = device ? device.uniqueId : null;
if (Logger.shouldLog('verbose')) { if (Logger.shouldLog('verbose')) {
// 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 ${outputUniqueId} (BLE address ${bleOutputAddress}), cmd ${cmd.toString( `Decoded: Device ${outputUniqueId} (BLE address ${bleOutputAddress}), cmd ${cmd.toString(
@ -869,6 +895,18 @@ class PlejBLEHandler extends EventEmitter {
command = COMMANDS.DIM; command = COMMANDS.DIM;
data = { state, dim }; data = { state, dim };
this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data); this.emit(PlejBLEHandler.EVENTS.commandReceived, outputUniqueId, command, data);
} else if (cmd === BLE_CMD_COLOR_CHANGE) {
const colorTempKelvin =
decoded.length > COLOR_TEMP_POSITION_OFFSET
? decoded.readUInt16BE(COLOR_TEMP_POSITION_OFFSET - 1)
: 0;
logger.debug(
`${deviceName} (${outputUniqueId}) got state+dim+color update. S: ${state}, D: ${dim}, C: ${colorTempKelvin}K`,
);
command = COMMANDS.COLOR;
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} (${outputUniqueId}) 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;
@ -1006,7 +1044,8 @@ class PlejBLEHandler extends EventEmitter {
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_createChallengeResponse(key, challenge) { _createChallengeResponse(key, challenge) {
const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest(); const xorResult = xor(key, challenge);
const intermediate = crypto.createHash('sha256').update(new Uint8Array(xorResult)).digest();
const part1 = intermediate.subarray(0, 16); const part1 = intermediate.subarray(0, 16);
const part2 = intermediate.subarray(16); const part2 = intermediate.subarray(16);
@ -1022,7 +1061,7 @@ class PlejBLEHandler extends EventEmitter {
const cipher = crypto.createCipheriv('aes-128-ecb', key, ''); const cipher = crypto.createCipheriv('aes-128-ecb', key, '');
cipher.setAutoPadding(false); cipher.setAutoPadding(false);
let ct = cipher.update(buf).toString('hex'); let ct = cipher.update(new Uint8Array(buf)).toString('hex');
ct += cipher.final().toString('hex'); ct += cipher.final().toString('hex');
const ctBuf = Buffer.from(ct, 'hex'); const ctBuf = Buffer.from(ct, 'hex');

View file

@ -1,10 +1,9 @@
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const Configuration = require('./Configuration'); const Configuration = require('./Configuration');
const constants = require('./constants'); const { COMMANDS } = require('./constants');
const Logger = require('./Logger'); const Logger = require('./Logger');
const PlejBLEHandler = require('./PlejdBLEHandler'); const PlejBLEHandler = require('./PlejdBLEHandler');
const { COMMANDS } = constants;
const logger = Logger.getLogger('device-comm'); const logger = Logger.getLogger('device-comm');
const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting
@ -18,7 +17,7 @@ class PlejdDeviceCommunication extends EventEmitter {
/** @type {import('./DeviceRegistry')} */ /** @type {import('./DeviceRegistry')} */
deviceRegistry; deviceRegistry;
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
/** @type {{uniqueOutputId: string, command: string, data: any, shouldRetry: boolean, retryCount?: number}[]} */ /** @type {{uniqueOutputId: string, command: {command: keyof typeof COMMANDS, brightness: number?, color_temp: number? }, shouldRetry: boolean, retryCount?: number}[]} */
writeQueue = []; writeQueue = [];
writeQueueRef = null; writeQueueRef = null;
@ -79,11 +78,9 @@ class PlejdDeviceCommunication extends EventEmitter {
turnOn(uniqueOutputId, command) { turnOn(uniqueOutputId, command) {
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId); const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
logger.info( logger.info(
`Plejd got turn on command for ${deviceName} (${uniqueOutputId}), brightness ${ `Plejd got turn on command for ${deviceName} (${uniqueOutputId})${JSON.stringify(command)}`,
command.brightness
}${command.transition ? `, transition: ${command.transition}` : ''}`,
); );
this._transitionTo(uniqueOutputId, command.brightness, command.transition, deviceName); this._transitionTo(uniqueOutputId, command, deviceName);
} }
turnOff(uniqueOutputId, command) { turnOff(uniqueOutputId, command) {
@ -93,17 +90,27 @@ class PlejdDeviceCommunication extends EventEmitter {
command.transition ? `, transition: ${command.transition}` : '' command.transition ? `, transition: ${command.transition}` : ''
}`, }`,
); );
this._transitionTo(uniqueOutputId, 0, command.transition, deviceName); this._transitionTo(uniqueOutputId, { ...command, brightness: 0 }, deviceName);
} }
_bleCommandReceived(uniqueOutputId, command, data) { _bleCommandReceived(uniqueOutputId, command, data) {
try { try {
if (command === COMMANDS.DIM) { if (command === COMMANDS.DIM) {
if (data.dim === 0 && data.state === 1) {
data.dim = 1; // Transform BLE brightness value 0 to 1, which is the minimum MQTT brightness value
}
this.deviceRegistry.setOutputState(uniqueOutputId, data.state, data.dim); this.deviceRegistry.setOutputState(uniqueOutputId, data.state, data.dim);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, { this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: !!data.state, state: !!data.state,
brightness: data.dim, brightness: data.dim,
}); });
} else if (command === COMMANDS.COLOR) {
this.deviceRegistry.setOutputState(uniqueOutputId, data.state, null, data.color);
logger.verbose(`Set color state to ${data.color}. Emitting EVENTS.stateChanged`);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
state: !!data.state,
color: data.color,
});
} else if (command === COMMANDS.TURN_ON) { } else if (command === COMMANDS.TURN_ON) {
this.deviceRegistry.setOutputState(uniqueOutputId, true); this.deviceRegistry.setOutputState(uniqueOutputId, true);
this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, { this.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
@ -132,7 +139,12 @@ class PlejdDeviceCommunication extends EventEmitter {
} }
} }
_transitionTo(uniqueOutputId, targetBrightness, transition, deviceName) { /**
* @param {string} uniqueOutputId
* @param {{ transition: number, brightness: number, color_temp: number? } } command
* @param { string } deviceName
*/
_transitionTo(uniqueOutputId, command, deviceName) {
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId); const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
const initialBrightness = device ? device.state && device.dim : null; const initialBrightness = device ? device.state && device.dim : null;
this._clearDeviceTransitionTimer(uniqueOutputId); this._clearDeviceTransitionTimer(uniqueOutputId);
@ -140,11 +152,11 @@ class PlejdDeviceCommunication extends EventEmitter {
const isDimmable = this.deviceRegistry.getOutputDevice(uniqueOutputId).dimmable; const isDimmable = this.deviceRegistry.getOutputDevice(uniqueOutputId).dimmable;
if ( if (
transition > 1 && command.transition > 1 &&
isDimmable && isDimmable &&
(initialBrightness || initialBrightness === 0) && (initialBrightness || initialBrightness === 0) &&
(targetBrightness || targetBrightness === 0) && (command.brightness || command.brightness === 0) &&
targetBrightness !== initialBrightness command.brightness !== initialBrightness
) { ) {
// Transition time set, known initial and target brightness // Transition time set, known initial and target brightness
// Calculate transition interval time based on delta brightness and max steps per second // Calculate transition interval time based on delta brightness and max steps per second
@ -152,16 +164,16 @@ class PlejdDeviceCommunication extends EventEmitter {
// If transition <= 1 second, Plejd will do a better job // If transition <= 1 second, Plejd will do a better job
// than we can in transitioning so transitioning will be skipped // than we can in transitioning so transitioning will be skipped
const deltaBrightness = targetBrightness - initialBrightness; const deltaBrightness = command.brightness - initialBrightness;
const transitionSteps = Math.min( const transitionSteps = Math.min(
Math.abs(deltaBrightness), Math.abs(deltaBrightness),
MAX_TRANSITION_STEPS_PER_SECOND * transition, MAX_TRANSITION_STEPS_PER_SECOND * command.transition,
); );
const transitionInterval = (transition * 1000) / transitionSteps; const transitionInterval = (command.transition * 1000) / transitionSteps;
logger.debug( logger.debug(
`transitioning from ${initialBrightness} to ${targetBrightness} ${ `transitioning from ${initialBrightness} to ${command.brightness} ${
transition ? `in ${transition} seconds` : '' command.transition ? `in ${command.transition} seconds` : ''
}.`, }.`,
); );
logger.verbose( logger.verbose(
@ -176,68 +188,105 @@ class PlejdDeviceCommunication extends EventEmitter {
const tElapsedMs = new Date().getTime() - dtStart.getTime(); const tElapsedMs = new Date().getTime() - dtStart.getTime();
let tElapsed = tElapsedMs / 1000; let tElapsed = tElapsedMs / 1000;
if (tElapsed > transition || tElapsed < 0) { if (tElapsed > command.transition || tElapsed < 0) {
tElapsed = transition; tElapsed = command.transition;
} }
let newBrightness = Math.round( let newBrightness = Math.round(
initialBrightness + (deltaBrightness * tElapsed) / transition, initialBrightness + (deltaBrightness * tElapsed) / command.transition,
); );
if (tElapsed === transition) { if (tElapsed === command.transition) {
nSteps++; nSteps++;
this._clearDeviceTransitionTimer(uniqueOutputId); this._clearDeviceTransitionTimer(uniqueOutputId);
newBrightness = targetBrightness; newBrightness = command.brightness;
logger.debug( logger.debug(
`Queueing finalize ${deviceName} (${uniqueOutputId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${ `Queueing finalize ${deviceName} (${uniqueOutputId}) transition from ${initialBrightness} to ${
command.brightness
} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
tElapsedMs / (nSteps || 1) tElapsedMs / (nSteps || 1)
} ms.`, } ms.`,
); );
this._setBrightness(uniqueOutputId, newBrightness, true, deviceName); this._setLightState(
uniqueOutputId,
{ ...command, brightness: newBrightness },
true,
deviceName,
);
} else { } else {
nSteps++; nSteps++;
logger.verbose( logger.verbose(
`Queueing dim transition for ${deviceName} (${uniqueOutputId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`, `Queueing dim transition for ${deviceName} (${uniqueOutputId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
); );
this._setBrightness(uniqueOutputId, newBrightness, false, deviceName); this._setLightState(
uniqueOutputId,
{ ...command, brightness: newBrightness },
false,
deviceName,
);
} }
}, transitionInterval); }, transitionInterval);
} else { } else {
if (transition && isDimmable) { if (command.transition && isDimmable) {
logger.debug( logger.debug(
`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 ${command.brightness}`,
); );
} }
this._setBrightness(uniqueOutputId, targetBrightness, true, deviceName); this._setLightState(uniqueOutputId, command, true, deviceName);
} }
} }
_setBrightness(unqiueOutputId, brightness, shouldRetry, deviceName) { /**
if (!brightness && brightness !== 0) { * @param {string} uniqueOutputId
* @param {{ brightness: number, color_temp: number? } } command
* @param { boolean } shouldRetry
* @param { string } deviceName
*/
_setLightState(uniqueOutputId, command, shouldRetry, deviceName) {
const lightCommand = {};
if (!command.brightness && command.brightness !== 0) {
logger.debug( logger.debug(
`Queueing turn on ${deviceName} (${unqiueOutputId}). No brightness specified, setting DIM to previous.`, `Queueing turn on ${deviceName} (${uniqueOutputId}). No brightness specified, setting DIM to previous.`,
); );
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_ON, null, shouldRetry); lightCommand.command = COMMANDS.TURN_ON;
} else if (brightness <= 0) { } else if (command.brightness <= 0) {
logger.debug(`Queueing turn off ${unqiueOutputId}`); logger.debug(`Queueing turn off ${uniqueOutputId}`);
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_OFF, null, shouldRetry); lightCommand.command = COMMANDS.TURN_OFF;
} else { } else {
if (brightness > 255) { if (command.brightness > 255) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
brightness = 255; command.brightness = 255;
} }
logger.debug(`Queueing ${unqiueOutputId} set brightness to ${brightness}`); logger.debug(`Queueing ${uniqueOutputId} set brightness to ${command.brightness}`);
// eslint-disable-next-line no-bitwise
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.DIM, brightness, shouldRetry); lightCommand.command = COMMANDS.DIM;
} lightCommand.brightness = command.brightness;
} }
_appendCommandToWriteQueue(uniqueOutputId, command, data, shouldRetry) { if (command.color_temp) {
lightCommand.command = COMMANDS.COLOR;
lightCommand.color_temp = command.color_temp;
}
this._appendCommandToWriteQueue(
uniqueOutputId,
// @ts-ignore
lightCommand,
shouldRetry,
);
}
/**
* @param {string} uniqueOutputId
* @param {{ command: keyof typeof COMMANDS, brightness: number?, color_temp: number? } } command
* @param { boolean } shouldRetry
*/
_appendCommandToWriteQueue(uniqueOutputId, command, shouldRetry) {
this.writeQueue.unshift({ this.writeQueue.unshift({
uniqueOutputId, uniqueOutputId,
command, command,
data,
shouldRetry, shouldRetry,
}); });
} }
@ -260,9 +309,9 @@ class PlejdDeviceCommunication extends EventEmitter {
const device = this.deviceRegistry.getOutputDevice(queueItem.uniqueOutputId); const device = this.deviceRegistry.getOutputDevice(queueItem.uniqueOutputId);
logger.debug( logger.debug(
`Write queue: Processing ${device.name} (${queueItem.uniqueOutputId}). Command ${ `Write queue: Processing ${device.name} (${
queueItem.command queueItem.uniqueOutputId
}${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${ }). Command ${JSON.stringify(queueItem.command)}. Total queue length: ${
this.writeQueue.length this.writeQueue.length
}`, }`,
); );
@ -278,9 +327,10 @@ class PlejdDeviceCommunication extends EventEmitter {
/* 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.command,
device.bleOutputAddress, device.bleOutputAddress,
queueItem.data, queueItem.command.brightness,
queueItem.command.color_temp,
); );
} catch (err) { } catch (err) {
if (queueItem.shouldRetry) { if (queueItem.shouldRetry) {

View file

@ -1,3 +1,5 @@
const { SCENE_STATES } = require('./constants');
class SceneStep { class SceneStep {
/** /**
* @param {import("./types/ApiSite").SceneStep} step * @param {import("./types/ApiSite").SceneStep} step
@ -6,7 +8,7 @@ class SceneStep {
this.sceneId = step.sceneId; this.sceneId = step.sceneId;
this.deviceId = step.deviceId; this.deviceId = step.deviceId;
this.output = step.output; this.output = step.output;
this.state = step.state === 'On' ? 1 : 0; this.state = step.state === SCENE_STATES.ON ? 1 : 0;
this.brightness = step.value; this.brightness = step.value;
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "Plejd", "name": "Plejd",
"version": "0.16.0", "version": "0.17.0",
"slug": "plejd", "slug": "plejd",
"description": "Adds support for the Swedish home automation devices from Plejd.", "description": "Adds support for the Swedish home automation devices from Plejd.",
"url": "https://github.com/icanos/hassio-plejd/", "url": "https://github.com/icanos/hassio-plejd/",

View file

@ -1,9 +1,125 @@
/** @type {import('./types/Mqtt').MQTT_TYPES} */
const MQTT_TYPES = {
LIGHT: 'light',
SCENE: 'scene',
SWITCH: 'switch',
DEVICE_AUTOMATION: 'device_automation',
SENSOR: 'sensor',
EXTENDER: 'extender',
};
/** @type {import('./types/Mqtt').TOPIC_TYPES} */
const TOPIC_TYPES = {
CONFIG: 'config',
STATE: 'state',
AVAILABILITY: 'availability',
SET: 'set',
};
const MQTT_TOPICS = {
STATUS: 'homeassistant/status',
};
const MQTT_STATE = {
ON: 'ON',
OFF: 'OFF',
};
const DEVICE_TYPES = {
SCENE: 'scene',
LIGHT: 'light',
SWITCH: 'switch',
SENSOR: 'sensor',
EXTENDER: 'extender',
};
const OUTPUT_TYPES = {
LIGHT: 'LIGHT',
};
const SCENE_STATES = {
ON: 'On',
OFF: 'Off',
};
const AVAILABILITY = {
ONLINE: 'online',
OFFLINE: 'offline',
};
const AUTOMATION_TYPES = {
TRIGGER: 'trigger',
BUTTON_SHORT_PRESS: 'button_short_press',
};
const BLUEZ = {
SERVICE_NAME: 'org.bluez',
ADAPTER_ID: 'org.bluez.Adapter1',
DEVICE_ID: 'org.bluez.Device1',
GATT_SERVICE_ID: 'org.bluez.GattService1',
GATT_CHAR_ID: 'org.bluez.GattCharacteristic1',
};
const DBUS = {
OM_INTERFACE: 'org.freedesktop.DBus.ObjectManager',
PROP_INTERFACE: 'org.freedesktop.DBus.Properties',
};
const API = {
APP_ID: 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak',
BASE_URL: 'https://cloud.plejd.com/parse/',
LOGIN_URL: 'login',
SITE_LIST_URL: 'functions/getSiteList',
SITE_DETAILS_URL: 'functions/getSiteById',
};
// BLE Protocol Constants
const BLE = {
UUID_SUFFIX: '6085-4726-be45-040c957391b5',
COMMANDS: {
REMOTE_CLICK: 0x0016,
TIME_UPDATE: 0x001b,
SCENE_TRIGGER: 0x0021,
STATE_CHANGE: 0x0097,
DIM_CHANGE: 0x00c8,
COLOR_CHANGE: 0x0420,
},
BROADCAST_DEVICE_ID: 0x01,
};
// Generate UUIDs
const PLEJD_UUIDS = {
PLEJD_SERVICE: `31ba0001-${BLE.UUID_SUFFIX}`,
LIGHTLEVEL_UUID: `31ba0003-${BLE.UUID_SUFFIX}`,
DATA_UUID: `31ba0004-${BLE.UUID_SUFFIX}`,
LAST_DATA_UUID: `31ba0005-${BLE.UUID_SUFFIX}`,
AUTH_UUID: `31ba0009-${BLE.UUID_SUFFIX}`,
PING_UUID: `31ba000a-${BLE.UUID_SUFFIX}`,
};
const COMMANDS = { const COMMANDS = {
TURN_ON: 'Turn on', TURN_ON: 'Turn on',
TURN_OFF: 'Turn off', TURN_OFF: 'Turn off',
DIM: 'Dim', DIM: 'Dim',
COLOR: 'Color',
TRIGGER_SCENE: 'Trigger scene', TRIGGER_SCENE: 'Trigger scene',
BUTTON_CLICK: 'Button click', BUTTON_CLICK: 'Button click',
}; };
module.exports = { COMMANDS }; module.exports = {
MQTT_TYPES,
TOPIC_TYPES,
MQTT_STATE,
MQTT_TOPICS,
DEVICE_TYPES,
OUTPUT_TYPES,
SCENE_STATES,
AVAILABILITY,
AUTOMATION_TYPES,
BLUEZ,
DBUS,
API,
BLE,
PLEJD_UUIDS,
COMMANDS,
};

View file

@ -6,7 +6,7 @@ export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice };
export interface OutputDevice { export interface OutputDevice {
bleOutputAddress: number; bleOutputAddress: number;
colorTemp: boolean; colorTemp: number?;
colorTempSettings?: OutputSettingColorTemperature colorTempSettings?: OutputSettingColorTemperature
deviceId: string; deviceId: string;
dim?: number; dim?: number;

View file

@ -3,5 +3,5 @@
export type TopicType = 'config' | 'state' | 'availability' | 'set'; export type TopicType = 'config' | 'state' | 'availability' | 'set';
export type TOPIC_TYPES = { [key: string]: TopicType }; export type TOPIC_TYPES = { [key: string]: TopicType };
export type MqttType = 'light' | 'scene' | 'switch' | 'device_automation'; export type MqttType = 'light' | 'scene' | 'switch' | 'device_automation' | 'sensor' | 'extender';
export type MQTT_TYPES = { [key: string]: MqttType }; export type MQTT_TYPES = { [key: string]: MqttType };

118
plejd/types/constants.d.ts vendored Normal file
View file

@ -0,0 +1,118 @@
import { MqttType, TopicType } from './Mqtt';
export interface MqttTypes {
LIGHT: MqttType;
SCENE: MqttType;
SWITCH: MqttType;
DEVICE_AUTOMATION: MqttType;
SENSOR: MqttType;
EXTENDER: MqttType;
}
export interface TopicTypes {
CONFIG: TopicType;
STATE: TopicType;
AVAILABILITY: TopicType;
SET: TopicType;
}
export interface MqttState {
ON: 'ON';
OFF: 'OFF';
}
export interface DeviceTypes {
SCENE: 'Scene';
LIGHT: 'light';
SWITCH: 'switch';
SENSOR: 'sensor';
EXTENDER: 'extender';
}
export interface OutputTypes {
LIGHT: 'LIGHT';
}
export interface SceneStates {
ON: 'On';
OFF: 'Off';
}
export interface Availability {
ONLINE: 'online';
OFFLINE: 'offline';
}
export interface AutomationTypes {
TRIGGER: 'trigger';
BUTTON_SHORT_PRESS: 'button_short_press';
}
export interface BluezIds {
SERVICE_NAME: 'org.bluez';
ADAPTER_ID: 'org.bluez.Adapter1';
DEVICE_ID: 'org.bluez.Device1';
GATT_SERVICE_ID: 'org.bluez.GattService1';
GATT_CHAR_ID: 'org.bluez.GattCharacteristic1';
}
export interface DbusInterface {
OM_INTERFACE: 'org.freedesktop.DBus.ObjectManager';
PROP_INTERFACE: 'org.freedesktop.DBus.Properties';
}
export interface ApiEndpoints {
APP_ID: string;
BASE_URL: string;
LOGIN_URL: string;
SITE_LIST_URL: string;
SITE_DETAILS_URL: string;
}
export interface BleCommands {
REMOTE_CLICK: number;
TIME_UPDATE: number;
SCENE_TRIGGER: number;
STATE_CHANGE: number;
DIM_CHANGE: number;
COLOR_CHANGE: number;
}
export interface Ble {
UUID_SUFFIX: string;
COMMANDS: BleCommands;
BROADCAST_DEVICE_ID: number;
}
export interface PlejdUuids {
PLEJD_SERVICE: string;
LIGHTLEVEL_UUID: string;
DATA_UUID: string;
LAST_DATA_UUID: string;
AUTH_UUID: string;
PING_UUID: string;
}
export interface Commands {
TURN_ON: string;
TURN_OFF: string;
DIM: string;
COLOR: string;
TRIGGER_SCENE: string;
BUTTON_CLICK: string;
}
export const MQTT_TYPES: MqttTypes;
export const TOPIC_TYPES: TopicTypes;
export const MQTT_STATE: MqttState;
export const DEVICE_TYPES: DeviceTypes;
export const AVAILABILITY: Availability;
export const AUTOMATION_TYPES: AutomationTypes;
export const BLE: Ble;
export const PLEJD_UUIDS: PlejdUuids;
export const COMMANDS: Commands;
export const OUTPUT_TYPES: OutputTypes;
export const SCENE_STATES: SceneStates;
export const BLUEZ: BluezIds;
export const DBUS: DbusInterface;
export const API: ApiEndpoints;

121
plejd/types/constants.js Normal file
View file

@ -0,0 +1,121 @@
/** @type {import('./Mqtt').MQTT_TYPES} */
const MQTT_TYPES = {
LIGHT: 'light',
SCENE: 'scene',
SWITCH: 'switch',
DEVICE_AUTOMATION: 'device_automation',
SENSOR: 'sensor',
EXTENDER: 'extender',
};
/** @type {import('./Mqtt').TOPIC_TYPES} */
const TOPIC_TYPES = {
CONFIG: 'config',
STATE: 'state',
AVAILABILITY: 'availability',
SET: 'set',
};
const MQTT_STATE = {
ON: 'ON',
OFF: 'OFF',
};
const DEVICE_TYPES = {
SCENE: 'scene',
LIGHT: 'light',
SWITCH: 'switch',
SENSOR: 'sensor',
EXTENDER: 'extender',
};
const OUTPUT_TYPES = {
LIGHT: 'LIGHT',
};
const SCENE_STATES = {
ON: 'On',
OFF: 'Off',
};
const AVAILABILITY = {
ONLINE: 'online',
OFFLINE: 'offline',
};
const AUTOMATION_TYPES = {
TRIGGER: 'trigger',
BUTTON_SHORT_PRESS: 'button_short_press',
};
const BLUEZ = {
SERVICE_NAME: 'org.bluez',
ADAPTER_ID: 'org.bluez.Adapter1',
DEVICE_ID: 'org.bluez.Device1',
GATT_SERVICE_ID: 'org.bluez.GattService1',
GATT_CHAR_ID: 'org.bluez.GattCharacteristic1',
};
const DBUS = {
OM_INTERFACE: 'org.freedesktop.DBus.ObjectManager',
PROP_INTERFACE: 'org.freedesktop.DBus.Properties',
};
const API = {
APP_ID: 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak',
BASE_URL: 'https://cloud.plejd.com/parse/',
LOGIN_URL: 'login',
SITE_LIST_URL: 'functions/getSiteList',
SITE_DETAILS_URL: 'functions/getSiteById',
};
// BLE Protocol Constants
const BLE = {
UUID_SUFFIX: '6085-4726-be45-040c957391b5',
COMMANDS: {
REMOTE_CLICK: 0x0016,
TIME_UPDATE: 0x001b,
SCENE_TRIGGER: 0x0021,
STATE_CHANGE: 0x0097,
DIM_CHANGE: 0x00c8,
COLOR_CHANGE: 0x0420,
},
BROADCAST_DEVICE_ID: 0x01,
};
// Generate UUIDs
const PLEJD_UUIDS = {
PLEJD_SERVICE: `31ba0001-${BLE.UUID_SUFFIX}`,
LIGHTLEVEL_UUID: `31ba0003-${BLE.UUID_SUFFIX}`,
DATA_UUID: `31ba0004-${BLE.UUID_SUFFIX}`,
LAST_DATA_UUID: `31ba0005-${BLE.UUID_SUFFIX}`,
AUTH_UUID: `31ba0009-${BLE.UUID_SUFFIX}`,
PING_UUID: `31ba000a-${BLE.UUID_SUFFIX}`,
};
// Commands from original constants.js
const COMMANDS = {
TURN_ON: 'Turn on',
TURN_OFF: 'Turn off',
DIM: 'Dim',
COLOR: 'Color',
TRIGGER_SCENE: 'Trigger scene',
BUTTON_CLICK: 'Button click',
};
module.exports = {
MQTT_TYPES,
TOPIC_TYPES,
MQTT_STATE,
DEVICE_TYPES,
AVAILABILITY,
AUTOMATION_TYPES,
BLE,
PLEJD_UUIDS,
COMMANDS,
OUTPUT_TYPES,
SCENE_STATES,
BLUEZ,
DBUS,
API,
};