diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index 8139f0a..7913810 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -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) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index 491b92a..238c5fb 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -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)}`); } diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index bae6144..ed35d9a 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -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), diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index 323678b..c2b90e6 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -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; } diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index c3a6afb..e57ab7f 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -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, }; diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index aa68442..5b0a21f 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -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'); diff --git a/plejd/PlejdDeviceCommunication.js b/plejd/PlejdDeviceCommunication.js index 9b38604..7968736 100644 --- a/plejd/PlejdDeviceCommunication.js +++ b/plejd/PlejdDeviceCommunication.js @@ -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) { diff --git a/plejd/SceneStep.js b/plejd/SceneStep.js index 3ccc06f..17980cc 100644 --- a/plejd/SceneStep.js +++ b/plejd/SceneStep.js @@ -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; } } diff --git a/plejd/config.json b/plejd/config.json index ba2a9df..e3ff4e4 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -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/", diff --git a/plejd/constants.js b/plejd/constants.js index 1b97f67..01f1a14 100644 --- a/plejd/constants.js +++ b/plejd/constants.js @@ -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, +}; diff --git a/plejd/types/DeviceRegistry.d.ts b/plejd/types/DeviceRegistry.d.ts index b81ccb6..2e8bc4f 100644 --- a/plejd/types/DeviceRegistry.d.ts +++ b/plejd/types/DeviceRegistry.d.ts @@ -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; diff --git a/plejd/types/Mqtt.d.ts b/plejd/types/Mqtt.d.ts index d04aeda..8be8bfd 100644 --- a/plejd/types/Mqtt.d.ts +++ b/plejd/types/Mqtt.d.ts @@ -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 }; diff --git a/plejd/types/constants.d.ts b/plejd/types/constants.d.ts new file mode 100644 index 0000000..71a9362 --- /dev/null +++ b/plejd/types/constants.d.ts @@ -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; diff --git a/plejd/types/constants.js b/plejd/types/constants.js new file mode 100644 index 0000000..b8a2b82 --- /dev/null +++ b/plejd/types/constants.js @@ -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, +};