Improve MQTT behavior and add experimental parsing for color temp support from Plejd API (#298)
* Update underlying docker containers and dependencies. * Minor linting and code issues fixes * Update supported devices section * Improve Mqtt message properties - retain, etc. - Retain discovery messages - Don't retain others - Set QoS to 1 consistently to ensure at least once delivery - Set session timeout to ensure a reasonable TTL on messages * Code and logic to remove any retained mqtt messages for SET/STATE/AVAILABILITY * Temporary restructure of init flow for mqtt. - No longer wait for HA birth message - Don't listen to incoming messages until old retained messages have been purged. - More details in https://github.com/icanos/hassio-plejd/issues/218 * Fix lingering incorrect access of connectedDevice.id * Fix to avoid Home Assistant setting retain flag on MQTT SET STATE messages * Parse new TRAIT=15, assuming this means dimmable and tunable white * Clarify TRAITS bitmask values * Add experimental parsing for color temp support from Plejd API * Lint fixes * Remove null coalescing operator since we don't compile code * Handle case where outputSettings is null in PlejdApi * Solve MQTT errors due to deprecated api color_temp and unsupported removal of retained state messages
This commit is contained in:
parent
a789d913d7
commit
b3c6334f0c
10 changed files with 255 additions and 147 deletions
|
|
@ -17,7 +17,10 @@ module.exports = {
|
||||||
// 'prettier',
|
// 'prettier',
|
||||||
// 'plugin:prettier/recommended'
|
// 'plugin:prettier/recommended'
|
||||||
],
|
],
|
||||||
parser: 'babel-eslint',
|
parser: '@babel/eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
requireConfigFile: false,
|
||||||
|
},
|
||||||
// plugins: ['prettier'],
|
// plugins: ['prettier'],
|
||||||
rules: getRules(),
|
rules: getRules(),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
ARG BUILD_FROM=hassioaddons/base:8.0.6
|
ARG BUILD_FROM=hassioaddons/base:14.2.2
|
||||||
FROM $BUILD_FROM
|
FROM $BUILD_FROM
|
||||||
|
|
||||||
ENV LANG C.UTF-8
|
ENV LANG C.UTF-8
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ const mqtt = require('mqtt');
|
||||||
const Configuration = require('./Configuration');
|
const Configuration = require('./Configuration');
|
||||||
const Logger = require('./Logger');
|
const Logger = require('./Logger');
|
||||||
|
|
||||||
const startTopics = ['hass/status', 'homeassistant/status'];
|
// const startTopics = ['hass/status', 'homeassistant/status'];
|
||||||
|
|
||||||
const logger = Logger.getLogger('plejd-mqtt');
|
const logger = Logger.getLogger('plejd-mqtt');
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ const TOPIC_TYPES = {
|
||||||
CONFIG: 'config',
|
CONFIG: 'config',
|
||||||
STATE: 'state',
|
STATE: 'state',
|
||||||
AVAILABILITY: 'availability',
|
AVAILABILITY: 'availability',
|
||||||
COMMAND: 'set',
|
SET: 'set',
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) =>
|
const getBaseTopic = (/** @type { string } */ uniqueId, /** @type { string } */ mqttDeviceType) =>
|
||||||
|
|
@ -43,9 +43,8 @@ const getSceneEventTopic = (/** @type {string} */ sceneId) =>
|
||||||
`${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`;
|
`${getTopicName(getTriggerUniqueId(sceneId), MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.STATE)}`;
|
||||||
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
|
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
|
||||||
|
|
||||||
const decodeTopicRegexp = new RegExp(
|
const decodeTopicRegexp =
|
||||||
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/,
|
/(?<prefix>[^[]+)\/(?<type>.+)\/plejd\/(?<id>.+)\/(?<command>config|state|availability|set|scene)/;
|
||||||
);
|
|
||||||
|
|
||||||
const decodeTopic = (topic) => {
|
const decodeTopic = (topic) => {
|
||||||
const matches = decodeTopicRegexp.exec(topic);
|
const matches = decodeTopicRegexp.exec(topic);
|
||||||
|
|
@ -62,11 +61,11 @@ const getOutputDeviceDiscoveryPayload = (
|
||||||
unique_id: device.uniqueId,
|
unique_id: device.uniqueId,
|
||||||
'~': getBaseTopic(device.uniqueId, device.type),
|
'~': getBaseTopic(device.uniqueId, device.type),
|
||||||
state_topic: `~/${TOPIC_TYPES.STATE}`,
|
state_topic: `~/${TOPIC_TYPES.STATE}`,
|
||||||
command_topic: `~/${TOPIC_TYPES.COMMAND}`,
|
command_topic: `~/${TOPIC_TYPES.SET}`,
|
||||||
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
||||||
optimistic: false,
|
optimistic: false,
|
||||||
qos: 1,
|
qos: 1,
|
||||||
retain: true,
|
retain: false, // State update messages from HA should not be retained
|
||||||
device: {
|
device: {
|
||||||
identifiers: `${device.uniqueId}`,
|
identifiers: `${device.uniqueId}`,
|
||||||
manufacturer: 'Plejd',
|
manufacturer: 'Plejd',
|
||||||
|
|
@ -76,6 +75,15 @@ const getOutputDeviceDiscoveryPayload = (
|
||||||
sw_version: device.version,
|
sw_version: device.version,
|
||||||
},
|
},
|
||||||
...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}),
|
...(device.type === MQTT_TYPES.LIGHT ? { brightness: device.dimmable, schema: 'json' } : {}),
|
||||||
|
...(device.type === MQTT_TYPES.LIGHT &&
|
||||||
|
device.colorTempSettings &&
|
||||||
|
device.colorTempSettings.behavior === 'adjustable'
|
||||||
|
? {
|
||||||
|
min_mireds: 1000000 / device.colorTempSettings.minTemperatureLimit,
|
||||||
|
max_mireds: 1000000 / device.colorTempSettings.maxTemperatureLimit,
|
||||||
|
supported_color_modes: ['color_temp'],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getSceneDiscoveryPayload = (
|
const getSceneDiscoveryPayload = (
|
||||||
|
|
@ -84,11 +92,11 @@ const getSceneDiscoveryPayload = (
|
||||||
name: sceneDevice.name,
|
name: sceneDevice.name,
|
||||||
unique_id: sceneDevice.uniqueId,
|
unique_id: sceneDevice.uniqueId,
|
||||||
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
|
'~': getBaseTopic(sceneDevice.uniqueId, MQTT_TYPES.SCENE),
|
||||||
command_topic: `~/${TOPIC_TYPES.COMMAND}`,
|
command_topic: `~/${TOPIC_TYPES.SET}`,
|
||||||
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
availability_topic: `~/${TOPIC_TYPES.AVAILABILITY}`,
|
||||||
payload_on: 'ON',
|
payload_on: 'ON',
|
||||||
qos: 1,
|
qos: 1,
|
||||||
retain: false,
|
retain: false, // State update messages from HA should not be retained
|
||||||
});
|
});
|
||||||
|
|
||||||
const getInputDeviceTriggerDiscoveryPayload = (
|
const getInputDeviceTriggerDiscoveryPayload = (
|
||||||
|
|
@ -98,9 +106,10 @@ const getInputDeviceTriggerDiscoveryPayload = (
|
||||||
payload: `${inputDevice.input}`,
|
payload: `${inputDevice.input}`,
|
||||||
'~': getBaseTopic(inputDevice.deviceId, MQTT_TYPES.DEVICE_AUTOMATION),
|
'~': getBaseTopic(inputDevice.deviceId, MQTT_TYPES.DEVICE_AUTOMATION),
|
||||||
qos: 1,
|
qos: 1,
|
||||||
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
||||||
|
subtype: `button_${inputDevice.input + 1}`,
|
||||||
topic: `~/${TOPIC_TYPES.STATE}`,
|
topic: `~/${TOPIC_TYPES.STATE}`,
|
||||||
type: 'button_short_press',
|
type: 'button_short_press',
|
||||||
subtype: `button_${inputDevice.input + 1}`,
|
|
||||||
device: {
|
device: {
|
||||||
identifiers: `${inputDevice.deviceId}`,
|
identifiers: `${inputDevice.deviceId}`,
|
||||||
manufacturer: 'Plejd',
|
manufacturer: 'Plejd',
|
||||||
|
|
@ -115,6 +124,7 @@ const getSceneDeviceTriggerhDiscoveryPayload = (
|
||||||
automation_type: 'trigger',
|
automation_type: 'trigger',
|
||||||
'~': getBaseTopic(`${sceneDevice.uniqueId}_trig`, MQTT_TYPES.DEVICE_AUTOMATION),
|
'~': getBaseTopic(`${sceneDevice.uniqueId}_trig`, MQTT_TYPES.DEVICE_AUTOMATION),
|
||||||
qos: 1,
|
qos: 1,
|
||||||
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
||||||
topic: `~/${TOPIC_TYPES.STATE}`,
|
topic: `~/${TOPIC_TYPES.STATE}`,
|
||||||
type: 'scene',
|
type: 'scene',
|
||||||
subtype: 'trigger',
|
subtype: 'trigger',
|
||||||
|
|
@ -127,7 +137,7 @@ const getSceneDeviceTriggerhDiscoveryPayload = (
|
||||||
});
|
});
|
||||||
|
|
||||||
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
|
const getMqttStateString = (/** @type {boolean} */ state) => (state ? 'ON' : 'OFF');
|
||||||
const AVAILABLILITY = { ONLINE: 'online', OFFLINE: 'offline' };
|
const AVAILABLITY = { ONLINE: 'online', OFFLINE: 'offline' };
|
||||||
|
|
||||||
class MqttClient extends EventEmitter {
|
class MqttClient extends EventEmitter {
|
||||||
/** @type {import('DeviceRegistry')} */
|
/** @type {import('DeviceRegistry')} */
|
||||||
|
|
@ -152,9 +162,13 @@ class MqttClient extends EventEmitter {
|
||||||
logger.info('Initializing MQTT connection for Plejd addon');
|
logger.info('Initializing MQTT connection for Plejd addon');
|
||||||
|
|
||||||
this.client = mqtt.connect(this.config.mqttBroker, {
|
this.client = mqtt.connect(this.config.mqttBroker, {
|
||||||
|
clean: true, // We're moving to not saving mqtt messages
|
||||||
clientId: `hassio-plejd_${Math.random().toString(16).substr(2, 8)}`,
|
clientId: `hassio-plejd_${Math.random().toString(16).substr(2, 8)}`,
|
||||||
password: this.config.mqttPassword,
|
password: this.config.mqttPassword,
|
||||||
protocolVersion: 4, // v5 not supported by HassIO Mosquitto
|
properties: {
|
||||||
|
sessionExpiryInterval: 120, // 2 minutes sessions for the QoS, after that old messages are discarded
|
||||||
|
},
|
||||||
|
protocolVersion: 5,
|
||||||
queueQoSZero: true,
|
queueQoSZero: true,
|
||||||
username: this.config.mqttUsername,
|
username: this.config.mqttUsername,
|
||||||
});
|
});
|
||||||
|
|
@ -166,28 +180,25 @@ class MqttClient extends EventEmitter {
|
||||||
this.client.on('connect', () => {
|
this.client.on('connect', () => {
|
||||||
logger.info('Connected to MQTT.');
|
logger.info('Connected to MQTT.');
|
||||||
|
|
||||||
this.client.subscribe(
|
this.emit(MqttClient.EVENTS.connected);
|
||||||
startTopics,
|
|
||||||
// Add below when mqtt v5 is supported in Mosquitto 1.6 or 2.0 and forward
|
|
||||||
// {
|
|
||||||
// qos: 1,
|
|
||||||
// nl: true, // don't echo back messages sent
|
|
||||||
// rap: true, // retain as published - don't force retain = 0
|
|
||||||
// },
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error('Unable to subscribe to status topics', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.client.subscribe(getSubscribePath(), (err) => {
|
// this.emit(MqttClient.EVENTS.connected);
|
||||||
if (err) {
|
// },
|
||||||
logger.error('Unable to subscribe to control topics');
|
// );
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('close', () => {
|
this.client.on('close', () => {
|
||||||
|
|
@ -197,64 +208,60 @@ class MqttClient extends EventEmitter {
|
||||||
|
|
||||||
this.client.on('message', (topic, message) => {
|
this.client.on('message', (topic, message) => {
|
||||||
try {
|
try {
|
||||||
if (startTopics.includes(topic)) {
|
logger.verbose(`Received mqtt message on ${topic}`);
|
||||||
logger.info('Home Assistant has started. lets do discovery.');
|
const decodedTopic = decodeTopic(topic);
|
||||||
this.emit(MqttClient.EVENTS.connected);
|
if (decodedTopic) {
|
||||||
} else {
|
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||||
logger.verbose(`Received mqtt message on ${topic}`);
|
let device;
|
||||||
const decodedTopic = decodeTopic(topic);
|
|
||||||
if (decodedTopic) {
|
|
||||||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
|
||||||
let device;
|
|
||||||
|
|
||||||
if (decodedTopic.type === MQTT_TYPES.SCENE) {
|
if (decodedTopic.type === MQTT_TYPES.SCENE) {
|
||||||
logger.verbose(`Getting scene ${decodedTopic.id} from registry`);
|
logger.verbose(`Getting scene ${decodedTopic.id} from registry`);
|
||||||
device = this.deviceRegistry.getScene(decodedTopic.id);
|
device = this.deviceRegistry.getScene(decodedTopic.id);
|
||||||
} else {
|
|
||||||
logger.verbose(`Getting device ${decodedTopic.id} from registry`);
|
|
||||||
device = this.deviceRegistry.getOutputDevice(decodedTopic.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageString = message.toString();
|
|
||||||
const isJsonMessage = messageString.startsWith('{');
|
|
||||||
const command = isJsonMessage ? JSON.parse(messageString) : messageString;
|
|
||||||
|
|
||||||
const deviceName = device ? device.name : '';
|
|
||||||
|
|
||||||
switch (decodedTopic.command) {
|
|
||||||
case 'set':
|
|
||||||
logger.verbose(
|
|
||||||
`Got mqtt SET command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}): ${messageString}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (device) {
|
|
||||||
this.emit(MqttClient.EVENTS.stateChanged, device, command);
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
`Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'state':
|
|
||||||
case 'config':
|
|
||||||
case 'availability':
|
|
||||||
logger.verbose(
|
|
||||||
`Sent mqtt ${decodedTopic.command} command for ${
|
|
||||||
decodedTopic.type
|
|
||||||
}, ${deviceName} (${decodedTopic.id}). ${
|
|
||||||
decodedTopic.command === 'availability' ? messageString : ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.verbose(
|
logger.verbose(`Getting device ${decodedTopic.id} from registry`);
|
||||||
`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`,
|
device = this.deviceRegistry.getOutputDevice(decodedTopic.id);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageString = message.toString();
|
||||||
|
const isJsonMessage = messageString.startsWith('{');
|
||||||
|
const command = isJsonMessage ? JSON.parse(messageString) : messageString;
|
||||||
|
|
||||||
|
const deviceName = device ? device.name : '';
|
||||||
|
|
||||||
|
switch (decodedTopic.command) {
|
||||||
|
case 'set':
|
||||||
|
logger.verbose(
|
||||||
|
`Got mqtt SET command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}): ${messageString}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (device) {
|
||||||
|
this.emit(MqttClient.EVENTS.stateChanged, device, command);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'state':
|
||||||
|
case 'config':
|
||||||
|
case 'availability':
|
||||||
|
logger.verbose(
|
||||||
|
`Sent mqtt ${decodedTopic.command} command for ${
|
||||||
|
decodedTopic.type
|
||||||
|
}, ${deviceName} (${decodedTopic.id}). ${
|
||||||
|
decodedTopic.command === 'availability' ? messageString : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.verbose(
|
||||||
|
`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Error processing mqtt message on topic ${topic}`, err);
|
logger.error(`Error processing mqtt message on topic ${topic}`, err);
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +282,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'),
|
||||||
AVAILABLILITY.OFFLINE,
|
AVAILABLITY.OFFLINE,
|
||||||
{
|
{
|
||||||
retain: true,
|
retain: true,
|
||||||
qos: 1,
|
qos: 1,
|
||||||
|
|
@ -287,7 +294,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),
|
||||||
AVAILABLILITY.OFFLINE,
|
AVAILABLITY.OFFLINE,
|
||||||
{
|
{
|
||||||
retain: true,
|
retain: true,
|
||||||
qos: 1,
|
qos: 1,
|
||||||
|
|
@ -298,37 +305,67 @@ class MqttClient extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
sendDiscoveryToHomeAssistant() {
|
sendDiscoveryToHomeAssistant() {
|
||||||
|
// -------- DISCOVERY FOR OUTPUT DEVICES -------------
|
||||||
|
|
||||||
const allOutputDevices = this.deviceRegistry.getAllOutputDevices();
|
const allOutputDevices = this.deviceRegistry.getAllOutputDevices();
|
||||||
logger.info(`Sending discovery for ${allOutputDevices.length} Plejd output devices`);
|
logger.info(`Sending discovery for ${allOutputDevices.length} Plejd output devices`);
|
||||||
allOutputDevices.forEach((outputDevice) => {
|
allOutputDevices.forEach((outputDevice) => {
|
||||||
logger.debug(`Sending discovery for ${outputDevice.name}`);
|
logger.debug(`Sending discovery for ${outputDevice.name}`);
|
||||||
|
|
||||||
const configPayload = getOutputDeviceDiscoveryPayload(outputDevice);
|
const configPayload = getOutputDeviceDiscoveryPayload(outputDevice);
|
||||||
logger.info(
|
// Publish mqtt CONFIG message which will create the device in Home Assistant
|
||||||
`Discovered ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`,
|
|
||||||
);
|
|
||||||
|
|
||||||
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, TOPIC_TYPES.CONFIG),
|
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.CONFIG),
|
||||||
JSON.stringify(configPayload),
|
JSON.stringify(configPayload),
|
||||||
{
|
{
|
||||||
retain: true,
|
retain: true, // Discovery messages should be retained to account for HA and MQTT broker restarts
|
||||||
|
qos: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Sent discovery message for ${outputDevice.typeName} (${outputDevice.type}) named ${outputDevice.name} (${outputDevice.bleOutputAddress} : ${outputDevice.uniqueId}).`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// -------- CLEANUP RETAINED MESSAGES FOR OUTPUT DEVICES -------------
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Forcefully removing any retained SET, STATE, and AVAILABILITY messages for ${outputDevice.name}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Forcefully remove retained (from Home Assistant) SET messages (wanted state from HA)
|
||||||
|
this.client.publish(getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.SET), null, {
|
||||||
|
retain: true, // Retain true to remove previously retained message
|
||||||
|
qos: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forcefully remove retained (from us, v0.11 and before) AVAILABILITY messages
|
||||||
|
this.client.publish(
|
||||||
|
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABLILITY),
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
retain: true, // Retain true to remove previously retained message
|
||||||
|
qos: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(`Removal messages sent for ${outputDevice.name}`);
|
||||||
|
|
||||||
|
logger.debug(`Setting device as AVAILABILITY = ONLINE: ${outputDevice.name}`);
|
||||||
|
|
||||||
|
this.client.publish(
|
||||||
|
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
||||||
|
AVAILABLITY.ONLINE,
|
||||||
|
{
|
||||||
|
retain: false, // Availability messages should NOT be retained
|
||||||
qos: 1,
|
qos: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
|
||||||
this.client.publish(
|
|
||||||
getTopicName(outputDevice.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
|
||||||
AVAILABLILITY.ONLINE,
|
|
||||||
{
|
|
||||||
retain: true,
|
|
||||||
qos: 1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}, 2000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -------- DISCOVERY FOR INPUT DEVICES -------------
|
||||||
|
|
||||||
const allInputDevices = this.deviceRegistry.getAllInputDevices();
|
const allInputDevices = this.deviceRegistry.getAllInputDevices();
|
||||||
logger.info(`Sending discovery for ${allInputDevices.length} Plejd input devices`);
|
logger.info(`Sending discovery for ${allInputDevices.length} Plejd input devices`);
|
||||||
allInputDevices.forEach((inputDevice) => {
|
allInputDevices.forEach((inputDevice) => {
|
||||||
|
|
@ -349,12 +386,14 @@ class MqttClient extends EventEmitter {
|
||||||
getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG),
|
getTopicName(inputDevice.uniqueId, MQTT_TYPES.DEVICE_AUTOMATION, TOPIC_TYPES.CONFIG),
|
||||||
JSON.stringify(inputInputPayload),
|
JSON.stringify(inputInputPayload),
|
||||||
{
|
{
|
||||||
retain: true,
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
||||||
qos: 1,
|
qos: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -------- DISCOVERY FOR SCENE DEVICES -------------
|
||||||
|
|
||||||
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
|
const allSceneDevices = this.deviceRegistry.getAllSceneDevices();
|
||||||
logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`);
|
logger.info(`Sending discovery for ${allSceneDevices.length} Plejd scene devices`);
|
||||||
allSceneDevices.forEach((sceneDevice) => {
|
allSceneDevices.forEach((sceneDevice) => {
|
||||||
|
|
@ -369,7 +408,7 @@ class MqttClient extends EventEmitter {
|
||||||
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.CONFIG),
|
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.CONFIG),
|
||||||
JSON.stringify(sceneConfigPayload),
|
JSON.stringify(sceneConfigPayload),
|
||||||
{
|
{
|
||||||
retain: true,
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
||||||
qos: 1,
|
qos: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -384,22 +423,39 @@ class MqttClient extends EventEmitter {
|
||||||
),
|
),
|
||||||
JSON.stringify(sceneTriggerConfigPayload),
|
JSON.stringify(sceneTriggerConfigPayload),
|
||||||
{
|
{
|
||||||
retain: true,
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
||||||
qos: 1,
|
qos: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
this.client.publish(
|
this.client.publish(
|
||||||
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
getTopicName(sceneDevice.uniqueId, MQTT_TYPES.SCENE, TOPIC_TYPES.AVAILABILITY),
|
||||||
AVAILABLILITY.ONLINE,
|
AVAILABLITY.ONLINE,
|
||||||
{
|
{
|
||||||
retain: true,
|
retain: true, // Discovery messages should be retained to account for HA restarts
|
||||||
qos: 1,
|
qos: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}, 2000);
|
// }, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -------- SUBSCRIBE TO INCOMING MESSAGES -------------
|
||||||
|
|
||||||
|
this.client.subscribe(
|
||||||
|
getSubscribePath(),
|
||||||
|
{
|
||||||
|
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 control topics');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -440,13 +496,13 @@ class MqttClient extends EventEmitter {
|
||||||
|
|
||||||
const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
const mqttType = device.type === 'switch' ? MQTT_TYPES.SWITCH : MQTT_TYPES.LIGHT;
|
||||||
this.client.publish(getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.STATE), payload, {
|
this.client.publish(getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.STATE), payload, {
|
||||||
retain: true,
|
retain: false,
|
||||||
qos: 1,
|
qos: 1,
|
||||||
});
|
});
|
||||||
// this.client.publish(
|
// this.client.publish(
|
||||||
// getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
// getTopicName(device.uniqueId, mqttType, TOPIC_TYPES.AVAILABILITY),
|
||||||
// AVAILABLILITY.ONLINE,
|
// AVAILABILITY.ONLINE,
|
||||||
// { retain: true, qos: 1 },
|
// { retain: false, qos: 1 },
|
||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,7 +512,10 @@ class MqttClient extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
buttonPressed(deviceId, deviceInput) {
|
buttonPressed(deviceId, deviceInput) {
|
||||||
logger.verbose(`Button ${deviceInput} pressed for deviceId ${deviceId}`);
|
logger.verbose(`Button ${deviceInput} pressed for deviceId ${deviceId}`);
|
||||||
this.client.publish(getButtonEventTopic(deviceId), `${deviceInput}`, { qos: 1 });
|
this.client.publish(getButtonEventTopic(deviceId), `${deviceInput}`, {
|
||||||
|
retain: false,
|
||||||
|
qos: 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -464,7 +523,10 @@ class MqttClient extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
sceneTriggered(sceneId) {
|
sceneTriggered(sceneId) {
|
||||||
logger.verbose(`Scene triggered: ${sceneId}`);
|
logger.verbose(`Scene triggered: ${sceneId}`);
|
||||||
this.client.publish(getSceneEventTopic(sceneId), '', { qos: 1 });
|
this.client.publish(getSceneEventTopic(sceneId), '', {
|
||||||
|
qos: 1,
|
||||||
|
retain: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ const API_SITE_LIST_URL = 'functions/getSiteList';
|
||||||
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
||||||
|
|
||||||
const TRAITS = {
|
const TRAITS = {
|
||||||
NO_LOAD: 0,
|
NO_LOAD: 0, // 0b0000
|
||||||
NON_DIMMABLE: 9,
|
NON_DIMMABLE: 9, // 0b1001
|
||||||
DIMMABLE: 11,
|
DIMMABLE: 11, // 0b1011
|
||||||
|
DIMMABLE_COLORTEMP: 15, // 0b1111
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = Logger.getLogger('plejd-api');
|
const logger = Logger.getLogger('plejd-api');
|
||||||
|
|
@ -338,8 +339,10 @@ class PlejdApi {
|
||||||
description: 'Dali broadcast with dimmer and tuneable white support',
|
description: 'Dali broadcast with dimmer and tuneable white support',
|
||||||
type: 'light',
|
type: 'light',
|
||||||
dimmable: true,
|
dimmable: true,
|
||||||
|
colorTemp: true,
|
||||||
broadcastClicks: false,
|
broadcastClicks: false,
|
||||||
};
|
};
|
||||||
|
// 13: Non-dimmable generic light
|
||||||
case 14:
|
case 14:
|
||||||
return {
|
return {
|
||||||
name: 'DIM-01',
|
name: 'DIM-01',
|
||||||
|
|
@ -395,6 +398,7 @@ class PlejdApi {
|
||||||
description: '1-channel LED dimmer/driver with tuneable white, 10 W',
|
description: '1-channel LED dimmer/driver with tuneable white, 10 W',
|
||||||
type: 'light',
|
type: 'light',
|
||||||
dimmable: true,
|
dimmable: true,
|
||||||
|
colorTemp: true,
|
||||||
broadcastClicks: false,
|
broadcastClicks: false,
|
||||||
};
|
};
|
||||||
case 167:
|
case 167:
|
||||||
|
|
@ -403,6 +407,7 @@ class PlejdApi {
|
||||||
description: 'Smart tunable downlight with a built-in dimmer function, 8W',
|
description: 'Smart tunable downlight with a built-in dimmer function, 8W',
|
||||||
type: 'light',
|
type: 'light',
|
||||||
dimmable: true,
|
dimmable: true,
|
||||||
|
colorTemp: true,
|
||||||
broadcastClicks: false,
|
broadcastClicks: false,
|
||||||
};
|
};
|
||||||
case 199:
|
case 199:
|
||||||
|
|
@ -411,6 +416,7 @@ class PlejdApi {
|
||||||
description: 'Smart tunable downlight with a built-in dimmer function, 8W',
|
description: 'Smart tunable downlight with a built-in dimmer function, 8W',
|
||||||
type: 'light',
|
type: 'light',
|
||||||
dimmable: true,
|
dimmable: true,
|
||||||
|
colorTemp: true,
|
||||||
broadcastClicks: false,
|
broadcastClicks: false,
|
||||||
};
|
};
|
||||||
// PLEASE CREATE AN ISSUE WITH THE HARDWARE ID if you own one of these devices!
|
// PLEASE CREATE AN ISSUE WITH THE HARDWARE ID if you own one of these devices!
|
||||||
|
|
@ -480,8 +486,18 @@ class PlejdApi {
|
||||||
(x) => x.deviceId === device.deviceId,
|
(x) => x.deviceId === device.deviceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dimmable = device.traits === TRAITS.DIMMABLE;
|
const dimmable =
|
||||||
// dimmable = settings.dimCurve !== 'NonDimmable';
|
device.traits === TRAITS.DIMMABLE || device.traits === TRAITS.DIMMABLE_COLORTEMP;
|
||||||
|
|
||||||
|
// Alternate approach looks at outputSettings.dimCurve and outputSettings.predefinedLoad
|
||||||
|
// 1. outputSettings.dimCurve === null: Not dimmable
|
||||||
|
// 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 {
|
try {
|
||||||
const decodedDeviceType = this._getDeviceType(plejdDevice);
|
const decodedDeviceType = this._getDeviceType(plejdDevice);
|
||||||
|
|
@ -499,6 +515,8 @@ class PlejdApi {
|
||||||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||||
const outputDevice = {
|
const outputDevice = {
|
||||||
bleOutputAddress,
|
bleOutputAddress,
|
||||||
|
colorTemp,
|
||||||
|
colorTempSettings: outputSettings ? outputSettings.colorTemperature : null,
|
||||||
deviceId: device.deviceId,
|
deviceId: device.deviceId,
|
||||||
dimmable,
|
dimmable,
|
||||||
name: device.title,
|
name: device.title,
|
||||||
|
|
@ -604,6 +622,7 @@ class PlejdApi {
|
||||||
const newDevice = {
|
const newDevice = {
|
||||||
bleOutputAddress: roomAddress,
|
bleOutputAddress: roomAddress,
|
||||||
deviceId: null,
|
deviceId: null,
|
||||||
|
colorTemp: false,
|
||||||
dimmable,
|
dimmable,
|
||||||
name: room.title,
|
name: room.title,
|
||||||
output: undefined,
|
output: undefined,
|
||||||
|
|
@ -633,6 +652,7 @@ class PlejdApi {
|
||||||
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
/** @type {import('types/DeviceRegistry').OutputDevice} */
|
||||||
const newScene = {
|
const newScene = {
|
||||||
bleOutputAddress: sceneNum,
|
bleOutputAddress: sceneNum,
|
||||||
|
colorTemp: false,
|
||||||
deviceId: undefined,
|
deviceId: undefined,
|
||||||
dimmable: false,
|
dimmable: false,
|
||||||
name: scene.title,
|
name: scene.title,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,10 @@ 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 delay = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout));
|
const delay = (timeout) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, timeout);
|
||||||
|
});
|
||||||
|
|
||||||
class PlejBLEHandler extends EventEmitter {
|
class PlejBLEHandler extends EventEmitter {
|
||||||
adapter;
|
adapter;
|
||||||
|
|
@ -303,11 +306,14 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
|
|
||||||
// After we've authenticated, we need to hook up the event listener
|
// After we've authenticated, we need to hook up the event listener
|
||||||
// for changes to lastData.
|
// for changes to lastData.
|
||||||
this.characteristics.lastDataProperties.on('PropertiesChanged', (
|
this.characteristics.lastDataProperties.on(
|
||||||
iface,
|
'PropertiesChanged',
|
||||||
properties,
|
(
|
||||||
// invalidated (third param),
|
iface,
|
||||||
) => this._onLastDataUpdated(iface, properties));
|
properties,
|
||||||
|
// invalidated (third param),
|
||||||
|
) => this._onLastDataUpdated(iface, properties),
|
||||||
|
);
|
||||||
this.characteristics.lastData.StartNotify();
|
this.characteristics.lastData.StartNotify();
|
||||||
this.consecutiveReconnectAttempts = 0;
|
this.consecutiveReconnectAttempts = 0;
|
||||||
this.emit(PlejBLEHandler.EVENTS.connected);
|
this.emit(PlejBLEHandler.EVENTS.connected);
|
||||||
|
|
@ -657,7 +663,7 @@ class PlejBLEHandler extends EventEmitter {
|
||||||
logger.info('Requesting current Plejd time...');
|
logger.info('Requesting current Plejd time...');
|
||||||
|
|
||||||
const payload = this._createHexPayload(
|
const payload = this._createHexPayload(
|
||||||
this.connectedDevice.id,
|
this.connectedDeviceId,
|
||||||
BLE_CMD_TIME_UPDATE,
|
BLE_CMD_TIME_UPDATE,
|
||||||
'',
|
'',
|
||||||
BLE_REQUEST_RESPONSE,
|
BLE_REQUEST_RESPONSE,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ The add-on has been tested on the following platforms:
|
||||||
- Windows 10 host / Oracle Virtualbox 6.1 / Home Assistant VBox image / ASUS BT400
|
- Windows 10 host / Oracle Virtualbox 6.1 / Home Assistant VBox image / ASUS BT400
|
||||||
- Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
- Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
||||||
- Home Assistant Yellow with RPI 4 compute module / Built-in BT
|
- Home Assistant Yellow with RPI 4 compute module / Built-in BT
|
||||||
|
- HP EliteDesk 800 G3 DM / Proxmox 7.5-3 / HAOS / Deltaco BT-118 with Cambridge Silicon Radio chipset
|
||||||
|
|
||||||
Supported Plejd devices are detailed in a specific "Plejd devices" section below.
|
Supported Plejd devices are detailed in a specific "Plejd devices" section below.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abandonware/bluetooth-hci-socket": "~0.5.3-7",
|
"@abandonware/bluetooth-hci-socket": "~0.5.3-10",
|
||||||
"axios": "~0.21.1",
|
"axios": "~1.6.1",
|
||||||
"buffer-xor": "~2.0.2",
|
"buffer-xor": "~2.0.2",
|
||||||
"dbus-next": "~0.9.2",
|
"dbus-next": "~0.10.2",
|
||||||
"fs": "0.0.1-security",
|
"fs": "0.0.1-security",
|
||||||
"jspack": "~0.0.4",
|
"jspack": "~0.0.4",
|
||||||
"mqtt": "~4.2.6",
|
"mqtt": "~5.1.2",
|
||||||
"winston": "~3.3.3"
|
"winston": "~3.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "~10.1.0",
|
"@babel/eslint-parser": "~7.23.3",
|
||||||
"eslint": "~7.23.0",
|
"eslint": "~8.53.0",
|
||||||
"eslint-config-airbnb": "~18.2.1",
|
"eslint-config-airbnb": "~19.0.4",
|
||||||
"eslint-config-prettier": "~8.1.0",
|
"eslint-config-prettier": "~9.0.0",
|
||||||
"eslint-plugin-import": "~2.22.1",
|
"eslint-plugin-import": "~2.29.0",
|
||||||
"eslint-plugin-prettier": "~3.3.1",
|
"eslint-plugin-prettier": "~5.0.1",
|
||||||
"prettier": "~2.2.1"
|
"prettier": "~3.0.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "npm run lint:prettier & npm run lint:scripts",
|
"lint": "npm run lint:prettier & npm run lint:scripts",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|
||||||
bashio::log.info 'Starting the Plejd service...'
|
bashio::log.info 'Starting the Plejd service...'
|
||||||
|
bashio::log.info 'Docker env updated 2023-10-17...'
|
||||||
|
|
||||||
# Change working directory
|
# Change working directory
|
||||||
cd /plejd || bashio::exit.nok 'Unable to change working directory'
|
cd /plejd || bashio::exit.nok 'Unable to change working directory'
|
||||||
|
|
|
||||||
11
plejd/types/ApiSite.d.ts
vendored
11
plejd/types/ApiSite.d.ts
vendored
|
|
@ -260,6 +260,7 @@ export interface OutputSetting {
|
||||||
deviceParseId: string;
|
deviceParseId: string;
|
||||||
siteId: string;
|
siteId: string;
|
||||||
predefinedLoad: OutputSettingPredefinedLoad;
|
predefinedLoad: OutputSettingPredefinedLoad;
|
||||||
|
colorTemperature: OutputSettingColorTemperature;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
dimMin: number;
|
dimMin: number;
|
||||||
|
|
@ -322,6 +323,16 @@ export interface OutputSettingPredefinedLoad {
|
||||||
filters?: Filters;
|
filters?: Filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OutputSettingColorTemperature {
|
||||||
|
"minTemperature": number,
|
||||||
|
"maxTemperature": number,
|
||||||
|
"slewRate": number,
|
||||||
|
"minTemperatureLimit": number,
|
||||||
|
"maxTemperatureLimit": number,
|
||||||
|
"behavior": "adjustable" | "UNKNOWN", // Todo: Fill with alternate values after finding more site jsons. UNKNOWN is placeholder for now.
|
||||||
|
"startTemperature": number
|
||||||
|
}
|
||||||
|
|
||||||
export interface PredefinedLoadACL {
|
export interface PredefinedLoadACL {
|
||||||
'*': Empty;
|
'*': Empty;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
plejd/types/DeviceRegistry.d.ts
vendored
4
plejd/types/DeviceRegistry.d.ts
vendored
|
|
@ -1,9 +1,13 @@
|
||||||
/* eslint-disable no-use-before-define */
|
/* eslint-disable no-use-before-define */
|
||||||
|
|
||||||
|
import { OutputSettingColorTemperature } from "./ApiSite";
|
||||||
|
|
||||||
export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice };
|
export type OutputDevices = { [deviceIdAndOutput: string]: OutputDevice };
|
||||||
|
|
||||||
export interface OutputDevice {
|
export interface OutputDevice {
|
||||||
bleOutputAddress: number;
|
bleOutputAddress: number;
|
||||||
|
colorTemp: boolean;
|
||||||
|
colorTempSettings?: OutputSettingColorTemperature
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
dim?: number;
|
dim?: number;
|
||||||
dimmable: boolean;
|
dimmable: boolean;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue