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:
parent
d801410200
commit
1766afb2e0
14 changed files with 650 additions and 165 deletions
|
|
@ -1,5 +1,21 @@
|
|||
# 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)
|
||||
|
||||
[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.15.0...0.16.0)
|
||||
|
|
|
|||
|
|
@ -257,8 +257,9 @@ class DeviceRegistry {
|
|||
* @param {string} uniqueOutputId
|
||||
* @param {boolean} state
|
||||
* @param {number?} [dim]
|
||||
* @param {number?} [color]
|
||||
*/
|
||||
setOutputState(uniqueOutputId, state, dim) {
|
||||
setOutputState(uniqueOutputId, state, dim, color) {
|
||||
const device = this.getOutputDevice(uniqueOutputId);
|
||||
if (!device) {
|
||||
logger.warn(
|
||||
|
|
@ -268,9 +269,12 @@ class DeviceRegistry {
|
|||
}
|
||||
|
||||
device.state = state;
|
||||
if (dim && device.dimmable) {
|
||||
if (typeof dim === 'number' && device.dimmable) {
|
||||
device.dim = dim;
|
||||
}
|
||||
if (typeof color === 'number' && device.colorTemp) {
|
||||
device.colorTemp = color;
|
||||
}
|
||||
if (Logger.shouldLog('silly')) {
|
||||
logger.silly(`Updated state: ${JSON.stringify(device)}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,22 @@
|
|||
const EventEmitter = require('events');
|
||||
const { EventEmitter } = require('events');
|
||||
const mqtt = require('mqtt');
|
||||
|
||||
const Configuration = require('./Configuration');
|
||||
const Logger = require('./Logger');
|
||||
|
||||
// const startTopics = ['hass/status', 'homeassistant/status'];
|
||||
const {
|
||||
MQTT_TYPES,
|
||||
TOPIC_TYPES,
|
||||
MQTT_STATE,
|
||||
DEVICE_TYPES,
|
||||
AVAILABILITY,
|
||||
MQTT_TOPICS,
|
||||
} = require('./constants');
|
||||
|
||||
const logger = Logger.getLogger('plejd-mqtt');
|
||||
|
||||
const discoveryPrefix = 'homeassistant';
|
||||
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) =>
|
||||
`${discoveryPrefix}/${mqttDeviceType}/${nodeId}/${uniqueId}`;
|
||||
|
||||
|
|
@ -79,8 +69,9 @@ const getOutputDeviceDiscoveryPayload = (
|
|||
device.colorTempSettings &&
|
||||
device.colorTempSettings.behavior === 'adjustable'
|
||||
? {
|
||||
min_mireds: 1000000 / device.colorTempSettings.minTemperatureLimit,
|
||||
max_mireds: 1000000 / device.colorTempSettings.maxTemperatureLimit,
|
||||
color_temp_kelvin: true,
|
||||
min_kelvin: device.colorTempSettings.minTemperatureLimit,
|
||||
max_kelvin: device.colorTempSettings.maxTemperatureLimit,
|
||||
supported_color_modes: ['color_temp'],
|
||||
}
|
||||
: {}),
|
||||
|
|
@ -94,7 +85,7 @@ const getSceneDiscoveryPayload = (
|
|||
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
|
||||
command_topic: `~/${TOPIC_TYPES.SET}`,
|
||||
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
||||
payload_on: 'ON',
|
||||
payload_on: MQTT_STATE.ON,
|
||||
qos: 1,
|
||||
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 AVAILABLITY = { ONLINE: 'online', OFFLINE: 'offline' };
|
||||
const getMqttStateString = (/** @type {boolean} */ state) =>
|
||||
state ? MQTT_STATE.ON : MQTT_STATE.OFF;
|
||||
|
||||
class MqttClient extends EventEmitter {
|
||||
/** @type {import('DeviceRegistry')} */
|
||||
deviceRegistry;
|
||||
|
||||
static STATE = MQTT_STATE;
|
||||
static DEVICE_TYPES = DEVICE_TYPES;
|
||||
|
||||
static EVENTS = {
|
||||
connected: 'connected',
|
||||
stateChanged: 'stateChanged',
|
||||
|
|
@ -180,25 +174,24 @@ class MqttClient extends EventEmitter {
|
|||
this.client.on('connect', () => {
|
||||
logger.info('Connected to MQTT.');
|
||||
|
||||
logger.verbose('Emitting internal MqttClient.EVENTS.connected event');
|
||||
this.emit(MqttClient.EVENTS.connected);
|
||||
|
||||
// Testing to skip listening to HA birth messages all together
|
||||
// this.client.subscribe(
|
||||
// startTopics,
|
||||
// {
|
||||
// qos: 1,
|
||||
// nl: true, // don't echo back messages sent
|
||||
// rap: true, // retain as published - don't force retain = 0
|
||||
// rh: 0, // Retain handling 0 presumably ignores retained messages
|
||||
// },
|
||||
// (err) => {
|
||||
// if (err) {
|
||||
// logger.error('Unable to subscribe to status topics', err);
|
||||
// }
|
||||
|
||||
// this.emit(MqttClient.EVENTS.connected);
|
||||
// },
|
||||
// );
|
||||
// Listen for future Home Assistant birth messages if HA is not yet started
|
||||
this.client.subscribe(
|
||||
[MQTT_TOPICS.STATUS],
|
||||
{
|
||||
qos: 1,
|
||||
nl: true, // don't echo back messages sent
|
||||
rap: true, // retain as published - don't force retain = 0
|
||||
rh: 0, // Retain handling 0 presumably ignores retained messages
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
logger.error('Unable to subscribe to status topic', err);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
this.client.on('close', () => {
|
||||
|
|
@ -256,6 +249,14 @@ class MqttClient extends EventEmitter {
|
|||
default:
|
||||
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 {
|
||||
logger.verbose(
|
||||
`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;
|
||||
this.client.publish(
|
||||
getTopicName(outputDevice.uniqueId, mqttType, 'availability'),
|
||||
AVAILABLITY.OFFLINE,
|
||||
AVAILABILITY.OFFLINE,
|
||||
{
|
||||
retain: false, // Availability messages should NOT be retained
|
||||
qos: 1,
|
||||
|
|
@ -294,7 +295,7 @@ class MqttClient extends EventEmitter {
|
|||
allSceneDevices.forEach((sceneDevice) => {
|
||||
this.client.publish(
|
||||
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
||||
AVAILABLITY.OFFLINE,
|
||||
AVAILABILITY.OFFLINE,
|
||||
{
|
||||
retain: false, // Availability messages should NOT be retained
|
||||
qos: 1,
|
||||
|
|
@ -342,7 +343,7 @@ class MqttClient extends EventEmitter {
|
|||
|
||||
// Forcefully remove retained (from us, v0.11 and before) AVAILABILITY messages
|
||||
this.client.publish(
|
||||
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABLILITY),
|
||||
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
||||
null,
|
||||
{
|
||||
retain: true, // Retain true to remove previously retained message
|
||||
|
|
@ -356,7 +357,7 @@ class MqttClient extends EventEmitter {
|
|||
|
||||
this.client.publish(
|
||||
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
||||
AVAILABLITY.ONLINE,
|
||||
AVAILABILITY.ONLINE,
|
||||
{
|
||||
retain: false, // Availability messages should NOT be retained
|
||||
qos: 1,
|
||||
|
|
@ -430,7 +431,7 @@ class MqttClient extends EventEmitter {
|
|||
|
||||
this.client.publish(
|
||||
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
||||
AVAILABLITY.ONLINE,
|
||||
AVAILABILITY.ONLINE,
|
||||
{
|
||||
retain: false, // Availability messages should NOT be retained
|
||||
qos: 1,
|
||||
|
|
@ -457,7 +458,7 @@ class MqttClient extends EventEmitter {
|
|||
|
||||
/**
|
||||
* @param {string} uniqueOutputId
|
||||
* @param {{ state: boolean; brightness?: number; }} data
|
||||
* @param {{ state: boolean; brightness?: number; color?: number}} data
|
||||
*/
|
||||
updateOutputState(uniqueOutputId, data) {
|
||||
const device = this.deviceRegistry.getOutputDevice(uniqueOutputId);
|
||||
|
|
@ -470,7 +471,7 @@ class MqttClient extends EventEmitter {
|
|||
logger.verbose(
|
||||
`Updating state for ${device.name}: ${data.state}${
|
||||
data.brightness ? `, dim: ${data.brightness}` : ''
|
||||
}`,
|
||||
}${data.color ? `, color: ${data.color}K` : ''}`,
|
||||
);
|
||||
let payload = null;
|
||||
|
||||
|
|
@ -478,10 +479,19 @@ class MqttClient extends EventEmitter {
|
|||
payload = getMqttStateString(data.state);
|
||||
} else {
|
||||
if (device.dimmable) {
|
||||
payload = {
|
||||
state: getMqttStateString(data.state),
|
||||
brightness: data.brightness,
|
||||
};
|
||||
if (data.color) {
|
||||
payload = {
|
||||
state: getMqttStateString(data.state),
|
||||
brightness: data.brightness,
|
||||
color_mode: 'color_temp',
|
||||
color_temp: data.color,
|
||||
};
|
||||
} else {
|
||||
payload = {
|
||||
state: getMqttStateString(data.state),
|
||||
brightness: data.brightness,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
state: getMqttStateString(data.state),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const EventEmitter = require('events');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
const Configuration = require('./Configuration');
|
||||
const Logger = require('./Logger');
|
||||
|
|
@ -55,6 +55,15 @@ class PlejdAddon extends EventEmitter {
|
|||
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, () => {
|
||||
try {
|
||||
logger.verbose('connected to mqtt.');
|
||||
|
|
@ -72,7 +81,7 @@ class PlejdAddon extends EventEmitter {
|
|||
try {
|
||||
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.
|
||||
// since scenes aren't "real" devices.
|
||||
this.sceneManager.executeScene(uniqueId);
|
||||
|
|
@ -93,7 +102,7 @@ class PlejdAddon extends EventEmitter {
|
|||
|
||||
if (typeof command === 'string') {
|
||||
// switch command
|
||||
state = command === 'ON';
|
||||
state = command === MqttClient.STATE.ON;
|
||||
commandObj = {
|
||||
state,
|
||||
};
|
||||
|
|
@ -106,7 +115,7 @@ class PlejdAddon extends EventEmitter {
|
|||
});
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
state = command.state === 'ON';
|
||||
state = command.state === MqttClient.STATE.ON;
|
||||
commandObj = command;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,16 @@ const fs = require('fs');
|
|||
const Configuration = require('./Configuration');
|
||||
const Logger = require('./Logger');
|
||||
|
||||
const API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak';
|
||||
const API_BASE_URL = 'https://cloud.plejd.com/parse/';
|
||||
const API_LOGIN_URL = 'login';
|
||||
const API_SITE_LIST_URL = 'functions/getSiteList';
|
||||
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
||||
const {
|
||||
API: {
|
||||
APP_ID: API_APP_ID,
|
||||
BASE_URL: API_BASE_URL,
|
||||
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 = {
|
||||
NO_LOAD: 0, // 0b0000
|
||||
|
|
@ -305,7 +310,7 @@ class PlejdApi {
|
|||
return {
|
||||
name: 'REL-01',
|
||||
description: '1 channel relay, 3500 VA',
|
||||
type: 'switch',
|
||||
type: DEVICE_TYPES.SWITCH,
|
||||
dimmable: false,
|
||||
broadcastClicks: false,
|
||||
};
|
||||
|
|
@ -313,7 +318,7 @@ class PlejdApi {
|
|||
return {
|
||||
name: 'SPR-01',
|
||||
description: 'Smart plug on/off with relay, 3500 VA',
|
||||
type: 'switch',
|
||||
type: DEVICE_TYPES.SWITCH,
|
||||
dimmable: false,
|
||||
broadcastClicks: false,
|
||||
};
|
||||
|
|
@ -363,7 +368,7 @@ class PlejdApi {
|
|||
return {
|
||||
name: 'REL-01-2P',
|
||||
description: '1-channel relay with 2-pole 3500 VA',
|
||||
type: 'switch',
|
||||
type: DEVICE_TYPES.SWITCH,
|
||||
dimmable: false,
|
||||
broadcastClicks: false,
|
||||
};
|
||||
|
|
@ -371,7 +376,7 @@ class PlejdApi {
|
|||
return {
|
||||
name: 'REL-02',
|
||||
description: '2-channel relay with combined 3500 VA',
|
||||
type: 'switch',
|
||||
type: DEVICE_TYPES.SWITCH,
|
||||
dimmable: false,
|
||||
broadcastClicks: false,
|
||||
};
|
||||
|
|
@ -512,11 +517,6 @@ class PlejdApi {
|
|||
// 2. outputSettings.dimCurve NOT IN ["NonDimmable", "RelayNormal"]: Dimmable
|
||||
// 3. outputSettings.predefinedLoad !== null && outputSettings.predefinedLoad.loadType === "DWN": Dimmable
|
||||
|
||||
const colorTemp =
|
||||
outputSettings &&
|
||||
outputSettings.colorTemperature &&
|
||||
outputSettings.colorTemperature.behavior === 'adjustable';
|
||||
|
||||
try {
|
||||
const decodedDeviceType = this._getDeviceType(plejdDevice);
|
||||
|
||||
|
|
@ -533,7 +533,7 @@ class PlejdApi {
|
|||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||
const outputDevice = {
|
||||
bleOutputAddress,
|
||||
colorTemp,
|
||||
colorTemp: null,
|
||||
colorTempSettings: outputSettings ? outputSettings.colorTemperature : null,
|
||||
deviceId: device.deviceId,
|
||||
dimmable,
|
||||
|
|
@ -640,7 +640,7 @@ class PlejdApi {
|
|||
const newDevice = {
|
||||
bleOutputAddress: roomAddress,
|
||||
deviceId: null,
|
||||
colorTemp: false,
|
||||
colorTemp: null,
|
||||
dimmable,
|
||||
name: room.title,
|
||||
output: undefined,
|
||||
|
|
@ -670,7 +670,7 @@ class PlejdApi {
|
|||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||
const newScene = {
|
||||
bleOutputAddress: sceneNum,
|
||||
colorTemp: false,
|
||||
colorTemp: null,
|
||||
deviceId: undefined,
|
||||
dimmable: false,
|
||||
name: scene.title,
|
||||
|
|
@ -678,9 +678,9 @@ class PlejdApi {
|
|||
roomId: undefined,
|
||||
roomName: undefined,
|
||||
state: false,
|
||||
type: 'scene',
|
||||
type: DEVICE_TYPES.SCENE,
|
||||
typeDescription: 'A Plejd scene',
|
||||
typeName: 'Scene',
|
||||
typeName: DEVICE_TYPES.SCENE,
|
||||
version: undefined,
|
||||
uniqueId: scene.sceneId,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,43 +4,44 @@ const xor = require('buffer-xor');
|
|||
const { EventEmitter } = require('events');
|
||||
|
||||
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 { COMMANDS } = constants;
|
||||
const logger = Logger.getLogger('plejd-ble');
|
||||
|
||||
// UUIDs
|
||||
const BLE_UUID_SUFFIX = '6085-4726-be45-040c957391b5';
|
||||
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 { PLEJD_SERVICE, AUTH_UUID, DATA_UUID, LAST_DATA_UUID, PING_UUID } = PLEJD_UUIDS;
|
||||
const { COMMANDS: BLE_COMMANDS, BROADCAST_DEVICE_ID: BLE_BROADCAST_DEVICE_ID } = BLE;
|
||||
|
||||
const BLE_CMD_DIM_CHANGE = 0x00c8;
|
||||
const BLE_CMD_DIM2_CHANGE = 0x0098;
|
||||
const BLE_CMD_STATE_CHANGE = 0x0097;
|
||||
const BLE_CMD_SCENE_TRIG = 0x0021;
|
||||
const BLE_CMD_TIME_UPDATE = 0x001b;
|
||||
const BLE_CMD_REMOTE_CLICK = 0x0016;
|
||||
// BLE commands for easier access
|
||||
const {
|
||||
REMOTE_CLICK: BLE_CMD_REMOTE_CLICK,
|
||||
TIME_UPDATE: BLE_CMD_TIME_UPDATE,
|
||||
SCENE_TRIGGER: BLE_CMD_SCENE_TRIG,
|
||||
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_REQUEST_NO_RESPONSE = 0x0110;
|
||||
const BLE_REQUEST_RESPONSE = 0x0102;
|
||||
// const BLE_REQUEST_READ_VALUE = 0x0103;
|
||||
|
||||
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 BLE_CMD_DIM2_CHANGE = 0x0098; // Dim + state update
|
||||
const BLE_REQUEST_NO_RESPONSE = 0x0110; // Set value, no response.
|
||||
const BLE_REQUEST_RESPONSE = 0x0102; // Request response, time for example
|
||||
// const BLE_REQUEST_READ_VALUE = 0x0103; // Read value?
|
||||
|
||||
const PAYLOAD_POSITION_OFFSET = 5;
|
||||
const DIM_LEVEL_POSITION_OFFSET = 7;
|
||||
const COLOR_TEMP_POSITION_OFFSET = 9;
|
||||
|
||||
const delay = (timeout) =>
|
||||
new Promise((resolve) => {
|
||||
|
|
@ -168,11 +169,13 @@ class PlejBLEHandler extends EventEmitter {
|
|||
/**
|
||||
* @param {string} command
|
||||
* @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 brightnessVal;
|
||||
|
||||
switch (command) {
|
||||
case COMMANDS.TURN_ON:
|
||||
payload = this._createHexPayload(bleOutputAddress, BLE_CMD_STATE_CHANGE, '01');
|
||||
|
|
@ -182,18 +185,39 @@ class PlejBLEHandler extends EventEmitter {
|
|||
break;
|
||||
case COMMANDS.DIM:
|
||||
// eslint-disable-next-line no-bitwise
|
||||
brightnessVal = (data << 8) | data;
|
||||
brightnessVal = (brightness << 8) | brightness;
|
||||
payload = this._createHexPayload(
|
||||
bleOutputAddress,
|
||||
BLE_CMD_DIM2_CHANGE,
|
||||
`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;
|
||||
default:
|
||||
logger.error(`Unknown command ${command}`);
|
||||
throw new Error(`Unknown command ${command}`);
|
||||
}
|
||||
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) {
|
||||
|
|
@ -302,10 +326,12 @@ class PlejBLEHandler extends EventEmitter {
|
|||
} else {
|
||||
logger.info('Plejd clock updates disabled in configuration.');
|
||||
}
|
||||
|
||||
this._startPing();
|
||||
|
||||
// After we've authenticated, we need to hook up the event listener
|
||||
// for changes to lastData.
|
||||
// After we've authenticated:
|
||||
|
||||
// Hook up the event listener for changes to lastData.
|
||||
this.characteristics.lastDataProperties.on(
|
||||
'PropertiesChanged',
|
||||
(
|
||||
|
|
@ -577,6 +603,7 @@ class PlejBLEHandler extends EventEmitter {
|
|||
);
|
||||
const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, payload);
|
||||
await this.characteristics.data.WriteValue([...encryptedData], {});
|
||||
|
||||
await this._onWriteSuccess();
|
||||
} catch (err) {
|
||||
await this._onWriteFailed(err);
|
||||
|
|
@ -850,7 +877,6 @@ class PlejBLEHandler extends EventEmitter {
|
|||
const outputUniqueId = device ? device.uniqueId : null;
|
||||
|
||||
if (Logger.shouldLog('verbose')) {
|
||||
// decoded.toString() could potentially be expensive
|
||||
logger.verbose(`Raw event received: ${decoded.toString('hex')}`);
|
||||
logger.verbose(
|
||||
`Decoded: Device ${outputUniqueId} (BLE address ${bleOutputAddress}), cmd ${cmd.toString(
|
||||
|
|
@ -869,6 +895,18 @@ class PlejBLEHandler extends EventEmitter {
|
|||
command = COMMANDS.DIM;
|
||||
data = { state, dim };
|
||||
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) {
|
||||
logger.debug(`${deviceName} (${outputUniqueId}) got state update. S: ${state}`);
|
||||
command = state ? COMMANDS.TURN_ON : COMMANDS.TURN_OFF;
|
||||
|
|
@ -1006,7 +1044,8 @@ class PlejBLEHandler extends EventEmitter {
|
|||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_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 part2 = intermediate.subarray(16);
|
||||
|
||||
|
|
@ -1022,7 +1061,7 @@ class PlejBLEHandler extends EventEmitter {
|
|||
const cipher = crypto.createCipheriv('aes-128-ecb', key, '');
|
||||
cipher.setAutoPadding(false);
|
||||
|
||||
let ct = cipher.update(buf).toString('hex');
|
||||
let ct = cipher.update(new Uint8Array(buf)).toString('hex');
|
||||
ct += cipher.final().toString('hex');
|
||||
const ctBuf = Buffer.from(ct, 'hex');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
const { EventEmitter } = require('events');
|
||||
const Configuration = require('./Configuration');
|
||||
const constants = require('./constants');
|
||||
const { COMMANDS } = require('./constants');
|
||||
const Logger = require('./Logger');
|
||||
const PlejBLEHandler = require('./PlejdBLEHandler');
|
||||
|
||||
const { COMMANDS } = constants;
|
||||
const logger = Logger.getLogger('device-comm');
|
||||
|
||||
const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting
|
||||
|
|
@ -18,7 +17,7 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
/** @type {import('./DeviceRegistry')} */
|
||||
deviceRegistry;
|
||||
// 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 = [];
|
||||
writeQueueRef = null;
|
||||
|
||||
|
|
@ -79,11 +78,9 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
turnOn(uniqueOutputId, command) {
|
||||
const deviceName = this.deviceRegistry.getOutputDeviceName(uniqueOutputId);
|
||||
logger.info(
|
||||
`Plejd got turn on command for ${deviceName} (${uniqueOutputId}), brightness ${
|
||||
command.brightness
|
||||
}${command.transition ? `, transition: ${command.transition}` : ''}`,
|
||||
`Plejd got turn on command for ${deviceName} (${uniqueOutputId})${JSON.stringify(command)}`,
|
||||
);
|
||||
this._transitionTo(uniqueOutputId, command.brightness, command.transition, deviceName);
|
||||
this._transitionTo(uniqueOutputId, command, deviceName);
|
||||
}
|
||||
|
||||
turnOff(uniqueOutputId, command) {
|
||||
|
|
@ -93,17 +90,27 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
command.transition ? `, transition: ${command.transition}` : ''
|
||||
}`,
|
||||
);
|
||||
this._transitionTo(uniqueOutputId, 0, command.transition, deviceName);
|
||||
this._transitionTo(uniqueOutputId, { ...command, brightness: 0 }, deviceName);
|
||||
}
|
||||
|
||||
_bleCommandReceived(uniqueOutputId, command, data) {
|
||||
try {
|
||||
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.emit(PlejdDeviceCommunication.EVENTS.stateChanged, uniqueOutputId, {
|
||||
state: !!data.state,
|
||||
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) {
|
||||
this.deviceRegistry.setOutputState(uniqueOutputId, true);
|
||||
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 initialBrightness = device ? device.state && device.dim : null;
|
||||
this._clearDeviceTransitionTimer(uniqueOutputId);
|
||||
|
|
@ -140,11 +152,11 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
const isDimmable = this.deviceRegistry.getOutputDevice(uniqueOutputId).dimmable;
|
||||
|
||||
if (
|
||||
transition > 1 &&
|
||||
command.transition > 1 &&
|
||||
isDimmable &&
|
||||
(initialBrightness || initialBrightness === 0) &&
|
||||
(targetBrightness || targetBrightness === 0) &&
|
||||
targetBrightness !== initialBrightness
|
||||
(command.brightness || command.brightness === 0) &&
|
||||
command.brightness !== initialBrightness
|
||||
) {
|
||||
// Transition time set, known initial and target brightness
|
||||
// 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
|
||||
// than we can in transitioning so transitioning will be skipped
|
||||
|
||||
const deltaBrightness = targetBrightness - initialBrightness;
|
||||
const deltaBrightness = command.brightness - initialBrightness;
|
||||
const transitionSteps = Math.min(
|
||||
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(
|
||||
`transitioning from ${initialBrightness} to ${targetBrightness} ${
|
||||
transition ? `in ${transition} seconds` : ''
|
||||
`transitioning from ${initialBrightness} to ${command.brightness} ${
|
||||
command.transition ? `in ${command.transition} seconds` : ''
|
||||
}.`,
|
||||
);
|
||||
logger.verbose(
|
||||
|
|
@ -176,68 +188,105 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
const tElapsedMs = new Date().getTime() - dtStart.getTime();
|
||||
let tElapsed = tElapsedMs / 1000;
|
||||
|
||||
if (tElapsed > transition || tElapsed < 0) {
|
||||
tElapsed = transition;
|
||||
if (tElapsed > command.transition || tElapsed < 0) {
|
||||
tElapsed = command.transition;
|
||||
}
|
||||
|
||||
let newBrightness = Math.round(
|
||||
initialBrightness + (deltaBrightness * tElapsed) / transition,
|
||||
initialBrightness + (deltaBrightness * tElapsed) / command.transition,
|
||||
);
|
||||
|
||||
if (tElapsed === transition) {
|
||||
if (tElapsed === command.transition) {
|
||||
nSteps++;
|
||||
this._clearDeviceTransitionTimer(uniqueOutputId);
|
||||
newBrightness = targetBrightness;
|
||||
newBrightness = command.brightness;
|
||||
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)
|
||||
} ms.`,
|
||||
);
|
||||
this._setBrightness(uniqueOutputId, newBrightness, true, deviceName);
|
||||
this._setLightState(
|
||||
uniqueOutputId,
|
||||
{ ...command, brightness: newBrightness },
|
||||
true,
|
||||
deviceName,
|
||||
);
|
||||
} else {
|
||||
nSteps++;
|
||||
logger.verbose(
|
||||
`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);
|
||||
} else {
|
||||
if (transition && isDimmable) {
|
||||
if (command.transition && isDimmable) {
|
||||
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(
|
||||
`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);
|
||||
} else if (brightness <= 0) {
|
||||
logger.debug(`Queueing turn off ${unqiueOutputId}`);
|
||||
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.TURN_OFF, null, shouldRetry);
|
||||
lightCommand.command = COMMANDS.TURN_ON;
|
||||
} else if (command.brightness <= 0) {
|
||||
logger.debug(`Queueing turn off ${uniqueOutputId}`);
|
||||
lightCommand.command = COMMANDS.TURN_OFF;
|
||||
} else {
|
||||
if (brightness > 255) {
|
||||
if (command.brightness > 255) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
brightness = 255;
|
||||
command.brightness = 255;
|
||||
}
|
||||
|
||||
logger.debug(`Queueing ${unqiueOutputId} set brightness to ${brightness}`);
|
||||
// eslint-disable-next-line no-bitwise
|
||||
this._appendCommandToWriteQueue(unqiueOutputId, COMMANDS.DIM, brightness, shouldRetry);
|
||||
logger.debug(`Queueing ${uniqueOutputId} set brightness to ${command.brightness}`);
|
||||
|
||||
lightCommand.command = COMMANDS.DIM;
|
||||
lightCommand.brightness = command.brightness;
|
||||
}
|
||||
|
||||
if (command.color_temp) {
|
||||
lightCommand.command = COMMANDS.COLOR;
|
||||
lightCommand.color_temp = command.color_temp;
|
||||
}
|
||||
|
||||
this._appendCommandToWriteQueue(
|
||||
uniqueOutputId,
|
||||
// @ts-ignore
|
||||
lightCommand,
|
||||
shouldRetry,
|
||||
);
|
||||
}
|
||||
|
||||
_appendCommandToWriteQueue(uniqueOutputId, command, data, 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({
|
||||
uniqueOutputId,
|
||||
command,
|
||||
data,
|
||||
shouldRetry,
|
||||
});
|
||||
}
|
||||
|
|
@ -260,9 +309,9 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
const device = this.deviceRegistry.getOutputDevice(queueItem.uniqueOutputId);
|
||||
|
||||
logger.debug(
|
||||
`Write queue: Processing ${device.name} (${queueItem.uniqueOutputId}). Command ${
|
||||
queueItem.command
|
||||
}${queueItem.data ? ` ${queueItem.data}` : ''}. Total queue length: ${
|
||||
`Write queue: Processing ${device.name} (${
|
||||
queueItem.uniqueOutputId
|
||||
}). Command ${JSON.stringify(queueItem.command)}. Total queue length: ${
|
||||
this.writeQueue.length
|
||||
}`,
|
||||
);
|
||||
|
|
@ -278,9 +327,10 @@ class PlejdDeviceCommunication extends EventEmitter {
|
|||
/* eslint-disable no-await-in-loop */
|
||||
try {
|
||||
await this.plejdBleHandler.sendCommand(
|
||||
queueItem.command,
|
||||
queueItem.command.command,
|
||||
device.bleOutputAddress,
|
||||
queueItem.data,
|
||||
queueItem.command.brightness,
|
||||
queueItem.command.color_temp,
|
||||
);
|
||||
} catch (err) {
|
||||
if (queueItem.shouldRetry) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
const { SCENE_STATES } = require('./constants');
|
||||
|
||||
class SceneStep {
|
||||
/**
|
||||
* @param {import("./types/ApiSite").SceneStep} step
|
||||
|
|
@ -6,7 +8,7 @@ class SceneStep {
|
|||
this.sceneId = step.sceneId;
|
||||
this.deviceId = step.deviceId;
|
||||
this.output = step.output;
|
||||
this.state = step.state === 'On' ? 1 : 0;
|
||||
this.state = step.state === SCENE_STATES.ON ? 1 : 0;
|
||||
this.brightness = step.value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Plejd",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"slug": "plejd",
|
||||
"description": "Adds support for the Swedish home automation devices from Plejd.",
|
||||
"url": "https://github.com/icanos/hassio-plejd/",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
TURN_ON: 'Turn on',
|
||||
TURN_OFF: 'Turn off',
|
||||
DIM: 'Dim',
|
||||
COLOR: 'Color',
|
||||
TRIGGER_SCENE: 'Trigger scene',
|
||||
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,
|
||||
};
|
||||
|
|
|
|||
2
plejd/types/DeviceRegistry.d.ts
vendored
2
plejd/types/DeviceRegistry.d.ts
vendored
|
|
@ -6,7 +6,7 @@ export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice };
|
|||
|
||||
export interface OutputDevice {
|
||||
bleOutputAddress: number;
|
||||
colorTemp: boolean;
|
||||
colorTemp: number?;
|
||||
colorTempSettings?: OutputSettingColorTemperature
|
||||
deviceId: string;
|
||||
dim?: number;
|
||||
|
|
|
|||
2
plejd/types/Mqtt.d.ts
vendored
2
plejd/types/Mqtt.d.ts
vendored
|
|
@ -3,5 +3,5 @@
|
|||
export type TopicType = 'config' | 'state' | 'availability' | 'set';
|
||||
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 };
|
||||
|
|
|
|||
118
plejd/types/constants.d.ts
vendored
Normal file
118
plejd/types/constants.d.ts
vendored
Normal 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
121
plejd/types/constants.js
Normal 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,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue